---
name: pup-screenshots-recording
user-invocable: true
description: "Capturing pup (Puppeteer bridge) windows: SCREENSHOTS (pup_screenshot — lossless PNG, full-page vs viewport, full-resolution via pup_screenshot_full_res, and how to actually READ the image) and RECORDING (pup_record_start/stop records ONE pup tab/window's page content to a video; desktop_record_start/stop records the WHOLE desktop across apps). Covers framing a window at an exact size for a clean shot WITHOUT foregrounding it, and why pup shots are true-to-life (real rendered window, not headless). Read when screenshotting a page, capturing full-page vs above-the-fold, recording a demo/walkthrough of an app, or choosing the window-recorder vs the desktop-recorder. Trigger words: pup_screenshot, pup_screenshot_full_res, full page screenshot, screenshot the page, read the screenshot, pup_record_start, pup_record_stop, desktop_record_start, desktop_record_stop, record my app, record a demo, screen recording, walkthrough video, pup screenshot, pup recording."
---

**Parent skill:** pup-bridge  ·  See also: `pup` (start here), `pup-windows-sessions-tabs`, `pup-browsers-and-chrome`.

# pup — screenshots & recording

pup windows are **real, GPU-rendered windows** (not headless), so screenshots and recordings are
true-to-life — fonts, canvas, WebGL, video, and CSS all render exactly as a user would see them. And it
all works while the window is in the **background** (see `pup-windows-sessions-tabs`), so you capture
without disturbing the user.

## Screenshots

```bash
adom-bridge-cli pup_screenshot '{"sessionId":"mytask-web"}'                 # viewport, lossless PNG
adom-bridge-cli pup_screenshot '{"sessionId":"mytask-web","fullPage":true}' # entire scrollable page
adom-bridge-cli pup_screenshot '{"sessionId":"mytask-web","tabId":"tab-2"}' # a specific tab
```

- Returns a **PNG saved on the container** (the machine running the CLI) — the response gives you the path.
  **`Read` that path** to actually see it; a returned path is not the same as having looked. Downscaled to
  stay under the image-read limit by default.
- **`fullPage:true`** stitches the whole scrollable page; omit for just the viewport (above the fold).
- **`pup_screenshot_full_res`** captures at full native resolution (no downscale) when you need to read
  fine detail — larger file; use when the default shrink loses text you need.
- **Framing for a clean shot without showing the user:** size/position the window on open —
  `pup_open_window {width,height,x,y}` — which places it **without foregrounding** (background stays
  background). Great for a deterministic, repeatable capture frame. To capture the OS window *plus* any
  owned popups/dialogs on top, use AD's `desktop_screenshot_window` instead (it returns the window with its
  child popups).
- **Sanity-check, don't assume:** a page that 404'd or hit a login wall still screenshots — pair with
  `pup_eval` on `document.title` when in doubt.
- **Rendering your OWN HTML to screenshot it?** Use **`pup_render_html {sessionId, html}`** — it renders
  the string via `page.setContent`, which is **UTF-8-correct**. (A `data:text/html,…` URL mojibakes accented
  characters — `Café` → `CafÃ©` — unless you hand-add `;charset=utf-8`; `render_html` just does it right.)
  Then screenshot/drive it like any page. Great for previewing generated markup without writing a temp file.

## Recording — two recorders, pick by what you're capturing

| Recorder | Captures | Use for |
|---|---|---|
| **`pup_record_start` / `pup_record_stop`** | ONE pup tab/window — the page content | "record my app", a feature walkthrough, a focused UI flow |
| **`desktop_record_start` / `desktop_record_stop`** | the WHOLE desktop (multiple apps, KiCad/Fusion, OS dialogs) | anything spanning more than the pup window |

