# Row 129 / #1623 — Console chat area Design for Sage before product code, per sage-0719. Claimed at rev 1737. Brief: docs/plans/2026-10-14_prd-1.2-build.md, The WebUI chat area, pinned blob 333adea856b1. Requirements REQ-WEB-1, REQ-THREAD-2, REQ-USER-2. Reviewer Filbert; landing gate Sage; public/ landing needs Mos's decision 123 go. No tests, mutations, product edits or live actions have run for this design. ## Existing behavior and boundaries Read landed row 121 (2c16d6b93), row 122 (7b5b682ae) and row 128 (ade628c8b): - Row 121's broker supplies project.create/list and thread.create/list/open/ send/messages. A project is a bus catalog entry, optionally linked to a configured repository key. It is not a new filesystem checkout. Each thread belongs to its creating human and to the business's one threaded assistant instance. A send/open asks the existing launcher to wake that session; its persistent store and idle lifecycle already exist. - Row 122 supplies the Console's human-only Settings write path and readout. It demonstrates the separate payload boundary behind the same signed-in send mode. Chat changes no model/limit setting or organization file. - Row 128 supplies the human CLI's project creation, thread listing/creation/ opening/history, and talk/follow --thread. Its thread.send uses class REQUEST. Chat uses those same broker verbs and ownership checks. Names are supplied at creation: there is no existing project/thread rename verb, and this row does not add one. - Console routing is a static allowlist in serve.mjs, hash routing in public/bus.js, and the legacy Board conversation in public/app.js. The Board's /api/conversations, /api/conversation and /api/reply are separate from managed business threads. Chat uses none of those legacy APIs. - busCaller in src/bus.mjs uses the existing human CLI proof. The bus binds the configured business human; the proxy's one --allow-user gates access to that Console. Chat retains that existing single-user mapping. It does not accept an owner/user/business/capability from a browser or invent a multi-user identity bridge. The broker remains the ownership authority. - Existing writes require --send, named preview Host, one exact authenticated user header, exact HTTPS Origin, same-origin Sec-Fetch-Site, JSON and a bounded payload. Existing Settings/project-file/message routes retain their behavior. No GET is allowed to call a write, including thread.open. ## UI and hash routes Add Chat to the existing section navigation and command palette: | Hash route | View | | --- | --- | | #/chat | Projects and their owned named threads, plus New project | | #/chat/projects/ | One project, its owned threads, New thread | | #/chat/threads/ | That owned thread's conversation and composer | Project creation accepts a name and an optional configured repository key. The optional selector reads the already-existing GET /api/business projects key list; it never exposes or accepts a filesystem path. If that read is unavailable, creating an unlinked project remains possible, as in the CLI. Successful project creation opens its project page. Successful thread creation opens its conversation page. No default project or thread is created implicitly. The conversation shows its name, project, assistant instance, timestamped messages and exact inert, pre-wrapped body text. User/assistant/chief messages stay in the one thread selected. Names/bodies use the existing control/bidi visibility convention and HTML-safe text; agent text never becomes executable HTML. The view claims no running/working status that the thread API does not report. A send receipt means accepted, not that the assistant has answered. Reading a conversation does not write or wake anything. A separate explicit Resume assistant button calls thread.open; sending a message also wakes through the existing pending-message mechanism. Create, Resume and Send are unavailable without signed-in send mode. All reads remain usable in the Console's existing read-only mode, subject to the same human transport and owner check. History uses the landed forward cursor, 100 messages per page (broker max 200), chronological order. A visible Read next messages control pages forward; a full page means more may exist, not a fabricated exact count. Once caught up, polling reads after the last message id and de-duplicates by id. While an older page is shown, the UI labels it and offers the next page instead of pretending it is current. This keeps each request/render bounded and uses the existing API; it does not add a latest-first/before cursor to the broker. The user can send while reading history; the accepted-message receipt remains until that id is seen in the conversation. This paging choice is part of the design for Sage. Drafts/receipts/in-flight flags are scoped to thread id (and creation forms to project), held only in memory. Polling preserves draft, focus, selection and scroll; arriving messages scroll only when already at the tail or after the user's own successful send. Otherwise show a new-message affordance. No chat history/draft is persisted to localStorage. Pending submit disables that form; there is one POST per explicit submit and no automatic retry. The module uses request generations and AbortController on route changes. A late A response cannot paint B or clear B's draft. Sign-out redirects/401 stop reads and clear private chat state. A thread ownership refusal clears its history/draft and displays a refusal; it cannot reuse cached private content. Ordinary read/network failures retain the current draft with a clear status. Outcome-unknown writes retain no invented success/message; ask the user to read the current list/history before submitting again. Sign-out still clears private drafts, including after an uncertain write. ## HTTP routes and exact bus calls New fixed /api/chat namespace; no arbitrary verb-forwarding endpoint. Browser ids must be full UUIDs. No browser-selected tenant, owner or capability field. Query names are allowlisted, single-valued and validated before any bus call. | HTTP | Bus verb and arguments | Result | | --- | --- | --- | | GET /api/chat/projects | project.list {} | {rows,at} | | POST /api/chat/projects | project.create {name,repo?} | verified {id,at} | | GET /api/chat/threads[?project=UUID] | thread.list {project?} | {rows,at}; owned threads only | | POST /api/chat/threads | thread.create {project,name} | verified {id,instance,at} | | GET /api/chat/threads/UUID/messages[?after=UUID&limit=N] | thread.messages {thread,after?,limit}; N=1..200 | {rows,after,hasMore,at}; hasMore is conservative | | POST /api/chat/threads/UUID/messages | thread.send {thread,body,class:'REQUEST'} | verified {id,at} | | POST /api/chat/threads/UUID/open | thread.open {thread} | verified {opened,at} | POST open accepts only an empty JSON object. Project/thread names obey the broker's 1..200 character, no CR/LF/NUL rule. Body remains exact, with a nonempty message and the existing bus character bound. Every POST also fits the Console's 16 KiB JSON byte bound; the browser checks encoded length and shows a normal "message is too long" validation message rather than silently truncating. No request can override the fixed REQUEST class. Names are not interpolated into URLs; ids are encoded and validated. All replies are no-store JSON. The new chat handler repeats the current Console send checks for every one of its four POST operations: --send + named preview Host, exact single user header, exact Origin, same-origin Fetch Metadata, JSON, size and exact shape. One chat-local fence covers all four; it does not refactor the already-reviewed Settings, file-editor or role-message fences. Every refused fence proves zero bus calls. GET/open cannot accidentally write. Preview read-only and loopback writes refuse, just as the existing Settings/file-editor writes do. Transport comes from the same busCaller built by src/cli.mjs; injected fixture transports test the handler. Adapter functions have fixed verb mappings and forward only validated fields. Backend proof/ownership checks remain in place. No change to packages/bus, packages/cli, schema, harness, sandbox or role grants. ### Refusals and uncertain writes A same-business thread owned by someone else returns HTTP 403: `{error:'thread-not-owned',message:'This thread is not available to this user.'}`. The POST response adds sent:false. No name, owner, body, count or history is returned. The bus's #thread check runs for messages/send/open, so a guessed id cannot bypass ownership. A foreign-business/absent id is 404 thread-not-found. Thread lists are filtered by the bus, never by hiding rows in the browser. The existing project.list is a business-wide catalog (not a per-owner project filter); this design preserves the CLI's catalog and displays only this human's threads within it. It introduces no new project-ownership semantics. Other mappings: invalid request/id/query/name/body/unknown repo → 400; project-exists/thread-exists → 409; missing project/thread → 404; human-required/unauthenticated/read-only → 403 (bad proxy user header → 401); not-configured/no-bus-host/thread-instance-missing/ambiguous → 503; unusable read response → 502. Fixed safe messages do not echo bus/parser data. A write transport loss, malformed success id or uncertain response is 503 outcome-unknown, sent:'unknown'. A definite broker refusal is sent:false. No catch/retry calls the writer again. Success ids/opened ids are checked before acknowledging success. Log one line per write with action, signed-in user, project/thread/message id if known and outcome, never names/body/capability. ## Files to create - packages/webui/src/chat.mjs: fixed project/thread bus adapter. - packages/webui/src/chat-http.mjs: strict reads, common chat POST fence, validation, safe status/refusal/uncertainty mappings and response checks. - packages/webui/src/public/chat.js: isolated chat UI, per-thread state, fetch/generation/redirect handling, forms, history and polling integration. - packages/webui/src/public/chat.css: scoped .chat-* layout/responsive styles. - packages/webui/tests/chat.test.mjs: adapter/HTTP/broker ownership fixtures. - packages/webui/tests/chat-browser.test.mjs: real browser behavior on fixtures. - packages/webui/tests/chat-fixture.mjs if shared fixture setup merits a helper. ## Existing files to edit — shared ownership/landing list - packages/webui/src/serve.mjs: import new route handler; add chat option; add chat.js/chat.css to static allowlist; dispatch only /api/chat paths before the generic preview write refusal. Preserve existing routes/branches. - packages/webui/src/cli.mjs: construct the chat adapter from existing busCaller and pass it to startServer. No new flag, proof, config source or live action. - packages/webui/src/public/index.html: one stylesheet and one deferred script before bus.js. This is a shared file with Researcher's row 154 asset includes. - packages/webui/src/public/bus.js: Chat nav/command palette item; delegate the three hash routes; refresh/pause/config-send hooks; auth forget hook; a small chat-refresh event hook. Reuse existing render-generation handling, and extend focused text-input selection preservation only if the chat forms require it. No Docs/textarea/editor, Settings, Board or role composer rewrite. - packages/webui/README.md and docs/TOOLS.md: chat routes, commands/parity, write/privacy boundary and pagination. No package dependencies or lockfile. public/app.js, public/markdown-preview.js, existing editor CSS, project reader/ writer and the legacy Board APIs are not edited. serve.mjs, index.html and bus.js are the shared row-154 paths for Sage to order. Row 124 also edits serve.mjs/bus.js/TOOLS and is in review: build on current landed refactor and rebase/re-gate whenever one of those rows lands. Do not apply its unlanded patch. A detached worktree will be created at the approved current base after this response; this design stage touches only agents/rocko/work/queue-129 and logs. ## Acceptance evidence and planned mutants No test runs without Sage's slot, including focused files. All eventual runs use a scratch TMPDIR, never a launcher run under /tmp. No live Console/bus units. 1. Real broker + injected verified-human transport behind the HTTP adapter: list projects and only owned threads, filter a project, create/names, duplicate names, optional repo key, missing/ambiguous assistant instance, direct deep link, cursor paging and invalid/cross-thread cursor. GET history changes no delivery/read/open evidence. Existing thread owner/biz rules hold. 2. Seed another owner's thread with sentinel messages. Read, send and open each return 403 with no sentinel, name or owner in body; list excludes it. A foreign-business id is 404. Spy confirms no rejected write changed evidence. 3. For each POST: missing/duplicate/wrong user header; wrong/missing Origin; wrong/missing Fetch Metadata; read-only/loopback; bad content type, oversize, invalid/extra fields; bad id/name/empty body. Zero transport calls on refusal. 4. Success, definite refusal, lost reply and malformed success response. Exactly one attempted bus write, correct sent status, no automatic retry. Fixed thread.send class/identity and exact content are asserted at the adapter. 5. Browser: project/thread list and create, independent conversation routes, send/receipt/following reply, Resume as explicit POST, page/de-dup order, unread failure, read-only controls, draft/caret/scroll preservation, no POST on polls/refreshes, late A response while B is selected, ownership refusal, sign-out with pending fetch/write, hostile names/bodies/control characters. 6. One mutant per acceptance/security check, including each fence, ownership status/no stale cache, fixed bus mapping, immutable send class, byte bound, once-only submission, uncertain-success handling and thread-scoped state. Proposed targeted gate: webui package suite (browser included), affected bus thread tests and CLI thread parity tests, plus exact shell readers of the changed files if any. Sage chooses/grants the slot and runs the landing gate. ## Scope ruling — sage-0724 (approved) The old Q156 conversion-order paragraph in the pinned file says row 128 makes cli a channel module and row 129 adds webui. The revised, landed row 128 brief (docs/plans/2026-10-18_row128-thread-cli.md) owns CLI only, and no channel kind exists in packages/plugins/src/kinds.mjs; its current kinds are usage-source, limit-check and search-source. Implementing that older paragraph would add a new core interface and settings contract beyond this UI design. Sage approved this design in sage-0724. Row 129 stays UI-only on the current human transport. No channel kind, CLI module or WebUI channel module is built here; that core interface gets a separate brief after 129. chat.mjs is the single HTTP-to-bus seam for that conversion. Forward paging, creation-only names and the 403/404 ownership distinction are approved as documented above. ## Build start Detached worktree: /mnt/storage/src/mosaic-stack-rocko-129. Base: 5e0437659 (contains landed 640f4cef5; later two commits are own logs). No unlanded row 124 or 154 source is applied. Ask Sage for the slot before the first focused run; no suite, browser or mutant run until granted.