---
name: pup-windows-sessions-tabs
user-invocable: true
description: "pup (Puppeteer bridge) windows, sessions, and tabs: how sessionIds work and why to task-prefix them, WINDOW OWNERSHIP so you never steal another AI thread's window (owner + session_owned_by_another_thread + takeover), the BACKGROUND-by-default rule (foreground:true is the only way to show a window; sizing/positioning does NOT foreground), running MANY TABS in one window instead of many windows, switching/listing/closing sessions and tabs, and verifying what you actually opened. Read when opening/reusing pup windows, managing tabs, deciding background vs foreground, or seeing errorCode session_owned_by_another_thread. Trigger words: pup sessionId, pup_open_window owner, session_owned_by_another_thread, takeover window, pup background, foreground:true, pup_open_tab, pup_switch_tab, pup_list_tabs, pup_switch_window, pup_list_windows, pup_close_window, pup tabs, many tabs one window, don't steal window, pup_lower_os_window, pup_raise_os_window, pup_alert_window."
---

**Parent skill:** pup-bridge  ·  See also: `pup` (start here), `pup-screenshots-recording`, `pup-browsers-and-chrome`.

# pup — windows, sessions & tabs

A **session** = one browser **window** (a fresh isolated Chrome/Edge process) that holds **many tabs**.
You address everything by `sessionId`. `profile` (defaults to `sessionId`) names the on-disk cookie jar.

## sessionIds — task-prefix them, and pass `owner`

`pup_open_window {sessionId}` either **creates** a window (new id) or **reuses/navigates** an existing
one (id already exists). Because sessions are **shared across every AI thread on this desktop**, a generic
id collides:

- **Task-prefix the id:** `"<your-task>-web"` (e.g. `wikifix-preview`), never `wiki` / `test` / `main`.
- **Always pass `owner:"<your-task>"`.** It's an advisory label that lets pup protect your window.
- Pick a **stable `profile`** across runs if you want the cookie jar to persist (user logs in once, reuse
  for months): `{"sessionId":"vendor-fetch","profile":"vendor","owner":"vendor-fetch", …}`.

## Window ownership — do NOT steal another AI thread's window

If you `pup_open_window` a sessionId that already exists **with a new url**, pup NAVIGATES that window
— destroying whatever another thread was doing there. pup guards this:

1. **Declared cross-owner reuse is REFUSED** — `errorCode: session_owned_by_another_thread` (also on
   `pup_navigate` / `pup_close` / `pup_close_window`). The `_hint` tells you the current owner
   + url + age.
2. **If YOU get that error** — the window isn't yours. Open your OWN with a unique task-prefixed sessionId.
   Pass **`takeover:true`** ONLY if the user explicitly told you to repurpose that window.
3. **Anonymous grabs are allowed but loudly reprimanded** in the `_hint` (with the previous url so you can
   put it back) — so ALWAYS declare `owner` to be a good citizen and to get protection.
4. **Reusing your OWN window is good** — same sessionId + same owner = a smooth refresh (e.g. re-rendering
   a page you just republished). Ownership only blocks *cross-thread* grabs.
5. **Unsure what exists?** `pup_status '{}'` lists every window with its `owner`, current `url`, and
   `ageMinutes`. Run it at the start of a task (especially after a context compaction) before picking an id.

```bash
# right — owned, task-prefixed
adom-bridge-cli pup_open_window '{"sessionId":"wikifix-preview","owner":"wikifix","url":"<url>"}'
# wrong — generic + anonymous: may hijack another thread's window (pup reprimands you in _hint)
adom-bridge-cli pup_open_window '{"sessionId":"wiki","url":"<url>"}'
```

## Background by default — pup NEVER pops onto the user's screen unless asked

**pup is the AI's workspace, not the user's.** Every window opens **in the background** (behind the user's
windows, no focus stolen) and stays fully drivable + screenshottable there — you do NOT lower it yourself.