```bash
# Recording a pup window the user will watch → DO bring it forward first
adom-bridge-cli pup_raise_os_window '{"sessionId":"demo-web"}'
adom-bridge-cli pup_record_start    '{"sessionId":"demo-web"}'
# ...drive the scene: navigate, click, type, scroll...
adom-bridge-cli pup_record_stop     '{"sessionId":"demo-web"}'   # → { path: "...webm" }
adom-bridge-cli pup_lower_os_window '{"sessionId":"demo-web"}'   # tuck it away again when done
```

- `pup_record_stop` returns the saved video **path**. Recording status/list verbs:
  `pup_record_status` / `pup_record_list` (window recorder) and `desktop_record_status` /
  `desktop_record_list` / `desktop_recorder_open` / `desktop_recorder_close` (desktop recorder).
- Closing a tab/window mid-recording auto-stops that recording.
- For a **narrated** walkthrough, drive the scene here and hand the video to the `video-post` / `adom-tts`
  flow for voiceover — pup just produces the raw capture.
- A pup window can record from the **background** too, but for a demo the user will actually watch, raise it
  first (that's an explicit "show me" case where `foreground` is warranted).

## Driving & verifying tricky pages (shadow DOM, nested scroll) — use the first-class verbs

Component-heavy pages (an app, a wiki widget) break the naive path: `document.querySelector` misses shadow
content, `window.scrollTo` no-ops when the page scrolls inside a nested `<div>`, `fullPage` returns only the
viewport, and a sized window can even come up ~170px wide. As of **v1.8.21 pup has first-class verbs for
all of it** — reach for these instead of hand-rolling evals:

- **`pup_screenshot_element { sessionId, selector? | text?, closest?, padding?, deep? }`** — screenshot
  ONE element. Resolve by CSS **selector** (most reliable) or visible **text** (pierces shadow DOM), scrolled
  into view first. **This is usually the whole answer to "capture just this card."** Text matches the
  *tightest* text node — to grab the enclosing card add `closest:".card"`; and if the phrase is split across
  spans or appears in prose too, use a **selector** (e.g. `.download-card`).
- **`pup_scroll { sessionId, dy?/dx? | toSelector?/toText?, block?, behavior? }`** — scrolls the ACTUAL
  scrolling element (nearest scrollable ancestor, not `window`). Scroll by delta or to an element/text
  (shadow-piercing). Returns `{ scrollElement, scrollTop, scrollHeight, clientHeight, atBottom }` so you can
  confirm it landed — no more overshoot from hand-computed offsets.
- **`pup_set_viewport { sessionId, width, height, deviceScaleFactor? }`** — pins the viewport
  deterministically (fixes a too-small innerWidth). `pup_open_window { width, height }` now applies it on
  open, so a sized window renders at the size you asked for.
- **`$deep(sel)` / `$$deep(sel)` / `deepText(txt)`** are auto-injected into every page, so `pup_eval` can
  pierce shadow roots directly — no hand-rolled walk:

```bash
adom-bridge-cli pup_eval '{"sessionId":"...","expr":"$$deep(\"a[href]\").length"}'   # counts across shadow roots
```

**Still verify by DOM, then by eye.** To count cards/links/rows, count in `pup_eval` by document-Y
(`rect.top + window.scrollY`) — immune to an overshot screenshot — then `pup_screenshot_element` (or
`pup_scroll` + `pup_screenshot`) to confirm. And **first check whether a page is even shadow DOM**
before assuming: a shadow survey (`$$deep` finding more than `querySelectorAll`) tells you; often an "empty"
result just means a wrong selector on plain DOM.

**fullPage caveat:** `pup_screenshot { fullPage:true }` only covers *document*-level scroll. On a
nested-scroll SPA it returns just the viewport and a `nestedScroll:true` hint — use `pup_screenshot_element`
for one region, or `pup_scroll` through the container.

Reminder: `pup_eval` allows **no semicolons** — use the comma operator or an IIFE. If a
screenshot lands on the wrong content, don't trust it — it almost always means an inner scroll container;
re-scroll the element, don't nudge the window.
