fix(acp): 显示命令工具调用详情

This commit is contained in:
jiwangyihao
2026-05-17 05:13:49 +08:00
parent 2d96feb6c6
commit b53fc964ce
2 changed files with 125 additions and 11 deletions
@@ -1,6 +1,7 @@
import type {
SessionNotification,
SessionUpdate,
ToolCall,
ToolCallContent,
ToolCallLocation,
ToolKind,
@@ -144,18 +145,13 @@ export function mapAgentSessionEventToAcpSessionUpdates(
case "message_end":
return mapAssistantMessageEnd(event, sessionId, options);
case "tool_execution_start": {
const update: SessionUpdate = {
sessionUpdate: "tool_call",
const update = buildToolCallStartUpdate({
toolCallId: event.toolCallId,
title: buildToolTitle(event.toolName, event.args, event.intent),
kind: mapToolKind(event.toolName),
status: "pending",
rawInput: event.args,
};
const locations = extractToolLocations(event.args, options.cwd);
if (locations.length > 0) {
update.locations = locations;
}
toolName: event.toolName,
args: event.args,
intent: event.intent,
cwd: options.cwd,
});
return [toSessionNotification(sessionId, update)];
}
case "tool_execution_update": {
@@ -312,6 +308,45 @@ function mapTodoStatus(status: TodoStatus): "pending" | "in_progress" | "complet
return todoStatusMap[status];
}
export function buildToolCallStartUpdate(input: {
toolCallId: string;
toolName: string;
args: unknown;
intent?: string;
cwd?: string;
status?: "pending" | "completed";
}): SessionUpdate {
const update: ToolCall & { sessionUpdate: "tool_call" } = {
sessionUpdate: "tool_call",
toolCallId: input.toolCallId,
title: buildToolTitle(input.toolName, input.args, input.intent),
kind: mapToolKind(input.toolName),
status: input.status ?? "pending",
rawInput: input.args,
};
const content = buildToolStartContent(input.toolName, input.args);
if (content.length > 0) {
update.content = content;
}
const locations = extractToolLocations(input.args, input.cwd);
if (locations.length > 0) {
update.locations = locations;
}
return update;
}
function buildToolStartContent(toolName: string, args: unknown): ToolCallContent[] {
if (!isCommandToolName(toolName)) {
return [];
}
const command = extractStringProperty<CommandContainer>(args, "command");
return command ? [textToolCallContent(`$ ${command}`)] : [];
}
function isCommandToolName(toolName: string): boolean {
return toolName === "bash" || toolName === "shell" || toolName === "exec";
}
function buildToolTitle(toolName: string, args: unknown, intent: string | undefined): string {
const trimmedIntent = intent?.trim();
if (trimmedIntent) {
@@ -1,4 +1,5 @@
import { describe, expect, it } from "bun:test";
import path from "node:path";
import type { SessionNotification } from "@agentclientprotocol/sdk";
import { zSessionNotification } from "@agentclientprotocol/sdk/dist/schema/zod.gen.js";
import { mapAgentSessionEventToAcpSessionUpdates } from "../src/modes/acp/acp-event-mapper";
@@ -241,6 +242,84 @@ describe("ACP event mapper", () => {
expect(update.sessionUpdate).toBe("tool_call_update");
expect(update.content).toEqual([{ type: "terminal", terminalId: "term-42" }]);
});
it("shows bash commands in visible tool call content", () => {
const updates = mapAgentSessionEventToAcpSessionUpdates(
{
type: "tool_execution_start",
toolCallId: "toolu_bash_1",
toolName: "bash",
args: { command: "npm run check", cwd: "/repo" },
} as AgentSessionEvent,
"session-1",
);
expect(updates).toHaveLength(1);
expectAcpNotifications(updates);
const update = updates[0]!.update as {
sessionUpdate: string;
toolCallId?: string;
title?: string;
kind?: string;
status?: string;
rawInput?: unknown;
content?: unknown;
};
expect(update.sessionUpdate).toBe("tool_call");
expect(update.toolCallId).toBe("toolu_bash_1");
expect(update.title).toBe("bash: npm run check");
expect(update.kind).toBe("execute");
expect(update.status).toBe("pending");
expect(update.rawInput).toEqual({ command: "npm run check", cwd: "/repo" });
expect(update.content).toEqual([{ type: "content", content: { type: "text", text: "$ npm run check" } }]);
});
it("does not add command text content to non-command tool starts", () => {
const updates = mapAgentSessionEventToAcpSessionUpdates(
{
type: "tool_execution_start",
toolCallId: "toolu_read_1",
toolName: "read",
args: { path: "README.md" },
} as AgentSessionEvent,
"session-1",
);
expect(updates).toHaveLength(1);
expectAcpNotifications(updates);
const update = updates[0]!.update as {
sessionUpdate: string;
title?: string;
kind?: string;
rawInput?: unknown;
locations?: { path: string }[];
content?: unknown;
};
expect(update.sessionUpdate).toBe("tool_call");
expect(update.title).toBe("read: README.md");
expect(update.kind).toBe("read");
expect(update.rawInput).toEqual({ path: "README.md" });
expect(update.locations).toEqual([{ path: "README.md" }]);
expect("content" in update).toBe(false);
});
it("resolves tool_execution_start locations against mapper cwd", () => {
const updates = mapAgentSessionEventToAcpSessionUpdates(
{
type: "tool_execution_start",
toolCallId: "toolu_read_cwd",
toolName: "read",
args: { path: "src/file.ts" },
} as AgentSessionEvent,
"session-1",
{ cwd: "/repo" },
);
expect(updates).toHaveLength(1);
expectAcpNotifications(updates);
const update = updates[0]!.update as { sessionUpdate: string; locations?: { path: string }[]; content?: unknown };
expect(update.sessionUpdate).toBe("tool_call");
expect(update.locations).toEqual([{ path: path.resolve("/repo", "src/file.ts") }]);
expect("content" in update).toBe(false);
});
it("emits distinct locations for move-style path arguments", () => {
const updates = mapAgentSessionEventToAcpSessionUpdates(
{