# Row 53 (#1542): Console tokens and shell restyle Dewey, 2026-10-10. Brief: `docs/plans/2026-10-10_design-implementation.md`, section "Console tokens and shell restyle" (origin/refactor a81915e8), with `docs/design/IMPLEMENTING.md` sections "Tokens", "Icons and the mark", "Components", "Strings", "Boundaries" and "Acceptance". Lead decision 81 (Harbor following the system setting by default, the Relay placeholder mark stays, no Decision Seen from the web). Built on 923957e2; no file under `packages/webui`, `docs/design` or the test scripts changed between it and the gate base 8a7871ff (only `scripts/mosaic-task.mjs`, row 56). Candidate: 16 files, uncommitted in the canonical checkout, listed in `candidate-manifest.sha256` beside this file (round 1 manifest sha256 `75569953c1a2cd7b7266208d6e6607290335f1bfe0cf94d96ed224e480a1dda8`; rounds 2 and 3 below; round 3's is current). ## Round 3 (answers #1542 comments 27163 and 27165) Scope from Sage: two fixes, each with a test. 1: a board 403 followed by a 503 or 500 must not bring back the refused scan's rows or waiting card (Filbert C4, Darkwing P9). 2: a board 403 must clear an open conversation's history (Darkwing P10). Round 3 candidate: the same 16 files, manifest sha256 `2562c05d4a92f90096c0fe68ef8c2adfa7244fa547f3390155f5bee5cce64cba` (`candidate-manifest.sha256` beside this file, replacing round 2's `78c3aef9`). Three files changed from round 2: `README.md`, `src/public/app.js`, `tests/shell.test.mjs`. The round 2 to round 3 delta is `round3.patch` beside this file. Applied to the round 2 files, it reproduces the manifest (16 OK, checked). Base for the gate: 03969219. ### Fixes | Item | Change | Test | |---|---|---| | Filbert C4, Darkwing P9: a 403 then a 500 or 503 showed the refused scan's table and waiting card again | `error()` now draws the empty board whenever it holds no data, not only on a refusal. It clears `#waiting`, `#seen` and `#sessions`, sets the three counts to `–`, closes the inspector, and says why in the tree, the footer and `#status`: "the board refused the read" or "the read failed". | Shell browser test: after the refusal, `brd.unavailable()` (503) and a refresh. The banner is `banner err` with "No board data loaded."; `#board-view` shows again; no session row, open or history button, waiting card or seen card; the three counts are `–`; the footer and `#fresh-board` read "Board not read: the read failed", `#status` "Not read: the read failed", the tree "Not read: the read failed.". Screenshot `board-refused-then-failed-400`. | | Darkwing P10: a 403 with History open left `#conversation` and the old `#conv-log` messages | On a refusal with a conversation open, `error()` empties `#conv-log` and calls `closeConversation()`, which hides the panel and its reply form. | A session with `registered` set; History open; the fixture message "Fixture history line" in `#conv-log` and the reply form shown. After a 403: `#conversation` hidden, no `has-conversation` on `
`, `#conv-log` has no child, `#conv-form` not visible, the board hidden and the refusal state shown. Then recovery. | Test fixtures in `shell.test.mjs`'s stub board: `one()` adds a third session that is seen (so the seen card is tested), `registered()` answers one live session with a conversation, `unavailable()` answers 503, and with status 200 the stub answers `/api/conversations` and `/api/conversation` with one fixture page. The new assertions sit in the "shell in a browser" test, where the board refusal state already lives. ### Also answered | Item | Done | |---|---| | Filbert T7: the counts after a refusal | Asserted: `#waiting-count`, `#seen-count` and `#session-count` are `–` after the 403 and after the 503 that follows it. | | Filbert N7 (my call): the project filter on a refusal | Tested, not changed: a project is picked before the refusal (`aria-pressed` true), and after recovery the "all" filter is the pressed one. | | README | The refusal line in the command bar section now names the cards, the project filter, the empty board on a later failure and the closed conversation. The shell tests line names the two new cases. | ### Changes in behaviour beyond the two fixes - A failed read before the first scan (no data yet, not a refusal) now draws the empty board with "Not read: the read failed" in the tree, footer and `#status`, where round 2 left the skeleton rows, "Loading board…" and "Loading projects…" under the error banner. This is the same code path as the C4 fix; the banner was already shown in both. - `error()` no longer calls `convForm()`. With P10, a refusal closes the conversation and `convForm()` does nothing without one; a non-refusal failure with no data never had an open conversation. The `error-noconvform` mutant survived for that reason and is dropped. ### Round 3 mutants Each applied to a scratch copy of the round 3 tree, then `shell.test.mjs` run (`~/dewey-scratch/r53b/r3/mutants.py`, outputs in `r3/out/`). 13 of 13 killed, every one by the "shell in a browser" test. | Mutant | Change | Killed at | |---|---|---| | c4-no-clear | no-data branch leaves the cards and the table | no rows or cards after the 503 (11, expected 0) | | c4-keep-sessions | the table kept | same (7) | | c4-keep-waiting | the waiting cards kept | same (2) | | c4-keep-seen | the seen cards kept | same (2) | | c4-refused-only | the empty board only on a refusal (round 2) | footer "refused" after the 503 | | c4-footer-refused | footer always says "refused" | footer after the 503 | | c4-status-refused | `#status` always says "refused" | `#status` after the 503 | | c4-tree-refused | the tree always says "refused" | tree after the 503 | | error-keeps-counts | the counts left as they were | `#waiting-count` after the 403 | | error-noinspect | the inspector left open | `#inspector` hidden after the 403 | | error-keeps-project | a 403 keeps the project filter | "all" filter pressed after recovery | | p10-no-close | the conversation left open | `#conversation` hidden after the 403 | | p10-no-clear | the panel closed but the history kept | `#conv-log` empty after the 403 | ### Round 3 gate Worktree at 03969219 plus the 16 round 3 files (`sha256sum -c` of the manifest: 16 OK), node_modules linked from the checkout, TMPDIR in scratch, `DOCKER_HOST` pointed at a missing socket, suites sequential, 2026-10-10T19:13:43Z to 19:18:19Z. `core.hooksPath` unset. Script `~/dewey-scratch/r53b/r3/gate.sh`, outputs `~/dewey-scratch/r53b/r3/gate/out/`. | Suite | Result | |---|---| | `node --test 'packages/webui/tests/*.test.mjs'` | 27 pass, 0 fail | | node suites of business, bus, cli, control-board, conversation, discord, ledger, mosaic, queue, runs, seat, tasks | 60, 67, 66, 124, 182, 178, 78, 69, 148, 41, 19, 51 pass; 0 fail in each | | `node docs/design/tools/build-tokens.mjs --check` | rc 0, "tokens.css is current" | | test-auth | 15 passed, 0 failed | | test-conductor | 17 passed, 0 failed | | test-config | 24 passed, 0 failed | | test-discord | 66 passed, 0 failed | | test-extension-package | 18 passed, 0 failed | | test-foundation | 44 passed, 0 failed | | test-queue | 27 passed, 0 failed | | test-release, no Docker | 4 passed, 0 failed (state-machine cases skipped: daemon unavailable) | | test-task, no Docker | 26 passed, 0 failed (adapter seam, workspace and live cases skipped) | Round 2's gate had Docker for test-release and test-task (14 and 98). Round 3 changes nothing those Docker cases run; Sage's gate rerun covers them. ### Not in this round Darkwing note 3, the refresh-timer focus race in `bus.js` `render()`: it is in HEAD, so per Sage it goes to row 54 with its own test, named there as a fix to HEAD behaviour beside the H1 `keep()`/`restore()` fix. ## Round 2 (answers #1542 comments 27143 and 27148) Round 2 candidate: the same 16 files, manifest sha256 `78c3aef963b7e35500c9f8afa6341057f1a6372ced70915d175e507b5fac8406` (`candidate-manifest.sha256` beside this file, replacing round 1's `75569953`). Five files changed from round 1: `README.md`, `src/public/app.js`, `src/public/bus.js`, `src/public/shell.css`, `tests/shell.test.mjs`. The round 1 to round 2 delta is `round2.patch` beside this file. Base for the gate: 2d4d4e7d (nothing under `packages/webui`, `docs/design` or `scripts` changed since 8a7871ff). ### Blocking items | Item | Change | Test | |---|---|---| | Filbert C1, Darkwing 1: a board 403 left the inspector, project tree, counts, footer and `#status` "Scanned" line | `error()` on a 403 drops `data`, `selected` and `project`, closes the inspector, and sets the tree to "Not read: the board refused the read.", the three counts to `–`, the footer to "Board not read: refused" and `#status` to "Not read: the board refused the read". The next good read restores them through `accept()` and `render()`. | Shell browser test opens the inspector and checks the footer "waiting 1 · working 1" and two tree counts before the refusal; after it, the inspector is hidden, `` has no `has-inspector`, the tree has no `.count` or `[data-project]`, the footer and `#fresh-board` read "Board not read: refused", `#status` has no "Scanned". After recovery the footer is back. | | Filbert C3: the freshness line called a refusal "stale" | `fresh()` with no data: "Board not read: refused", "Board not read: the read failed" or "Board not read yet". | Same test: `#fresh-board` equals "Board not read: refused". | | Filbert C2, Darkwing 2 and note 6: a bus refusal left the Inbox count, BLOCKING chip and palette rows | `forget()` clears `last` and the Inbox count on any refusal: in the view's read, in the board route's inbox read (was `catch {}`), and in `pkOpen`'s refetch, which then redraws the section list. A refused read anywhere drops every kept bus read, so the pre-refusal rows can't come back as stale on a later failure either (Darkwing's P7). The bus freshness line reads "Bus not read: refused". | Five-states test: `#sections` first reads "Inbox 1 1 blocking"; after the refusal it has no `.count` or `.tag-block`, and Ctrl+K lists only the four views. A second step refuses only `pkOpen`'s own refetch and checks the kept tasks and the count are gone. On the board page a `no-bus-host` refusal of the board route's inbox read drops the count and the palette shows views only. | | Filbert T1, Darkwing 4 (M01): only 403 was tested as a refusal | none needed | The five-states refusal step runs for `human-required` (403) and `no-bus-host` over kept rows: banner code, 0 rows, "Nothing to show.", "Bus not read: refused", no section counts, then recovery. | | Darkwing 3: tests with the inspector open | as C1 | as C1 | | Darkwing 5 (M08): palette labels with markup | none needed | The inbox question is ``) | Unchanged. A Seen 403 now also drops the board data, the same as a read 403; since only control-board's Host/Origin check gives it, the read would be refused too. |
### Round 2 mutants
Each applied to a scratch copy of the round 2 webui, then `shell.test.mjs`
run (`mut/tools/mutants.py`, `run.sh`). 20 of 20 killed.
| Mutant | Change | Killed by |
|---|---|---|
| C1-keep-data | a board 403 keeps `data` | shell browser test |
| C1-keep-inspector | a board 403 keeps `selected` and the inspector | shell browser test |
| C1-keep-footer | a board 403 leaves the footer | shell browser test |
| C3-fresh-wording | "stale" wording on a refusal | shell browser test |
| C2-render-forget | the view's refused read doesn't `forget()` | five-states test |
| C2-pk-forget | `pkOpen`'s refused refetch doesn't `forget()` | five-states test |
| C2-board-forget | the board route's refused inbox read doesn't `forget()` | five-states test |
| C2-forget-keeps-count | `forget()` clears `last` but not the count | five-states test |
| M01-refusal-403-only | `refusal()` only for 403 | five-states test |
| M08-pk-label-raw | palette label set as HTML | five-states test |
| nav-wraps | the round 1 wrapping section list | shell browser test (strip) |
| toast-display-none | the round 1 `display:none` toast | shell browser test |
| M09-offset-0 | focus `outline-offset: 0` | shell browser test |
| M16-no-tabular | no `tabular-nums` | shell browser test |
| N1-local-time | board time in local time | shell browser test |
| N2-one-copy | one copy of the duplicated task string changed | strings test |
| T6-ctrl-alt-k | Ctrl+Alt+K opens the palette | shell browser test |
| T3-err-toast-hides | the error toast closes after 2.6 s | shell browser test |
| T2-stale-sticks | `accept()` keeps `failed` | shell browser test |
| bus-fresh-refused | bus refusal shows the round 1 freshness wording | shell browser test |
### Round 2 gate
Worktree at 2d4d4e7d plus the 16 round 2 files (`sha256sum -c` of the
manifest: all OK), node_modules linked from the checkout, TMPDIR in scratch,
suites sequential, 2026-10-10T18:15:17Z to 18:19:16Z. Outputs:
`~/dewey-scratch/r53b/gate/out/`.
| Suite | Result |
|---|---|
| `node --test 'packages/webui/tests/*.test.mjs'` | 27 pass, 0 fail |
| `node docs/design/tools/build-tokens.mjs --check` | rc 0, "tokens.css is current" |
| test-auth | 15 passed, 0 failed |
| test-conductor | 17 passed, 0 failed |
| test-config | 24 passed, 0 failed |
| test-discord | 66 passed, 0 failed |
| test-extension-package | 18 passed, 0 failed |
| test-foundation | 44 passed, 0 failed |
| test-queue | 27 passed, 0 failed |
| test-release | 14 passed, 0 failed |
| test-task, Docker present | 98 passed, 0 failed |
The round 1 packet follows unchanged, except where rounds 2 and 3 supersede it
(the nav deviation is withdrawn; the manifest above replaces `75569953`).
# Round 1 packet
## What changed
All in `packages/webui`. No new endpoint, read, write or dependency.
| File | Change |
|---|---|
| `src/public/tokens.css` | New. Byte copy of `docs/design/tokens.css`. |
| `src/public/icons.svg` | New. Byte copy of `docs/design/icons.svg`: 17 icons and the `mark` symbol. |
| `src/public/shell.css` | New. The components on top of the tokens: command bar, freshness line, section list with icons, dense table with tabular numbers, inspector, status mark (glyph plus word), class chip and BLOCKING chip, copy command, toast, Ctrl+K palette, empty, not found, banners (`err`, `ref`), skeleton rows, `@font-face` for JetBrains Mono, `--r` 6px and `--r-lg` 10px use, focus rings, the 400px layout. Loaded last. |
| `src/public/assets/fonts/jetbrains-mono-{400,500,700}.woff2`, `jetbrains-mono-OFL.txt`, `jetbrains-mono-sources.txt` | JetBrains Mono 2.304 from the official release archive (URL and archive sha256 in the sources file), each file's sha256, and the OFL text from the archive. |
| `src/public/index.html` | Loads `tokens.css` after `shared/app.css` and `shell.css` last; no `data-mode` on ``; the mark from the sprite in the wordmark; the Ctrl+K button, freshness line, System option, banner error element, board refusal empty state, board skeleton, toast and palette `