mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
Fix showing of diffs in permission prompts
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
80
src/tools.ts
80
src/tools.ts
@@ -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 }] : [],
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user