Skip to content

Commit e29c3a0

Browse files
authored
fix(ui): scope project session list requests (#565)
## Summary - update @opencode-ai/sdk to 1.17.8 - load session lists through client.session.list with scope=project - use a single high-limit project request because this endpoint does not support cursor pagination ## Validation - npm run typecheck --workspace packages/ui ## Notes - Existing untracked .opencode/package-lock.json was left untouched.
1 parent fdc0c92 commit e29c3a0

8 files changed

Lines changed: 82 additions & 84 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ui/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"dependencies": {
1414
"@git-diff-view/solid": "^0.0.8",
1515
"@kobalte/core": "0.13.11",
16-
"@opencode-ai/sdk": "1.16.0",
16+
"@opencode-ai/sdk": "^1.17.8",
1717
"@solidjs/router": "^0.13.0",
1818
"@suid/icons-material": "^0.9.0",
1919
"@suid/material": "^0.19.0",

packages/ui/src/App.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ import {
4747
clearActiveParentSession,
4848
createSession,
4949
fetchSessions,
50-
getSessionFetchLimit,
5150
updateSessionAgent,
5251
updateSessionModel,
5352
} from "./stores/sessions"
@@ -419,7 +418,7 @@ const App: Component = () => {
419418
clearActiveParentSession(instanceId)
420419

421420
try {
422-
await fetchSessions(instanceId, { reset: true, limit: getSessionFetchLimit(instanceId) })
421+
await fetchSessions(instanceId, { reset: true })
423422
} catch (error) {
424423
log.error("Failed to refresh sessions after closing", error)
425424
}

packages/ui/src/stores/session-api.ts

Lines changed: 36 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
type SessionStatus,
88
} from "../types/session"
99
import type { Message } from "../types/message"
10-
import type { SessionV2Info, V2SessionsResponse } from "@opencode-ai/sdk/v2/client"
10+
import type { Session as SDKSession, SessionListResponse } from "@opencode-ai/sdk/v2/client"
1111

1212
import { instances } from "./instances"
1313
import { preferences, setAgentModelPreference } from "./preferences"
@@ -36,8 +36,6 @@ import {
3636
cleanupBlankSessions,
3737
syncInstanceSessionIndicator,
3838
updateThreadTotalsForParent,
39-
SESSION_PAGE_SIZE,
40-
getSessionNextCursor,
4139
setSessionPage,
4240
prependSessionListId,
4341
removeSessionListId,
@@ -58,6 +56,7 @@ import { getRootClient } from "./opencode-client"
5856
import { getWorktreeSlugForSession, migrateLegacyWorktreeMapToSessionMetadata, pruneStaleLegacyWorktreeMapEntries, removeLegacyParentSessionMapping, setWorktreeSlugForParentSession } from "./worktrees"
5957
import { getOpenCodeWorkspaceIdForSession } from "./opencode-workspaces"
6058
import { hydrateSessionMetadataWithClient } from "./session-metadata"
59+
import { PROJECT_SESSION_LIST_LIMIT, buildProjectSessionListOptions } from "./session-list-options"
6160

6261
const log = getLogger("api")
6362

@@ -111,17 +110,19 @@ interface SessionForkResponse {
111110

112111
type V2SessionListOptions = {
113112
directory?: string
114-
limit?: number
115113
search?: string
116-
cursor?: string
117114
}
118115

119-
function getKnownParentId(session: SessionV2Info | Session): string | null | undefined {
116+
type ProjectSessionListResponse = {
117+
data: SDKSession[]
118+
}
119+
120+
function getKnownParentId(session: SDKSession | Session): string | null | undefined {
120121
return (session as any).parentID ?? (session as Session).parentId
121122
}
122123

123-
function hasMissingParentChain(session: SessionV2Info, loaded: Map<string, SessionV2Info | Session>): boolean {
124-
let current: SessionV2Info | Session = session
124+
function hasMissingParentChain(session: SDKSession, loaded: Map<string, SDKSession | Session>): boolean {
125+
let current: SDKSession | Session = session
125126
const seen = new Set<string>()
126127

127128
while (getKnownParentId(current)) {
@@ -137,20 +138,17 @@ function hasMissingParentChain(session: SessionV2Info, loaded: Map<string, Sessi
137138
return false
138139
}
139140

140-
async function fetchV2Sessions(instanceId: string, options: V2SessionListOptions): Promise<V2SessionsResponse> {
141+
async function fetchV2Sessions(instanceId: string, options: V2SessionListOptions): Promise<ProjectSessionListResponse> {
141142
const client = getRootClient(instanceId)
142-
return requestData<V2SessionsResponse>(client.v2.session.list(options), "v2.session.list")
143+
const listOptions = buildProjectSessionListOptions(options)
144+
const data = await requestData<SessionListResponse>(client.session.list(listOptions), "session.list")
145+
return { data }
143146
}
144147

145-
function getV2SessionItems(response: V2SessionsResponse): SessionV2Info[] {
148+
function getV2SessionItems(response: ProjectSessionListResponse): SDKSession[] {
146149
return response.data
147150
}
148151

149-
function getV2NextCursor(response: V2SessionsResponse): string | undefined {
150-
const next = (response as any)?.cursor?.next
151-
return typeof next === "string" && next.length > 0 ? next : undefined
152-
}
153-
154152
async function hydrateMissingSessionMetadata(instanceId: string, sessionIds: string[]): Promise<void> {
155153
const uniqueIds = Array.from(new Set(sessionIds)).filter(Boolean)
156154
if (uniqueIds.length === 0) return
@@ -167,43 +165,33 @@ async function hydrateMissingSessionMetadata(instanceId: string, sessionIds: str
167165
}
168166
}
169167

170-
async function ensureV2ParentChainsLoaded(instanceId: string, apiSessions: SessionV2Info[], directory?: string): Promise<void> {
168+
async function ensureV2ParentChainsLoaded(instanceId: string, apiSessions: SDKSession[], directory?: string): Promise<void> {
171169
const currentSessions = sessions().get(instanceId) ?? new Map<string, Session>()
172-
const loaded = new Map<string, SessionV2Info | Session>(currentSessions)
170+
const loaded = new Map<string, SDKSession | Session>(currentSessions)
173171
for (const session of apiSessions) loaded.set(session.id, session)
174172

175173
if (!apiSessions.some((session) => hasMissingParentChain(session, loaded))) return
176174

177-
const limit = SESSION_PAGE_SIZE
178-
let cursor: string | undefined
179-
let remainingPages = 25
180-
181-
while (apiSessions.some((session) => hasMissingParentChain(session, loaded)) && remainingPages > 0) {
182-
const page = await fetchV2Sessions(instanceId, { directory, limit, ...(cursor ? { cursor } : {}) })
183-
const items = getV2SessionItems(page)
184-
if (items.length === 0) break
185-
186-
setSessions((prev) => {
187-
const next = new Map(prev)
188-
const instanceSessions = new Map(next.get(instanceId) ?? new Map())
175+
const page = await fetchV2Sessions(instanceId, { directory })
176+
const items = getV2SessionItems(page)
177+
if (items.length === 0) return
189178

190-
for (const apiSession of items) {
191-
const existingSession = instanceSessions.get(apiSession.id)
192-
instanceSessions.set(apiSession.id, toClientSessionV2(instanceId, apiSession, existingSession))
193-
loaded.set(apiSession.id, apiSession)
194-
}
179+
setSessions((prev) => {
180+
const next = new Map(prev)
181+
const instanceSessions = new Map(next.get(instanceId) ?? new Map())
195182

196-
next.set(instanceId, instanceSessions)
197-
return next
198-
})
183+
for (const apiSession of items) {
184+
const existingSession = instanceSessions.get(apiSession.id)
185+
instanceSessions.set(apiSession.id, toClientSessionV2(instanceId, apiSession, existingSession))
186+
loaded.set(apiSession.id, apiSession)
187+
}
199188

200-
cursor = getV2NextCursor(page)
201-
if (!cursor) break
202-
remainingPages -= 1
203-
}
189+
next.set(instanceId, instanceSessions)
190+
return next
191+
})
204192
}
205193

206-
async function fetchSessions(instanceId: string, options?: { limit?: number; reset?: boolean; cursor?: string }): Promise<void> {
194+
async function fetchSessions(instanceId: string, options?: { reset?: boolean }): Promise<void> {
207195
const instance = instances().get(instanceId)
208196
if (!instance || !instance.client) {
209197
throw new Error("Instance not ready")
@@ -218,17 +206,10 @@ async function fetchSessions(instanceId: string, options?: { limit?: number; res
218206
})
219207

220208
try {
221-
const limit = Math.min(options?.limit ?? SESSION_PAGE_SIZE, 200)
222-
223-
const sessionListOptions: { directory?: string; limit?: number; cursor?: string } = {
224-
limit,
225-
...(instance.folder ? { directory: instance.folder } : {}),
226-
...(options?.cursor ? { cursor: options.cursor } : {}),
227-
}
209+
const sessionListOptions = instance.folder ? { directory: instance.folder } : {}
228210

229-
log.info("v2.session.list", { instanceId, limit, directory: sessionListOptions.directory, cursor: sessionListOptions.cursor })
211+
log.info("session.list", { instanceId, limit: PROJECT_SESSION_LIST_LIMIT, directory: sessionListOptions.directory, scope: "project" })
230212
const response = await fetchV2Sessions(instanceId, sessionListOptions)
231-
const nextCursor = getV2NextCursor(response)
232213

233214
let statusById: Record<string, any> = {}
234215
try {
@@ -298,7 +279,7 @@ async function fetchSessions(instanceId: string, options?: { limit?: number; res
298279
})
299280
}
300281

301-
setSessionPage(instanceId, rootIds, Boolean(nextCursor), options?.reset ?? true, nextCursor)
282+
setSessionPage(instanceId, rootIds, false, options?.reset ?? true)
302283

303284
syncInstanceSessionIndicator(instanceId)
304285

@@ -339,9 +320,7 @@ async function fetchSessions(instanceId: string, options?: { limit?: number; res
339320
}
340321

341322
async function loadMoreSessions(instanceId: string): Promise<void> {
342-
const cursor = getSessionNextCursor(instanceId)
343-
if (!cursor) return
344-
await fetchSessions(instanceId, { limit: SESSION_PAGE_SIZE, reset: false, cursor })
323+
return
345324
}
346325

347326
async function searchSessions(instanceId: string, query: string): Promise<void> {
@@ -359,7 +338,6 @@ async function searchSessions(instanceId: string, query: string): Promise<void>
359338
log.info("v2.session.search", { instanceId, query: trimmedQuery, directory: instance.folder })
360339
const response = await fetchV2Sessions(instanceId, {
361340
search: trimmedQuery,
362-
limit: SESSION_PAGE_SIZE,
363341
directory: instance.folder,
364342
})
365343
if (!isLatestSessionSearch(instanceId, trimmedQuery, requestId)) return
@@ -410,7 +388,7 @@ async function searchSessions(instanceId: string, query: string): Promise<void>
410388
}
411389
}
412390

413-
function toClientSessionV2(instanceId: string, apiSession: SessionV2Info, existingSession?: Session): Session {
391+
function toClientSessionV2(instanceId: string, apiSession: SDKSession, existingSession?: Session): Session {
414392
return {
415393
id: apiSession.id,
416394
instanceId,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export const PROJECT_SESSION_LIST_LIMIT = 1000
2+
3+
type ProjectSessionListInput = {
4+
directory?: string
5+
search?: string
6+
}
7+
8+
export type ProjectSessionListOptions = ProjectSessionListInput & {
9+
limit: typeof PROJECT_SESSION_LIST_LIMIT
10+
scope: "project"
11+
}
12+
13+
export function buildProjectSessionListOptions(options: ProjectSessionListInput): ProjectSessionListOptions {
14+
return {
15+
...(options.directory ? { directory: options.directory } : {}),
16+
...(options.search ? { search: options.search } : {}),
17+
limit: PROJECT_SESSION_LIST_LIMIT,
18+
scope: "project",
19+
}
20+
}

packages/ui/src/stores/session-pagination.test.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,33 @@ import assert from "node:assert/strict"
22
import { describe, it } from "node:test"
33

44
import { applySessionPage, getDefaultSessionPaginationState } from "./session-pagination-model.ts"
5+
import { PROJECT_SESSION_LIST_LIMIT, buildProjectSessionListOptions } from "./session-list-options.ts"
56

6-
describe("session pagination cursor state", () => {
7-
it("stores the v2 next cursor and appends loaded pages", () => {
8-
const firstPage = applySessionPage(getDefaultSessionPaginationState(), ["root-1", "root-2"], true, true, "cursor-page-2")
7+
describe("project session list loading", () => {
8+
it("builds a one-shot project-scoped request without pagination params", () => {
9+
const options = buildProjectSessionListOptions({ directory: "/tmp/project", search: "worktree" })
910

10-
assert.deepEqual(firstPage.ids, ["root-1", "root-2"])
11-
assert.equal(firstPage.hasMore, true)
12-
assert.equal(firstPage.nextCursor, "cursor-page-2")
11+
assert.deepEqual(options, {
12+
directory: "/tmp/project",
13+
search: "worktree",
14+
limit: PROJECT_SESSION_LIST_LIMIT,
15+
scope: "project",
16+
})
17+
assert.equal("start" in options, false)
18+
assert.equal("cursor" in options, false)
19+
})
1320

14-
const secondPage = applySessionPage(firstPage, ["root-2", "root-3"], false, false, undefined)
21+
it("marks the loaded session list complete because the API does not paginate", () => {
22+
const state = applySessionPage(getDefaultSessionPaginationState(), ["root-1", "root-2"], false, true)
1523

16-
assert.deepEqual(secondPage.ids, ["root-1", "root-2", "root-3"])
17-
assert.equal(secondPage.hasMore, false)
18-
assert.equal(secondPage.nextCursor, undefined)
24+
assert.deepEqual(state.ids, ["root-1", "root-2"])
25+
assert.equal(state.hasMore, false)
26+
assert.equal(state.nextCursor, undefined)
1927
})
2028

21-
it("resets ids and cursor when a fresh first page is loaded", () => {
29+
it("resets stale cursor state when the one-shot list refreshes", () => {
2230
const previous = applySessionPage(getDefaultSessionPaginationState(), ["old-root"], true, true, "old-cursor")
23-
const next = applySessionPage(previous, ["new-root"], false, true, undefined)
31+
const next = applySessionPage(previous, ["new-root"], false, true)
2432

2533
assert.deepEqual(next.ids, ["new-root"])
2634
assert.equal(next.hasMore, false)

packages/ui/src/stores/session-state.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,6 @@ function getSessionListIds(instanceId: string): string[] {
8585
return getSessionPaginationState(instanceId).ids
8686
}
8787

88-
function getSessionFetchLimit(instanceId: string): number {
89-
return Math.max(getSessionPaginationState(instanceId).ids.length, SESSION_PAGE_SIZE)
90-
}
91-
9288
function getSessionNextCursor(instanceId: string): string | undefined {
9389
return getSessionPaginationState(instanceId).nextCursor
9490
}
@@ -1070,7 +1066,6 @@ export {
10701066
sessionPagination,
10711067
sessionSearch,
10721068
getSessionListIds,
1073-
getSessionFetchLimit,
10741069
getSessionNextCursor,
10751070
setSessionPage,
10761071
getSessionHasMore,

packages/ui/src/stores/sessions.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ import {
4343
setSessionStatus,
4444
toggleSessionParentExpanded,
4545
clearSessionSearch,
46-
getSessionFetchLimit,
4746
getSessionHasMore,
4847
isSessionSearchLoading,
4948
resetSessionPagination,
@@ -161,7 +160,6 @@ export {
161160
updateSessionAgent,
162161
updateSessionModel,
163162
clearSessionSearch,
164-
getSessionFetchLimit,
165163
getSessionHasMore,
166164
isSessionSearchLoading,
167165
resetSessionPagination,

0 commit comments

Comments
 (0)