fix(coding-agent): hide internal tool activity blocks

This commit is contained in:
Daniel Anderson-Little
2026-08-11 14:43:54 -04:00
parent 052095e156
commit 3a4ea6a376
19 changed files with 300 additions and 34 deletions
+3
View File
@@ -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
@@ -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<AgentMessage, { role: "custom" | "hookMessage" }>): 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<unknown>,
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<unknown>));
return;
@@ -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();
@@ -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();
@@ -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);
}
}
@@ -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);
}
}
@@ -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<ToolActivityComponent>).setToolActivityVisible === "function";
}
@@ -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;
@@ -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<AgentSessionEvent, { type: "todo_reminder" }>): Promise<void> {
const component = new TodoReminderComponent(event.todos, event.attempt, event.maxAttempts);
component.setToolActivityVisible(!this.ctx.hideToolActivity);
this.ctx.present(component);
}
async #handleTodoAutoClear(_event: Extract<AgentSessionEvent, { type: "todo_auto_clear" }>): Promise<void> {
await this.ctx.reloadTodos();
}
@@ -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);
}
}
@@ -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();
@@ -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();
+1
View File
@@ -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;
@@ -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<AgentMessage, { role: "assistant" }>;
* 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);
}
/**
@@ -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<unknown>,
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();
@@ -19,13 +19,13 @@ afterAll(() => {
interface Fixture {
ctx: InteractiveModeContext;
controller: EventController;
showWarning: Mock<InteractiveModeContext["showWarning"]>;
showToolActivityWarning: Mock<InteractiveModeContext["showToolActivityWarning"]>;
/** 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 () => {
@@ -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");
});
});
@@ -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);
}
});
});
@@ -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",