--- title: "Let the agent show me the UI - screenshot galleries" description: "How I capture every UI state with Playwright, and the tiny HTML layer I want to add so an agent can hand me one gallery instead of a folder of PNGs." pubDate: 'July 16, 2026' tags: ['AI', 'Frontend'] --- Another spin-off from [the box and the loop](/blog/how-i-build-small-products-autonomously-hetzner-swarm-and-agent-loops). I called this "my favorite little trick" over there and then moved on. It deserves more than a sentence. [I posted a quick example of it on X.](https://x.com/Kuba_Szw/status/2075219506171883704) ## The problem with reviewing UI from a diff When an agent changes UI, the PR tells you *what changed in the code*. It does not tell you what the screen now looks like - and worse, it doesn't show you the five states you didn't think to check. Empty. Filled. Accepted. Expired. The dead-end where the link is invalid. Reading a diff and imagining the rendered result is slow and I'm bad at it. So I stopped. Now the agent shows me. ## Step one: a script that walks every state The real workhorse is a plain script that drives the running app with Playwright and screenshots each meaningful state. Here's the shape I use - a tiny `shot()` helper plus a flat list of the states I care about: ```js // Condensed from the real script - setup and data helpers omitted. async function shot(name, url, prep) { const page = await browser.newPage({ viewport: VP, deviceScaleFactor: 2 }); await page.goto(`${BASE}${url}`, { waitUntil: "networkidle" }); if (prep) await prep(page); // fill a form, expand a tree, etc. await page.screenshot({ path: `${OUT}/${name}.png`, fullPage: true }); await page.close(); } await shot("01-request-empty", "/t/demo/request"); await shot("02-request-filled", "/t/demo/request", fillTheForm); await shot("03-confirmation", "/t/demo/request", submitIt); await shot("04-quote-sent", quoteUrl("DEMO-04")); await shot("05-quote-accepted", quoteUrl("DEMO-05")); await shot("06-quote-declined", quoteUrl("DEMO-06")); await shot("07-quote-expired", quoteUrl("DEMO-08")); await shot("07b-quote-sent-past-expiry", quoteUrl("DEMO-07")); await shot("08-quote-cancelled", quoteUrl("DEMO-09")); await shot("09-quote-unavailable", quoteUrl("DEMO-09", "bad-token")); ``` The value isn't the screenshot mechanics - Playwright makes that trivial. The value is that the list is exhaustive **for this customer-facing lifecycle**. Every decision state, every dead end, captured in one run. `BASE` defaults to the [local gateway](/blog/one-caddy-gateway-for-all-your-local-docker-stacks) hostname and can be overridden with an environment variable when the script runs somewhere else. A couple of things I learned the boring way: - `deviceScaleFactor: 2` and `fullPage: true`, or the shots look cheap and cut off. - Sometimes you have to seed known dev-only state first (staged tokens, demo rows) so the "expired" and "cancelled" states actually exist to screenshot. - Playwright's actionability checks race open animations. For flaky accordions I just click through the DOM directly in `page.evaluate` and move on. ## Step two: an HTML page over the folder The real script currently stops at a directory of PNGs. That's not much of a review surface. I couldn't find the bigger gallery wrapper I remembered, so this is the obvious next layer: a dozen lines that turn the folder into one skimmable page. ```js import fs from "node:fs"; const shots = fs.readdirSync(OUT).filter((f) => f.endsWith(".png")).sort(); const cards = shots .map((f) => `
${f}
`) .join("\n"); fs.writeFileSync(`${OUT}/index.html`, `
${cards}
`); ``` Add that block to the capture script, open `index.html`, and the whole lifecycle is on one screen. That's the gallery. ## Where it really pays off: comparing concepts The single-run gallery would already be good for review. The extension I actually want is pointing it at **variants**. Have the agent render two or three ideas for a component, group the same state into columns, and you get a side-by-side lookbook - concept A, B, C, same states, same page. Deciding stops being an argument in prose ("I think the second one feels cleaner?") and becomes a glance. You pick with your eyes, in seconds, the way you'd choose between photo prints on a table. This grouping layer isn't in the current script yet; now I know exactly what the reusable skill still needs. ## The point This is the direction I want for the visual half of the [autonomous loop](/blog/how-i-build-small-products-autonomously-hetzner-swarm-and-agent-loops). Today the agent can hand me ordered screenshots for every state. One tiny HTML index turns those into a page I can skim; a variant-aware index turns it into a decision tool. "Show, don't tell" turns out to be a solid engineering principle too.