Problem
Per-turn latency is 3-30 seconds (playbook-heavy turns + regen attempts push this higher). The current loading UX is presumably a generic spinner — the operator has no signal of what's happening, which feels broken when a 25-second turn drags on.
The backend pipeline has clear phase boundaries that we can surface:
- Routing (intent + knowability + scope filter) — ~50ms
- Substrate fetch (catalog match + playbook match + RAG retrieve + tool calls) — 100-2000ms
- Model generation (vLLM streaming) — 2-20s
- Verification (provenance verifier runs) — ~10ms
- Regen attempt (if hard_failures) — repeats phases 3+4
A phase indicator turns dead time into useful signal.
Acceptance criteria
- Build a
<PhaseIndicator> component shown in place of the current spinner:
- 4-5 phase steps with icons (lucide-react:
Route, Database, Cpu, ShieldCheck)
- Current phase highlighted, completed phases checked, pending phases dimmed
- On regen, the indicator restarts from "Model generation" with a small "(attempt 2/3)" badge
- Phase transitions driven by either:
- Option A (simpler): time-based pseudo-progression — advance phases on a heuristic timer; not accurate but better than a spinner
- Option B (proper, preferred): backend SSE / WebSocket emits phase events; frontend listens. Requires backend change — coordinate with backend folks before going down this path.
- If Option B requires backend work, scope this issue to Option A and open a separate issue for the SSE plumbing
- Should be visually integrated with the chat message bubble (not a modal)
- Reduce-motion friendly (
prefers-reduced-motion: reduce → no animations, just static state)
Constraints
- No new dependencies — use existing lucide-react + Tailwind
- Keep it understated — this is operator UX, not a marketing animation
- Accessible —
aria-live="polite" so screen readers announce phase transitions
Pointers
- Current chat component:
frontend/src/components/chat/ (find the message-pending render)
- vLLM streaming is supported on the backend (see
src/agent/model_adapter.py streaming path) — check whether the case_server currently relays it
- Phase definitions on the backend: walk
src/agent/skills.py → _skill_answer_freeform for the canonical phase order
Difficulty
S (2-3 hours) for Option A (time-based heuristic), L (1-2 days) for Option B (real backend streaming). Recommend starting with A.
Problem
Per-turn latency is 3-30 seconds (playbook-heavy turns + regen attempts push this higher). The current loading UX is presumably a generic spinner — the operator has no signal of what's happening, which feels broken when a 25-second turn drags on.
The backend pipeline has clear phase boundaries that we can surface:
A phase indicator turns dead time into useful signal.
Acceptance criteria
<PhaseIndicator>component shown in place of the current spinner:Route,Database,Cpu,ShieldCheck)prefers-reduced-motion: reduce→ no animations, just static state)Constraints
aria-live="polite"so screen readers announce phase transitionsPointers
frontend/src/components/chat/(find the message-pending render)src/agent/model_adapter.pystreaming path) — check whether the case_server currently relays itsrc/agent/skills.py → _skill_answer_freeformfor the canonical phase orderDifficulty
S (2-3 hours) for Option A (time-based heuristic), L (1-2 days) for Option B (real backend streaming). Recommend starting with A.