diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index f8fc9380..302d4c87 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -1294,6 +1294,9 @@ export class CodexAcpClient { cursor: request.cursor ?? null, modelProviders: modelProviders, sourceKinds: sourceKinds, + cwd: requestedCwd, + sortKey: "updated_at", + sortDirection: "desc", useStateDbOnly: true, }); @@ -1304,6 +1307,7 @@ export class CodexAcpClient { updatedAt: new Date(thread.updatedAt * 1000).toISOString(), }); + // App Server owns pagination; normalize the returned page for ACP path compatibility. let sessions = listResponse.data.map(mapThreadToSession); if (requestedCwd) { const filtered = listResponse.data diff --git a/src/__tests__/CodexACPAgent/data/session-index-list-params-legacy.json b/src/__tests__/CodexACPAgent/data/session-index-list-params-legacy.json index 8c85448b..a42a375d 100644 --- a/src/__tests__/CodexACPAgent/data/session-index-list-params-legacy.json +++ b/src/__tests__/CodexACPAgent/data/session-index-list-params-legacy.json @@ -9,6 +9,9 @@ "appServer", "unknown" ], + "cwd": "/repo/project", + "sortKey": "updated_at", + "sortDirection": "desc", "useStateDbOnly": true }, "plain": { @@ -21,6 +24,9 @@ "appServer", "unknown" ], + "cwd": "/repo/project", + "sortKey": "updated_at", + "sortDirection": "desc", "useStateDbOnly": true } } diff --git a/src/__tests__/CodexACPAgent/list-sessions.test.ts b/src/__tests__/CodexACPAgent/list-sessions.test.ts index 3aaeb970..00ad026b 100644 --- a/src/__tests__/CodexACPAgent/list-sessions.test.ts +++ b/src/__tests__/CodexACPAgent/list-sessions.test.ts @@ -4,6 +4,94 @@ import { createCodexMockTestFixture } from "../acp-test-utils"; import type { Thread } from "../../app-server/v2"; describe("CodexACPAgent - list sessions", () => { + it.each([ + { + serverCwdBehavior: "string-only", + acceptsRequestedCwd: (cwd: string | string[] | null | undefined) => cwd === "/repo/project", + }, + { + serverCwdBehavior: "string-or-array", + acceptsRequestedCwd: (cwd: string | string[] | null | undefined) => { + const cwds = Array.isArray(cwd) ? cwd : cwd ? [cwd] : []; + return cwds.includes("/repo/project"); + }, + }, + ])("forwards cwd before $serverCwdBehavior thread pagination", async ({acceptsRequestedCwd}) => { + const fixture = createCodexMockTestFixture(); + const codexAcpAgent = fixture.getCodexAcpAgent(); + const codexAcpClient = fixture.getCodexAcpClient(); + const codexAppServerClient = fixture.getCodexAppServerClient(); + + codexAcpClient.readAuthRequirement = vi.fn().mockResolvedValue({required: false, account: null}); + + const matchingThread = createThread("sess-project", "/repo/project"); + const unrelatedThread = createThread("sess-other", "/repo/other"); + codexAppServerClient.threadList = vi.fn().mockImplementation(async (params) => { + if (acceptsRequestedCwd(params.cwd)) { + return {data: [matchingThread], nextCursor: null}; + } + return {data: [unrelatedThread], nextCursor: "unrelated-global-cursor"}; + }); + + const response = await codexAcpAgent.listSessions({ + cwd: "/repo/project", + cursor: null, + }); + + expect(response).toEqual({ + sessions: [{ + sessionId: "sess-project", + cwd: "/repo/project", + title: "Session sess-project", + updatedAt: "1970-01-01T00:03:20.000Z", + }], + nextCursor: null, + }); + }); + + it("forwards cwd, cursor, and CLI ordering across pagination", async () => { + const fixture = createCodexMockTestFixture(); + const codexAcpAgent = fixture.getCodexAcpAgent(); + const codexAcpClient = fixture.getCodexAcpClient(); + const codexAppServerClient = fixture.getCodexAppServerClient(); + + codexAcpClient.readAuthRequirement = vi.fn().mockResolvedValue({required: false, account: null}); + codexAppServerClient.threadList = vi.fn() + .mockResolvedValueOnce({ + data: [createThread("sess-newer", "/repo/project")], + nextCursor: "project-page-2", + }) + .mockResolvedValueOnce({ + data: [createThread("sess-older", "/repo/project")], + nextCursor: null, + }); + + const firstPage = await codexAcpAgent.listSessions({ + cwd: "/repo/project", + cursor: null, + }); + await codexAcpAgent.listSessions({ + cwd: "/repo/project", + cursor: firstPage.nextCursor ?? null, + }); + + expect(codexAppServerClient.threadList).toHaveBeenNthCalledWith(1, expect.objectContaining({ + cwd: "/repo/project", + cursor: null, + sortKey: "updated_at", + sortDirection: "desc", + })); + expect(codexAppServerClient.threadList).toHaveBeenNthCalledWith(2, expect.objectContaining({ + cwd: "/repo/project", + cursor: "project-page-2", + sortKey: "updated_at", + sortDirection: "desc", + })); + for (const [params] of vi.mocked(codexAppServerClient.threadList).mock.calls) { + expect(params).not.toHaveProperty("limit"); + } + }); + it("should list sessions filtered by cwd", async () => { const fixture = createCodexMockTestFixture(); const codexAcpAgent = fixture.getCodexAcpAgent(); @@ -73,10 +161,10 @@ describe("CodexACPAgent - list sessions", () => { turns: [], }; - codexAppServerClient.threadList = vi.fn().mockResolvedValue({ - data: [threadA, threadB], + codexAppServerClient.threadList = vi.fn().mockImplementation(async (params) => ({ + data: [threadA, threadB].filter(thread => thread.cwd === params.cwd), nextCursor: "next-cursor", - }); + })); codexAppServerClient.threadLoadedList = vi.fn().mockResolvedValue({ data: [], nextCursor: null, @@ -332,3 +420,36 @@ describe("CodexACPAgent - list sessions", () => { expect(response.sessions[0]?.additionalDirectories).toEqual(["/repo/extra"]); }); }); + +function createThread(id: string, cwd: string): Thread { + return { + id, + sessionId: id, + parentThreadId: null, + threadSource: null, + originator: null, + forkedFromId: null, + preview: `Session ${id}`, + ephemeral: false, + modelProvider: "openai", + model: null, + reasoningEffort: null, + createdAt: 100, + updatedAt: 200, + recencyAt: null, + status: {type: "idle"}, + path: null, + cwd, + cliVersion: "0.0.0", + section: null, + sectionEnteredAt: null, + projectId: null, + historyMode: "legacy", + source: "cli", + agentNickname: null, + agentRole: null, + gitInfo: null, + name: null, + turns: [], + }; +}