Skip to content

Commit 213358d

Browse files
cpcloudclaude
andauthored
feat: add /capture-ui skill for automated TUI screenshot and video capture (#943)
## Summary - New `/capture-ui` skill that writes ad-hoc VHS tapes and records screenshots (PNG) or short videos (animated WebP) of TUI changes for PR review - New `capture-adhoc` nix app wrapping VHS + ffmpeg with proper Hack Nerd Font config, supporting `--video` flag for animated output - Supports PR mode (`/capture-ui 123`): checks out PR branch in a worktree, builds micasa from that source, captures against it - Skill auto-triggers after TUI feature/bugfix work via AGENTS.md rule; always asks before uploading via `gh image` - Ephemeral captures in `.claude/captures/` (gitignored), named `<short-sha>-<desc>` for commit traceability Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e0fb443 commit 213358d

5 files changed

Lines changed: 305 additions & 0 deletions

File tree

.claude/commands/capture-ui.md

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
<!-- Copyright 2026 Phillip Cloud -->
2+
<!-- Licensed under the Apache License, Version 2.0 -->
3+
4+
Capture a screenshot or short video demonstrating a TUI feature or bugfix
5+
for PR review.
6+
7+
## Arguments
8+
9+
Optional: a PR number (e.g. `/capture-ui 123`). When given, the PR is
10+
checked out in an isolated worktree, micasa is built from that source, and
11+
the capture runs against the PR's code. Without a PR number, captures run
12+
against the current working tree.
13+
14+
## When to use
15+
16+
- After completing TUI feature or bugfix work, before or during PR creation
17+
- To visually verify an existing PR's UI changes
18+
19+
## Procedure
20+
21+
### 0. PR mode setup (skip if no PR number)
22+
23+
When a PR number is provided:
24+
25+
```sh
26+
pr_num=<N>
27+
pr_branch=$(gh pr view "$pr_num" --json headRefName --jq .headRefName)
28+
worktree_dir=".claude/worktrees/pr-$pr_num"
29+
git worktree add "$worktree_dir" "$pr_branch"
30+
```
31+
32+
Build micasa from the PR source:
33+
```sh
34+
CGO_ENABLED=0 go build -trimpath -o "$worktree_dir/micasa" -C "$worktree_dir" ./cmd/micasa
35+
```
36+
37+
All remaining steps use `$worktree_dir/micasa` instead of `micasa` on
38+
PATH. The tape preamble changes accordingly (see step 3).
39+
40+
### 1. Understand what changed
41+
42+
Before writing any tape, understand what the PR or recent work actually
43+
changed in the UI. Random screenshots of unrelated screens are useless.
44+
45+
**PR mode:**
46+
```sh
47+
gh pr view <N> --json body --jq .body
48+
gh pr diff <N> --name-only
49+
```
50+
51+
Read the PR description and changed files. Identify:
52+
- Which screen/tab/overlay was affected
53+
- What the visual difference is (new element, changed layout, error state, etc.)
54+
- What user interaction triggers the change
55+
- Whether the change is even visually demonstrable (some changes like
56+
error handling require specific failure conditions that demo mode can't
57+
produce — in that case, tell the user and skip the capture)
58+
59+
**Normal mode:** You already know what changed because you just wrote it.
60+
Still pause and identify the specific screen state that demonstrates it.
61+
62+
If the change is not visually demonstrable in demo mode, say so and abort.
63+
Do not capture random unrelated screens.
64+
65+
### 2. Decide capture mode (screenshot vs video)
66+
67+
- **Screenshot** (default): single PNG of the final relevant state. Use for
68+
layout changes, new columns, style tweaks, form additions.
69+
- **Video** (`--video`): animated WebP of an interaction sequence. Use for
70+
filtering, sorting, navigation, overlays, animations -- anything where
71+
the change is in the *transition*, not just the end state.
72+
73+
### 2. Write an ad-hoc VHS tape
74+
75+
Get the current short commit hash for the filename:
76+
```sh
77+
git rev-parse --short HEAD # normal mode
78+
git -C "$worktree_dir" rev-parse --short HEAD # PR mode
79+
```
80+
81+
Create `.claude/captures/<short-sha>-<descriptive-name>.tape`.
82+
83+
**Standard preamble (no PR number):**
84+
85+
```tape
86+
Require micasa
87+
88+
Output .claude/captures/<short-sha>-<descriptive-name>.webm
89+
90+
Set Shell bash
91+
Set FontFamily "Hack Nerd Font"
92+
Set FontSize 32
93+
Set Width 2400
94+
Set Height 1200
95+
Set Padding 20
96+
Set Theme "Dracula"
97+
Set CursorBlink false
98+
Set TypingSpeed 0
99+
100+
Env NO_COLOR ""
101+
Env TERM "xterm-256color"
102+
Env COLORTERM "truecolor"
103+
Env COLORFGBG "15;0"
104+
Env PS1 ""
105+
106+
Hide
107+
Type "exec micasa demo"
108+
Enter
109+
Sleep 5s
110+
```
111+
112+
**PR mode preamble** (uses the locally built binary, no `Require`):
113+
114+
```tape
115+
Output .claude/captures/<short-sha>-<descriptive-name>.webm
116+
117+
Set Shell bash
118+
Set FontFamily "Hack Nerd Font"
119+
Set FontSize 32
120+
Set Width 2400
121+
Set Height 1200
122+
Set Padding 20
123+
Set Theme "Dracula"
124+
Set CursorBlink false
125+
Set TypingSpeed 0
126+
127+
Env NO_COLOR ""
128+
Env TERM "xterm-256color"
129+
Env COLORTERM "truecolor"
130+
Env COLORFGBG "15;0"
131+
Env PS1 ""
132+
133+
Hide
134+
Type "exec <absolute-path-to-worktree>/micasa demo"
135+
Enter
136+
Sleep 5s
137+
```
138+
139+
After the preamble, add keystrokes to navigate to the target state.
140+
Reference existing tapes in `docs/tapes/` for navigation patterns:
141+
- `Type "D"` + `Sleep 2s` -- dismiss/toggle dashboard
142+
- `Type "f"` -- advance to next tab
143+
- `Type "b"` -- go to previous tab
144+
- `Type "j"` / `Type "k"` -- navigate rows
145+
- `Type "l"` / `Type "h"` -- navigate columns
146+
- `Type "s"` -- sort by current column
147+
- `Enter` -- drilldown / open
148+
- `Escape` -- close overlay / go back
149+
- Tab / Shift+Tab -- toggle house profile
150+
151+
**For screenshots:** navigate to target state hidden, then:
152+
153+
```tape
154+
Show
155+
Sleep 1.5s
156+
Hide
157+
Ctrl+Q
158+
Sleep 1s
159+
```
160+
161+
**For videos:** `Show` before the interaction begins, capture the full
162+
action sequence with appropriate sleeps between keystrokes (0.4-0.8s
163+
between navigation keys, 1-2s pauses at interesting states):
164+
165+
```tape
166+
Show
167+
Sleep 1s
168+
# ... interaction keystrokes with sleeps ...
169+
Sleep 1.5s
170+
Hide
171+
Ctrl+Q
172+
Sleep 1s
173+
```
174+
175+
Keep tapes short -- 5-10 seconds visible for screenshots, 10-20 seconds
176+
for videos.
177+
178+
### 3. Create the capture directory
179+
180+
```sh
181+
mkdir -p .claude/captures
182+
```
183+
184+
### 4. Record
185+
186+
Both modes use `capture-adhoc` (bundles vhs, fonts, ffmpeg). In PR mode
187+
the tape drops `Require micasa` and uses the absolute path instead, so
188+
capture-adhoc's bundled micasa is never invoked.
189+
190+
For screenshot (PNG):
191+
```sh
192+
nix run '.#capture-adhoc' -- .claude/captures/<name>.tape
193+
```
194+
195+
For video (animated WebP):
196+
```sh
197+
nix run '.#capture-adhoc' -- --video .claude/captures/<name>.tape
198+
```
199+
200+
Output path is printed to stdout.
201+
202+
If VHS fails, check:
203+
- Tape syntax (compare against `docs/tapes/dashboard.tape`)
204+
- That the binary exists and runs (normal: `which micasa`, PR: `<path>/micasa --help`)
205+
- That `Sleep` after app launch is long enough (5s minimum)
206+
207+
### 5. Present to user and ask about upload
208+
209+
Do NOT clean up the PR worktree yet — the user may want additional
210+
captures from the same PR.
211+
212+
Show the user:
213+
- What the capture shows
214+
- The file path
215+
- Which PR it targets (if PR mode)
216+
217+
Then ask: **"Upload this to the PR via `gh image`?"**
218+
219+
Do NOT upload without explicit approval.
220+
221+
### 6. Upload if approved
222+
223+
```sh
224+
gh image .claude/captures/<name>.png # screenshot
225+
gh image .claude/captures/<name>.webp # video
226+
```
227+
228+
Output is a markdown image reference. Include it in:
229+
- A PR comment: `gh pr comment <N> --body "<markdown-ref>"`
230+
- The PR body (if creating a PR next)
231+
232+
## Notes
233+
234+
- Captures are ephemeral -- `.claude/captures/` is gitignored
235+
- To promote a capture to a permanent demo tape, copy the `.tape` to
236+
`docs/tapes/` and adjust its `Output` path
237+
- VHS recording takes ~1-2 minutes; do not record unnecessarily
238+
- Existing tapes in `docs/tapes/` are the authoritative reference for
239+
keystroke patterns
240+
- PR mode worktrees are kept alive for additional captures; clean up
241+
manually with `git worktree remove <dir>` when done

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ dev.md
7676
.claude/settings.local.json
7777
.claude/worktrees
7878
.claude/*.lock
79+
.claude/captures/
7980

8081
# Superpowers brainstorm artifacts
8182
.superpowers/

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ details; do not duplicate that detail here.
169169
- `/fix-osv-finding` -- when osv-scanner reports findings (findings are blockers)
170170
- `/create-issue` -- immediately for every user request, including small asks
171171
- `/record-demo` -- after any UI/UX feature work; commit the GIF
172+
- `/capture-ui` -- after TUI feature/bugfix work; capture screenshot or video for PR review
172173
- `/deprecate-config` -- when renaming or removing a config key
173174
- `/new-fk-relationship` -- when adding FK links between soft-deletable entities
174175
- `/add-entity` -- when adding a new entity model (full wiring checklist)

flake.nix

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,21 @@
328328
text = builtins.readFile ./nix/scripts/capture-one.bash;
329329
};
330330

331+
# Captures an ad-hoc VHS tape as PNG screenshot or animated WebP video
332+
capture-adhoc = pkgs.writeShellApplication {
333+
name = "capture-adhoc";
334+
runtimeInputs = [
335+
micasa
336+
pkgs.vhs
337+
pkgs.nerd-fonts.hack
338+
pkgs.ffmpeg-headless
339+
];
340+
runtimeEnv = {
341+
FONTCONFIG_FILE = "${vhsFontsConf}";
342+
};
343+
text = builtins.readFile ./nix/scripts/capture-adhoc.bash;
344+
};
345+
331346
# Captures VHS tapes in parallel: capture-screenshots [name ...]
332347
capture-screenshots = pkgs.writeShellApplication {
333348
name = "capture-screenshots";
@@ -462,6 +477,7 @@
462477
site = app p.site "Start local Hugo dev server";
463478
record-tape = app p.record-tape "Record a VHS tape to WebM";
464479
record-demo = app p.record-demo "Record the main demo tape";
480+
capture-adhoc = app p.capture-adhoc "Capture an ad-hoc VHS tape screenshot or video";
465481
capture-one = app p.capture-one "Capture a VHS tape screenshot";
466482
capture-screenshots = app p.capture-screenshots "Capture all VHS screenshots in parallel";
467483
record-animated = app p.record-animated "Record all animated demo tapes";

nix/scripts/capture-adhoc.bash

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env bash
2+
# Copyright 2026 Phillip Cloud
3+
# Licensed under the Apache License, Version 2.0
4+
5+
# Capture an ad-hoc VHS tape as a PNG screenshot or animated WebP video.
6+
# Reads the output path from the tape's Output directive.
7+
#
8+
# Usage:
9+
# capture-adhoc <tape-file> # PNG screenshot (last frame)
10+
# capture-adhoc --video <tape-file> # Animated WebP (full recording)
11+
12+
set -euo pipefail
13+
14+
video=false
15+
if [[ "${1:-}" == "--video" ]]; then
16+
video=true
17+
shift
18+
fi
19+
20+
if [[ $# -ne 1 ]]; then
21+
echo "usage: capture-adhoc [--video] <tape-file>" >&2
22+
exit 1
23+
fi
24+
25+
tape="$1"
26+
27+
webm_path=$(grep -m1 '^Output ' "$tape" | awk '{print $2}')
28+
if [[ -z "$webm_path" || "$webm_path" != *.webm ]]; then
29+
echo "error: tape must contain an Output directive ending in .webm" >&2
30+
exit 1
31+
fi
32+
33+
mkdir -p "$(dirname "$webm_path")"
34+
vhs "$tape"
35+
36+
if [[ "$video" == true ]]; then
37+
webp_path="${webm_path%.webm}.webp"
38+
ffmpeg -y -i "$webm_path" -c:v libwebp_anim -compression_level 6 -loop 0 "$webp_path"
39+
rm -f "$webm_path"
40+
echo "$webp_path"
41+
else
42+
png_path="${webm_path%.webm}.png"
43+
ffmpeg -y -sseof -0.04 -i "$webm_path" -frames:v 1 -update 1 "$png_path"
44+
rm -f "$webm_path"
45+
echo "$png_path"
46+
fi

0 commit comments

Comments
 (0)