Skip to content

Commit eb59a8a

Browse files
authored
Add vhs skill for deterministic terminal demo recording (#6)
## Summary - Add new `vhs` skill with techniques for creating deterministic terminal demo screencasts - Includes the "concatenation trick" for detecting LLM response completion in TUI recordings - Update README table and test assertions
1 parent ae719f6 commit eb59a8a

3 files changed

Lines changed: 53 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ AI skill pack — reusable [SKILL.md](https://opencode.ai/docs/skills/) definiti
88
|-------|-------------|
99
| [`nix-flake`](./skills/nix-flake/SKILL.md) | Writing flakes with flake-parts, formatter, shell scripts, and language templates |
1010
| [`nix-haskell`](./skills/nix-haskell/SKILL.md) | Haskell projects with haskell-flake: dependencies, settings, devShell, autoWire |
11+
| [`vhs`](./skills/vhs/SKILL.md) | Deterministic terminal demo screencasts with VHS and wait patterns |
1112

1213
## Setup (home-manager)
1314

skills/vhs/SKILL.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
name: vhs
3+
description: Techniques for creating deterministic terminal demo screencasts with VHS
4+
---
5+
6+
# VHS Demo Recording
7+
8+
[VHS](https://github.com/charmbracelet/vhs) records terminal sessions by scripting keystrokes in `.tape` files and rendering them as GIFs. Use `Wait+Screen /regex/` to synchronize with interactive TUI elements instead of fragile fixed `Sleep` durations. VHS can only detect text *appearing* on screen, not disappearing — keep this constraint in mind when designing wait conditions.
9+
10+
## Non-Deterministic TUI Responses
11+
12+
When recording an LLM-powered TUI, detecting when a response is **complete** requires special care. Naive approaches all fail:
13+
14+
| Approach | Why it fails |
15+
|---|---|
16+
| Fixed `Sleep` | Not deterministic |
17+
| Unique marker in prompt (`XYZENDXYZ`) | Appears in the typed prompt on screen — `Wait+Screen` matches immediately |
18+
| Math formula (`347+829` → wait for `1176`) | LLM computes the answer in its visible *thinking trace* before the response finishes |
19+
| `Wait+Line /^MARKER$/` | TUI padding/borders prevent exact line matching |
20+
| `Hide` + type marker | `Hide` only hides VHS command log, not terminal content |
21+
22+
### The concatenation trick
23+
24+
Ask the LLM to **concatenate two words** and print the result. The prompt contains both words *separately* but never the combined string:
25+
26+
```tape
27+
Type "briefly explain this repo. Then print ALFA concatenated with BRAVO."
28+
Enter
29+
Wait+Screen /ALFABRAVO/
30+
```
31+
32+
**Why it works:**
33+
1. **Typed prompt** shows `...ALFA concatenated with BRAVO` — no `ALFABRAVO`
34+
2. **Thinking trace** says "I need to concatenate ALFA and BRAVO" — no `ALFABRAVO`
35+
3. **Response** outputs `ALFABRAVO` — the only match
36+
37+
Use uncommon words (ALFA, BRAVO, ZULU) so the combined form can't appear accidentally.
38+
39+
## Tips
40+
41+
- **Run VHS from the correct CWD** — it inherits the working directory. If the prompt references "this repo", the CWD must be the repo root, not a subdirectory.
42+
- **Inspect GIF frames** when debugging timing issues:
43+
```bash
44+
ffmpeg -i demo.gif -vf "select='eq(n\,100)'" -vsync vfr /tmp/frame.png
45+
```
46+
47+
## Nix Integration
48+
49+
- If the project uses Nix, create a dedicated `flake.nix` for the demo (e.g., `doc/demo/flake.nix`) so anyone can reproduce the recording with `nix run`.
50+
- **Reference tape by Nix store path** in flake apps (`vhs "${./.}/demo.tape"`) — don't `cd` into the store, as it may contain a `flake.nix` that confuses `nix run` inside the recording.

test/flake.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@
5050
# OpenCode
5151
machine.succeed("test -f /home/testuser/.config/opencode/skill/nix-flake/SKILL.md")
5252
machine.succeed("test -f /home/testuser/.config/opencode/skill/nix-haskell/SKILL.md")
53+
machine.succeed("test -f /home/testuser/.config/opencode/skill/vhs/SKILL.md")
5354
machine.succeed("grep -q 'name: nix-flake' /home/testuser/.config/opencode/skill/nix-flake/SKILL.md")
5455
5556
# Claude Code
5657
machine.succeed("test -f /home/testuser/.claude/skills/nix-flake/SKILL.md")
5758
machine.succeed("test -f /home/testuser/.claude/skills/nix-haskell/SKILL.md")
59+
machine.succeed("test -f /home/testuser/.claude/skills/vhs/SKILL.md")
5860
machine.succeed("grep -q 'name: nix-flake' /home/testuser/.claude/skills/nix-flake/SKILL.md")
5961
'';
6062
};

0 commit comments

Comments
 (0)