# 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 1. `app.js` — formalize the state machine (single `setUiState(state)` function driving: typing indicator, send button disabled/spinner/label, `#send-status` live text). Replace ad-hoc busy handling from Phase 03. 2. 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). 3. Streaming: first `delta` removes the typing indicator and starts appending to the answer bubble; button stays busy. 4. Error paths: `{"type":"error"}` SSE event, non-2xx response, or **120s client-side timeout** (clear on first delta) → red banner `role="alert"` ("Try again — if this persists, check the LLM is reachable") + state → idle. 5. `prefers-reduced-motion`: CSS already slows animations — verify; add a static fallback for the dots if needed. 6. Server: confirm the per-turn log line includes `embed_ms` and `total_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: ```bash 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 ```bash git add -A && git commit --no-gpg-sign -m "feat(ui): explicit chat state machine — typing indicator, streaming progress, timeout and error recovery" ```