mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 04:30:59 +00:00
feat: show project-relative paths in tool call titles (#393)
## Summary - Tool call titles (Read, Write, Edit) currently display absolute file paths like `/Users/jane/my-project/src/main.ts`, which are long and redundant since the user already knows their project root. - This PR converts those paths to project-relative display paths (e.g., `src/main.ts`) by comparing against the session's `cwd`. Paths outside the project directory remain absolute. - The `locations.path` field is **not** changed, it stays absolute for Zed's file navigation. ## Test plan - [x] Added unit tests for `toDisplayPath` (relativizes paths inside cwd, keeps absolute paths outside, handles partial directory name matches) - [x] Added unit test for `toolInfoFromToolUse` with `cwd` parameter (verifies title uses relative path, locations stay absolute) - [x] All 124 existing tests pass
This commit is contained in:
@@ -110,6 +110,7 @@ type Session = {
|
||||
query: Query;
|
||||
input: Pushable<SDKUserMessage>;
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
25
src/tools.ts
25
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 }] : [],
|
||||
|
||||
Reference in New Issue
Block a user