Skip to content

Commit 9479e1f

Browse files
fix(frontend): adapt session list to OpenCode v1.16.0 API response changes (#252)
* fix(frontend): adapt session list to OpenCode v1.16.0 API response changes * fix: support both old and new /api/session response formats
1 parent ce4fd6e commit 9479e1f

2 files changed

Lines changed: 85 additions & 4 deletions

File tree

frontend/src/api/opencode.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,45 @@ describe('OpenCodeClient', () => {
4444
})
4545

4646
describe('listSessionsPage', () => {
47+
it('handles v1.16.0+ API response format with data envelope and nested location', async () => {
48+
fetchMock.mockResolvedValue(
49+
new Response(
50+
JSON.stringify({
51+
data: [
52+
{
53+
id: 'ses_v2_1',
54+
projectID: 'proj_v2',
55+
parentID: 'parent_v2',
56+
title: 'V2 Session',
57+
time: { created: 3000, updated: 4000 },
58+
location: { directory: '/my-repo', workspaceID: 'ws_v2' },
59+
agent: 'default',
60+
model: { id: 'gpt-4', providerID: 'openai' },
61+
cost: 0.05,
62+
tokens: { input: 100, output: 50, reasoning: 10, cache: { read: 0, write: 0 } },
63+
},
64+
],
65+
cursor: { next: 'v2_cursor' },
66+
}),
67+
{ status: 200 },
68+
),
69+
)
70+
71+
const result = await new OpenCodeClient('/api/opencode', '/repo').listSessionsPage({ limit: 10 })
72+
73+
expect(result.items).toHaveLength(1)
74+
expect(result.items[0]).toMatchObject({
75+
id: 'ses_v2_1',
76+
projectID: 'proj_v2',
77+
workspaceID: 'ws_v2',
78+
directory: '/repo',
79+
parentID: 'parent_v2',
80+
title: 'V2 Session',
81+
version: 'v2',
82+
})
83+
expect(result.nextCursor).toBe('v2_cursor')
84+
})
85+
4786
it('sends first-page params to /api/session with directory and returns adapted sessions', async () => {
4887
fetchMock.mockResolvedValue(
4988
new Response(

frontend/src/api/opencode.ts

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,62 @@ type LspStatusResponse = paths['/lsp']['get']['responses']['200']['content']['ap
2323
type LspStatus = LspStatusResponse[number]
2424

2525
type LegacySession = SessionListResponse[number]
26-
type SessionV2Info = {
26+
27+
/** Pre-v1.16.0 session shape returned by /api/session */
28+
type SessionV2InfoV1 = {
2729
id: string
2830
parentID?: string
2931
projectID: string
3032
workspaceID?: string
3133
title: string
32-
time: LegacySession['time']
34+
time: { created: number; updated: number; compacting?: number; archived?: number }
3335
path?: unknown
3436
}
37+
38+
/** v1.16.0+ session shape returned by /api/session */
39+
type SessionV2InfoV2 = {
40+
id: string
41+
parentID?: string
42+
projectID: string
43+
title: string
44+
time: { created: number; updated: number; archived?: number }
45+
location: { directory: string; workspaceID?: string }
46+
agent?: string
47+
model?: { id: string; providerID: string; variant?: string }
48+
cost: number
49+
tokens: { input: number; output: number; reasoning: number; cache: { read: number; write: number } }
50+
subpath?: string
51+
}
52+
53+
type SessionV2Info = SessionV2InfoV1 | SessionV2InfoV2
3554
type SessionPageCursor = { previous?: string; next?: string }
36-
type SessionPageResponse = { items: SessionV2Info[]; cursor?: SessionPageCursor }
55+
56+
/** Response from /api/session — may be old (items) or new (data) format */
57+
type SessionPageResponse = {
58+
data?: SessionV2InfoV2[]
59+
items?: SessionV2InfoV1[]
60+
cursor?: SessionPageCursor
61+
}
3762
type SessionPageParams = { limit?: number; order?: 'asc' | 'desc'; search?: string; cursor?: string }
3863
type SessionPage = { items: LegacySession[]; nextCursor?: string }
3964

65+
function isNewSession(session: SessionV2Info): session is SessionV2InfoV2 {
66+
return 'location' in session && session.location !== undefined
67+
}
68+
4069
function toLegacySession(session: SessionV2Info, directory?: string): LegacySession {
70+
if (isNewSession(session)) {
71+
return {
72+
id: session.id,
73+
projectID: session.projectID,
74+
workspaceID: session.location.workspaceID,
75+
directory: directory ?? session.location.directory ?? '',
76+
parentID: session.parentID,
77+
title: session.title || 'Untitled Session',
78+
version: 'v2',
79+
time: session.time,
80+
} as LegacySession
81+
}
4182
return {
4283
id: session.id,
4384
projectID: session.projectID,
@@ -88,8 +129,9 @@ export class OpenCodeClient {
88129
const response = await fetchWrapper<SessionPageResponse>(`${this.baseURL}/api/session`, {
89130
params: queryParams,
90131
})
132+
const rawItems = response.data ?? response.items ?? []
91133
return {
92-
items: response.items.map((item) => toLegacySession(item, this.directory)),
134+
items: rawItems.map((item) => toLegacySession(item, this.directory)),
93135
nextCursor: response.cursor?.next,
94136
}
95137
}

0 commit comments

Comments
 (0)