diff --git a/src/tests/acp-scenarios-v2.test.ts b/src/tests/acp-scenarios-v2.test.ts index d9db6fd9..a1e76cba 100644 --- a/src/tests/acp-scenarios-v2.test.ts +++ b/src/tests/acp-scenarios-v2.test.ts @@ -61,8 +61,6 @@ const here = path.dirname(fileURLToPath(import.meta.url)); const PASSES = "passes"; /** Step 4 of `docs/acp-v2.md`. */ const REPLAY = "Invalid params: The ACP v2 surface does not replay session history yet"; -/** Step 5 of `docs/acp-v2.md`. */ -const PLANS = "The ACP v2 surface does not translate plan session updates yet"; /** * How each scenario runs on the v2 surface: it passes, or stops for a reason @@ -91,8 +89,8 @@ const V2_STATUS: Record = { "subagent-nested": PASSES, "subagent-late-child-update": PASSES, "subagent-transcript-extension": PASSES, - "todo-write": PLANS, - "task-create-update": PLANS, + "todo-write": PASSES, + "task-create-update": PASSES, goal: PASSES, "network-permission": PASSES, "exit-plan-approve": PASSES, diff --git a/src/tests/acp-scenarios/__snapshots__/v2/task-create-update.jsonl b/src/tests/acp-scenarios/__snapshots__/v2/task-create-update.jsonl new file mode 100644 index 00000000..7c08dc61 --- /dev/null +++ b/src/tests/acp-scenarios/__snapshots__/v2/task-create-update.jsonl @@ -0,0 +1,26 @@ +{"kind":"response","method":"initialize","payload":{"_meta":{"steering":{"supported":true}},"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":""},"protocolVersion":2}} +{"kind":"response","method":"session/new","payload":{"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":""}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [|all]]","type":"text"},"name":"mcp"}],"sessionUpdate":"available_commands_update"}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"","sessionUpdate":"user_message"}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"sessionUpdate":"state_update","state":"running"}}} +{"kind":"response","method":"session/prompt","payload":{"messageId":""}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"plan":{"entries":[{"content":"Write tests","priority":"medium","status":"pending"}],"planId":"plan","type":"items"},"sessionUpdate":"plan_update"}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"plan":{"entries":[{"content":"Write tests","priority":"medium","status":"pending"}],"planId":"plan","type":"items"},"sessionUpdate":"plan_update"}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"plan":{"entries":[{"content":"Write tests","priority":"medium","status":"in_progress"}],"planId":"plan","type":"items"},"sessionUpdate":"plan_update"}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"plan":{"entries":[{"content":"Write tests","priority":"medium","status":"completed"}],"planId":"plan","type":"items"},"sessionUpdate":"plan_update"}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"plan":{"entries":[{"content":"Write tests","priority":"medium","status":"completed"}],"planId":"plan","type":"items"},"sessionUpdate":"plan_update"}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"content":[{"text":"prompt 2","type":"text"}],"messageId":"","sessionUpdate":"user_message"}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"sessionUpdate":"state_update","state":"running"}}} +{"kind":"response","method":"session/prompt","payload":{"messageId":""}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"content":{"text":"All ","type":"text"},"messageId":"msg_follow","sessionUpdate":"agent_message_chunk"}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"content":{"text":"done.","type":"text"},"messageId":"msg_follow","sessionUpdate":"agent_message_chunk"}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}} diff --git a/src/tests/acp-scenarios/__snapshots__/v2/todo-write.jsonl b/src/tests/acp-scenarios/__snapshots__/v2/todo-write.jsonl new file mode 100644 index 00000000..afc3c6a2 --- /dev/null +++ b/src/tests/acp-scenarios/__snapshots__/v2/todo-write.jsonl @@ -0,0 +1,11 @@ +{"kind":"response","method":"initialize","payload":{"_meta":{"steering":{"supported":true}},"authMethods":[],"capabilities":{"_meta":{"authStatus":{},"claudeCode":{"promptQueueing":true}}},"info":{"name":"@agentclientprotocol/claude-agent-acp","title":"Claude Agent","version":""},"protocolVersion":2}} +{"kind":"response","method":"session/new","payload":{"configOptions":[{"category":"mode","configId":"mode","currentValue":"default","description":"Session permission mode","name":"Mode","options":[{"description":"Always ask before making changes","name":"Manual","value":"default"},{"description":"Automatically accept all file edits","name":"Accept edits","value":"acceptEdits"},{"description":"Create a plan before making changes","name":"Plan","value":"plan"},{"description":"Claude handles permission decisions","name":"Auto","value":"auto"},{"description":"Accepts all permissions","name":"Bypass permissions","value":"bypassPermissions"}],"type":"select"},{"category":"model","configId":"model","currentValue":"claude-sonnet-4-6","description":"AI model to use","name":"Model","options":[{"description":"Fast","name":"Claude Sonnet 4.6","value":"claude-sonnet-4-6"}],"type":"select"}],"sessionId":""}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"availableCommands":[{"description":"Show the MCP servers and their status, or reconnect, enable, or disable a server","input":{"hint":"[reconnect|enable|disable [|all]]","type":"text"},"name":"mcp"}],"sessionUpdate":"available_commands_update"}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"content":[{"text":"prompt 1","type":"text"}],"messageId":"","sessionUpdate":"user_message"}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"sessionUpdate":"state_update","state":"running"}}} +{"kind":"response","method":"session/prompt","payload":{"messageId":""}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":2}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"sessionUpdate":"usage_update","size":200000,"used":3}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"plan":{"entries":[{"content":"Writing tests","priority":"medium","status":"in_progress"},{"content":"Ship","priority":"medium","status":"pending"}],"planId":"plan","type":"items"},"sessionUpdate":"plan_update"}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"_meta":{"_claude/model":"claude-sonnet-4-6"},"cost":{"amount":0,"currency":"USD"},"sessionUpdate":"usage_update","size":200000,"used":2}}} +{"kind":"notification","method":"session/update","payload":{"sessionId":"","update":{"_meta":{"quota":{"model_usage":[],"token_count":{"cachedInputTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"reasoningOutputTokens":0,"totalTokens":0}}},"sessionUpdate":"state_update","state":"idle","stopReason":"end_turn","usage":{"cachedReadTokens":0,"cachedWriteTokens":0,"inputTokens":0,"outputTokens":0,"totalTokens":0}}}} diff --git a/src/tests/acp-v2.test.ts b/src/tests/acp-v2.test.ts index 983a03a5..dd5db898 100644 --- a/src/tests/acp-v2.test.ts +++ b/src/tests/acp-v2.test.ts @@ -1036,9 +1036,19 @@ describe("ACP v2 session translation", () => { }); it("fails on session updates that it does not translate yet", () => { - expect(() => v2SessionUpdate({ sessionUpdate: "plan", entries: [] })).toThrow( - "does not translate plan session updates yet", - ); + expect(() => + v2SessionUpdate({ sessionUpdate: "compaction_update", compactionId: "c", status: "started" }), + ).toThrow("does not translate compaction_update session updates yet"); + }); + + it("reports the plan of the session as one v2 plan", () => { + const entries = [{ content: "Test", priority: "high" as const, status: "pending" as const }]; + const meta = { claudeCode: { parentToolUseId: "toolu_agent" } }; + expect(v2SessionUpdate({ sessionUpdate: "plan", entries, _meta: meta })).toEqual({ + sessionUpdate: "plan_update", + plan: { type: "items", planId: "plan", entries }, + _meta: meta, + }); }); it("reports a new tool call with an upserting tool_call_update", () => { diff --git a/src/v2/session-update.ts b/src/v2/session-update.ts index 63861721..2909ea9b 100644 --- a/src/v2/session-update.ts +++ b/src/v2/session-update.ts @@ -7,6 +7,13 @@ import type { AcpSessionNotification } from "../acp-subagents.js"; import { v2AvailableCommands, v2ConfigOptions } from "./session.js"; import { v2ToolCallUpdate } from "./tool-call.js"; +/** + * The id of the plan of a session. v1 has one plan per session, which every + * plan update replaces whole, from TodoWrite and from the Task tools alike. + * v2 tracks plans by id, so that plan gets one. + */ +export const SESSION_PLAN_ID = "plan"; + /** * The v2 form of a v1 session update, or `undefined` when v2 has no * counterpart and the update tells a v2 client nothing new. @@ -39,6 +46,13 @@ export function v2SessionUpdate( case "tool_call": case "tool_call_update": return v2ToolCallUpdate(update); + case "plan": + // A v2 plan of items is replaced whole as well. + return { + sessionUpdate: "plan_update", + plan: { type: "items", planId: SESSION_PLAN_ID, entries: update.entries }, + ...(update._meta != null ? { _meta: update._meta } : {}), + }; case "current_mode_update": // v2 has no modes: the mode is the `mode` config option. Every mode // change also reaches the client as a `config_option_update` or in the