name: pup-screenshots-recording user-invocable: true description: "Capturing pup (Puppeteer bridge) windows: SCREENSHOTS (browser_screenshot — lossless PNG, full-page vs viewport, full-resolution via browser_screenshot_full_res, and how to actually READ the image) and RECORDING (browser_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: browser_screenshot, browser_screenshot_full_res, full page screenshot, screenshot the page, read the screenshot, browser_record_start, browser_record_stop, desktop_record_start, desktop_record_stop, record my app, record a demo, screen recording, walkthrough video, pup screenshot, pup recording."

Parent skill: adom-desktop-puppeteer-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-desktop browser_screenshot '{"sessionId":"mytask-web"}'                 # viewport, lossless PNG
adom-desktop browser_screenshot '{"sessionId":"mytask-web","fullPage":true}' # entire scrollable page
adom-desktop browser_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).
  • browser_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 — browser_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 browser_eval on document.title when in doubt.
  • Rendering your OWN HTML to screenshot it? Use browser_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
browser_record_start / browser_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
# Recording a pup window the user will watch → DO bring it forward first
adom-desktop browser_raise_os_window '{"sessionId":"demo-web"}'
adom-desktop browser_record_start    '{"sessionId":"demo-web"}'
# ...drive the scene: navigate, click, type, scroll...
adom-desktop browser_record_stop     '{"sessionId":"demo-web"}'   # → { path: "...webm" }
adom-desktop browser_lower_os_window '{"sessionId":"demo-web"}'   # tuck it away again when done
  • browser_record_stop returns the saved video path. Recording status/list verbs: browser_record_status / browser_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:

  • browser_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).
  • browser_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.
  • browser_set_viewport { sessionId, width, height, deviceScaleFactor? } — pins the viewport deterministically (fixes a too-small innerWidth). browser_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 browser_eval can pierce shadow roots directly — no hand-rolled walk:
adom-desktop browser_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 browser_eval by document-Y (rect.top + window.scrollY) — immune to an overshot screenshot — then browser_screenshot_element (or browser_scroll + browser_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: browser_screenshot { fullPage:true } only covers document-level scroll. On a nested-scroll SPA it returns just the viewport and a nestedScroll:true hint — use browser_screenshot_element for one region, or browser_scroll through the container.

Reminder: browser_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.