diff --git a/packages/server/src/server/agent/providers/acp-agent.test.ts b/packages/server/src/server/agent/providers/acp-agent.test.ts index 8b42e12c4..cf5e58036 100644 --- a/packages/server/src/server/agent/providers/acp-agent.test.ts +++ b/packages/server/src/server/agent/providers/acp-agent.test.ts @@ -31,12 +31,14 @@ import { import { COPILOT_ALLOW_ALL_MODE_ID, COPILOT_MODES, + CopilotACPAgentClient, beforeCopilotModeWriter, transformCopilotConfigOptions, transformCopilotModeId, transformCopilotSessionResponse, writeCopilotProviderMode, } from "./copilot-acp-agent.js"; +import { GenericACPAgentClient } from "./generic-acp-agent.js"; import { transformPiModels } from "./pi/agent.js"; import type { AgentStreamEvent } from "../agent-sdk-types.js"; import { createTestLogger } from "../../../test-utils/test-logger.js"; @@ -1362,6 +1364,111 @@ describe("ACPAgentClient listModes", () => { }); }); +describe("ACPAgentClient listImportableSessions", () => { + function makeClient(args: { listSessions: ReturnType; supportsList?: boolean }) { + class TestACPAgentClient extends ACPAgentClient { + protected override async spawnProcess(): Promise { + return { + child: { kill: vi.fn(), exitCode: 0, signalCode: null, once: vi.fn() }, + connection: { listSessions: args.listSessions }, + initialize: { + agentCapabilities: + args.supportsList === false ? {} : { sessionCapabilities: { list: {} } }, + }, + } as unknown as SpawnedACPProcess; + } + + protected override async closeProbe(): Promise {} + } + + return new TestACPAgentClient({ + provider: "kimi", + logger: createTestLogger(), + defaultCommand: ["kimi", "acp"], + defaultModes: [], + }); + } + + test("forwards the requested cwd to session/list so the agent filters by directory", async () => { + const listSessions = vi.fn().mockResolvedValue({ + sessions: [ + { + sessionId: "session-1", + cwd: "/Users/moonshot", + title: "细致查看一下本仓库内容", + updatedAt: "2026-06-13T00:00:00.000Z", + }, + ], + nextCursor: null, + }); + + const client = makeClient({ listSessions }); + const result = await client.listImportableSessions({ cwd: "/Users/moonshot", limit: 20 }); + + expect(listSessions).toHaveBeenCalledWith({ cwd: "/Users/moonshot" }); + expect(result).toEqual([ + { + providerHandleId: "session-1", + cwd: "/Users/moonshot", + title: "细致查看一下本仓库内容", + firstPromptPreview: null, + lastPromptPreview: null, + lastActivityAt: new Date("2026-06-13T00:00:00.000Z"), + }, + ]); + }); + + test("omits cwd from session/list when none is requested", async () => { + const listSessions = vi.fn().mockResolvedValue({ sessions: [], nextCursor: null }); + const client = makeClient({ listSessions }); + + await client.listImportableSessions({ limit: 20 }); + + expect(listSessions).toHaveBeenCalledWith({}); + }); + + test("forwards cwd alongside the pagination cursor across pages", async () => { + const listSessions = vi + .fn() + .mockResolvedValueOnce({ + sessions: [{ sessionId: "s1", cwd: "/Users/moonshot", title: null, updatedAt: null }], + nextCursor: "cursor-2", + }) + .mockResolvedValueOnce({ + sessions: [{ sessionId: "s2", cwd: "/Users/moonshot", title: null, updatedAt: null }], + nextCursor: null, + }); + + const client = makeClient({ listSessions }); + await client.listImportableSessions({ cwd: "/Users/moonshot" }); + + expect(listSessions).toHaveBeenNthCalledWith(1, { cwd: "/Users/moonshot" }); + expect(listSessions).toHaveBeenNthCalledWith(2, { + cursor: "cursor-2", + cwd: "/Users/moonshot", + }); + }); +}); + +describe("ACP providers advertise session listing", () => { + // The daemon's agent-manager only queries providers whose + // capabilities.supportsSessionListing is true. Without it, ACP providers + // (Kimi and other custom ACP agents, Copilot) are skipped and import shows + // nothing even though listImportableSessions is implemented. + test("generic ACP clients (e.g. Kimi) report supportsSessionListing", () => { + const client = new GenericACPAgentClient({ + logger: createTestLogger(), + command: ["kimi", "acp"], + }); + expect(client.capabilities.supportsSessionListing).toBe(true); + }); + + test("Copilot ACP client reports supportsSessionListing", () => { + const client = new CopilotACPAgentClient({ logger: createTestLogger() }); + expect(client.capabilities.supportsSessionListing).toBe(true); + }); +}); + describe("transformPiModels", () => { test("keeps slash-free labels unchanged", () => { expect( diff --git a/packages/server/src/server/agent/providers/acp-agent.ts b/packages/server/src/server/agent/providers/acp-agent.ts index da8a4783f..e22281f5b 100644 --- a/packages/server/src/server/agent/providers/acp-agent.ts +++ b/packages/server/src/server/agent/providers/acp-agent.ts @@ -186,6 +186,10 @@ function resolveTerminalCommand( export const DEFAULT_ACP_CAPABILITIES: AgentCapabilityFlags = { supportsStreaming: true, supportsSessionPersistence: true, + // ACP agents can list prior sessions via `session/list`. The runtime probe in + // listImportableSessions returns nothing for agents that don't advertise the + // capability, so enabling this here only makes the daemon query them. + supportsSessionListing: true, supportsDynamicModes: true, supportsMcpServers: true, supportsReasoningStream: true, @@ -758,7 +762,13 @@ export class ACPAgentClient implements AgentClient { let cursor: string | null | undefined; for (;;) { const page: ListSessionsResponse = await this.runACPRequest(() => - probe.connection.listSessions(cursor ? { cursor } : {}), + probe.connection.listSessions({ + ...(cursor ? { cursor } : {}), + // Filter by working directory at the source. Without this the agent + // returns globally-recent sessions, which the `limit` below can + // truncate before the current directory's sessions are reached. + ...(options?.cwd ? { cwd: options.cwd } : {}), + }), ); for (const session of page.sessions) { sessions.push({ diff --git a/packages/server/src/server/agent/providers/copilot-acp-agent.ts b/packages/server/src/server/agent/providers/copilot-acp-agent.ts index d6a8fadf9..1d493b623 100644 --- a/packages/server/src/server/agent/providers/copilot-acp-agent.ts +++ b/packages/server/src/server/agent/providers/copilot-acp-agent.ts @@ -26,6 +26,7 @@ import { const COPILOT_CAPABILITIES: AgentCapabilityFlags = { supportsStreaming: true, supportsSessionPersistence: true, + supportsSessionListing: true, supportsDynamicModes: true, supportsMcpServers: true, supportsReasoningStream: true,