Browse Source

feat: add cursor pagination to global session list

Ryan Vogel 2 tháng trước cách đây
mục cha
commit
34e69735cb

+ 13 - 2
packages/opencode/src/server/routes/global.ts

@@ -135,6 +135,10 @@ export const GlobalRoutes = lazy(() =>
             .number()
             .optional()
             .meta({ description: "Filter sessions updated on or after this timestamp (milliseconds since epoch)" }),
+          cursor: z.coerce
+            .number()
+            .optional()
+            .meta({ description: "Return sessions updated before this timestamp (milliseconds since epoch)" }),
           search: z.string().optional().meta({ description: "Filter sessions by title (case-insensitive)" }),
           limit: z.coerce.number().optional().meta({ description: "Maximum number of sessions to return" }),
           archived: z.coerce.boolean().optional().meta({ description: "Include archived sessions (default false)" }),
@@ -142,18 +146,25 @@ export const GlobalRoutes = lazy(() =>
       ),
       async (c) => {
         const query = c.req.valid("query")
+        const limit = query.limit ?? 100
         const sessions: Session.GlobalInfo[] = []
         for await (const session of Session.listGlobal({
           directory: query.directory,
           roots: query.roots,
           start: query.start,
+          cursor: query.cursor,
           search: query.search,
-          limit: query.limit,
+          limit: limit + 1,
           archived: query.archived,
         })) {
           sessions.push(session)
         }
-        return c.json(sessions)
+        const hasMore = sessions.length > limit
+        const list = hasMore ? sessions.slice(0, limit) : sessions
+        if (hasMore && list.length > 0) {
+          c.header("x-next-cursor", String(list[list.length - 1].time.updated))
+        }
+        return c.json(list)
       },
     )
     .get(

+ 5 - 1
packages/opencode/src/session/index.ts

@@ -10,7 +10,7 @@ import { Flag } from "../flag/flag"
 import { Identifier } from "../id/id"
 import { Installation } from "../installation"
 
-import { Database, NotFoundError, eq, and, or, gte, isNull, desc, like, inArray } from "../storage/db"
+import { Database, NotFoundError, eq, and, or, gte, isNull, desc, like, inArray, lt } from "../storage/db"
 import type { SQL } from "../storage/db"
 import { SessionTable, MessageTable, PartTable } from "./session.sql"
 import { ProjectTable } from "../project/project.sql"
@@ -568,6 +568,7 @@ export namespace Session {
     directory?: string
     roots?: boolean
     start?: number
+    cursor?: number
     search?: string
     limit?: number
     archived?: boolean
@@ -583,6 +584,9 @@ export namespace Session {
     if (input?.start) {
       conditions.push(gte(SessionTable.time_updated, input.start))
     }
+    if (input?.cursor) {
+      conditions.push(lt(SessionTable.time_updated, input.cursor))
+    }
     if (input?.search) {
       conditions.push(like(SessionTable.title, `%${input.search}%`))
     }

+ 24 - 0
packages/opencode/test/server/global-session-list.test.ts

@@ -62,4 +62,28 @@ describe("Session.listGlobal", () => {
 
     expect(allIds).toContain(archived.id)
   })
+
+  test("supports cursor pagination", async () => {
+    await using tmp = await tmpdir({ git: true })
+
+    const first = await Instance.provide({
+      directory: tmp.path,
+      fn: async () => Session.create({ title: "page-one" }),
+    })
+    await new Promise((resolve) => setTimeout(resolve, 5))
+    const second = await Instance.provide({
+      directory: tmp.path,
+      fn: async () => Session.create({ title: "page-two" }),
+    })
+
+    const page = [...Session.listGlobal({ directory: tmp.path, limit: 1 })]
+    expect(page.length).toBe(1)
+    expect(page[0].id).toBe(second.id)
+
+    const next = [...Session.listGlobal({ directory: tmp.path, limit: 10, cursor: page[0].time.updated })]
+    const ids = next.map((session) => session.id)
+
+    expect(ids).toContain(first.id)
+    expect(ids).not.toContain(second.id)
+  })
 })