2.7 KiB
Phase 06 — Story: Loading Feedback & Progress
Story: .agent/user_stories/loading-feedback.md
Context: .agent/PLAN.md §7.4 ("never stale" contract), §9
Goal
An unambiguous state machine — idle → thinking → streaming → done | error → idle — so the user always knows what's happening, and a stale
Send button is impossible.
Implementation steps
app.js— formalize the state machine (singlesetUiState(state)function driving: typing indicator, send button disabled/spinner/label,#send-statuslive text). Replace ad-hoc busy handling from Phase 03.- Pre-token: typing indicator (
role="status",aria-label="Brain of Reese is thinking"); after 10s pre-token, update the label with elapsed seconds (setInterval, cleared on state change). - Streaming: first
deltaremoves the typing indicator and starts appending to the answer bubble; button stays busy. - Error paths:
{"type":"error"}SSE event, non-2xx response, or 120s client-side timeout (clear on first delta) → red bannerrole="alert"("Try again — if this persists, check the LLM is reachable") + state → idle. prefers-reduced-motion: CSS already slows animations — verify; add a static fallback for the dots if needed.- Server: confirm the per-turn log line includes
embed_msandtotal_ms(add if Phase 03 omitted it).
UI Verification
Walk the full state machine by hand (dev server + mock LLM slow path): submit → indicator + "Thinking…" disabled button → live tokens → done (enabled, focused input). Kill the mock mid-stream → banner + recovery. Contrast of disabled button + spinner OK; reduced-motion pass.
Testing & Quality
- Unit/integration: SSE error event serialization; timeout constant exported/testable; (JS logic is E2E-covered).
- Coverage: >90% on
app/.
Playwright Execution Phase
Run ONLY this story's suite:
uv run pytest tests/e2e/test_loading_feedback.py -v --no-cov
Implements the story mapping (mock LLM's 3s "pretend to think slowly"
warm-up + a fixture that stops the mock): typing indicator visible during
pre-token and gone by answer; button disabled→"Thinking…"→enabled "Send";
streaming appends (two-timestamp length check); LLM-down ⇒ role=alert
banner + button recovered.
Success criteria
- every in-flight state has a visible indicator; button never zombies
- error + 120s timeout paths both recover cleanly
- reduced-motion respected
- unit + integration green, coverage >90%
- story E2E green in isolation
- committed
Commit
git add -A && git commit --no-gpg-sign -m "feat(ui): explicit chat state machine — typing indicator, streaming progress, timeout and error recovery"