diff --git a/packages/app/src/context/global-sync.tsx b/packages/app/src/context/global-sync.tsx index c11edd292d1..ea0b90d5de7 100644 --- a/packages/app/src/context/global-sync.tsx +++ b/packages/app/src/context/global-sync.tsx @@ -38,6 +38,7 @@ type State = { config: Config path: Path session: Session[] + sessionTotal: number session_status: { [sessionID: string]: SessionStatus } @@ -98,6 +99,7 @@ function createGlobalSync() { agent: [], command: [], session: [], + sessionTotal: 0, session_status: {}, session_diff: {}, todo: {}, @@ -117,8 +119,10 @@ function createGlobalSync() { async function loadSessions(directory: string) { const [store, setStore] = child(directory) - globalSDK.client.session - .list({ directory }) + const limit = store.limit + + return globalSDK.client.session + .list({ directory, roots: true }) .then((x) => { const fourHoursAgo = Date.now() - 4 * 60 * 60 * 1000 const nonArchived = (x.data ?? []) @@ -128,10 +132,12 @@ function createGlobalSync() { .sort((a, b) => a.id.localeCompare(b.id)) // Include up to the limit, plus any updated in the last 4 hours const sessions = nonArchived.filter((s, i) => { - if (i < store.limit) return true + if (i < limit) return true const updated = new Date(s.time?.updated ?? s.time?.created).getTime() return updated > fourHoursAgo }) + // Store total session count (used for "load more" pagination) + setStore("sessionTotal", nonArchived.length) setStore("session", reconcile(sessions, { key: "id" })) }) .catch((err) => { diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index cffefd5634d..39f397ac466 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -944,7 +944,7 @@ export default function Layout(props: ParentProps) { .toSorted(sortSessions), ) const rootSessions = createMemo(() => sessions().filter((s) => !s.parentID)) - const hasMoreSessions = createMemo(() => store.session.length >= store.limit) + const hasMoreSessions = createMemo(() => store.sessionTotal > store.session.length) const loadMoreSessions = async () => { setProjectStore("limit", (limit) => limit + 5) await globalSync.project.loadSessions(props.project.worktree) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 52457515b8e..7015c818822 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -724,6 +724,8 @@ export namespace Server { validator( "query", z.object({ + directory: z.string().optional().meta({ description: "Filter sessions by project directory" }), + roots: z.coerce.boolean().optional().meta({ description: "Only return root sessions (no parentID)" }), start: z.coerce .number() .optional() @@ -737,6 +739,8 @@ export namespace Server { const term = query.search?.toLowerCase() const sessions: Session.Info[] = [] for await (const session of Session.list()) { + if (query.directory !== undefined && session.directory !== query.directory) continue + if (query.roots && session.parentID) continue if (query.start !== undefined && session.time.updated < query.start) continue if (term !== undefined && !session.title.toLowerCase().includes(term)) continue sessions.push(session) diff --git a/packages/opencode/test/server/session-list.test.ts b/packages/opencode/test/server/session-list.test.ts new file mode 100644 index 00000000000..623c16a8114 --- /dev/null +++ b/packages/opencode/test/server/session-list.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, test } from "bun:test" +import path from "path" +import { Instance } from "../../src/project/instance" +import { Server } from "../../src/server/server" +import { Session } from "../../src/session" +import { Log } from "../../src/util/log" + +const projectRoot = path.join(__dirname, "../..") +Log.init({ print: false }) + +describe("session.list", () => { + test("filters by directory", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const app = Server.App() + + const first = await Session.create({}) + + const otherDir = path.join(projectRoot, "..", "__session_list_other") + const second = await Instance.provide({ + directory: otherDir, + fn: async () => Session.create({}), + }) + + const response = await app.request(`/session?directory=${encodeURIComponent(projectRoot)}`) + expect(response.status).toBe(200) + + const body = (await response.json()) as unknown[] + const ids = body + .map((s) => (typeof s === "object" && s && "id" in s ? (s as { id: string }).id : undefined)) + .filter((x): x is string => typeof x === "string") + + expect(ids).toContain(first.id) + expect(ids).not.toContain(second.id) + }, + }) + }) +}) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index f83913ea5e1..697dac7eefe 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -781,6 +781,7 @@ export class Session extends HeyApiClient { public list( parameters?: { directory?: string + roots?: boolean start?: number search?: string limit?: number @@ -793,6 +794,7 @@ export class Session extends HeyApiClient { { args: [ { in: "query", key: "directory" }, + { in: "query", key: "roots" }, { in: "query", key: "start" }, { in: "query", key: "search" }, { in: "query", key: "limit" }, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index acc29d9b43e..2bb3a600274 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2589,7 +2589,14 @@ export type SessionListData = { body?: never path?: never query?: { + /** + * Filter sessions by project directory + */ directory?: string + /** + * Only return root sessions (no parentID) + */ + roots?: boolean /** * Filter sessions updated on or after this timestamp (milliseconds since epoch) */