Skip to content

Interactive UI

cappa review serves more than a report. The same UI can drive the browser: pick what to capture, start a run, and watch it happen — without leaving the page to run cappa capture in another terminal.

Terminal window
cappa review

Open the URL it prints and choose Capture in the sidebar.

The capture panel lists every task your plugins discovered. From there you can:

  • Capture everything — the same work cappa capture does.
  • Capture one plugin — tick a plugin to run only its tasks.
  • Capture specific screenshots — filter by name and tick the ones you want.

A selection of specific screenshots leaves the rest of your results alone, so you can iterate on one component without re-capturing the whole suite. A full run still clears actual/ and diff/ first, exactly as the CLI does.

While the run is going you get live progress, a row per task moving from pending through running to its outcome, a count by status, and the run’s log output. Cancel stops it: tasks already in flight finish, but nothing new starts.

When the run finishes, the screenshot lists refresh themselves — the Changed, New and Passed tabs reflect what was just captured with no reload.

Every screenshot’s detail page has a Re-capture button. It runs that one task and leaves every other result untouched, which makes the tighten-a-diff loop short: change your component, re-capture, look at the diff, approve.

The button needs to know which capture task produced the screenshot, and that is not something a filename can tell it — @cappa/plugin-storybook writes example/button/primary.png for the task example-button--primary. Cappa records the link as it captures, in a .cappa-manifest.json beside actual/ and expected/. So the button appears once a screenshot has been captured at least once by this version of cappa, and is hidden for anything older until the next run. The file is small, safe to commit and safe to delete — deleting it only hides the button until the next capture.

The capture page reads the run in flight from the server rather than remembering its own, so reloading mid-capture reattaches to the same run — progress, task table and log intact — and a cappa capture running in another terminal shows up here too, with the capture controls disabled until it finishes.

The server captures one run at a time — the browser pool is the real constraint, and a second concurrent run would only fight it. Starting a run while one is active is rejected rather than queued, and the UI disables the button while a run is in flight.

Between captures the browser is kept alive so an interactive run does not pay browser startup every time. It shuts down once it has been idle for a while, so a forgotten review session does not hold a browser process indefinitely.

Contexts are recycled before every run, so a capture started from the UI is taken against the same clean state the CLI would use.

Tune the idle window with review.browserIdleTimeout.

An active watch session suspends that timer entirely: a session left idle over a coffee break would otherwise pay a full browser start-up on the next save, which is the cost watch mode exists to avoid. The timer resumes when watching stops.

Turn on Watch files in the capture panel and every save re-captures what it affects. The run streams into the same run view a capture you started by hand does, and the run header says which file triggered it.

The session lives on the server, not in the tab, which is what makes it survive a reload — and what makes a second tab, or a cappa capture --watch in a terminal, show the same state rather than disagreeing with it. The status line under the toggle says what the last change re-captured:

src/components/Button.stories.tsx changed — re-capturing 2 tasks

How much gets re-captured depends on what the plugins can tell Cappa about the file you saved — precise for a story file with @cappa/plugin-storybook, a whole-plugin re-run for a component it cannot attribute. Watch mode covers the rules, and Custom Plugins covers teaching your own plugin the mapping.

Two things watch mode deliberately does not do: it never clears actual/ between iterations, so the diffs you are looking at for screenshots you did not touch stay where they are; and it never approves anything. Watch captures — a human still approves.

The toggle is hidden when the server cannot watch: a read-only server refuses every mutation, and an engine with no local filesystem has nothing to watch.

Capture drives a real browser and writes to disk. When you only want to share a report — CI artifacts, for instance — start the server read-only:

Terminal window
cappa review --read-only

The capture page, the sidebar entry and the re-capture buttons all disappear, and the server refuses capture, approval and every other mutation.

By default the server binds to 127.0.0.1, reachable only from your own machine. Binding anywhere else exposes control of a browser that reads local files, so a token is required:

Terminal window
cappa review --host 0.0.0.0

Cappa generates one and prints it as part of the URL. Pass your own with --token <value> if you would rather choose it. Every /api/* request needs it — the whole UI, not just the capture page.

The URL is printed with localhost as its host even when you bound to 0.0.0.0, because a wildcard is an address to listen on and not one a browser can open. Replace it with the machine’s own hostname when opening the UI from somewhere else.

Open the URL the server printed and the UI takes care of the token from there. It keeps it for the tab, so reloading or navigating still works once the token has left the address bar — and it removes it from the address bar as soon as it has been read, to keep it out of bookmarks, screenshots and the referer header. A new tab needs the printed URL again: the token is not stored beyond the tab it was opened in — open one without the token and the UI says so rather than sitting on an empty screen.

A request can only capture tasks that discovery produced — the UI selects from that list and never supplies a URL of its own — so a client cannot point the browser somewhere arbitrary.

Flag Description
--port <port> Port to listen on. Defaults to review.port.
--host <host> Host to bind. Defaults to 127.0.0.1; anything else requires a token.
--read-only Serve the report without capture, approval or any other mutation.
--token <token> Require this token on every API request.