Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
248 lines
15 KiB
Markdown
248 lines
15 KiB
Markdown
# 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/<project UUID> | One project, its owned threads, New thread |
|
|
| #/chat/threads/<thread UUID> | 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.
|