---
title: PR Visual Evidence
---

# PR Visual Evidence — before/after UI screenshots + recordings on a coding-agent's PR

## What it is

**Shipped** (2026-08-20) — on for everyone, but nothing happens until a repo opts in (per-repo, off by default). Phase 1 shipped before/after screenshots; **Phase 2 adds a short before/after screen recording (video)** of the changed flow (a capture may be screenshot-only, video-only, or both); **Phase 3 adds a PURE-PLATFORM fast path** that captures the before/after WITHOUT the agent driving the browser.

When a coding-agent session finishes a change that affects the **UI**, PR Visual Evidence captures
a **before/after screenshot** of the changed screen — or a short **before/after screen recording
(video)** of the changed flow — and attaches it where the change is reviewed: your **private Omniscio
session** by default, and the **public GitHub PR** only when the repo explicitly opts into that a
second time.

It's the automated version of what a careful engineer does by hand: show what the screen (or the
flow) looked like before the change and after it, so a reviewer can see the visual diff at a glance.
Screenshots suit a static change; a recording suits a flow / interaction / animation change.

## Where to find it

Every install ships the feature, but nothing happens until a repository opts in. The switches are
per repository, in that project's own settings under **Edit Project → More options** — the same
place where you set the preview command and the separate opt-in for public PR posting.

## How it behaves

### The opt-in (the privacy model)

The feature ships **on for everyone**, but it does nothing until a repo opts in — and posting to a
public PR needs a **separate** switch. In **Edit Project → More options**, three fields control it:

- **Attach before/after PR screenshots** — the per-repo opt-in (off by default). Off ⇒ the feature is
  inert for that repo, and no browser-capture tools are loaded for its sessions.
- **Preview command** — how to launch that repo's dev server so the screen can be captured (e.g.
  `npm run dev`). Leave blank to skip the "before" shot.
- **Also post to the public GitHub PR** — the SECOND opt-in (off by default). Off ⇒ the before/after
  stays only in your private session; on ⇒ it is ALSO posted as one comment on the branch's open PR.

So the default for an opted-in repo is **private**: the screenshots land in the session, never on a
public URL, until the repo owner deliberately turns on public posting.

### How it works

- The bundled **`pr-visual-evidence` skill** runs at the end of a UI-affecting session. It checks the
  preconditions (feature + repo opt-in, a web UI, an actual UI change, **synthetic/test data only**),
  then drives Omniscio's embedded browser to screenshot the changed screen.
- The **"before" is real** — the platform resolves the change's merge-base, checks out the _pre-change_
  version in a throwaway worktree, launches its dev server with the repo's preview command, screenshots
  it, then tears the throwaway down (no leaked worktree or process).
- The agent confirms the two shots actually **differ** (else it skips — no misleading identical pair),
  and hands the paths to the platform, which attaches them privately and — only on the second opt-in —
  hosts the screenshots on **GitHub user-attachments** (an access-controlled `github.com/user-attachments/…`
  URL that follows repo membership — private repo → only repo viewers, never a public capability link)
  and posts one PR comment embedding both images. If that upload fails, the PR image is skipped (still
  saved privately) and it never falls back to a public link.
- **Video (Phase 2)** — for a flow change the skill records the SAME short flow on each version: the
  platform records the active browser tab (frame snapshots → mp4 via the bundled ffmpeg, capped ~20s /
  ≤1280px), attaches the mp4 privately, and — on the second opt-in — hosts it on **GitHub
  user-attachments** (the same access-controlled seam as the screenshots; nothing here is ever on a
  public link) and adds a "▶ Watch the before/after recording" link to the same PR comment.
- **Best-effort throughout**: a missing preview, a build failure, no PR for the branch, a missing
  ffmpeg, or any capture/record error just means the visual is skipped and logged — it NEVER blocks,
  delays, or fails the actual code change.

### Pure-platform capture (the fast path — no agent driving the browser)

The skill now asks the platform to do the whole thing FIRST, via `POST /pr-visual-evidence/auto-capture
{ sessionId }`. For the common case — a web app whose **home screen** shows the change — the platform
captures the before/after entirely server-side and the agent never touches the browser:

- It confirms the change is UI-affecting (from the diff, **including uncommitted edits**), boots the
  pristine "before" preview (merge-base) and an "after" preview from the session's **live** worktree,
  waits for each to load, screenshots the home screen of both using the same offscreen browser engine
  the agent flow uses, confirms they differ, attaches the pair **privately**, and tears both previews down.
