diff --git a/src/acp-agent.ts b/src/acp-agent.ts index d99ee5a3..3a381864 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -110,6 +110,7 @@ type Session = { query: Query; input: Pushable; cancelled: boolean; + cwd: string; permissionMode: PermissionMode; settingsManager: SettingsManager; accumulatedUsage: AccumulatedUsage; @@ -672,7 +673,10 @@ export class ClaudeAcpAgent implements Agent { this.toolUseCache, this.client, this.logger, - { clientCapabilities: this.clientCapabilities }, + { + clientCapabilities: this.clientCapabilities, + cwd: session.cwd, + }, )) { await this.client.sessionUpdate(notification); } @@ -784,6 +788,7 @@ export class ClaudeAcpAgent implements Agent { { clientCapabilities: this.clientCapabilities, parentToolUseId: message.parent_tool_use_id, + cwd: session.cwd, }, )) { await this.client.sessionUpdate(notification); @@ -958,7 +963,11 @@ export class ClaudeAcpAgent implements Agent { toolUseCache, this.client, this.logger, - { registerHooks: false, clientCapabilities: this.clientCapabilities }, + { + registerHooks: false, + clientCapabilities: this.clientCapabilities, + cwd: this.sessions[sessionId]?.cwd, + }, )) { await this.client.sessionUpdate(notification); } @@ -1005,6 +1014,7 @@ export class ClaudeAcpAgent implements Agent { ...toolInfoFromToolUse( { name: toolName, input: toolInput, id: toolUseID }, supportsTerminalOutput, + session?.cwd, ), }, }); @@ -1069,6 +1079,7 @@ export class ClaudeAcpAgent implements Agent { ...toolInfoFromToolUse( { name: toolName, input: toolInput, id: toolUseID }, supportsTerminalOutput, + session?.cwd, ), }, }); @@ -1382,6 +1393,7 @@ export class ClaudeAcpAgent implements Agent { query: q, input: input, cancelled: false, + cwd: params.cwd, permissionMode, settingsManager, accumulatedUsage: { @@ -1697,6 +1709,7 @@ export function toAcpNotifications( registerHooks?: boolean; clientCapabilities?: ClientCapabilities; parentToolUseId?: string | null; + cwd?: string; }, ): SessionNotification[] { const registerHooks = options?.registerHooks !== false; @@ -1825,7 +1838,7 @@ export function toAcpNotifications( toolCallId: chunk.id, sessionUpdate: "tool_call_update", rawInput, - ...toolInfoFromToolUse(chunk, supportsTerminalOutput), + ...toolInfoFromToolUse(chunk, supportsTerminalOutput, options?.cwd), }; } else { // First encounter (streaming content_block_start or replay) — @@ -1843,7 +1856,7 @@ export function toAcpNotifications( sessionUpdate: "tool_call", rawInput, status: "pending", - ...toolInfoFromToolUse(chunk, supportsTerminalOutput), + ...toolInfoFromToolUse(chunk, supportsTerminalOutput, options?.cwd), }; } } @@ -1949,7 +1962,10 @@ export function streamEventToAcpNotifications( toolUseCache: ToolUseCache, client: AgentSideConnection, logger: Logger, - options?: { clientCapabilities?: ClientCapabilities }, + options?: { + clientCapabilities?: ClientCapabilities; + cwd?: string; + }, ): SessionNotification[] { const event = message.event; switch (event.type) { @@ -1964,6 +1980,7 @@ export function streamEventToAcpNotifications( { clientCapabilities: options?.clientCapabilities, parentToolUseId: message.parent_tool_use_id, + cwd: options?.cwd, }, ); case "content_block_delta": @@ -1977,6 +1994,7 @@ export function streamEventToAcpNotifications( { clientCapabilities: options?.clientCapabilities, parentToolUseId: message.parent_tool_use_id, + cwd: options?.cwd, }, ); // No content diff --git a/src/lib.ts b/src/lib.ts index 87421a8f..984e358e 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -15,7 +15,12 @@ export { Pushable, unreachable, } from "./utils.js"; -export { toolInfoFromToolUse, planEntries, toolUpdateFromToolResult } from "./tools.js"; +export { + toolInfoFromToolUse, + toDisplayPath, + planEntries, + toolUpdateFromToolResult, +} from "./tools.js"; export { SettingsManager, type ClaudeCodeSettings, diff --git a/src/tests/acp-agent.test.ts b/src/tests/acp-agent.test.ts index ab3897c8..554c6071 100644 --- a/src/tests/acp-agent.test.ts +++ b/src/tests/acp-agent.test.ts @@ -20,6 +20,7 @@ import { nodeToWebWritable, nodeToWebReadable } from "../utils.js"; import { markdownEscape, toolInfoFromToolUse, + toDisplayPath, toolUpdateFromToolResult, toolUpdateFromEditToolResponse, } from "../tools.js"; @@ -563,6 +564,20 @@ describe("tool conversions", () => { }); }); + it("should use relative path in title when cwd is provided", () => { + const tool_use = { + type: "tool_use", + id: "toolu_01READ_CWD", + name: "Read", + input: { file_path: "/Users/test/project/src/main.ts" }, + }; + + const result = toolInfoFromToolUse(tool_use, false, "/Users/test/project"); + expect(result.title).toBe("Read src/main.ts"); + // locations.path stays absolute for navigation + expect(result.locations).toStrictEqual([{ path: "/Users/test/project/src/main.ts", line: 1 }]); + }); + it("should handle plan entries", () => { const received: SDKAssistantMessage = { type: "assistant", @@ -969,6 +984,22 @@ describe("tool conversions", () => { }); }); +describe("toDisplayPath", () => { + it("should relativize paths inside cwd and keep absolute paths outside", () => { + expect(toDisplayPath("/Users/test/project/src/main.ts", "/Users/test/project")).toBe( + "src/main.ts", + ); + expect(toDisplayPath("/etc/hosts", "/Users/test/project")).toBe("/etc/hosts"); + expect(toDisplayPath("/Users/test/project/src/main.ts")).toBe( + "/Users/test/project/src/main.ts", + ); + // Partial directory name match should not be treated as inside cwd + expect(toDisplayPath("/Users/test/project-other/file.ts", "/Users/test/project")).toBe( + "/Users/test/project-other/file.ts", + ); + }); +}); + describe("toolUpdateFromEditToolResponse", () => { it("should return empty for non-object input", () => { expect(toolUpdateFromEditToolResponse(null)).toEqual({}); @@ -1279,6 +1310,7 @@ describe("stop reason propagation", () => { query: gen as any, input: new Pushable(), cancelled: false, + cwd: "/test", permissionMode: "default", settingsManager: {} as any, accumulatedUsage: { diff --git a/src/tools.ts b/src/tools.ts index 2b6bac82..07791c45 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -1,3 +1,4 @@ +import path from "node:path"; import { ContentBlock, PlanEntry, @@ -103,9 +104,24 @@ interface ToolUpdate { }; } +/** + * Convert an absolute file path to a project-relative path for display. + * Returns the original path if it's outside the project directory or if no cwd is provided. + */ +export function toDisplayPath(filePath: string, cwd?: string): string { + if (!cwd) return filePath; + const resolvedCwd = path.resolve(cwd); + const resolvedFile = path.resolve(filePath); + if (resolvedFile.startsWith(resolvedCwd + path.sep) || resolvedFile === resolvedCwd) { + return path.relative(resolvedCwd, resolvedFile); + } + return filePath; +} + export function toolInfoFromToolUse( toolUse: any, supportsTerminalOutput: boolean = false, + cwd?: string, ): ToolInfo { const name = toolUse.name; @@ -154,8 +170,9 @@ export function toolInfoFromToolUse( } else if (input.offset) { limit = " (from line " + input.offset + ")"; } + const displayPath = input.file_path ? toDisplayPath(input.file_path, cwd) : "File"; return { - title: "Read " + (input.file_path ?? "File") + limit, + title: "Read " + displayPath + limit, kind: "read", locations: input.file_path ? [ @@ -189,8 +206,9 @@ export function toolInfoFromToolUse( }, ]; } + const displayPath = input?.file_path ? toDisplayPath(input.file_path, cwd) : undefined; return { - title: input?.file_path ? `Write ${input.file_path}` : "Write", + title: displayPath ? `Write ${displayPath}` : "Write", kind: "edit", content, locations: input?.file_path ? [{ path: input.file_path }] : [], @@ -210,8 +228,9 @@ export function toolInfoFromToolUse( }, ]; } + const displayPath = input?.file_path ? toDisplayPath(input.file_path, cwd) : undefined; return { - title: input?.file_path ? `Edit ${input.file_path}` : "Edit", + title: displayPath ? `Edit ${displayPath}` : "Edit", kind: "edit", content, locations: input?.file_path ? [{ path: input.file_path }] : [],