Let the agent show me the UI - screenshot galleries
Another spin-off from the box and the loop. 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.
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:
// 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 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: 2andfullPage: 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.evaluateand 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.
import fs from "node:fs";
const shots = fs.readdirSync(OUT).filter((f) => f.endsWith(".png")).sort();
const cards = shots
.map((f) => `<figure><img src="${f}" loading="lazy"><figcaption>${f}</figcaption></figure>`)
.join("\n");
fs.writeFileSync(`${OUT}/index.html`, `
<style>
body { margin:0; background:#111; font-family:system-ui }
main { display:grid; grid-template-columns:repeat(auto-fill,minmax(360px,1fr)); gap:1rem; padding:1rem }
figure { margin:0 } img { width:100%; border-radius:8px }
figcaption { color:#aaa; font-size:.8rem; padding:.4rem 0 }
</style>
<main>${cards}</main>`);
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. 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.