main
Kyle Bergstedt Publish 3.0.0 259e308 13d ago

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 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.
# 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 (pup's browser app is hidden, the Cmd+H state, so nothing covers the user and no focus is 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 posts a macOS notification the user can click (a non-intrusive "it's ready/done" nudge that never takes focus) — 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 updateNote / pup_alert_window, NOT raise.

Signalling what you changed — 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. macOS has no taskbar button to flash, so say it in the record:

  • A pure read should pass pup_eval {"readOnly":true} so a polling loop doesn't 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.
# 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}'

One window, many tabs (multi-URL tasks)

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

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?
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 Hide / show / focus / notify
pup_close_window (one session) / pup_close (all) Close — destructive