From 7dad27e5f2eaa30f25992a6ae400519586e8b568 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 21 May 2026 12:03:55 +0200 Subject: [PATCH] fix(coding-agent): avoid duplicate bash truncation path closes #4819 --- packages/coding-agent/CHANGELOG.md | 4 ++ packages/coding-agent/src/core/tools/bash.ts | 12 ++++-- .../test/tool-execution-component.test.ts | 37 +++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 468c4d84..61f7ad6e 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed final bash tool cards to avoid rendering duplicate full-output truncation paths ([#4819](https://github.com/earendil-works/pi/issues/4819)). + ## [0.75.4] - 2026-05-20 ### New Features diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index f70c9470..b9e55142 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -200,7 +200,15 @@ function rebuildBashResultRenderComponent( const state = component.state; component.clear(); - const output = getTextOutput(result as any, showImages).trim(); + let output = getTextOutput(result as any, showImages).trim(); + const truncation = result.details?.truncation; + const fullOutputPath = result.details?.fullOutputPath; + if (!options.isPartial && truncation?.truncated && fullOutputPath && output.endsWith("]")) { + const footerStart = output.lastIndexOf("\n\n["); + if (footerStart !== -1 && output.slice(footerStart).includes(fullOutputPath)) { + output = output.slice(0, footerStart).trimEnd(); + } + } if (output) { const styledOutput = output @@ -236,8 +244,6 @@ function rebuildBashResultRenderComponent( } } - const truncation = result.details?.truncation; - const fullOutputPath = result.details?.fullOutputPath; if (truncation?.truncated || fullOutputPath) { const warnings: string[] = []; if (fullOutputPath) { diff --git a/packages/coding-agent/test/tool-execution-component.test.ts b/packages/coding-agent/test/tool-execution-component.test.ts index 54c75806..c227515b 100644 --- a/packages/coding-agent/test/tool-execution-component.test.ts +++ b/packages/coding-agent/test/tool-execution-component.test.ts @@ -123,6 +123,43 @@ describe("ToolExecutionComponent parity", () => { await promise; }); + test("bash renderer does not duplicate final full output truncation details", async () => { + const operations: BashOperations = { + exec: async (_command, _cwd, { onData }) => { + for (let i = 1; i <= 4000; i++) { + onData(Buffer.from(`line-${String(i).padStart(4, "0")}\n`)); + } + return { exitCode: 0 }; + }, + }; + const tool = createBashToolDefinition(process.cwd(), { operations }); + const result = await tool.execute( + "tool-bash-1b", + { command: "generate output" }, + undefined, + undefined, + {} as never, + ); + const component = new ToolExecutionComponent( + "bash", + "tool-bash-1b", + { command: "generate output" }, + {}, + tool, + createFakeTui(), + process.cwd(), + ); + component.setExpanded(true); + component.updateResult({ ...result, isError: false }, false); + + const rendered = stripAnsi(component.render(200).join("\n")); + expect(rendered.match(/Full output:/g)?.length ?? 0).toBe(1); + expect(rendered).toMatch(/line-4000[^\n]*\n[^\S\n]*\n \[Full output:/); + expect(rendered).not.toMatch(/line-4000[^\n]*\n[^\S\n]*\n[^\S\n]*\n \[Full output:/); + expect(rendered).toContain("Truncated: showing 2000 of 4001 lines"); + expect(rendered).not.toContain("[Showing lines 2002-4001 of 4001. Full output:"); + }); + test("does not duplicate built-in headers when passed the active built-in definition", () => { const component = new ToolExecutionComponent( "read",