feat(workflow-bar): processing-only elapsed timer + optimistic phase paint on resume

Timer now accumulates only while the harness works, pausing while a gate awaits
the reviewer and freezing when finalized (keyed per session, in-memory). Resume
now colours the re-entry phase immediately from the manifest's phase (1..8),
mirroring the F1 paint for new questions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 14:13:02 +02:00
co-authored by Claude Opus 4.8
parent e4ada07dab
commit 56ed8b29bd
5 changed files with 91 additions and 49 deletions
+19 -10
View File
@@ -15,12 +15,12 @@ import { Button } from "../components/ui/button";
import { Toaster } from "../components/ui/sonner";
import { toast } from "sonner";
import {
closeSession, listSessions, resumeSession,
closeSession, listSessions, resumeSession, getSession,
renameSession, setSessionGroup, archiveSession, unarchiveSession, deleteSession,
} from "../api/sessions";
import type { SessionSummary } from "../api/types";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useMemo, useRef, useState } from "react";
import { useMemo, useRef, useState } from "react";
/**
* The page is designed to live INSIDE the Omics Portal chrome (its left sidebar
@@ -55,12 +55,6 @@ export function AppShell() {
const refresh = () => queryClient.invalidateQueries({ queryKey: ["sessions"] });
const activeSession = sessions.find((s) => s.id === activeSessionId) ?? null;
const finalized = activeSession?.status === "finalized";
const [localStart, setLocalStart] = useState<string | null>(null);
useEffect(() => {
if (activeSessionId && !activeSession) setLocalStart((prev) => prev ?? new Date().toISOString());
else if (!activeSessionId) setLocalStart(null);
}, [activeSessionId, activeSession]);
const startedAt = activeSession?.created_at ?? localStart;
function openPanel(id: string) {
const s = sessions.find((x) => x.id === id);
@@ -78,6 +72,17 @@ export function AppShell() {
await resumeSession(id);
setPanelSession(null);
setActiveSessionId(id);
// Optimistic phase paint on resume: colour the re-entry phase immediately,
// before the first gate arrives (mirrors the F1 paint for new questions).
// The manifest's `phase` is the 1-based current phase (1..8).
try {
const m = (await getSession(id)) as { phase?: number };
if (typeof m.phase === "number" && m.phase >= 1 && m.phase <= 8) {
setPhase(`F${m.phase}`);
}
} catch {
/* non-fatal: the first gate will set the phase */
}
} catch {
toast.error("Failed to resume session.");
}
@@ -141,7 +146,11 @@ export function AppShell() {
// waiting on the human; once a widget appears, input is back in the user's court.
const pendingWidget = useSessionStore((s) => s.pendingWidget);
const resetSession = useSessionStore((s) => s.resetSession);
const setPhase = useSessionStore((s) => s.setPhase);
const working = Boolean(activeSessionId) && !pendingWidget;
// Processing time counts only while the harness works, not while a finalized
// session sits idle or a gate awaits the reviewer (pendingWidget).
const running = working && !finalized;
useSessionStream(activeSessionId);
@@ -179,8 +188,8 @@ export function AppShell() {
<div className="mx-auto max-w-3xl">
<WorkflowBar
finalized={finalized}
createdAt={startedAt}
updatedAt={activeSession?.updated_at ?? null}
running={running}
resetKey={activeSessionId}
/>
</div>
</div>