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.
This commit is contained in:
roboomp
2026-07-01 18:26:34 +00:00
parent 9e7814a638
commit 469ff4a0ad
2 changed files with 33 additions and 3 deletions
@@ -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;
@@ -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();
}
});
});