- **`foreground:true` is the ONLY thing that shows a window — and it is GATED (v1.8.70).** Not
  sizing, not navigating, not screenshotting, not recording setup. Foregrounding requires BOTH
  `foreground:true` AND `foregroundReason:"<quote/paraphrase what the user said>"` (min 10 chars).
  Without the reason the open still succeeds but the window stays BACKGROUND
  (`foregroundDenied:'foreground_reason_required'`). The reason is logged, recorded in
  `lastAgentUpdate`, and surfaced as `lastForeground` in status — the user can always audit who
  put a window on their screen and why. Pass it ONLY when the user explicitly wants to WATCH (a
  demo, a "show me", a live recording); after the show-me, `pup_lower_os_window` to return it.
- **Sizing/positioning does NOT foreground** — `pup_open_window {width,height,x,y}` places the window
  at an exact size/spot **for clean screenshot framing** while it stays in the background. So you can frame
  a shot without ever disturbing the user's main work.
- **`pup_alert_window`** flashes the taskbar (a non-intrusive "it's ready/done" nudge) — prefer it over
  raising the window. Use it for an explicit end-of-task "it's ready".
- `pup_raise_os_window` / `pup_focus_window` bring a window forward — **GATED the same way:
  REFUSED (`foreground_reason_required`) unless you pass `foregroundReason`.** Use only for an
  explicit user show-me; `pup_lower_os_window` tucks it back after. **Raise steals focus and can
  leak a mid-typed keystroke into the page** (pup blurs the page's focused element first to reduce
  it) — to just signal "I updated this window", use the auto-flash / `pup_alert_window`, NOT raise.

## Signalling what you changed — auto-flash + `updateNote` (so the user follows along)

When you drive a background window the user can't tell you touched it — and across several tabs they lose
track of WHICH one you just changed. pup handles both, automatically:

- **Auto-flash:** every **mutating** verb (`navigate`, `reload`, `back`/`forward`, `switch_tab`, `click`,
  `type`, `press_key`, `input_dispatch`, `eval`, `open_tab`, `open_window`) flashes that window's taskbar
  icon orange — **without stealing focus**, self-suppressed while it's foreground, and cleared when the
  user clicks it. It's **debounced to one flash per window per ~5s burst**, so a whole `open→navigate→
  fill→screenshot` sequence flashes once, not five times.
- **Tune it:** `{"silent":true}` on any one call suppresses that flash; `pup_open_window
  {"autoFlash":false}` silences the whole session (for a background poller); and a pure read should pass
  `pup_eval {"readOnly":true}` so a polling loop doesn't flash or count as an update.
- **Say what you did:** pass **`updateNote":"<short what-I-did>"`** on any mutating verb. pup records
  `lastAgentUpdate {tabId, verb, note, ts}` and surfaces it **top-level and per-tab** in
  `pup_list_tabs` / `pup_list_windows` — so the user (or a Session Monitor) sees which tab you
  staged and why. Add an `updateNote` on the meaningful steps of a multi-tab task.

```bash
# default: background — drive it invisibly, user never sees a thing
adom-bridge-cli pup_open_window '{"sessionId":"job-web","owner":"job","url":"<url>"}'
adom-bridge-cli pup_screenshot  '{"sessionId":"job-web"}'
# only if the user asked to SEE it:
adom-bridge-cli pup_open_window '{"sessionId":"demo-web","owner":"demo","url":"<url>","foreground":true}'
```

## Taskbar look — split vs grouped (user preference, `pup_configure`)

Each pup window wears the Adom Pup icon; how they arrange on the taskbar is the USER's choice:

- **`split` (default):** one taskbar button PER WINDOW, each badged with its own tab count
  (no badge = 1 tab). Best for telling AI threads apart: "the button with the 8 is my
  adom-google review window."
- **`grouped`:** all pup windows stack under ONE "Adom Pup" button; its badge shows the
  WINDOW count. Tidy taskbar for users who open many windows.

`pup_configure {"taskbarGrouping":"grouped"}` switches live (persists across updates).
**Ask the user before changing it — it's their taskbar.** Read current: `pup_configure {}`.

