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, browser_open_window owner, session_owned_by_another_thread, takeover window, pup background, foreground:true, browser_open_tab, browser_switch_tab, browser_list_tabs, browser_switch_window, browser_list_windows, browser_close_window, pup tabs, many tabs one window, don't steal window, browser_lower_os_window, browser_raise_os_window, browser_alert_window."

Parent skill: adom-desktop-puppeteer-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

browser_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 browser_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 REFUSEDerrorCode: session_owned_by_another_thread (also on browser_navigate / browser_close / browser_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? browser_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-desktop browser_open_window '{"sessionId":"wikifix-preview","owner":"wikifix","url":"<url>"}'
# wrong — generic + anonymous: may hijack another thread's window (pup reprimands you in _hint)
adom-desktop browser_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, browser_lower_os_window to return it.
  • Sizing/positioning does NOT foregroundbrowser_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.
  • browser_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".
  • browser_raise_os_window / browser_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; browser_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 / browser_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; browser_open_window {"autoFlash":false} silences the whole session (for a background poller); and a pure read should pass browser_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 browser_list_tabs / browser_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-desktop browser_open_window '{"sessionId":"job-web","owner":"job","url":"<url>"}'
adom-desktop browser_screenshot  '{"sessionId":"job-web"}'
# only if the user asked to SEE it:
adom-desktop browser_open_window '{"sessionId":"demo-web","owner":"demo","url":"<url>","foreground":true}'

Taskbar look — split vs grouped (user preference, browser_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.

browser_configure {"taskbarGrouping":"grouped"} switches live (persists across updates). Ask the user before changing it — it's their taskbar. Read current: browser_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:

adom-desktop browser_open_window '{"sessionId":"task-web","owner":"task","url":"https://first.example.com"}'
adom-desktop browser_open_tab    '{"sessionId":"task-web","url":"https://second.example.com"}'   # → { tabId: "tab-2", tabCount: 2 }
adom-desktop browser_switch_tab  '{"sessionId":"task-web","tabId":"tab-2"}'
adom-desktop browser_screenshot  '{"sessionId":"task-web","tabId":"tab-2"}'   # tabId optional — omit for the active tab
adom-desktop browser_close_tab   '{"sessionId":"task-web","tabId":"tab-2"}'
  • browser_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 browser_list_tabs; a closed popup's last URL is retained so you can browser_fetch_url it if you missed it.

Don't double-open; reuse correctly

  • New session entirely → browser_open_window.
  • Same session, new page in the same tab → browser_navigate (with tabId if multiple tabs).
  • A second view in the same session → browser_open_tab (NOT a second window).
  • Switch which window is "active" → browser_switch_window; list them → browser_list_windows.

Opening the same content under a new sessionId leaves the user staring at two identical windows — so browser_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, browser_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: browser_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 (browser_navigate / browser_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

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

Verbs at a glance

Verb Does
browser_open_window Create (or reuse) a session/window. Args: sessionId, owner, url?, profile?, foreground?, width/height/x/y?, takeover?, updateNote?, autoFlash? (false = silence session)
browser_navigate Drive an existing tab to a URL (no new window) — sessionId, url, tabId?
browser_open_tab / browser_switch_tab / browser_close_tab / browser_list_tabs Tabs within one window
browser_switch_window / browser_list_windows Multiple sessions
browser_status List every session + owner/url/ageMinutes (NOT a readiness check)
browser_lower_os_window / browser_raise_os_window / browser_focus_window / browser_alert_window Z-order / focus / taskbar flash
browser_close_window (one session) / browser_close (all) Close — destructive