From cbd7dc2e70f86d136dd258336431cb1eb3f1bb92 Mon Sep 17 00:00:00 2001 From: Daniel Anderson-Little Date: Tue, 11 Aug 2026 15:51:13 -0400 Subject: [PATCH] fix(coding-agent): centralize hidden activity state --- .../components/chat-transcript-builder.ts | 11 ++--- .../components/tool-activity-container.ts | 27 ----------- .../modes/components/tool-activity-warning.ts | 22 --------- .../src/modes/components/tool-activity.ts | 28 ++++++++++-- .../modes/components/transcript-container.ts | 16 +++++++ .../src/modes/controllers/event-controller.ts | 19 +++----- .../src/modes/controllers/input-controller.ts | 15 +++---- .../modes/controllers/selector-controller.ts | 9 ++-- .../src/modes/interactive-mode.ts | 1 + .../modes/utils/transcript-render-helpers.ts | 2 +- .../src/modes/utils/ui-helpers.ts | 14 +----- .../tool-activity-visibility.test.ts | 45 ++++++++++--------- .../utils/render-initial-messages.test.ts | 25 ++++++++--- 13 files changed, 108 insertions(+), 126 deletions(-) delete mode 100644 packages/coding-agent/src/modes/components/tool-activity-container.ts delete mode 100644 packages/coding-agent/src/modes/components/tool-activity-warning.ts diff --git a/packages/coding-agent/src/modes/components/chat-transcript-builder.ts b/packages/coding-agent/src/modes/components/chat-transcript-builder.ts index 2b40162ec..21d9e148c 100644 --- a/packages/coding-agent/src/modes/components/chat-transcript-builder.ts +++ b/packages/coding-agent/src/modes/components/chat-transcript-builder.ts @@ -54,7 +54,7 @@ import { EvalExecutionComponent } from "./eval-execution"; import { type LateDiagnosticsFile, LateDiagnosticsMessageComponent } from "./late-diagnostics-message"; import { groupedReadUsageCallIds, ReadToolGroupComponent, readArgsCollapseIntoGroup } from "./read-tool-group"; import { SkillMessageComponent } from "./skill-message"; -import { ToolActivityContainer } from "./tool-activity-container"; +import { ToolActivityContainer } from "./tool-activity"; import { ToolExecutionComponent } from "./tool-execution"; import { TranscriptContainer } from "./transcript-container"; import { createUsageRowBlock } from "./usage-row"; @@ -97,7 +97,9 @@ export class ChatTranscriptBuilder { #expandables: Array<{ setExpanded(expanded: boolean): void }> = []; #expanded = false; - constructor(private readonly deps: ChatTranscriptBuilderDeps) {} + constructor(private readonly deps: ChatTranscriptBuilderDeps) { + this.container.setToolActivityVisible(!settings.get("display.hideToolActivity")); + } /** Whether the transcript currently holds any rendered rows. */ get isEmpty(): boolean { @@ -194,7 +196,6 @@ export class ChatTranscriptBuilder { this.#readGroup = new ReadToolGroupComponent({ showContentPreview: settings.get("read.toolResultPreview"), }); - this.#readGroup.setToolActivityVisible(!settings.get("display.hideToolActivity")); this.#trackExpandable(this.#readGroup); this.container.addChild(this.#readGroup); } @@ -409,7 +410,6 @@ export class ChatTranscriptBuilder { this.deps.cwd, content.id, ); - component.setToolActivityVisible(!settings.get("display.hideToolActivity")); this.#trackExpandable(component); this.container.addChild(component); @@ -470,14 +470,12 @@ export class ChatTranscriptBuilder { if (!message.display) return; if (message.customType === "async-result") { const component = buildAsyncResultBlock(message); - component.setToolActivityVisible(!settings.get("display.hideToolActivity")); this.container.addChild(component); return; } if (message.customType === LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE) { const details = (message as CustomMessage<{ files?: LateDiagnosticsFile[] }>).details; const component = new LateDiagnosticsMessageComponent(details?.files ?? []); - component.setToolActivityVisible(!settings.get("display.hideToolActivity")); this.#trackExpandable(component); this.container.addChild(component); return; @@ -512,7 +510,6 @@ export class ChatTranscriptBuilder { ); this.#trackExpandable(messageComponent); const component = new ToolActivityContainer(messageComponent); - component.setToolActivityVisible(!settings.get("display.hideToolActivity")); this.container.addChild(component); return; } diff --git a/packages/coding-agent/src/modes/components/tool-activity-container.ts b/packages/coding-agent/src/modes/components/tool-activity-container.ts deleted file mode 100644 index 0de88c858..000000000 --- a/packages/coding-agent/src/modes/components/tool-activity-container.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { type Component, Container } from "@oh-my-pi/pi-tui"; -import type { ToolActivityComponent } from "./tool-activity"; - -/** Keeps model/tool activity mounted while making its rows reversible. */ -export class ToolActivityContainer extends Container implements ToolActivityComponent { - #visible = true; - - constructor(component: Component | Component[]) { - super(); - if (Array.isArray(component)) { - for (const child of component) this.addChild(child); - } else { - this.addChild(component); - } - } - - setToolActivityVisible(visible: boolean): void { - if (this.#visible === visible) return; - this.#visible = visible; - this.invalidate(); - } - - override render(width: number): readonly string[] { - if (!this.#visible) return []; - return super.render(width); - } -} diff --git a/packages/coding-agent/src/modes/components/tool-activity-warning.ts b/packages/coding-agent/src/modes/components/tool-activity-warning.ts deleted file mode 100644 index a127ae275..000000000 --- a/packages/coding-agent/src/modes/components/tool-activity-warning.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Text } from "@oh-my-pi/pi-tui"; -import type { ToolActivityComponent } from "./tool-activity"; - -/** A tool-originated warning that follows the reversible activity toggle. */ -export class ToolActivityWarningComponent extends Text implements ToolActivityComponent { - #visible = true; - - constructor(message: string) { - super(message, 1, 0); - } - - setToolActivityVisible(visible: boolean): void { - if (this.#visible === visible) return; - this.#visible = visible; - this.invalidate(); - } - - override render(width: number): readonly string[] { - if (!this.#visible) return []; - return super.render(width); - } -} diff --git a/packages/coding-agent/src/modes/components/tool-activity.ts b/packages/coding-agent/src/modes/components/tool-activity.ts index 59bc632bd..2c0386995 100644 --- a/packages/coding-agent/src/modes/components/tool-activity.ts +++ b/packages/coding-agent/src/modes/components/tool-activity.ts @@ -1,11 +1,33 @@ -import type { Component } from "@oh-my-pi/pi-tui"; +import { type Component, Container } from "@oh-my-pi/pi-tui"; -/** A transcript component whose rows belong to model/tool activity. */ export interface ToolActivityComponent { setToolActivityVisible(visible: boolean): void; } -/** Narrows mounted transcript children to the reversible tool-activity contract. */ export function isToolActivityComponent(component: Component): component is Component & ToolActivityComponent { return typeof (component as Partial).setToolActivityVisible === "function"; } + +export class ToolActivityContainer extends Container implements ToolActivityComponent { + #visible = true; + + constructor(component: Component | Component[]) { + super(); + if (Array.isArray(component)) { + for (const child of component) this.addChild(child); + } else { + this.addChild(component); + } + } + + setToolActivityVisible(visible: boolean): void { + if (this.#visible === visible) return; + this.#visible = visible; + this.invalidate(); + } + + override render(width: number): readonly string[] { + if (!this.#visible) return []; + return super.render(width); + } +} diff --git a/packages/coding-agent/src/modes/components/transcript-container.ts b/packages/coding-agent/src/modes/components/transcript-container.ts index e477bd179..8e4902c07 100644 --- a/packages/coding-agent/src/modes/components/transcript-container.ts +++ b/packages/coding-agent/src/modes/components/transcript-container.ts @@ -6,6 +6,7 @@ import { type RenderStablePrefix, type ViewportTailProvider, } from "@oh-my-pi/pi-tui"; +import { isToolActivityComponent } from "./tool-activity"; /** * A transcript block that is still mutating (a foreground tool awaiting its @@ -160,6 +161,7 @@ export class TranscriptContainer extends Container implements NativeScrollbackLiveRegion, NativeScrollbackCommittedRows, RenderStablePrefix, ViewportTailProvider { + #toolActivityVisible = true; // Bumped to retire every block segment at once (theme change / clear); a // segment is only reused when its stored generation matches. #generation = 0; @@ -182,6 +184,20 @@ export class TranscriptContainer // consumes the report and re-bases the baseline). Out-of-band renders // between engine frames lower it; they can never inflate it. #stableRowsFloor = 0; + override addChild(component: Component): void { + if (isToolActivityComponent(component)) component.setToolActivityVisible(this.#toolActivityVisible); + super.addChild(component); + } + + setToolActivityVisible(visible: boolean): void { + if (this.#toolActivityVisible === visible) return; + this.#toolActivityVisible = visible; + for (const child of this.children) { + if (isToolActivityComponent(child)) child.setToolActivityVisible(visible); + } + this.invalidate(); + } + override invalidate(): void { // Theme/global invalidation: retire every diff snapshot so stale styling // is not diffed against the recolored render. diff --git a/packages/coding-agent/src/modes/controllers/event-controller.ts b/packages/coding-agent/src/modes/controllers/event-controller.ts index aa654303d..b4decfac1 100644 --- a/packages/coding-agent/src/modes/controllers/event-controller.ts +++ b/packages/coding-agent/src/modes/controllers/event-controller.ts @@ -345,7 +345,6 @@ export class EventController { showContentPreview: this.ctx.settings.get("read.toolResultPreview"), }); group.setExpanded(this.ctx.toolOutputExpanded); - group.setToolActivityVisible(!this.ctx.hideToolActivity); this.ctx.chatContainer.addChild(group); this.#lastReadGroup = group; } @@ -1131,7 +1130,6 @@ export class EventController { content.id, ); component.setExpanded(this.ctx.toolOutputExpanded); - component.setToolActivityVisible(!this.ctx.hideToolActivity); this.ctx.chatContainer.addChild(component); this.ctx.pendingTools.set(content.id, component); this.#toolTimelineComponents.set(content.id, component); @@ -1384,7 +1382,6 @@ export class EventController { ); component.setArgsComplete(event.toolCallId); component.setExpanded(this.ctx.toolOutputExpanded); - component.setToolActivityVisible(!this.ctx.hideToolActivity); this.ctx.chatContainer.addChild(component); this.ctx.pendingTools.set(event.toolCallId, component); this.#toolTimelineComponents.set(event.toolCallId, component); @@ -1624,13 +1621,13 @@ export class EventController { // This text can be a provider error copied verbatim off the wire (the // Cursor todo bridge forwards the server's string), so it may carry // ANSI escapes, other C0/C1 controls, tabs, newlines, or a line far - // wider than the terminal. `showToolActivityWarning` renders through - // a reversible activity component, so hiding tool activity does not - // lose this warning. `sanitizeText` drops the control sequences (and - // returns the same reference when there are none), then `previewLine` - // collapses the remaining whitespace and bounds the width. Sanitizing - // first matters: truncating before stripping can cut an escape - // mid-sequence and leave a dangling introducer. + // wider than the terminal. `showToolActivityWarning` renders through a + // plain `Text`, which strips none of that — an escape reaches the terminal + // and can repaint outside the row. `sanitizeText` drops the control + // sequences (and returns the same reference when there are none), + // then `previewLine` collapses the remaining whitespace and bounds + // the width. Sanitizing first matters: truncating before stripping + // can cut an escape mid-sequence and leave a dangling introducer. // // This is the render boundary, not the persisted result: the stored // error stays full-fidelity for the transcript and for replays. @@ -2019,14 +2016,12 @@ export class EventController { } const component = new TtsrNotificationComponent(event.rules); component.setExpanded(this.ctx.toolOutputExpanded); - component.setToolActivityVisible(!this.ctx.hideToolActivity); this.ctx.present(component); this.#lastTtsrNotification = component; } async #handleTodoReminder(event: Extract): Promise { const component = new TodoReminderComponent(event.todos, event.attempt, event.maxAttempts); - component.setToolActivityVisible(!this.ctx.hideToolActivity); this.ctx.present(component); } async #handleTodoAutoClear(_event: Extract): Promise { diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index da2c99d1c..fa59659ea 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -11,7 +11,6 @@ import { extractImagePathFromText } from "../../modes/components/custom-editor"; import { ReadToolGroupComponent } from "../../modes/components/read-tool-group"; import { renderSegmentTrack } from "../../modes/components/segment-track"; import { TinyTitleDownloadProgressComponent } from "../../modes/components/tiny-title-download-progress"; -import { isToolActivityComponent } from "../../modes/components/tool-activity"; import { ToolExecutionComponent } from "../../modes/components/tool-execution"; import { TreeSelectorComponent } from "../../modes/components/tree-selector"; import { expandEmoticons } from "../../modes/emoji-autocomplete"; @@ -1925,18 +1924,16 @@ export class InputController { } for (const child of this.ctx.chatContainer.children) { - if (isToolActivityComponent(child)) { - if ( - !this.ctx.hideToolActivity && - (child instanceof ToolExecutionComponent || child instanceof ReadToolGroupComponent) - ) { - child.setExpanded(false); - } - child.setToolActivityVisible(!this.ctx.hideToolActivity); + if ( + !this.ctx.hideToolActivity && + (child instanceof ToolExecutionComponent || child instanceof ReadToolGroupComponent) + ) { + child.setExpanded(false); } else if (child instanceof AssistantMessageComponent) { child.setToolResultImagesVisible(!this.ctx.hideToolActivity); } } + this.ctx.chatContainer.setToolActivityVisible(!this.ctx.hideToolActivity); if (this.ctx.hideToolActivity) this.ctx.ui.clearInlineImages(); this.ctx.ui.resetDisplay(); diff --git a/packages/coding-agent/src/modes/controllers/selector-controller.ts b/packages/coding-agent/src/modes/controllers/selector-controller.ts index dc2161cfe..0e6e67fd4 100644 --- a/packages/coding-agent/src/modes/controllers/selector-controller.ts +++ b/packages/coding-agent/src/modes/controllers/selector-controller.ts @@ -98,7 +98,6 @@ import { renderSegmentTrack } from "../components/segment-track"; import { SessionAccountSelectorComponent } from "../components/session-account-selector"; import { SessionSelectorComponent, type SessionSelectorOptions } from "../components/session-selector"; import { SettingsSelectorComponent } from "../components/settings-selector"; -import { isToolActivityComponent } from "../components/tool-activity"; import { ToolExecutionComponent } from "../components/tool-execution"; import { TranscriptBlock } from "../components/transcript-container"; import { TreeSelectorComponent } from "../components/tree-selector"; @@ -495,15 +494,13 @@ export class SelectorController { this.ctx.hideToolActivity = hidden; if (!hidden) this.ctx.toolOutputExpanded = false; for (const child of this.ctx.chatContainer.children) { - if (isToolActivityComponent(child)) { - if (!hidden && (child instanceof ToolExecutionComponent || child instanceof ReadToolGroupComponent)) { - child.setExpanded(false); - } - child.setToolActivityVisible(!hidden); + if (!hidden && (child instanceof ToolExecutionComponent || child instanceof ReadToolGroupComponent)) { + child.setExpanded(false); } else if (child instanceof AssistantMessageComponent) { child.setToolResultImagesVisible(!hidden); } } + this.ctx.chatContainer.setToolActivityVisible(!hidden); if (hidden) this.ctx.ui.clearInlineImages(); this.ctx.ui.resetDisplay(); break; diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 64e2f41c0..35e87686c 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -838,6 +838,7 @@ export class InteractiveMode implements InteractiveModeContext { this.editor.setTopBorderProvider(availableWidth => this.statusLine.getTopBorder(availableWidth)); this.hideToolActivity = settings.get("display.hideToolActivity"); + this.chatContainer.setToolActivityVisible(!this.hideToolActivity); this.hideThinkingBlock = settings.get("hideThinkingBlock"); this.proseOnlyThinking = settings.get("proseOnlyThinking"); diff --git a/packages/coding-agent/src/modes/utils/transcript-render-helpers.ts b/packages/coding-agent/src/modes/utils/transcript-render-helpers.ts index 6f1a15217..81cc58250 100644 --- a/packages/coding-agent/src/modes/utils/transcript-render-helpers.ts +++ b/packages/coding-agent/src/modes/utils/transcript-render-helpers.ts @@ -16,7 +16,7 @@ import { import { createIrcMessageCard } from "../../tools/hub"; import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils"; import { canonicalizeMessage } from "../../utils/thinking-display"; -import { ToolActivityContainer } from "../components/tool-activity-container"; +import { ToolActivityContainer } from "../components/tool-activity"; import { TranscriptBlock } from "../components/transcript-container"; import { theme } from "../theme/theme"; diff --git a/packages/coding-agent/src/modes/utils/ui-helpers.ts b/packages/coding-agent/src/modes/utils/ui-helpers.ts index b0a584c99..06033cc94 100644 --- a/packages/coding-agent/src/modes/utils/ui-helpers.ts +++ b/packages/coding-agent/src/modes/utils/ui-helpers.ts @@ -32,8 +32,7 @@ import { } from "../../modes/components/read-tool-group"; import { SkillMessageComponent } from "../../modes/components/skill-message"; import { StrippedToolCallsPlaceholder } from "../../modes/components/stripped-tool-calls-placeholder"; -import { ToolActivityContainer } from "../../modes/components/tool-activity-container"; -import { ToolActivityWarningComponent } from "../../modes/components/tool-activity-warning"; +import { ToolActivityContainer } from "../../modes/components/tool-activity"; import { ToolExecutionComponent } from "../../modes/components/tool-execution"; import { TranscriptBlock } from "../../modes/components/transcript-container"; import { createUsageRowBlock } from "../../modes/components/usage-row"; @@ -164,7 +163,6 @@ export class UiHelpers { if (message.display) { if (message.customType === "async-result") { const component = buildAsyncResultBlock(message); - component.setToolActivityVisible(!this.ctx.hideToolActivity); this.ctx.chatContainer.addChild(component); break; } @@ -176,7 +174,6 @@ export class UiHelpers { ).details; const component = new LateDiagnosticsMessageComponent(details?.files ?? []); component.setExpanded(this.ctx.toolOutputExpanded); - component.setToolActivityVisible(!this.ctx.hideToolActivity); this.ctx.chatContainer.addChild(component); break; } @@ -187,7 +184,6 @@ export class UiHelpers { ); messageComponent.setExpanded(this.ctx.toolOutputExpanded); const component = new ToolActivityContainer(messageComponent); - component.setToolActivityVisible(!this.ctx.hideToolActivity); this.ctx.chatContainer.addChild(component); break; } @@ -462,7 +458,6 @@ export class UiHelpers { showContentPreview: this.ctx.settings.get("read.toolResultPreview"), }); readGroup.setExpanded(this.ctx.toolOutputExpanded); - readGroup.setToolActivityVisible(!this.ctx.hideToolActivity); this.ctx.chatContainer.addChild(readGroup); } readGroup.updateArgs(content.arguments, content.id); @@ -477,7 +472,6 @@ export class UiHelpers { showContentPreview: this.ctx.settings.get("read.toolResultPreview"), }); readGroup.setExpanded(this.ctx.toolOutputExpanded); - readGroup.setToolActivityVisible(!this.ctx.hideToolActivity); this.ctx.chatContainer.addChild(readGroup); } readGroup.updateArgs(content.arguments, content.id); @@ -531,7 +525,6 @@ export class UiHelpers { content.id, ); component.setExpanded(this.ctx.toolOutputExpanded); - component.setToolActivityVisible(!this.ctx.hideToolActivity); this.ctx.chatContainer.addChild(component); if (hasErrorStop && errorMessage) { @@ -591,7 +584,6 @@ export class UiHelpers { showContentPreview: this.ctx.settings.get("read.toolResultPreview"), }); readGroup.setExpanded(this.ctx.toolOutputExpanded); - readGroup.setToolActivityVisible(!this.ctx.hideToolActivity); this.ctx.chatContainer.addChild(readGroup); } const args = readToolCallArgs.get(message.toolCallId); @@ -757,9 +749,7 @@ export class UiHelpers { this.ctx.present([new Spacer(1), text]); } showToolActivityWarning(warningMessage: string): void { - const text = new ToolActivityWarningComponent(`Warning: ${warningMessage}`).setStyleFn(t => - theme.fg("warning", t), - ); + const text = new Text(`Warning: ${warningMessage}`, 1, 0).setStyleFn(t => theme.fg("warning", t)); this.ctx.present(new ToolActivityContainer([new Spacer(1), text])); } diff --git a/packages/coding-agent/test/modes/components/tool-activity-visibility.test.ts b/packages/coding-agent/test/modes/components/tool-activity-visibility.test.ts index e4097af48..e1c45b48e 100644 --- a/packages/coding-agent/test/modes/components/tool-activity-visibility.test.ts +++ b/packages/coding-agent/test/modes/components/tool-activity-visibility.test.ts @@ -2,10 +2,11 @@ import { beforeEach, describe, expect, it } from "bun:test"; import { stripVTControlCharacters } from "node:util"; import type { Rule } from "@oh-my-pi/pi-coding-agent/capability/rule"; import { TodoReminderComponent } from "@oh-my-pi/pi-coding-agent/modes/components/todo-reminder"; -import { ToolActivityContainer } from "@oh-my-pi/pi-coding-agent/modes/components/tool-activity-container"; -import { ToolActivityWarningComponent } from "@oh-my-pi/pi-coding-agent/modes/components/tool-activity-warning"; +import { ToolActivityContainer } from "@oh-my-pi/pi-coding-agent/modes/components/tool-activity"; +import { TranscriptContainer } from "@oh-my-pi/pi-coding-agent/modes/components/transcript-container"; import { TtsrNotificationComponent } from "@oh-my-pi/pi-coding-agent/modes/components/ttsr-notification"; import { getThemeByName, setThemeInstance } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; +import { Text } from "@oh-my-pi/pi-tui"; const darkTheme = await getThemeByName("dark"); @@ -15,7 +16,7 @@ describe("tool activity visibility", () => { setThemeInstance(darkTheme); }); - it("hides and restores mounted activity blocks without losing their content", () => { + it("applies visibility to mounted and subsequently added activity blocks", () => { const rule: Rule = { name: "ts-no-tiny-functions", path: "/rules/ts-no-tiny-functions.md", @@ -27,24 +28,26 @@ describe("tool activity visibility", () => { level: "project", }, }; - const components = [ - { component: new TtsrNotificationComponent([rule]), text: "ts-no-tiny-functions" }, - { - component: new TodoReminderComponent([{ content: "finish the task", status: "in_progress" }], 1, 3), - text: "finish the task", - }, - { - component: new ToolActivityContainer(new ToolActivityWarningComponent("Warning: tool failed")), - text: "tool failed", - }, - ] as const; + const transcript = new TranscriptContainer(); + transcript.addChild(new TtsrNotificationComponent([rule])); + transcript.addChild(new TodoReminderComponent([{ content: "finish the task", status: "in_progress" }], 1, 3)); + transcript.addChild(new ToolActivityContainer(new Text("tool warning", 1, 0))); - for (const { component, text } of components) { - expect(stripVTControlCharacters(component.render(120).join("\n"))).toContain(text); - component.setToolActivityVisible(false); - expect(stripVTControlCharacters(component.render(120).join("\n"))).toBe(""); - component.setToolActivityVisible(true); - expect(stripVTControlCharacters(component.render(120).join("\n"))).toContain(text); - } + const visible = stripVTControlCharacters(transcript.render(120).join("\n")); + expect(visible).toContain("ts-no-tiny-functions"); + expect(visible).toContain("finish the task"); + expect(visible).toContain("tool warning"); + + transcript.setToolActivityVisible(false); + expect(stripVTControlCharacters(transcript.render(120).join("\n"))).toBe(""); + transcript.addChild(new ToolActivityContainer(new Text("late activity", 1, 0))); + expect(stripVTControlCharacters(transcript.render(120).join("\n"))).toBe(""); + + transcript.setToolActivityVisible(true); + const restored = stripVTControlCharacters(transcript.render(120).join("\n")); + expect(restored).toContain("ts-no-tiny-functions"); + expect(restored).toContain("finish the task"); + expect(restored).toContain("tool warning"); + expect(restored).toContain("late activity"); }); }); diff --git a/packages/coding-agent/test/modes/utils/render-initial-messages.test.ts b/packages/coding-agent/test/modes/utils/render-initial-messages.test.ts index 76bc75537..304310fcf 100644 --- a/packages/coding-agent/test/modes/utils/render-initial-messages.test.ts +++ b/packages/coding-agent/test/modes/utils/render-initial-messages.test.ts @@ -17,7 +17,7 @@ import type { AssistantMessage, ImageContent, Usage } from "@oh-my-pi/pi-ai"; import { kStreamingPartialJson } from "@oh-my-pi/pi-ai/utils/block-symbols"; import { resetSettingsForTest, Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { AssistantMessageComponent } from "@oh-my-pi/pi-coding-agent/modes/components/assistant-message"; -import { StrippedToolCallsPlaceholder } from "@oh-my-pi/pi-coding-agent/modes/components/stripped-tool-calls-placeholder"; +import { TranscriptContainer } from "@oh-my-pi/pi-coding-agent/modes/components/transcript-container"; import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types"; import { UiHelpers } from "@oh-my-pi/pi-coding-agent/modes/utils/ui-helpers"; @@ -142,8 +142,9 @@ function makeRenderCtx( transcript: SessionContext, showImages = true, hideToolActivity = false, -): { ctx: InteractiveModeContext; chatContainer: Container } { - const chatContainer = new Container(); +): { ctx: InteractiveModeContext; chatContainer: TranscriptContainer } { + const chatContainer = new TranscriptContainer(); + chatContainer.setToolActivityVisible(!hideToolActivity); let helpers: UiHelpers; const ctx = { chatContainer, @@ -157,6 +158,10 @@ function makeRenderCtx( updateEditorTopBorder: vi.fn(), ui: { requestRender: vi.fn(), imageBudget: undefined }, resetTranscript: () => chatContainer.clear(), + present: (content: Component | readonly Component[]) => { + const components = Array.isArray(content) ? content : [content]; + for (const component of components) chatContainer.addChild(component); + }, // Rebuild paths honor terminal.showImages since the native-image work; // keep it on so the image-replay contracts below stay meaningful. settings: { @@ -487,6 +492,16 @@ describe("UiHelpers.renderInitialMessages — hidden tool activity", () => { expect(visibleRender).toContain("LAUNCH_COMPLETION_MARKER"); }); + it("hides tool-originated warnings when activity is hidden", () => { + const hidden = makeRenderCtx(makeEmptyContext(), true, true); + new UiHelpers(hidden.ctx).showToolActivityWarning("TODO_WARNING_MARKER"); + expect(Bun.stripANSI(hidden.chatContainer.render(120).join("\n"))).not.toContain("TODO_WARNING_MARKER"); + + const visible = makeRenderCtx(makeEmptyContext(), true, false); + new UiHelpers(visible.ctx).showToolActivityWarning("TODO_WARNING_MARKER"); + expect(Bun.stripANSI(visible.chatContainer.render(120).join("\n"))).toContain("TODO_WARNING_MARKER"); + }); + it("hides the stripped-tool-calls placeholder with tool activity and restores it on reveal", () => { const strippedAssistant: AgentMessage & StrippedToolCallsMarker = { role: "assistant", @@ -508,9 +523,7 @@ describe("UiHelpers.renderInitialMessages — hidden tool activity", () => { ); // A live reveal must restore the placeholder without a transcript rebuild. - for (const child of hidden.chatContainer.children) { - if (child instanceof StrippedToolCallsPlaceholder) child.setToolActivityVisible(true); - } + hidden.chatContainer.setToolActivityVisible(true); expect(Bun.stripANSI(hidden.chatContainer.render(120).join("\n"))).toContain( "2 tool calls elided — no result on this branch", );