fix(chat): keep the in-flight answer when navigating away mid-turn — partial answer restored on return

This commit is contained in:
2026-08-24 15:53:07 -04:00
parent 824914ca3d
commit 76c6a01199
5 changed files with 578 additions and 91 deletions
@@ -1,83 +0,0 @@
# Task 01 — app.js: persist the partial answer on navigate-away (pagehide)
**Phase:** `20_sources_midstream_bug` · **Source:** `TODO.md` L3 —
*"Clicking "sources" while chat is generating clears chat and result will
never show up"*
## Objective
`frontend/assets/app.js` gains exactly one new save point: on `pagehide`
(navigate-away / bfcache), if a turn is in flight and answer text has
already streamed, the partial answer is persisted via the existing
`rememberBrainTurn` helper — so returning to the chat restores the
question **and** what had been generated.
## Work
1. `frontend/assets/app.js`
- Next to the other turn-local state (around the `acc` /
`thinkingAcc` / `sawThinking` / `aborted` declarations, ~line 920+),
declare a turn-local flag:
```js
let persistedOnLeave = false; // pagehide partial-persist at most once
```
and reset it to `false` at the top of `runTurn` (where `acc`,
`thinkingAcc`, `sawThinking`, `wrap` are initialized), so it is
turn-scoped like the rest.
- Register one handler at module boot (next to the other
`window.addEventListener` calls):
```js
/* Navigate-away save point (phase 20, owner choice 2026-08-24 A1):
* leaving the chat mid-turn would otherwise drop the in-flight
* answer — the brain message persists only on `done`, and
* navigation aborts the stream. On `pagehide`, if a turn is in
* flight and answer text has streamed, persist the partial raw text
* (reusing the save-point helper, so restore re-renders it exactly
* like a completed answer — no "(partial)" marker, no sources).
* Thinking-only (no answer tokens yet) persists nothing brain-side:
* the question is already saved on send and the user can re-ask.
* `persistedOnLeave` makes this idempotent across pagehide/bfcache
* churn. */
window.addEventListener("pagehide", () => {
if (persistedOnLeave) return;
if (uiState !== UI_STATE.thinking && uiState !== UI_STATE.streaming)
return;
if (!acc) return; // nothing brain-side to save yet
persistedOnLeave = true;
rememberBrainTurn(acc, { thinking: thinkingAcc || undefined });
});
```
**ASSUMPTION (owner-confirmed A1):** the partial is a plain brain
message (no marker, no sources); the variables `uiState`, `acc`,
`thinkingAcc` are the turn's existing ones — match the names actually
in scope (the file keeps `let acc = ""` / `let thinkingAcc = ""`
turn-locals inside `runTurn`; if the handler needs them outside that
scope, hoist the turn-locals to module scope *without* changing any
behavior — smallest diff wins).
- Update the phase-14 persistence block comment (~line 631): the
"Save points:" sentence now lists three — the user message on send,
the brain message on `done`, and the **partial** brain message on
navigate-away (`pagehide`, phase 20).
2. `tests/unit/test_sources_midstream.py` (new — follow the repo's
source-level pin pattern used by `tests/unit/test_shared_header.py`):
- `app.js` contains `addEventListener("pagehide"` exactly once.
- The pagehide body is guarded by the in-flight states (`thinking`
and `streaming`) and a non-empty `acc` check.
- The pagehide body calls `rememberBrainTurn(acc,` with
`thinking: thinkingAcc || undefined`.
- `persistedOnLeave` is declared and reset in `runTurn`.
- The persistence block comment lists the `pagehide` save point.
- `clearChatStorage` (header.js) is untouched — still the only
deliberate clear.
## Testing & Quality
- `uv run pytest tests/unit/test_sources_midstream.py -v` green.
- `uv run ruff check . && uv run pyright` clean.
- Manual smoke (dev server, DEBUGPY optional): send a question against
the real LLM (or any slow stream), click Sources mid-stream, return —
the partial answer is visible.
## Completion Criteria
- [ ] The `pagehide` handler exists, is turn-scoped and idempotent, and
reuses `rememberBrainTurn` (no duplicated storage code).
- [ ] No other save point changed: send + `done` behave byte-identically
to before (existing persistence unit pins still pass).
- [ ] Unit pins green; lint/types clean.