From 469ff4a0ad9192a8b9f0fd257e33db42e1ce0795 Mon Sep 17 00:00:00 2001 From: roboomp Date: Wed, 1 Jul 2026 18:26:34 +0000 Subject: [PATCH] fix(tui): stopped async tool spinner ticks Excluded running async result snapshots from live partial spinner intervals so finalized background tool rows do not repaint scrollback. Added regression coverage for async bash snapshots staying static. --- .../src/modes/components/tool-execution.ts | 5 ++- .../components/tool-execution-spinner.test.ts | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/src/modes/components/tool-execution.ts b/packages/coding-agent/src/modes/components/tool-execution.ts index e7ebc6c7c..2aca3309f 100644 --- a/packages/coding-agent/src/modes/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/components/tool-execution.ts @@ -551,14 +551,13 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac */ #updateSpinnerAnimation(): void { // Live partial tool blocks stay repaintable until a terminal result seals - // them. Todo snapshots and detached background task progress are deliberate + // them. Todo snapshots and detached background tool progress are deliberate // static exceptions because their rows can be superseded or committed to // scrollback while later updates continue elsewhere. const isStreamingArgs = !this.#argsComplete && (isEditLikeToolName(this.#toolName) || this.#toolName === "write"); const isBackgroundAsyncRunning = (this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running"; - const isBackgroundAsyncTask = this.#toolName === "task" && isBackgroundAsyncRunning; - const isLivePartialTool = this.#isPartial && this.#toolName !== "todo" && !isBackgroundAsyncTask; + const isLivePartialTool = this.#isPartial && this.#toolName !== "todo" && !isBackgroundAsyncRunning; const needsSpinner = isStreamingArgs || isLivePartialTool || this.#displaceableByToolName === "job"; if (needsSpinner && !this.#spinnerInterval) { const frameCount = theme.spinnerFrames.length; diff --git a/packages/coding-agent/test/modes/components/tool-execution-spinner.test.ts b/packages/coding-agent/test/modes/components/tool-execution-spinner.test.ts index d667214ce..4fe760d61 100644 --- a/packages/coding-agent/test/modes/components/tool-execution-spinner.test.ts +++ b/packages/coding-agent/test/modes/components/tool-execution-spinner.test.ts @@ -69,4 +69,35 @@ describe("ToolExecutionComponent live preview spinners", () => { component.stopAnimation(); } }); + + it("does not tick detached async bash result snapshots", () => { + vi.useFakeTimers(); + const requestRender = vi.fn(); + const component = new ToolExecutionComponent( + "bash", + { command: "sleep 600", async: true }, + {}, + undefined, + { requestRender } as unknown as TUI, + process.cwd(), + ); + + try { + component.updateResult( + { + content: [{ type: "text", text: "started background job" }], + details: { + command: "sleep 600", + async: { state: "running", jobId: "job-1", type: "bash" }, + }, + }, + true, + ); + requestRender.mockClear(); + vi.advanceTimersByTime(500); + expect(requestRender).not.toHaveBeenCalled(); + } finally { + component.stopAnimation(); + } + }); });