/* 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 * (one button, two roles: "Send" when idle, the enabled "Stop" control * while a turn is in flight — phase 48, 2026-08-29, TODO.md L3), and * the #send-status live region: * * idle → thinking → streaming → done | error → idle * stop → idle (no banner — phase 48) * * • thinking — pre-token: typing dots; the button is the enabled * "Stop" control (it stays so until `done`). 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" — the button stays the "Stop" * control) 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 the "Stop" control until * `done`. * • stop — phase 48: clicking the in-flight button (or pressing * Enter) aborts the fetch (AbortController, the task-01 * server teardown closes the model's stream on * disconnect). The partial answer is kept on screen and * persisted with the optional `stopped` marker (a * pre-token stop persists nothing brain-side — phase-20 * convention), the live region confirms "Answer * stopped.", and the turn settles to idle through the * SAME finally path — no error banner, no scroll * (phase 42). * • 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; phase 68 added * search_documents): a grounded turn may call the three server-side * document tools (list_documents / read_document / search_documents, * bounded only by the round cap — phases 45/68). 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" (the * button stays the enabled "Stop" control — phase 48 — never stale, * PLAN §7.4) while the STATUS LABELS change — the #send-status + * typing-indicator labels say what Brain is doing ("…is listing * documents" / "…is reading source/path" / "…is searching for * pattern" — the name prefix resolves from window.BOR_BRAND at call * time, phase 39) — the button no longer relabels to "Calling tool…" * (phase 48, owner-locked: it stays "Stop" for the whole turn) — 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). * * LLM retry status (phase 67, TODO.md L3): if the endpoint dies BEFORE * the first frame of an LLM request lands, the server restarts that * request (up to BOR_LLM_RETRIES retries, BOR_LLM_RETRY_DELAY seconds * apart — a flat delay, no backoff, owner-locked A3) and streams one * `retry` SSE frame per wait. The handler treats it with the same * status pattern as the `tool` frames: the 120s guard clears (a frame * arrived) and the EXISTING #send-status live region + the * typing-indicator aria-label read the owner-locked copy * "Communication interrupted — retrying (n of N)…" (n = the attempt * about to be tried, N = the configured total). Nothing else changes — * no bubble, no tool line, no banner, no UI-state change: it is a * transient status that the next thinking/tool/delta frame replaces * through its own branch. A retry arrives only before the request's * first piece (locked A2), so it never races a partial answer; the * status gate still covers BOTH live states (thinking AND streaming) * because a LATER agent round may restart while an earlier round * already emitted content (the rare content+tool-call stream). * * 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. * * No reply autoscroll (owner direction 2026-08-27, TODO.md L5 — * revising the phase 18 follow-the-bottom choice): the page NEVER * auto-scrolls while a turn streams — no thinking, tool, or delta frame * moves the viewport, so scrolling up to read earlier content holds for * the rest of the turn. The only scroll call sites are user intent: the * submit (your own message is revealed) and the phase-14 restore landing * (one-shot, load-time). scrollReveal(wrap) is the one page scroll in * this file — a document-BOTTOM landing, not a message-bottom alignment * (its comment explains why block:"end" hopped the page up on submit); * addMessage(who, html, scroll) carries the intent. The * thinking block's internal bottom-pin (textEl.scrollTop, phase 17 — * reworked separately in phase 43) pins the block's own clip, not the * page, and is untouched here. * * 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