Files
stack/packages/webui/README.md
T
jason.woltjeandClaude Opus 5.5 1bdb6f9b13 feat(webui): console tokens and shell restyle (#1542, row 53)
The console takes the design package's tokens.css and icons.svg as byte
copies, guarded by a test, and a new shell.css holds the components:
command bar with freshness line, section list (a strip that scrolls
inside itself below 760px), dense table, inspector, status mark, chips,
copy command, toast, Ctrl+K palette, not found, banners and skeleton
rows. JetBrains Mono 2.304 ships from the official archive with sha256
and OFL. Harbor following the system setting is the default palette.
A refusal (403, not-configured, no-bus-host, or the board's 403) fails
closed everywhere: no kept rows, counts, inspector, palette entries or
conversation history, and a later 500 or 503 doesn't bring them back.

Dewey built it in three rounds. Round 1 (75569953) and round 2
(78c3aef9) got changes from Darkwing (27148, 27165) and Filbert (27143,
27163) for refusal leaks outside the main view; round 3 (2562c05d) was
approved by Darkwing (27171) and Filbert (27174). Sage's gate on
ea080fd8 plus the candidate: runs 41/0, queue 148/0, webui 27/0,
conversation 182/0, control-board 124/0, build-tokens --check current,
every scripts/test-*.sh 0 failed (task 98/0 and release 14/0 with
Docker). The round 2 gate had one discord engine timing failure in the
full run, 66/0 alone, filed as #1553.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 14:49:48 -05:00

14 KiB

Mosaic Console

Piece 4, #1507. The control board is the first and only WebUI screen.

Run

Start the existing board in one terminal, then the WebUI in another:

node packages/control-board/src/cli.mjs serve
node packages/webui/src/cli.mjs serve

Open http://127.0.0.1:7330/. Optional --port N and --board http://127.0.0.1:7331 select another port or loopback board origin. --business ID picks the business the bus views read; without it they read the running bus host's business (mosaic bus start <business>). Port 0 picks a free port. Ctrl-C stops each foreground server. No daemon, installation, account, authentication or deployment is added.

Only HTTP loopback board origins are accepted. The WebUI binds to IPv4 loopback by default, rejects nonlocal Host and cross-origin requests, sends no CORS headers, refuses redirects and accepts only JSON object POST bodies up to 4096 bytes. Do not expose either unauthenticated server through a public proxy.

Use

  • Waiting on you uses the board's waitingOnYou flag across all projects.
  • Select a project on the left to filter the session table. Counts show visible rows out of the total when Hide offline or Hide seen hides anything.
  • Select an agent to open its inspector. Arrow keys move between table agent buttons, Enter opens, and Close or Escape in the inspector returns focus.
  • Seen clears the row from Waiting on you until the board detects new activity. The collapsed Seen section keeps those rows available; Unsee returns them.
  • Reply appears only where the board's registration permits it. A failure shows the board's stderr and keeps the draft. Transport success is the board's delivered receipt, not proof that the seat processed the message.
  • Refresh runs the existing board scan. Automatic refresh uses the board page's ten-second interval; Pause stops it. Failed refreshes keep the last snapshot with a warning. There are no automatic action retries.
  • Palette and appearance come from tokens.css (see "Shell" below) and persist in this browser when local storage is available. Appearance defaults to System. No settings screen is added.
  • History opens a read-only conversation view for a seat, from its Waiting card, its table row or its inspector (#1507, CHAT-02). It shows the whole branch with nothing clipped. Tool calls, tool results and thinking start collapsed. Session text is always shown as text: Markdown stays as source, and terminal controls, bidi controls and marks show as visible symbols. Reply in the view uses the same board reply path as the inspector.
  • The view checks for new entries on the same ten-second refresh, and Pause stops it. It never switches on its own. A fork, a newer session for the seat or a rewritten file each shows a marker with a button to open the other history. Reloading after a rewrite keeps the branch the view was on; if that branch is gone, the view opens the latest one and says so. Session lists older sessions for the seat. Only repository Pi seats have history; other harnesses say so.

Drafts and receipts stay in page memory, including across refresh, inspector changes and a stale registration. Reloading or closing the page loses them. A pending send stays disabled across refresh; new text typed during a send is not cleared by the earlier send's success. If the proxy loses the response, delivery may be unknown: inspect the seat before sending again.

Inbox, tasks, agents and trails (slice 1 S5, #1522)

The Console sidebar adds Inbox, Tasks and Agents next to the control board, which stays at #/. Routes: #/inbox, #/inbox/<decision>, #/tasks, #/tasks/<vikunja:project/task>, #/agents, #/trail/task/<ref> and #/trail/decision/<id>, with ?kind= filters on a trail. The design note is agents/dewey/work/wui/SLICE1-VIEWS.md.

  • Every view reads the four verbs of the Q1 module (packages/bus/src/views.mjs, lead decision 56): inbox, tasks, agents and trail. The CLI reads the same functions, so both show the same broker data.
  • Nothing writes. A decision shows mosaic decide <id> <key> for each choice, with Copy, which only puts the command on the clipboard. It is answered in a terminal (REQ-DEC-3, Q4). Seen is not set from the browser either. The command has no --business, so mosaic decide uses the running bus host's business, as the Console does by default. From a Console started with --business for another business the copied command refuses (no bus host, or no such decision) rather than answering elsewhere.
  • What no Q1 verb returns is labelled where it would show, for example "Bot names: not in the Q1 module" (lead decision 63). Nothing is guessed.
  • A task's list row can only say its current state came from a Vikunja read, since the list has no trail. The task page reads the trail and tells a task the stack never created from one changed in Vikunja after the stack wrote it. A stale poll read is shown in the snapshots and never as current.
  • All bus- and agent-authored text is rendered inert: controls show as control pictures or [U+XXXX], and Markdown stays as source.
  • Views refresh on the board's ten-second interval and stop on Pause. Focus, open sections and the copy status survive a refresh.
  • A failed read keeps the last good answer for that read and says it is old, with the time it was read. A refusal (403, not-configured or no-bus-host) never shows kept rows (row 53), and it drops every kept bus read: the Inbox count and the palette's decisions and tasks go too, until a read succeeds. A page never read, or refused, shows the failure instead, titled "Bus refused the read" (403: the bus refused the Console's read, for example human-required from a Console started inside an agent run), "No bus to read" (no system config, or no bus host and no --business) or "The read failed".

Shell (row 53, #1542)

The Console is restyled to docs/design (IMPLEMENTING.md). It is a restyle only: no new data, route or write.

  • src/public/tokens.css and src/public/icons.svg are byte copies of docs/design/tokens.css and docs/design/icons.svg; tests/shell.test.mjs fails if either differs. Edit docs/design, rebuild there, then copy.
  • Colours come only from tokens.css, selected by data-palette and data-mode on <html>. The page sets no colour inline. Appearance System removes data-mode, so the OS light or dark setting applies; it is the default, with Harbor. The choice is kept under mosaic-console-appearance.
  • shell.css loads last and holds the restyle: radii --r 6px and --r-lg 10px, the type scale, tabular numbers, the command bar, section list, dense table, inspector, status marks, class chips, toast, command palette and not-found page. The earlier CSS files are unchanged.
  • JetBrains Mono 400, 500 and 700 come from the official JetBrains release (v2.304); assets/fonts/jetbrains-mono-sources.txt has the URL and sha256 of the archive and each file, and jetbrains-mono-OFL.txt the licence. Manrope has no 800 file here, so 800 headings render at 700.
  • Ctrl+K (or the search field) opens the palette over views, inbox decisions and tasks; arrows move, Enter opens, Esc closes and returns focus. Arrow keys also move between task links in the bus tables.
  • Copy reports in a toast. If the clipboard is unavailable, the toast stays until dismissed and the command is selected for Ctrl+C. When no toast shows, the toast is an empty live region, visually hidden but still in the accessibility tree, so the first copy is announced.
  • The command bar shows, in UTC, when the board was scanned and the bus read, and says when the last attempt failed. A board refusal drops the last scan and everything drawn from it: the cards, the table, the inspector, the project filter and tree, the counts, the footer and the scan line all say the board was not read. A failed read with no scan held, after a refusal or before the first scan, shows the board empty and says the read failed; it never brings back rows from a refused scan. A refusal also closes an open conversation and clears its history.
  • From 760px the footer is a one-line status bar and the side column fits between it and the command bar. Below 760px the section list is one strip that scrolls inside itself.

Data and boundaries

GET /api/board, /api/conversations and /api/conversation, and POST /api/seen and /api/reply proxy only those board paths. Only the two conversation routes carry their query string; the board validates it. POST bytes and upstream status/JSON are preserved. GET /api/config returns the configured board URL for the page's error message. No scanner, registration, session reader or transport is implemented here. The session reader is packages/conversation, served by the board.

GET /api/bus/inbox, /api/bus/tasks, /api/bus/agents and /api/bus/trail?subject=<id> are the bus reads (src/bus.mjs). Any other method is 405. The subject must match the broker's identifier rule, else 400. Each read runs the S4 human transport (packages/bus/src/human-cli.mjs) as a child process, so a slow read doesn't stall the server. The bus checks that child's ancestry, which ends at the Console process; it never sees the browser or any other client of the port. So while the Console runs, anything that can connect to its port (7330 by default) reads what a reader capability reads ("Human and reader paths" in packages/bus/README.md): the inbox, tasks, agents and trails. That includes a T3 seat using curl and a managed S6 session, since S6 doesn't confine the network. No route writes (Q4), so the exposure is reads only. Slice 1 doesn't change this (Filbert F1 on #1522). Answers are { rows, at }; failures are { error, message } with 403 for a refusal, 503 for no bus or no answer, and 502 for an answer that can't be used. The bus needs the system config (~/.config/mosaic-dev/config.json); without it the board still serves and the bus routes answer not-configured.

Console's shared CSS, Console CSS, brand.js and local Manrope fonts were copied unchanged from agents/dewey/work/wui/. Font license and source URLs accompany the files under src/public/assets/fonts/. live.css contains the live-page adaptations; original mockups and unrelated pending design work stay untouched. Unsupported mockup fields and controls are listed in docs/plans/DEFERRED.md. The source tree is a project filter; no invented workspace registration tree or mockup session hierarchy is presented as live data.

Verify

node --test packages/webui/tests/
node --test packages/control-board/tests/ packages/seat/tests/ packages/ledger/tests/ packages/mosaic/tests/
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/browser.test.mjs
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/conversation.test.mjs
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/bus-browser.test.mjs
WEBUI_EVIDENCE=/tmp/webui-evidence node --test packages/webui/tests/shell.test.mjs

Node's test runner and installed /usr/bin/chromium are required. Set CHROMIUM to another installed Chromium path. No npm download is needed. The copied CDP helper starts a separate temporary browser profile and removes it on exit. Tests use temporary board session/registration files and stub the board's transport. They never send to real seats or read real session logs.

Browser tests exercise the rendered real board fixture, exact counts, project filtering, keyboard return focus, Seen/Unsee, success/failure receipts, draft and caret preservation, pending-send exclusion, storage denial, stale registration, loading/empty/malformed/unreachable states and hostile text. Contrast is measured on 330 rendered samples across ten palettes and three modes. Layouts are checked at 320, 390, 768, 1440 and 2560px. Horizontal scrolling is intentional within the dense table; the page itself must not overflow.

Conversation tests run the real board routes over temporary repository-layout session files. They cover full-length answers, collapsed tools and thinking, hostile content rendered inert, malformed-line and reconcile markers, forks, and the return flow: a send from the view, a tool call and a delayed result while a draft is typed, a peer message, a 4.5-million-character answer split into continuation parts, and a relaunch mid-turn.

Bus tests (bus.test.mjs, bus-browser.test.mjs) run the routes and the views against an in-process broker read through its reader session, so every row has the broker's own shape. humanCall runs against a stub CLI script. They cover each view, the gap labels, inert hostile text, Copy, the stale fallback and each failure title, and that no route writes. They never start the human CLI against a live bus.

Shell tests (shell.test.mjs) check the copies, the font hashes and the kept strings, and in the browser at 400px: System mode under an emulated dark and light OS, the empty, stale and refused states of the board and the bus views (a board refusal with the inspector open and a project picked, then a failed read, and a refusal with a conversation open), not found, the palette, the toast, keyboard and visible focus, the section strip at 360 and 400px, no sideways scroll, no script error and no request other than GET. A second test walks the Board, Inbox, Tasks, Agents and a decision trail through loading (a held read), normal, a failed read over kept rows, and a refusal by the bus (human-required) and for want of one (no-bus-host). It then checks that a refusal empties the palette and the Inbox count, from a view, from the palette's own reads and from the board page, and that palette labels are text.

No root CI workflow is configured for this package. These local tests are not a claim of CI, deployment, live-seat delivery or user acceptance.

User test and rollback

Gate E belongs to Jason on Monday 2026-09-14. Use Console to find who is waiting, open a registered repo seat, reply and see its next answer after a scan. Mark a completion Seen and find it again in Seen. Record any reason to open the board's own page or a repo seat terminal in DEFERRED.md. Pass is Jason's end-of-day say-so, not a test result. Keep #1507 open pending that ruling.

Rollback requires only stopping the WebUI and using the unchanged board at 7331. Revert the scoped WebUI commit to remove it; there is no data migration to undo. Seen and reply actions already taken belong to the board and are not rolled back.