Compare commits

...

20 Commits

Author SHA1 Message Date
Agus Zubiaga
13cd9214d7 0.2.4 2025-09-03 15:15:13 -03:00
Agus Zubiaga
78975d75f5 Provide more helpful message on init error 2025-09-03 15:15:03 -03:00
Agus Zubiaga
bbd0774ae5 0.2.3 2025-09-03 15:07:22 -03:00
Agus Zubiaga
76331017b2 Pass node executable path 2025-09-03 15:07:13 -03:00
Agus Zubiaga
90f1447e99 0.2.2 2025-09-03 14:04:10 -03:00
Agus Zubiaga
dae4c9956e Raise timeout 2025-09-03 14:03:47 -03:00
Agus Zubiaga
eb282abb50 Fix readme format 2025-09-03 11:39:33 -03:00
Agus Zubiaga
74c0be057b Add npm badge 2025-09-03 11:36:39 -03:00
Agus Zubiaga
e66091f723 Add permission request note 2025-09-03 11:02:33 -03:00
Agus Zubiaga
1b99ab96a0 Fix test 2025-09-03 09:28:11 -03:00
Agus Zubiaga
8d14d9dd1d 0.2.1 2025-09-03 09:25:44 -03:00
Agus Zubiaga
98b6cd5c07 Fix edit location reporting (#15)
When reporting the edit tool uses, we were accessing a field with a typo
(we should start using Zod, btw), and that caused locations not to be
reported correctly. We also now report all line numbers.

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-09-03 09:25:15 -03:00
Agus Zubiaga
a4c9ba3c7a 0.2.0 2025-09-03 09:05:24 -03:00
Agus Zubiaga
a08ce88f30 Improve edit tool quality (#14)
- Return a unified diff after applying changes
- Fail if the `old_string` isn't found
- Fail if the `old_string` is empty
- Throw instead of returning succesful content on error

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
2025-09-03 09:04:06 -03:00
Agus Zubiaga
f717175cdc 0.1.11 2025-09-02 23:16:46 -03:00
Agus Zubiaga
d1fd794ecb Add backticks around glob pattern (#13)
Looks nicer and prevents potential markdown issues
2025-09-02 23:13:27 -03:00
Agus Zubiaga
f54da50bf5 Update README.md 2025-09-02 22:35:45 -03:00
Agus Zubiaga
55c61986e8 0.1.10 2025-09-02 20:03:22 -03:00
Agus Zubiaga
46f643b9f4 Improve background terminal output 2025-09-02 20:03:14 -03:00
Agus Zubiaga
07316e2eb0 Redirect console.log et al to stderr 2025-09-02 19:46:30 -03:00
9 changed files with 250 additions and 304 deletions

View File

@@ -1,12 +1,14 @@
# ACP adapter for Claude Code
[![npm](https://img.shields.io/npm/v/%40zed-industries%2Fclaude-code-acp)](https://www.npmjs.com/package/@zed-industries/claude-code-acp)
Use [Claude Code](https://www.anthropic.com/claude-code) from [ACP-compatible](https://agentclientprotocol.com) clients such as [Zed](https://zed.dev)!
This tool implements an ACP agent by using the official [Claude Code SDK](https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-overview), supporting:
- Context @-mentions
- Images
- Tool calls
- Tool calls (with permission requests)
- Following
- Edit review
- TODO lists
@@ -26,7 +28,7 @@ To use Claude Code, open the Agent Panel and click "New Claude Code Thread" from
https://github.com/user-attachments/assets/ddce66c7-79ac-47a3-ad59-4a6a3ca74903
[Read the docs](https://zed.dev/docs/ai/external-agents) on External Agent support.
Read the docs on [External Agent](https://zed.dev/docs/ai/external-agents) support.
### Other clients

22
package-lock.json generated
View File

@@ -1,18 +1,19 @@
{
"name": "@zed-industries/claude-code-acp",
"version": "0.1.9",
"version": "0.2.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@zed-industries/claude-code-acp",
"version": "0.1.9",
"version": "0.2.4",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/claude-code": "^1.0.100",
"@modelcontextprotocol/sdk": "^1.17.4",
"@types/express": "^5.0.3",
"@zed-industries/agent-client-protocol": "0.2.0-alpha.4",
"diff": "^8.0.2",
"express": "^5.1.0",
"minimist": "^1.2.8",
"uuid": "11.1.0"
@@ -2215,10 +2216,9 @@
}
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true,
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
"integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
@@ -4362,6 +4362,16 @@
}
}
},
"node_modules/ts-node/node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@@ -6,7 +6,7 @@
"bin": {
"claude-code-acp": "./dist/index.js"
},
"version": "0.1.9",
"version": "0.2.4",
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
"main": "dist/index.js",
"type": "module",
@@ -47,6 +47,7 @@
"@modelcontextprotocol/sdk": "^1.17.4",
"@types/express": "^5.0.3",
"@zed-industries/agent-client-protocol": "0.2.0-alpha.4",
"diff": "^8.0.2",
"express": "^5.1.0",
"minimist": "^1.2.8",
"uuid": "11.1.0"

View File

@@ -134,6 +134,9 @@ export class ClaudeAcpAgent implements Agent {
mcpServers,
permissionPromptToolName: "mcp__acp__permission",
stderr: (err) => console.error(err),
// note: although not documented by the types, passing an absolute path
// here works to find zed's managed node version.
executable: process.execPath as any,
};
const allowedTools = [];
@@ -314,14 +317,16 @@ async function availableSlashCommands(query: Query): Promise<AvailableCommand[]>
const commands = await Promise.race([
//todo: Do not use `as any` once `supportedCommands` is exposed via the typescript interface
(query as any).supportedCommands(),
sleep(5000).then(() => {
sleep(10000).then(() => {
if (
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))
) {
throw RequestError.authRequired();
}
throw new Error("can't load supported slash commands");
throw new Error(
"Failed to intialize Claude Code.\n\nThis may be caused by incorrect MCP server configuration, try disabling them.",
);
}),
]);

View File

@@ -1,5 +1,16 @@
#!/usr/bin/env node
// stdout is used to send messages to the client
// we redirect everything else to stderr to make sure it doesn't interfere with ACP
console.log = console.error;
console.info = console.error;
console.warn = console.error;
console.debug = console.error;
process.on("unhandledRejection", (reason, promise) => {
console.error("Unhandled Rejection at:", promise, "reason:", reason);
});
import { runAcp as runAcp } from "./acp-agent.js";
runAcp();

View File

@@ -6,6 +6,7 @@ import { z } from "zod";
import { Server } from "node:http";
import { ClaudeAcpAgent } from "./acp-agent.js";
import { ClientCapabilities, TerminalOutputResponse } from "@zed-industries/agent-client-protocol";
import * as diff from "diff";
import { sleep, unreachable } from "./utils.js";
@@ -167,7 +168,8 @@ File editing instructions:
- For unique lines, include only those lines.
- For non-unique lines, include enough context to identify them.
- Do not escape quotes, newlines, or other characters.
- Only edit the specified file.`,
- Only edit the specified file.
- If the \`old_string\` value isn't found in the file, the edit won't be applied. The tool will fail and must be retried.`,
inputSchema: {
abs_path: z.string().describe("The absolute path to the file to read."),
old_string: z.string().describe("The old text to replace (must be unique in the file)"),
@@ -182,56 +184,47 @@ File editing instructions:
},
},
async (input) => {
try {
const session = agent.sessions[sessionId];
if (!session) {
return {
content: [
{
type: "text",
text: "The user has left the building",
},
],
};
}
const { content } = await agent.readTextFile({
sessionId,
path: input.abs_path,
});
const { newContent, lineNumbers } = replaceAndCalculateLocation(content, [
{
oldText: input.old_string,
newText: input.new_string,
replaceAll: false,
},
]);
await agent.writeTextFile({
sessionId,
path: input.abs_path,
content: newContent,
});
const session = agent.sessions[sessionId];
if (!session) {
return {
content: [
{
type: "text",
text: JSON.stringify({ lineNumbers }),
},
],
};
} catch (error: any) {
return {
content: [
{
type: "text",
text: "Editing file failed: " + error.message,
text: "The user has left the building",
},
],
};
}
const { content } = await agent.readTextFile({
sessionId,
path: input.abs_path,
});
const { newContent } = replaceAndCalculateLocation(content, [
{
oldText: input.old_string,
newText: input.new_string,
replaceAll: false,
},
]);
const patch = diff.createPatch(input.abs_path, content, newContent);
await agent.writeTextFile({
sessionId,
path: input.abs_path,
content: newContent,
});
return {
content: [
{
type: "text",
text: patch,
},
],
};
},
);
@@ -239,7 +232,20 @@ File editing instructions:
"multi-edit",
{
title: "Multi Edit",
description: `Edit a file with multiple sequential edits.`,
description: `Edit a file with multiple sequential edits.
In sessions with mcp__acp__multi-edit always use it instead of MultiEdit as it will
allow the user to conveniently review changes.
File editing instructions:
- The \`old_string\` param must match existing file content, including indentation.
- The \`old_string\` param must come from the actual file, not an outline.
- The \`old_string\` section must not be empty.
- Be minimal with replacements:
- For unique lines, include only those lines.
- For non-unique lines, include enough context to identify them, unless you're using \`replace_all\`
- Do not escape quotes, newlines, or other characters.
- If any of the provided \`old_string\` values aren't found in the file, no edits will be applied. The tool will fail and must be retried.`,
inputSchema: {
file_path: z.string().describe("The absolute path to the file to modify"),
edits: z
@@ -282,7 +288,7 @@ File editing instructions:
path: input.file_path,
});
const { newContent, lineNumbers } = replaceAndCalculateLocation(
const { newContent } = replaceAndCalculateLocation(
content,
input.edits.map((edit) => ({
oldText: edit.old_string,
@@ -291,6 +297,8 @@ File editing instructions:
})),
);
const patch = diff.createPatch(input.file_path, content, newContent);
await agent.writeTextFile({
sessionId,
path: input.file_path,
@@ -301,7 +309,7 @@ File editing instructions:
content: [
{
type: "text",
text: JSON.stringify({ lineNumbers }),
text: patch,
},
],
};
@@ -379,13 +387,13 @@ File editing instructions:
});
const statusPromise = Promise.race([
handle.waitForExit().then(() => ({ status: "exited" as const })),
abortPromise.then(() => ({ status: "aborted" as const })),
handle.waitForExit().then((exitStatus) => ({ status: "exited" as const, exitStatus })),
abortPromise.then(() => ({ status: "aborted" as const, exitStatus: null })),
sleep(input.timeout_ms).then(async () => {
if (agent.backgroundTerminals[handle.id]?.status === "started") {
await handle.kill();
}
return { status: "timedOut" as const };
return { status: "timedOut" as const, exitStatus: null };
}),
]);
@@ -396,7 +404,7 @@ File editing instructions:
status: "started",
};
statusPromise.then(async ({ status }) => {
statusPromise.then(async ({ status, exitStatus }) => {
const bgTerm = agent.backgroundTerminals[handle.id];
if (bgTerm.status !== "started") {
@@ -410,6 +418,7 @@ File editing instructions:
pendingOutput: {
...currentOutput,
output: stripCommonPrefix(bgTerm.lastOutput?.output ?? "", currentOutput.output),
exitStatus: exitStatus ?? currentOutput.exitStatus,
},
};
@@ -700,9 +709,10 @@ function toolCommandOutput(
let toolOutput = "";
switch (status) {
case "started": {
case "started":
case "exited": {
if (exitStatus && (exitStatus.exitCode ?? null) === null) {
toolOutput += `Interrupted. `;
toolOutput += `Interrupted by the user. `;
}
break;
}
@@ -712,7 +722,6 @@ function toolCommandOutput(
case "timedOut":
toolOutput += `Timed out. `;
break;
case "exited":
case "aborted":
break;
default: {
@@ -722,16 +731,19 @@ function toolCommandOutput(
}
if (exitStatus) {
if (typeof exitStatus.exitCode === "number" && exitStatus.exitCode !== 0) {
toolOutput += `Failed with exit code ${exitStatus.exitCode}.`;
if (typeof exitStatus.exitCode === "number") {
toolOutput += `Exited with code ${exitStatus.exitCode}.`;
}
if (typeof exitStatus.signal === "string") {
toolOutput += `Signal \`${exitStatus.signal}\`. `;
}
toolOutput += "Final output:\n\n";
} else {
toolOutput += "New output:\n\n";
}
toolOutput += "Output:\n\n";
toolOutput += commandOutput;
if (truncated) {
@@ -767,7 +779,7 @@ export function replaceAndCalculateLocation(
for (const edit of edits) {
// Skip empty oldText
if (edit.oldText === "") {
continue;
throw new Error(`The provided \`old_string\` is empty.\n\nNo edits were applied.`);
}
if (edit.replaceAll) {
@@ -779,6 +791,11 @@ export function replaceAndCalculateLocation(
while (true) {
const index = currentContent.indexOf(edit.oldText, searchIndex);
if (index === -1) {
if (searchIndex === 0) {
throw new Error(
`The provided \`old_string\` does not appear in the file: "${edit.oldText}".\n\nNo edits were applied.`,
);
}
break;
}
@@ -800,7 +817,11 @@ export function replaceAndCalculateLocation(
} else {
// Replace first occurrence only
const index = currentContent.indexOf(edit.oldText);
if (index !== -1) {
if (index === -1) {
throw new Error(
`The provided \`old_string\` does not appear in the file: "${edit.oldText}".\n\nNo edits were applied.`,
);
} else {
const marker = `${markerPrefix}${markerCounter++}__`;
markers.push(marker);
currentContent =

View File

@@ -216,7 +216,7 @@ describe("tool conversions", () => {
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "search",
title: "Find */**.ts",
title: "Find `*/**.ts`",
content: [],
locations: [],
});
@@ -638,36 +638,6 @@ describe("tool conversions", () => {
]);
});
it("should extract locations from edit tool results", () => {
const toolUse = {
type: "tool_use",
id: "toolu_01ABC123",
name: "edit",
input: {
abs_path: "/Users/test/project/example.txt",
old_string: "old content",
new_string: "new content",
},
};
const toolResult = {
content: [
{
type: "text",
text: JSON.stringify({ lineNumbers: [5, 10, 15] }),
},
],
};
const update = toolUpdateFromToolResult(toolResult, toolUse);
expect(update.locations).toEqual([
{ path: "/Users/test/project/example.txt", line: 5 },
{ path: "/Users/test/project/example.txt", line: 10 },
{ path: "/Users/test/project/example.txt", line: 15 },
]);
});
it("should show full diff for multi edit tool", () => {
const toolUse = {
type: "tool_use",
@@ -722,110 +692,22 @@ describe("tool conversions", () => {
locations: [
{
path: "/Users/test/project/config.json",
line: 1,
},
{
path: "/Users/test/project/config.json",
line: 3,
},
],
title: "Edit /Users/test/project/config.json",
});
});
it("should extract locations from multi-edit tool results", () => {
const toolUse = {
type: "tool_use",
id: "toolu_01DEF456",
name: "multi-edit",
input: {
file_path: "/Users/test/project/config.json",
edits: [
{
old_string: "version: 1.0",
new_string: "version: 2.0",
replace_all: false,
},
{
old_string: "enabled: false",
new_string: "enabled: true",
replace_all: true,
},
],
},
};
const toolResult = {
content: [
{
type: "text",
text: JSON.stringify({ lineNumbers: [2, 8, 12, 20] }),
},
],
};
const update = toolUpdateFromToolResult(toolResult, toolUse);
expect(update.locations).toEqual([
{ path: "/Users/test/project/config.json", line: 2 },
{ path: "/Users/test/project/config.json", line: 8 },
{ path: "/Users/test/project/config.json", line: 12 },
{ path: "/Users/test/project/config.json", line: 20 },
]);
});
it("should handle mcp__acp__edit tool results", () => {
const toolUse = {
type: "tool_use",
id: "toolu_01GHI789",
name: "mcp__acp__edit",
input: {
abs_path: "/Users/test/project/main.py",
old_string: "def hello():",
new_string: "def hello_world():",
},
};
const toolResult = {
content: [
{
type: "text",
text: JSON.stringify({ lineNumbers: [42] }),
},
],
};
const update = toolUpdateFromToolResult(toolResult, toolUse);
expect(update.locations).toEqual([{ path: "/Users/test/project/main.py", line: 42 }]);
});
it("should handle empty locations from edit tools", () => {
const toolUse = {
type: "tool_use",
id: "toolu_01JKL012",
name: "edit",
input: {
abs_path: "/Users/test/project/notfound.txt",
old_string: "nonexistent",
new_string: "replacement",
},
};
const toolResult = {
content: [
{
type: "text",
text: JSON.stringify({ lineNumbers: [] }),
},
],
};
const update = toolUpdateFromToolResult(toolResult, toolUse);
expect(update.locations).toEqual([]);
});
it("should handle malformed tool results gracefully", () => {
it("should return empty update for successful edit result", () => {
const toolUse = {
type: "tool_use",
id: "toolu_01MNO345",
name: "edit",
name: "mcp__acp__edit",
input: {
abs_path: "/Users/test/project/test.txt",
old_string: "old",
@@ -840,6 +722,9 @@ describe("tool conversions", () => {
text: "not valid json",
},
],
tool_use_id: "test",
is_error: false,
type: "tool_result" as const,
};
const update = toolUpdateFromToolResult(toolResult, toolUse);
@@ -847,6 +732,40 @@ describe("tool conversions", () => {
// Should return empty object when parsing fails
expect(update).toEqual({});
});
it("should return content update for edit failure", () => {
const toolUse = {
type: "tool_use",
id: "toolu_01MNO345",
name: "mcp__acp__edit",
input: {
abs_path: "/Users/test/project/test.txt",
old_string: "old",
new_string: "new",
},
};
const toolResult = {
content: [
{
type: "text",
text: "Failed to find `old_string`",
},
],
tool_use_id: "test",
is_error: true,
type: "tool_result" as const,
};
const update = toolUpdateFromToolResult(toolResult, toolUse);
// Should return empty object when parsing fails
expect(update).toEqual({
content: [
{ content: { type: "text", text: "Failed to find `old_string`" }, type: "content" },
],
});
});
});
describe("escape markdown", () => {

View File

@@ -22,12 +22,9 @@ describe("replaceAndCalculateLocation", () => {
it("should return empty line numbers array when no match found", () => {
const content = "line 1\nline 2\nline 3";
const result = replaceAndCalculateLocation(content, [
{ oldText: "notfound", newText: "replaced" },
]);
expect(result.newContent).toBe(content);
expect(result.lineNumbers).toEqual([]);
expect(() => {
replaceAndCalculateLocation(content, [{ oldText: "notfound", newText: "replaced" }]);
}).toThrow('The provided `old_string` does not appear in the file: "notfound"');
});
it("should handle Windows line endings (CRLF)", () => {
@@ -89,35 +86,31 @@ describe("replaceAndCalculateLocation", () => {
});
it("should handle empty file content", () => {
const content = "";
const result = replaceAndCalculateLocation(content, [{ oldText: "text", newText: "replaced" }]);
expect(result.newContent).toBe("");
expect(result.lineNumbers).toEqual([]);
expect(() => {
replaceAndCalculateLocation("", [{ oldText: "text", newText: "replaced" }]);
}).toThrowError(
'The provided `old_string` does not appear in the file: "text".\n\nNo edits were applied.',
);
});
it("should handle empty search text", () => {
// Test with replaceAll false (default)
const content1 = "line 1\nline 2\nline 3";
const result1 = replaceAndCalculateLocation(content1, [{ oldText: "", newText: "replaced" }]);
expect(result1.newContent).toBe(content1);
expect(result1.lineNumbers).toEqual([]);
expect(() => {
replaceAndCalculateLocation(content1, [{ oldText: "", newText: "replaced" }]);
}).toThrowError("The provided `old_string` is empty.\n\nNo edits were applied.");
// Test with replaceAll true on single line
const content2 = "abc";
const result2 = replaceAndCalculateLocation(content2, [
{ oldText: "", newText: "X", replaceAll: true },
]);
expect(result2.newContent).toBe(content2);
expect(result2.lineNumbers).toEqual([]);
expect(() => {
replaceAndCalculateLocation(content2, [{ oldText: "", newText: "X", replaceAll: true }]);
}).toThrowError("The provided `old_string` is empty.\n\nNo edits were applied.");
// Test with replaceAll true on multiline content
const content3 = "ab\ncd";
const result3 = replaceAndCalculateLocation(content3, [
{ oldText: "", newText: "X", replaceAll: true },
]);
expect(result3.newContent).toBe(content3);
expect(result3.lineNumbers).toEqual([]);
expect(() => {
replaceAndCalculateLocation(content3, [{ oldText: "", newText: "X", replaceAll: true }]);
}).toThrowError("The provided `old_string` is empty.\n\nNo edits were applied.");
});
it("should handle single line content", () => {

View File

@@ -140,27 +140,27 @@ export function toolInfoFromToolUse(
const path = input?.abs_path ?? input?.file_path;
let oldText = input.old_string ?? null;
let newText = input.new_string ?? "";
let line = null;
let affectedLines: number[] = [];
if (path && input.old_text) {
if (path && oldText) {
try {
const oldContent = cachedFileContent[path] || "";
const newContent = replaceAndCalculateLocation(oldContent, [
{
oldText: input.old_string,
newText: input.new_string,
oldText,
newText,
replaceAll: false,
},
]);
oldText = oldContent;
newText = newContent.newContent;
line = newContent.lineNumbers[0];
affectedLines = newContent.lineNumbers;
} catch (e) {
console.error(e);
}
}
return {
title: path ? `Edit ${path}` : "Edit",
title: path ? `Edit \`${path}\`` : "Edit",
kind: "edit",
content:
input && path
@@ -173,7 +173,11 @@ export function toolInfoFromToolUse(
},
]
: [],
locations: path ? [{ path, line }] : [],
locations: path
? affectedLines.length > 0
? affectedLines.map((line) => ({ line, path }))
: [{ path }]
: [],
};
}
@@ -189,6 +193,7 @@ export function toolInfoFromToolUse(
};
let oldTextMulti = multiInput.edits.map((edit: any) => edit.old_string).join("\n");
let newTextMulti = multiInput.edits.map((edit: any) => edit.new_string).join("\n");
let affectedLines: number[] = [];
try {
if (multiInput.edits && multiInput.file_path) {
const oldContent =
@@ -204,6 +209,7 @@ export function toolInfoFromToolUse(
);
oldTextMulti = oldContent;
newTextMulti = newContent.newContent;
affectedLines = newContent.lineNumbers;
}
} catch (e) {
console.error(e);
@@ -221,7 +227,11 @@ export function toolInfoFromToolUse(
newText: newTextMulti,
},
],
locations: input?.file_path ? [{ path: input.file_path }] : [],
locations: input?.file_path
? affectedLines.length > 0
? affectedLines.map((line) => ({ line, path: input.file_path }))
: [{ path: input.file_path }]
: [],
};
}
case "mcp__acp__write": {
@@ -272,10 +282,10 @@ export function toolInfoFromToolUse(
case "Glob": {
let label = "Find";
if (input.path) {
label += ` ${input.path}`;
label += ` \`${input.path}\``;
}
if (input.pattern) {
label += ` ${input.pattern}`;
label += ` \`${input.pattern}\``;
}
return {
title: label,
@@ -424,60 +434,18 @@ export function toolInfoFromToolUse(
}
}
export function toolUpdateFromToolResult(toolResult: any, toolUse: any | undefined): ToolUpdate {
type ToolResult = {
type: "tool_result";
content: any;
tool_use_id: string;
is_error: boolean;
};
export function toolUpdateFromToolResult(
toolResult: ToolResult,
toolUse: any | undefined,
): ToolUpdate {
switch (toolUse?.name) {
case "mcp__acp__edit":
case "edit":
case "Edit": {
// Parse the line numbers from the tool result
if (toolResult.content && toolResult.content.length > 0) {
try {
const firstContent = toolResult.content[0];
const data = JSON.parse(firstContent.text || firstContent);
if (data.lineNumbers && Array.isArray(data.lineNumbers)) {
const locations: ToolCallLocation[] = data.lineNumbers.map((line: number) => ({
path: toolUse?.input?.abs_path || toolUse?.input?.file_path,
line: line,
}));
return { locations };
}
} catch (error) {
console.error(`Error parsing edit tool result: ${error}`);
// If parsing fails, return empty object
return {};
}
}
return {};
}
case "mcp__acp__multi-edit":
case "multi-edit":
case "MultiEdit": {
// Parse the line numbers from the tool result
if (toolResult.content && toolResult.content.length > 0) {
try {
const firstContent = toolResult.content[0];
const data = JSON.parse(firstContent.text || firstContent);
if (data.lineNumbers && Array.isArray(data.lineNumbers)) {
const locations: ToolCallLocation[] = data.lineNumbers.map((line: number) => ({
path: toolUse?.input?.file_path || toolUse?.input?.abs_path,
line: line,
}));
return { locations };
}
} catch (error) {
console.error(`Error parsing edit tool result: ${error}`);
// If parsing fails, return empty object
return {};
}
}
return {};
}
case "mcp__acp__write":
case "Write":
return {};
case "Read":
case "mcp__acp__read":
if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
@@ -509,8 +477,20 @@ export function toolUpdateFromToolResult(toolResult: any, toolUse: any | undefin
return {};
case "mcp__acp__Bash":
// The bash tool already embeds a terminal
case "mcp__acp__edit":
case "edit":
case "Edit":
case "mcp__acp__multi-edit":
case "multi-edit":
case "MultiEdit":
case "mcp__acp__write":
case "Write": {
if (toolResult.is_error && toolResult.content?.length > 0) {
// Only return errors
return toAcpContentUpdate(toolResult.content);
}
return {};
}
case "Task":
case "NotebookEdit":
@@ -527,31 +507,35 @@ export function toolUpdateFromToolResult(toolResult: any, toolUse: any | undefin
case "WebSearch":
case "Other":
default: {
if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
return {
content: toolResult.content.map((content: any) => ({
type: "content",
content,
})),
};
} else if (typeof toolResult.content === "string" && toolResult.content.length > 0) {
return {
content: [
{
type: "content",
content: {
type: "text",
text: toolResult.content,
},
},
],
};
}
return {};
return toAcpContentUpdate(toolResult.content);
}
}
}
function toAcpContentUpdate(content: any): { content?: ToolCallContent[] } {
if (Array.isArray(content) && content.length > 0) {
return {
content: content.map((content: any) => ({
type: "content",
content,
})),
};
} else if (typeof content === "string" && content.length > 0) {
return {
content: [
{
type: "content",
content: {
type: "text",
text: content,
},
},
],
};
}
return {};
}
type ClaudePlanEntry = {
content: string;
status: "pending" | "in_progress" | "completed";