Regenerable GIF/MP4 clips for the public README hero, social posts, and docs.
All assets are produced from scripts against the scrubbed snapshot
examples/demo.db — no hand-recorded screen captures, no real company data.
One asset bends the first half of that rule without touching the second:
raycast.* records a native Raycast overlay handing off to the installed
app through a gadak:// link, which no headless browser can drive. The
setup and the take are still a script (tools/record-raycast.sh — it
reseeds the demo profile from the snapshot first, captures only the app
window's rectangle, and scrubs the signed-in account line in the encode),
but the script runs on a live screen, so review the frames before
committing a regen.
A capture workspace is frozen. Reseeding the mirror is not enough on
its own — the profile's config.json is where a real site and token survive
between takes, and the app syncs on open. That is how 71 real rows once
landed on top of the scrubbed ones under the same external_id, with the
fictional author names replaced by real ones (GDK-181, caught in review
before the shot). "frozen": true in that config (gadak config set frozen true) makes the workspace refuse every request to the origin — pulls and
writes alike (GDK-507); tools/record-raycast.sh sets it before it launches
anything. Do the same for any capture profile you build by hand — gadak status and gadak doctor both say whether it is on. (The VHS fixture under
tools/tapes/ needs no latch: prepare.sh rewrites its config from scratch
each run with a fake credential.)
| File | Source | Use |
|---|---|---|
docs/media/web-demo.gif |
Playwright walkthrough | README tour (folded <details>, not inline hero) |
docs/media/web-demo.mp4 |
same recording, h264 | Twitter / LinkedIn / anywhere GIF is too heavy |
docs/media/search.gif |
Playwright e2e/demo/search-demo.spec.ts |
README — ⌘K All search (ignores filters) |
docs/media/search.mp4 |
same recording, h264 | Twitter / LinkedIn / anywhere GIF is too heavy |
docs/media/agent.gif / agent.mp4agent-poster.png) — removed 2026-09-03: the paper-terminal composite is retired now that the shell is in the window (GDK-1353); terminal-demo.* below is the same two beats in gadak's own pane. The rig (e2e/demo/agent-demo.spec.ts, make media-agent) stays |
was the README agent clip | |
docs/media/terminal-demo.mp4 (+terminal-demo.gif, terminal-demo-poster.png) |
Playwright e2e/demo/terminal-demo.spec.ts via terminal.config.ts (make media-terminal; serve.sh with GADAK_E2E_ORIGIN=builtin — the mirror migrated onto the built-in tracker so the claim is a write that lands), cut by e2e/demo/export-terminal.sh |
README + landing agent exhibit — gadak's own pane under the list: gadak claim names the tab after the issue and moves the row, then sql piped into views open --keys -, then views open --jql; 4:5 |
docs/media/mcp.gif |
VHS tape tools/tapes/mcp.tape |
README — Claude Code registers gadak mcp and answers a question JQL cannot express |
docs/media/mcp.mp4 |
same tape, second Output line |
Twitter / LinkedIn / anywhere GIF is too heavy |
docs/media/raycast.gif |
scripted live take tools/record-raycast.sh |
README — Raycast searches the mirror per keystroke, Enter opens the hit via gadak:// |
docs/media/raycast.mp4 |
same take, h264 | Twitter / LinkedIn / anywhere GIF is too heavy |
docs/media/tokens.gif |
Playwright split e2e/demo/tokens-demo.spec.ts |
README — CLI config set ui.tokens / ui.dataColors retints an open tab; locked bg-base saves with a warning |
docs/media/tokens.mp4 |
same recording, h264 | Twitter / LinkedIn / anywhere GIF is too heavy |
docs/media/dashboards.gif |
Playwright split e2e/demo/dashboards-demo.spec.ts |
README — CLI dashboards save / open renders the triage wall; a second save swaps the open frame |
docs/media/dashboards.mp4 |
same recording, h264 | Twitter / LinkedIn / anywhere GIF is too heavy |
docs/media/tokens-vertical.mp4 |
Playwright stacked e2e/demo/tokens-demo.spec.ts via tokens-vertical.config.ts |
social/vertical, 4:5 for X feeds — README uses the landscape cut |
docs/media/dashboards-vertical.mp4 |
Playwright stacked e2e/demo/dashboards-demo.spec.ts via dashboards-vertical.config.ts |
social/vertical, 4:5 for X feeds — README uses the landscape cut |
docs/media/claude-drive.gif / claude-drive.mp4 / claude-drive-vertical.mp4terminal-hero.* below is the live session in gadak's own pane. The rig (tools/tapes/claude-drive.tape, record-claude-drive.sh) stays for the dashboards/tokens verticals |
was the README skill clip and the landing skill-drive slot | |
docs/media/terminal-hero.mp4 (+terminal-hero.gif, terminal-hero-poster.png) |
Playwright e2e/demo/terminal-claude-demo.spec.ts via e2e/demo/record-terminal-claude.sh (live Claude Code in the pane; isolated HOME + demo mirror migrated onto the built-in tracker per take; not in make media), cut by export-terminal.sh — dense-cut.py measures the take and time-lapses the stretches where only the transcript moves |
README skill clip + landing skill drive — gadak claim binds the shell to NMA-140, claude starts in it, one Korean prompt becomes the list, the next saves and opens a dashboard; 16:10, the window itself |
docs/media/claude-dashboards-vertical.mp4 |
VHS tools/tapes/claude-dashboards.tape + the same serve tab (record-claude-drive.sh vertical claude-dashboards) |
social/vertical, 4:5 — the dashboards half of the flagship, ending on a key clicked off the wall that opens the issue in the app (the open verb, GDK-854) |
docs/media/claude-dashboards-vertical.gif (+-poster.png) |
430-wide reduction of that mp4 | README — GitHub strips <video> from markdown (measured 2026-08-25 via gh api /markdown), so the README pair ships as GIF; the poster is the landing's still |
docs/media/claude-tokens-vertical.mp4 |
VHS tools/tapes/claude-tokens.tape + the same serve tab (record-claude-drive.sh vertical claude-tokens) |
social/vertical, 4:5 — the team-look half: colours plus the dimension axes, including a token saved with a warning the agent then acts on (GDK-858) |
docs/media/claude-tokens-vertical.gif (+-poster.png) |
430-wide reduction of that mp4 | README — same reason as the dashboards GIF |
docs/media/scale.mp4 (+scale.gif, scale-poster.png) |
Playwright e2e/demo/scale-demo.spec.ts + post-process camera work e2e/demo/export-scale.sh (make media-scale — deliberately outside the make media aggregate: the committed artifacts are what the site ships) |
landing flagship — record-time counts focus over a 20k-issue snapshot |
docs/media/hero.mp4hero-poster.png) — removed 2026-09-02: the phone app is not shipped, so no public surface shows it (user call). The rig stays: e2e/demo/record-hero.sh (desk take record-hero-desk.sh + phone take record-hero-phone.sh inside its away-wait, cut by cut-hero.sh) needs a live Claude Code login AND a booted iOS simulator with a dev build of the phone app |
was the 0.19 hero — one serve, one terminal session: the desk hands work to an agent, a phone closes an issue while the chair is empty, the desk comes back to the scrollback and the board. Re-shoot and re-add when the phone app ships | |
docs/media/roundtrip.mp4 (+roundtrip-poster.png) |
Playwright e2e/demo/roundtrip.spec.ts via e2e/demo/record-roundtrip.sh --live, cut by e2e/demo/camera.mjs e2e/demo/roundtrip.shots.mjs (the camera layer, below). Like the hero, not in make media and not reproducible from a checkout alone — the four shells run a live Claude Code login against e2e/demo/shop-fixture/; nothing in the terminals is scripted, so takes vary and the rig retries until the beat contract holds |
the 0.20 release cut — four shells bound to four issues investigate on their own (the roster column of the dock names them by key), the camera punches in on a card and the shell it brings back, findings standing, then the same from ⌘K |
docs/media/groupby.gif / groupby.mp4 (+groupby-poster.png) |
Playwright e2e/demo/groupby-demo.spec.ts via e2e/demo/export-groupby.sh |
group-by exhibit motion cut — the landing exhibit itself ships the still below |
docs/media/groupby-still.png |
e2e/demo/site-stills.mjs |
landing group-by exhibit (2x still; see the landing policy table) |
docs/media/history.gif / history.mp4 (+history-poster.png) |
Playwright e2e/demo/history-demo.spec.ts via e2e/demo/export-history.sh |
history exhibit motion cut — the landing exhibit itself ships the still below |
docs/media/history-still.png |
e2e/demo/site-stills.mjs |
landing history exhibit (2x still; see the landing policy table) |
The landing keeps a video only where motion is the claim (typing-speed search, a command changing the visible view); every claim that reads from a single frame ships a core-region crop still instead of a full-screen clip. Kept videos are framed to the action region, not the whole window — the landing's exhibit column is narrow, and full-frame 1280 px recordings render glyphs unreadably small there.
Current state:
| Landing slot | Asset | Policy |
|---|---|---|
| flagship (hero) | scale.mp4 (+scale-poster.png) |
video kept, with post-process camera work (export-scale.sh): smoothstep push-in to the palette for the typing beats, out for the regroup, in to the counts band for the chip narrowing, back to full frame at the loop point. Source pacing untouched — only the crop moves |
| search exhibit | search.mp4 (+search-poster.png) |
video kept, mp4 cropped to the palette+detail region (export-search.sh); the README search.gif stays full-frame |
| group-by exhibit | groupby-still.png |
still — assignee filter submenu with live counts over the epic-grouped list (2x, 1656×1110; reframed 2026-09-02 when the filter popover moved to the toolbar's right half) |
| history exhibit | history-still.png |
still — NMB-139 header badges + bot comment + changelog with the Reopened marker (2x, 876×1740; a 900-tall viewport since the 0.20 detail grew a breadcrumb and a taller composer) |
| agent exhibit | terminal-demo.mp4 (+terminal-demo-poster.png) |
video — the command→view causality is the claim; 0.20 replaces the paper-terminal composite with gadak's own pane (4:5, vertical cap) |
| agent proof | mcp.mp4 (+mcp-poster.png) |
video — the claim is a conversation flow, so the exhibit plays the tape (was mcp-still.png until the user call of 2026-08-24) |
| skill drive | terminal-hero.mp4 (+terminal-hero-poster.png) |
video — a command changing the visible view is the motion rule's own example; 0.20 plays the live session inside gadak's own pane (16:10 at column width — the take is the window itself, so no vertical cap). Was claude-drive-vertical.mp4 from v0.17.2 |
Regenerate the two app stills against the standard e2e fixture (the history still needs serve.sh's NMB-139 enrichment):
GADAK_FRESHEN=1 bash e2e/serve.sh & # :7877 fixture
node e2e/demo/site-stills.mjs # → docs/media/{groupby,history}-still.pngThe MCP still is a frame of the committed clip (re-pick the timestamp after a re-record so the answer is fully printed; the crop drops the personal usage-limit status line):
ffmpeg -y -ss 23.5 -i docs/media/mcp.mp4 -frames:v 1 -vf "crop=1080:450:0:0" \
docs/media/mcp-still.pngsite/public/media is a symlink to docs/media (created by the Pages
workflow at build time; a fresh checkout has no such entry) — stills land on
the site by existing there. The landing references them via MediaSlot still=… with a
displayWidth equal to the crop's CSS-pixel width (half the PNG width).
| Asset | Budget | Notes |
|---|---|---|
web-demo.gif |
≤ 8 MB (prefer ≤ 5 MB) | GitHub README inline limit; palette 2-pass |
web-demo.mp4 |
soft ≤ 8 MB | h264 yuv420p + faststart |
search.gif |
≤ 8 MB (prefer ≤ 5 MB) | same README inline budget as the hero (900 px render) |
search.mp4 |
soft ≤ 8 MB | h264 yuv420p + faststart |
terminal-demo.gif |
≤ 8 MB (prefer ≤ 5 MB) | 860-wide (README renders at 430) @ 9 fps, palette 2-pass; same >8 MB ladder as agent |
terminal-demo.mp4 |
soft ≤ 8 MB | 1080×1350 h264 yuv420p + faststart |
terminal-hero.gif |
≤ 8 MB (prefer ≤ 5 MB) | 1200-wide (README renders at 900) @ 9 fps, palette 2-pass — a dark pane under a paper list is two palettes in one frame, so this one runs closest to the limit |
terminal-hero.mp4 |
soft ≤ 8 MB | 1440×900 h264 yuv420p + faststart; the working stretches are time-lapsed by dense-cut.py, never the payoffs or the typing |
mcp.gif |
≤ 3.5 MB | VHS + gifsicle -O3 --colors 64; cut the idle tail if still over |
mcp.mp4 |
soft ≤ 8 MB | VHS Output (h264) |
raycast.gif |
≤ 3.5 MB | dark overlay + small motion area compress well; 960 px @ 10 fps lands ~1 MB |
raycast.mp4 |
soft ≤ 8 MB | h264 yuv420p + faststart |
tokens.gif |
≤ 8 MB (prefer ≤ 5 MB) | split 1744×672 scaled to 1280 @ 9 fps, 128-color palette |
tokens.mp4 |
soft ≤ 8 MB | h264 yuv420p + faststart |
dashboards.gif |
≤ 8 MB (prefer ≤ 5 MB) | same split and palette ladder as tokens |
dashboards.mp4 |
soft ≤ 8 MB | h264 yuv420p + faststart |
tokens-vertical.mp4 |
soft ≤ 8 MB | 1080×1350 h264 yuv420p + faststart; no GIF (1080-wide GIF would miss the 8 MiB budget) |
dashboards-vertical.mp4 |
soft ≤ 8 MB | same 4:5 stack as tokens-vertical; no GIF |
claude-dashboards-vertical.mp4 |
soft ≤ 8 MB | 1080×1350 h264 yuv420p + faststart; no GIF |
claude-tokens-vertical.mp4 |
soft ≤ 8 MB | same 4:5 stack |
claude-{dashboards,tokens}-vertical.gif |
soft ≤ 4 MB each | 430-wide @ 9 fps, palettegen=max_colors=64:stats_mode=diff then gifsicle -O3 --colors 64. 430 rather than 540: two of them sit side by side in one README row, and the pair has to fit the 900 px column the other exhibits use |
scale.gif |
≤ 8 MB (prefer ≤ 5 MB) | same README inline budget as the hero |
scale.mp4 |
soft ≤ 8 MB | h264 yuv420p + faststart; camera work is a post-process crop, source pacing untouched |
{groupby,history}.gif |
≤ 8 MB (prefer ≤ 5 MB) | same palette ladder as tokens |
{groupby,history}.mp4 |
soft ≤ 8 MB | h264 yuv420p + faststart |
{groupby,history}-still.png |
soft ≤ 1 MB | 2x app stills; regenerate via node e2e/demo/site-stills.mjs against the standard e2e fixture |
Measured 2026-09-03 via ls -la docs/media/ + ffprobe (decimal MB = bytes/1e6):
| Asset | Size | Bytes (ls -la) |
Duration | Resolution / fps |
|---|---|---|---|---|
web-demo.gif |
7.73 MB | 7725746 | 17.1 s | 960×600 @ 9 fps |
web-demo.mp4 |
1.04 MB | 1042959 | 17.1 s | 1024×640 h264 |
search.gif |
3.66 MB | 3659975 | 7.4 s | 960×600 @ 9 fps |
search.mp4 |
0.55 MB | 547850 | 7.4 s | 800×640 h264 |
terminal-demo.gif |
5.70 MB | 5699598 | 23.0 s | 860×1075 @ 9 fps |
terminal-demo.mp4 |
1.46 MB | 1461253 | 23.0 s | 1080×1350 h264 |
terminal-hero.gif |
7.40 MB | 7396608 | 37.0 s | 1200×750 @ 9 fps |
terminal-hero.mp4 |
2.31 MB | 2305272 | 37.0 s | 1440×900 h264 (57.4 s take, dense cut) |
mcp.gif |
0.16 MB | 157323 | 24.8 s | 1080×620 @ 25 fps |
mcp.mp4 |
0.18 MB | 176644 | 24.8 s | 1080×620 h264 |
raycast.gif |
0.98 MB | 982043 | 13.2 s | 960×579 @ 10 fps |
raycast.mp4 |
0.26 MB | 255880 | 13.2 s | 1088×656 h264 |
tokens.gif |
3.97 MB | 3966234 | 26.0 s | 1280×493 @ 9 fps |
tokens.mp4 |
0.92 MB | 916969 | 26.0 s | 1744×672 h264 |
dashboards.gif |
2.91 MB | 2909844 | 38.3 s | 1280×493 @ 9 fps |
dashboards.mp4 |
0.99 MB | 988500 | 38.3 s | 1744×672 h264 |
tokens-vertical.mp4 |
1.34 MB | 1339418 | 26.9 s | 1080×1350 h264 |
dashboards-vertical.mp4 |
1.88 MB | 1877890 | 37.4 s | 1080×1350 h264 |
claude-dashboards-vertical.mp4 |
1.78 MB | 1776640 | 25.6 s | 1080×1350 h264 |
claude-tokens-vertical.mp4 |
1.43 MB | 1428013 | 20.4 s | 1080×1350 h264 |
claude-dashboards-vertical.gif |
1.80 MB | 1797436 | 25.6 s | 430×538 @ 9 fps |
claude-tokens-vertical.gif |
1.39 MB | 1391483 | 20.4 s | 430×538 @ 9 fps |
roundtrip.mp4 |
3.29 MB | 3292613 | 21.2 s | 1920×1296 h264 |
Readers reported the text was too small in every clip. The only real lever is the logical size of what is being recorded — a wider viewport or a wider terminal makes the glyphs smaller, because the README renders these at a fixed width (900 px for the web GIF, 800 px for the terminal ones).
So the recordings are deliberately small:
- Web: viewport 1024×640, not 1280×800. 1024 is the floor that still matches
Tailwind's
lg:breakpoint, below which the row's epic chip and trailing strip disappear and the UI stops looking like itself. - Agent clip: 1024×808 — a 168 px paper terminal stacked on the same 1024×640 app frame as the hero. Do not scale the iframe; the list has to match the hero's glyph size.
Web GIF tradeoff: the walkthrough is four beats (search, an open
issue, documents, the board) on the paper UI. export-video.sh starts at
fps=9 width=960 colors=128 and steps down to 8/960/96, then 8/900/64.
Note that fewer colors does not reliably shrink a GIF here: bayer dither
noise compresses worse, so a 96-colour pass can come out larger than a 128 one.
Motion is the real cost — an instant scrollTop jump costs a fraction of a
one-second smooth scroll. Cut a beat or a scroll before cutting resolution, and
point anyone who wants a light asset at the MP4.
Terminal GIFs: VHS's own encoder is generous, and Set Framerate does not
change the GIF's 25 fps frame table. make media-mcp finishes the take with
gifsicle -O3 --colors 64 when gifsicle is on PATH, which roughly halves
the file with no visible loss on a 64-colour paper theme.
A cut used to be segments of one take, full frame. e2e/demo/camera.mjs adds
a camera over a marked take: the spec's marks carry the rect of what each beat
is about (markAt(name, locator) in roundtrip.spec.ts), a shot list next to
the spec (roundtrip.shots.mjs) says in mark names where the camera arrives
and when, and the compositor renders the frames — eased punch-ins and pans,
motion blur, the end-card dissolve, the poster. No coordinate lives in the
shot list, so a reshoot moves the marks and the camera follows.
node e2e/demo/camera.mjs e2e/demo/roundtrip.shots.mjs --dry # timeline + camera keys, no render
node e2e/demo/camera.mjs e2e/demo/roundtrip.shots.mjs --sheet # one stamped frame per mark and per camera arrival
node e2e/demo/camera.mjs e2e/demo/roundtrip.shots.mjs # the cutThree rules it encodes, each from a measured failure:
- The zoom is legibility, not decoration. release-video.md's post-mortem: body text at ~1.1% of frame height is unreadable on a phone. A punch-in on the card or the roster is how a beat gets its text to readable size while the establishing frames stay whole.
- Source pixels are the ceiling. Playwright's screencast records CSS px at
1× whatever
deviceScaleFactorsays (measured 2026-09-03: DPR 2 with a 2× video size padded the 1× frame with grey). Every zoom enlarges the capture, somaxZoomin the shot list is a budget checked on the sheet, and the recording stays at the viewport's own size — one resample in the compositor rather than one in the recorder and another in post. - Blur is the camera's, and holds are crisp. Each frame averages K sub-samples of the camera transform across a 180° shutter; a still camera renders one sample. The sub-samples read the neighbouring source frame when the shutter crosses a frame boundary, so a fast pan does not strobe.
The lead between the spec's clock and the recorder's still comes from
rt-marks.py — now from the pane's own rect (the pane_open mark carries it)
instead of a hardcoded region that encoded the 0.19 side pane. Check the
--sheet before a render: a cut a half-second early loses a card landing, a
punch-in a half-second early zooms onto an empty spot.
Several takes, and a wipe. A shot list may name several takes
(takes: {before, after}) and give each segment its own camera; a segment can
open on a frozen frame of another take and wipe this one in over it under the
same camera (wipe: {from, at, start, dur, dir, labels}). That is the
before/after cut: e2e/demo/record-before-after.sh builds the previous tag in
a worktree, serves it and this checkout on the same demo fixture with a bare
/bin/sh in the pane, records before-after.spec.ts against each, and cuts
before-after.shots.mjs → scratch/before-after/read.mp4. The reveal starts
on the side the beat's subject sits on (dir: 'rtl' for the detail panel);
the version tags in the corners say which release is which.
Framing takes use e2e/demo/claude-stub.sh as the pane's claude
(record-roundtrip.sh without --live): the rig's gates pass in seconds and
the camera can be re-shot in ~3 minutes. Every stub frame says STUB in its
first line — it is for the sheet, never for a viewer.
# Prerequisites (already on a typical gadak dev machine):
# go, node ≥ 20, ffmpeg, Playwright chromium
# vhs + a Claude Code login — only for `make media-mcp` and
# `bash e2e/demo/record-claude-drive.sh`, which `make media` deliberately
# excludes (a live take spends your own model quota)
make mediaIndividual targets:
make media-web # Playwright → webm → gif + mp4 (self-contained)
make media-search # Playwright: ⌘K All search → search.gif + search.mp4
make media-agent # Playwright split: sql \| views open --keys + paper list → gif + mp4 (rig kept; the asset retired for terminal-demo)
make media-terminal # Playwright: gadak's own pane — claim, sql | views open --keys, --jql → terminal-demo.{mp4,gif,-poster.png}
bash e2e/demo/record-terminal-claude.sh # live Claude Code in the pane → terminal-hero.{mp4,gif,-poster.png} (not in `make media`; needs a Claude login; run `bash tools/tapes/prepare-claude-drive.sh --clean` after)
make media-mcp # VHS: claude mcp add + live Claude Code session on the mirror
make media-prep # build gadak + seed tools/tapes/.tmp from demo.db
bash e2e/demo/record-promo.sh # tokens + dashboards split (not in `make media`)
bash e2e/demo/record-vertical.sh # tokens + dashboards 1080×1350 (not in `make media`; README stays landscape)
bash e2e/demo/record-claude-drive.sh # flagship: live Claude Code × serve tab (not in `make media`; needs vhs + Claude login)
bash e2e/demo/record-claude-drive.sh vertical # same take, 1080×1350 (mp4 only)
bash e2e/demo/record-hero.sh # desk + phone interleaved, one serve — output not committed until the phone app ships (needs a Claude login AND a booted iOS simulator)
tools/record-raycast.sh # scripted LIVE take (not in make media): Raycast → gadak:// — needs Raycast + installed app
make brand # logo, wordmarks, favicons, OG cardOutputs land in docs/media/. Commit them — the README references the paths
directly, and CI does not regenerate media.
~20 s of readable motion, viewport 1024×640 @ deviceScaleFactor: 2:
- Boot — the epic breakdown (the startup view whenever the mirror has
epics,
startup-view.ts), 534 issues in the sidebar, labels on the rows - Instant local search (
pagination) with per-keystroke narrowing and<mark> - NMA-123 open beside the list — title, priority, labels, reopen badge
- Sidebar Documents — Viewed, then one page open with its breadcrumb and the issues it cites
- Sidebar All open, then Board in the view-settings menu: the same open work as status columns. (An Epics beat would repeat the boot frame now that the epic breakdown is the startup view — swapped 2026-09-02.)
The in-app Jira/Confluence pane is desktop-only (a native WKWebView). This
clip is the browser tab against examples/demo.db. The spaces tree was
dropped from this take — it did not earn its bytes next to the issue-detail
beat.
~7.5 s, same viewport 1024×640 @ deviceScaleFactor: 2 as the hero
(README render width 900 px):
- Boot on
/#/?pj=NMS— paper list, one chip Project: NMS, 157 issues, toolbar Search ⌘K in frame - ⌘K opens the palette (
Searches every issue and document.) - Type
work(local Documents + Issues sections highlight title hits), thenaround— the usearch.spec.ts comment-only token. Local rows empty; after the 250 ms debounce (UNIFIED_DEBOUNCE_MSinweb/src/lib/unified-search.ts) ALL SEARCH fills with issue rows and Comment match snippets - Enter on the first unified hit (NMA-36, not in the NMS chip) opens the issue; the last ~1.8 s hold the detail (the matching comment is on screen) beside the still-active NMS chip
consequences (usearch's page-body token) is not in this take: the last beat
has to be an issue detail, and that token matches pages only.
Config lives in e2e/demo/search.config.ts. Same GADAK_MEDIA=1 gate as the
other demo specs.
Re-shoot trap (2026-08-06): a leftover :7877 from an earlier e2e run is not
freshened, and "Sync delayed" prints into every frame. The demo config now
starts a fresh server (reuseExistingServer: false) and GADAK_FRESHEN=1
in e2e/serve.sh updates the snapshot clock. Stop a leftover with
pkill -f 'e2e/.tmp/gadak' if a recording still shows the orange chip.
Note the video size in e2e/demo/playwright.config.ts must equal the viewport.
Playwright does not upscale into a larger video frame — it pins the capture in
the top-left corner and pads the rest black.
Config lives in e2e/demo/playwright.config.ts (separate from the E2E suite).
The demo test is gated by GADAK_MEDIA=1 so a plain
playwright test --config e2e/playwright.config.ts still runs only the original
10 specs (the demo file is discovered as skipped).
No caption overlays are injected into the DOM — the app code under web/src/
is never touched by this pipeline.
One take, two panes, two beats, ~19 s, viewport 1024×808:
- A paper terminal types
gadak sql --no-header "select key from issues where status_category='inprogress' order by status_changed_at asc limit 5" \| gadak views open --keys -("stuck the longest in progress" — universal, where a reopen workflow is team-specific;--no-headerkeeps the header row from becoming a fake key) - The real app (iframe, same 1024×640 frame as the hero) sits underneath on the default All open view (368 issues)
- On Enter the test runs that pipe against the serve fixture (
--no-open); the iframe pollsGET ui-focus/and the list becomes those five keys (NMA-1, NMS-3, NMB-5, NMB-6, NMB-10 on the current snapshot), with a 5 keys chip and 5 issues. Sort reads Given order. Defaultgroup_byis still status category (jql.Hashwith an empty Display does not emitg=none), so the five rows section into In progress / New - The terminal clears and types
gadak views open --jql 'project = NMA AND priority = High AND resolution is EMPTY'. The hash ispj=NMA&pr=High&sc=new%2Cinprogressand the list lands on four chips — Project: NMA, Priority: High, and the two status categories thatresolution is EMPTYbecomes (decision 0007) — at 22 issues
Beat 4 exists because the two beats are different promises: the pipe says any answer you can compute is a view, the JQL says you do not have to leave the query language you already know to get one. A reader who saw only the pipe would conclude gadak requires SQL.
The command is typed in the wrapper, not in the app — web/src/ stays
untouched. The on-screen output is only the hash\tks=… line; the web\t
URL views open also prints is dropped so a loopback address does not land
in the frame. No live model, no credential, no take-to-take drift. --no-open
keeps the take from launching Gadak.app or a second tab.
tools/tapes/agent.tape is the older CLI-only SQL take (status / reopen /
search). It is not what the README embeds.
~21 s, viewport 1744×672 (720 px paper terminal | 1024×640 app iframe),
deviceScaleFactor: 2. Sibling of the agent split, left/right instead of
stacked. Regen: bash e2e/demo/record-promo.sh. The capture home is frozen
(gadak config set frozen true) before the first frame.
- Boot on
/#/?lb=quick-win&sc=new%2Cinprogress— list already painted, Labels: quick-win chip in the filter bar - Terminal types
gadak config set ui.tokens '{"colors":{"accent":"#7a4bd0"}}'(the value indocs/CONFIGURATION.md). On Enter the test runs that string;--color-accentbecomes#7a4bd0without a reload — the filter chips' border, the 가 mark and the Feed badge pick up the purple (the New issue button is neutral since the 0.20 UI pass, so the retint reads quietly) gadak config set ui.dataColors '{"label":{"quick-win":"#2e7d32"}}'— row label chips pick up the green tintgadak config set ui.tokens '{"colors":{"bg-base":"#000000"}}'saves with alockedwarning (GDK-789: grounds are palette authoring, so the write is honoured and the warning says an upgrade may re-derive it); the list ground goes black on the next poll and the take holds there ~3 s before scene 4 restores it. Honest, but the half-black frame is the ugliest second in the clip — a candidate for cutting the scene (noted 2026-09-02)ui.tokenswithspacing.row50px: rows repaint taller in place (dimension axis, GDK-842)
Commands are real (spawnSync of the typed string). Loopback web\t lines
are dropped, same as the agent clip.
~28 s, same split viewport as tokens. SQL keys status_category, never a
localized status name.
- Boot on the default paper list
- Terminal types
gadak dashboards save triage --html examples/dashboards/triage.htmlwith--datasourceby_statusandmonthly_opened(the two queries ine2e/demo/dashboards-demo.spec.ts). Output issaved\t<id>\ttriage gadak dashboards open triagewriteshash\tdash=<id>; the iframe opens the wall (count cards, uPlot monthly line, sidebar DASHBOARDS / triage)- A second save with
e2e/.tmp/triage-v2.html(<h1>Triage · v2</h1>) replaces the open frame (data-render-genmoves; the h1 reads Triage · v2) - Refresh once, then a four-click burst (the host's 2 s throttle)
tools/tapes/prepare-promo.sh latches frozen true on a capture home (the
demo specs also call gadak config set frozen true themselves).
Same bits as the landscape tokens / dashboards takes, re-shot stacked at 1080×1350 (4:5 — X feed max without crop). Mac bar 48 + terminal band 340
- web 962. Regen:
bash e2e/demo/record-vertical.sh(port 7890). mp4 only — no GIF. README keeps the landscape cut.
The nested dashboard iframe is narrower than 960 px beside the app sidebar, so
the vertical dashboards take injects capture-only CSS (applyVerticalDashboardLayout
in e2e/demo/promo-split.ts) to keep four count cards in a row and pin the
uPlot chart in frame. examples/dashboards/triage.html is not edited.
26.6 s, flagship landscape split (1880×720, 720 px VHS terminal |
1160×688 serve tab; tokens/dashboards stay 1744×672). Paper chrome from
promo-split.ts (FLAGSHIP_L_*). Vertical sibling
claude-drive-vertical.mp4 is 1080×1350 (bar 48 + Claude TUI band 520 +
web 782), 30.9 s. Regen: bash e2e/demo/record-claude-drive.sh (landscape) or
bash e2e/demo/record-claude-drive.sh vertical. Not in make media — it
needs vhs and a Claude Code login, same reason as make media-mcp. The
capture home is a throwaway directory (/private/tmp/gadak-claude-drive/gadak-home),
seeded from examples/demo.db, with gadak config set frozen true
(tools/tapes/prepare-claude-drive.sh). gadak skill install writes the
embedded skill into that isolated HOME (not ~/.claude). Run
bash tools/tapes/prepare-claude-drive.sh --clean afterwards — the isolated
HOME holds a 0600 copy of this machine's credentials (from
~/.claude/.credentials.json when present, otherwise the macOS keychain
service Claude Code-credentials).
Nothing on the left is scripted output: VHS types claude, then one English
prompt, and Sleeps while the model works. Commands and the HTML it writes are
the model's. The orchestrator re-runs up to 3 takes until the result contract
holds (two colour changes reflected in the open tab, plus a saved dashboard
whose HTML contains uPlot/canvas and whose frame actually opened). After the
wall opens, Playwright sends a mouse-wheel into the dashboard iframe until a
chart canvas is in view (camera work, not an edit of Claude's HTML).
Left/right start epochs are logged and stacked (e2e/demo/export-claude-drive.sh).
Static holds ≥ 0.6 s are sped to 0.5 s (e2e/demo/static-cut.py). The head
does not blanket-protect 0–18 s: $ claude (~1.2 s) and the measured
prompt-typing window stay 1×, as do colour changes and dashboard_open
± 1.2 s (e2e/demo/edit-claude-drive.py); the TUI boot freeze between
them uses the same static-run rule.
A paper-coloured drawbox covers the Claude TUI footer (manual mode on) so a
parent-session harness leak does not land in the public frame. Claude's own
usage-limit banner is left in the conversation.
The flagship asks for the whole job in one take; the two split clips ask for half each, so a social-length cut can hold on one thing. Same rig, same geometry, one argument:
bash e2e/demo/record-claude-drive.sh vertical claude-dashboards
bash e2e/demo/record-claude-drive.sh vertical claude-tokensEach clip carries its own result contract, because "a take finished" is not
"a take is shippable" — measured 2026-08-25, a dashboards take passed the old
chart-exists contract on a wall whose cards all read 0. The dashboards
contract now requires the dashboard_data beat (the wall painted real
numbers) and dashboard_link_nav (a key on the wall was clicked and the
app opened that issue); the tokens contract requires a colour change plus a
dimension override in the reopened tab.
The recorder's serve deliberately listens inside serveProbePorts()' sweep
(7796 landscape, 7795 vertical). On an out-of-range port dashboards open
finds no serve and the take records the agent hunting the port by hand — a
rig artifact standing in for a product failure. The discovery gap itself is
real and filed as GDK-859; the port choice only keeps it out of the frame.
Two scenes, both real, VHS tape tools/tapes/mcp.tape, 1080×620,
font 20 (same 77×24 / 800 px README convention as the other terminal tapes):
-
claude mcp add gadak -- gadak mcp— the one line that teaches an MCP client the mirror exists (docs/AGENT_SETUP.md). -
A live Claude Code session answering "Search our Jira and wiki for idempotency. Answer in 3 lines." — the question Jira cannot answer at all, because the wiki is a second search. The model calls
gadak_search(and only the MCP toolsprepare-agent.shpre-allows). Nothing is scripted, so takes differ; re-run until one reads well.Not a
reopen_countquestion, deliberately. That field already carries the README quick start and the agent clip; a third use made the demo read as if it were the only derived column gadak has.Take notes worth keeping: pin the answer length ("Answer in 3 lines" — "Brief." is not binding, and a long answer scrolls the question out of frame). Extended thinking is off and
Task/Glob/Grepare denied inprepare-agent.sh; without those denies a take spawned an Explore subagent and searched the codebase. Haiku was tried for speed and rejected: it calledgadak_searchwith the wrong argument name and then reported "zero results" instead of the error.
Theme is gadak-paper (same JSON as tools/tapes/agent.tape). Claude
Code's own UI is ink-on-paper on that background; the only token that
vanishes is the collapsed tool-call count ("Called gadak times" —
the number is a bright color). The question, the "Calling gadak…" line,
and the answer stay readable, so this take does not fall back to a dark
theme.
Everything visible comes from examples/demo.db. On that snapshot
idempotency matches 7 issues and 4 wiki pages through the one FTS
index, which is why the answer can put NMA-142 and the Confluence page
API Platform Brief — Idempotency (PROD space) in the same sentence — the
join Jira and Confluence never make for you.
make media-mcp requires vhs (fails with media-mcp: vhs required (brew install vhs) when missing), runs prepare.sh then
prepare-agent.sh, records the tape, and runs gifsicle -O3 --colors 64
when gifsicle is on PATH. Timing is not deterministic: if the GIF is
still over 3.5 MB, cut the idle tail after the answer lands (VHS GIF
frames are 25 fps regardless of Set Framerate):
gifsicle --info docs/media/mcp.gif | head -2 # total frame count
gifsicle -O3 --colors 64 docs/media/mcp.gif '#0-N' -o docs/media/mcp.gif# 1–2. seed the throwaway mirror + isolated HOME, write pin/strip helpers
# into that HOME, record, gifsicle. This is the supported path — raw
# `vhs tools/tapes/mcp.tape` needs those helpers already in
# /private/tmp/gadak-demo/ (make writes them; VHS cannot parse
# escaped $/" inside Type).
make media-mcp
# 3. inspect frames; re-run make media-mcp until a take reads
ffmpeg -y -ss 00:00:02 -i docs/media/mcp.gif -frames:v 1 /tmp/mcp-frame.png
# 4. LAST — remove the isolated HOME, including the credential copy
bash tools/tapes/prepare-agent.sh --cleanprepare-agent.sh refuses if this machine has no Claude Code login
(~/.claude/.credentials.json). Do not skip step 4: the isolated HOME
at /private/tmp/gadak-demo holds a 0600 copy of that file.
e2e/demo/
agent.config.ts # 1024×808, video on, fresh :7877
agent-demo.spec.ts # GADAK_MEDIA=1 gated split
export-agent.sh # webm → gif (palette 2-pass) + mp4
search.config.ts # 1024×640, separate output dir
search-demo.spec.ts # GADAK_MEDIA=1 gated ⌘K take
export-search.sh # webm → search.gif + search.mp4
The demo seed keeps some Korean Jira status/type labels (완료, 진행 중,
버그). Terminal columns need a monospace CJK font or Hangul cells
mis-width and the agent tape drifts.
Tapes set:
Set FontFamily "D2Coding, Menlo, Monaco, monospace"
D2Coding is present on the machine that produced the committed GIFs. On another host, install one of:
- D2Coding
- Sarasa Mono K / Sarasa Gothic
- Noto Sans Mono CJK
Check with fc-list :lang=ko family or system_profiler SPFontsDataType. If no
CJK mono font is available, remove Set FontFamily and accept Latin-only
alignment — note that in the tape comment when you do.
- Data source is always
examples/demo.db(fictional people and*.example.comaddresses). - VHS tapes
source tools/tapes/.tmp/env.shunderHideso the visible prompt is a bare$— no username, hostname, or home path. HOMEis redirected totools/tapes/.tmp/fake-homefor the recording shell.- Do not re-record against a real Jira mirror. If a frame ever shows a real
company, person, or domain, discard the asset and re-seed from
examples/. prepare-agent.sh(required formake media-mcp) copies a credential into an isolated HOME at/private/tmp/gadak-demo. Run--cleanafterwards — it is the last step of every MCP re-shoot, includingmake media.
Re-run make media whenever any of these change in a way that is visible:
examples/demo.dbis regenerated (tools/seed-demoor similar)- Web UI layout / copy that appears in the walkthrough
- CLI output shape for
search/sql/issue/status/views - MCP tool names or the
claude mcp add gadak -- gadak mcpregistration line
The mirror is disposable; the scripts are the source of truth. After regenerating,
spot-check frames with ffprobe and a mid-clip PNG extract before committing:
ffprobe -v error -show_entries format=duration,size -of default=nw=1 docs/media/web-demo.gif
ffmpeg -y -ss 00:00:02 -i docs/media/web-demo.gif -frames:v 1 /tmp/web-frame.pnge2e/demo/
playwright.config.ts # video on, fixed viewport — not the E2E suite
web-demo.spec.ts # GADAK_MEDIA=1 gated walkthrough
export-video.sh # webm → gif (palette 2-pass) + mp4
search.config.ts # 1024×640, separate output dir
search-demo.spec.ts # GADAK_MEDIA=1 gated ⌘K All-search take
export-search.sh # webm → search.gif + search.mp4
agent.config.ts # 1024×808 split, separate output dir
agent-demo.spec.ts # GADAK_MEDIA=1 gated agent-focus take
export-agent.sh # webm → agent.gif + agent.mp4
tokens.config.ts # 1744×672 L/R split, port 7888
tokens-demo.spec.ts # GADAK_MEDIA=1 gated tokens take
export-tokens.sh # webm → tokens.gif + tokens.mp4
dashboards.config.ts # same split as tokens
dashboards-demo.spec.ts
export-dashboards.sh
record-promo.sh # unattended tokens + dashboards regen
tokens-vertical.config.ts # 1080×1350 stacked, port 7890
dashboards-vertical.config.ts
record-vertical.sh # unattended vertical regen (mp4 only)
export-vertical.sh # webm → *-vertical.mp4, no GIF
promo-split.ts # paper terminal + iframe chrome (landscape + vertical)
roundtrip.spec.ts # four shells, four issues; marks (+ rects via markAt) to proof.jsonl
record-roundtrip.sh # seed + serve + take; --live runs real Claude Code, else claude-stub.sh
roundtrip.shots.mjs # shot list: segments and camera arrivals, keyed by mark names
camera.mjs # marked take + shot list → eased zoom/pan, motion blur, end card, poster
rt-marks.py # mark → video seconds; lead from the pane_open luma cliff (rect-driven)
claude-stub.sh # the pane's `claude` on framing takes — says STUB, never shipped
endcard.mjs # end card PNG (ENDCARD_VERSION / ENDCARD_LINE)
before-after.spec.ts # the same screens against two serves; marks + rects to proof.jsonl
before-after.config.ts # 1472×994, no webServer (record-before-after.sh owns both serves)
before-after.shots.mjs # shot list: wipes from the previous release into this one
record-before-after.sh # previous tag worktree + two serves + two takes + camera
claude-drive.config.ts # 1880×720 flagship landscape, port 7796 (vertical 1080×1350 on 7795)
claude-drive-web.spec.ts
record-claude-drive.sh # VHS + Playwright, up to 3 live takes
export-claude-drive.sh # epoch offset stack + palette gif
edit-claude-drive.py # protect timestamps + tail trim for static-cut
static-cut.py # clip-agnostic: compress static runs ≥0.6s to 0.5s
tools/tapes/
prepare.sh # build binary, seed GADAK_HOME from demo.db
prepare-promo.sh # `gadak config set frozen true` on a capture home
prepare-agent.sh # isolated HOME + auth for the live Claude MCP take
prepare-claude-drive.sh # frozen demo home + skill install + Claude auth copy
agent.tape # optional CLI-only VHS (not the README clip)
mcp.tape # VHS: claude mcp add + live Claude Code session
claude-drive.tape # VHS: live Claude Code drives ui.* + dashboards
.tmp/ # disposable (gitignored)
docs/media/ # committed outputs