diff --git a/packages/coding-agent/src/modes/controllers/event-controller.ts b/packages/coding-agent/src/modes/controllers/event-controller.ts index b4decfac1..defd73ef8 100644 --- a/packages/coding-agent/src/modes/controllers/event-controller.ts +++ b/packages/coding-agent/src/modes/controllers/event-controller.ts @@ -1621,8 +1621,8 @@ 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 - // plain `Text`, which strips none of that — an escape reaches the terminal + // 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 @@ -1632,8 +1632,9 @@ export class EventController { // 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.showToolActivityWarning( + this.ctx.showWarning( `Todo update failed${detail ? `: ${detail}` : ". Progress may be stale until todo succeeds."}`, + { toolActivity: true }, ); } // Plan approval rides a `write` to xd://propose: the dispatch metadata on diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 35e87686c..2bb281390 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -4403,12 +4403,8 @@ export class InteractiveMode implements InteractiveModeContext { this.ui.requestRender(); } - showWarning(message: string): void { - this.#uiHelpers.showWarning(message); - } - - showToolActivityWarning(message: string): void { - this.#uiHelpers.showToolActivityWarning(message); + showWarning(message: string, options?: { toolActivity?: boolean }): void { + this.#uiHelpers.showWarning(message, options); } #handleLspStartupEvent(event: LspStartupEvent): void { diff --git a/packages/coding-agent/src/modes/types.ts b/packages/coding-agent/src/modes/types.ts index a6722b70b..b2997a25b 100644 --- a/packages/coding-agent/src/modes/types.ts +++ b/packages/coding-agent/src/modes/types.ts @@ -271,8 +271,7 @@ export interface InteractiveModeContext { showError(message: string): void; showPinnedError(message: string): void; clearPinnedError(): void; - showWarning(message: string): void; - showToolActivityWarning(message: string): void; + showWarning(message: string, options?: { toolActivity?: boolean }): void; showNewVersionNotification(newVersion: string): void; clearEditor(): void; updatePendingMessagesDisplay(): void; diff --git a/packages/coding-agent/src/modes/utils/ui-helpers.ts b/packages/coding-agent/src/modes/utils/ui-helpers.ts index 06033cc94..621e5520d 100644 --- a/packages/coding-agent/src/modes/utils/ui-helpers.ts +++ b/packages/coding-agent/src/modes/utils/ui-helpers.ts @@ -744,13 +744,10 @@ export class UiHelpers { this.ctx.present([new Spacer(1), text]); } - showWarning(warningMessage: string): void { + showWarning(warningMessage: string, options?: { toolActivity?: boolean }): void { 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 Text(`Warning: ${warningMessage}`, 1, 0).setStyleFn(t => theme.fg("warning", t)); - this.ctx.present(new ToolActivityContainer([new Spacer(1), text])); + const content = [new Spacer(1), text]; + this.ctx.present(options?.toolActivity ? new ToolActivityContainer(content) : content); } showNewVersionNotification(newVersion: string): void { 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 02879c4db..d555d5d0a 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; - showToolActivityWarning: Mock; + showWarning: Mock; /** Components the controller committed to the transcript, in order. */ blocks: unknown[]; } function createFixture(): Fixture { - const showToolActivityWarning = vi.fn(); + const showWarning = vi.fn(); const blocks: unknown[] = []; const ctx = { isInitialized: true, @@ -55,9 +55,9 @@ function createFixture(): Fixture { toolOutputExpanded: false, setTodos: vi.fn(), present: vi.fn(), - showToolActivityWarning, + showWarning, } as unknown as InteractiveModeContext; - return { ctx, controller: new EventController(ctx), showToolActivityWarning, blocks }; + return { ctx, controller: new EventController(ctx), showWarning, 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.showToolActivityWarning).toHaveBeenCalledTimes(1); - const message = f.showToolActivityWarning.mock.calls[0]![0] as string; + expect(f.showWarning).toHaveBeenCalledTimes(1); + const message = f.showWarning.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 @@ -117,6 +117,7 @@ describe("EventController + Cursor todo bridge", () => { // The prefix is ours and fixed; only the untrusted tail is bounded. expect(message.startsWith("Todo update failed: ")).toBe(true); expect(Bun.stringWidth(message.slice("Todo update failed: ".length))).toBeLessThanOrEqual(TRUNCATE_LENGTHS.LINE); + expect(f.showWarning.mock.calls[0]![1]).toEqual({ toolActivity: true }); }); it("keeps the standalone hint when the failure carries no text", async () => { @@ -126,9 +127,9 @@ describe("EventController + Cursor todo bridge", () => { await f.controller.handleEvent(todoFailure("")); - expect(f.showToolActivityWarning).toHaveBeenCalledWith( - "Todo update failed. Progress may be stale until todo succeeds.", - ); + expect(f.showWarning).toHaveBeenCalledWith("Todo update failed. Progress may be stale until todo succeeds.", { + toolActivity: true, + }); }); it("settles a card whose completion arrived before the streamed block created it", async () => { @@ -165,13 +166,13 @@ describe("EventController + Cursor todo bridge", () => { const f = createFixture(); await f.controller.handleEvent(todoFailure("boom")); - expect(f.showToolActivityWarning).toHaveBeenCalledTimes(1); + expect(f.showWarning).toHaveBeenCalledTimes(1); await f.controller.handleEvent(streamedTodoBlock("todo-1")); expect(f.blocks).toHaveLength(1); expect(f.ctx.pendingTools.size).toBe(0); - expect(f.showToolActivityWarning).toHaveBeenCalledTimes(1); + expect(f.showWarning).toHaveBeenCalledTimes(1); }); it("refreshes the panel exactly once when a successful completion is replayed", async () => { diff --git a/packages/coding-agent/test/event-controller-mixed-assistant-render.test.ts b/packages/coding-agent/test/event-controller-mixed-assistant-render.test.ts index 94515123e..867814b2e 100644 --- a/packages/coding-agent/test/event-controller-mixed-assistant-render.test.ts +++ b/packages/coding-agent/test/event-controller-mixed-assistant-render.test.ts @@ -53,6 +53,7 @@ function lineContaining(lines: string[], marker: string): number { function createFixture(hideToolActivity = false) { const chatContainer = new TranscriptContainer(); + chatContainer.setToolActivityVisible(!hideToolActivity); const pendingTools = new Map(); const ui = { requestRender: vi.fn(), diff --git a/packages/coding-agent/test/input-controller-keybindings.test.ts b/packages/coding-agent/test/input-controller-keybindings.test.ts index 4b668b5d5..eef9c416c 100644 --- a/packages/coding-agent/test/input-controller-keybindings.test.ts +++ b/packages/coding-agent/test/input-controller-keybindings.test.ts @@ -205,7 +205,7 @@ async function createContext() { hideToolActivity: false, toolOutputExpanded: false, settings: { set: vi.fn() }, - chatContainer: { children: [] }, + chatContainer: { children: [], setToolActivityVisible: vi.fn() }, handleHotkeysCommand: vi.fn(), handlePlanModeCommand: vi.fn(), handleClearCommand: vi.fn(), @@ -305,6 +305,7 @@ describe("InputController keybinding setup", () => { expect(ctx.settings.set).toHaveBeenCalledWith("display.hideToolActivity", true); expect(spies.clearInlineImages).toHaveBeenCalledTimes(1); expect(spies.resetDisplay).toHaveBeenCalledTimes(1); + expect(ctx.chatContainer.setToolActivityVisible).toHaveBeenCalledWith(false); }); it("does not mark pasted shell prompts as Python mode while editing", async () => { diff --git a/packages/coding-agent/test/modes/controllers/input-controller-tool-expansion.test.ts b/packages/coding-agent/test/modes/controllers/input-controller-tool-expansion.test.ts index 3422c0acb..630af6c1f 100644 --- a/packages/coding-agent/test/modes/controllers/input-controller-tool-expansion.test.ts +++ b/packages/coding-agent/test/modes/controllers/input-controller-tool-expansion.test.ts @@ -63,11 +63,12 @@ describe("InputController tool activity visibility", () => { const clearInlineImages = vi.fn(); const resetDisplay = vi.fn(); const showStatus = vi.fn(); + const setToolActivityVisible = vi.fn(); const ctx = { hideToolActivity: false, toolOutputExpanded: true, settings: { set }, - chatContainer: { children, clear, addChild }, + chatContainer: { children, clear, addChild, setToolActivityVisible }, rebuildChatFromMessages, showStatus, ui: { clearInlineImages, resetDisplay }, @@ -89,6 +90,7 @@ describe("InputController tool activity visibility", () => { expect(clearInlineImages.mock.invocationCallOrder[0]).toBeLessThan(resetDisplay.mock.invocationCallOrder[0]); expect(showStatus).toHaveBeenLastCalledWith("Tool activity: hidden"); expect(setToolResultImagesVisible).toHaveBeenLastCalledWith(false); + expect(setToolActivityVisible).toHaveBeenLastCalledWith(false); controller.toggleToolActivityVisibility(); @@ -103,5 +105,6 @@ describe("InputController tool activity visibility", () => { expect(resetDisplay).toHaveBeenCalledTimes(2); expect(showStatus).toHaveBeenLastCalledWith("Tool activity: visible"); expect(setToolResultImagesVisible).toHaveBeenLastCalledWith(true); + expect(setToolActivityVisible).toHaveBeenLastCalledWith(true); }); }); 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 304310fcf..b91aeedea 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 @@ -492,13 +492,17 @@ describe("UiHelpers.renderInitialMessages — hidden tool activity", () => { expect(visibleRender).toContain("LAUNCH_COMPLETION_MARKER"); }); - it("hides tool-originated warnings when activity is hidden", () => { + it("keeps normal warnings visible and hides warnings tagged as tool activity", () => { 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 hiddenHelpers = new UiHelpers(hidden.ctx); + hiddenHelpers.showWarning("NORMAL_WARNING_MARKER"); + hiddenHelpers.showWarning("TODO_WARNING_MARKER", { toolActivity: true }); + const hiddenRender = Bun.stripANSI(hidden.chatContainer.render(120).join("\n")); + expect(hiddenRender).toContain("NORMAL_WARNING_MARKER"); + expect(hiddenRender).not.toContain("TODO_WARNING_MARKER"); const visible = makeRenderCtx(makeEmptyContext(), true, false); - new UiHelpers(visible.ctx).showToolActivityWarning("TODO_WARNING_MARKER"); + new UiHelpers(visible.ctx).showWarning("TODO_WARNING_MARKER", { toolActivity: true }); expect(Bun.stripANSI(visible.chatContainer.render(120).join("\n"))).toContain("TODO_WARNING_MARKER"); }); diff --git a/packages/coding-agent/test/selector-settings-side-effects.test.ts b/packages/coding-agent/test/selector-settings-side-effects.test.ts index 3cff7a77d..82c220278 100644 --- a/packages/coding-agent/test/selector-settings-side-effects.test.ts +++ b/packages/coding-agent/test/selector-settings-side-effects.test.ts @@ -127,16 +127,13 @@ describe("selector setting side effects", () => { } for (const hidden of [true, false]) { - it(`applies display.hideToolActivity=${hidden} to existing tool components`, () => { - const setToolVisible = vi.fn(); + it(`delegates display.hideToolActivity=${hidden} to the transcript container`, () => { + const setToolActivityVisible = vi.fn(); const setToolExpanded = vi.fn(); const tool = Object.create(ToolExecutionComponent.prototype) as ToolExecutionComponent; - tool.setToolActivityVisible = setToolVisible; tool.setExpanded = setToolExpanded; - const setReadVisible = vi.fn(); const setReadExpanded = vi.fn(); const readGroup = Object.create(ReadToolGroupComponent.prototype) as ReadToolGroupComponent; - readGroup.setToolActivityVisible = setReadVisible; readGroup.setExpanded = setReadExpanded; const setToolResultImagesVisible = vi.fn(); const assistant = Object.create(AssistantMessageComponent.prototype) as AssistantMessageComponent; @@ -146,7 +143,7 @@ describe("selector setting side effects", () => { const ctx = { hideToolActivity: !hidden, toolOutputExpanded: true, - chatContainer: { children: [tool, readGroup, assistant] }, + chatContainer: { children: [tool, readGroup, assistant], setToolActivityVisible }, ui: { clearInlineImages, resetDisplay }, }; const controller = new SelectorController(ctx as unknown as InteractiveModeContext); @@ -154,8 +151,7 @@ describe("selector setting side effects", () => { controller.handleSettingChange("display.hideToolActivity", hidden); expect(ctx.hideToolActivity).toBe(hidden); - expect(setToolVisible).toHaveBeenCalledWith(!hidden); - expect(setReadVisible).toHaveBeenCalledWith(!hidden); + expect(setToolActivityVisible).toHaveBeenCalledWith(!hidden); expect(setToolResultImagesVisible).toHaveBeenCalledWith(!hidden); expect(setToolExpanded).toHaveBeenCalledTimes(hidden ? 0 : 1); expect(setReadExpanded).toHaveBeenCalledTimes(hidden ? 0 : 1);