# Story: Sources Mid-Stream — an in-flight answer is not lost on navigation **Phase:** `20_sources_midstream_bug` · **E2E:** `tests/e2e/test_sources_midstream_bug.py` ## Bug report (TODO.md L3, verbatim) > *"Clicking "sources" while chat is generating clears chat and result > will never show up"* ## Narrative As **a user with a question in flight**, I want to **click "Sources" while Brain is still generating** without losing the answer — when I come back to the chat, what had already streamed should be waiting for me. - **Given** I asked a question and Brain's answer is still streaming - **When** I click the "Sources" nav link (or otherwise leave the chat page) mid-stream - **Then** the stream is aborted by the navigation (no server-side resume — A10 stays stateless), but returning to `/` shows my question **and** the partial answer that had already streamed, rendered exactly like any brain message (thinking block included, if any had streamed). ## Owner-confirmed (2026-08-24, roadmap A1) 1. **A partial answer is persisted as a plain brain message** — no "(partial)" marker, no sources/suggestions (the turn is dead; the user can re-ask for the full answer). 2. Navigation **before the first answer token** (pure thinking) persists nothing brain-side: the question is restored, no empty/partial bubble. 3. The deliberate **New Chat** `clearChatStorage()` (sources/viewer pages) is untouched — that clear is by design (phase 14/19). 4. No server-side resume (A10 stays stateless) and no "leave page?" confirmation dialog. ## Acceptance criteria 1. `pagehide` (navigate-away / bfcache store) with an in-flight turn (`thinking` or `streaming`) and non-empty streamed text persists the partial raw answer via the existing `rememberBrainTurn` save-point helper (idempotent per turn — `persistedOnLeave`). 2. Returning to `/` restores the question and the partial brain message through the existing restore path (escape-first markdown, optional collapsed thinking block) — no new UI surface, no error banner. 3. Navigation during pure thinking (no answer tokens) persists nothing brain-side: exactly the user message is restored. 4. A completed turn persists exactly as before (done metadata intact; no duplicate brain message). 5. New Chat from the sources page still clears the conversation (`clearChatStorage`, phase 14/19 — untouched). 6. Unit + integration green, `app/` coverage ≥ today's number (>90% gate), story E2E green in isolation, one `--no-gpg-sign` commit. ## Playwright Mapping Rule **Test Scenario → `tests/e2e/test_sources_midstream_bug.py`** (mock LLM, seeded KB): 1. `test_partial_answer_survives_sources_nav_midstream` — admin (login, `next=/`), send the ~9s long-answer question, wait for the first streamed frames to render, click `#nav-sources` (lands on `/sources.html`), `page.goto("/")` → question + partial answer rendered, no `role="alert"`, `bor.chat.v1` holds the partial brain message (starts with the first streamed chunk, shorter than the full answer, no done metadata). 2. `test_no_orphan_brain_message_when_navigated_before_first_token` — the mock streams the phase-17 thinking scratchpad, then a 4s pre-token pause; navigate during the pause, return to `/` → exactly one user message, zero brain messages, in the DOM and in storage. 3. `test_completed_turn_unaffected` — a fully settled turn → trip to Sources and back → full answer, source chip, and a byte-identical `bor.chat.v1` payload (done metadata intact, no duplicate). 4. `test_new_chat_still_clears_conversation` — completed turn → `/sources.html` → click the sources-page New Chat button → lands on `/` with the empty state and `bor.chat.v1` removed.