Jason accepted the draft 1 direction on 2026-10-10; the refined Relay mark stays as a placeholder. Adds IMPLEMENTING.md (gap map against row 40 and the bus/CLI modules, strings, boundaries, acceptance, proposed rows for Sage), tokens.json and generated tokens.css using the webui's existing names, icons.svg, and tools/build-tokens.mjs and tools/check-design.mjs. Coverage statuses corrected against the code (digest implemented; mosaic stop is row 41; priority and agent state not in the bus reads). No packages/ change. Co-Authored-By: Claude Opus 5.5 <[email protected]>
17 KiB
Implementing the Mosaic Stack design
This brief is for the agents who build the design. Read it with
README.md (the spec) and the preview mosaic-stack-design.html.
Status
- 2026-10-10: Jason accepted the direction of draft 1. The mark stays the refined Relay as a placeholder until he revises it. Do not spend work on logo variants.
- Provisional defaults, carried in the files and reversible:
- Harbor palette, following the system light or dark setting;
- the web shows decisions and copies
mosaic decide; it never resolves them (REQ-DEC-3); mosaicwith no arguments opens the full-screen app. Today it exits 4 (usage).
- Not authorized by this brief: queue rows, dispatch, push, or any change to a security boundary. Sage owns the queue. Each implementation row needs its own brief and review.
Sources of truth
When two sources disagree, the higher one wins:
- The PRD (
docs/prd/mosaic-stack.md) and the repository invariants (AGENTS.md). - Shipped behaviour and strings in
packages/. The design quotes them, and the code is the reference. - This brief.
tokens.json, and the generatedtokens.css.- The preview. It uses sample data and its own internal names. Copy layout and states from it, not data, ids or code.
Files
| File | Use |
|---|---|
tokens.json |
Every design value: 10 palettes × light, dim and dark (14 colours each), type scale, radii, spacing, layout widths, terminal colours, status vocabulary |
tokens.css |
Generated from tokens.json. Do not edit by hand |
icons.svg |
Sprite: mark (placeholder) and 17 line icons i-board … i-flag |
brand/relay-refined.svg |
The placeholder mark as a standalone file |
tools/build-tokens.mjs |
Regenerates tokens.css; --check exits 1 when it is stale |
tools/check-design.mjs |
Browser checks for the preview (see Acceptance) |
Tokens
-
Names match the shipped console.
tokens.cssuses the property namespackages/webuialready uses:--canvas…--danger,--onAction(camelCase, asbrand.jssets it),--font,--mono,--r,--r-lg,--pad,--gap. It adds two names:--r-table(8 px);--strong(acolor-mixof--text).
-
Selectors. No attribute means Harbor, light or dark from
prefers-color-scheme.data-mode="light|dim|dark"sets the mode.data-palette="<id>"picks any other palette. The webui already sets both attributes on<html>. -
Values are the shipped values. For all 10 × 3 × 14 colours,
tokens.jsonequals whatpackages/webui/src/public/brand.jstokens()computes. Checked 2026-10-10: 0 mismatches. -
Adopting it in the webui:
- serve
tokens.css(add it to thefilesmap inpackages/webui/src/serve.mjs); - stop the inline
setPropertyloop inapp.jstheme(), which would override the stylesheet; - keep setting
data-paletteanddata-mode; - add a System choice that removes
data-mode; - keep the localStorage key
mosaic-console-appearance.
- serve
-
Changes the design makes to existing values:
--rgoes from 8 to 6 px;--r-lggoes from 14 to 10 px;--gapstays 12 px and--padstays 16 px;--shadowstays owned byapp.css. Use it only for overlays: the inspector over the table, the command palette and the toast.
-
Fonts. The console's CSP is
font-src 'self', so every font is self-hosted. Manrope 400 to 700 is already served. Add JetBrains Mono 400, 500 and 700. Manrope 800 is optional: titles may use 700. -
Type scale:
- page title 28/800;
- section 20/800;
- question 16/700;
- text 14/400 at 1.5;
- table cells 13;
- labels 11/700 in capitals with 0.06em tracking;
- mono 12.5.
Use
font-variant-numeric: tabular-numsin columns. -
Regenerate after editing
tokens.json:node docs/design/tools/build-tokens.mjs. Copy the result to the webui; the design directory stays the source.
Icons and the mark
- Format. 24-unit grid,
stroke="currentColor", width 1.75, round caps and joins, no fills. The webui inlines icons throughBrand.icon(id)with the same stroke rules. Replace those paths with the sprite's paths so there is one icon set, or serveicons.svgand use<svg><use href="/icons.svg#i-inbox"/></svg>. CSPdefault-src 'self'allows a same-origin sprite. - Labels. An icon never carries state alone. Every icon in navigation sits next to its text label.
- The mark. The
marksymbol usesfill="currentColor"andviewBox="0 0 16 17". Draw it at 16, 24 or 32 px. It is a placeholder: keep it in one component so a later swap touches one file.
Components
Every component needs the listed states, keyboard behaviour and
focus ring (--focus, 2 px, offset 2 px).
| Component | States and rules |
|---|---|
| Command bar | Business name, freshness line ("Vikunja last read 14:21:07 (12 s ago)"), Ctrl+K button. Warning colour plus words when stale |
| Section list | Icon + label + count; aria-current="page" on the active view. Below 760 px it becomes a horizontal strip that scrolls inside itself |
| Dense table | 13 px cells, padding 7/10, header labels in caps. Row hover, selected row, ↑/↓ moves, Enter opens the inspector. Wide tables scroll inside their own container |
| Inspector | Right drawer, 300 to 370 px. Over the table below 1100 px; a full-screen sheet below 760 px. Esc closes and returns focus to the row |
| Status mark | Glyph + word, never colour alone: ● working, ◐ waiting, ▲ error, ○ offline, – idle, ? unknown |
| Class chip | routine, within-role, cross-role, gated; BLOCKING is a separate chip |
| Copy command | Mono command + Copy button. Uses the shipped strings (below). Never runs anything |
| Toast | One line, polite live region, auto-dismiss; errors stay until dismissed |
| Command palette | Ctrl+K opens; type to filter views, decisions and tasks; Enter goes; Esc closes and restores focus |
| View states | Five per view: normal; loading (skeleton rows, aria-busy); empty (says what would appear and where it comes from); error (banner, last good data stays on screen with its read time); refusal (fail closed: the refusal code in mono and the file or rule that is missing) |
| Not found | Names the bad route, links to the Board, mentions Ctrl+K |
Views and the gap map
Existing routes in packages/webui are hash routes: #/ (the board),
#/inbox[/<id>], #/tasks[/<ref>], #/agents,
#/trail/task/<ref>, #/trail/decision/<id> (with ?kind=). New
views follow the same form: #/talk, #/queue, #/runs,
#/ledger, #/business, #/library, #/settings, #/setup.
Kinds of work:
- restyle: built; apply tokens, layout and states;
- extend: built; add design pieces whose data the current read already returns;
- new view: the data exists in a module; add a read-only endpoint to the webui server and a view;
- backend: the data is not produced or exposed yet;
- blocked: waits on another row or on Jason.
| View | Data today | Kind | Notes |
|---|---|---|---|
Board #/ |
GET /api/board, POST /api/seen (control board) |
restyle | Seat table with Input needed and Seen/Unsee already works |
Inbox #/inbox |
GET /api/bus/inbox: open decisions routed to human |
restyle | Detail, authorization sentence, copy commands and history already work |
| Inbox tabs: Routed to roles, Recent | none: inbox returns open decisions for the caller's role only |
backend | Needs a reader op for decisions routed to other roles and for closed ones |
| Decision Seen | broker decision.seen needs a human session (broker.mjs:614-633) |
blocked | The web holds a reader session. Writing Seen from the browser gives it human authority, a security-boundary change for Jason. The web may show existing seen events (bus.js already reads them). The TUI may write Seen once a CLI verb exists |
Tasks #/tasks |
GET /api/bus/tasks: title, bucket, done, markers, freshness |
restyle | |
| Task priority and requirement columns | not in task_current.fields; requirement is in the task.created event body |
backend | |
| Field writers in the task inspector | prose in docs/plans/2026-10-04_slice-1.md:62-72 |
backend | Encode the table as data first |
Agents #/agents |
GET /api/bus/agents: latest claim per role |
restyle | |
| Agent state and Input needed per role | the control board has state per seat; the bus has none per role | backend | Needs a seat-to-role mapping, or state on the bus |
| Launch budget (Opus n of 4, Sonnet n of 4) | ceiling only (vocabulary.mjs:85-86); session.launched has no model family |
blocked | Row 41 (S6) |
Stop launches (mosaic stop) |
broker launch.revoke / launch.restore, no CLI verb |
blocked | Row 41 (S6). Web shows the command only |
| Credentials (service, account, role, expiry) | Credentials.status() in packages/bus/src/credentials.mjs:118; not on the broker |
backend | Metadata only. Never a value, token or path |
Trail #/trail/... |
GET /api/bus/trail?subject=; filter chips shipped (bus.js:313) |
restyle | |
Talk #/talk |
the conversation History view exists; the live PM session is row 41 | blocked | Row 41 (S6). Reuse the controller/observer rules of packages/conversation |
Queue #/queue |
packages/queue/src/store.mjs list/show; no endpoint |
new view | Read-only. Rows move only through queue move |
Ledger #/ledger |
packages/ledger summarize, queueChecks |
new view | Read-only |
Business #/business |
packages/business loadBusiness, loadRole, resolveInstance, vocabulary.mjs |
new view | Read-only. A missing or invalid file shows the refusal state. Credential refs as metadata only |
Runs and releases #/runs |
readers live inside scripts/mosaic-task.mjs and scripts/release.sh, not exported |
backend | Extract a read-only module first |
Library #/library |
no meta-harness in committed code | blocked | Row 41 (S6) creates packages/harness |
Settings #/settings |
appearance exists in the webui header; notifier config readNotifyConfig |
extend | Appearance, notifications (binding names only), about (loopback, release). Read-only apart from appearance |
Set up #/setup |
validation lives in scripts/mosaic-config.mjs (a script) |
backend | First-run checklist; the installer is out of v1 scope |
| Command palette, five states, not found | client-side | extend |
The Board is the console home. The control board's own page on port 7331 stays until the console's Board covers it.
The full-screen terminal app
- Where.
packages/cli. Node built-ins only; the CLI has no dependencies and must keep none. - Talk. Embed the conversation
Terminalclass (packages/conversation/src/terminal.mjs:54,new Terminal({client, write, rows, onQuit})). Itsmain()owns the process, so do not call it. The app owns the screen and draws the conversation inside the Talk pane. - Size. Read
process.stdout.columnsandrows; redraw onresize. Designed at 120×36; every screen must fit 80×24. Below 100 columns:- the nav shows keys only;
- Inbox drops RAISED BY and REACHES YOU;
- detail moves below the list.
- Colour.
- Use the terminal's own 16 colours (
tokens.jsonterminal.ansi), never 256-colour or RGB. - Honour
NO_COLOR(any non-empty value): no colour codes; reverse video stays for the selected row, and every state still reads from its word and mark. - Not a TTY: refuse the full-screen app and point at the verbs.
- Use the terminal's own 16 colours (
- Refusal. In an agent run, exit 3 with the transport's message
(
packages/cli/src/transport.mjs:30). That is the same check the verbs use; do not write a second one. - Deciding. In Inbox, a digit picks an option. Then print the
CLI's lines and resolve only on
y. Reuse thedecidecode path rather than a copy of it. "Outcome unknown" offers no retry key. - Keys:
ctalk,iinbox,ttasks,aagents,rtrail;- ↑↓ or j/k move, Enter opens,
vopens the trail of the selection; :command line,?help,qquit;- Tab moves between the nav and the composer;
- the composer keeps the conversation keys.
- No-argument behaviour is a CLI change from exit 4 to opening the app. It is provisional; land it behind its own review.
Strings to keep exactly
These are shipped. Reuse them; do not reword.
- Decide:
your choice: <key> (<text>); this authorizes the action, or…; this declines the action;resolve <id8> with <key>? [y/N];resolved <full id>: <key>;outcome unknown: the broker may have recorded it. Check mosaic inbox or mosaic trail <id> before trying again.
- Refusal:
refused: this runs only from a human shell, outside any agent run (found <VARS> in the environment). - Web copy:
Copied: <command>;The clipboard is unavailable. The command is selected; press Ctrl+C to copy it.;Decisions are answered in a terminal with mosaic decide; Console only shows them.
- CLI list formats (
packages/cli/src/format.mjs):- inbox
<id8> <action> (<class>[, blocking]) raised <at> by <role>; - tasks
<ref> <state> <title>; - agents
<role> <holder_run> <harness|-> since <at>; - trail
<at> <table>#<seq> <summary>.
- inbox
- Trail sentences, empty states and not-found lines are in
packages/webui/src/public/bus.js(for exampleNo task <ref> in this business.). The "feat" labels there mark the gaps this design fills: poller status, bucket names, role instances and launch limits. - Bus wording stays in mono: event kinds (
task.changed.external), refusal codes (decision-closed,launch-revoked), task refs and ids.
Boundaries
- No Resolve button on the web. Copy the
mosaic decidecommand only (REQ-DEC-3). - Seen is not resolution. Board Seen/Unsee exists. Decision Seen from the web is blocked (see the gap map).
- Read-only web, apart from Board Seen/Unsee, the board's
existing reply path, and appearance settings stored in the browser.
Queue rows move only through
queue move; the business file is written by the human only. - Secrets. Show service, account, role and expiry. Never show values, tokens or file paths. Never log them.
- Server.
- Loopback only, no sign-in;
- keep the existing CSP, Host and Origin checks;
- new endpoints are
GETonly and return JSON; - no new dependencies in
packages/webuiorpackages/cli.
Acceptance
For every implementation row:
- the package's own suite passes:
node --test tests/inpackages/webuiorpackages/cli, plus the repository suites the row touches; node docs/design/tools/build-tokens.mjs --checkpasses when tokens changed;- every changed view shows its five states. Seeded tests cover the empty, error-with-stale-data and refusal states;
- browser tests (
packages/webui/tests/browser.mjsstyle):- no script errors;
- no sideways page scroll at 400 px;
- keyboard path: ↑/↓, Enter, Esc returning focus to the row;
- focus is visible;
- terminal app tests:
- every screen fits 120 columns and 80×24;
NO_COLORoutput has no escape codes;- an agent-run environment exits 3;
- strings in "Strings to keep exactly" are unchanged (a grep test is enough).
node docs/design/tools/check-design.mjs checks the preview itself:
- no script errors across 4 tabs, 13 views × 5 states and 13 terminal screens;
- no sideways scroll at 400 px;
- terminal widths;
- palette colours against
tokens.json.
It needs a local Chromium ($CHROMIUM or /usr/bin/chromium). Run it
after editing the preview.
Proposed rows (for Sage)
Draft order. These go after rows 41 and 42. Sage decides whether to queue them, and in what order.
-
Console tokens and shell restyle (frontend; Dewey's area).
- Scope:
- adopt
tokens.css; - command bar, section list, dense table, inspector, toast, command palette, not found;
- five states on Board, Inbox, Tasks, Agents and Trail.
- adopt
- No new data.
- Scope:
-
Read-only views with existing data (frontend plus webui server). Queue, Ledger, Business, Settings:
GETendpoints over the existing modules. -
Full-screen
mosaicapp (CLI): Inbox, Tasks, Agents, Trail, Help, decide, NO_COLOR and 80×24. Talk joins after row 41.- Needs Jason's confirmation of no-argument
mosaic.
- Needs Jason's confirmation of no-argument
-
Backend reads for the design (bus):
- decisions routed to roles and recent decisions;
- task priority and requirement;
- agent state per role;
- credential status on the broker;
- the field-writer table as data;
- a runs and release reader module.
Each is a separate small row.
-
After row 41: Talk view, launch budget,
mosaic stopin both surfaces, Library.
Open decisions for Jason
- Default palette and mode (provisional: Harbor, following the system setting).
- The final mark (placeholder: refined Relay).
- No-argument
mosaicopens the full-screen app (provisional). - Decision Seen from the web. It needs a human-authority write path from the browser. The recommendation is to keep it in the terminal for v1.