Skip to content

Commit e5094de

Browse files
committed
🤖 refactor: clean up workspace status handoff rendering
Remove the mirrored timeout from the sidebar handoff slot and keep the cleanup tied to the CSS transition end. --- _Generated with `mux` • Model: `openai:gpt-5.4` • Thinking: `xhigh` • Cost: `$16.21`_ <!-- mux-attribution: model=openai:gpt-5.4 thinking=xhigh costs=16.21 -->
1 parent 9331413 commit e5094de

2 files changed

Lines changed: 19 additions & 24 deletions

File tree

‎src/browser/components/WorkspaceStatusIndicator/WorkspaceStatusIndicator.test.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import "../../../../tests/ui/dom";
22

33
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
4-
import { cleanup, render } from "@testing-library/react";
4+
import { cleanup, fireEvent, render } from "@testing-library/react";
55
import { installDom } from "../../../../tests/ui/dom";
66
import * as WorkspaceStoreModule from "@/browser/stores/WorkspaceStore";
77

@@ -142,6 +142,9 @@ describe("WorkspaceStatusIndicator", () => {
142142
expect(getPhaseSlot()?.getAttribute("class") ?? "").toContain("w-0");
143143
expect(getPhaseSlot()?.getAttribute("class") ?? "").toContain("mr-0");
144144
expect(getPhaseIcon()?.getAttribute("class") ?? "").not.toContain("animate-spin");
145+
fireEvent.transitionEnd(getPhaseSlot()!, { propertyName: "width" });
146+
147+
expect(getPhaseSlot()).toBeNull();
145148
expect(getModelDisplay()?.textContent ?? "").toContain(pendingDisplayName);
146149
expect(getModelDisplay()?.textContent ?? "").not.toContain(fallbackDisplayName);
147150
expect(view.container.textContent?.toLowerCase()).toContain("streaming");

‎src/browser/components/WorkspaceStatusIndicator/WorkspaceStatusIndicator.tsx‎

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,30 @@ export const WorkspaceStatusIndicator = memo<{
3131
const [isCollapsingPhaseSlot, setIsCollapsingPhaseSlot] = useState(false);
3232
const shouldCollapsePhaseSlot =
3333
isCollapsingPhaseSlot || (previousPhaseRef.current === "starting" && phase === "streaming");
34+
const showPhaseSlot = phase === "starting" || shouldCollapsePhaseSlot;
3435

3536
useEffect(() => {
3637
const previousPhase = previousPhaseRef.current;
3738
previousPhaseRef.current = phase;
3839

3940
if (previousPhase === "starting" && phase === "streaming") {
4041
setIsCollapsingPhaseSlot(true);
41-
const timeoutId = window.setTimeout(() => {
42-
setIsCollapsingPhaseSlot(false);
43-
}, 150);
44-
return () => window.clearTimeout(timeoutId);
42+
return;
4543
}
4644

47-
setIsCollapsingPhaseSlot(false);
45+
if (phase !== "streaming") {
46+
setIsCollapsingPhaseSlot(false);
47+
}
4848
}, [phase]);
4949

50+
// Let the CSS transition decide when the handoff slot can disappear so the JS logic
51+
// does not need a mirrored timeout that can drift from the rendered duration.
52+
const handlePhaseSlotTransitionEnd = () => {
53+
if (phase === "streaming" && isCollapsingPhaseSlot) {
54+
setIsCollapsingPhaseSlot(false);
55+
}
56+
};
57+
5058
// Show prompt when ask_user_question is pending - make it prominent
5159
if (awaitingUserQuestion) {
5260
return (
@@ -109,35 +117,19 @@ export const WorkspaceStatusIndicator = memo<{
109117
: (currentModel ?? pendingStreamModel ?? fallbackModel);
110118
const suffix = phase === "starting" ? "- starting..." : "- streaming...";
111119

112-
if (phase === "streaming" && !shouldCollapsePhaseSlot) {
113-
return (
114-
<div className="text-muted flex min-w-0 items-center gap-1.5 text-xs">
115-
{modelToShow ? (
116-
<>
117-
<span className="min-w-0 truncate">
118-
<ModelDisplay modelString={modelToShow} showTooltip={false} />
119-
</span>
120-
<span className="shrink-0 opacity-70">{suffix}</span>
121-
</>
122-
) : (
123-
<span className="min-w-0 truncate">Assistant - streaming...</span>
124-
)}
125-
</div>
126-
);
127-
}
128-
129120
return (
130121
<div className="text-muted flex min-w-0 items-center text-xs">
131122
{/* Keep the old steady-state layout, but hold the spinner slot just long enough to
132123
animate the start -> stream handoff instead of flashing the label left. */}
133-
{(phase === "starting" || shouldCollapsePhaseSlot) && (
124+
{showPhaseSlot && (
134125
<span
135126
className={
136127
phase === "starting"
137128
? "mr-1.5 inline-flex w-3 shrink-0 overflow-hidden opacity-100"
138129
: "mr-0 inline-flex w-0 shrink-0 overflow-hidden opacity-0 transition-[margin,width,opacity] duration-150 ease-out"
139130
}
140131
data-phase-slot
132+
onTransitionEnd={handlePhaseSlotTransitionEnd}
141133
>
142134
<Loader2
143135
aria-hidden="true"

0 commit comments

Comments
 (0)