main
Kyle Bergstedt Publish 3.0.0 259e308 13d ago

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 window through ab's native recorder, MP4, extension lane included; 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

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: one recorder, ab's, and pup's verbs ride it

Desktop recording belongs to ab, not pup (John, 2026-09-07), and since 2.0.582 so does window recording. pup_record_start {window} and pup_record_stop {window} delegate to ab's native recorder (desktop_record_window_start / _stop keyed on the pup window's handle) when this Mac's Adom Bridge offers it; otherwise pup records the page itself, which needs ffmpeg (brew install ffmpeg, re-detected without a restart). It works on the extension lane. Frames are emitted only when the window's content changes, so a static page yields few frames and a small file; that is by design, not a dropped take. The response carries filePath, bytes, frames and recordedMs; pull the file with adom-bridge pull_file {filePaths:[path]}.

Ask Verb Captures
"record my app", a UI walkthrough pup_record_start / pup_record_stop ONE pup window, whole window frame, MP4, no audio
"record my desktop / the demo with sound" ab desktop_record_start / desktop_record_stop (ab 2.1.36+) a whole monitor plus the system audio mix, MP4

The old Chrome screen-share recorder (pup_screen_record_* through the pup_recorder_open HUD) and the DevTools screencast are retired to fallbacks pup uses only where ab's recorder is absent; do not offer them. pup_record_status and pup_record_list report through ab too.

Say it the way the user will understand: "ab records your desktop natively (GPU accelerated); pup can do it Meet-style if that is not available." Never present desktop recording as pup's own feature.

# 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. Status and list verbs: pup_record_status / pup_record_list (the tab/window recorder), pup_screen_record_status / pup_screen_record_list (pup's Meet-style desktop fallback), desktop_record_status / desktop_record_list (ab's native recorder; see the adom-bridge-recording skill). On an ab older than 2.1.36 the desktop_record_* names were routed to pup's fallback; pup now answers those with wrong_bridge and the fallback names.

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:
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: since v2.0.274 pup_eval accepts statement lists (semicolons fine — a syntax error in the expression form retries with devtools-console semantics, and bad code fails honestly with eval_failed); a comma-operator/IIFE still gives the cleanest single-expression result. 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.