The Console gains three read-only views. Queue lists every row from
rows() in packages/queue (lead decision 83: the same lock-free read as
`queue list`, writing nothing), run in a child with a timeout and an
output cap; a row page shows the full row. Business shows names, an
allowlist of vars, arbiters, authority per action and credential
metadata (service, account, role, date, never a value, file or
variable). Settings shows RELEASE, the board origin and the notifier
binding. Every route is GET only, and every string in a body or
refusal passes a redactor: the config directory and dataRoot become
<config> and <dataRoot>, other absolute, ~/ and file:// paths <path>,
and 17 to 20 digit runs <id>. A queue-read that fails to start sends a
fixed message, never node's stderr.
Four fixes to HEAD behaviour, each with a test: the bus view's refresh
timer is cleared at render, a same-page refresh keeps focus on the H1,
#conv-pick is emptied when a conversation opens, and error() returns
focus to <main> only when something had focus. Filbert's T8 tests the
failed first read.
Dewey built it in two rounds. Round 1 (dba2429e) got changes from
Filbert (27185: After rendered [object Object], ~/ and :/ paths leaked)
and Darkwing (27186: Arbiters always none, queue-read import failure
leaked a file:// path and stack). Round 2 (afb2ae0e) was approved by
Filbert (27190) and Darkwing (27191, correction 27192). Sage's gate on
d64f434f plus the candidate: 13 package node suites 0 failed (queue
149, webui 39), every scripts/test-*.sh 0 failed (task 98/0 with
Docker, 26/0 without; release 14/0), build-tokens --check current.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
334 lines
20 KiB
Markdown
334 lines
20 KiB
Markdown
# 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:
|
|
|
|
```sh
|
|
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. Settings (row 54) shows the same control and nothing else that
|
|
changes.
|
|
- 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, and is empty while the list is read or when
|
|
that read fails. 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`, `no-bus-host`,
|
|
or row 54's `queue-refused`) never shows kept rows (row 53), and it drops
|
|
every kept read: the Inbox count and the palette's decisions, tasks and
|
|
queue rows 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,
|
|
tasks and (row 54) queue rows from the last queue read; 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. If that removes what had focus, focus
|
|
moves to the page.
|
|
- 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.
|
|
|
|
## Queue, Business and Settings (row 54, #1543)
|
|
|
|
Three read-only views, in the section list and the palette. Each has the
|
|
same five states as the bus views: loading (the skeleton names the source),
|
|
normal, empty, a failed read over kept data (labelled stale, with its time),
|
|
and a refusal that shows nothing kept. The command bar names the source:
|
|
"Queue read …", "Business not read: refused". A refusal shows the code in
|
|
mono and the module's own message, redacted by the server.
|
|
|
|
- `#/queue` lists every row with its state, owner, reviewers, brief and last
|
|
update, filterable by state. `#/queue/<id>` adds the gate, issues, claim,
|
|
`after`, note and review rounds. Rows move only with
|
|
`scripts/mosaic queue move` in a terminal; the row page shows that command
|
|
with a Copy button and has no button that moves a row. The brief is shown
|
|
as its path and anchor, because Console serves no repository files.
|
|
- `#/business` shows the business named by `--business`, else the running
|
|
bus host's business: the human, the arbiters as instance and kind ("pm
|
|
(delivery), cto (technical)"), projects, launch rules, the vars
|
|
on an allowlist, each role instance with its holder and harness, the
|
|
authority of each instance for each action as the business package
|
|
classifies it (`resolveInstance`, `classify`), and credentials as
|
|
metadata only: service, account, role, expiry or rotate-by date, and a
|
|
state worked out from the date alone. A missing or invalid business file
|
|
is a `not-configured` refusal with the business module's own text.
|
|
- `#/settings` shows appearance (the row 53 control, kept in this browser),
|
|
the notifier binding by name only, and about: the Console's loopback
|
|
address, `RELEASE`, the business and the board URL. A Settings refusal
|
|
still shows appearance, because it needs no read.
|
|
|
|
Console never reads a token, a token file or a credential variable, and no
|
|
response, page or log carries one, a file path or a Discord channel, guild or
|
|
user id. The server redacts every message and string field before it answers
|
|
(`redactor` in `src/reads.mjs`): the config directory becomes `<config>`, the
|
|
data root `<dataRoot>`, any other absolute or `~/` path `<path>`, a 17 to 20
|
|
digit run `<id>`, and a JSON parse error's quote of the file is dropped. The
|
|
startup log goes through the same redactor. A path starts at the beginning
|
|
of the text or after a space, an opening bracket, a quote, `=`, `,`, `<` or
|
|
`:`, so `file:/x` and `file:///x` are redacted and `https://host/x` is not.
|
|
Known gap: a path stops at the first space, so a path containing a space is
|
|
redacted only up to that space and the rest shows. A bare `~` or `~user/`
|
|
is not treated as a path, and neither is a relative path.
|
|
|
|
## 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`.
|
|
|
|
GET `/api/queue`, `/api/queue/<id>`, `/api/business` and `/api/settings` are
|
|
row 54's reads (`src/reads.mjs`), under the same Host, Origin and CSP rules.
|
|
Any other method is 405, and a queue id that is not a positive integer is
|
|
400. The queue is read in a child process (`src/queue-read.mjs`) through
|
|
`rows()` from `packages/queue/src/store.mjs` (lead decision 83): the same
|
|
read as `queue list`, with no write and no network. Like `list`, it takes
|
|
the queue lock only to recheck a disagreement before reporting it. The child
|
|
imports the store inside its `try`, so a store that fails to load reports
|
|
"the queue read failed". Only the store's exit 2 (invalid data or refused)
|
|
forwards the child's text, as `queue-refused`; any other exit is the fixed
|
|
`read-failed` message "the queue read failed (exit N)", because that stderr
|
|
may be Node's own, with a `file://` path and a stack. The business
|
|
read uses `packages/business` in process. No read writes, calls out or adds a
|
|
dependency. Answers carry `at`; failures are `{ error, message }` with 503
|
|
for `not-configured`, `no-bus-host`, `queue-refused` and `read-failed`, 404
|
|
for a row that isn't there, 400 for a bad address and 502 otherwise. Settings
|
|
answers 200 even without a system config, so appearance always works; its
|
|
notifier part then says `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
|
|
|
|
```sh
|
|
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 --test packages/webui/tests/reads.test.mjs packages/webui/tests/views.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 failed first read drawn empty, a board refusal with the inspector open and a project picked, then a failed
|
|
read, a failed session list, and a refusal with a conversation open and where
|
|
focus lands after it), 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.
|
|
|
|
Row 54's tests share the seeded fixture in `reads-fixture.mjs`: a scratch
|
|
queue whose row note names an id and token paths shaped like real notes
|
|
(absolute, `~/` and after `file:`; the store refuses `<` in a note, so the
|
|
`<…>` case is in the redactor test), and a business,
|
|
notifier config and Discord binding that hold token files, an environment
|
|
variable name, an agent `model` var holding a path, and guild, channel, bot
|
|
and user ids. Row 9's After entry has the `{id, when}` shape the real queue
|
|
stores. `reads.test.mjs` checks each
|
|
route's body and status, the redactor, the credential state, methods and
|
|
Origin, and that no body carries any seeded value or the temporary directory;
|
|
that a `store.mjs` with a syntax error, or none, gives the fixed message with
|
|
no path; and that the child's timeout, output cap and non-2 exits hold, over a
|
|
fake child script.
|
|
`views.test.mjs` walks Queue, a queue row, Business and Settings through the
|
|
five states over a stub read, with each refusal code the view can meet, at
|
|
400px; checks that a refusal leaves no queue row in the palette, that no view
|
|
has a control but Copy and Read again, and that the row page shows the
|
|
`queue move` command, that the 10 s refresh keeps focus on a Settings mirror
|
|
select, and that the palette keeps its queue rows after its own inbox and tasks
|
|
reads answer; then, over the seeded fixture, that the Arbiters line reads the
|
|
business file's object, that neither the page nor
|
|
any response it read carries a seeded value, that a missing or invalid
|
|
business file shows the module's text, and that the startup log names no
|
|
config path.
|
|
|
|
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.
|