Skip to content

Commit be51f94

Browse files
committed
core: add pagination support for session messages with cursor-based navigation
Enables loading messages in chunks for better performance with long conversations. Users can now navigate through large session histories without loading all messages at once. Includes before/after cursors for bi-directional pagination.
1 parent 60e2152 commit be51f94

4 files changed

Lines changed: 82 additions & 8 deletions

File tree

packages/opencode/src/cli/cmd/tui/context/sync-v2.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,8 @@ export const { use: useSyncV2, provider: SyncProviderV2 } = createSimpleContext(
254254
session: {
255255
message: {
256256
async sync(sessionID: string) {
257-
const response = await sdk.client.v2.session.messages({
258-
sessionID,
259-
})
260-
setStore("messages", sessionID, reconcile(response.data ?? []))
257+
const response = await sdk.client.v2.session.messages({ sessionID })
258+
setStore("messages", sessionID, reconcile(response.data?.items ?? []))
261259
},
262260
fromSession(sessionID: string) {
263261
const messages = store.messages[sessionID]

packages/opencode/src/server/routes/instance/v2.ts

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,43 @@ import { SessionV2 } from "@/v2/session"
44
import { zod } from "@/util/effect-zod"
55
import { lazy } from "@/util/lazy"
66
import { Effect, Schema } from "effect"
7+
import * as DateTime from "effect/DateTime"
78
import { Hono } from "hono"
89
import { describeRoute, resolver, validator } from "hono-openapi"
10+
import { HTTPException } from "hono/http-exception"
911
import z from "zod"
1012
import { errors } from "../../error"
1113
import { jsonRequest } from "./trace"
1214

15+
const DefaultMessagesLimit = 50
16+
17+
const Cursor = Schema.Struct({
18+
id: SessionMessage.ID,
19+
time: Schema.Number,
20+
from: Schema.Union([Schema.Literal("start"), Schema.Literal("end")]),
21+
})
22+
23+
const MessagesResponse = Schema.Struct({
24+
items: Schema.Array(SessionMessage.Message),
25+
cursor: Schema.Struct({
26+
before: Schema.String.pipe(Schema.optional),
27+
after: Schema.String.pipe(Schema.optional),
28+
}),
29+
}).annotate({ identifier: "V2SessionMessagesResponse" })
30+
31+
const decodeCursor = Schema.decodeUnknownSync(Cursor)
32+
33+
const cursor = {
34+
encode(message: SessionMessage.Message, from: "start" | "end") {
35+
return Buffer.from(
36+
JSON.stringify({ id: message.id, time: DateTime.toEpochMillis(message.time.created), from }),
37+
).toString("base64url")
38+
},
39+
decode(input: string) {
40+
return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8")))
41+
},
42+
}
43+
1344
export const V2Routes = lazy(() =>
1445
new Hono().get(
1546
"/session/:sessionID/message",
@@ -22,20 +53,51 @@ export const V2Routes = lazy(() =>
2253
description: "List of v2 session messages",
2354
content: {
2455
"application/json": {
25-
schema: resolver(zod(Schema.Array(SessionMessage.Message))),
56+
schema: resolver(zod(MessagesResponse)),
2657
},
2758
},
2859
},
2960
...errors(400, 404),
3061
},
3162
}),
3263
validator("param", z.object({ sessionID: SessionID.zod })),
64+
validator(
65+
"query",
66+
z.object({
67+
limit: z.coerce.number().int().min(1).max(200).optional(),
68+
cursor: z.string().optional(),
69+
}),
70+
),
3371
async (c) => {
3472
const sessionID = c.req.valid("param").sessionID
73+
const query = c.req.valid("query")
74+
const decoded = (() => {
75+
try {
76+
return query.cursor && query.cursor !== "start" && query.cursor !== "end"
77+
? cursor.decode(query.cursor)
78+
: undefined
79+
} catch {
80+
throw new HTTPException(400)
81+
}
82+
})()
3583
return jsonRequest("V2Routes.messages", c, function* () {
3684
return yield* Effect.gen(function* () {
3785
const session = yield* SessionV2.Service
38-
return yield* session.messages({ sessionID })
86+
const messages = yield* session.messages({
87+
sessionID,
88+
limit: query.limit ?? DefaultMessagesLimit,
89+
from: decoded?.from ?? (query.cursor === "start" ? "start" : "end"),
90+
cursor: decoded ? { id: decoded.id, time: decoded.time } : undefined,
91+
})
92+
const oldest = messages[0]
93+
const newest = messages.at(-1)
94+
return {
95+
items: messages,
96+
cursor: {
97+
before: oldest ? cursor.encode(oldest, "end") : undefined,
98+
after: newest ? cursor.encode(newest, "start") : undefined,
99+
},
100+
}
39101
}).pipe(Effect.provide(SessionV2.defaultLayer))
40102
})
41103
},

packages/sdk/js/src/v2/gen/sdk.gen.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3192,6 +3192,8 @@ export class Session3 extends HeyApiClient {
31923192
sessionID: string
31933193
directory?: string
31943194
workspace?: string
3195+
limit?: number
3196+
cursor?: string
31953197
},
31963198
options?: Options<never, ThrowOnError>,
31973199
) {
@@ -3203,6 +3205,8 @@ export class Session3 extends HeyApiClient {
32033205
{ in: "path", key: "sessionID" },
32043206
{ in: "query", key: "directory" },
32053207
{ in: "query", key: "workspace" },
3208+
{ in: "query", key: "limit" },
3209+
{ in: "query", key: "cursor" },
32063210
],
32073211
},
32083212
],

packages/sdk/js/src/v2/gen/types.gen.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2842,6 +2842,14 @@ export type SessionMessage =
28422842
| SessionMessageAssistant
28432843
| SessionMessageCompaction
28442844

2845+
export type V2SessionMessagesResponse = {
2846+
items: Array<SessionMessage>
2847+
cursor: {
2848+
before?: string
2849+
after?: string
2850+
}
2851+
}
2852+
28452853
export type Symbol = {
28462854
name: string
28472855
kind: number
@@ -5528,6 +5536,8 @@ export type V2SessionMessagesData = {
55285536
query?: {
55295537
directory?: string
55305538
workspace?: string
5539+
limit?: number
5540+
cursor?: string
55315541
}
55325542
url: "/api/session/{sessionID}/message"
55335543
}
@@ -5549,10 +5559,10 @@ export type V2SessionMessagesResponses = {
55495559
/**
55505560
* List of v2 session messages
55515561
*/
5552-
200: Array<SessionMessage>
5562+
200: V2SessionMessagesResponse
55535563
}
55545564

5555-
export type V2SessionMessagesResponse = V2SessionMessagesResponses[keyof V2SessionMessagesResponses]
5565+
export type V2SessionMessagesResponse2 = V2SessionMessagesResponses[keyof V2SessionMessagesResponses]
55565566

55575567
export type FindTextData = {
55585568
body?: never

0 commit comments

Comments
 (0)