retake-demos 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example CHANGED
@@ -1,3 +1,13 @@
1
+ # Your app's demo account, for demos that start behind a login. You rarely edit
2
+ # this by hand: the agent asks by name and the Retake window (or `retake secret
3
+ # APP_USER APP_PASSWORD`) writes it here. Stays on this machine; never sent to the
4
+ # agent. A manifest references ${APP_USER} with `secret: true` — never the values.
5
+ # APP_TOTP_SECRET is the authenticator enrolment secret; ${TOTP:APP_TOTP_SECRET}
6
+ # becomes the current code. Blank lines below count as unset.
7
+ APP_USER=
8
+ APP_PASSWORD=
9
+ APP_TOTP_SECRET=
10
+
1
11
  # Retake — optional. Only `retake describe` and `retake ideas` use a model; validate/dry/run/render/check never do.
2
12
  # With a coding agent driving Retake over MCP you can leave all of this empty.
3
13
  # Pick explicitly with RETAKE_MODEL=claude-code|codex|groq|mistral|local; otherwise the first available is used.
package/AGENTS.md ADDED
@@ -0,0 +1,277 @@
1
+ # Using Retake from Claude Code, Codex, or any coding agent
2
+
3
+ Retake records product demos of web apps from a small YAML manifest. Its CLI
4
+ is deliberately agent-shaped: every command prints plain text, exits non-zero
5
+ on failure, and the cheap checks come before the expensive recording. An agent
6
+ can drive the whole loop — write, check, record, inspect, fix — without ever
7
+ opening the UI.
8
+
9
+ This file is also what Claude Code and Codex read when you point them at this
10
+ folder, so it doubles as the tool's own instructions to them.
11
+
12
+ `retake` here is the installed command (`npm install -g retake-demos`). From a
13
+ clone of this repo the same commands run from source as `npm run retake -- …`.
14
+ The MCP route is better than the CLI when it exists: `retake install` registers
15
+ the tools and the skill with Claude Code, so the loop below becomes tool calls.
16
+
17
+ ## What a change costs
18
+
19
+ Reaching for a new recording when a re-render would do is the one mistake
20
+ that wastes real time. Almost nothing needs the camera twice.
21
+
22
+ | You want to change | Do this | Cost |
23
+ |---|---|---|
24
+ | size, shape, format | `retake render <dir> --preset <other>` | seconds |
25
+ | caption words, speed, zoom, cards, music | edit the manifest, `retake render` | seconds |
26
+ | **where** a caption or still sits | `nudge: <ms>` on the scene, then render | seconds |
27
+ | the ENDING of a demo | `retake run … --from <scene>` | the tail only |
28
+ | one beat, while iterating | `retake run … --until <scene>` | the head only |
29
+ | what the demo actually DOES | full `retake run` | the whole thing |
30
+
31
+ `--from` runs the earlier steps at full speed and off camera so the app
32
+ reaches the right state, then starts recording there. A seven-minute demo
33
+ whose last third changed costs ninety seconds.
34
+
35
+ Housekeeping, since outputs grow: `retake takes <dir>` lists the kept
36
+ versions of a demo and `retake restore <dir> <id>` puts one back;
37
+ `retake tidy` reports reclaimable disk and removes nothing without
38
+ `--apply`. Neither ever touches a raw recording or a take.json — those are
39
+ the only things a re-render cannot rebuild.
40
+
41
+
42
+ ## Starting from nothing (the person pasted the repo link)
43
+
44
+ ```
45
+ npm install -g retake-demos
46
+ mkdir -p ~/retake-demos && cd ~/retake-demos && retake install # workspace + tools + skill
47
+ retake ui # their window: http://localhost:4310
48
+ ```
49
+
50
+ `retake install` registers the MCP tools with Claude Code (user scope) and
51
+ prints the Codex / Cursor config. Tools load at the start of a session, so
52
+ in the session that ran the install, use the CLI loop below and tell the
53
+ person to restart you afterwards. `retake doctor` says what, if anything,
54
+ is missing. Always tell the person the window's address — they may not know
55
+ it exists.
56
+
57
+ ## The loop an agent should run
58
+
59
+ ```
60
+ write demos/<name>.yaml
61
+ → retake validate demos/<name>.yaml schema + warnings (instant)
62
+ → retake dry demos/<name>.yaml every selector & wait, no camera (~30–60s)
63
+ → fix what dry reports, repeat until "all N steps resolved"
64
+ → retake run demos/<name>.yaml --preset preview-fast first take (~2 min)
65
+ → read outputs/<name>/proof-log.md what actually happened, per step
66
+ → retake check outputs/<name> pass/fail on the result
67
+ → adjust, then: retake run demos/<name>.yaml final quality
68
+ ```
69
+
70
+ Rule of thumb: **never go straight to `run`.** `dry` catches most failures in
71
+ seconds; a failed `run` costs two minutes and a confusing video. When `dry`
72
+ fails it prints the step, the error, and the text that was on screen — read
73
+ that before changing anything.
74
+
75
+ ## How to tell Claude Code / Codex to use it
76
+
77
+ From the Retake folder:
78
+
79
+ ```
80
+ claude "Record a demo of http://localhost:3000 that shows a user creating
81
+ their first project. Use Retake: draft demos/first-project.yaml,
82
+ dry-run it until it passes, record a preview, then check it.
83
+ Show me the proof log."
84
+ ```
85
+
86
+ or with Codex:
87
+
88
+ ```
89
+ codex "Using Retake (see AGENTS.md), make a 30-second demo of the sign-up
90
+ flow at http://localhost:3000. Dry-run before recording."
91
+ ```
92
+
93
+ Both CLIs read this file automatically when run in this directory, so the
94
+ loop above is already in their context. If the agent works from *another*
95
+ folder (your app's repo), add the workspace: `claude --add-dir <your retake workspace>`
96
+ — or skip all this with `retake install`, which gives the agent the tools directly.
97
+
98
+ ## Letting the agent draft with Retake's own scouting
99
+
100
+ The agent does not have to write the manifest from nothing:
101
+
102
+ ```bash
103
+ retake describe <name> <url> "<one sentence of what to show>" -P <path-to-app-source>
104
+ retake ideas <url> -P <path-to-app-source> # 5–7 demo ideas, saved to ideas/
105
+ ```
106
+
107
+ `describe` scouts the live page for real, unique selectors, reads the app's
108
+ source for routes / sign-in fields / things that animate, and drafts the YAML.
109
+ `-P` is optional but makes drafts much better. The model is whichever is set in
110
+ `.env` (`RETAKE_MODEL=claude-code|codex|groq|mistral|local`) — so an agent can
111
+ use Retake's drafting, or draft itself; both are fine.
112
+
113
+ ## What the agent must know about manifests
114
+
115
+ Full reference: `README.md` → "Demo-as-code". The short version:
116
+
117
+ - **Wait for the RESULT, and know the three ways a bare selector lies.** It
118
+ resolves the moment the element exists, which is wrong when (a) the element
119
+ is the PREVIOUS action's banner still on screen — use
120
+ `{ action: waitFor, selector: ".landed", gone: true }` and wait for it to
121
+ leave; (b) the element is a shell that streams its text in — use
122
+ `minChars: 40`; (c) the element is there but still changing — use
123
+ `stableMs: 500`, which resolves when its subtree has been quiet that long.
124
+ Each of these cost a real take before it existed.
125
+ - **Wait for results, not timers.** After any click that loads or computes
126
+ something, `{ action: waitFor, selector: <thing that appears> }`. A fixed
127
+ `wait` is for pacing only.
128
+ - **Selectors must be unique.** Playwright runs strict: a selector matching two
129
+ elements fails. Prefer ids, `data-testid`, `aria-label`, then
130
+ `button:has-text("…")`; append `>> nth=0` only when duplicates are intended.
131
+ - **`run --name <x>`** records into outputs/<x> without touching the
132
+ original; **`render <dir> --out <newdir>`** re-renders a copy and leaves
133
+ the published cut alone. Revising is never overwriting.
134
+ - **A failed step writes `failed-step.png`** (full page, the exact moment)
135
+ and the failing URL into the log and take.json. Read the picture first.
136
+ - **`manifest.used.yaml`** sits in every output — the exact manifest this
137
+ video came from, not whatever the file on disk says today.
138
+ - **Size is a render-time choice.** One preset = one output size, always
139
+ (the caption band sits inside the canvas). Any take renders at any preset:
140
+ `retake render outputs/<name> --preset post-vertical`. Never re-record to
141
+ change a shape, and never set `viewport` — `validate` warns because it
142
+ makes one demo a different shape from the rest.
143
+ - **`retake contact outputs/<name>`** — one timestamped grid of the whole
144
+ video. This is how you review a cut without pulling frames by hand, and
145
+ how you check a caption is still true in the middle of its own scene.
146
+ Launch cuts write `contact.png` automatically.
147
+ - **The poster** is `thumbnail.png`. `outputs.thumbnail: { scene: <label> }` picks the scene; the person can override it in the window from any frame, a generated cover, or their own image (`POST /api/cover/<name>`). Launch mode also writes `cover.png` (the title card) and `cover-titled.png` (a real frame with the title on it) as candidates. Do not re-render to change a poster.
148
+ - **`mode:`** — `demo` (default) proves the interaction: nothing in the frame
149
+ but the product. A CLIENT WALKTHROUGH IS A DEMO. `launch` presents the
150
+ product in public (a launch, a post, an ad) and may carry a title card,
151
+ music, emphasis and a branded ending — ending with THEIR product, never
152
+ with Retake. Decide from the audience, not the subject; never upgrade a
153
+ demo yourself (`validate` warns).
154
+ - **voiceover** needs a continuous `script:` and `approved: true` from a
155
+ person who heard it. Off otherwise.
156
+ - **The launch cut**: `compressIdle: true` (waits shown short — render-time),
157
+ `voiceover: true` (captions read aloud), `typing: brisk` (re-record). Offer
158
+ as one package when the destination is a launch or post; default is plain.
159
+ - **`music: <file>`** mixes a bed under the video at render (looped, faded
160
+ out). The person supplies the file — never invent a path, never bundle.
161
+ - **Cards and callouts:** `intro: { title, subtitle }` / `outro:` splice a
162
+ title card (render-time; the intro frame becomes cover.png). A
163
+ `{ action: callout, selector, label }` step rings an element on camera.
164
+ At most one callout per scene; never callout what the caption already says.
165
+ - **Scenes carry the story.** `{ action: scene, label, caption }` at each beat;
166
+ captions burn into the video. The camera is still by default; `camera: auto`
167
+ eases toward the last thing touched, or per scene `camera: { focus:
168
+ <selector>, zoom: 1.3 }`. Add zoom only when the person asks.
169
+ - **Logins go in `auth.setup`**, never in `steps` — see "Logins" below.
170
+ - **Backend not available?** `stub:` answers API calls with canned JSON for the
171
+ take, and a `stub` step swaps the answer mid-demo. Every stub is named in the
172
+ proof log.
173
+ - **Long forms:** `{ action: scroll, to: <selector>, align: center }` before
174
+ filling them, so the viewer sees the whole thing.
175
+
176
+ ## Logins
177
+
178
+ Most real demos start behind a sign-in. The agent never sees a password:
179
+
180
+ - **Ask by name, not for the value.** Over MCP, call `secrets` with the
181
+ variable names (`APP_USER`, `APP_PASSWORD`, `APP_TOTP_SECRET` for an
182
+ authenticator) and a one-line why. With the Retake window open, a form
183
+ appears there and the values go straight into the workspace `.env` — the
184
+ tool returns "set". Without a window, the tool returns the sentence to
185
+ relay: the person runs `retake secret APP_USER APP_PASSWORD` (typed into
186
+ their terminal, hidden, kept on their machine) and the agent calls
187
+ `secrets` again. From the CLI only, say the same sentence yourself.
188
+ - **Put the sign-in under `auth.setup`** with `${APP_USER}` / `${APP_PASSWORD}`
189
+ and `secret: true`, plus `auth.storageState: .auth/<name>.json`. It runs
190
+ before the camera, is trimmed off, and the session is reused by later takes.
191
+ A failed sign-in never saves a session.
192
+ - **Authenticator codes:** `${TOTP:APP_TOTP_SECRET}` is the current six-digit
193
+ code, computed when the field is filled.
194
+ - **SMS, SSO, captcha:** a script cannot; a person can, once.
195
+ `retake signin demos/<name>.yaml` opens a real window, they log in, press
196
+ Enter, and the session is saved. Leave `auth.setup` empty; a take with no
197
+ fresh session and no setup refuses to run rather than record logged out.
198
+ - **Firebase-style sessions** (IndexedDB) cannot be restored from a saved
199
+ session: put the login under plain `setup` so it runs every take.
200
+ - **Demo accounts only.** The output is a video.
201
+
202
+ ## Reading the results
203
+
204
+ - `outputs/<name>/proof-log.md` — per-step ✓/✗ with timings, the shot list,
205
+ quality facts, and which endpoints were stubbed. This is the file to read
206
+ when something looks wrong.
207
+ - `outputs/<name>/take.json` — the same, machine-readable.
208
+ - `outputs/<name>/demo.mp4` — the video. `master.mp4` is the CRF-14 keeper
209
+ on post presets.
210
+ - `retake check outputs/<name>` — resolution, fps, duration, files,
211
+ pass/fail. Exit 3 on failure.
212
+
213
+ Exit codes: `run` → 0 all steps passed, 2 something failed (video still
214
+ produced, proof log says why). `dry` → 3 if any step would fail.
215
+
216
+ ## Fixing a take without re-recording
217
+
218
+ Captions, camera zoom, hold length, trim, format and layout are **render-time**.
219
+ Change them in the YAML and `retake render outputs/<name>` — seconds,
220
+ no browser. Only changes to what the browser *does* (steps, selectors, waits,
221
+ viewport) need `run` again; `run --reuse` skips the browser when nothing
222
+ recording-relevant changed, and the UI's one button works this out for you.
223
+
224
+ ## Things that trip agents up
225
+
226
+ - The app must be running and reachable at the manifest's `url` before `dry`
227
+ or `run`. Retake says "Nothing is running at …" if not.
228
+ - `describe` and `ideas` need a model configured (`.env` or the UI's Settings).
229
+ `validate`, `dry`, `run`, `render`, `check` never use a model.
230
+ - Two runs on the same demo at once are refused (a lock); wait for the first.
231
+ - Deleted demos go to `.trash/`; `POST /api/demos/<name>/restore` or copy the
232
+ file back.
233
+ - **Wait for durable state, not its announcement.** Toasts and banners clear
234
+ themselves (and often share a class with other notices); a wait on one can
235
+ time out while the thing it announced sits on the board. Wait for the row,
236
+ the count, the heading — `.act >> nth=1` beats `.landed`.
237
+ - `dry` runs file/command seeds first so it sees the state `run` will
238
+ (`--no-seed` to skip). `evaluate` seeds only run with a camera.
239
+ - A one-line `script:` with braces is a YAML mapping, not a string — write
240
+ `script: |` and put the JS on the next line.
241
+ - Iterating on one beat of a long demo: `run demos/x.yaml --until <scene>`
242
+ records up to the end of that scene and stops (MCP: `run` with `until`).
243
+ Better still, keep demos short — above ~40 steps, split the story.
244
+ - If Retake itself fails (render/ffmpeg error, crash), stop and report it —
245
+ do not patch around the tool.
246
+ - **Cursor cap: ~180 moves per take.** (Was ~45; Retake now flattens testreel's
247
+ cursor expressions at install.) `validate` warns past it, the recorder
248
+ detects a failed overlay and `check` FAILs. Split long stories anyway —
249
+ above ~40 steps nobody is watching.
250
+ - Stills: `NN-label.png` is mid-scene, `NN-label-end.png` the last moment.
251
+ - Scroll pacing defaults to constant pixels-per-second; `speed:` overrides.
252
+ - **New tabs are folded back into the recorded page** (`keepInTab`, default
253
+ true): `window.open` navigates in place and `target="_blank"` is stripped.
254
+ Retake records one page, so a popup login or a "preview" button used to lose
255
+ the demo's subject entirely. Set `keepInTab: false` if a flow genuinely needs
256
+ a separate window — and know it will not be on camera.
257
+ - **Iterate at `--preset draft`.** Same 960px layout as the post presets (so
258
+ selectors, wrapping and timing match) at a quarter of the pixels. Measured on
259
+ a 22s demo: capture 22.4s vs 30.1s, render 1.2s vs 3.6s — the 1080p encoder
260
+ adds ~8s to every take and competes with the app being recorded. Ladder:
261
+ `draft` while fixing, `preview-fast` to check final framing, a post preset to
262
+ ship. Never ship a draft.
263
+
264
+ ## Every release gets a note a non-developer can read
265
+
266
+ `site/releases.json` is the one list of what shipped. The landing page and the
267
+ Retake window both read it — there is no second copy to update.
268
+
269
+ When a release changes anything a person can see, add an entry: a version, a
270
+ date, a headline, and a few lines. Write them for the person using Retake, not
271
+ for whoever wrote the code. Name no files. Name no flag they will not type. Do
272
+ not write "refactor", "regression", "schema" or "pipeline". If a line only
273
+ makes sense to someone who has read the source, it is not a release note —
274
+ it belongs in the commit message, which is where that audience already is.
275
+
276
+ Good: "You choose the cover image. Click any frame to make it the one people
277
+ see first." Bad: "Poster selection is now persisted via a marker file."
package/README.md CHANGED
@@ -19,6 +19,19 @@ It is built for the way people work now: **you ask your coding agent for a demo
19
19
 
20
20
  <p align="center"><img src="assets/window.png" width="900" alt="The Retake window: your demos on the left, the video and its scenes in the middle"></p>
21
21
 
22
+ ## If you are a coding agent reading this
23
+
24
+ Someone pasted this link and wants demos recorded. Do this, in order, and say what you are doing as you go — assume they have never seen Retake:
25
+
26
+ 1. `npm install -g retake-demos` (Node 20+; ffmpeg is bundled, Chromium downloads once).
27
+ 2. Make a workspace and set up from inside it: `mkdir -p ~/retake-demos && cd ~/retake-demos && retake install`. This creates `demos/` and `outputs/`, registers Retake's tools with Claude Code and installs its skill, and prints the config for Codex/Cursor — apply it if that is what you are.
28
+ 3. **Your tools arrive at your next session.** For this one, drive Retake from the CLI exactly as [`AGENTS.md`](AGENTS.md) describes (`validate → dry → run → check`). Tell the person: "Restart me after this and I'll have Retake's tools directly."
29
+ 4. Start the window for them — `retake ui` in the workspace — and give them the link: http://localhost:4310. That is where the video, stills and proof log appear.
30
+ 5. If they did not say what to record, ask one question: which app (URL) and what to show. If the app needs a login, read the "Behind a login" section below — you never handle the password yourself.
31
+ 6. Read [`skill/SKILL.md`](skill/SKILL.md) once: it is the method (order of operations, failures as text, when to stop) and it is what Claude Code loads automatically after step 2.
32
+
33
+ Everything below is for the person; it is accurate for you too.
34
+
22
35
  ## Install
23
36
 
24
37
  Needs Node 20+. ffmpeg ships with the package; Chromium is downloaded once.
@@ -48,12 +61,15 @@ outputs/<name>/
48
61
  demo.mp4 the shareable — captions burned in
49
62
  master.mp4 the CRF-14 keeper (post presets only)
50
63
  stills/ one PNG per scene, mid-scene and at its last moment
51
- thumbnail.png the frame at a chosen scene
64
+ thumbnail.png the poster — pick any frame, a cover, or your own image in the window
65
+ cover.png launch mode: the title card, settled
66
+ cover-titled.png launch mode: a real frame with the title over it
52
67
  proof-log.md result, shot list, every step's timing, pass/fail, what was on screen when it failed
68
+ contact.png launch cuts: one timestamped grid of the whole video (`retake contact`)
53
69
  take.json the raw timeline the renderer reads
54
70
  ```
55
71
 
56
- In the window: play it, switch speed (0.75× to 1.3×, re-rendered in seconds), download, or open its folder — stills, proof log, manifest. Demos group by the app they point at; archive what you're done with.
72
+ In the window: play it, switch speed (0.75× to 1.3×, re-rendered in seconds), download, or open its folder — stills, proof log, manifest. **The poster is chosen, not guessed**: scrub to a frame and press *Use this frame*, click any scene still, take one of the generated covers, or upload your own. It rewrites `thumbnail.png`, so it costs no re-render — and on a feed the poster decides whether anyone presses play. Demos group by the app they point at; archive what you're done with.
57
73
 
58
74
  ## The loop
59
75
 
@@ -82,7 +98,7 @@ preset: post-landscape # `retake presets` — 1920×1080 page + capti
82
98
  viewport: { width: 1440, height: 1080 }
83
99
  scale: 1.8 # page drawn at 1.8× so text stays crisp
84
100
  reducedMotion: true
85
- camera: auto # each scene eases toward the last thing touched
101
+ camera: static # still (the default); `auto` eases toward the last thing touched
86
102
 
87
103
  seed: [] # put the app in a known state first
88
104
  setup: [] # runs before the camera; trimmed off the video
@@ -103,9 +119,13 @@ outputs:
103
119
 
104
120
  The full template is [`demos/example.yaml`](demos/example.yaml); `retake init` copies it into a new workspace.
105
121
 
106
- **Steps** — `click`, `type`, `fill`, `hover`, `drag`, `scroll`, `keyboard`, `navigate`, `wait`, `waitFor`, `evaluate`, `upload`, `download`, `screenshot`, `scene`. Every step takes `pauseAfter`, `waitFor`, `timeout`, `secret`. Targets are a selector, or a point — `{x, y}` or `{selector, dx, dy}` — for canvases and SVG editors. `${VAR}` expands from `.env` and fails loudly when unset.
122
+ **Steps** — `click`, `type`, `fill`, `select`, `hover`, `drag`, `scroll`, `keyboard`, `navigate`, `wait`, `waitFor`, `evaluate`, `upload`, `download`, `screenshot`, `scene`. Every step takes `pauseAfter`, `waitFor`, `timeout`, `secret`. Targets are a selector, or a point — `{x, y}` or `{selector, dx, dy}` — for canvases and SVG editors. `${VAR}` expands from `.env` and fails loudly when unset.
123
+
124
+ **Waits** take three modifiers, because a bare selector resolves the moment the element exists: `gone: true` waits for it to leave (the previous action's banner), `minChars: 40` waits for it to hold real text (a streamed reply), `stableMs: 500` waits for its subtree to stop changing.
125
+
126
+ **Scenes** are the spine: `{ action: scene, label, caption }` at each beat. They take their timestamps from the actual run, so captions land on the frames they describe even when the app took 3 seconds this time and 11 the last. **Labels must be unique** — `thumbnail`, `until`, the stills and the proof log all address scenes by label, so two scenes called the same thing make every one of those ambiguous. `validate` rejects it.
107
127
 
108
- **Scenes** are the spine: `{ action: scene, label, caption }` at each beat. They take their timestamps from the actual run, so captions land on the frames they describe even when the app took 3 seconds this time and 11 the last.
128
+ **Two kinds of take.** `mode: demo` (the default) proves how something works — a client walkthrough, a PR clip, a lesson — and keeps everything but the product out of the frame. `mode: launch` presents the product in public and may carry a title card, music, emphasis and a branded ending. `validate` warns if a demo is carrying launch furniture. **The launch cut.** `compressIdle: true` shows the app's long waits as ~1.5 s each (your own pacing `wait`s are never touched); `voiceover: true` reads the captions aloud (edge-tts, `pipx install edge-tts` once); `typing: brisk` makes typed text land fast with the pauses carrying the meaning. All render-time except typing. **Music.** `music: track.mp3` (or `{ file, gainDb, fadeOutMs }`) mixes a bed under the video at render time, looped or trimmed to fit and faded out at the end. Bring your own track — it ships inside your video, so it must be one you may use (CC0 or licensed). **Cards and callouts.** `intro:`/`outro:` add a title card in Retake's own look — rendered at render time, so changing a word is a re-render, and the intro's settled frame is written as `cover.png`, the poster. `{ action: callout, selector, label, ms }` draws an animated ring and label around an element while the take holds — recorded coordinates, drawn at render, moving with the camera. Sparingly: the captions carry the story.
109
129
 
110
130
  **Seeds** put the app in a known state before the camera: write a JSON file, run JS in the page, or run a shell command. **Setup** is the stuff that runs before recording and is trimmed off the front — logins live there. **`tempo`** speeds the finished video up or down at render time; **`lock`** names a shared resource so two demos that touch the same backend don't record over each other.
111
131
 
@@ -115,33 +135,54 @@ Think in the publishing format, not the browser window.
115
135
 
116
136
  | Preset | Video | Page area | For |
117
137
  |---|---:|---:|---|
118
- | `draft` | 960×616 | 960×540 | iterating — the finals' layout at a quarter of the pixels, fastest |
119
- | `preview-fast` | 1920×1230 | 1920×1080 | checking timing and framing, hardware-encoded |
120
- | `post-landscape` *(default)* | 1920×1230 | 1920×1080 | the general-purpose demo, plus a CRF-14 master |
121
- | `post-square` | 1080×1080 | 1080×950 | feeds — a true square |
122
- | `post-vertical` | 1080×1920 | 1080×1750 | shorts and reels — a true 9:16 |
123
- | `docs-gif` | 1440×1000 | 1440×900 | README and docs GIFs |
124
- | `master` | 1920×1230 | 1920×1080 | archive, CRF 12 |
138
+ | `draft` | 960×540 | 960×464 | iterating — the finals' layout at a quarter of the pixels, fastest |
139
+ | `preview-fast` | 1920×1080 | 1920×930 | checking timing and framing, hardware-encoded |
140
+ | `post-landscape` *(default)* | 1920×1080 | 1920×930 | the general-purpose demo, plus a CRF-14 master |
141
+ | `post-square` | 1080×1080 | 1080×960 | feeds |
142
+ | `post-vertical` | 1080×1920 | 1080×1800 | shorts and reels |
143
+ | `docs-gif` | 1440×900 | 1440×800 | README and docs GIFs |
144
+ | `master` | 1920×1080 | 1920×930 | archive, CRF 12 |
125
145
 
126
- **The video is the page area plus the caption band.** Landscape keeps a full 1920×1080 page and the band goes under it (1920×1230) — the captions never cover the app. Square and vertical fit the band inside the canvas, because a feed that wants 1:1 or 9:16 will letterbox anything else. A manifest's `viewport` replaces the page area and the video follows it, so a 1240×1080 viewport gives a 1240×1230 video. `retake presets` prints this table, and `retake validate` tells you the exact size of the video a manifest will produce before you record it.
146
+ **One preset, one size — and size is not recorded.** The caption band sits inside the canvas, so `post-landscape` is always exactly 1920×1080 and two demos of the same app are never different shapes. Better still, the shape is decided at render: the take is fitted into whichever canvas you ask for, so `retake render outputs/x --preset post-vertical` gives you a 9:16 cut of a demo you recorded landscape — seconds, no browser, no re-record. Setting `viewport` in a manifest overrides the page and `validate` warns, because that is how one demo ends up a different shape from the rest.
127
147
 
128
- **Page scale** is what makes it crisp: the page lays out as if the viewport were smaller and every glyph is drawn at 2×, so 1080p text reads on a phone. **Camera** eases toward whatever the demo just touched, at render time, clamped so it can never crop the thing it points at — per scene: `camera: static` or `camera: { focus: ".result", zoom: 1.3 }`. Captions, camera, speed, trim, format and layout are all render-time: change them and `retake render` takes seconds, never touches the browser.
148
+ **Page scale** is what makes it crisp: the page lays out as if the viewport were smaller and every glyph is drawn at 2×, so 1080p text reads on a phone. **Camera** is still by default — a calm demo reads as real. `camera: auto` eases toward whatever the demo just touched, at render time, clamped so it can never crop the thing it points at; per scene: `camera: { focus: ".result", zoom: 1.3 }`. Zoom is something you ask for after seeing a take, not something that happens to you. Captions, camera, speed, trim, format and layout are all render-time: change them and `retake render` takes seconds, never touches the browser.
129
149
 
130
- ## Signing in
150
+ ## Behind a login
151
+
152
+ Most real demos start on the far side of a sign-in. You never put a password anywhere an agent can read it.
153
+
154
+ **With the window open** (`retake ui`): when the agent sees the app needs a login, a small form appears in the window asking for a demo account by name — `APP_USER`, `APP_PASSWORD`. You type them; they are written to one local file, the workspace `.env` (readable by you only), and never sent to the agent, to any server, or to anyone. The agent learns only that the names are set.
155
+
156
+ **Without the window:** the agent relays one line — run this in your workspace and type the values (hidden):
157
+
158
+ ```bash
159
+ retake secret APP_USER APP_PASSWORD
160
+ ```
161
+
162
+ **Then:** the login runs *before the camera* and is trimmed off the front; the video starts signed in. The session is saved to `.auth/`, so later takes skip the login. A wrong password stops the take instead of recording a logged-out one.
163
+
164
+ **Two-factor.** Authenticator apps: add the enrolment secret as `APP_TOTP_SECRET` and the manifest uses `${TOTP:APP_TOTP_SECRET}` — Retake computes the current code when it fills the field. SMS codes, SSO, captchas — anything only a person can do:
165
+
166
+ ```bash
167
+ retake signin demos/<name>.yaml
168
+ ```
169
+
170
+ opens a real browser; you log in by hand, press Enter, and Retake keeps the session (not the password) for every later take.
171
+
172
+ What the agent writes:
131
173
 
132
174
  ```yaml
133
175
  auth:
134
176
  storageState: .auth/myapp.json # session saved here, reused until stale
135
177
  maxAgeHours: 8
136
- setup:
137
- - { action: fill, selector: "#user", text: "${APP_USER}" }
138
- - { action: fill, selector: "#password", text: "${APP_PASSWORD}", secret: true }
139
- - { action: click, selector: "button[type=submit]" }
178
+ setup:
179
+ - { action: fill, selector: "#user", text: "${APP_USER}" }
180
+ - { action: fill, selector: "#password", text: "${APP_PASSWORD}", secret: true }
181
+ - { action: fill, selector: "#code", text: "${TOTP:APP_TOTP_SECRET}", secret: true } # only if asked
182
+ - { action: click, selector: "button[type=submit]" }
140
183
  ```
141
184
 
142
- Secrets live in `.env`, never in a manifest. `secret: true` keeps a value out of the terminal, the window and the proof log — but not out of the video, which is why `validate` warns when a secret step is in `steps` rather than `setup`. A session is only saved when the sign-in actually succeeded. Use a demo account: the output is a video you may publish.
143
-
144
- Apps that keep their session in IndexedDB (Firebase) can't be restored from `storageState`; put the login in plain `setup` instead.
185
+ `secret: true` keeps a value out of the terminal, the window and the proof log — but not out of the video, which is why `validate` warns when a secret step is in `steps` rather than `auth.setup`. Use a demo account: the output is a video you may publish. Apps that keep their session in IndexedDB (Firebase) can't be restored from a saved session; put the login under plain `setup` instead.
145
186
 
146
187
  ## Limits worth knowing
147
188
 
@@ -0,0 +1,55 @@
1
+ /** Word-wrap long captions; two-line captions break near the middle so no
2
+ word is left orphaned on the second line. */
3
+ export function wrap(text, max) {
4
+ if (text.length <= max)
5
+ return text;
6
+ const words = text.split(/\s+/);
7
+ if (text.length <= max * 2) {
8
+ let best = 1, bestDiff = Infinity;
9
+ for (let i = 1; i < words.length; i++) {
10
+ const a = words.slice(0, i).join(" ").length;
11
+ const b = words.slice(i).join(" ").length;
12
+ if (a <= max && b <= max && Math.abs(a - b) < bestDiff) {
13
+ best = i;
14
+ bestDiff = Math.abs(a - b);
15
+ }
16
+ }
17
+ return words.slice(0, best).join(" ") + "\n" + words.slice(best).join(" ");
18
+ }
19
+ const out = [];
20
+ let line = "";
21
+ for (const w of words) {
22
+ if (line && (line + " " + w).length > max) {
23
+ out.push(line);
24
+ line = w;
25
+ }
26
+ else
27
+ line = line ? line + " " + w : w;
28
+ }
29
+ if (line)
30
+ out.push(line);
31
+ return out.join("\n");
32
+ }
33
+ /** Characters per caption line at this width and font size. */
34
+ export const maxCharsFor = (width, fontSize) => Math.max(28, Math.round((width / fontSize) * 1.55));
35
+ /** How many lines the longest caption needs. */
36
+ export function captionLines(texts, width, fontSize) {
37
+ const max = maxCharsFor(width, fontSize);
38
+ return Math.max(1, ...texts.map((t) => wrap(t, max).split("\n").length));
39
+ }
40
+ /** The band's height for this take: fitted to the text, or the preset's
41
+ fixed band when the canvas is the constraint. 0 when captions are off. */
42
+ export function bandHeightFor(q, captionTexts) {
43
+ if (q.layout !== "band" && q.layout !== "card")
44
+ return 0;
45
+ if (!q.captions)
46
+ return 0;
47
+ // A band with nothing to draw in it is an empty strip on the video: when
48
+ // the texts are known and none exist, the page gets the whole frame.
49
+ if (captionTexts && !captionTexts.some((t) => t && t.trim()))
50
+ return 0;
51
+ // Fixed per preset. The default page is the canvas minus this, so the
52
+ // finished video is exactly the preset's size — every take the same shape,
53
+ // and no player letterboxing it.
54
+ return q.bandHeight;
55
+ }