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(() =>