/* 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. * * Agent tool calls (phase 37, PLAN §4 extension): a grounded turn may * call the two server-side document tools (list_documents / * read_document, budgeted server-side). Each call streams a `tool` SSE * frame, and the UI shows the "calling tool" state IN ADDITION to * "thinking": the UI state itself stays "thinking" (button stays * disabled — never stale, PLAN §7.4) while the LABELS change — the * button says "Calling tool…", the #send-status + typing-indicator * labels say what Brain is doing ("Brain of Reese is listing documents" * / "Brain of Reese is reading source/path"), and a visible `.tool-call` * line (own icon + accent color, distinct from the brand-ink Thinking * block) is appended above the answer, one per call, in order. * Append-only like thinking: frames are tolerated in any interleaving * (a frame after the first delta just appends — the agent loop never * emits one, but it must not crash). The turn record persists an * optional `tools: [{name, argument}]` array next to `thinking` and * restore re-renders the lines (phase 14 convention). * * 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