fix(chat): keep the in-flight answer when navigating away mid-turn — partial answer restored on return
This commit is contained in:
+38
-5
@@ -501,6 +501,13 @@ let uiState = UI_STATE.idle;
|
||||
let thinkingClock = 0; // setInterval id — elapsed-seconds hint
|
||||
let thinkingStart = 0; // Date.now() when "thinking" began
|
||||
let turnTimeout = 0; // setTimeout id — 120s pre-token guard
|
||||
/* Turn accumulators + the navigate-away flag (phase 20, owner choice
|
||||
* 2026-08-24 A1): module scope because the `pagehide` handler reads them
|
||||
* while a turn is still in flight; reset per turn at the top of
|
||||
* handleSend, so they stay turn-scoped like the rest of the turn locals. */
|
||||
let acc = ""; // accumulated answer text this turn
|
||||
let thinkingAcc = ""; // accumulated thinking text (persisted with the turn)
|
||||
let persistedOnLeave = false; // pagehide partial-persist at most once
|
||||
|
||||
function stopThinkingClock() {
|
||||
if (thinkingClock) {
|
||||
@@ -640,8 +647,11 @@ function appendMaybeTry(wrap, suggestions) {
|
||||
*
|
||||
* Only RAW TEXT is stored — restore re-renders it through the escape-first
|
||||
* markdown renderer, so no HTML is ever persisted. Save points: the user
|
||||
* message on send (a failed turn keeps the question) and the brain message
|
||||
* on `done` (with sources/deflected/suggestions). Every localStorage access
|
||||
* message on send (a failed turn keeps the question), the brain message on
|
||||
* `done` (with sources/deflected/suggestions), and the PARTIAL brain
|
||||
* message on navigate-away (`pagehide`, phase 20 — an in-flight turn keeps
|
||||
* whatever had already streamed; thinking-only turns persist nothing
|
||||
* brain-side). Every localStorage access
|
||||
* is try/catch'd — private mode or quota exhaustion degrades silently to
|
||||
* in-memory-only chat. If the serialized state outgrows the budget (~700k
|
||||
* chars, far under the ~5MB quota) the oldest messages are dropped first.
|
||||
@@ -833,11 +843,14 @@ async function handleSend(e) {
|
||||
clearErrorBanner();
|
||||
|
||||
let wrap = null;
|
||||
let acc = "";
|
||||
let res = null;
|
||||
let aborted = false; // the 120s guard already took the turn to error
|
||||
// Phase 17 (thinking display): turn-local reasoning state.
|
||||
let thinkingAcc = ""; // accumulated thinking text (persisted with the turn)
|
||||
// Phase 20: acc / thinkingAcc / persistedOnLeave live at module scope
|
||||
// (the pagehide handler reads them) but reset here, so they stay
|
||||
// turn-scoped exactly like the other turn locals.
|
||||
acc = "";
|
||||
thinkingAcc = "";
|
||||
persistedOnLeave = false;
|
||||
let sawThinking = false; // did any `thinking` frame arrive this turn?
|
||||
let sawDone = false; // did the stream end with a `done` event?
|
||||
|
||||
@@ -972,6 +985,26 @@ input.addEventListener("keydown", (e) => {
|
||||
});
|
||||
composer.addEventListener("submit", handleSend);
|
||||
|
||||
/* 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 });
|
||||
});
|
||||
|
||||
/* Boot: auth state FIRST — it decides whether the restored conversation
|
||||
gets Tune buttons and whether the steering UI exists at all (phase 16).
|
||||
Phase 14: the conversation then comes back exactly as left. Phase 19:
|
||||
|
||||
Reference in New Issue
Block a user