- **Private-only.** Pure-platform capture NEVER posts to a public PR — it can't personally assert the
  screen shows synthetic data the way the agent flow does, so public posting stays on the agent path
  (gated on the repo's `postToPublicPr` opt-in).
- **Degrades to the agent.** When the platform can't get a clean shot it returns a `reason`
  (`no-ui-change`, `no-base-preview`, `no-worktree`, `preview-not-ready`, `not-web-ui`,
  `no-visual-change`, `capture-failed`) and the skill falls back to the agent-driven flow above — the
  agent knows the login/seed/route the platform doesn't.
- **Safety.** The "after" preview runs against the session's real worktree, so its teardown only stops
  the dev server — it NEVER removes the worktree (the session's uncommitted work is never touched).
- **Scope (proof-slice):** the home route `/` only; Omniscio's own renderer is out (a plain browser tab
  can't load its Electron bridge). Diff→route inference for deeper screens is a later layer.

### What the platform enforces (so the agent can't get it wrong)

- **Private-always, PR-post-only-on-opt-in.** The publish path always attaches to the session first; it
  only posts to the PR when the repo's `postToPublicPr` is on AND a PR is known for the session — and the
  screenshots AND recordings it embeds host on GitHub user-attachments (access-controlled, never a
  public capability URL).
- **Ownership.** The public PR is auto-detected from the session's own branch on the project's own
  origin repo (`gh pr list --head <branch> --repo <origin>`), and the repo is validated server-side — a
  session can never post to some other repo's PR.
- **Idempotent.** A re-run for the same session finds its hidden marker in the PR's comments and skips
  the duplicate.
- **Synthetic data only.** Capture runs against a running app, so the skill requires test/seed data and
  skips when it can't guarantee it — an "after" shot or clip could otherwise reach a public PR. A VIDEO
  records a whole flow, so EVERY screen in it must be synthetic, not just the last.
- **Video hosting respects GitHub's own caps.** A clip hosts on the same access-controlled
  user-attachments URL as screenshots; a clip over GitHub's per-file plan cap (10 MB on free-plan
  repos, 100 MB on paid) or any failed upload is simply skipped from the PR comment while the
  private attachment still stands — never a public fallback. No new dependency — encoding reuses
  the bundled ffmpeg.

## For agents

### Turning it on (CLI)

The feature is shipped (on for everyone) — no global toggle to flip. Opt a repo in per-project:

- `PATCH /project/:id { "prVisualEvidence": true, "previewCommand": "npm run dev", "postToPublicPr": false }`
  (or Edit Project → More options).

### Where it lives

- Feature gate + settings: `src/shared/unreleased-features.ts` (`pr-visual-evidence`, `status: 'shipped'`;
  `settingKey`/`envVar` retained as gate helpers) + the per-project `prVisualEvidence` / `previewCommand` /
  `postToPublicPr` fields (`src/shared/types/project.ts`, migration `20260819170759`).
- Backend service: `src/main/services/pr-visual-evidence/` (`provision-base.ts`, `publish.ts`,
  `record.ts` — the video recorder, `session-pr-gh.ts`, `auto-capture.ts` (pure-platform orchestrator),
  `capture.ts` (browser-readiness + screenshot + perceptual differ), `index.ts`). CLI routes:
  `POST /pr-visual-evidence/provision-base`, `/record/start`, `/record/stop`, `/publish`, `/auto-capture`.
- Capture engine: the embedded ai-browser (`resolveAgentWebContents` + `screenshotPng` for stills and the
  recorder's frames; `runAiBrowserTool` for the pure-platform path's offscreen `WebContents.capturePage()`),
  composed PER-REPO via `isBrowserCaptureVisibleForProject` (only opted-in repos' sessions; the route
  404-gate stays global on `isBrowserCaptureVisible`, not un-retired for general use). Video encode reuses
  `runFfmpeg`; hosting of screenshots AND recordings goes through the one GitHub user-attachments uploader
  (`github-attachment.ts`). The pure-platform
  path drives the browser from main; the agent path drives it via MCP.
- UI-change heuristic: `src/shared/ui-affecting-diff.ts`. Agent flow:
  `.claude/skills/pr-visual-evidence/SKILL.md`.
- Full invariants + route contracts: `.claude/memory/contracts/pr-visual-evidence-contract.md`.

## Related

[pull-requests.md](pull-requests.md) covers the PR lifecycle the comment above lands in.
[web-app-verification.md](web-app-verification.md) covers verifying a running web app from a
session — the same browser machinery this capture path drives. And
[screenshot-snip-tool.md](screenshot-snip-tool.md) is the manual, in-app way to grab a screen when
you want a picture by hand rather than an automatic before/after pair.
