--- name: app-screenshots description: Capture screenshots of the running Mailspring dev app for docs, PRs, or visual verification — launching with a CDP port, driving the UI, anonymizing or seeding data, and clipping to an element. Use whenever a task needs a picture of the app. --- # Screenshots of the running app `screencapture` is blocked in this environment (no Screen Recording permission), and the app has no test-id hooks. The reliable path is Chrome DevTools Protocol against a dev build launched with a remote-debugging port. Everything below runs from the repo root unless noted. Scripts live in `.claude/skills/app-screenshots/scripts/`. ## 1. Launch Only one instance can run at a time, so check first. If one is running that you didn't start, it is probably the user's: ask before closing it. ```bash pgrep -fl "[Ee]lectron ./app" # anything listed will be closed by the next line pkill -f "[Ee]lectron ./app"; pkill -f "app/mailsync --mode sync"; sleep 2 (./node_modules/.bin/electron ./app --enable-logging --dev --remote-debugging-port=9333 > /tmp/app.log 2>&1 &) sleep 18 # window + plugins + first sync curl -s localhost:9333/json | grep -c webSocketDebuggerUrl # >0 means ready ``` - Always kill orphaned `mailsync` processes too; they outlive the Electron process and hold the SQLite DB open (a later seed script will silently fail to commit). - `location.reload()` does nothing in the renderer. Pick up renderer changes with `AppEnv.commands.dispatch('window:reload')`; main-process changes (`app/src/browser`) need a full relaunch. Edited LESS can be hot-loaded with `AppEnv.themes.reloadCoreStyles()`. - For doc-sized captures, shrink the window first: `AppEnv.getCurrentWindow().setSize(1280, 800)`. ## 2. Drive the UI `scripts/eval.mjs` evaluates JS in the main window (picks the target whose `document.body.className` contains `window-type-default`) and optionally captures the whole window or dispatches a real mouse click: ```bash node --experimental-websocket scripts/eval.mjs "" [out.png] [clickX clickY] ``` Useful snippets inside the JS: - Navigate to a sheet: `$m.Actions.focusMailboxPerspective(new (require('../internal_packages/activity/lib/activity-mailbox-perspective').default)($m.FocusedPerspectiveStore.sidebarAccountIds()))` - Pick from a `DropdownMenu`: click `.dropdown-menu > div`, wait 300ms, then find the item by text under `.menu div` and dispatch `mousedown` **and** `click` on it. - Drive a controlled ``: set the value through `Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set` and dispatch `input`; plain `.value =` doesn't reach React. - Test real selection/hover behaviour with the click args (`Input.dispatchMouseEvent`), not synthetic `el.click()` — the two differ for caret placement and `:hover`. - Wait after every action; the Reports tab has a 2s minimum "thinking" cover. ## 3. Capture an element `scripts/shoot.mjs` runs setup JS, then captures one element by selector using `Page.captureScreenshot` with a `clip` in CSS px: ```bash node --experimental-websocket scripts/shoot.mjs "" "" out.png [pad] [maxHeight] ``` - `scale: 1` already yields device pixels (2x on retina). Don't pass `scale: 2` — that produces 4x images. - Clip by element rect; don't crop afterwards with `sips -c`, which crops from the centre and pads with black. Use `maxHeight` to trim tall columns instead. - Connecting a CDP session blurs the window (`body.is-blurred`, greyed traffic lights). The script strips the class before capturing. - **Popovers close on blur.** Open the popover inside the same `setup-js` that the capture runs in, never in a previous invocation, and never call `activeElement.blur()` to remove the focus ring — inject a `