Help us improve
Share bugs, ideas, or general feedback.
From mthines-agent-skills
Records short videos of page sections using Playwright and ffmpeg to capture animations, transitions, and interactive motion. Use when a still screenshot cannot prove a multi-frame interaction.
npx claudepluginhub mthines/agent-skillsHow this skill is triggered — by the user, by Claude, or both
Slash command
/mthines-agent-skills:screen-recorderThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Produces a short `.webm` clip of a specific page section so an agent —
Guides technical evaluation of code review feedback: read fully, restate for understanding, verify against codebase, respond with reasoning or pushback before implementing.
Share bugs, ideas, or general feedback.
Produces a short .webm clip of a specific page section so an agent —
or a reviewer reading a PR — can see motion that a static screenshot
hides.
Not a test suite, not a permanent fixture: every recording is a
throwaway artifact written to .agent/recordings/ for one-shot visual
review.
This
SKILL.mdis a thin index. Detailed rules live inrules/*.mdand load on demand. Literal Playwright andffmpegboilerplate the skill emits lives intemplates/*.md. Do not preload everything — load only what the current phase asks for.
Reach for this skill when any of the following is true:
@keyframes, transition, View Transitions, Motion layout,
@starting-style, scroll-driven timelines).animations skill has produced an animation and wants visual
evidence it hits 60 fps and respects prefers-reduced-motion.ux skill flagged a flow (drag, focus ring, hover reveal,
expanding nav, modal entrance) where the asserted UX claim is
about timing, not layout.Do not reach for this skill when:
reviewer agent's visual pass owns PR-attached screenshots).e2e-testing skill.trace.zip — use
playwright-trace-analyzer.video-analyser.Before recording anything, verify the environment. Halt and ask the user before installing tools.
Run these checks (read-only):
# 1. Playwright installed?
command -v npx >/dev/null && npx --no-install playwright --version 2>/dev/null
# 2. Chromium driver available?
npx --no-install playwright install --dry-run chromium 2>/dev/null
# 3. ffmpeg available (cropping + transcoding)?
command -v ffmpeg
Decision table:
| State | Action |
|---|---|
Playwright + Chromium + ffmpeg all present | Proceed to Phase 1. |
| Playwright missing | Halt. Print install plan, ask permission. See rules/preflight.md. |
| Playwright present, Chromium driver missing | Halt. Print npx playwright install chromium, ask first. |
ffmpeg missing | Halt. Print install plan (brew install ffmpeg), ask first. Cropping disabled until installed. |
Target URL is localhost:* and not reachable | Halt. Ask the user to start the dev server before recording. |
Print exact commands; do not run silently.
Full preflight rules: rules/preflight.md.
Collect, then confirm back to the user before running:
| Input | Required | Default | Notes |
|---|---|---|---|
url | Yes | — | Full URL or localhost:<port>/<path>. Auth handled in Phase 2. |
selector | Yes | — | CSS, data-testid="...", or role=button[name="..."]. The crop target. |
interaction | No | idle (record state only) | Named recipe or inline script. See rules/interactions.md. |
duration | No | 5000 ms | Total recording length. Cap 15 s. Default 5 s gives the video-analyser enough I-frames (≈ 10 at the 0.5 s GOP) for its 8-frame default. |
viewport | No | { width: 1280, height: 800 } | The recording canvas — kept large for layout fidelity (responsive breakpoints, container queries). Downscaling happens at the crop stage. |
output-name | No | <selector-slug>-<ts> | File slug. .webm is always produced; .mp4 / .gif opt-in. |
out-format | No | webm | One of webm, mp4, gif. Non-webm requires ffmpeg transcode. |
max-width | No | 768 px | Output downscale ceiling. 768 px is the video-analyser Pareto knee — UI text stays legible, image tokens stay cheap (~786 tokens/frame on Sonnet). Crops smaller than this are not upscaled. Set 0 to disable downscale. |
keyint | No | 15 frames | Forced GOP length. At 30 fps this places one I-frame every 0.5 s, so the video-analyser's select='eq(pict_type,I)' sampling always lands enough frames for short clips. |
reduced-motion | No | false | If true, emulate prefers-reduced-motion: reduce. |
Echo the resolved inputs back as a one-screen summary before Phase 2.
Caller-specific overrides. When caller: reviewer and out-format is unspecified, default to mp4 (GitHub previews .mp4 inline). When caller: animations and the immediate next step is a Skill("video-analyser") invocation, keep max-width: 768 and keyint: 15 — they are already analyser-optimal. Pass max-width: 0 only when a human reviewer has reported text-still-unreadable at 768 px (rare).
Write a single-purpose Node.js script to
.agent/recordings/<slug>/record.mjs using the template at
templates/record.mjs.template.
Substitution rules and the interaction recipe catalog are in
rules/recording-script.md and
rules/interactions.md.
Hard rules:
chromium.launch({ headless: true }) — never headed, never webkit,
never firefox (recording fidelity differs between engines).recordVideo: { dir, size: viewport } on the context, not the
browser. Playwright emits one .webm per page.await context.close() before reading the video path — the file
is finalised only on context close.node .agent/recordings/<slug>/record.mjs
Capture stdout / stderr. On non-zero exit, do not crop — surface the error and stop.
Resolve the produced .webm path from the script's stdout (the template
prints VIDEO=<absolute-path> as its last line).
Use ffmpeg to crop the full-viewport .webm to the bounding box of
the target element, then downscale to a max-width-px-wide output with
a fixed 0.5 s GOP. The script in Phase 2 captures the bbox via
locator.boundingBox() and writes it to a sibling bbox.json.
The defaults (max-width: 768, keyint: 15) are tuned for the
video-analyser skill's Pareto knee — see
the Analyser-optimised sizing
section for the numbers. Net effect: 768 px wide keeps UI text legible
at ~786 image tokens/frame on Sonnet, and the short GOP guarantees the
analyser's keyframe-first sampling (Step 5a) lands on signal-dense scene
transitions rather than falling back to uniform-time sampling.
Crop command, downscale filter, and format-conversion rules: rules/cropping.md.
If ffmpeg is unavailable, skip cropping with a one-line note and
deliver the uncropped .webm.
Print a delivery summary:
Recording: <absolute path>
Format: <webm | mp4 | gif>
Size: <KB>
Duration: <ms>
Viewport: <w>x<h>
Cropped: <yes (WxH at X,Y) | no — ffmpeg missing>
URL: <recorded URL>
Selector: <selector>
Interaction: <recipe or inline summary>
If the caller is another skill (Phase 6 below), return the path only — no narration.
This skill is called by four other consumers via Skill("screen-recorder").
Full handshake spec: rules/integrations.md.
| Caller | When | Required inputs |
|---|---|---|
animations | After Phase 7 ("Measure") to attach a clip to the delivery, or when the user asks "show me". | url, selector, animation name (becomes output-name). |
ux | When a finding is severity Critical / High and concerns timing, motion, focus order, or interaction feedback. | url, selector, the finding ID. |
reviewer | In PR Mode when the diff matches animations / ux heuristics and the PR author has not attached a recording. | url, selector, PR number (for the upload path). |
storybook | When a scaffolded story includes motion or transitions that a still screenshot cannot prove (multi-frame interactions). | url (story permalink), selector, story name (becomes output-name). |
Callers pass inputs in their Skill() call body; this skill never asks
the calling skill questions — it falls back to defaults and proceeds, or
fails fast with one explanatory line.
Load on demand — do not preload.
| Phase | Files |
|---|---|
| 0 | rules/preflight.md |
| 1 | rules/interactions.md |
| 2 | rules/recording-script.md, templates/record.mjs.template |
| 2/3 | rules/interactions.md |
| 4 | rules/cropping.md |
| 6 | rules/integrations.md |
.agent/recordings/ and are never checked into git.
Add .agent/recordings/ to .gitignore if it is not already covered.transform and filter differently.ffmpeg, not with viewport gymnastics.
Resizing the viewport to the element bbox changes layout (responsive
breakpoints, container queries) and lies about what the user sees.
Record the real viewport, crop after.prefers-reduced-motion explicitly.
If the user passes reduced-motion: true, the recording must show
the reduced variant. The animations skill calls this skill twice
(default + reduced) to validate both paths.headed: true and submitting the result as evidence
(rules/recording-script.md).rules/cropping.md).await context.close() and reading a half-written .webm
(rules/recording-script.md).page.goto() calls in one recording — the second navigation
appends to the same video and confuses the viewer.localhost:3000 without checking the dev server is up
(rules/preflight.md).div > div:nth-child(3)) instead
of data-testid or role
(rules/interactions.md)..gif for anything over 4 s — the file balloons past
10 MB (rules/cropping.md)..agent/recordings/ into git.ffmpeg are present.record.mjs written from the template, no manual edits to the
Playwright API surface.VIDEO=<path>..webm exists at the printed path and is > 0 bytes.bbox.json is non-empty and the cropped
output exists..agent/recordings/ is in .gitignore (or covered by an existing
pattern).