Fix showing of diffs in permission prompts

This commit is contained in:
Conrad Irwin
2025-08-28 20:58:45 -06:00
parent 77f667c619
commit b875e24ad5
5 changed files with 183 additions and 41 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@zed-industries/claude-code-acp",
"version": "0.1.0",
"version": "0.1.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@zed-industries/claude-code-acp",
"version": "0.1.0",
"version": "0.1.2",
"license": "Apache-2.0",
"dependencies": {
"@anthropic-ai/claude-code": "^1.0.96",

View File

@@ -11,9 +11,13 @@ import {
NewSessionResponse,
PromptRequest,
PromptResponse,
ReadTextFileRequest,
ReadTextFileResponse,
RequestError,
ToolCallContent,
ToolKind,
WriteTextFileRequest,
WriteTextFileResponse,
} from "@zed-industries/agent-client-protocol";
import {
McpServerConfig,
@@ -54,11 +58,13 @@ export class ClaudeAcpAgent implements Agent {
};
client: Client;
toolUseCache: { [key: string]: any };
fileContentCache: { [key: string]: any };
constructor(client: Client) {
this.sessions = {};
this.client = client;
this.toolUseCache = {};
this.fileContentCache = {};
}
async initialize(params: InitializeRequest): Promise<InitializeResponse> {
return {
@@ -196,6 +202,7 @@ export class ClaudeAcpAgent implements Agent {
message,
params.sessionId,
this.toolUseCache,
this.fileContentCache,
)) {
await this.client.sessionUpdate(notification);
}
@@ -215,6 +222,24 @@ export class ClaudeAcpAgent implements Agent {
this.sessions[params.sessionId].cancelled = true;
await this.sessions[params.sessionId].query.interrupt();
}
async readTextFile(
params: ReadTextFileRequest,
): Promise<ReadTextFileResponse> {
let response = await this.client.readTextFile(params);
if (!params.limit && !params.line) {
this.fileContentCache[params.path] = response.content;
}
return response;
}
async writeTextFile(
params: WriteTextFileRequest,
): Promise<WriteTextFileResponse> {
let response = await this.client.writeTextFile(params);
this.fileContentCache[params.path] = params.content;
return response;
}
}
function formatUriAsLink(uri: string): string {
@@ -313,6 +338,7 @@ export function toAcpNotifications(
message: SDKAssistantMessage | SDKUserMessage,
sessionId: string,
toolUseCache: { [key: string]: any },
fileContentCache: { [key: string]: string },
): SessionNotification[] {
let chunks = message.message.content as ContentChunk[];
let output = [];
@@ -363,7 +389,7 @@ export function toAcpNotifications(
sessionUpdate: "tool_call",
rawInput: chunk.input,
status: "pending",
...toolInfoFromToolUse(chunk),
...toolInfoFromToolUse(chunk, fileContentCache),
};
}
break;

View File

@@ -59,7 +59,7 @@ In sessions with mcp__acp__read always use it instead of Read as it contains the
],
};
}
let content = await agent.client.readTextFile({
let content = await agent.readTextFile({
sessionId,
path: input.abs_path,
limit: input.limit,
@@ -126,7 +126,7 @@ allow the user to conveniently review changes.`,
],
};
}
let content = await agent.client.writeTextFile({
let content = await agent.writeTextFile({
sessionId,
path: input.abs_path,
content: input.content,
@@ -195,7 +195,7 @@ File editing instructions:
};
}
let { content } = await agent.client.readTextFile({
let { content } = await agent.readTextFile({
sessionId,
path: input.abs_path,
});
@@ -211,7 +211,7 @@ File editing instructions:
],
);
await agent.client.writeTextFile({
await agent.writeTextFile({
sessionId,
path: input.abs_path,
content: newContent,
@@ -286,7 +286,7 @@ File editing instructions:
};
}
let { content } = await agent.client.readTextFile({
let { content } = await agent.readTextFile({
sessionId,
path: input.file_path,
});
@@ -300,7 +300,7 @@ File editing instructions:
})),
);
await agent.client.writeTextFile({
await agent.writeTextFile({
sessionId,
path: input.file_path,
content: newContent,

View File

@@ -56,7 +56,7 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
throw new Error("Method not implemented.");
}
async sessionUpdate(params: SessionNotification): Promise<void> {
console.error("RECEVIED", params);
console.error("RECEVIED", JSON.stringify(params, null, 4));
}
writeTextFile(
params: WriteTextFileRequest,
@@ -68,7 +68,7 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
): Promise<ReadTextFileResponse> {
return {
content:
"This project attempts to take over the world by solving the Goldbach conjecture.",
"One\nTwo\nThree\nFour\nFive\nSix\nSeven\nEight\nNine\nTen\nEleven\nTwelve\nThirteen\nFourteen\nFifteen\nSixteen\nSeventeen\nEighteen\nNineteen\nTwenty\n",
};
}
}
@@ -87,7 +87,7 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
prompt: [
{
type: "text",
text: "What does this project do? Read the README.md file.",
text: "Use the multi edit tool to remove all multiples of 5 from the README and replace with buzz",
},
],
sessionId: session.sessionId,
@@ -109,7 +109,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "execute",
title: "`rm README.md.rm`",
content: [
@@ -134,7 +134,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "search",
title: "Find */**.ts",
content: [],
@@ -155,7 +155,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "think",
title: "Handle user's work request",
content: [
@@ -180,7 +180,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "search",
title: "/Users/benbrandt/github/claude-code-acp",
content: [],
@@ -198,7 +198,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "search",
title: 'grep ".*"',
content: [],
@@ -216,7 +216,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "edit",
title: "Write /Users/test/project/example.txt",
content: [
@@ -242,7 +242,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "edit",
title: "Write /Users/test/project/config.json",
content: [
@@ -267,7 +267,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "read",
title: "Read File",
content: [
@@ -290,7 +290,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "read",
title: "Read /Users/test/project/data.json",
content: [],
@@ -309,7 +309,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "read",
title: "Read /Users/test/project/large.txt (1 - 100)",
content: [],
@@ -329,7 +329,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "read",
title: "Read /Users/test/project/large.txt (51 - 150)",
content: [],
@@ -348,7 +348,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "read",
title: "Read /Users/test/project/large.txt (from line 201)",
content: [],
@@ -368,7 +368,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "fetch",
title: "Fetch https://agentclientprotocol.com",
content: [
@@ -393,7 +393,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "fetch",
title: '"agentclientprotocol.com"',
content: [],
@@ -410,7 +410,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "execute",
title: `Kill Process`,
content: [],
@@ -427,7 +427,7 @@ describe("tool conversions", () => {
},
};
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use, {})).toStrictEqual({
kind: "execute",
title: `Tail Logs`,
content: [],
@@ -512,7 +512,7 @@ describe("tool conversions", () => {
session_id: "d056596f-e328-41e9-badd-b07122ae5227",
uuid: "b7c3330c-de8f-4bba-ac53-68c7f76ffeb5",
};
expect(toAcpNotifications(received, "test", {})).toStrictEqual([
expect(toAcpNotifications(received, "test", {}, {})).toStrictEqual([
{
sessionId: "test",
update: {
@@ -594,6 +594,66 @@ describe("tool conversions", () => {
]);
});
it("should show full diff for multi edit tool", () => {
const toolUse = {
type: "tool_use",
id: "toolu_01DEF456",
name: "mcp__acp__multi-edit",
input: {
file_path: "/Users/test/project/config.json",
edits: [
{
old_string: 'version": 1',
new_string: 'version": 2',
replace_all: false,
},
{
old_string: 'enabled": false',
new_string: 'enabled": true',
replace_all: true,
},
],
},
};
let fileCache: { [key: string]: string } = {
"/Users/test/project/config.json": JSON.stringify(
{
version: 1,
filler: "filler",
enabled: false,
},
null,
4,
),
};
expect(toolInfoFromToolUse(toolUse, fileCache)).toEqual({
content: [
{
newText: `{
\"version\": 2,
\"filler\": \"filler\",
\"enabled\": true
}`,
oldText: `{
\"version\": 1,
\"filler\": \"filler\",
\"enabled\": false
}`,
path: "/Users/test/project/config.json",
type: "diff",
},
],
kind: "edit",
locations: [
{
path: "/Users/test/project/config.json",
},
],
title: "Edit /Users/test/project/config.json",
});
});
it("should extract locations from multi-edit tool results", () => {
const toolUse = {
type: "tool_use",

View File

@@ -1,10 +1,12 @@
import { ContentBlock } from "@modelcontextprotocol/sdk/types.js";
import {
Client,
PlanEntry,
ToolCallContent,
ToolCallLocation,
ToolKind,
} from "@zed-industries/agent-client-protocol";
import { replaceAndCalculateLocation } from "./mcp-server.js";
interface ToolInfo {
title: string;
@@ -19,7 +21,10 @@ interface ToolUpdate {
locations?: ToolCallLocation[];
}
export function toolInfoFromToolUse(toolUse: any): ToolInfo {
export function toolInfoFromToolUse(
toolUse: any,
cachedFileContent: { [key: string]: string },
): ToolInfo {
const name = toolUse.name;
const input = toolUse.input;
@@ -160,6 +165,25 @@ export function toolInfoFromToolUse(toolUse: any): ToolInfo {
case "mcp__acp__edit":
case "Edit":
let oldText = input.old_text || null;
let newText = input.new_text;
if (input?.abs_path && input.old_text) {
try {
let oldContent = cachedFileContent[input.abs_path] || "";
let newContent = replaceAndCalculateLocation(oldContent, [
{
oldText: input.old_text,
newText: input.new_text,
replaceAll: false,
},
]);
oldText = oldContent;
newText = newContent.newContent;
} catch (e) {
console.error(e);
}
}
return {
title: input?.abs_path ? `Edit ${input.abs_path}` : "Edit",
kind: "edit",
@@ -169,8 +193,8 @@ export function toolInfoFromToolUse(toolUse: any): ToolInfo {
{
type: "diff",
path: input.abs_path,
oldText: input.old_text || null,
newText: input.new_text,
oldText,
newText,
},
]
: [],
@@ -179,20 +203,52 @@ export function toolInfoFromToolUse(toolUse: any): ToolInfo {
case "mcp__acp__multi-edit":
case "MultiEdit":
let multiInput = input as {
file_path: string;
edits: {
old_string: string;
new_string: string;
replace_all?: boolean;
}[];
};
let oldTextMulti = multiInput.edits
.map((edit: any) => edit.old_string)
.join("\n");
let newTextMulti = multiInput.edits
.map((edit: any) => edit.new_string)
.join("\n");
try {
if (multiInput.edits && multiInput.file_path) {
let oldContent =
cachedFileContent[multiInput.file_path] ||
multiInput.edits.map((edit: any) => edit.oldText).join("\n");
let newContent = replaceAndCalculateLocation(
oldContent,
multiInput.edits.map((edit) => ({
oldText: edit.old_string,
newText: edit.new_string,
replaceAll: edit.replace_all,
})),
);
oldTextMulti = oldContent;
newTextMulti = newContent.newContent;
}
} catch (e) {
console.error(e);
}
// Display it as a normal edit, because end users don't care about
// the distinction between edits and multi-edits.
return {
title: input?.file_path ? `Edit ${input.file_path}` : "Edit",
kind: "edit",
content:
input && input.edits && input.edits.length > 0
? input.edits.map((edit: any) => ({
type: "diff" as const,
path: input.file_path,
oldText: edit.old_string || null,
newText: edit.new_string,
}))
: [],
content: [
{
type: "diff" as const,
path: input.file_path,
oldText: oldTextMulti,
newText: newTextMulti,
},
],
locations: input?.file_path ? [{ path: input.file_path }] : [],
};