diff --git a/packages/coding-agent/src/modes/acp/acp-event-mapper.ts b/packages/coding-agent/src/modes/acp/acp-event-mapper.ts index 725e86fae..bbad196c3 100644 --- a/packages/coding-agent/src/modes/acp/acp-event-mapper.ts +++ b/packages/coding-agent/src/modes/acp/acp-event-mapper.ts @@ -69,6 +69,16 @@ interface CommandContainer { command?: unknown; } +interface EvalCellContainer { + cells?: unknown; +} + +interface EvalCellLike { + language?: unknown; + title?: unknown; + code?: unknown; +} + interface PatternContainer { pattern?: unknown; } @@ -435,11 +445,43 @@ function getToolExecutionEndArgs( } function buildToolStartContent(toolName: string, args: unknown): ToolCallContent[] { - if (!isCommandToolName(toolName)) { - return []; + const text = buildToolStartText(toolName, args); + return text ? [textToolCallContent(text)] : []; +} + +function buildToolStartText(toolName: string, args: unknown): string | undefined { + if (isCommandToolName(toolName)) { + const command = extractStringProperty(args, "command"); + return command ? limitText(`$ ${command}`) : undefined; } - const command = extractStringProperty(args, "command"); - return command ? [textToolCallContent(`$ ${command}`)] : []; + if (toolName === "eval") { + return buildEvalStartText(args); + } + return undefined; +} + +function buildEvalStartText(args: unknown): string | undefined { + if (typeof args !== "object" || args === null || Array.isArray(args)) { + return undefined; + } + const cells = (args as EvalCellContainer).cells; + if (!Array.isArray(cells) || cells.length === 0) { + return undefined; + } + const lines: string[] = []; + for (const cell of cells) { + if (typeof cell !== "object" || cell === null || Array.isArray(cell)) { + continue; + } + const language = extractStringProperty(cell, "language") ?? "?"; + const title = extractStringProperty(cell, "title"); + const code = extractStringProperty(cell, "code"); + if (!code) { + continue; + } + lines.push(title ? `[${language}] ${title}` : `[${language}]`, code); + } + return lines.length > 0 ? limitText(lines.join("\n")) : undefined; } function mergeToolUpdateContent(startContent: ToolCallContent[], resultContent: ToolCallContent[]): ToolCallContent[] { @@ -465,6 +507,14 @@ function isCommandToolName(toolName: string): boolean { } function buildToolTitle(toolName: string, args: unknown, intent: string | undefined): string { + if (isCommandToolName(toolName)) { + const commandText = buildToolStartText(toolName, args); + if (commandText) return commandText; + } + if (toolName === "eval") { + const evalText = buildEvalStartText(args); + if (evalText) return evalText; + } const trimmedIntent = intent?.trim(); if (trimmedIntent) { return trimmedIntent; diff --git a/packages/coding-agent/test/acp-event-mapper.test.ts b/packages/coding-agent/test/acp-event-mapper.test.ts index 7a501060c..c6c10e1cb 100644 --- a/packages/coding-agent/test/acp-event-mapper.test.ts +++ b/packages/coding-agent/test/acp-event-mapper.test.ts @@ -188,6 +188,128 @@ describe("ACP event mapper", () => { expect(doneUpdates).toEqual([]); }); + it("preserves command text when a new command tool is started", () => { + const updates = mapAgentSessionEventToAcpSessionUpdates( + { + type: "tool_execution_start", + toolCallId: "tc-command-start", + toolName: "bash", + args: { command: "npm run check" }, + } as AgentSessionEvent, + "session-1", + ); + + expect(updates).toHaveLength(1); + expectAcpNotifications(updates); + const update = updates[0]!.update as { + sessionUpdate: string; + content?: Array<{ type: string; content?: { type: string; text?: string } }>; + }; + expect(update.sessionUpdate).toBe("tool_call"); + expect(update.content).toContainEqual({ type: "content", content: { type: "text", text: "$ npm run check" } }); + }); + + it("uses command text for a new command tool even when intent is generic", () => { + const updates = mapAgentSessionEventToAcpSessionUpdates( + { + type: "tool_execution_start", + toolCallId: "tc-command-start-generic-intent", + toolName: "bash", + args: { command: "echo hi" }, + intent: "Running command", + } as AgentSessionEvent, + "session-1", + ); + + expect(updates).toHaveLength(1); + expectAcpNotifications(updates); + const update = updates[0]!.update as { + title: string; + content?: Array<{ type: string; content?: { type: string; text?: string } }>; + }; + expect(update.title).toBe("$ echo hi"); + expect(update.content).toContainEqual({ type: "content", content: { type: "text", text: "$ echo hi" } }); + }); + + it("preserves eval source when a new eval tool is started", () => { + const updates = mapAgentSessionEventToAcpSessionUpdates( + { + type: "tool_execution_start", + toolCallId: "tc-eval-start", + toolName: "eval", + args: { cells: [{ language: "js", title: "sum", code: "return 1 + 1;" }] }, + intent: "sum", + } as AgentSessionEvent, + "session-1", + ); + + expect(updates).toHaveLength(1); + expectAcpNotifications(updates); + const update = updates[0]!.update as { + sessionUpdate: string; + title: string; + kind?: string; + status?: string; + rawInput?: unknown; + content?: Array<{ type: string; content?: { type: string; text?: string } }>; + }; + expect(update.sessionUpdate).toBe("tool_call"); + expect(update.title).toBe("[js] sum\nreturn 1 + 1;"); + expect(update.kind).toBe("execute"); + expect(update.status).toBe("pending"); + expect(update.rawInput).toEqual({ cells: [{ language: "js", title: "sum", code: "return 1 + 1;" }] }); + expect(update.content).toContainEqual({ + type: "content", + content: { type: "text", text: "[js] sum\nreturn 1 + 1;" }, + }); + }); + + it("builds eval source content from valid cells only", () => { + const updates = mapAgentSessionEventToAcpSessionUpdates( + { + type: "tool_execution_start", + toolCallId: "tc-eval-mixed-cells", + toolName: "eval", + args: { + cells: [null, {}, { code: "" }, { code: "x" }, { language: "py", code: "y" }], + }, + intent: "evaluating", + } as AgentSessionEvent, + "session-1", + ); + + expect(updates).toHaveLength(1); + expectAcpNotifications(updates); + const update = updates[0]!.update as { + title: string; + content?: Array<{ type: string; content?: { type: string; text?: string } }>; + }; + expect(update.title).toBe("[?]\nx\n[py]\ny"); + expect(update.content).toEqual([{ type: "content", content: { type: "text", text: "[?]\nx\n[py]\ny" } }]); + }); + + it("limits eval source before emitting visible tool-call text", () => { + const source = "x".repeat(4_100); + const updates = mapAgentSessionEventToAcpSessionUpdates( + { + type: "tool_execution_start", + toolCallId: "tc-eval-long-source", + toolName: "eval", + args: { cells: [{ language: "js", code: source }] }, + } as AgentSessionEvent, + "session-1", + ); + + expect(updates).toHaveLength(1); + expectAcpNotifications(updates); + const update = updates[0]!.update as { + title: string; + content?: Array<{ type: string; content?: { type: string; text?: string } }>; + }; + expect(update.title).toHaveLength(4_000); + expect(update.title.endsWith("…")).toBe(true); + expect(update.content).toEqual([{ type: "content", content: { type: "text", text: update.title } }]); + }); it("emits a diff ToolCallContent for each per-file edit result", () => { const updates = mapAgentSessionEventToAcpSessionUpdates( { @@ -547,7 +669,7 @@ describe("ACP event mapper", () => { }; expect(update.sessionUpdate).toBe("tool_call"); expect(update.toolCallId).toBe("toolu_bash_1"); - expect(update.title).toBe("bash: npm run check"); + expect(update.title).toBe("$ npm run check"); expect(update.kind).toBe("execute"); expect(update.status).toBe("pending"); expect(update.rawInput).toEqual({ command: "npm run check", cwd: "/repo" }); @@ -676,7 +798,7 @@ describe("ACP event mapper", () => { expect(update).toMatchObject({ sessionUpdate: "tool_call", toolCallId: "toolu_replay_1", - title: "bash: npm test", + title: "$ npm test", kind: "execute", status: "completed", rawInput: { command: "npm test", cwd: "/repo" }, @@ -741,7 +863,7 @@ describe("ACP event mapper", () => { expect(replayArgs.args).toBe(rawArgs); expectAcpStructure(zSessionNotification, { sessionId: "session-1", update }); expect(update).toMatchObject({ - title: "bash: bun test", + title: "$ bun test", status: "completed", rawInput: rawArgs, content: [{ type: "content", content: { type: "text", text: "$ bun test" } }],