fix(acp): make ACP/Kimi sessions importable again (#1510)

ACP-based providers (Kimi and other custom ACP agents, Copilot, Cursor) were
absent from the import session picker. Two causes, both in the import path:

1. Never queried. The import refactor (51d1d007) gated provider listing on
   capabilities.supportsSessionListing and set it for claude/codex/opencode/pi
   but not for ACP providers, so agent-manager skipped every ACP provider in
   listImportableSessions — the picker showed 'no sessions' even though
   listImportableSessions is fully implemented. Set supportsSessionListing on
   DEFAULT_ACP_CAPABILITIES and COPILOT_CAPABILITIES. Agents that don't support
   session/list still return an empty list at the runtime probe, so this only
   controls whether the daemon queries them.

2. Ignored cwd. listImportableSessions never forwarded the requested cwd to the
   agent's session/list call, so agents returned globally-recent sessions that
   the import limit could truncate before the current directory's sessions were
   reached. Forward options.cwd as the session/list cwd filter (with the
   pagination cursor) so the agent filters by directory at the source.
This commit is contained in:
qer
2026-06-14 14:52:12 +08:00
committed by GitHub
parent 72b67f48e3
commit bf8f2510c8
3 changed files with 119 additions and 1 deletions

View File

@@ -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<typeof vi.fn>; supportsList?: boolean }) {
class TestACPAgentClient extends ACPAgentClient {
protected override async spawnProcess(): Promise<SpawnedACPProcess> {
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<void> {}
}
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(

View File

@@ -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({

View File

@@ -26,6 +26,7 @@ import {
const COPILOT_CAPABILITIES: AgentCapabilityFlags = {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsSessionListing: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,