From 581e9f878552bc63dea4177504eaa091d8533d4f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 27 Aug 2025 11:46:19 -0600 Subject: [PATCH] Bash tool Co-authored-by: Richard Feldman Co-authored-by: Ben Brandt --- src/acp-agent.ts | 70 +---------- src/tests/example.test.ts | 27 ++++ src/tools.ts | 251 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 284 insertions(+), 64 deletions(-) create mode 100644 src/tools.ts diff --git a/src/acp-agent.ts b/src/acp-agent.ts index f2e9a6ea..25eab63e 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -11,6 +11,8 @@ import { NewSessionResponse, PromptRequest, PromptResponse, + ToolCallContent, + ToolKind, } from "@zed-industries/agent-client-protocol"; import { McpServerConfig, @@ -32,6 +34,7 @@ import { ContentBlock } from "@zed-industries/agent-client-protocol"; import { SessionNotification } from "@zed-industries/agent-client-protocol"; import { createMcpServer } from "./mcp-server.js"; import { AddressInfo } from "node:net"; +import { toolContent, toolLabel, toolKind } from "./tools.js"; // Implement the ACP Agent interface export class ClaudeAcpAgent implements Agent { @@ -265,11 +268,12 @@ function toAcpNotifications( case "tool_use": update = { toolCallId: chunk.id, - title: chunk.name, + title: toolLabel(chunk), sessionUpdate: "tool_call", - kind: "other", // todo!() + kind: toolKind(chunk.name), // todo!() rawInput: chunk.input, status: "pending", + content: toolContent(chunk), }; break; @@ -292,68 +296,6 @@ function toAcpNotifications( return output; } -2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: "content": [ -2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: { -2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: "type": "tool_use", -2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: "id": "toolu_01BZ6ZfAoU5Vw1g4YiRa8kv3", -2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: "name": "Bash", -2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: "input": { -2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: "command": "rm README.md.es", -2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: "description": "Delete README.md.es file" -2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: } -2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: } -2025-08-27T11:10:22-06:00 WARN [agent_servers::acp] agent stderr: ], - -pub fn label(&self) -> String { - match &self { - Self::Task(Some(params)) => params.description.clone(), - Self::Task(None) => "Task".into(), - Self::NotebookRead(Some(params)) => { - format!("Read Notebook {}", params.notebook_path.display()) - } - Self::NotebookRead(None) => "Read Notebook".into(), - Self::NotebookEdit(Some(params)) => { - format!("Edit Notebook {}", params.notebook_path.display()) - } - Self::NotebookEdit(None) => "Edit Notebook".into(), - Self::Terminal(Some(params)) => format!("`{}`", params.command), - Self::Terminal(None) => "Terminal".into(), - Self::ReadFile(_) => "Read File".into(), - Self::Ls(Some(params)) => { - format!("List Directory {}", params.path.display()) - } - Self::Ls(None) => "List Directory".into(), - Self::Edit(Some(params)) => { - format!("Edit {}", params.abs_path.display()) - } - Self::Edit(None) => "Edit".into(), - Self::MultiEdit(Some(params)) => { - format!("Multi Edit {}", params.file_path.display()) - } - Self::MultiEdit(None) => "Multi Edit".into(), - Self::Write(Some(params)) => { - format!("Write {}", params.abs_path.display()) - } - Self::Write(None) => "Write".into(), - Self::Glob(Some(params)) => { - format!("Glob `{params}`") - } - Self::Glob(None) => "Glob".into(), - Self::Grep(Some(params)) => format!("`{params}`"), - Self::Grep(None) => "Grep".into(), - Self::WebFetch(Some(params)) => format!("Fetch {}", params.url), - Self::WebFetch(None) => "Fetch".into(), - Self::WebSearch(Some(params)) => format!("Web Search: {}", params), - Self::WebSearch(None) => "Web Search".into(), - Self::TodoWrite(Some(params)) => format!( - "Update TODOs: {}", - params.todos.iter().map(|todo| &todo.content).join(", ") - ), - Self::TodoWrite(None) => "Update TODOs".into(), - Self::ExitPlanMode(_) => "Exit Plan Mode".into(), - Self::Other { name, .. } => name.clone(), - } -} export function runAcp() { new AgentSideConnection( diff --git a/src/tests/example.test.ts b/src/tests/example.test.ts index 56846384..877cd00f 100644 --- a/src/tests/example.test.ts +++ b/src/tests/example.test.ts @@ -14,6 +14,7 @@ import { WriteTextFileResponse, } from "@zed-industries/agent-client-protocol"; import { nodeToWebWritable, nodeToWebReadable } from "../utils.js"; +import { toolContent, toolKind, toolLabel } from "../tools.js"; describe("ACP subprocess integration", () => { let child: ReturnType; @@ -70,3 +71,29 @@ describe("ACP subprocess integration", () => { }); }); }); + +describe("tool conversions", () => { + it("should handle Bash nicely", () => { + const tool_use = { + type: "tool_use", + id: "toolu_01VtsS2mxUFwpBJZYd7BmbC9", + name: "Bash", + input: { + command: "rm README.md.rm", + description: "Delete README.md.rm file", + }, + }; + + expect(toolKind(tool_use.name)).toBe("execute"); + expect(toolLabel(tool_use)).toBe("rm README.md.rm"); + expect(toolContent(tool_use)).toStrictEqual([ + { + content: { + text: "Delete README.md.rm file", + type: "text", + }, + type: "content", + }, + ]); + }); +}); diff --git a/src/tools.ts b/src/tools.ts new file mode 100644 index 00000000..0550119a --- /dev/null +++ b/src/tools.ts @@ -0,0 +1,251 @@ +import { + ToolCallContent, + ToolKind, +} from "@zed-industries/agent-client-protocol"; + +export function toolLabel(toolUse: any): string { + const name = toolUse.name; + const input = toolUse.input; + + switch (name) { + case "Task": + return input?.description ? input.description : "Task"; + case "NotebookRead": + return input?.notebook_path + ? `Read Notebook ${input.notebook_path}` + : "Read Notebook"; + case "NotebookEdit": + return input?.notebook_path + ? `Edit Notebook ${input.notebook_path}` + : "Edit Notebook"; + case "Terminal": + case "Bash": + return input?.command ?? "Terminal"; + case "ReadFile": + return "Read File"; + case "LS": + return input?.path ? `List Directory ${input.path}` : "List Directory"; + case "Edit": + return input?.abs_path ? `Edit ${input.abs_path}` : "Edit"; + case "MultiEdit": + return input?.file_path ? `Multi Edit ${input.file_path}` : "Multi Edit"; + case "Write": + return input?.abs_path ? `Write ${input.abs_path}` : "Write"; + case "Glob": + return input ? `Glob \`${input}\`` : "Glob"; + case "Grep": + return input ? `\`${input}\`` : "Grep"; + case "WebFetch": + return input?.url ? `Fetch ${input.url}` : "Fetch"; + case "WebSearch": + return input ? `Web Search: ${input}` : "Web Search"; + case "TodoWrite": + return input?.todos + ? `Update TODOs: ${input.todos.map((todo: any) => todo.content).join(", ")}` + : "Update TODOs"; + case "exit_plan_mode": + return "Exit Plan Mode"; + default: + return name || "Unknown Tool"; + } +} + +export function toolKind(toolName: string): ToolKind { + switch (toolName) { + case "Task": + return "think"; + case "NotebookRead": + return "read"; + case "NotebookEdit": + return "edit"; + case "Edit": + return "edit"; + case "MultiEdit": + return "edit"; + case "Write": + return "edit"; + case "ReadFile": + return "read"; + case "LS": + return "search"; + case "Glob": + return "search"; + case "Grep": + return "search"; + case "Bash": + case "Terminal": + return "execute"; + case "WebSearch": + return "search"; + case "WebFetch": + return "fetch"; + case "TodoWrite": + return "think"; + case "exit_plan_mode": + return "think"; + default: + return "other"; + } +} + +export function toolContent(toolUse: any): ToolCallContent[] { + const input = toolUse.input; + const name = toolUse.name; + + switch (name) { + case "Other": + return [ + { + type: "content", + content: { + type: "text", + text: `\`\`\`json\n${JSON.stringify(input, null, 2) || "{}"}\`\`\``, + }, + }, + ]; + case "Task": + if (input && input.prompt) { + return [ + { type: "content", content: { type: "text", text: input.prompt } }, + ]; + } + break; + case "NotebookRead": + if (input && input.notebook_path) { + return [ + { + type: "content", + content: { type: "text", text: input.notebook_path.toString() }, + }, + ]; + } + break; + case "NotebookEdit": + if (input && input.new_source) { + return [ + { + type: "content", + content: { type: "text", text: input.new_source }, + }, + ]; + } + break; + case "Bash": + if (input && input.command) { + return [ + { + type: "content", + content: { + type: "text", + text: input.description, + }, + }, + ]; + } + break; + case "ReadFile": + if (input && input.abs_path) { + return [ + { + type: "content", + content: { type: "text", text: input.abs_path.toString() }, + }, + ]; + } + break; + case "LS": + if (input && input.path) { + return [ + { + type: "content", + content: { type: "text", text: input.path.toString() }, + }, + ]; + } + break; + case "Glob": + if (input) { + return [ + { + type: "content", + content: { type: "text", text: input.toString() }, + }, + ]; + } + break; + case "Grep": + if (input) { + return [ + { type: "content", content: { type: "text", text: `\`${input}\`` } }, + ]; + } + break; + case "WebFetch": + if (input && input.prompt) { + return [ + { type: "content", content: { type: "text", text: input.prompt } }, + ]; + } + break; + case "WebSearch": + if (input) { + return [ + { + type: "content", + content: { type: "text", text: input.toString() }, + }, + ]; + } + break; + case "exit_plan_mode": + if (input && input.plan) { + return [ + { type: "content", content: { type: "text", text: input.plan } }, + ]; + } + break; + case "Edit": + if (input && input.abs_path) { + return [ + { + type: "diff", + path: input.abs_path, + oldText: input.old_text || null, + newText: input.new_text, + }, + ]; + } + break; + case "Write": + if (input && input.abs_path) { + return [ + { + type: "diff", + path: input.abs_path, + oldText: null, + newText: input.content, + }, + ]; + } + break; + case "MultiEdit": + if (input && input.edits && input.edits.length > 0) { + // todo: show multiple edits in a multibuffer? + const edit = input.edits[0]; + return [ + { + type: "diff", + path: input.file_path, + oldText: edit.old_string || null, + newText: edit.new_string, + }, + ]; + } + break; + case "TodoWrite": + // These are mapped to plan updates later + return []; + } + + return []; +}