gallery health: check the design specs' geometry rules from the DOM
Leo, 2026-11-01: "how about like the apparatuses for improving ui verification? let's examine what we can do better for our workflows"
The same night, Leo found these himself:
- the spacing between rows;
- an agent-message row off the tool-call icon column;
- a picker that is too wide.
Each one breaks a rule the design specs already state with a number. Review reads screenshots by eye and misses a row that is 2 to 6 px off. This checks the numbers from the DOM of every state gallery health renders.
Stacked on #30250 (the layout-shift gate). Its base becomes feat-cmux-next when #30250 lands.
Rules (webviews/src/gallery/invariants.ts), quoted from the specs
| rule | spec | check |
|---|---|---|
| icon-column | chat-transcript.md "One icon column": "the 16px icon box of every tool, status and message row share one left edge" | every top-level transcript row's lead edge (its leading icon's 16 px box, else its first text) sits on the column most rows share; an edge 0.5 to 8 px off fails |
| center-line | chat-transcript.md "glyph centered"; composer.md "every center on the prompt's first line" | a row's leading icon centres on the line of text beside it, within 1 px |
| gap-token | tokens.md "A new literal padding or gap must use 4/8/12/16/24/32px" | the gap between two transcript rows is 0, a gap token, or two of them |
| menu-label | menus.md "Every row of one menu starts its label on one line" | label x shared across a menu's rows |
| menu-row | menus.md "Rows are --ds-control-row (28) high" | a one-line .ui-menu-item is --ds-control-row high |
| menu-fit | Leo's "a picker that is too wide" | a menu is at most 112 px wider than its fullest row draws, unless a field-like trigger sets its width |
The gate (gallery health, no new job)
invariant-baseline.jsonlists the violations each state already has.- A violation that isn't in the state's row fails, once every retry shows it.
- A state with no row only warns.
- Rows only shrink (
bun invariant-baseline.ts --from <artifacts>).--addseeds new states.
test_reds.pycounts each violation as its own red, so a break the base shares is a warning.- The baseline is seeded from this PR's own full-gallery CI artifacts before it lands.
First findings (local headless Chromium, real debt, follow-ups in the tracking issue)
- The turn footer's first glyph (Copy) is 6 px right of the transcript column, in every chat (synthetic and recorded). chat-transcript.md:16 says the footer's first action glyph shares the column, and :430 says "its glyph does not" overhang.
- The prompt bubble's icon centres 1.5 px off its first line on agent inbox (steer) messages, in the recorded Codex chat.
- The composer's model menu is 280 px wide while its fullest row draws 108 px (172 px spare).
Tests
webviews/test/gallery-invariants.test.tscovers the column, offset, reach and gap-token rules.scripts/gallery-matrix/invariant-baseline.test.tscovers rows, every-run and shrink.tests/test_test_reds.py GalleryHealthcovers geometry reds per violation.
Observed / Not run
- Ran locally:
- the suites above: all pass.
health.tsonagent-pane.shift-scenariosand the recorded chats (findings above).scripts/web-lint.sh: passes.
- Not run: the full gallery. It is this PR's CI run.
Changelog
none
Takes back #30250: the README's "Layout shift gate" section is folded into one "Layout shift and geometry gates" section that keeps all of it; nothing #30250 added is removed.
Agent session: agent session
š¤ Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Gallery health now checks the design specs' geometry rules directly from the DOM of every state it renders, catching pixel-level misses that screenshot review overlooks. It rides on the layout-shift gate, whose baseline is seeded from the worst run so a flaky state never gets a 0 row. Both baselines are seeded from CI artifacts: 2843 shift rows (609 non-zero) and 2841 geometry rows (345 with listed violations), including rows for violations the base itself gained since seeding. The shift row for motion.sync-list#inserts-from-second-client was raised to 0.0059 after seeding, since a real second-client insert animates the list rows while three seed runs scored it 0.
webviews/src/gallery/invariants.tsenforces one transcript icon column, centered icons, gap tokens, menu label columns,--ds-control-rowheights, and menu width fit.invariant-baseline.jsonlists each state's existing violations; a violation not in the state's row fails the PR that introduces it, a state with no row only warns, and rows only shrink.- Each violation counts as its own red, so a break the base already shares is a warning.
- Every shift row's limit (a zero row's included) adds
LIMIT_SLACK, so a 0 row is not stricter than a tiny one. - Violation keys prefer row ids and data attributes over element text, so copy-only PRs do not rename listed violations.
- A
listboxcounts as a menu only inside a popover or dialog, so pane-wide list panels do not trip the menu rules.
First findings: the turn footer's Copy glyph is 6 px right of the transcript column in every chat, the prompt bubble's icon centers 1.5 px off on recorded agent messages, and the composer's model menu is 280 px wide while its fullest row draws 108 px.