diff --git a/frontend/src/shell/AppShell.tsx b/frontend/src/shell/AppShell.tsx index 022b00ed..04c1c9e2 100644 --- a/frontend/src/shell/AppShell.tsx +++ b/frontend/src/shell/AppShell.tsx @@ -53,6 +53,8 @@ export function AppShell() { const ungroupedActive = activeList.filter((s) => !s.group); const archivedList = sessions.filter((s) => s.archived); const refresh = () => queryClient.invalidateQueries({ queryKey: ["sessions"] }); + const activeSession = sessions.find((s) => s.id === activeSessionId) ?? null; + const finalized = activeSession?.status === "finalized"; function openPanel(id: string) { const s = sessions.find((x) => x.id === id); @@ -169,7 +171,7 @@ export function AppShell() { {activeSessionId && (
- +
)} diff --git a/frontend/src/shell/SteerInput.tsx b/frontend/src/shell/SteerInput.tsx index 2abda292..6dd47327 100644 --- a/frontend/src/shell/SteerInput.tsx +++ b/frontend/src/shell/SteerInput.tsx @@ -25,6 +25,7 @@ export function SteerInput({ sessionId, onSessionCreated, onStop, inputRef }: Pr const [text, setText] = useState(""); const [busy, setBusy] = useState(false); const setLastUserEntry = useSessionStore((s) => s.setLastUserEntry); + const setPhase = useSessionStore((s) => s.setPhase); async function submit() { const trimmed = text.trim(); @@ -35,6 +36,7 @@ export function SteerInput({ sessionId, onSessionCreated, onStop, inputRef }: Pr await postSteer(sessionId, trimmed); } else { const { id } = await createSession({ question: trimmed }); + setPhase("F1"); // optimistic: paint F1 yellow during the cold start, before the first gate onSessionCreated?.(id); } setLastUserEntry({ kind: "input", text: trimmed }); diff --git a/frontend/src/shell/WorkflowBar.test.tsx b/frontend/src/shell/WorkflowBar.test.tsx index bcc60b10..59090edb 100644 --- a/frontend/src/shell/WorkflowBar.test.tsx +++ b/frontend/src/shell/WorkflowBar.test.tsx @@ -36,3 +36,17 @@ test("the active dot turns to the error state when phaseError flags it", () => { render(); expect(screen.getByTestId("phase-F3")).toHaveAttribute("data-state", "error"); }); + +test("currentPhase F1 renders F1 as running (yellow)", () => { + useSessionStore.getState().setPhase("F1"); + render(); + expect(screen.getByTestId("phase-F1")).toHaveAttribute("data-state", "running"); +}); + +test("finalized marks all phases done (green)", () => { + useSessionStore.getState().setPhase("F8"); + render(); + for (const id of ["F1", "F4", "F8"]) { + expect(screen.getByTestId(`phase-${id}`)).toHaveAttribute("data-state", "done"); + } +}); diff --git a/frontend/src/shell/WorkflowBar.tsx b/frontend/src/shell/WorkflowBar.tsx index 0439b041..9caed2b8 100644 --- a/frontend/src/shell/WorkflowBar.tsx +++ b/frontend/src/shell/WorkflowBar.tsx @@ -24,7 +24,7 @@ const DOT: Record = { pending: "bg-transparent text-muted-foreground/70 ring-border", }; -export function WorkflowBar() { +export function WorkflowBar({ finalized = false }: { finalized?: boolean }) { const currentPhase = useSessionStore((s) => s.currentPhase); const phaseError = useSessionStore((s) => s.phaseError); const activeIdx = PHASES.findIndex((p) => p.id === currentPhase); @@ -37,14 +37,16 @@ export function WorkflowBar() { {PHASES.map((p, i) => { const isActive = currentPhase === p.id; const isDone = activeIdx > -1 && i < activeIdx; - const state: DotState = isActive - ? phaseError === p.id - ? "error" - : "running" - : isDone - ? "done" - : "pending"; - const connectorDone = activeIdx > -1 && i <= activeIdx; + const state: DotState = finalized + ? "done" + : isActive + ? phaseError === p.id + ? "error" + : "running" + : isDone + ? "done" + : "pending"; + const connectorDone = finalized || (activeIdx > -1 && i <= activeIdx); return (
{i > 0 && ( diff --git a/frontend/src/shell/f1-loop.test.tsx b/frontend/src/shell/f1-loop.test.tsx index 3e9eba7b..57982c69 100644 --- a/frontend/src/shell/f1-loop.test.tsx +++ b/frontend/src/shell/f1-loop.test.tsx @@ -38,6 +38,9 @@ test("F1: create session -> widget via SSE -> respond -> POST /response", async // Wait for the session to be created and SSE to connect await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1)); + // Optimistic lifecycle: a brand-new question paints F1 immediately (before any gate). + await waitFor(() => expect(useSessionStore.getState().currentPhase).toBe("F1")); + // Simulate the backend emitting the F1 widget via a NAMED SSE event // (`event: ui_request`) — the same addEventListener path production uses. act(() =>