diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f057f2832..a740b4168 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -5,11 +5,17 @@ ### Added - Added `+name` and `-name` entries to `--tools`, which change the default tool selection instead of replacing it, for example `pi -t +codemode` +- Added `outputPad` to the tool render context ([#10557](https://github.com/earendil-works/pi/pull/10557) by [@rwachtler](https://github.com/rwachtler)) + +### Changed + +- Changed `outputPad` to also apply to `!` command output, tool output, and summary blocks ([#9946](https://github.com/earendil-works/pi/issues/9946), [#10557](https://github.com/earendil-works/pi/pull/10557) by [@rwachtler](https://github.com/rwachtler)) ### Fixed - Fixed managed installs keeping every old release; `pi update` now keeps only the new release and the one it updated from ([#10392](https://github.com/earendil-works/pi/issues/10392)) - Fixed standalone binaries loading `.env`, `.env.local`, and `.env.development` from the launch directory into Pi's environment ([#10473](https://github.com/earendil-works/pi/issues/10473)) +- Fixed `!!` command headers losing their dim color once output arrives ([#10557](https://github.com/earendil-works/pi/pull/10557) by [@rwachtler](https://github.com/rwachtler)) ## [1.0.4] - 2026-10-05 diff --git a/packages/coding-agent/docs/settings.md b/packages/coding-agent/docs/settings.md index 0a2e0d9a0..e7063179e 100644 --- a/packages/coding-agent/docs/settings.md +++ b/packages/coding-agent/docs/settings.md @@ -97,7 +97,7 @@ See [Compaction Reference](compaction.md) for trigger, summarization, and valida | `fullscreenCopyOnSelect` | boolean | `true` | Copy selected text automatically in fullscreen mode. | | `fullscreenWheelScrollLines` | `"auto"` \| number | `"auto"` | Lines per mouse-wheel event in fullscreen mode, from 1 to 100. `"auto"` moves one line per event in local macOS terminals, which already accelerate wheel and trackpad input; elsewhere, and over SSH, it speeds up fast wheel spins to at most 6 lines per event. Alt+wheel moves five times as far. | | `editorPaddingX` | number | `0` | Horizontal editor padding from 0 to 3 cells. | -| `outputPad` | `0 \| 1` | `1` | Horizontal transcript padding. | +| `outputPad` | `0 \| 1` | `1` | Horizontal transcript padding for messages, tool output, `!` command output, and summary blocks. | | `autocompleteMaxVisible` | number | `5` | Visible autocomplete entries, from 3 to 20. | | `showHardwareCursor` | boolean | `false` | Show the terminal cursor while Pi positions it for input methods. | | `terminal.showImages` | boolean | `true` | Display inline images when supported. | 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 abcf4cfd9..377de6415 100644 --- a/packages/coding-agent/src/core/export-html/tool-renderer.ts +++ b/packages/coding-agent/src/core/export-html/tool-renderer.ts @@ -92,6 +92,7 @@ export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRend expanded, showImages: false, isError, + outputPad: 1, }; }; diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index 941cfe8cb..8a4be6c27 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -489,6 +489,8 @@ export interface ToolRenderContext { showImages: boolean; /** Whether the current result is an error. */ isError: boolean; + /** Horizontal padding configured by the outputPad setting. Renderers with `renderShell: "self"` apply it themselves. */ + outputPad: number; } /** diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index 3c615c39b..2dd248eb4 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -170,7 +170,7 @@ export interface Settings { treeFilterMode?: "default" | "no-tools" | "user-only" | "labeled-only" | "all"; // Default filter when opening /tree thinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels editorPaddingX?: number; // Horizontal padding for input editor (default: 0) - outputPad?: 0 | 1; // Horizontal padding for chat message output (default: 1) + outputPad?: 0 | 1; // Horizontal padding for transcript content (default: 1) autocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5) showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME markdown?: MarkdownSettings; diff --git a/packages/coding-agent/src/core/tools/renderers/edit.ts b/packages/coding-agent/src/core/tools/renderers/edit.ts index 6db555671..f4d1df6dd 100644 --- a/packages/coding-agent/src/core/tools/renderers/edit.ts +++ b/packages/coding-agent/src/core/tools/renderers/edit.ts @@ -133,8 +133,10 @@ function buildEditCallComponent( args: RenderableEditArgs | undefined, theme: Theme, cwd: string, + outputPad: number, ): EditCallRenderComponent { component.setBgFn(getEditHeaderBg(component.preview, component.settledError, theme)); + component.setPaddingX(outputPad); component.clear(); component.addChild(new Text(formatEditCall(args, theme, cwd), 0, 0)); @@ -192,7 +194,13 @@ export const editRenderers: Pick, "renderCall" | "rende }); } - return buildEditCallComponent(component, args as RenderableEditArgs | undefined, theme, context.cwd); + return buildEditCallComponent( + component, + args as RenderableEditArgs | undefined, + theme, + context.cwd, + context.outputPad, + ); }, renderResult(result, _options, theme, context) { const callComponent = context.state.callComponent; @@ -215,7 +223,13 @@ export const editRenderers: Pick, "renderCall" | "rende changed = true; } if (changed) { - buildEditCallComponent(callComponent, context.args as RenderableEditArgs | undefined, theme, context.cwd); + buildEditCallComponent( + callComponent, + context.args as RenderableEditArgs | undefined, + theme, + context.cwd, + context.outputPad, + ); } } @@ -232,7 +246,7 @@ export const editRenderers: Pick, "renderCall" | "rende return component; } component.addChild(new Spacer(1)); - component.addChild(new Text(output, 1, 0)); + component.addChild(new Text(output, context.outputPad, 0)); return component; }, }; diff --git a/packages/coding-agent/src/modes/interactive/components/bash-execution.ts b/packages/coding-agent/src/modes/interactive/components/bash-execution.ts index b46a9d0b9..2c789bf5b 100644 --- a/packages/coding-agent/src/modes/interactive/components/bash-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/bash-execution.ts @@ -28,14 +28,16 @@ export class BashExecutionComponent extends Container { private fullOutputPath?: string; private expanded = false; private contentContainer: Container; + /** `dim` marks `!!` commands, whose output is excluded from the model context. */ + private readonly colorKey: "dim" | "bashMode"; + private outputPad: number; - constructor(command: string, ui: TUI, excludeFromContext = false) { + constructor(command: string, ui: TUI, excludeFromContext = false, outputPad = 1) { super(); this.command = command; - - // Use dim border for excluded-from-context commands (!! prefix) - const colorKey = excludeFromContext ? "dim" : "bashMode"; - const borderColor = (str: string) => theme.fg(colorKey, str); + this.colorKey = excludeFromContext ? "dim" : "bashMode"; + this.outputPad = outputPad; + const borderColor = (str: string) => theme.fg(this.colorKey, str); // Add spacer this.addChild(new Spacer(1)); @@ -47,21 +49,17 @@ export class BashExecutionComponent extends Container { this.contentContainer = new Container(); this.addChild(this.contentContainer); - // Command header - const header = new Text(theme.fg(colorKey, theme.bold(`$ ${command}`)), 1, 0); - this.contentContainer.addChild(header); - - // Loader this.loader = new Loader( ui, - (spinner) => theme.fg(colorKey, spinner), + (spinner) => theme.fg(this.colorKey, spinner), (text) => theme.fg("muted", text), `Running... (${keyText("tui.select.cancel")} to cancel)`, // Plain text for loader ); - this.contentContainer.addChild(this.loader); // Bottom border this.addChild(new DynamicBorder(borderColor)); + + this.updateDisplay(); } /** @@ -72,6 +70,11 @@ export class BashExecutionComponent extends Container { this.updateDisplay(); } + setOutputPad(outputPad: number): void { + this.outputPad = outputPad; + this.updateDisplay(); + } + override invalidate(): void { super.invalidate(); this.updateDisplay(); @@ -135,7 +138,7 @@ export class BashExecutionComponent extends Container { this.contentContainer.clear(); // Command header - const header = new Text(theme.fg("bashMode", theme.bold(`$ ${this.command}`)), 1, 0); + const header = new Text(theme.fg(this.colorKey, theme.bold(`$ ${this.command}`)), this.outputPad, 0); this.contentContainer.addChild(header); // Output @@ -143,7 +146,7 @@ export class BashExecutionComponent extends Container { if (this.expanded) { // Show all lines const displayText = availableLines.map((line) => theme.fg("muted", line)).join("\n"); - this.contentContainer.addChild(new Text(`\n${displayText}`, 1, 0)); + this.contentContainer.addChild(new Text(`\n${displayText}`, this.outputPad, 0)); } else { // Use shared visual truncation utility with width-aware caching const styledOutput = previewLogicalLines.map((line) => theme.fg("muted", line)).join("\n"); @@ -153,7 +156,7 @@ export class BashExecutionComponent extends Container { this.contentContainer.addChild({ render: (width: number) => { if (cachedLines === undefined || cachedWidth !== width) { - const result = truncateToVisualLines(styledInput, PREVIEW_LINES, width, 1); + const result = truncateToVisualLines(styledInput, PREVIEW_LINES, width, this.outputPad); cachedLines = result.visualLines; cachedWidth = width; } @@ -199,7 +202,7 @@ export class BashExecutionComponent extends Container { } if (statusParts.length > 0) { - this.contentContainer.addChild(new Text(`\n${statusParts.join("\n")}`, 1, 0)); + this.contentContainer.addChild(new Text(`\n${statusParts.join("\n")}`, this.outputPad, 0)); } } } diff --git a/packages/coding-agent/src/modes/interactive/components/branch-summary-message.ts b/packages/coding-agent/src/modes/interactive/components/branch-summary-message.ts index 54979ca40..f0915bcf7 100644 --- a/packages/coding-agent/src/modes/interactive/components/branch-summary-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/branch-summary-message.ts @@ -12,8 +12,8 @@ export class BranchSummaryMessageComponent extends Box { private message: BranchSummaryMessage; private markdownTheme: MarkdownTheme; - constructor(message: BranchSummaryMessage, markdownTheme: MarkdownTheme = getMarkdownTheme()) { - super(1, 1, (t) => theme.bg("customMessageBg", t)); + constructor(message: BranchSummaryMessage, markdownTheme: MarkdownTheme = getMarkdownTheme(), outputPad = 1) { + super(outputPad, 1, (t) => theme.bg("customMessageBg", t)); this.message = message; this.markdownTheme = markdownTheme; this.updateDisplay(); @@ -24,6 +24,10 @@ export class BranchSummaryMessageComponent extends Box { this.updateDisplay(); } + setOutputPad(outputPad: number): void { + this.setPaddingX(outputPad); + } + override invalidate(): void { super.invalidate(); this.updateDisplay(); diff --git a/packages/coding-agent/src/modes/interactive/components/compaction-summary-message.ts b/packages/coding-agent/src/modes/interactive/components/compaction-summary-message.ts index dc5b9fbdb..883b6f250 100644 --- a/packages/coding-agent/src/modes/interactive/components/compaction-summary-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/compaction-summary-message.ts @@ -12,8 +12,8 @@ export class CompactionSummaryMessageComponent extends Box { private message: CompactionSummaryMessage; private markdownTheme: MarkdownTheme; - constructor(message: CompactionSummaryMessage, markdownTheme: MarkdownTheme = getMarkdownTheme()) { - super(1, 1, (t) => theme.bg("customMessageBg", t)); + constructor(message: CompactionSummaryMessage, markdownTheme: MarkdownTheme = getMarkdownTheme(), outputPad = 1) { + super(outputPad, 1, (t) => theme.bg("customMessageBg", t)); this.message = message; this.markdownTheme = markdownTheme; this.updateDisplay(); @@ -24,6 +24,10 @@ export class CompactionSummaryMessageComponent extends Box { this.updateDisplay(); } + setOutputPad(outputPad: number): void { + this.setPaddingX(outputPad); + } + override invalidate(): void { super.invalidate(); this.updateDisplay(); diff --git a/packages/coding-agent/src/modes/interactive/components/custom-entry.ts b/packages/coding-agent/src/modes/interactive/components/custom-entry.ts index 26444edbb..078ad9fca 100644 --- a/packages/coding-agent/src/modes/interactive/components/custom-entry.ts +++ b/packages/coding-agent/src/modes/interactive/components/custom-entry.ts @@ -13,11 +13,13 @@ export class CustomEntryComponent extends Container { private renderer: EntryRenderer; private customComponent?: Component; private _expanded = false; + private outputPad: number; - constructor(entry: CustomEntry, renderer: EntryRenderer) { + constructor(entry: CustomEntry, renderer: EntryRenderer, outputPad = 1) { super(); this.entry = entry; this.renderer = renderer; + this.outputPad = outputPad; this.rebuild(); } @@ -32,6 +34,11 @@ export class CustomEntryComponent extends Container { } } + setOutputPad(outputPad: number): void { + this.outputPad = outputPad; + this.rebuild(); + } + override invalidate(): void { super.invalidate(); this.rebuild(); @@ -46,7 +53,7 @@ export class CustomEntryComponent extends Container { component = this.renderer(this.entry, { expanded: this._expanded }, theme); } catch (error) { const message = error instanceof Error ? error.message : String(error); - const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text)); + const box = new Box(this.outputPad, 1, (text) => theme.bg("customMessageBg", text)); box.addChild(new Text(theme.fg("error", `[${this.entry.customType}] renderer failed: ${message}`), 0, 0)); component = box; } diff --git a/packages/coding-agent/src/modes/interactive/components/custom-message.ts b/packages/coding-agent/src/modes/interactive/components/custom-message.ts index b5ee92658..6c757e0dd 100644 --- a/packages/coding-agent/src/modes/interactive/components/custom-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/custom-message.ts @@ -87,6 +87,7 @@ export class CustomMessageComponent extends Container { // Default rendering uses our box this.addChild(this.box); this.box.clear(); + this.box.setPaddingX(this.outputPad); // Default rendering: label + content const label = theme.fg("customMessageLabel", `\x1b[1m[${this.message.customType}]\x1b[22m`); diff --git a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts index 22fe0f363..af42ab631 100644 --- a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts @@ -827,7 +827,7 @@ export class SettingsSelectorComponent extends Container { items.splice(editorPaddingIndex + 1, 0, { id: "output-padding", label: "Output padding", - description: "Horizontal padding for user messages, assistant messages, and thinking", + description: "Horizontal padding for messages, tool output, and command output", currentValue: String(config.outputPad), values: ["0", "1"], }); diff --git a/packages/coding-agent/src/modes/interactive/components/skill-invocation-message.ts b/packages/coding-agent/src/modes/interactive/components/skill-invocation-message.ts index 789a4dff1..55acf6928 100644 --- a/packages/coding-agent/src/modes/interactive/components/skill-invocation-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/skill-invocation-message.ts @@ -13,8 +13,8 @@ export class SkillInvocationMessageComponent extends Box { private skillBlock: ParsedSkillBlock; private markdownTheme: MarkdownTheme; - constructor(skillBlock: ParsedSkillBlock, markdownTheme: MarkdownTheme = getMarkdownTheme()) { - super(1, 1, (t) => theme.bg("customMessageBg", t)); + constructor(skillBlock: ParsedSkillBlock, markdownTheme: MarkdownTheme = getMarkdownTheme(), outputPad = 1) { + super(outputPad, 1, (t) => theme.bg("customMessageBg", t)); this.skillBlock = skillBlock; this.markdownTheme = markdownTheme; this.updateDisplay(); @@ -25,6 +25,10 @@ export class SkillInvocationMessageComponent extends Box { this.updateDisplay(); } + setOutputPad(outputPad: number): void { + this.setPaddingX(outputPad); + } + override invalidate(): void { super.invalidate(); this.updateDisplay(); 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 ac5615da7..f90719530 100644 --- a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts @@ -25,6 +25,7 @@ const FALLBACK_PREVIEW_LINES = 10; export interface ToolExecutionOptions { showImages?: boolean; imageWidthCells?: number; + outputPad?: number; } export class ToolExecutionComponent extends Container { @@ -46,6 +47,7 @@ export class ToolExecutionComponent extends Container { private expanded = false; private showImages: boolean; private imageWidthCells: number; + private outputPad: number; private isPartial = true; private toolDefinition?: ToolRenderers; private ui: TUI; @@ -75,6 +77,7 @@ export class ToolExecutionComponent extends Container { this.toolDefinition = toolDefinition; this.showImages = options.showImages ?? true; this.imageWidthCells = options.imageWidthCells ?? 60; + this.outputPad = options.outputPad ?? 1; this.ui = ui; this.cwd = cwd; @@ -130,6 +133,7 @@ export class ToolExecutionComponent extends Container { expanded: this.expanded, showImages: this.showImages, isError: this.result?.isError ?? false, + outputPad: this.outputPad, }; } @@ -196,6 +200,11 @@ export class ToolExecutionComponent extends Container { this.updateDisplay(); } + setOutputPad(outputPad: number): void { + this.outputPad = outputPad; + this.updateDisplay(); + } + setShowImages(show: boolean): void { this.showImages = show; this.updateDisplay(); @@ -267,6 +276,7 @@ export class ToolExecutionComponent extends Container { const renderContainer = this.getRenderShell() === "self" ? this.selfRenderContainer : this.contentBox; if (renderContainer instanceof Box) { renderContainer.setBgFn(bgFn); + renderContainer.setPaddingX(this.outputPad); } renderContainer.clear(); @@ -318,6 +328,7 @@ export class ToolExecutionComponent extends Container { } } else { this.contentText.setCustomBgFn(bgFn); + this.contentText.setPaddingX(this.outputPad); this.contentText.setText(this.formatToolExecution()); hasContent = true; } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 85fc86edc..0928f918f 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -3515,6 +3515,7 @@ export class InteractiveMode { { showImages: this.settingsManager.getShowImages(), imageWidthCells: this.settingsManager.getImageWidthCells(), + outputPad: this.outputPad, }, this.getRegisteredToolDefinition(content.name), this.ui, @@ -3593,6 +3594,7 @@ export class InteractiveMode { { showImages: this.settingsManager.getShowImages(), imageWidthCells: this.settingsManager.getImageWidthCells(), + outputPad: this.outputPad, }, this.getRegisteredToolDefinition(event.toolName), this.ui, @@ -3825,7 +3827,7 @@ export class InteractiveMode { if (!renderer) { return; } - const component = new CustomEntryComponent(entry, renderer); + const component = new CustomEntryComponent(entry, renderer, this.outputPad); component.setExpanded(this.toolOutputExpanded); if (!component.hasContent()) { return; @@ -3845,7 +3847,12 @@ export class InteractiveMode { private addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void { switch (message.role) { case "bashExecution": { - const component = new BashExecutionComponent(message.command, this.ui, message.excludeFromContext); + const component = new BashExecutionComponent( + message.command, + this.ui, + message.excludeFromContext, + this.outputPad, + ); if (message.output) { component.appendOutput(message.output); } @@ -3874,14 +3881,22 @@ export class InteractiveMode { } case "compactionSummary": { this.chatContainer.addChild(new Spacer(1)); - const component = new CompactionSummaryMessageComponent(message, this.getMarkdownThemeWithSettings()); + const component = new CompactionSummaryMessageComponent( + message, + this.getMarkdownThemeWithSettings(), + this.outputPad, + ); component.setExpanded(this.toolOutputExpanded); this.chatContainer.addChild(component); break; } case "branchSummary": { this.chatContainer.addChild(new Spacer(1)); - const component = new BranchSummaryMessageComponent(message, this.getMarkdownThemeWithSettings()); + const component = new BranchSummaryMessageComponent( + message, + this.getMarkdownThemeWithSettings(), + this.outputPad, + ); component.setExpanded(this.toolOutputExpanded); this.chatContainer.addChild(component); break; @@ -3900,6 +3915,7 @@ export class InteractiveMode { const component = new SkillInvocationMessageComponent( skillBlock, this.getMarkdownThemeWithSettings(), + this.outputPad, ); component.setExpanded(this.toolOutputExpanded); this.chatContainer.addChild(component); @@ -3996,6 +4012,7 @@ export class InteractiveMode { { showImages: this.settingsManager.getShowImages(), imageWidthCells: this.settingsManager.getImageWidthCells(), + outputPad: this.outputPad, }, this.getRegisteredToolDefinition(content.name), this.ui, @@ -5040,23 +5057,14 @@ export class InteractiveMode { onOutputPadChange: (padding) => { this.settingsManager.setOutputPad(padding); this.outputPad = padding; - if (this.streamingComponent || this.session.isStreaming) { - for (const child of this.chatContainer.children) { - if ( - child instanceof AssistantMessageComponent || - child instanceof CustomMessageComponent || - child instanceof UserMessageComponent - ) { + for (const container of [this.chatContainer, this.pendingMessagesContainer]) { + for (const child of container.children) { + if ("setOutputPad" in child && typeof child.setOutputPad === "function") { child.setOutputPad(padding); } } - if (this.streamingComponent) { - this.streamingComponent.setOutputPad(padding); - } - this.ui.requestRender(); - return; } - this.rebuildChatFromMessages(); + this.ui.requestRender(); }, onAutocompleteMaxVisibleChange: (maxVisible) => { this.settingsManager.setAutocompleteMaxVisible(maxVisible); @@ -6971,7 +6979,7 @@ export class InteractiveMode { const result = eventResult.result; // Create UI component for display - this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext); + this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext, this.outputPad); if (this.session.isStreaming) { this.pendingMessagesContainer.addChild(this.bashComponent); this.pendingBashComponents.push(this.bashComponent); @@ -6999,7 +7007,7 @@ export class InteractiveMode { // Normal execution path (possibly with custom operations) const isDeferred = this.session.isStreaming; - this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext); + this.bashComponent = new BashExecutionComponent(command, this.ui, excludeFromContext, this.outputPad); if (isDeferred) { // Show in pending area when agent is streaming diff --git a/packages/coding-agent/test/codemode-renderer.test.ts b/packages/coding-agent/test/codemode-renderer.test.ts index 99a221745..bd88c0c6f 100644 --- a/packages/coding-agent/test/codemode-renderer.test.ts +++ b/packages/coding-agent/test/codemode-renderer.test.ts @@ -26,6 +26,7 @@ function render( expanded, showImages: false, isError, + outputPad: 1, } satisfies ToolRenderContext; const component = codemodeRenderers.renderResult?.( result, diff --git a/packages/coding-agent/test/output-pad.test.ts b/packages/coding-agent/test/output-pad.test.ts new file mode 100644 index 000000000..6f1c3e699 --- /dev/null +++ b/packages/coding-agent/test/output-pad.test.ts @@ -0,0 +1,94 @@ +import { type Component, TuiMainScreen } from "@earendil-works/pi-tui"; +import { Type } from "typebox"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; +import { VirtualTerminal } from "../../tui/test/virtual-terminal.ts"; +import type { ToolDefinition } from "../src/core/extensions/types.ts"; +import { createEditToolDefinition } from "../src/core/tools/edit.ts"; +import { BashExecutionComponent } from "../src/modes/interactive/components/bash-execution.ts"; +import { CompactionSummaryMessageComponent } from "../src/modes/interactive/components/compaction-summary-message.ts"; +import { ToolExecutionComponent } from "../src/modes/interactive/components/tool-execution.ts"; +import { initTheme } from "../src/modes/interactive/theme/theme.ts"; +import { stripAnsi } from "../src/utils/ansi.ts"; + +const ui = new TuiMainScreen(new VirtualTerminal(80, 24)); + +const tool: ToolDefinition = { + name: "custom_tool", + label: "custom_tool", + description: "custom tool", + parameters: Type.Any(), + execute: async () => ({ content: [{ type: "text", text: "ok" }], details: {} }), +}; + +type OutputPaddedComponent = Component & { setOutputPad(outputPad: number): void }; + +/** Text lines without ANSI codes or trailing fill. Blank lines and full-width borders are skipped. */ +function renderLines(component: Component): string[] { + return component + .render(60) + .map((line) => stripAnsi(line).trimEnd()) + .filter((line) => /[\w$(]/.test(line)); +} + +function createTool(definition: ToolDefinition | undefined, outputPad: number): ToolExecutionComponent { + const component = new ToolExecutionComponent("custom_tool", "id", {}, { outputPad }, definition, ui, "/"); + component.updateResult({ content: [{ type: "text", text: "ok" }], isError: false }); + return component; +} + +const components: Array<{ name: string; create: (outputPad: number) => OutputPaddedComponent }> = [ + { + name: "bash execution", + create: (outputPad) => { + const component = new BashExecutionComponent("pwd", ui, false, outputPad); + component.appendOutput("/tmp"); + component.setComplete(1, false); + return component; + }, + }, + { name: "tool execution", create: (outputPad) => createTool(tool, outputPad) }, + { name: "tool execution without a definition", create: (outputPad) => createTool(undefined, outputPad) }, + { + name: "self-rendered edit result", + create: (outputPad) => { + const component = new ToolExecutionComponent( + "edit", + "id", + { path: "file.txt", edits: [{ oldText: "old", newText: "new" }] }, + { outputPad }, + createEditToolDefinition("/"), + ui, + "/", + ); + component.updateResult({ content: [{ type: "text", text: "Could not find old text" }], isError: true }); + return component; + }, + }, + { + name: "compaction summary", + create: (outputPad) => + new CompactionSummaryMessageComponent( + { role: "compactionSummary", summary: "summary", tokensBefore: 10, timestamp: 0 }, + undefined, + outputPad, + ), + }, +]; + +describe("outputPad", () => { + beforeAll(() => { + initTheme("dark"); + }); + + afterAll(() => { + ui.stop(); + }); + + test.each(components)("$name renders at outputPad 0 and 1", ({ create }) => { + const component = create(0); + const lines = renderLines(component); + expect(lines.filter((line) => line.startsWith(" "))).toEqual([]); + component.setOutputPad(1); + expect(renderLines(component)).toEqual(lines.map((line) => ` ${line}`)); + }); +}); diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index abe3e5795..9f7649d4e 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added `Box.setPaddingX(...)` and `Text.setPaddingX(...)` ([#10557](https://github.com/earendil-works/pi/pull/10557) by [@rwachtler](https://github.com/rwachtler)) + ## [1.0.4] - 2026-10-05 ## [1.0.3] - 2026-10-05 diff --git a/packages/tui/src/components/box.ts b/packages/tui/src/components/box.ts index c70bd6003..185c78c89 100644 --- a/packages/tui/src/components/box.ts +++ b/packages/tui/src/components/box.ts @@ -50,6 +50,11 @@ export class Box implements Component { // Don't invalidate here - we'll detect bgFn changes by sampling output } + setPaddingX(paddingX: number): void { + this.paddingX = paddingX; + this.invalidateCache(); + } + private invalidateCache(): void { this.cache = undefined; } diff --git a/packages/tui/src/components/text.ts b/packages/tui/src/components/text.ts index 2ce82e742..024748912 100644 --- a/packages/tui/src/components/text.ts +++ b/packages/tui/src/components/text.ts @@ -36,6 +36,11 @@ export class Text implements Component { this.cachedLines = undefined; } + setPaddingX(paddingX: number): void { + this.paddingX = paddingX; + this.invalidate(); + } + invalidate(): void { this.cachedText = undefined; this.cachedWidth = undefined;