From b6362d5629d79776bcdc7dbc61aeab19d17e09d0 Mon Sep 17 00:00:00 2001 From: Xiangzhe Date: Sun, 2 Aug 2026 21:10:01 +0800 Subject: [PATCH] fix(ui): render Agent tool errors verbatim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use Pi’s public render context to distinguish invocation failures from structured Agent lifecycle results. Fall back to result text for missing or unknown details instead of inferring a max-turn abort. --- src/index.ts | 15 +++++-- test/agent-tool-error-rendering.test.ts | 59 +++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 test/agent-tool-error-rendering.test.ts diff --git a/src/index.ts b/src/index.ts index 8736dd23..8c28aab4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -969,10 +969,19 @@ Terse command-style prompts produce shallow, generic work. return new Text("▸ " + theme.fg("toolTitle", theme.bold(displayName)) + (desc ? " " + theme.fg("muted", desc) : ""), 0, 0); }, - renderResult(result, { expanded, isPartial }, theme) { + renderResult(result, { expanded, isPartial }, theme, renderContext) { const details = result.details as AgentDetails | undefined; - if (!details) { - const text = result.content[0]?.type === "text" ? result.content[0].text : ""; + const text = result.content[0]?.type === "text" ? result.content[0].text : ""; + const knownStatuses = new Set([ + "running", + "background", + "completed", + "steered", + "stopped", + "error", + "aborted", + ]); + if (renderContext.isError || !details || !knownStatuses.has(details.status)) { return new Text(text, 0, 0); } diff --git a/test/agent-tool-error-rendering.test.ts b/test/agent-tool-error-rendering.test.ts new file mode 100644 index 00000000..5558a68f --- /dev/null +++ b/test/agent-tool-error-rendering.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it, vi } from "vitest"; +import subagentsExtension from "../src/index.js"; + +function agentTool() { + const tools = new Map(); + const pi = { + registerMessageRenderer: vi.fn(), + registerTool: vi.fn((tool: any) => tools.set(tool.name, tool)), + registerCommand: vi.fn(), + on: vi.fn(), + events: { emit: vi.fn(), on: vi.fn(() => vi.fn()) }, + appendEntry: vi.fn(), + sendMessage: vi.fn(), + } as any; + subagentsExtension(pi); + return tools.get("Agent"); +} + +const theme = { + fg: (_color: string, text: string) => text, + bold: (text: string) => text, +} as any; + +function render(tool: any, result: any): string { + return tool.renderResult( + { content: result.content, details: result.details }, + { expanded: false, isPartial: false }, + theme, + { isError: result.isError }, + ).render(120).join("\n"); +} + +describe("Agent tool invocation error rendering", () => { + it("shows a Pi tool error instead of structured terminal status", () => { + const output = render(agentTool(), { + content: [{ type: "text", text: 'Cannot run with isolation: "worktree" — Git probe failed.' }], + isError: true, + details: { status: "aborted" }, + }); + + expect(output).toContain('Cannot run with isolation: "worktree" — Git probe failed.'); + expect(output).not.toContain("Aborted (max turns exceeded)"); + }); + + it.each([ + ["missing details", undefined], + ["empty details", {}], + ["unknown status", { status: "unknown" }], + ])("shows the real result text for %s", (_name, details) => { + const output = render(agentTool(), { + content: [{ type: "text", text: "Unstructured Agent result." }], + isError: false, + details, + }); + + expect(output).toContain("Unstructured Agent result."); + expect(output).not.toContain("Aborted (max turns exceeded)"); + }); +});