## One window, many tabs (multi-URL tasks)

For a vendor fetch / multi-doc review, put the URLs in **tabs of one window** (one taskbar icon), not
separate windows:

```bash
adom-bridge-cli pup_open_window '{"sessionId":"task-web","owner":"task","url":"https://first.example.com"}'
adom-bridge-cli pup_open_tab    '{"sessionId":"task-web","url":"https://second.example.com"}'   # → { tabId: "tab-2", tabCount: 2 }
adom-bridge-cli pup_switch_tab  '{"sessionId":"task-web","tabId":"tab-2"}'
adom-bridge-cli pup_screenshot  '{"sessionId":"task-web","tabId":"tab-2"}'   # tabId optional — omit for the active tab
adom-bridge-cli pup_close_tab   '{"sessionId":"task-web","tabId":"tab-2"}'
```

- `pup_open_tab` returns `{tabId, tabCount, activeTabId}`; pass the `tabId` to
  `switch`/`close`/`screenshot`/`eval`/`navigate` (omit to target the active tab).
- **Closing the last tab closes the window.**
- Popup tabs (e.g. a PDF the page opens) auto-attach and appear in `pup_list_tabs`; a closed popup's
  last URL is retained so you can `pup_fetch_url` it if you missed it.

## Don't double-open; reuse correctly

- New session entirely → `pup_open_window`.
- Same session, new page in the same tab → `pup_navigate` (with `tabId` if multiple tabs).
- A second view in the same session → `pup_open_tab` (NOT a second window).
- Switch which window is "active" → `pup_switch_window`; list them → `pup_list_windows`.

Opening the same content under a new `sessionId` leaves the user staring at two identical windows — so
`pup_status` first and reuse when the URL is already open.

## Clean up after yourself — close windows when the task is done

pup **never auto-closes** a window (a "stale" one might belong to a thread that's just paused
between turns). So every window you open lingers on the **user's real desktop** until *you* close it.
Left alone, a day of tasks piles up a dozen dead pup windows the user has to find and close.

- **When you finish a task, `pup_close_window` the windows you opened for it.** Same for one-off
  windows (a quick screenshot/scrape) — close them right after.
- **Check what's yours:** `pup_status` lists every window with its `owner` and `ageMinutes`, and
  now nudges you (a `🧹 CLEANUP` hint) when windows are old (≥30 min) or piling up. Act on it — close
  the ones you're done with. Only close windows **you own**; never close another thread's.
- **Long-running work?** Reuse ONE window (`pup_navigate` / `pup_open_tab`) instead of opening
  a fresh window each step, so there's only one thing to clean up.
- If you truly need a window to persist across turns (a login you'll reuse for hours), that's fine —
  but it's a deliberate choice, and you still own closing it when done.

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

`pup_open_window` returns `ok:true` as soon as navigation *starts*, not when content rendered. Confirm
with `pup_eval '{"sessionId":"…","expr":"document.title"}'` (fastest — 404s, login walls, anti-bot
blocks all change the title) or a `pup_screenshot` when the title isn't discriminating.

## Verbs at a glance

| Verb | Does |
|---|---|
| `pup_open_window` | Create (or reuse) a session/window. Args: `sessionId`, `owner`, `url?`, `profile?`, `foreground?`, `width/height/x/y?`, `takeover?`, `updateNote?`, `autoFlash?` (false = silence session) |
| `pup_navigate` | Drive an existing tab to a URL (no new window) — `sessionId`, `url`, `tabId?` |
| `pup_open_tab` / `pup_switch_tab` / `pup_close_tab` / `pup_list_tabs` | Tabs within one window |
| `pup_switch_window` / `pup_list_windows` | Multiple sessions |
| `pup_status` | List every session + `owner`/`url`/`ageMinutes` (NOT a readiness check) |
| `pup_lower_os_window` / `pup_raise_os_window` / `pup_focus_window` / `pup_alert_window` | Z-order / focus / taskbar flash |
| `pup_close_window` (one session) / `pup_close` (all) | Close — destructive |
