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]>
325 lines
17 KiB
Markdown
325 lines
17 KiB
Markdown
# 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="<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.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 `<svg><use href="/icons.svg#i-inbox"/></svg>`. 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[/<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 `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: <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>`.
|
||
- **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.
|