--- name: app-screenshots description: Drive Open Screenshot Generator headlessly (puppeteer-core + Edge) to take UI screenshots, add palette elements, upload device screenshots, export artboard PNGs, and regenerate the 3D device thumbnails. Use when asked to visually verify UI changes, capture the palette or canvas, test PNG exports, check rendering quality, or refresh public/elements/device-3d thumbs. metadata: # Contributor tooling for this repository, not a skill anyone installs. # `npx skills add` walks .claude/skills as a priority container, so without # this flag every person installing the published skills would be offered # our internal harnesses too, including scripts that shell a hardcoded # Windows Edge path. Set INSTALL_INTERNAL_SKILLS=1 to see them anyway. internal: true --- # App Screenshots & Browser Verification Drives the real app in headless Edge to verify changes end-to-end: screenshots, element adds, screenshot uploads, PNG exports, and pixel-level quality checks. ## Prerequisites - Dev server on **http://localhost:9002** — usually already running (`npm run dev`; `EADDRINUSE` means reuse it, Next.js hot-reloads your edits). - A Chromium browser. `lib.js` finds it per platform (Edge first, then Chrome/Chromium/Brave; `C:/Program Files...` on Windows, `/Applications/...` on macOS, `/usr/bin/...` on Linux) and every script imports `EDGE` from there. Set `APP_BROWSER` to override. Headless Edge via puppeteer uses the **real GPU** (verified: ANGLE D3D11), so WebGL renders match what the user sees. No swiftshader flags needed. - ffmpeg/ffprobe at `C:/ffmpeg-2026-02-04-git-627da1111c-essentials_build/bin/`. - One-time: `cd .claude/skills/app-screenshots/scripts && npm install` (installs puppeteer-core). ## Golden rules (each one cost real debugging time — do not skip) 1. **Never use `page.screenshot({ clip })`.** Clipped captures briefly resize the emulated viewport, which trips the responsive sidebar breakpoint and **remounts the palette, wiping tab/drill-in state**. Always take full-page screenshots and crop afterwards with ffmpeg. 2. **Radix tabs ignore synthetic `.click()`** — switch tabs with a real mouse click at the trigger's bounding-box center (`page.mouse.click`). Plain buttons/tiles are fine with DOM `.click()` via `page.evaluate` (also bypasses overlays). 3. **`waitForFunction` needs `polling: 500`** — the default rAF polling starves on static headless pages. Prefer string-expression predicates (`"document.querySelectorAll(...).length > 3"`) over function+args. 4. **After clicking "Start Blank", wait for `?projectId=` in the URL** before interacting — project creation settles asynchronously. 5. **File uploads:** start `page.waitForFileChooser()` *before* clicking the app's "Upload Screenshot" button, then `chooser.accept([path])`. 6. **Exports:** set `Browser.setDownloadBehavior` (CDP) to a download dir, click the export button, poll the dir until the expected number of `.png` files appears, then wait ~3s for writes to finish. ## App selectors - Start screen: the blank-canvas card is a button whose text contains `Start blank` (lowercase b — it reads "Start blank" inside the "Start with a blank canvas" card). The template gallery is still the first thing on screen; an **AI agent banner** sits above the tabs. `lib.js` `openAgentScreen(page)` steps into that agent screen (back out via `button[aria-label="Back"]`), and `clickByTextContains` clicks a button by a text fragment. - Agent screen: its first tab, selected when the screen opens, is **Claude Code** (`[role="tab"]` with that text and the orange logo). On desktop it holds a detection card ("Claude Code 2.1.202 is ready", or a not installed or not signed in card with `Check again`), a model picker and `Start with Claude Code`; on the web it says Claude Code runs in the desktop app and has no Start button. The plan modes come after it (`Free, use my account`, the desktop-only `Free, built in`, `Use my API key`), so a script that drives one of them has to click its tab first, with a real mouse click (rule 2). Step 1 holds, on every tab, the code folder row `Your app's code (Claude Code only)` with `button[aria-label="Add your app's code folder"]` and its chips (hidden in the Mac App Store build). A folder alone can start a Claude Code run. On the web that button toggles an alert titled `Code folders need the desktop app`; scope any locator to that alert, because the Claude Code tab has its own `Get the desktop app` button. - Tabs: `[role="tab"]` containing `Elements` / `Devices` / `Images` / `Previews`. **Previews** holds whole App Preview boards (see `src/lib/previewScenes.ts`); its tiles read `Add the preview board (scene:)` and each one adds an ARTBOARD, not an element, so count `[data-artboard-dom-id]` rather than `[data-element-id]` to detect the add. To review the scenes themselves, do NOT drive the canvas: headless Edge returns torn frames after scrolling the board row. Bundle `StaticArtboard` + `previewScenes.ts` with esbuild (same recipe as `regen-3d-thumbs.js`), serve the harness from `public/` so the posters resolve, and screenshot the mount node per scene. There is NO Layers tab anymore: Properties (top) and Layers (bottom) live in one right dock with a draggable divider (`[role="separator"]`) between them. Collapse the dock via `button[aria-label="Collapse right panel"]`; collapsed it becomes a slim vertical rail — expand via `button[aria-label="Expand right panel"]` or the rotated `Open Properties` / `Open Layers` buttons (by `title`). Dock state persists in localStorage (`abs-right-dock-open`, `abs-right-dock-layers-height`, `abs-right-dock-width`), so reset those keys if a test needs the default layout. The dock's left edge is `[role="separator"][aria-orientation="vertical"]` (`aria-label="Resize right panel"`): drag it left to widen the dock from 320px up to 720px, double-click to reset. - Agent dock tab (desktop only): the dock gains an `Agent` tab, first in the strip, once a first `Start with Claude Code` turns it on, and the Layers list steps aside while it is active. The panel root carries `data-agent-panel`. The input is `textarea[aria-label="Message the agent"]` (Enter sends, Shift+Enter is a new line), next to `button[aria-label="Send"]`, which becomes `button[title="Stop the agent"]` while a turn runs, `button[aria-label="Attach screenshots"]`, `button[aria-label="Add your app's code folder"]` (`aria-busy` while the native folder dialog is open; CDP cannot answer that dialog, so a debug build started with `OSG_CLAUDE_TEST_FOLDER=` grants that folder instead), the chip list `[aria-label="Folders Claude Code can read"]` with one `button[aria-label="Remove the folder"]` per folder, `button[aria-label="Past chats"]` (toggles the list of saved chats in place of the transcript, `aria-pressed` while open; a row is a button named by the chat's first message), `button[aria-label="Start a new chat"]` and `button[aria-label="Agent options"]` (model, Recent runs, Check Claude Code again). Collapsed, the rail button is `title="Open Agent"`; on a phone it is `aria-label="Open agent chat"`. localStorage keys: `osg-claude-agent-v1` (the chat, its code folders included), `osg-claude-agent-panel` (`1` keeps the tab) and `osg-claude-agent-model`. Remove all three for a clean start. Past chats live in the `agentChats` IndexedDB table, and opening another project swaps the panel to that project's latest chat (or an empty one). - Palette categories: `button[title="Browse "]` (e.g. `Browse 3D iPhone 17 Pro Max`, `Browse Colored iPhone`, `Browse Basic`); close with the `Back` button. - Tiles: `button[aria-label="Add