fix(coding-agent): hide internal tool activity blocks
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user