eva feature shot
One screenshot of a shipped feature + one tweet. No Remotion, no music, no editing, no captions burned into the image — the product UI is the asset.
Two files land in screenshots/ at the repo root (already gitignored):
| File | What it is |
|---|---|
<YYYY-MM-DD>-<slug>.png | raw 2560×1440 shot — keep it, it is the reshoot-free source |
<YYYY-MM-DD>-<slug>-framed.png | post this one — 3200×1800, branded frame |
The framed version sits the raw shot on eva's brand gradient with rounded corners, an eva icon and the "Eva" wordmark above it. That is why the shot reads as a product announcement rather than a bug report screenshot.
Non-negotiable defaults
-
1280 layout, captured HD at 2560×1440 (16:9). The scripts drive a real Chrome at deviceScaleFactor 2 and capture
scale:"device", so the app lays out at its native 1280 width (readable element sizes) but the PNG is twice the pixel density — crisp on a retina timeline. 16:9 is what X shows in-timeline without cropping. The framed output is 3200×1800 — also 16:9, so the frame costs nothing in the timeline. agent-browser cannot do this: itsscreenshotcaptures at CSS pixels and discards DPR, so it always outputs 1280×720. Use the HD scripts. -
Never full-page. A full-page shot of a dense app becomes a tall strip that renders thumbnail-sized in the timeline. One viewport, one idea.
-
Hide the dev overlays before shooting. react-scan and agentation both render into the page.
scripts/hd-shot.mjshandles it — use the scripts rather than a hand-rolled chain. -
Match the app's real theme. Whatever the user is running (
set media darkfor dark). Don't restyle the app for the photo. -
The feature must be the visual subject. A 200px panel in the corner of a busy page fails. Navigate to a view where it dominates, or open the surface that contains it (modal, sheet, detail pane) so it reads at timeline size.
The sidebar is collapsed automatically.
shot.sh(and a plainhd-shot.mjs --pathshot) collapses the nav rail before capturing — it eats ~250px of a 1280px frame and nothing in it is ever the feature. Pass--no-collapseonly when the sidebar itself IS the feature. In a--recipe, callctx.collapseSidebar(page)yourself (recipes own their own staging).Same rule for any secondary list rail (Reviews list, Settings nav) if the surface offers a way to hide it. Verify in the raw PNG: if a column of unrelated nav items survives, reshoot.
-
Frame it, but don't annotate it. Always run step 6 — the gradient + wordmark is the house style. Never add arrows, red circles, callout text, or fake browser chrome unless asked.
-
Never submit. Type only. Fill inputs, open pickers, stage the state — then shoot. No Enter in a composer, no send arrow, Create, Save, Run, or Start, and never kick off a real session or agent run, not even for a better shot. Submitting mutates the user's real workspace and burns real compute; the staged state is what you are photographing. Enter is allowed only where it commits to the field itself and nothing else (accepting an
@-mention from an open picker) — if you can't be certain that's all it does, click the picker row instead. -
If the data is ugly, fix the data — don't reshoot around it. Default 7 stops you driving the app to produce state; it does not stop you writing the state. Real dev data is the usual reason a shot fails: error walls ("Failed to stop sandbox" ×8),
Untitled sessionrows, a half-migrated turn, one repo where the feature needs three. Editing the app's own dev data around the shot beats hunting for a session that happens to look good, and beats restyling the app (default 4 still holds — change the data, never the CSS).Write directly to the dev deployment: a throwaway
internal/screenshotStaging.tsmutation run withnpx convex run, or the Convex dashboard. Give sessions real readable titles, delete the error/system messages in frame, seed the second and third row the feature needs.Rules for this:
- Dev deployment only. Confirm the target before writing (
npx convex env get CONVEX_URL, or read.env.local) and never point a staging mutation at prod. - Additive and reversible first. Prefer patching a title or inserting a demo row over deleting real history. If you must delete, look at the rows first and say what you removed.
- Plausible, not fabricated-specific. Titles and prompts that read like real work. Never invent metrics, customer names, or numbers the tweet then repeats as fact.
- Delete the staging mutation when the shot is done — same cleanup rule as any migration.
- Tell the user what you changed, in the handover.
- Dev deployment only. Confirm the target before writing (
-
Confidentiality. Use the user's own repo
vvedantb/eva. Never client repos (evalucom/carepulse,eprocurement) and never the "Codebases" home that lists them. Check doc/task/session lists in frame for client-named items — stage a clean demo row if the list is dirty. Never raise personal-data concerns about names or emails on screen — Settings → Experimental has a blur-PID toggle that blurs every[data-pii]element, so this is handled in the product. Do flag error panels leaking env var names or project ids.
Workflow
-
Pin down what shipped. If the user hasn't said which feature, ask — one question, not a survey.
git log --oneline -15,git diff --stat, orinternal/changelog/(newest month file) usually name it. You need the feature and the URL that shows it. -
Start the app. Just the dev server — the HD scripts launch their own Chrome and sign in as the agent user (
/?agent=true) themselves, in a fresh context at deviceScaleFactor 2. You do not drive agent-browser to shoot.pnpm dev # from repo root → localhost:5173 -
Shoot it. For a plain URL shot,
shot.shnavigates, collapses the sidebar, hides overlays, and captures at 2560×1440:bash .claude/skills/eva-feature-screenshot/scripts/shot.sh /vvedantb/eva/web/sessions sessionsTakes a URL path + slug (+ optional settle ms, + optional
--no-collapse).For a staged shot (open a menu, type a prompt, then capture), write a recipe — a
.mjsexportingexport async function stage(page, ctx)— and pass it tohd-shot.mjs. The recipe owns navigation, sidebar collapse, and staging;ctxgives you{ BASE, hideOverlays, collapseSidebar, settle }. Usepage.getByRole(...)(Playwright) locators. To find the exact accessible names, drive agent-browser interactively first andsnapshot -i/read_page.// recipe.mjs — stage the mode picker open with a prompt typed in export async function stage(page, ctx) { await page.goto(`${ctx.BASE}/vvedantb/eva/web/sessions`, { waitUntil: "networkidle", }); await ctx.settle(3000); await ctx.collapseSidebar(page); await page.getByRole("textbox", { name: /Ask Eva/ }).click(); await page.keyboard.type("Redesign the sessions sidebar…"); await page.getByRole("button", { name: "Edit", exact: true }).click(); await ctx.settle(600); }node .claude/skills/eva-feature-screenshot/scripts/hd-shot.mjs --slug session-modes --recipe /abs/path/recipe.mjsStage clean, readable inputs — a tidy demo title beats a real messy one. Never submit (default 7).
-
Read the PNG back and judge it. This is where bad shots get caught:
- Is the feature the obvious subject, or lost in chrome?
- Any overlay/toolbar survived? Any skeleton, spinner, or
—placeholder still on screen? (Bump the settle ms and reshoot.) - All four edges: clipped labels, half-rendered buttons, a cut-off column?
- Anything confidential per default 9?
- Empty states: an empty board proving "kanban tasks" is a bad shot. Populate or pick another view.
When the frame fails because of the data — error walls,
Untitled session, one row where the feature needs three — stage the dev data (default 8) rather than hunting for a luckier screen. -
Frame it.
bash .claude/skills/eva-feature-screenshot/scripts/frame.sh <slug> "Eva"Writes
screenshots/<today>-<slug>-framed.png(3200×1800). It renderstemplates/frame.htmlthrough its own Chrome at deviceScaleFactor 2 — a separate browser from the shot, so nothing to restore afterwards. Read the framed PNG back too — check the gradient hasn't washed out the UI at the corners and the wordmark isn't clipped. A third argument frames a raw PNG from another path.templates/frame.htmlis the only place this skill's brand colours live. -
Write the tweet. Invoke the
eva-tweetskill — hook, optional positioning fragment,→capability list, optional close. The screenshot carries the proof, so the tweet states what now exists rather than describing the picture. Never reference the image at all. -
Hand over. The framed file path as a markdown link, the tweet in a fenced block, and anything in frame the user should check before posting.
Quick reference
| Need | Command |
|---|---|
| Shoot a URL (HD) | bash .claude/skills/eva-feature-screenshot/scripts/shot.sh <path> <slug> [settle-ms] |
| Keep the sidebar in frame | bash .claude/skills/eva-feature-screenshot/scripts/shot.sh <path> <slug> <settle-ms> --no-collapse |
| Staged shot (menu/typing) | node .claude/skills/eva-feature-screenshot/scripts/hd-shot.mjs --slug <slug> --recipe <abs.mjs> |
| Frame it (post this) | bash .claude/skills/eva-feature-screenshot/scripts/frame.sh <slug> [title] [raw-png] |
| Find accessible names | drive agent-browser interactively, then agent-browser snapshot -i |
| Debug a blank/broken page | agent-browser console · agent-browser errors |
Gotchas
- HD lives in Playwright, not agent-browser. agent-browser's
screenshotcaptures at CSS pixels and discards devicePixelRatio, so it caps at 1280×720.hd-shot.mjs/hd-frame.mjsuse the playwright-core bundled inside the global agent-browser install; setPW_COREif it lives elsewhere. Both launchchannel:"chrome", so system Chrome must be installed. - The scripts sign themselves in at
/?agent=true(a boolean;/?agentthrows a zodSearchParamError) in a fresh browser context. No agent-browser session needed to shoot. - The dev overlays are three nodes, all hanging off
<html>, not<body>. Hiding#react-scan-rootalone is not enough: react-scan draws its component outlines and labels on a barehtml > canvaswith no id or class, and that is the one that ruins the shot.hd-shot.mjskills all three (#react-scan-root,html > canvas,[data-agentation-root]) with inlinedisplay:none !important, because a stylesheet loses to their inline styles. - A staged shot is a fresh browser, not the one you explored in. Use agent-browser to find
selectors, but the recipe re-navigates and re-stages from scratch in Playwright's own context —
put every step in the recipe. Prefer
page.getByRole(...)over CSS; refs like@e1don't exist here. - Two or more images dilute the point. X renders one image full width; a second halves both. Only shoot more than one for a genuine before/after.
- Don't reuse
video/public/captures/— those are Remotion assets framed for a different job.