gallery health: check the design specs' geometry rules from the DOM

Merged
TE
4d ago
+7463-32

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

rulespeccheck
icon-columnchat-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-linechat-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-tokentokens.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-labelmenus.md "Every row of one menu starts its label on one line"label x shared across a menu's rows
menu-rowmenus.md "Rows are --ds-control-row (28) high"a one-line .ui-menu-item is --ds-control-row high
menu-fitLeo'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.json lists 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>). --add seeds new states.
  • test_reds.py counts 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.ts covers the column, offset, reach and gap-token rules.
  • scripts/gallery-matrix/invariant-baseline.test.ts covers rows, every-run and shrink.
  • tests/test_test_reds.py GalleryHealth covers geometry reds per violation.

Observed / Not run

  • Ran locally:
    • the suites above: all pass.
    • health.ts on agent-pane.shift-scenarios and 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 Autofix with [code]smith
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.ts enforces one transcript icon column, centered icons, gap tokens, menu label columns, --ds-control-row heights, and menu width fit.
  • invariant-baseline.json lists 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 listbox counts 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.