/* Brain of Reese — chat shell. * * Renders suggestions (onboarding chips + "Maybe try" deflection chips — * one shared .suggestion-chip component, renderChips below), shows KB * health, and runs chat turns against POST /api/chat (SSE, PLAN §4). * * Loading feedback (PLAN §7.4 "never stale" contract, loading-feedback * story) is one explicit state machine with a single entry point — * setUiState(state) — driving the typing indicator, the send button * (disabled/spinner/label), and the #send-status live region: * * idle → thinking → streaming → done | error → idle * * • thinking — pre-token: typing dots + disabled "Thinking…" button; * after 10s the indicator's aria-label shows elapsed * seconds so screen-reader users are never left guessing. * Phase 17: while the model streams reasoning (`thinking` * SSE events), the live collapsible Thinking block IS the * visible feedback (it replaces the typing dots; the UI * state stays "thinking" — button still disabled, * "Thinking…") and the 120s guard clears on the first * thinking *or* delta event. * • streaming — the first delta removes the dots and appends live into * the answer bubble (auto-collapsing the Thinking block, * phase 17); the button stays busy until `done`. * • error — red banner (role="alert") with an actionable retry hint; * the 120s guard (TURN_TIMEOUT_MS) catches hung pre-token * streams and the sawDone guard (phase 17) catches a * stream that dies after frames but before `done`, so the * button can never sit zombified. * * Conversation persistence (phase 14) makes the chat a durable LOCAL * session: the message list (raw text + turn metadata) lives in * localStorage under the versioned key `bor.chat.v1` and is re-rendered on * load — refresh, tab close, and a trip to Sources never lose it. Phase * 17: a brain record may carry an optional `thinking` field — the * collapsed Thinking block is restored with it; records without it (old * sessions) restore exactly as before, so no version bump. A10 is * untouched: the API stays stateless, nothing is stored server-side. * "New chat" (#new-chat-btn — bound by the shared header module, * phase 34 task 02) clears the key + the list back to the empty state. * * Steering notes (phase 15) let the owner tune how Brain answers: a * "Tune" button under every completed brain bubble (deflected included) * opens an inline form → POST /api/steering → the note is stored in * Postgres and injected into the system prompt of every subsequent turn * (the section). The header "Tuning" panel (#steering-panel) * — toggle, list, per-note delete, count badge, announcer — is owned by * the shared header module (assets/header.js, phase 34); this file keeps * only the chat-specific per-bubble Tune button + inline form, whose * success path refreshes the panel (refreshSteering()) and announces * (announceSteering()) through the module. Note text is always rendered * with textContent (XSS-safe) in both places. * * Scroll (phase 18, owner choice 2026-08-23): the page auto-scrolls only * while the user is pinned to the bottom. NEAR_BOTTOM_PX (200px) covers * the composer zone — the textarea auto-grows to 192px plus the button * row — so "the composer is in view" counts as pinned: submitting from * the composer reveals your own message, and the answer follows token by * token while you stay pinned. Once you scroll up to read earlier * content, nothing drags the viewport back down for the rest of the turn * (thinking or answer). scrollReveal(wrap) is the single scroll gate; * `force` is reserved for the one-shot phase-14 restore landing. * * Document modal (phase 26): a source chip opens the cited document in * the almost-fullscreen modal overlay (assets/document-modal.js) on the * SAME page — no new tab, no navigation. The chip keeps its * /document.html href as the no-JS / context-menu escape hatch; * left-clicks are intercepted (preventDefault) and routed to * openDocumentModal. The module is loaded through the relative import * below — the header.js single-evaluation design (no direct