app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
← Commit history
1.8.57: self-heal stranded windows at reattach and tab hot path, debounced re-badge, owner tip for anonymous creators, richer tab verb docs
5 files changed
+88−8
CHANGELOG.md+18@@ -1,5 +1,23 @@ # Changelog — Adom Desktop Puppeteer Bridge +## 1.8.54 → 1.8.57 — 2026-07-14 — Honest click signal; OS maximize; focus-latch fix; self-heal stranded windows++- **1.8.54:** `GetForegroundWindow` replaces `document.hasFocus()` for user-click detection —+ hasFocus LIES for CDP-created windows (false-fired `user-foreground` on every open).+- **1.8.55:** park places via CDP `windowState:'maximized'` (OS-true full size) instead of pixel+ math from a screen probe that could fail mid-navigation (shipped 1600×900 windows once).+- **1.8.56:** flash `pending/cleared` focus latch fixed — the hook now also injects into the+ CURRENT document (`evaluateOnNewDocument` alone only fires on the NEXT navigation, so clicked+ windows kept reporting `pending`).+- **1.8.57 (the adom-shotlog fix):** a window created under a build whose park failed sat+ stranded off-screen forever — taskbar click "showed" an invisible window, badge gone. pup now+ **self-heals**: cheap CDP bounds check at reattach / `open_tab` / `switch_tab`; only if actually+ off-screen, re-park + re-badge (never touches a healthy or user-foreground window). Badge also+ re-applies (debounced 10 min) on the tab hot path so long-lived shared windows stay badged+ across bridge restarts. Anonymous window CREATORS now get an owner-tip in the open response+ (an ownerless long-lived window has zero cross-thread protection). Enriched `browser_describe`+ entries for `open_tab`/`switch_tab` (return shape, updateNote, background/click contract).+ ## 1.8.53 — 2026-07-14 — Direct-API park: CDP + AD verbs replace the PowerShell window-hunt John caught a parallel-open window (catch-2) still stuck OFF-SCREEN while the bridge claimed
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.8.56",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.56/adom-bridge-puppeteer-v1.8.56.zip",- "sha256": "00b520326ff4576fecebc1def443d93b427d7d181a6474fc36e2dfc788d97371",- "size": 168782,+ "version": "1.8.57",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.57/adom-bridge-puppeteer-v1.8.57.zip",+ "sha256": "98faae019cb48d8d1eee51551e300e92aed5874b75922e8b6059637a91390953",+ "size": 170117, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.8.56+1.8.57
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.8.56",+ "version": "1.8.57", "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. browser_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. browser_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job.", "homepage": "https://wiki.adom.inc/adom/adom-desktop", "author": "Adom Inc.",
src/server.js+64−2@@ -945,6 +945,14 @@ async function rescanProfile(browser, profileName, { adoptOrphans = false } = {} `[rescan] profile "${profileName}": ${reattached} reattached, ${adopted} orphans adopted, ` + `${alreadyLive} pages already live` );+ // v1.8.57: after a reattach, self-heal a window stranded off-screen by an older build's+ // failed park (one heal per profile — all this profile's sessions share the window).+ if (reattached > 0) {+ for (const sid of seenSessions) {+ const s = sessions.get(sid);+ if (s && s.tabs.length > 0) { healOffscreenWindow(s, sid).catch(() => {}); break; }+ }+ } return { reattached, adopted, alreadyLive }; } @@ -2026,6 +2034,34 @@ async function parkSessionWindowDirect(page, sessionId) { } } +// v1.8.57: SELF-HEAL a stranded window. A window created under a build whose park failed (or+// any future placement bug) sits at its off-screen launch coords forever: its taskbar button+// "activates" an invisible window and the badge is gone — exactly what happened to the+// adom-shotlog app's long-lived shared window. pup now detects and repairs this at every+// natural touchpoint (bridge-restart reattach, open_tab, switch_tab): cheap CDP bounds check;+// only if actually off-screen, re-run the standard park (which respects a user-foreground+// window) and re-apply the badge. Healthy windows are never touched.+async function healOffscreenWindow(session, sessionId) {+ if (process.platform !== 'win32') return null;+ try {+ const tab = session && session.tabs && session.tabs[0];+ const page = tab && tab.page;+ if (!page) return null;+ const cdp = await page.target().createCDPSession();+ let b = null;+ try {+ const { windowId } = await cdp.send('Browser.getWindowForTarget');+ const gb = await cdp.send('Browser.getWindowBounds', { windowId });+ b = gb && gb.bounds;+ } finally { try { await cdp.detach(); } catch (e) {} }+ if (!b || (b.left > -5000 && b.top > -5000)) return null; // on-screen — healthy, hands off+ console.log(`[heal] "${sessionId}" window stranded off-screen (${b.left},${b.top}) — re-parking + re-badging`);+ const r = await parkSessionWindowDirect(page, sessionId);+ badgePupTaskbar(sessionId).catch(() => {});+ return r;+ } catch (e) { return null; }+}+ // v1.8.45: TRULY clear a window's taskbar attention (the orange/pink Win11 "flash" tint). // FLASHW_STOP only stops the PULSE — the tint persists until the window is ACTIVATED. So we // clear it the only way Windows allows: activate the window. To avoid a visible pop, we move@@ -3442,7 +3478,7 @@ const server = http.createServer(async (req, res) => { }; sendJSON(res, { success: true, bridge: 'puppeteer', verbs: [ V('browser_open_window', 'Open a generic pup window — a fresh isolated Chrome/Edge process (never your real signed-in browser), fully drivable via CDP. ALWAYS opens in the BACKGROUND (does not steal focus; never disturbs the user\'s main work; still fully screenshottable/drivable) UNLESS you pass foreground:true — do that ONLY when the user explicitly wants to watch. Pass width+height (+ optional x,y, screen px) to place it at a specific size/position for screenshot framing — this NO LONGER foregrounds it (sizing stays in the background too; default unsized is maximized). profile defaults to sessionId. OWNERSHIP: sessionIds are shared across ALL AI threads — use a task-prefixed sessionId + owner:"<your-task>" so pup can refuse other threads stealing your window (reusing an existing id with a new url navigates it away; declared cross-owner reuse errors with session_owned_by_another_thread unless takeover:true). (For the user\'s REAL Chrome/Edge, use the Adom extension\'s nbrowser_* verbs.)', { sessionId: 'required str — task-prefix it (e.g. "mytask-web"); generic ids collide across threads', url: 'optional str', profile: 'optional str — defaults to sessionId', owner: 'optional str — your thread/task label; lets pup protect this window from other threads', takeover: 'optional bool — claim a window owned by another thread (only when the user explicitly asks)', foreground: 'optional bool — the ONLY way to show the window (default false = background, always, even when sized)', width: 'optional int px — sizes for screenshot framing, stays background', height: 'optional int px', x: 'optional int px', y: 'optional int px', updateNote: 'optional str — a short "what I did" note, surfaced back in list_tabs/list_windows as lastAgentUpdate so the user knows what changed', autoFlash: 'optional bool (default true) — mutating verbs auto-flash this window\'s taskbar (debounced, never steals focus) so the user notices the agent touched it; pass false to make this window silent by default', silent: 'optional bool — suppress the auto-flash for just this call' }, 60, { sessionId: 'mytask-web', owner: 'mytask', url: 'https://example.com', width: 960, height: 540 }),- V('browser_open_tab', 'Open a new tab in a session.', { sessionId: 'required str', url: 'optional str' }, 30, { sessionId: 'work', url: 'https://example.com' }),+ V('browser_open_tab', 'Open a new tab in an EXISTING session/window (returns {tabId, tabCount, activeTabId} — no need to diff list_tabs). This is the right verb for multi-URL work: ONE window with many tabs (one taskbar button), not many windows. The window stays in the background; the tab auto-flashes the taskbar (debounced) so the user notices. The USER can click the window\'s taskbar button at any time to view it — pup never pulls it back down.', { sessionId: 'required str', url: 'optional str', updateNote: 'optional str — short "what I did" note surfaced in list_tabs as lastAgentUpdate', silent: 'optional bool — suppress the auto-flash for this call' }, 30, { sessionId: 'work', url: 'https://example.com' }), V('browser_close_tab', 'Close a tab.', { sessionId: 'required str', tabId: 'optional str (default active)' }, 15, { sessionId: 'work' }), V('browser_close_window', 'Close a window/session (its tabs go too).', { sessionId: 'required str' }, 20, { sessionId: 'work' }), V('browser_navigate', 'Navigate the active tab to a URL.', { sessionId: 'required str', url: 'required str' }, 45, { sessionId: 'work', url: 'https://example.com' }),@@ -3478,7 +3514,7 @@ const server = http.createServer(async (req, res) => { V('browser_list_windows', 'List every open session/window with its tabs.', {}, 10, {}), V('browser_switch_window', 'Make a session the active one (for verbs that default to active).', { sessionId: 'required str' }, 10, { sessionId: 'work' }), V('browser_list_tabs', 'List the tabs in a session.', { sessionId: 'required str' }, 10, { sessionId: 'work' }),- V('browser_switch_tab', 'Bring a tab to the front within its session.', { sessionId: 'required str', tabId: 'required str' }, 10, { sessionId: 'work' }),+ V('browser_switch_tab', 'Activate a tab WITHIN its session/window (idempotent — safe to call when the tab is already active). Does NOT raise a background window to the user\'s screen: the tab becomes the window\'s visible tab wherever the window is, and the taskbar auto-flash tells the user something changed. Also self-heals the window (re-park if a bug ever stranded it off-screen) and refreshes the Adom taskbar badge — good citizenship for long-lived shared windows.', { sessionId: 'required str', tabId: 'required str', updateNote: 'optional str', silent: 'optional bool' }, 10, { sessionId: 'work', tabId: 'tab-2' }), V('browser_screenshot_full_res', 'FULL-resolution page screenshot (NOT downscaled — for saving to disk, NOT for Read: it can exceed Claude\'s image limit). Use browser_screenshot for AI-readable shots.', { sessionId: 'required str', fullPage: 'optional bool' }, 30, { sessionId: 'work' }), V('browser_errors', 'Console errors + failed network requests for a session/tab (clears by default).', { sessionId: 'required str', tabId: 'optional str', clear: 'optional bool (default true)' }, 10, { sessionId: 'work' }), V('browser_wait', 'Wait N ms (or for a selector) before the next step.', { sessionId: 'required str', ms: 'optional int', selector: 'optional str' }, 60, { sessionId: 'work', ms: 500 }),@@ -3673,6 +3709,11 @@ const server = http.createServer(async (req, res) => { } } else if (reuse && reuse.reused) { ownHint = ' Reused the existing window (same/no url — benign refresh).';+ } else if (!args.owner) {+ // v1.8.57: teach anonymous CREATORS too (a long-lived app like adom-shotlog that+ // never declares owner gets zero cross-thread protection — its window can be+ // navigated away or closed by any other AI thread without pup refusing).+ ownHint = ` TIP: you created this window WITHOUT owner — pass owner:"<your-app-or-task>" on browser_open_window so pup REFUSES other threads' attempts to navigate/close it (session_owned_by_another_thread) instead of letting them take it. Especially important for a long-lived shared window.`; } sendJSON(res, { success: true,@@ -3827,6 +3868,16 @@ const server = http.createServer(async (req, res) => { const tab = getTabById(session, tabId); recordAgentUpdate(session, 'browser_open_tab', args, tabId); // v1.8.23 scheduleAutoFlash(session, { silent: !!args.silent });+ // v1.8.57: long-lived-app touchpoint (see switch_tab) — heal a stranded window,+ // re-badge debounced.+ if (process.platform === 'win32') {+ healOffscreenWindow(session, sessionId).catch(() => {});+ const now = Date.now();+ if (!session._badgedAt || now - session._badgedAt > 600000) {+ session._badgedAt = now;+ badgePupTaskbar(sessionId).catch(() => {});+ }+ } const info = await describeTab(tab, session.activeTabId); // v1.8.23: return tabId + tabCount + activeTabId at the TOP level (was only inside `info`). sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId, tabId, tabCount: session.tabs.length, activeTabId: session.activeTabId, ...info }) });@@ -3876,6 +3927,17 @@ const server = http.createServer(async (req, res) => { // v1.8.26: only raise the OS window for a session the user is watching. For a // background window, just track the active tab (pup targets tabs by id). if (session._foreground) { try { await tab.page.bringToFront(); } catch {} }+ // v1.8.57: switch_tab is a long-lived app's hot path (adom-shotlog switches its+ // shared window's tabs for hours) — self-heal a stranded window + re-badge here+ // (debounced) so the window stays clickable/badged across bridge restarts.+ if (process.platform === 'win32') {+ healOffscreenWindow(session, sessionId).catch(() => {});+ const now = Date.now();+ if (!session._badgedAt || now - session._badgedAt > 600000) {+ session._badgedAt = now;+ badgePupTaskbar(sessionId).catch(() => {});+ }+ } const info = await describeTab(tab, session.activeTabId); sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId, ...info }) }); return;