diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4a84f0216..0bcf24e69 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -7,6 +7,9 @@ ### Added - Added `externalThinking` setting for private scratchpad reasoning via the new `think` tool +### Fixed + +- Fixed `display.hideToolActivity` leaving TTSR rules, todo reminders, late diagnostics, launch completions, async completions, and tool warnings visible; all activity blocks now hide and reappear without discarding their mounted state. ## [17.2.13] - 2026-08-11 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 625b71de3..2b40162ec 100644 --- a/packages/coding-agent/src/modes/components/chat-transcript-builder.ts +++ b/packages/coding-agent/src/modes/components/chat-transcript-builder.ts @@ -18,6 +18,7 @@ import type { AdvisorMessageDetails } from "../../advisor"; import { COLLAB_PROMPT_MESSAGE_TYPE, type CollabPromptDetails } from "../../collab/protocol"; import { settings } from "../../config/settings"; import type { MessageRenderer } from "../../extensibility/extensions/types"; +import { LAUNCH_COMPLETION_MESSAGE_TYPE } from "../../session/launch-completion"; import { BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE, type CustomMessage, @@ -53,6 +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 { ToolExecutionComponent } from "./tool-execution"; import { TranscriptContainer } from "./transcript-container"; import { createUsageRowBlock } from "./usage-row"; @@ -464,16 +466,18 @@ export class ChatTranscriptBuilder { this.#todoSnapshot = pending; } } - #appendCustomMessage(message: Extract): void { if (!message.display) return; if (message.customType === "async-result") { - this.container.addChild(buildAsyncResultBlock(message)); + 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; @@ -501,6 +505,17 @@ export class ChatTranscriptBuilder { this.container.addChild(createAdvisorMessageCard(details, () => this.#expanded, theme)); return; } + if (message.customType === LAUNCH_COMPLETION_MESSAGE_TYPE) { + const messageComponent = new CustomMessageComponent( + message as CustomMessage, + this.deps.getMessageRenderer?.(message.customType), + ); + this.#trackExpandable(messageComponent); + const component = new ToolActivityContainer(messageComponent); + component.setToolActivityVisible(!settings.get("display.hideToolActivity")); + this.container.addChild(component); + return; + } if (message.customType === BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE) { this.container.addChild(createBackgroundTanDispatchBlock(message as CustomMessage)); return; diff --git a/packages/coding-agent/src/modes/components/late-diagnostics-message.ts b/packages/coding-agent/src/modes/components/late-diagnostics-message.ts index 4f2dfd8c7..ee766ef42 100644 --- a/packages/coding-agent/src/modes/components/late-diagnostics-message.ts +++ b/packages/coding-agent/src/modes/components/late-diagnostics-message.ts @@ -17,7 +17,7 @@ export interface LateDiagnosticsFile { */ export class LateDiagnosticsMessageComponent extends Container { #expanded = false; - + #toolActivityVisible = true; constructor(private readonly files: LateDiagnosticsFile[]) { super(); this.#rebuild(); @@ -29,6 +29,17 @@ export class LateDiagnosticsMessageComponent extends Container { this.#rebuild(); } + setToolActivityVisible(visible: boolean): void { + if (this.#toolActivityVisible === visible) return; + this.#toolActivityVisible = visible; + this.invalidate(); + } + + override render(width: number): readonly string[] { + if (!this.#toolActivityVisible) return []; + return super.render(width); + } + override invalidate(): void { super.invalidate(); this.#rebuild(); diff --git a/packages/coding-agent/src/modes/components/todo-reminder.ts b/packages/coding-agent/src/modes/components/todo-reminder.ts index 9dfb3bf4c..8716c79bd 100644 --- a/packages/coding-agent/src/modes/components/todo-reminder.ts +++ b/packages/coding-agent/src/modes/components/todo-reminder.ts @@ -10,6 +10,7 @@ import type { TodoItem } from "../../tools/todo"; */ export class TodoReminderComponent extends Container { #box: Box; + #toolActivityVisible = true; constructor( private readonly todos: TodoItem[], @@ -27,6 +28,17 @@ export class TodoReminderComponent extends Container { this.#rebuild(); } + setToolActivityVisible(visible: boolean): void { + if (this.#toolActivityVisible === visible) return; + this.#toolActivityVisible = visible; + this.invalidate(); + } + + override render(width: number): readonly string[] { + if (!this.#toolActivityVisible) return []; + return super.render(width); + } + #rebuild(): void { this.#box.clear(); diff --git a/packages/coding-agent/src/modes/components/tool-activity-container.ts b/packages/coding-agent/src/modes/components/tool-activity-container.ts new file mode 100644 index 000000000..0de88c858 --- /dev/null +++ b/packages/coding-agent/src/modes/components/tool-activity-container.ts @@ -0,0 +1,27 @@ +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 new file mode 100644 index 000000000..a127ae275 --- /dev/null +++ b/packages/coding-agent/src/modes/components/tool-activity-warning.ts @@ -0,0 +1,22 @@ +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 new file mode 100644 index 000000000..59bc632bd --- /dev/null +++ b/packages/coding-agent/src/modes/components/tool-activity.ts @@ -0,0 +1,11 @@ +import type { Component } 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"; +} diff --git a/packages/coding-agent/src/modes/components/ttsr-notification.ts b/packages/coding-agent/src/modes/components/ttsr-notification.ts index 2882da7bf..c5a670c40 100644 --- a/packages/coding-agent/src/modes/components/ttsr-notification.ts +++ b/packages/coding-agent/src/modes/components/ttsr-notification.ts @@ -16,6 +16,7 @@ export class TtsrNotificationComponent extends Container { #box: Box; #expanded = false; #rules: Rule[]; + #toolActivityVisible = true; constructor(rules: Rule[]) { super(); @@ -31,6 +32,17 @@ export class TtsrNotificationComponent extends Container { this.#rebuild(); } + setToolActivityVisible(visible: boolean): void { + if (this.#toolActivityVisible === visible) return; + this.#toolActivityVisible = visible; + this.invalidate(); + } + + override render(width: number): readonly string[] { + if (!this.#toolActivityVisible) return []; + return super.render(width); + } + /** Merge additional rules into this block (deduped by rule name). */ addRules(rules: Rule[]): void { let changed = false; diff --git a/packages/coding-agent/src/modes/controllers/event-controller.ts b/packages/coding-agent/src/modes/controllers/event-controller.ts index cafb37dfd..aa654303d 100644 --- a/packages/coding-agent/src/modes/controllers/event-controller.ts +++ b/packages/coding-agent/src/modes/controllers/event-controller.ts @@ -1624,18 +1624,18 @@ 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. `showWarning` 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. + // 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. // // This is the render boundary, not the persisted result: the stored // error stays full-fidelity for the transcript and for replays. const detail = textContent ? previewLine(sanitizeText(textContent), TRUNCATE_LENGTHS.LINE) : ""; - this.ctx.showWarning( + this.ctx.showToolActivityWarning( `Todo update failed${detail ? `: ${detail}` : ". Progress may be stale until todo succeeds."}`, ); } @@ -2019,15 +2019,16 @@ 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 { await this.ctx.reloadTodos(); } diff --git a/packages/coding-agent/src/modes/controllers/input-controller.ts b/packages/coding-agent/src/modes/controllers/input-controller.ts index b56074a21..da2c99d1c 100644 --- a/packages/coding-agent/src/modes/controllers/input-controller.ts +++ b/packages/coding-agent/src/modes/controllers/input-controller.ts @@ -10,8 +10,8 @@ import { AssistantMessageComponent } from "../../modes/components/assistant-mess import { extractImagePathFromText } from "../../modes/components/custom-editor"; import { ReadToolGroupComponent } from "../../modes/components/read-tool-group"; import { renderSegmentTrack } from "../../modes/components/segment-track"; -import { StrippedToolCallsPlaceholder } from "../../modes/components/stripped-tool-calls-placeholder"; 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,13 +1925,16 @@ export class InputController { } for (const child of this.ctx.chatContainer.children) { - if (child instanceof ToolExecutionComponent || child instanceof ReadToolGroupComponent) { - if (!this.ctx.hideToolActivity) child.setExpanded(false); + if (isToolActivityComponent(child)) { + if ( + !this.ctx.hideToolActivity && + (child instanceof ToolExecutionComponent || child instanceof ReadToolGroupComponent) + ) { + child.setExpanded(false); + } child.setToolActivityVisible(!this.ctx.hideToolActivity); } else if (child instanceof AssistantMessageComponent) { child.setToolResultImagesVisible(!this.ctx.hideToolActivity); - } else if (child instanceof StrippedToolCallsPlaceholder) { - child.setToolActivityVisible(!this.ctx.hideToolActivity); } } diff --git a/packages/coding-agent/src/modes/controllers/selector-controller.ts b/packages/coding-agent/src/modes/controllers/selector-controller.ts index 4fc046a4e..dc2161cfe 100644 --- a/packages/coding-agent/src/modes/controllers/selector-controller.ts +++ b/packages/coding-agent/src/modes/controllers/selector-controller.ts @@ -98,7 +98,7 @@ 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 { StrippedToolCallsPlaceholder } from "../components/stripped-tool-calls-placeholder"; +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,13 +495,13 @@ export class SelectorController { this.ctx.hideToolActivity = hidden; if (!hidden) this.ctx.toolOutputExpanded = false; for (const child of this.ctx.chatContainer.children) { - if (child instanceof ToolExecutionComponent || child instanceof ReadToolGroupComponent) { - if (!hidden) child.setExpanded(false); + if (isToolActivityComponent(child)) { + if (!hidden && (child instanceof ToolExecutionComponent || child instanceof ReadToolGroupComponent)) { + child.setExpanded(false); + } child.setToolActivityVisible(!hidden); } else if (child instanceof AssistantMessageComponent) { child.setToolResultImagesVisible(!hidden); - } else if (child instanceof StrippedToolCallsPlaceholder) { - child.setToolActivityVisible(!hidden); } } if (hidden) this.ctx.ui.clearInlineImages(); diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 0f9d54026..64e2f41c0 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -4406,6 +4406,10 @@ export class InteractiveMode implements InteractiveModeContext { this.#uiHelpers.showWarning(message); } + showToolActivityWarning(message: string): void { + this.#uiHelpers.showToolActivityWarning(message); + } + #handleLspStartupEvent(event: LspStartupEvent): void { this.#updateWelcomeLspServers(); diff --git a/packages/coding-agent/src/modes/types.ts b/packages/coding-agent/src/modes/types.ts index 287b43d4a..a6722b70b 100644 --- a/packages/coding-agent/src/modes/types.ts +++ b/packages/coding-agent/src/modes/types.ts @@ -272,6 +272,7 @@ export interface InteractiveModeContext { showPinnedError(message: string): void; clearPinnedError(): void; showWarning(message: string): void; + showToolActivityWarning(message: string): void; showNewVersionNotification(newVersion: string): void; clearEditor(): void; updatePendingMessagesDisplay(): void; 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 a91a956da..6f1a15217 100644 --- a/packages/coding-agent/src/modes/utils/transcript-render-helpers.ts +++ b/packages/coding-agent/src/modes/utils/transcript-render-helpers.ts @@ -16,6 +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 { TranscriptBlock } from "../components/transcript-container"; import { theme } from "../theme/theme"; @@ -27,7 +28,7 @@ type AssistantAgentMessage = Extract; * or a batch of them) as a transcript block of one "Background job completed" * row per job. */ -export function buildAsyncResultBlock(message: CustomOrHookMessage): TranscriptBlock { +export function buildAsyncResultBlock(message: CustomOrHookMessage): ToolActivityContainer { const details = ( message as CustomMessage<{ jobId?: string; @@ -63,7 +64,7 @@ export function buildAsyncResultBlock(message: CustomOrHookMessage): TranscriptB .join(" "); block.addChild(new Text(line, 1, 0)); } - return block; + return new ToolActivityContainer(block); } /** diff --git a/packages/coding-agent/src/modes/utils/ui-helpers.ts b/packages/coding-agent/src/modes/utils/ui-helpers.ts index 346b52d16..b0a584c99 100644 --- a/packages/coding-agent/src/modes/utils/ui-helpers.ts +++ b/packages/coding-agent/src/modes/utils/ui-helpers.ts @@ -32,6 +32,8 @@ 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 { ToolExecutionComponent } from "../../modes/components/tool-execution"; import { TranscriptBlock } from "../../modes/components/transcript-container"; import { createUsageRowBlock } from "../../modes/components/usage-row"; @@ -40,6 +42,7 @@ import { decodeStreamedToolArgs, streamingStringKeysForTool } from "../../modes/ import { materializeImageReferenceLinksSync } from "../../modes/image-references"; import { theme } from "../../modes/theme/theme"; import type { CompactionQueuedMessage, InteractiveModeContext, RenderSessionContextOptions } from "../../modes/types"; +import { LAUNCH_COMPLETION_MESSAGE_TYPE } from "../../session/launch-completion"; import { BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE, type CustomMessage, @@ -160,7 +163,9 @@ export class UiHelpers { case "custom": { if (message.display) { if (message.customType === "async-result") { - this.ctx.chatContainer.addChild(buildAsyncResultBlock(message)); + const component = buildAsyncResultBlock(message); + component.setToolActivityVisible(!this.ctx.hideToolActivity); + this.ctx.chatContainer.addChild(component); break; } if (message.customType === LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE) { @@ -171,6 +176,18 @@ 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; + } + if (message.customType === LAUNCH_COMPLETION_MESSAGE_TYPE) { + const messageComponent = new CustomMessageComponent( + message as CustomMessage, + this.ctx.viewSession.extensionRunner?.getMessageRenderer(message.customType), + ); + messageComponent.setExpanded(this.ctx.toolOutputExpanded); + const component = new ToolActivityContainer(messageComponent); + component.setToolActivityVisible(!this.ctx.hideToolActivity); this.ctx.chatContainer.addChild(component); break; } @@ -739,6 +756,12 @@ export class UiHelpers { const text = new Text(`Warning: ${warningMessage}`, 1, 0).setStyleFn(t => theme.fg("warning", t)); this.ctx.present([new Spacer(1), text]); } + showToolActivityWarning(warningMessage: string): void { + const text = new ToolActivityWarningComponent(`Warning: ${warningMessage}`).setStyleFn(t => + theme.fg("warning", t), + ); + this.ctx.present(new ToolActivityContainer([new Spacer(1), text])); + } showNewVersionNotification(newVersion: string): void { const block = new TranscriptBlock(); diff --git a/packages/coding-agent/test/event-controller-cursor-todo.test.ts b/packages/coding-agent/test/event-controller-cursor-todo.test.ts index ef67d2782..02879c4db 100644 --- a/packages/coding-agent/test/event-controller-cursor-todo.test.ts +++ b/packages/coding-agent/test/event-controller-cursor-todo.test.ts @@ -19,13 +19,13 @@ afterAll(() => { interface Fixture { ctx: InteractiveModeContext; controller: EventController; - showWarning: Mock; + showToolActivityWarning: Mock; /** Components the controller committed to the transcript, in order. */ blocks: unknown[]; } function createFixture(): Fixture { - const showWarning = vi.fn(); + const showToolActivityWarning = vi.fn(); const blocks: unknown[] = []; const ctx = { isInitialized: true, @@ -55,9 +55,9 @@ function createFixture(): Fixture { toolOutputExpanded: false, setTodos: vi.fn(), present: vi.fn(), - showWarning, + showToolActivityWarning, } as unknown as InteractiveModeContext; - return { ctx, controller: new EventController(ctx), showWarning, blocks }; + return { ctx, controller: new EventController(ctx), showToolActivityWarning, blocks }; } /** A cumulative `message_update` whose content carries the streamed todo toolCall block. */ @@ -106,8 +106,8 @@ describe("EventController + Cursor todo bridge", () => { todoFailure(`\u001b[31mrejected:\u001b[0m\tid 4\r\n\tconflicts with ${"x".repeat(400)}`), ); - expect(f.showWarning).toHaveBeenCalledTimes(1); - const message = f.showWarning.mock.calls[0]![0] as string; + expect(f.showToolActivityWarning).toHaveBeenCalledTimes(1); + const message = f.showToolActivityWarning.mock.calls[0]![0] as string; expect(message).not.toContain("\t"); expect(message).not.toContain("\n"); // ANSI and other C0/C1 controls reach the terminal verbatim through @@ -126,7 +126,9 @@ describe("EventController + Cursor todo bridge", () => { await f.controller.handleEvent(todoFailure("")); - expect(f.showWarning).toHaveBeenCalledWith("Todo update failed. Progress may be stale until todo succeeds."); + expect(f.showToolActivityWarning).toHaveBeenCalledWith( + "Todo update failed. Progress may be stale until todo succeeds.", + ); }); it("settles a card whose completion arrived before the streamed block created it", async () => { @@ -163,13 +165,13 @@ describe("EventController + Cursor todo bridge", () => { const f = createFixture(); await f.controller.handleEvent(todoFailure("boom")); - expect(f.showWarning).toHaveBeenCalledTimes(1); + expect(f.showToolActivityWarning).toHaveBeenCalledTimes(1); await f.controller.handleEvent(streamedTodoBlock("todo-1")); expect(f.blocks).toHaveLength(1); expect(f.ctx.pendingTools.size).toBe(0); - expect(f.showWarning).toHaveBeenCalledTimes(1); + expect(f.showToolActivityWarning).toHaveBeenCalledTimes(1); }); it("refreshes the panel exactly once when a successful completion is replayed", async () => { diff --git a/packages/coding-agent/test/modes/components/late-diagnostics-message.test.ts b/packages/coding-agent/test/modes/components/late-diagnostics-message.test.ts index 59248dc01..9d56b3d13 100644 --- a/packages/coding-agent/test/modes/components/late-diagnostics-message.test.ts +++ b/packages/coding-agent/test/modes/components/late-diagnostics-message.test.ts @@ -91,4 +91,21 @@ describe("LateDiagnosticsMessageComponent", () => { ]); expect(plain(component).trim()).toBe(""); }); + + it("hides and restores diagnostics without discarding the rendered block", () => { + const component = new LateDiagnosticsMessageComponent([ + { + path: "/abs/src/foo.ts", + summary: "1 error(s)", + errored: true, + messages: ["src/foo.ts:1:1 [error] [typescript] bad (2322)"], + }, + ]); + + expect(plain(component)).toContain("Late diagnostics"); + component.setToolActivityVisible(false); + expect(plain(component)).toBe(""); + component.setToolActivityVisible(true); + expect(plain(component)).toContain("Late diagnostics"); + }); }); 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 new file mode 100644 index 000000000..e4097af48 --- /dev/null +++ b/packages/coding-agent/test/modes/components/tool-activity-visibility.test.ts @@ -0,0 +1,50 @@ +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 { 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"; + +const darkTheme = await getThemeByName("dark"); + +describe("tool activity visibility", () => { + beforeEach(() => { + if (!darkTheme) throw new Error("Failed to load dark theme"); + setThemeInstance(darkTheme); + }); + + it("hides and restores mounted activity blocks without losing their content", () => { + const rule: Rule = { + name: "ts-no-tiny-functions", + path: "/rules/ts-no-tiny-functions.md", + content: "Inline tiny wrappers.", + _source: { + provider: "test", + providerName: "Test", + path: "/rules/ts-no-tiny-functions.md", + 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; + + 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); + } + }); +}); 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 f96a7ebe2..76bc75537 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 @@ -436,6 +436,57 @@ describe("UiHelpers.renderInitialMessages — hidden tool activity", () => { expect(visibleRender).toContain(toolResultMarker); }); + it("hides and restores persisted internal activity blocks", () => { + const transcript = transcriptWith([ + { + role: "custom", + customType: "async-result", + content: "", + display: true, + details: { jobId: "ASYNC_JOB_MARKER", type: "bash", label: "async marker" }, + timestamp: 1, + }, + { + role: "custom", + customType: "lsp-late-diagnostic", + content: "", + display: true, + details: { + files: [ + { + path: "/tmp/internal.ts", + summary: "1 error(s)", + errored: true, + messages: ["internal.ts:1:1 [error] [typescript] LATE_DIAGNOSTIC_MARKER (2322)"], + }, + ], + }, + timestamp: 2, + }, + { + role: "custom", + customType: "launch-completion", + content: "LAUNCH_COMPLETION_MARKER", + display: true, + timestamp: 3, + }, + ]); + + const hidden = makeRenderCtx(transcript, true, true); + new UiHelpers(hidden.ctx).renderInitialMessages(); + const hiddenRender = Bun.stripANSI(hidden.chatContainer.render(120).join("\n")); + expect(hiddenRender).not.toContain("ASYNC_JOB_MARKER"); + expect(hiddenRender).not.toContain("LATE_DIAGNOSTIC_MARKER"); + expect(hiddenRender).not.toContain("LAUNCH_COMPLETION_MARKER"); + + const visible = makeRenderCtx(transcript, true, false); + new UiHelpers(visible.ctx).renderInitialMessages(); + const visibleRender = Bun.stripANSI(visible.chatContainer.render(120).join("\n")); + expect(visibleRender).toContain("ASYNC_JOB_MARKER"); + expect(visibleRender).toContain("LATE_DIAGNOSTIC_MARKER"); + expect(visibleRender).toContain("LAUNCH_COMPLETION_MARKER"); + }); + it("hides the stripped-tool-calls placeholder with tool activity and restores it on reveal", () => { const strippedAssistant: AgentMessage & StrippedToolCallsMarker = { role: "assistant",