Repository navigation
Console: layout fixes from the 2026-10-23 review #912
Description
Part of #906.
Scope
Seven layout faults in the console are fixed. After this PR the page no longer shifts when a scrollbar appears; the worker page has no breadcrumb; the warm-pool tab has a line between its summary and its table; the fleet table's row ⋯ sits inside its cell; Inbox row buttons have room above and below; a toast's close button sits beside its text, not over it; and a ⋯ list never repeats the button beside it. UI only: no route, contract or daemon change.
In short
The console's layout stops shifting, cramping and overlapping in the seven places the 2026-10-23 review found. The one change in behaviour: a ⋯ list beside a button (in a page header, a drawer or an Inbox row) no longer lists that button's action. When that leaves the list empty, the ⋯ is not drawn.
flowchart LR
E[Item's actions] --> B[First safe action → button]
E --> M["⋯ list: the rest (changed: without the button's action)"]
M -->|empty| N[No ⋯ drawn]
Before and after, a drained worker's page header:
| Button | ⋯ list | |
|---|---|---|
| Today | Undrain | Undrain, … |
| After | Undrain | the worker's other actions; no ⋯ when there are none |
If this goes wrong, a user would notice an action missing from both the button and the ⋯, or a box that moved.
Technical spec
Facts
| # | Fact | Source |
|---|---|---|
| F1 | html, body set only margin: 0; no scrollbar-gutter anywhere in ui/src; the document scrolls (.app-main has no overflow-y) |
ui/src/ds/foundation.css:81-84; ui/src/ds/sidebar.css:9-15; rg scrollbar-gutter ui/src → no match at 192cb5fbb |
| F2 | The worker page renders <nav className="crumbs" aria-label="Breadcrumb"> with a "Fleet" link to page.back; .crumbs is styled in master-detail.css and used nowhere else |
ui/src/screens/fleet/worker-page.tsx:73-77; ui/src/ds/master-detail.css:61-80; rg crumbs ui/src → those two files |
| F3 | Browser tests click that breadcrumb to return to the Fleet with its ?page= kept |
e2e/console/long-lists.spec.ts:378-423; e2e/console/workers.spec.ts:933,1133,1291 |
| F4 | The warm tab draws Facts then DataTable in one Panel with no rule between; .panel .table-card drops the table's border |
ui/src/screens/fleet/worker-page.tsx:405-419; ui/src/ds/table.css:30-33 |
| F5 | .table .table-menu { width: 3.5rem }; cells are border-box with padding: var(--space-4) var(--space-3) and the last cell padding-right: var(--space-6); the ⋯ IconButton is 40px wide |
ui/src/ds/table.css:35-52,214-217; ui/src/ds/foundation.css:78; ui/src/ds/button.css:76-88 |
| F6 | .inbox-actions has padding-right: var(--space-4) and no vertical padding; the 40px buttons sit in a row whose padding is on the link only |
ui/src/screens/inbox/inbox.css:96-113 |
| F7 | .toast-close is position: absolute; top/right: var(--space-2) over .toast-content (padding 12px 16px); the close is a 40px bordered IconButton |
ui/src/ds/toast.css:35-39,72-76; ui/src/ds/toast.tsx:57-65 |
| F8 | ActionBar shows the first non-danger entry as a button and passes every entry, that one included, to RowMenu |
ui/src/ds/menu.tsx:92-108 |
| F9 | An Inbox row renders actions.fix as a button and actions.menu as a RowMenu; inboxActions keeps the fix in menu, and the Inbox drawer feeds the same menu to ActionBar |
ui/src/screens/inbox/index.tsx:41,142-156; ui/src/model/actions.ts:345-350; ui/src/state/inbox.ts:146-148 |
| F13 | Headless Chromium runs with --hide-scrollbars, so a scrollbar takes no layout space in the console lane |
playwright-core 1.63.0 lib/coreBundle.js:43338-43343; playwright.config.ts:18 |
| F14 | A closed Base UI menu renders no popup, so static markup shows the ⋯ trigger only | @base-ui/react@1.9.0/menu/portal/MenuPortal.js:29-32; ui/src/ds/menu.tsx:79 |
| F15 | The worker page header's ⋯ is already pinned in the browser: menuitem "Undrain" visible, entries ["Undrain", "Remove worker"] |
e2e/console/actions.spec.ts:450-485 |
| F16 | WorkerPageView.back is read only by the breadcrumb; .crumb rules sit beside .crumbs |
ui/src/state/workers.ts:78,152; ui/src/screens/fleet/fleet.test.tsx:119; ui/src/ds/master-detail.css:61-81 |
| F17 | #915 edits inboxActions (ui/src/model/actions.ts) and ui/src/state/inbox.ts, not ui/src/screens/inbox/index.tsx |
#915 Modules touched |
| F10 | ActionBar is used by the Inbox drawer, the lease drawer, the device drawer, the Queue drawer and the worker page header |
rg ActionBar ui/src --glob '!ds/*' → inbox/index.tsx:41, leases/drawer.tsx:27, fleet/device-drawer.tsx:34, queue/index.tsx:126, fleet/worker-page.tsx:71 |
| F11 | DESIGN.md: "a drawer or page header shows its item's first safe action as a secondary button beside its own ⋯"; "A list with no entry draws no ⋯"; toasts have "a Close button"; buttons are "at least 40px tall"; spacing picks from the 4px scale | docs/internal/DESIGN.md:119-124,168-174,180-184 |
| F12 | Unit tests run in Node without a DOM, a ds component with renderToStaticMarkup; the browser lane is Playwright under e2e/console/ |
ui/AGENTS.md Rules; vitest.config.ts:18-20; package.json:55 |
Modules touched
Searches: rg "crumbs|table-menu|inbox-actions|toast-close|ActionBar|scrollbar" over ui/src, e2e/console, docs/internal/DESIGN.md, website/src/content/docs.
flowchart LR
WP[screens/fleet/worker-page.tsx *] --> DS[ds/menu.tsx ActionBar *]
IN[screens/inbox/index.tsx *] --> DS
IN --> A[model/actions.ts]
WP --> T[ds/table.css *]
TO[ds/toast.tsx, toast.css *]
F[ds/foundation.css *]
MD[ds/master-detail.css *]
Also screens/inbox/inbox.css, screens/fleet/fleet.css (warm divider), docs/internal/DESIGN.md (Menu and Toast paragraphs), e2e/console/long-lists.spec.ts, e2e/console/workers.spec.ts, e2e/console/actions.spec.ts, and a new e2e/console/layout.spec.ts. The Inbox row drops the button's entry in screens/inbox/index.tsx (InboxActions), not in inboxActions or state/inbox.ts, so the Inbox drawer keeps Undrain (as its ActionBar button) and #915's files are untouched (F17).
Contract and event changes
None (searched: no file under src/ changes).
Other code on the same state
UI state only: the ⋯ list's entries. Searched rg "RowMenu|ActionBar|actions.menu" ui/src (12 hits). Every caller that shows a button beside a ⋯ goes through ActionBar (F10) or InboxActions (F9); DataTable's row ⋯ shows no button beside it and keeps every entry. #910 adds tabs to the device drawer and keeps its ActionBar as is; no shared lines.
Failure modes
None: the task crosses no module boundary and writes no state.
Rules in play
- DESIGN.md Spacing: "Pick from the scale; never a value between" — every new value is a
--space-*token. - DESIGN.md Spacing: "Buttons and inputs are at least 40px tall" — the toast close keeps a 40px hit area; only its border goes.
- DESIGN.md Menu: "A list with no entry draws no ⋯" — kept; the paragraph gains "and the list does not repeat that action".
- documentation rule 9 — the console's public look changes in no documented way (
console.mdxnames no ⋯ entries; checkedrg "⋯|menu" website/src/content/docs/docs/worker/console.mdx), so no website page changes.
Tests
Seam: the browser lane, e2e/console/ against a started daemon (model: e2e/console/long-lists.spec.ts), plus ActionBar with renderToStaticMarkup (model: ui/src/ds/chart.test.tsx).
- The page reserves room for a scrollbar: the computed
scrollbar-gutterofhtmlisstableon every page (Overview, Fleet, a worker page). A layout measure cannot fail in the lane, which hides scrollbars (F13). - An operator opens a worker page: no element named "Breadcrumb"; on the Warm pool tab a 1px
--borderline sits between the facts and the table. The tests that clicked the breadcrumb to return with the Fleet's page kept (long-lists.spec.ts:378-423,workers.spec.ts:933,1133) use the browser's Back, which keeps it; the keyboard-only test (workers.spec.ts:1252,1290-1293) follows the sidebar's Fleet link by keyboard. - An operator looks at fleet and Inbox rows: the fleet row's ⋯ lies inside its cell with 24px to the table's right edge; each Inbox row button has 8px between it and the row's top and bottom lines.
- An operator sees a toast: the close button lies inside the toast's box; the title's box does not overlap it; on a one-line toast the text's and the button's vertical centres are within 4px. Cases: a one-line success toast, a drain toast with a line and Undo.
- An operator opens a ⋯ beside a button: drained, connected worker page header → Undrain button and no ⋯, its only action being Undrain (
actions.spec.ts:477-478); drained, disconnected worker → Undrain button, ⋯ entries["Remove worker"](actions.spec.ts:485); Inbox "drained" row → Undrain button, ⋯ without Undrain; Inbox drained drawer → Undrain button still there; lease drawer, whose only action is dangerous → no button, ⋯ keeps Release lease (asactions.spec.ts:460-462pins); anActionBarwhose only safe entry is the button draws no ⋯ (static markup, which can see the trigger, F14).
Done when
pnpm testandpnpm test:consolepass, with the five Tests lines above.- DESIGN.md's Menu paragraph says the ⋯ beside a button does not repeat it; its Toast paragraph says the close button sits beside the text.
rg -n 'crumb' ui/srcfinds nothing.
Out of scope
-
Removing
WorkerPageView.back, which only the breadcrumb read: it sits inuseWorkerPage, whose reads The console reads per page through a query cache, and events mark data stale #914 rewrites, so it stays until a later change (F16). -
Any change to which actions an item has, or their order.
-
The device drawer's tabs and Screen (Console: watch a device from its drawer's Screen tab; the lease stream route goes #910), the worker Events tab (Console: the worker's Events tab scrolls back through its history #911).
-
The toast hover timing (Console e2e: a success toast leaves after 6 s fails when the mouse rests over the new toast #900, PR fix(e2e): move the mouse off the menu so a new toast is not hovered (#900) #903).
Depends on
Written by an agent.
Activity
test: the page reserves room for a scrollbar (#912)
fix(ui): the page reserves room for a scrollbar (#912)
fix(ui): the page reserves room for a scrollbar (#912)
test: a worker page has no breadcrumb and a line between the warm poo…
feat(ui): a worker page drops its breadcrumb and the warm pool tab ge…
test: a fleet row's ⋯ lies inside its cell and an Inbox row's button …
test: a toast's close button sits beside its text (#912)
fix(ui): a toast's close button sits beside its text, inside the toast (
docs(design): a ⋯ does not repeat its button; a toast's close sits be…
test: the 360px drawer fills the page width the scrollbar gutter leav…
test: the toast centring check measures the title's text, not its str…
test: drop the false stretched-box comment in the toast centring check (
fix(ui): console layout fixes from the 2026-10-23 review (#924)
feat(history): the worker's Events tab scrolls back through its histo…