ui-verification ยท diff
git:20261018.95e63c5 to git:20261018.d929595
16 added, 12 removed. Audit B to B.
---
description: UI layout checks before claiming work is done
globs: external/ui/**/*
alwaysApply: false
---
# UI verification (Playwright MCP)
Before you tell the user that a UI change is complete or merge-ready:
1. Choose one free port (for example **`PORT=5201`**) and start **`CODDY_UI_BACKEND=http://127.0.0.1:<serve port> npx vite --host localhost --port "$PORT"`** in **`external/ui`**, against a **`coddy serve`** that has a session with messages (a session bundle written into a throwaway **`CODDY_HOME`** is enough; no model is called).
2. Use **Playwright MCP** (or the repo browser tools) to open **`http://localhost:$PORT/`** and that conversation, **`http://localhost:$PORT/#/s/<session id>`** (the edges below exist only on the chat screen), with at least two viewports: **narrow** (for example **390px**) and **wide** (for example **1280px**).
3. Run **`browser_evaluate`** to compare **`getBoundingClientRect()`** edges for **`.chat-header`**, **`.messages-inner`** first child, and **`.composer-card`** (left and right should match within about **1px** after shared column padding; a user message keeps to the right, so a first row that is one is compared by its right edge).
4. If anything is off, fix CSS and re-check before reporting done.
This is required for changes to **`external/ui/src/styles.css`**, **`ChatScreen`**, **`Composer`**, or nav layout.
## Safari and other WebKit reports
A Safari-only report is reproducible without a Mac: Playwright's WebKit is the engine Safari is cut
from and its version tracks Safari's. When the change touches a capped, scrollable or **`position:
fixed`** surface, run **`external/ui/scripts/webkit-scroll-check.mjs`** against a live
**`coddy serve`** (setup and env vars in **`docs/surfaces/web-ui.md`**, *Reproducing a Safari report without a
Mac*), and run it again with **`CODDY_ENGINE=chromium`** to tell a WebKit-only regression from a
layout bug every engine shares.
## The transcript at every width of the grid
Nothing in the transcript may be wider than the transcript: on the stacked shell its column is the
page, and one row that cannot wrap makes a phone scroll sideways (**`DESIGN.md`**, *Layout grid*,
*Tool card UI*, *Markdown*). When the change touches the transcript's rows, the Markdown styles or a
width query, run **`external/ui/scripts/phone-overflow-check.mjs`** against a **`vite`** dev server
(setup in **`docs/surfaces/web-ui.md`**, *Checking the transcript at every width of the grid*): it
mounts the rows that used to overflow and fails when the page scrolls sideways or anything sticks out
of **`.messages-inner`** at 360 to 1280px. A new width query goes into the grid first; the vitest
**`layoutGridCss.test.ts`** fails on one the grid does not name.
## The fold chevron
The chevron is an SVG whose ink is centred in its viewBox, never a text glyph: a glyph's ink moves
with the platform's font, which is why the reports of a chevron riding above its label kept coming
back (**`DESIGN.md`**, *Chevron*). jsdom has no layout, so vitest cannot see where it lands. When the
change touches the chevron, the rows it sits on or the type around them, run
**`external/ui/scripts/chevron-align-check.mjs`** against a **`vite`** dev server (setup in
**`docs/surfaces/web-ui.md`**, *Checking the fold chevron against its label*): it measures the
chevron's ink centre against the label's on a transcript row and on the Tasks panel toggle, and
fails past **1px**.
- ## The line numbers of a diff
-
- Every line number cell of a diff in the edits window is as wide as the file's widest number,
- never as its own (**`DESIGN.md`**, *Edits of a chat*): a cell sized by its content put the code of a
- removed or an added line, whose other cell is blank, a few pixels left of the context around it,
- and every vitest still passed. When the change touches **`changes/DiffFileSection.tsx`**, the
- **`.dv-*`** row styles or the type of the diffs, run **`external/ui/scripts/diff-gutter-check.mjs`**
- (**`npm run check:diff-gutter`**) against a **`vite`** dev server (setup in
- **`docs/surfaces/web-ui.md`**, *Checking the line numbers of a diff*): it reads where the code of
- every row starts in the unified view and in each column of the split view, and fails past **0.5px**
- or on a number that does not fit its cell.
-
## The composer caret
The composer's textarea keeps the caret and paints its glyphs transparent; the mirror under it
draws the draft (**`DESIGN.md`**, *Composer mirror and caret sync*). Whether the two put a glyph on
the same pixel is a layout fact, and page zoom or a screen density moves it: a classic scrollbar
keeps its device pixels and scroll offsets turn fractional. When the change touches the composer's
field, its mirror, its padding or its type, run **`external/ui/scripts/composer-caret-check.mjs`**
(**`npm run check:caret`**) against a **`vite`** dev server (setup in **`docs/surfaces/web-ui.md`**,
*Checking the composer caret at every zoom*): it compares the textarea's glyphs with the mirror's at
device scale factors 1 to 3 and page zoom 110% to 175%, scrolled and changed under an open page,
and fails on any pixel that differs.
## Long prompts and quotes
The composer's field follows its text and expands over the chat, a text selection in the transcript
offers Quote, and another conversation opens on its newest message (**`DESIGN.md`**, *Composer field
height and expand*, *Quote from the transcript*). Where the field stops, where the button stands and
whether a tap reaches it are layout and input facts jsdom cannot see. When the change touches the
composer's height, the expand control, the Quote button or the jumps of the transcript, run
**`external/ui/scripts/long-prompts-check.mjs`** (**`npm run check:long-prompts`**) against a running
**`coddy serve`** with two sessions (setup in **`docs/surfaces/web-ui.md`**, *Checking long prompts and
quotes*), and once more with **`CODDY_ENGINE=webkit`**: a cancelled **`pointerdown`** costs a touch its
click in WebKit, which is how a tap on Quote did nothing there while Chromium passed.
## A long transcript
Only a bounded slice of a transcript is in the DOM, and a long session holds only the end of its
history until the reader scrolls up (**`DESIGN.md`**, *Transcript window*; **`docs/surfaces/web-ui.md`**,
*Long sessions*). jsdom has no layout, so vitest renders every row and cannot see the window at work.
When the change touches the transcript window, the rows, the messages or tool-calls routes, or what a
row renders, run **`external/ui/scripts/transcript-window-check.mjs`** against a binary built with
**`TAGS="http ui swarm"`** (setup in **`docs/surfaces/web-ui.md`**, *Checking a long transcript*): it
opens a 3306-message session under CPU throttling, scrolls it without a jump, edits a prompt of an
older page, runs a turn in a second browser, reads through a swarm relay, and fails past a budget.
Run it once more with **`CODDY_ENGINE=webkit`** when the change touches scroll anchoring: WebKit does
not anchor scrolling, so there the window's own correction is all that keeps the reader's row still.
## The views of a chat
The view buttons of the chat header, the dock beside the chat and the Files window are
placement, focus and media through a relay, none of which jsdom can see. When the change
touches **`chat/ChatHeader.tsx`**, the dock, **`files/`**, the files routes or the swarm
mount, run **`external/ui/scripts/files-check.mjs`** against a binary built with
**`TAGS="http ui swarm"`** (setup in **`docs/surfaces/web-ui.md`**, *Checking the views of a
chat through a swarm relay*): it drives the menu, the dock and the window through an
authenticated relay, plays a sound from its signed address through the relay, and measures
the buttons and the window at every tier of the grid in English and in Russian. Run it once more with
**`CODDY_ENGINE=webkit`** when the change touches the window's layout.
+
+ ## The canvas
+
+ A frame of the canvas is a sandboxed iframe whose page loads its own files from an address that
+ carries its capability, through a remote server and a swarm relay alike; whether the page runs,
+ whether a pick reaches the page through a CSS-scaled out-of-process frame and whether the window fits
+ are facts of a real browser, which jsdom cannot see. When the change touches
+ **`external/ui/src/ui/canvas/`**, **`internal/canvas`**, the canvas routes or the relay's mount, run
+ **`external/ui/scripts/canvas-check.mjs`** (**`npm run check:canvas`**) against a binary built with
+ **`TAGS="http ui swarm"`** (setup in **`docs/surfaces/web-ui.md`**, *Checking the canvas through a
+ swarm relay*): it drives a `.canvas` tab of the Files window through an authenticated relay, checks
+ the sandbox, a pick, the reload of only the frame whose files moved, a wheel zoom that draws nothing
+ again in about one frame, a page leaving the canvas, a frame's address opened on its own, an
+ Obsidian `.canvas` file opened in the window, and the window at every tier of the grid in English
+ and in Russian. Run it once more with **`CODDY_ENGINE=webkit`** when the change touches the frames'
+ scaling or the pointer handling of the board.
## The shared file cards
The card of a shared file is `overflow: hidden`, so its actions menu is portalled to the document
and placed from the trigger, and the trigger is placed from the extension badge's own numbers
(**`DESIGN.md`**, *Shared file cards*). Whether a menu can be seen and reached is a layout fact jsdom
cannot answer: while the menu sat inside the card, a click on the trigger drew nothing and every
vitest still passed. When the change touches **`messages/ToolArtifactCards.tsx`** or the card's
styles, run **`external/ui/scripts/artifact-card-check.mjs`** (**`npm run check:artifacts`**)
against a **`vite`** dev server (setup in **`docs/surfaces/web-ui.md`**, *Checking the shared file
cards*): it measures the cards, the trigger against the badge and the open menu on desktops and on
emulated phones driven by taps, down to 320px, and fails past **1px**.
## Capture isolation and redaction
- Captures use a temporary browser-context page, never the operator's main tab, and close it afterward.
- Navigate the capture page through localhost to avoid host-specific zoom. Set the viewport explicitly and blur focus unless focus is what the screenshot documents.
- Screenshot stands use isolated `HOME` and `CODDY_HOME` with neutral fixtures.
- Before pushing, inspect every PNG for skill names, hosts, tokens, usernames, and personal paths.
- Screenshots for English pages and pull requests are taken with `?lang=en`, so the browser's locale does not decide the language of the capture.