---
name: pup
user-invocable: true
description: "Control Puppeteer browser windows on the user's desktop via the adom-desktop CLI. Use for: opening/closing browser windows, reloading pages, taking screenshots, evaluating JS, flashing taskbar alerts, navigating URLs, checking errors, managing multi-session Chrome. Trigger words: pup, puppeteer, browser window, browser reload, browser screenshot, pup reload, pup screenshot, pup alert, browser debug, visual debug, open in browser, reload browser, flash taskbar, browser_reload, browser_alert_window, browser_open_window, browser_screenshot, browser_eval."
---

# Pup — Puppeteer Browser Control

Control Chrome browser windows on the user's desktop via the `adom-desktop` CLI.

## How to Run Commands

All `browser_*` commands are run via the `adom-desktop` CLI using Bash:

```bash
adom-desktop <command> '<json_args>'
```

**Examples:**
```bash
adom-desktop browser_reload '{"sessionId":"dart2"}'
adom-desktop browser_alert_window '{"sessionId":"dart2"}'
adom-desktop browser_screenshot '{"sessionId":"dart2"}'
adom-desktop browser_open_window '{"sessionId":"myapp","profile":"myapp","url":"http://localhost:3000"}'
adom-desktop browser_eval '{"sessionId":"dart2","expr":"document.title"}'
```

**IMPORTANT:** These are NOT MCP tools. They are Bash commands. Use the Bash tool to run them.

## Available Commands

### Session Management
| Command | Description | Required Args |
|---|---|---|
| `browser_open_window` | **Spawns a new pup window**. Only use when there isn't an existing session of that name. | `sessionId`, `profile`, `url` |
| `browser_close_window` | Close a specific session | `sessionId` |
| `browser_focus_window` | Bring window to foreground | `sessionId` (optional) |
| `browser_alert_window` | Flash taskbar icon (non-intrusive) | `sessionId` (optional) |
| `browser_switch_window` | Switch global active session | `sessionId` |
| `browser_list_windows` | List all open sessions | none |

### ⚠ Don't double-open windows — `browser_navigate` reuses, `browser_open_window` spawns

If a session already exists (verify with `browser_list_windows` or `browser_list_tabs`), point an existing tab at a new URL with **`browser_navigate`** — passing `tabId` if you want to be explicit:

```bash
adom-desktop browser_navigate '{"sessionId":"chip-fetcher","tabId":"tab-1","url":"<new-url>"}'
```

Using `browser_open_window` against a session that's already alive **spawns a second physical pup window** with the same title. The new tab is tracked but the old window is orphaned — the user sees two windows showing the same content. Verified painful 2026-05-04: opened a chip-fetcher dashboard verification tab while the original UL Pro detail tab was still active; user noticed the duplicate before I did. Cleanup required `desktop_close_window` and a tracked-tab marker dance.

**Decision tree:**
- Need a new session entirely (different `sessionId`) → `browser_open_window`.
- Need to reuse an existing session's tab → `browser_navigate` (with `tabId` if multiple tabs).
- Need a second tab in the same session → `browser_open_tab` (not `browser_open_window`).

### ⚠ Cross-sessionId duplicates — same app, two windows

The double-open trap above catches *same-sessionId* re-opens. There's a second flavor that catches you across conversation boundaries: **a previous session left a pup window open under one `sessionId`, and the new conversation opens the same app under a different `sessionId`** — adom-desktop happily creates a second window because the keys differ. The user sees two pup windows showing the same content.

**Always run `browser_status '{}'` before `browser_open_window`** at the start of a new conversation, especially after a context compaction. Look for any session whose URL maps to the app you're about to open. Reuse it (`browser_navigate`) instead of creating a peer.

Verified 2026-05-07 with chipsmith: an earlier session had `sessionId: cs-v2` open at `…/proxy/8872/`. New conversation opened `sessionId: chipsmith` at the same port — two windows for the same chip. User noticed the duplicate first.

When closing a duplicate, prefer the OLDER one (lower `cb=` timestamp in URL); the newer one usually has the working state you just built up.

### Page Commands (all accept optional `sessionId`)
| Command | Description | Key Args |
|---|---|---|
| `browser_reload` | Reload page + clear error log | `sessionId` |
| `browser_navigate` | Navigate to new URL | `url`, `sessionId` |
| `browser_screenshot` | Capture page screenshot (lossless PNG) | `sessionId`, `maxWidth`, `fullPage` |
| `browser_eval` | Evaluate JS expression in page | `expr`, `sessionId` |
| `browser_errors` | Get console/page errors | `sessionId` |
| `browser_wait` | Wait for content to settle | `ms` (default 3000) |
| `browser_status` | Check all sessions | none |
| `browser_close` | Close ALL sessions | none |

## Common Patterns

### Reload + Alert (after code changes)
```bash
adom-desktop browser_reload '{"sessionId":"dart2"}' && \
adom-desktop browser_alert_window '{"sessionId":"dart2"}'
```

### Debug Loop
1. Edit code
2. `browser_reload`
3. `browser_wait` (3-5s for 3D tiles)
4. `browser_screenshot`
5. `browser_errors`
6. Analyze, repeat if needed

### Check Connectivity
```bash
adom-desktop ping
```

## Pick ONE surface — respect the user's window-mode preference

