feat(workflow-bar): optimistic F1 at start; all-green when session finalized
This commit is contained in:
@@ -53,6 +53,8 @@ export function AppShell() {
|
|||||||
const ungroupedActive = activeList.filter((s) => !s.group);
|
const ungroupedActive = activeList.filter((s) => !s.group);
|
||||||
const archivedList = sessions.filter((s) => s.archived);
|
const archivedList = sessions.filter((s) => s.archived);
|
||||||
const refresh = () => queryClient.invalidateQueries({ queryKey: ["sessions"] });
|
const refresh = () => queryClient.invalidateQueries({ queryKey: ["sessions"] });
|
||||||
|
const activeSession = sessions.find((s) => s.id === activeSessionId) ?? null;
|
||||||
|
const finalized = activeSession?.status === "finalized";
|
||||||
|
|
||||||
function openPanel(id: string) {
|
function openPanel(id: string) {
|
||||||
const s = sessions.find((x) => x.id === id);
|
const s = sessions.find((x) => x.id === id);
|
||||||
@@ -169,7 +171,7 @@ export function AppShell() {
|
|||||||
{activeSessionId && (
|
{activeSessionId && (
|
||||||
<div className="shrink-0 border-b border-border/70 bg-background/80 px-6 py-2.5 backdrop-blur">
|
<div className="shrink-0 border-b border-border/70 bg-background/80 px-6 py-2.5 backdrop-blur">
|
||||||
<div className="mx-auto max-w-3xl">
|
<div className="mx-auto max-w-3xl">
|
||||||
<WorkflowBar />
|
<WorkflowBar finalized={finalized} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function SteerInput({ sessionId, onSessionCreated, onStop, inputRef }: Pr
|
|||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const setLastUserEntry = useSessionStore((s) => s.setLastUserEntry);
|
const setLastUserEntry = useSessionStore((s) => s.setLastUserEntry);
|
||||||
|
const setPhase = useSessionStore((s) => s.setPhase);
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
const trimmed = text.trim();
|
const trimmed = text.trim();
|
||||||
@@ -35,6 +36,7 @@ export function SteerInput({ sessionId, onSessionCreated, onStop, inputRef }: Pr
|
|||||||
await postSteer(sessionId, trimmed);
|
await postSteer(sessionId, trimmed);
|
||||||
} else {
|
} else {
|
||||||
const { id } = await createSession({ question: trimmed });
|
const { id } = await createSession({ question: trimmed });
|
||||||
|
setPhase("F1"); // optimistic: paint F1 yellow during the cold start, before the first gate
|
||||||
onSessionCreated?.(id);
|
onSessionCreated?.(id);
|
||||||
}
|
}
|
||||||
setLastUserEntry({ kind: "input", text: trimmed });
|
setLastUserEntry({ kind: "input", text: trimmed });
|
||||||
|
|||||||
@@ -36,3 +36,17 @@ test("the active dot turns to the error state when phaseError flags it", () => {
|
|||||||
render(<WorkflowBar />);
|
render(<WorkflowBar />);
|
||||||
expect(screen.getByTestId("phase-F3")).toHaveAttribute("data-state", "error");
|
expect(screen.getByTestId("phase-F3")).toHaveAttribute("data-state", "error");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("currentPhase F1 renders F1 as running (yellow)", () => {
|
||||||
|
useSessionStore.getState().setPhase("F1");
|
||||||
|
render(<WorkflowBar />);
|
||||||
|
expect(screen.getByTestId("phase-F1")).toHaveAttribute("data-state", "running");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("finalized marks all phases done (green)", () => {
|
||||||
|
useSessionStore.getState().setPhase("F8");
|
||||||
|
render(<WorkflowBar finalized />);
|
||||||
|
for (const id of ["F1", "F4", "F8"]) {
|
||||||
|
expect(screen.getByTestId(`phase-${id}`)).toHaveAttribute("data-state", "done");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const DOT: Record<DotState, string> = {
|
|||||||
pending: "bg-transparent text-muted-foreground/70 ring-border",
|
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 currentPhase = useSessionStore((s) => s.currentPhase);
|
||||||
const phaseError = useSessionStore((s) => s.phaseError);
|
const phaseError = useSessionStore((s) => s.phaseError);
|
||||||
const activeIdx = PHASES.findIndex((p) => p.id === currentPhase);
|
const activeIdx = PHASES.findIndex((p) => p.id === currentPhase);
|
||||||
@@ -37,14 +37,16 @@ export function WorkflowBar() {
|
|||||||
{PHASES.map((p, i) => {
|
{PHASES.map((p, i) => {
|
||||||
const isActive = currentPhase === p.id;
|
const isActive = currentPhase === p.id;
|
||||||
const isDone = activeIdx > -1 && i < activeIdx;
|
const isDone = activeIdx > -1 && i < activeIdx;
|
||||||
const state: DotState = isActive
|
const state: DotState = finalized
|
||||||
? phaseError === p.id
|
? "done"
|
||||||
? "error"
|
: isActive
|
||||||
: "running"
|
? phaseError === p.id
|
||||||
: isDone
|
? "error"
|
||||||
? "done"
|
: "running"
|
||||||
: "pending";
|
: isDone
|
||||||
const connectorDone = activeIdx > -1 && i <= activeIdx;
|
? "done"
|
||||||
|
: "pending";
|
||||||
|
const connectorDone = finalized || (activeIdx > -1 && i <= activeIdx);
|
||||||
return (
|
return (
|
||||||
<div key={p.id} className="flex items-center">
|
<div key={p.id} className="flex items-center">
|
||||||
{i > 0 && (
|
{i > 0 && (
|
||||||
|
|||||||
@@ -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
|
// Wait for the session to be created and SSE to connect
|
||||||
await waitFor(() => expect(FakeEventSource.instances).toHaveLength(1));
|
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
|
// Simulate the backend emitting the F1 widget via a NAMED SSE event
|
||||||
// (`event: ui_request`) — the same addEventListener path production uses.
|
// (`event: ui_request`) — the same addEventListener path production uses.
|
||||||
act(() =>
|
act(() =>
|
||||||
|
|||||||
Reference in New Issue
Block a user