Skip to content

feat(browse): add record command for video evidence of interactive bugs - #2497

Open
Topherhindman wants to merge 4 commits into
garrytan:mainfrom
Topherhindman:feat/browse-record-video
Open

feat(browse): add record command for video evidence of interactive bugs#2497
Topherhindman wants to merge 4 commits into
garrytan:mainfrom
Topherhindman:feat/browse-record-video

Conversation

@Topherhindman

Copy link
Copy Markdown

Why

A screenshot proves what a page looked like. It cannot show what a page did. The bugs that most need evidence are the ones in the timing — a double-submit that fires before React re-renders, a loading flicker, focus jumping, a drag that lands wrong — and those are exactly the ones a still frame loses.

browse already owns a Chromium and Playwright already records video at the context level, so the primitive is a few lines from free. This adds it behind $B so any skill (and any agent driving browse directly) can attach a repro clip instead of describing one.

What

$B record start [dir] [--size WxH]    # rebuild the context with recordVideo on
$B record stop                         # flush, then list the .webm files it produced
$B record status                       # what's recording, if anything

Headless only, control scope, defaults to a timestamped directory under TEMP_DIR.

Design notes

Context lifecycle. Recording is a BrowserContext option, so start and stop each go through recreateContext(), which already preserves cookies, storage, and open tabs. The first commit is a prerequisite refactor: launch(), recreateContext(), and its clean-slate fallback each built their own BrowserContextOptions from the same fields, so any context-level setting had to be written three times or it silently failed to survive a rebuild. They now share one buildContextOptions(). That is a standalone improvement — the three copies had already drifted in shape — and it is what keeps recordVideo from becoming a fourth thing to remember in three places.

Reporting is the hard part, not the recording. Three ways this can quietly cost the caller evidence they believe they have, all handled:

  • recreateContext() reports a degraded rebuild by return value, not by throwing — and that rebuild is the flush. Dropping it prints "Recording stopped" over a list of files at the same moment the caller's tabs were reset. start and stop both carry it out.
  • stop reads the recording directory, not the live pages. A tab closed mid-recording still produced a video; enumerating open pages misses it. Names present before the recording started are excluded, so reusing a directory across takes does not re-report the earlier one.
  • Starting over an active recording reports the take it superseded. That video is already on disk; a caller who is not handed the path has lost it.

Headed mode. handoff/connect leave the manager headed with the recording flag still set, and recreateContext() refuses to run there. The videos have already flushed by that point, so stop skips the rebuild and hands them back rather than raising.

Scope. record sits in SCOPE_CONTROL, not meta: it rebuilds the context out from under every open tab (same blast radius as state) and writes whatever is on screen — including anything typed into a login form — to disk. Read-only and write-scoped tokens cannot record; there is a test pinning that.

Tests

browse/test/record.test.ts — 20 tests, ~12s, real browser against the local test server.

Live capture asserts EBML magic bytes rather than "a file exists", --size asserts the frame dimensions parsed out of the WebM header (no ffprobe dependency), and there is coverage for the closed-tab case, cookie survival across both rebuilds, the superseded take, a degraded flush, an unreadable directory, the headed path, directory reuse, path policy, and every argument error.

Two of these are mutation-checked — dropping the size option from the context options, or discarding the flush warning, each turns the suite red.

Teardown await bm.close()s the browser instead of calling process.exit, so running this file alongside the rest of the suite cannot truncate it.

Verification

  • bun test — 19 failures, byte-identical to the count on a clean main checkout on this machine (all gstack-gbrain-*, check-careful, and a live codex CLI smoke; they need tooling I don't have installed). Zero failures in anything this branch touches.
  • bun run build clean; bun run gen:skill-docs --host all --dry-run reports no drift.
  • End-to-end against the built binary: record start → navigate → CSS transition → record stop produces a 43KB WebM, 800×450, valid EBML, and the frame extracted mid-clip shows the transition captured in progress.

Notes for the maintainer

  • NEW_IN_VERSION['record'] is set to 1.62.0.0 so the unknown-command hint works the moment this lands. Adjust if you cut a different version.
  • No VERSION bump or CHANGELOG entry — left for you to write in your voice.
  • Deliberately out of scope: no duration or size cap on a recording, and a recording still running when the daemon idles out leaves its .webm on disk without printing the path. Both are documented in the skill rather than guessed at; happy to add a cap if you want one.

This is a fresh implementation of the idea in #1483, written against the current context lifecycle. That PR is conflicted and its review points (hidden flush failures, videos from closed pages, the forced test-process exit) are addressed above with tests covering each. Closes #1483 if you prefer this one; credit to @itstimwhite for the original idea and for identifying recordVideo as the right primitive.

launch(), recreateContext(), and recreateContext()'s clean-slate
fallback each built their own BrowserContextOptions from the same
manager fields. Every context-level setting has to be repeated in all
three or it silently fails to survive a rebuild, and the three copies
had already drifted in shape.

Collapse them onto one builder so a new context option is added in one
place. No behavior change: the resulting options are identical.
A screenshot proves what a page looked like; it cannot show what a page
did. Timing bugs — double-submit, loading flicker, focus jumping, a drag
that lands wrong — are the ones a developer most needs to see happen,
and they are exactly the ones a still frame loses.

Playwright records at the context level and flushes each page's .webm on
context close, so start and stop both route through recreateContext(),
which already preserves cookies, storage, and open tabs. Refs do not
survive that rebuild, so stop says so rather than leaving the caller to
find out on the next click.

Three things the reporting has to get right, because each one silently
costs the caller evidence they think they have:

  - recreateContext() reports a degraded rebuild by return value, not by
    throwing, and that rebuild is the flush. Dropping it would print
    "Recording stopped" over a list of files while the caller's tabs had
    just been reset, so start and stop both carry it out.
  - stop reads the recording directory rather than the live pages. A tab
    closed mid-recording still produced a video; enumerating open pages
    would miss it. Files present before the recording began are excluded
    so a reused directory does not re-report an earlier take.
  - starting over an active recording reports the take it superseded.
    That video is already on disk, and a caller who is not handed its
    path has lost it.

handoff and connect leave the manager headed with the recording still
flagged, where recreateContext() refuses to run. The videos have already
flushed by then, so stop skips the rebuild there and hands them back
instead of raising.

record is control-scoped: it rebuilds the context out from under every
open tab, and it writes whatever is on screen — including anything typed
into a login form — to disk.
Covers when video beats a screenshot, that the context rebuild
invalidates refs, and the limits: headless-only, control scope, stop
before the daemon idles out, and a tab that never paints records
nothing. BROWSER.md gains the command rows and lists record with the
other context-recreation triggers.
Generated by bun run gen:skill-docs --host all.
@trunk-io

trunk-io Bot commented Aug 9, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant