| name | feature-demo |
| description | Record a narrated walkthrough of a feature in a LOCAL web app by driving it with Playwright, so the user can see exactly what a feature does. Produces a captioned screen recording (video.webm / .mp4) AND a PDF of per-step screenshots with explanations. Use when the user invokes `/feature-demo`, or asks to "demo", "record", "show me", or "make a walkthrough of" a feature, page, ticket, or PR. Argument is a free-text description of what to demo and/or a ticket / PR number. |
feature-demo
Turn "show me what this feature actually does" into an artifact the user can watch and skim:
video.webm (and video.mp4 if ffmpeg is installed) — the full browser session, with a caption banner baked into the frames so each action is narrated on screen.
report.pdf — a cover page plus one section per step: the screenshot of that moment with a written explanation.
report.html, screenshots/NN.png, and a sanitized spec.json alongside.
Produce both the video and the PDF unless the user says otherwise.
Configure this for your app first
This skill is app-agnostic but needs to know about your app. Two things matter:
base_url — the LOCAL URL your app is served at (e.g. http://localhost:8000, http://localhost:3000, https://myapp.localhost). Set it in each spec (it defaults to http://localhost:8000 in run_demo.py). Only ever point at a local/dev URL — never staging or production.
- Containerized app? Just give the URL — the skill won't touch your stack. It runs host-side (the browser runs on your machine, not in the container) and never starts/stops anything. The only requirement is that the URL is reachable from the host: publish the container's port (Docker
-p 3000:3000, or a compose ports: entry) and use http://localhost:<published-port>. Note localhost here means the host, not inside the container. If it's not reachable, tell the user to bring the container up — don't try to start it yourself.
- Auth — how to log in, if at all:
- No login (this setup's default): set
"skip_login": true in the spec and the driver goes straight to your steps.
- Email/password form: provide an
auth block (login path + field selectors) and pass the password via the DEMO_PASSWORD env var. See references/spec-format.md.
- SSO / OAuth: the form-login path won't work as-is; demo a page reachable without auth, or ask the user how to obtain a logged-in session.
If base_url isn't reachable, stop and tell the user to start their app — don't try to spin up infrastructure yourself.
Core principle: drive it like a human
The demo must exercise the feature the way a real user would — navigate to real pages, then click the actual buttons, links, rows, and menu items to reach modals, inline edits, wizards, and confirmations. A demo is only trustworthy if it goes through the same affordances a person does.
Do NOT shortcut by goto-ing directly to a partial/fragment URL (modal forms, inline-edit endpoints, AJAX/HTMX swaps, autocomplete endpoints). Hitting such a URL directly often returns just a fragment with no <head> and no stylesheet, so it renders as raw unstyled HTML — and it isn't how the feature is actually used. Tell-tales you took a shortcut: a bare serif page, naked inputs, or a stray "Close" button at the top with no surrounding page.
Concretely:
goto is for landing on real, full pages (a list, a detail page, a dashboard) — pages the server renders with the full layout + CSS.
- Everything interactive (opening a modal, an inline edit, a create wizard, a confirm dialog, submitting a form) must be reached by
click / fill / select_option / press on the real trigger.
- If you can't find the trigger, that's a research task (read the template / router / the element's
hx-*/data-* attributes or button text) — not a reason to fall back to a direct partial URL.
When a flow genuinely has no UI entry point yet, say so to the user rather than faking it with a direct fragment load.
Where things live
- Scripts:
<this-skill-dir>/scripts/setup.sh and run_demo.py.
- Spec format reference:
<this-skill-dir>/references/spec-format.md (read it before authoring a spec).
- Output: write demos to a gitignored dir so videos never get committed (e.g. your project's
.local/ or a tmp/demos/ you've gitignored). Pass it as --out.
- Isolated Playwright env:
~/.cache/feature-demo/venv (created by setup.sh; nothing is installed into your repo).
- Worked example:
<this-skill-dir>/references/examples/product-card-grid.json (a verified demo with on-screen assertions).
Verified demos (measure · assert · highlight)
A demo is far more useful when it proves the spec instead of just showing it. The driver injects a toolkit (window.__demo) into every page and exposes declarative actions that measure a value, outline the element on-screen with its measured value, and assert it. A failed assertion fails the whole run (ok:false, non-zero exit) without stopping it — so every frame is still captured and failures show as red boxes. Reach for these whenever you're demoing a visual/CSS or interaction acceptance criterion.
assert_style — selector + expect, e.g. {"width":205,"height":183,"border-radius":8,"font-family":"Poppins","font-size":14,"font-weight":500}. Numbers compare within tolerance (default 1px); strings are case-insensitive "contains". Boxes the element green/red with the measured values.
assert_count — selector + count (a number, or {"min":N}); optional container to outline. Counts only visible elements.
highlight — selector (+ label): box an element, no assertion.
hide — selector/selectors: remove elements (e.g. a dev toolbar). Usually set the top-level hide list instead — it's reapplied after every navigation.
summary — draw a pass/fail panel listing every recorded check (nice final frame; ok already reflects the checks with or without it).
Selectors in these actions resolve to the first visible element, skipping hidden duplicates (carousel .slick-cloned / aria-hidden clones) — so you measure a tile a user can actually see. For interactions (clicking an arrow, etc.), call the toolkit from an eval: window.__demo.record(label, ok, actual) / .box(el,label,ok) / .visible(sel) — the result feeds the same gate. Full schema in references/spec-format.md.
Gotchas & lessons (learned the hard way)
- Carousels / virtualized lists clone nodes. Slick prepends hidden
aria-hidden .slick-cloned slides, so a bare querySelector/nth=0 grabs an off-screen clone. Target the visible/current element — the assert_* actions do this for you; in eval use window.__demo.visible(sel) or scope to .slick-current.
- Hide dev overlays before capturing. Debug toolbars (e.g. Django Debug Toolbar
#djDebug) overlap content — list them in the top-level hide.
- Pick the viewport to fit the content. Wide layouts are often capped by a max-width container, so a bigger viewport won't always show more — probe it. Add a separate mobile-viewport run for responsive behaviour.
- Probe before you assert. Spend one tiny throwaway Playwright script reading the real selectors/computed values first, so you assert against the right (visible) element with the right numbers — don't guess.
- Pass an absolute
--out (the PDF renderer needs it; the driver resolves it now, but be explicit).
Workflow
1. Figure out what to demo
From $ARGUMENTS (a description and/or a ticket / PR number):
- If a PR / branch is referenced, look at the diff (
gh pr view, git diff) to see which pages, routes, templates, and components changed — those are what the demo should exercise.
- If a ticket is referenced and you have a tool/MCP for the tracker, pull it for the acceptance criteria / described behavior.
- Ground the flow in the codebase: find the real URL paths (the router / routes config), the form field ids / button text in the templates or components, and any permissions needed. Selectors must be real. Prefer stable selectors: visible text (
text=Save), form-field ids (#id_<field> for Django, name=/data-testid for others), ARIA roles, or data-* hooks over brittle CSS.
- If the flow is ambiguous (which records to use, which path through a wizard), ask the user with
AskUserQuestion rather than guessing.
2. Confirm the app is up
- Check it responds:
curl -sk -o /dev/null -w "%{http_code}" <base_url>/. If down, tell the user to start their app.
- If the demo needs login, confirm credentials with the user and keep the password out of the spec (pass
DEMO_PASSWORD). For a no-login demo, set "skip_login": true.
3. Author the demo spec
Write a JSON spec (schema in references/spec-format.md). Each step is one action plus a caption (shown on-screen, kept short) and an optional longer explanation (shown in the PDF only). Keep captions to a sentence; put the "why this matters" in explanation. Order the steps as a clean narrative: goto a real full page to land, then click/fill/select through the real UI to perform the key actions and show the result/confirmation (see "Core principle: drive it like a human" — never goto a modal/inline partial URL). Save it to the output dir.
4. Run it
PY=$(bash "<skill-dir>/scripts/setup.sh")
"$PY" "<skill-dir>/scripts/run_demo.py" \
--spec path/to/demo.json \
--out /path/to/your/gitignored/demos/<timestamp>-<name>
- For a login demo, prefix with
DEMO_PASSWORD='…'. For a no-login demo (skip_login: true), no env var is needed.
- Default is headless (video still records). Add
--slowmo 250 for a calmer, more watchable video; --headed if you want to watch it happen.
- The driver prints a JSON result with absolute paths to
video_webm, video_mp4, pdf, and html, plus ok/failed.
5. Verify and iterate
- The driver fails fast on a missing selector or failed assertion, captures a
*-FAILED.png frame, and still writes a partial video + PDF. If a step fails, it's almost always a wrong selector or a missing precondition (record doesn't exist, user lacks permission). Inspect the failure screenshot, fix the spec (or the precondition), and rerun — the run is cheap and idempotent.
- A demo is also genuine verification: it routinely catches render/template bugs that green tests miss (e.g. a table column that shows nothing for a new data shape). Treat "looks broken in the demo" as a real bug to chase, not a demo glitch.
- When it's green, give the user the absolute paths and offer to open them (
open <pdf> / open <mp4-or-webm>). Briefly summarize what the demo shows.
Notes
.webm plays in browsers and QuickTime-via-conversion; video.mp4 is only produced when ffmpeg is on PATH (brew install ffmpeg). Mention this if the user wants mp4 and it's absent.
- The driver sets
ignore_https_errors, so a local self-signed / mkcert cert won't block the demo.
- Don't add Playwright to the repo or run browsers inside an app container — this skill is deliberately host-side and isolated so it reaches your app the same way your own browser does.
- Assert on focused surfaces. Wide list tables can clip right-hand columns off-screen (so
expect_text on a clipped column fails even when present), and detail pages often render derived fields rather than the raw value. Prove a data point on a modal/detail/edit form where it's visibly rendered, or via a column you've confirmed is on-screen.
- Opening a record from a list sometimes opens a side preview drawer (which then intercepts later clicks), not the detail page. To land on the full detail page,
goto its URL (a full page) rather than clicking the row.
- Branding: set
"brand": "Your App" in the spec to label the title card; it defaults to "Feature demo".