diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 28401d53..a9f3c926 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -13,6 +13,7 @@ - Fixed SDK embedding in bundled Node apps failing with `ENOENT` when `package.json` is not present next to the bundle entrypoint. The package metadata reader now gracefully handles missing `package.json` by using defaults, enabling `createAgentSession()` without requiring package-adjacent files at runtime ([#5226](https://github.com/earendil-works/pi/issues/5226)). - Fixed HTTP timeout setting not being respected for non-Codex providers (e.g., llama.cpp via OpenAI-compatible API). The `httpIdleTimeoutMs` setting (set via `/settings` HTTP timeout) now applies as the default SDK request timeout for all providers that support it, not just OpenAI Codex Responses. Disabling the timeout (HTTP timeout = false) now correctly disables SDK timeouts for all supported providers by sending a maximum int32 value (effectively infinite) instead of 0, since SDKs treat timeout=0 as an immediate timeout ([#5294](https://github.com/earendil-works/pi/issues/5294)). - Fixed opening and listing very large JSONL session files by reading session entries line-by-line instead of materializing the full file as one string ([#5231](https://github.com/earendil-works/pi/issues/5231)). +- Fixed `renderShell: "self"` tool renderers that emit no component lines leaving a blank chat row ([#5299](https://github.com/earendil-works/pi/issues/5299)). ## [0.78.0] - 2026-05-29 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 b941dfc8..ad84f441 100644 --- a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts @@ -222,6 +222,31 @@ export class ToolExecutionComponent extends Container { if (this.hideComponent) { return []; } + + if (this.hasRendererDefinition() && this.getRenderShell() === "self") { + const contentLines = this.selfRenderContainer.render(width); + if (contentLines.length === 0 && this.imageComponents.length === 0) { + return []; + } + + const lines: string[] = []; + if (contentLines.length > 0) { + lines.push(""); + lines.push(...contentLines); + } + for (let i = 0; i < this.imageComponents.length; i++) { + const spacer = this.imageSpacers[i]; + if (spacer) { + lines.push(...spacer.render(width)); + } + const imageComponent = this.imageComponents[i]; + if (imageComponent) { + lines.push(...imageComponent.render(width)); + } + } + return lines; + } + return super.render(width); } diff --git a/packages/coding-agent/test/tool-execution-component.test.ts b/packages/coding-agent/test/tool-execution-component.test.ts index 7a4a813d..bf890959 100644 --- a/packages/coding-agent/test/tool-execution-component.test.ts +++ b/packages/coding-agent/test/tool-execution-component.test.ts @@ -67,6 +67,37 @@ describe("ToolExecutionComponent parity", () => { expect(rendered).toContain("custom result"); }); + test("self-rendered empty tool rows take no layout space", () => { + const toolDefinition: ToolDefinition = { + ...createBaseToolDefinition(), + renderShell: "self", + renderCall: () => new Text("", 0, 0), + renderResult: () => new Text("", 0, 0), + }; + + const component = new ToolExecutionComponent( + "custom_tool", + "tool-empty-self-render", + {}, + {}, + toolDefinition, + createFakeTui(), + process.cwd(), + ); + expect(component.render(120)).toEqual([]); + + component.updateResult( + { + content: [], + details: {}, + isError: false, + }, + false, + ); + + expect(component.render(120)).toEqual([]); + }); + test("uses built-in rendering for built-in overrides without custom renderers", () => { const overrideDefinition: ToolDefinition = { ...createBaseToolDefinition("edit"),