# 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); - `mosaic` with 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: 1. The PRD (`docs/prd/mosaic-stack.md`) and the repository invariants (`AGENTS.md`). 2. Shipped behaviour and strings in `packages/`. The design quotes them, and the code is the reference. 3. This brief. 4. `tokens.json`, and the generated `tokens.css`. 5. 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.css` uses the property names `packages/webui` already uses: `--canvas` … `--danger`, `--onAction` (camelCase, as `brand.js` sets it), `--font`, `--mono`, `--r`, `--r-lg`, `--pad`, `--gap`. It adds two names: - `--r-table` (8 px); - `--strong` (a `color-mix` of `--text`). - **Selectors.** No attribute means Harbor, light or dark from `prefers-color-scheme`. `data-mode="light|dim|dark"` sets the mode. `data-palette=""` picks any other palette. The webui already sets both attributes on ``. - **Values are the shipped values.** For all 10 × 3 × 14 colours, `tokens.json` equals what `packages/webui/src/public/brand.js` `tokens()` computes. Checked 2026-10-10: 0 mismatches. - **Adopting it in the webui:** - serve `tokens.css` (add it to the `files` map in `packages/webui/src/serve.mjs`); - stop the inline `setProperty` loop in `app.js` `theme()`, which would override the stylesheet; - keep setting `data-palette` and `data-mode`; - add a System choice that removes `data-mode`; - keep the localStorage key `mosaic-console-appearance`. - **Changes the design makes to existing values:** - `--r` goes from 8 to 6 px; - `--r-lg` goes from 14 to 10 px; - `--gap` stays 12 px and `--pad` stays 16 px; - `--shadow` stays owned by `app.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-nums` in 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 through `Brand.icon(id)` with the same stroke rules. Replace those paths with the sprite's paths so there is one icon set, or serve `icons.svg` and use ``. CSP `default-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 `mark` symbol uses `fill="currentColor"` and `viewBox="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[/]`, `#/tasks[/]`, `#/agents`, `#/trail/task/`, `#/trail/decision/` (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 `Terminal` class (`packages/conversation/src/terminal.mjs:54`, `new Terminal({client, write, rows, onQuit})`). Its `main()` 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.columns` and `rows`; redraw on `resize`. 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.json` `terminal.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. - **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 the `decide` code path rather than a copy of it. "Outcome unknown" offers no retry key. - **Keys:** - `c` talk, `i` inbox, `t` tasks, `a` agents, `r` trail; - ↑↓ or j/k move, Enter opens, `v` opens the trail of the selection; - `:` command line, `?` help, `q` quit; - 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: (); this authorizes the action`, or `…; this declines the action`; - `resolve with ? [y/N] `; - `resolved : `; - `outcome unknown: the broker may have recorded it. Check mosaic inbox or mosaic trail before trying again`. - **Refusal:** `refused: this runs only from a human shell, outside any agent run (found in the environment)`. - **Web copy:** - `Copied: `; - `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 ` ([, blocking]) raised by `; - tasks ` `; - agents `<role> <holder_run> <harness|-> since <at>`; - trail `<at> <table>#<seq> <summary>`. - **Trail sentences, empty states and not-found lines** are in `packages/webui/src/public/bus.js` (for example `No 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 decide` command 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 `GET` only and return JSON; - no new dependencies in `packages/webui` or `packages/cli`. ## Acceptance For every implementation row: - the package's own suite passes: `node --test tests/` in `packages/webui` or `packages/cli`, plus the repository suites the row touches; - `node docs/design/tools/build-tokens.mjs --check` passes 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.mjs` style): - 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_COLOR` output 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. 1. **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. - No new data. 2. **Read-only views with existing data** (frontend plus webui server). Queue, Ledger, Business, Settings: `GET` endpoints over the existing modules. 3. **Full-screen `mosaic` app** (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`. 4. **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. 5. **After row 41:** Talk view, launch budget, `mosaic stop` in both surfaces, Library. ## Open decisions for Jason 1. Default palette and mode (provisional: Harbor, following the system setting). 2. The final mark (placeholder: refined Relay). 3. No-argument `mosaic` opens the full-screen app (provisional). 4. 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.