From 480d6bc62da39d99670905eaaabc36cf8b9b474f Mon Sep 17 00:00:00 2001 From: Aljosa Asanovic Date: Mon, 2 Mar 2026 10:22:41 -0500 Subject: [PATCH 1/2] fix(coding-agent): allow suppressing custom tool transcript blocks --- .../src/core/export-html/tool-renderer.ts | 6 ++ .../coding-agent/src/core/extensions/types.ts | 8 ++- .../interactive/components/tool-execution.ts | 33 ++++++++- .../test/tool-execution-component.test.ts | 72 +++++++++++++++++++ 4 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 packages/coding-agent/test/tool-execution-component.test.ts diff --git a/packages/coding-agent/src/core/export-html/tool-renderer.ts b/packages/coding-agent/src/core/export-html/tool-renderer.ts index 455dd0cc..d1b86d3c 100644 --- a/packages/coding-agent/src/core/export-html/tool-renderer.ts +++ b/packages/coding-agent/src/core/export-html/tool-renderer.ts @@ -49,6 +49,9 @@ export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRend } const component = toolDef.renderCall(args, theme); + if (!component) { + return undefined; + } const lines = component.render(width); return ansiLinesToHtml(lines); } catch { @@ -79,6 +82,9 @@ export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRend // Always render expanded, client-side will apply truncation const component = toolDef.renderResult(agentToolResult, { expanded: true, isPartial: false }, theme); + if (!component) { + return undefined; + } const lines = component.render(width); return ansiLinesToHtml(lines); } catch { diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index 97c1a295..66a8d6d5 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -352,10 +352,14 @@ export interface ToolDefinition>; /** Custom rendering for tool call display */ - renderCall?: (args: Static, theme: Theme) => Component; + renderCall?: (args: Static, theme: Theme) => Component | undefined; /** Custom rendering for tool result display */ - renderResult?: (result: AgentToolResult, options: ToolRenderResultOptions, theme: Theme) => Component; + renderResult?: ( + result: AgentToolResult, + options: ToolRenderResultOptions, + theme: Theme, + ) => Component | undefined; } // ============================================================================ diff --git a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts index 85de9fc2..8633b81c 100644 --- a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts @@ -95,6 +95,8 @@ export class ToolExecutionComponent extends Container { private convertedImages: Map = new Map(); // Incremental syntax highlighting cache for write tool call args private writeHighlightCache?: WriteHighlightCache; + // When true, this component intentionally renders no lines + private hideComponent = false; constructor( toolName: string, @@ -354,6 +356,13 @@ export class ToolExecutionComponent extends Container { this.updateDisplay(); } + override render(width: number): string[] { + if (this.hideComponent) { + return []; + } + return super.render(width); + } + private updateDisplay(): void { // Set background based on state const bgFn = this.isPartial @@ -362,8 +371,12 @@ export class ToolExecutionComponent extends Container { ? (text: string) => theme.bg("toolErrorBg", text) : (text: string) => theme.bg("toolSuccessBg", text); + const useBuiltInRenderer = this.shouldUseBuiltInRenderer(); + let customRendererHasContent = false; + this.hideComponent = false; + // Use built-in rendering for built-in tools (or overrides without custom renderers) - if (this.shouldUseBuiltInRenderer()) { + if (useBuiltInRenderer) { if (this.toolName === "bash") { // Bash uses Box with visual line truncation this.contentBox.setBgFn(bgFn); @@ -383,16 +396,19 @@ export class ToolExecutionComponent extends Container { if (this.toolDefinition.renderCall) { try { const callComponent = this.toolDefinition.renderCall(this.args, theme); - if (callComponent) { + if (callComponent !== undefined) { this.contentBox.addChild(callComponent); + customRendererHasContent = true; } } catch { // Fall back to default on error this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0)); + customRendererHasContent = true; } } else { // No custom renderCall, show tool name this.contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0)); + customRendererHasContent = true; } // Render result component if we have a result @@ -403,14 +419,16 @@ export class ToolExecutionComponent extends Container { { expanded: this.expanded, isPartial: this.isPartial }, theme, ); - if (resultComponent) { + if (resultComponent !== undefined) { this.contentBox.addChild(resultComponent); + customRendererHasContent = true; } } catch { // Fall back to showing raw output on error const output = this.getTextOutput(); if (output) { this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0)); + customRendererHasContent = true; } } } else if (this.result) { @@ -418,8 +436,13 @@ export class ToolExecutionComponent extends Container { const output = this.getTextOutput(); if (output) { this.contentBox.addChild(new Text(theme.fg("toolOutput", output), 0, 0)); + customRendererHasContent = true; } } + } else { + // Unknown tool with no registered definition - show generic fallback + this.contentText.setCustomBgFn(bgFn); + this.contentText.setText(this.formatToolExecution()); } // Handle images (same for both custom and built-in) @@ -463,6 +486,10 @@ export class ToolExecutionComponent extends Container { } } } + + if (!useBuiltInRenderer && this.toolDefinition) { + this.hideComponent = !customRendererHasContent && this.imageComponents.length === 0; + } } /** diff --git a/packages/coding-agent/test/tool-execution-component.test.ts b/packages/coding-agent/test/tool-execution-component.test.ts new file mode 100644 index 00000000..20dfd6ff --- /dev/null +++ b/packages/coding-agent/test/tool-execution-component.test.ts @@ -0,0 +1,72 @@ +import { Text, type TUI } from "@mariozechner/pi-tui"; +import { Type } from "@sinclair/typebox"; +import stripAnsi from "strip-ansi"; +import { beforeAll, describe, expect, test } from "vitest"; +import type { ToolDefinition } from "../src/core/extensions/types.js"; +import { ToolExecutionComponent } from "../src/modes/interactive/components/tool-execution.js"; +import { initTheme } from "../src/modes/interactive/theme/theme.js"; + +function createBaseToolDefinition(): ToolDefinition { + return { + name: "custom_tool", + label: "custom_tool", + description: "custom tool", + parameters: Type.Any(), + execute: async () => ({ + content: [{ type: "text", text: "ok" }], + details: {}, + }), + }; +} + +function createFakeTui(): TUI { + return { + requestRender: () => {}, + } as unknown as TUI; +} + +describe("ToolExecutionComponent custom renderer suppression", () => { + beforeAll(() => { + initTheme("dark"); + }); + + test("renders no lines when custom renderers return undefined", () => { + const toolDefinition: ToolDefinition = { + ...createBaseToolDefinition(), + renderCall: () => undefined, + renderResult: () => undefined, + }; + + const component = new ToolExecutionComponent("custom_tool", {}, {}, toolDefinition, createFakeTui()); + expect(component.render(120)).toEqual([]); + + component.updateResult( + { + content: [{ type: "text", text: "hidden" }], + details: {}, + isError: false, + }, + false, + ); + + expect(component.render(120)).toEqual([]); + }); + + test("keeps built-in tool rendering visible", () => { + const component = new ToolExecutionComponent("read", { path: "README.md" }, {}, undefined, createFakeTui()); + const rendered = stripAnsi(component.render(120).join("\n")); + expect(rendered).toContain("read"); + }); + + test("keeps custom tool rendering visible when renderer returns a component", () => { + const toolDefinition: ToolDefinition = { + ...createBaseToolDefinition(), + renderCall: () => new Text("custom call", 0, 0), + renderResult: () => undefined, + }; + + const component = new ToolExecutionComponent("custom_tool", {}, {}, toolDefinition, createFakeTui()); + const rendered = stripAnsi(component.render(120).join("\n")); + expect(rendered).toContain("custom call"); + }); +}); From b97310474bcc3056b3a3b1c890c180578fb7edb1 Mon Sep 17 00:00:00 2001 From: Aljosa Asanovic Date: Mon, 2 Mar 2026 10:22:51 -0500 Subject: [PATCH 2/2] fix(coding-agent): remove extra spacer before streaming tool blocks --- packages/coding-agent/src/modes/interactive/interactive-mode.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 91fc7214..6e448524 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -2109,7 +2109,6 @@ export class InteractiveMode { for (const content of this.streamingMessage.content) { if (content.type === "toolCall") { if (!this.pendingTools.has(content.id)) { - this.chatContainer.addChild(new Text("", 0, 0)); const component = new ToolExecutionComponent( content.name, content.arguments,