ui-verification · git:20261018.654e9bb · 2026-10-18 · sha256 17b900fbe4dfe7d5
ui-verification git:20261018.654e9bbB
Immutable. This exact content never changes and is served at /api/v1/blob/17b900fbe4dfe7d5.
--- 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.