Hydrogen stores a per-session **window mode** preference: `webview` (default) or `pup`. Check it before opening a window:

```bash
adom-cli hydrogen probe  # → { ..., "window_mode": "webview" | "pup" }
```

- **`webview`** → open as a Hydrogen webview tab (`webview open-or-refresh`)
- **`pup`** → open via Adom Desktop (`pup browser_open_window`). If Adom Desktop isn't reachable, fall back to webview and warn.

**Never open the same content in both at once.** User
feedback 2026-04-26: *"why the fuck did you open something in pup
and some stuff in webview. pick one or the other."*

Two costs of dual-opening:
1. Two copies of the page → user sees changes in one but not the
   other → "why doesn't my upload show up?" or "which one is
   real?".
2. Background WS / polling traffic doubles, which makes the
   Cloudflare-blipping problem worse.

When window-mode is `webview`, the decision rule:

| Use case | Surface | Why |
|---|---|---|
| Automated test, screenshot, eval, ralph loop, scripted multi-step demo | **pup** (even in webview mode — this is automation, not user-facing) | scriptable; doesn't compete with the user's tabs; pup's `browser_eval` works on every tab |
| Showing the user a finished result they should look at | **Hydrogen webview tab** (`adom-cli hydrogen webview open-or-refresh`) | the tab persists in their workspace; they can interact normally |
| Both at once for the SAME content | **don't** | pick whichever role the situation calls for and stick with it |

When window-mode is `pup`, everything goes through Adom Desktop — including user-facing results.

If you've opened the user's Hydrogen tab for a dashboard / viewer,
use **that** tab for screenshots too via `adom-cli hydrogen
screenshot panel --name "<tab-name>"`. Don't ALSO open a pup
window of the same URL. If you started in pup for testing and the
user wants to see the final state, route them to a Hydrogen tab
once and close the pup window.

Memory rule still applies for testing: pup is the testing surface,
NOT Hydrogen. The point of THIS rule is: don't open both for the
SAME content at the SAME time.

## Multi-URL workflows — ONE window, MANY tabs

When you'd open more than one URL for the same user task (vendor
fetch, multi-doc review, compare-pages workflows), put them in
**tabs of one window**, not separate windows. User feedback
2026-04-26: *"when you make all those pup windows, they clutter
up the user's desktop, so you should open all of those as tabs
in one pup window."*

```bash
# 1) Create the window with the first URL.
adom-desktop browser_open_window '{
  "sessionId":"my-task","profile":"my-task",
  "url":"https://first.example.com"
}'

# 2) Add the rest as tabs in the SAME session.
adom-desktop browser_open_tab '{"sessionId":"my-task","url":"https://second.example.com"}'
adom-desktop browser_open_tab '{"sessionId":"my-task","url":"https://third.example.com"}'

# 3) Activate a specific tab when you want to screenshot or eval it.
adom-desktop browser_switch_tab '{"sessionId":"my-task","tabId":"tab-2"}'
adom-desktop browser_screenshot   '{"sessionId":"my-task","maxWidth":1500}'
adom-desktop browser_eval         '{"sessionId":"my-task","expr":"document.title"}'
```

`browser_open_tab` lives alongside the documented `browser_*`
verbs. Same JSON-args convention; same `sessionId` semantics.

Pick a stable `profile` name across runs of the same task so the
cookie jar persists — the user logs in once and reuses for 12+
months. Cross-vendor cookie blending is fine because cookies are
per-domain.

## Verify what you opened — `"ok": true` is not enough

`browser_open_window` / `browser_open_tab` returns `"ok": true` as
soon as the navigation starts, NOT when the page actually
rendered useful content. Always sanity-check the result:

```bash
# Title check — fastest. 404s, anti-bot blocks, and login walls
# all change the title.
adom-desktop browser_eval '{"sessionId":"my-task","expr":"document.title"}'

# Screenshot — slowest, most thorough. Use when title isn't
# discriminating (e.g. SPA that updates title async).
adom-desktop browser_screenshot '{"sessionId":"my-task"}'
```

**Never blindly template a part number into a vendor URL pattern
without verifying the page exists.** The cost of a 404 you didn't
catch is the user spotting it instead. (Real example: shipped a
manufacturer-search-URL pattern that 404'd on `ti.com/product/`
and `nxp.com/` for parts those vendors don't make. Using a
`google.com/search?q=…` link routes through Google's resolver
and gets a real product hit on the first result.)

## Key Rules

- **Always pass `sessionId`** — never use `'{}'` as args. The default targets the *active* session which may not be what you want.
- **`adom-desktop browser_<cmd> <json>` — never `adom-desktop pup <cmd> --flags`.** There is no `pup` subcommand at the top level; that pattern fails with "Invalid JSON args".
- **No semicolons in `browser_eval`** — use comma operator or IIFE instead.
- **Sessions persist** — IndexedDB, cookies, localStorage survive restarts. Profile name is the persistence key.
- **Each session = independent Chrome window** — crash-isolated. Multiple tabs share one session and one cookie jar.
- **Never kill Chrome broadly** — use `browser_close_window` for specific sessions.
- **Always reload + alert after browser-facing code changes** — the user expects to see updates immediately.

## Full Reference

For detailed docs (profiles, sleep/wake recovery, troubleshooting, first-time setup), see `~/.claude/skills/adom/guides/pup.md`.
