Skip to content

Commit 957aa71

Browse files
authored
🤖 fix: stop idle todo status from spinning (#3066)
## Summary Stop animating the sidebar/workspace status `🔄` icon once a workspace is idle so unfinished TODO snapshots no longer look like an actively running turn. ## Background After the stream-end TODO nudge was removed, unfinished TODO-derived status can legitimately persist after a turn settles. Showing the refresh icon as a spinner in that idle state is misleading because it implies the agent is still actively working. ## Implementation - add a spin override in `WorkspaceStatusIndicator` so refresh-style status icons only animate while the workspace is streaming or starting - leave the underlying TODO-derived status text unchanged so the user can still see what was left unfinished - add a focused component test that covers both idle and active-stream rendering ## Validation - `bun test src/browser/components/WorkspaceStatusIndicator/WorkspaceStatusIndicator.test.tsx` - `make static-check` ## Risks Low. This is a small presentation-only change scoped to the workspace status indicator; it does not change TODO persistence or stream lifecycle state. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$2.12`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=2.12 -->
1 parent da87d6b commit 957aa71

File tree

2 files changed

+82
-1
lines changed

2 files changed

+82
-1
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import "../../../../tests/ui/dom";
2+
3+
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
4+
import { cleanup, render } from "@testing-library/react";
5+
import { installDom } from "../../../../tests/ui/dom";
6+
import * as WorkspaceStoreModule from "@/browser/stores/WorkspaceStore";
7+
8+
import { WorkspaceStatusIndicator } from "./WorkspaceStatusIndicator";
9+
10+
function mockSidebarState(
11+
overrides: Partial<WorkspaceStoreModule.WorkspaceSidebarState> = {}
12+
): void {
13+
spyOn(WorkspaceStoreModule, "useWorkspaceSidebarState").mockImplementation(() => ({
14+
canInterrupt: false,
15+
isStarting: false,
16+
awaitingUserQuestion: false,
17+
lastAbortReason: null,
18+
currentModel: null,
19+
recencyTimestamp: null,
20+
loadedSkills: [],
21+
skillLoadErrors: [],
22+
agentStatus: undefined,
23+
terminalActiveCount: 0,
24+
terminalSessionCount: 0,
25+
...overrides,
26+
}));
27+
}
28+
29+
describe("WorkspaceStatusIndicator", () => {
30+
let cleanupDom: (() => void) | null = null;
31+
32+
beforeEach(() => {
33+
cleanupDom = installDom();
34+
});
35+
36+
afterEach(() => {
37+
cleanup();
38+
cleanupDom?.();
39+
cleanupDom = null;
40+
mock.restore();
41+
});
42+
43+
test("keeps unfinished todo status static once the stream is idle", () => {
44+
mockSidebarState({
45+
agentStatus: { emoji: "🔄", message: "Run checks" },
46+
});
47+
48+
const view = render(
49+
<WorkspaceStatusIndicator workspaceId="workspace-idle" fallbackModel="openai:gpt-5.4" />
50+
);
51+
52+
const icon = view.container.querySelector("svg");
53+
expect(icon).toBeTruthy();
54+
expect(icon?.getAttribute("class") ?? "").not.toContain("animate-spin");
55+
});
56+
57+
test("keeps refresh-style status animated while a stream is still active", () => {
58+
mockSidebarState({
59+
canInterrupt: true,
60+
agentStatus: { emoji: "🔄", message: "Run checks" },
61+
});
62+
63+
const view = render(
64+
<WorkspaceStatusIndicator workspaceId="workspace-streaming" fallbackModel="openai:gpt-5.4" />
65+
);
66+
67+
const icon = view.container.querySelector("svg");
68+
expect(icon).toBeTruthy();
69+
expect(icon?.getAttribute("class") ?? "").toContain("animate-spin");
70+
});
71+
});

src/browser/components/WorkspaceStatusIndicator/WorkspaceStatusIndicator.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,20 @@ export const WorkspaceStatusIndicator = memo<{
2525
);
2626
}
2727

28+
// Todo-derived status can outlive the stream that produced it. Once the turn is idle,
29+
// keep refresh-style status icons static so unfinished work does not look actively running.
30+
const agentStatusSpinOverride = canInterrupt || isStarting || isCreating ? undefined : false;
31+
2832
if (agentStatus) {
2933
return (
3034
<div className="text-muted flex min-w-0 items-center gap-1.5 text-xs">
31-
{agentStatus.emoji && <EmojiIcon emoji={agentStatus.emoji} className="h-3 w-3 shrink-0" />}
35+
{agentStatus.emoji && (
36+
<EmojiIcon
37+
emoji={agentStatus.emoji}
38+
spin={agentStatusSpinOverride}
39+
className="h-3 w-3 shrink-0"
40+
/>
41+
)}
3242
<span className="min-w-0 truncate">{agentStatus.message}</span>
3343
{agentStatus.url && (
3444
<Tooltip>

0 commit comments

Comments
 (0)