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
Make window-handle resolution conservative (correct-or-bare, never wrong): raise the wiggle serialization timeout above the enumeration time so concurrent wiggles cannot collapse multiple windows onto one handle; use a distinctive narrow marker width real windows never sit at; verify the width actually applied; require EXACTLY ONE match or leave the window bare rather than paint a guessed/shared handle
4 files changed
+36−21
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.9.301",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.301/adom-bridge-puppeteer-v1.9.301.zip",- "sha256": "1d208e689b3e2b6a3d3d5b03a961cdee7018cc7aae4fa599046681d563173952",- "size": 725229,+ "version": "1.9.302",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.302/adom-bridge-puppeteer-v1.9.302.zip",+ "sha256": "d0b115d36bddeb0b067080ff27d2758aeec7a990076168c175adda9fc9175040",+ "size": 725797, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.301+1.9.302
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.9.301",+ "version": "1.9.302", "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. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, browser_alert_window posts a native toast whose click focuses the window, browser_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.", "homepage": "https://wiki.adom.inc/adom/adom-desktop", "author": "Adom Inc.",
src/server.js+30−15@@ -4913,7 +4913,7 @@ async function resolveSessionHwndByBounds(session, sessionId) { // v1.9.290 UNIQUE-WIDTH WIGGLE: parked windows share the identical rect, so plain geometry can't // tell two same-profile windows apart (dup-hwnd bug). Give THIS window a UNIQUE width via CDP // (pup controls it directly — no page observer can fight it), match on that exact width, restore.- let bounds = null, cdp = null, windowId = null, markerW = null;+ let bounds = null, cdp = null, windowId = null, markerW = null, _widthChanged = false; try { cdp = await page.target().createCDPSession(); ({ windowId } = await cdp.send('Browser.getWindowForTarget'));@@ -4938,11 +4938,23 @@ async function resolveSessionHwndByBounds(session, sessionId) { // anyway (a briefly-overlapping wiggle is far better than a permanent deadlock), and our own // releaser always frees the next caller. const _prev = _wiggleLock; let _res; _wiggleLock = new Promise(r => { _res = r; }); _releaseWiggle = _res;- await Promise.race([Promise.resolve(_prev), new Promise(r => setTimeout(r, 8000))]);+ // v1.9.302: 15s wait (was 8s). The old 8s was SHORTER than the 9s PS enumeration each holder+ // runs, so under load the wait timed out and TWO wiggles ran concurrently — both windows sat at+ // a marker width at once and every session matched whichever the enum hit first (the collapse+ // that put 3 sessions on ONE handle). 15s comfortably exceeds a full wiggle, so normal operation+ // stays strictly serialized; the timeout only fires on a genuine hang (a deadlock ceiling).+ await Promise.race([Promise.resolve(_prev), new Promise(r => setTimeout(r, 15000))]); try {- markerW = Math.max(400, Math.round(_origW) - 3 - (_hwndWiggle++ % 90));+ // DISTINCTIVE narrow marker (480-599) that no real browser window sits at, so the shrunk+ // target is the ONLY window in that band. The old near-original width (orig-3) sat right on+ // top of real ~1500px windows and matched them by accident.+ markerW = 480 + (_hwndWiggle++ % 120); await cdp.send('Browser.setWindowBounds', { windowId, bounds: { width: markerW } });+ _widthChanged = true; await new Promise(r => setTimeout(r, 140));+ // VERIFY the width actually applied — a minimized/un-settable window would otherwise leave us+ // matching on a width we never set (a wrong handle). If it didn't take, abandon the wiggle.+ try { const rb = await cdp.send('Browser.getWindowBounds', { windowId }); if (!rb || !rb.bounds || Math.abs(rb.bounds.width - markerW) > 2) markerW = null; } catch (e) { markerW = null; } } catch (e) { markerW = null; } } const scriptPath = path.join(require('os').tmpdir(), `_adom_hwnd_${process.pid}.ps1`);@@ -4980,33 +4992,36 @@ async function resolveSessionHwndByBounds(session, sessionId) { if (!out) return null; // Every chrome.exe under this profile counts (renderers have no windows, so PID may be the parent // or a sibling in the tree — accept any, then disambiguate purely on geometry).- let best = null;+ // v1.9.302: collect ALL matching windows and demand EXACTLY ONE. The old code broke on the first+ // match, so an accidental collision silently won. If 0 or 2+ windows match, we cannot know which is+ // ours, so we LEAVE IT BARE rather than paint a wrong window (John's rule: bare > wrong, forever+ // after the Edge-overlay incident). A bare window self-heals on the next resolve; a wrong one lies.+ const matches = new Set(); for (const line of out.split(/\r?\n/)) { const [h, , L, T, W, H] = line.split(',').map(Number); if (!h || !Number.isFinite(W)) continue; for (const sc of [1, 1.25, 1.5, 1.75, 2]) { if (markerW != null) {- // WIGGLE match: the unique marker width identifies exactly this window (tight tolerance).- if (Math.abs(W - Math.round(markerW * sc)) <= 4) { best = { h, score: 0 }; break; }+ if (Math.abs(W - Math.round(markerW * sc)) <= 3) { matches.add(h); break; } } else { const dx = Math.abs(L - Math.round(bounds.left * sc)); const dy = Math.abs(T - Math.round(bounds.top * sc)); const dw = Math.abs(W - Math.round(bounds.width * sc));- const score = dx + dy + dw;- if (dx <= 40 && dy <= 40 && dw <= 60 && (!best || score < best.score)) best = { h, score };+ if (dx <= 30 && dy <= 30 && dw <= 40) { matches.add(h); break; } } }- if (best && best.score === 0) break; }- try { if (markerW != null) await cdp.send('Browser.setWindowBounds', { windowId, bounds: { width: _origW } }); } catch (e) {}+ try { if (_widthChanged) await cdp.send('Browser.setWindowBounds', { windowId, bounds: { width: _origW } }); } catch (e) {} if (_releaseWiggle) { try { _releaseWiggle(); } catch (e) {} } try { if (cdp) await cdp.detach(); } catch (e) {}- if (best) {- session._hwnd = best.h;- persistSessionHwnd(sessionId, best.h);- console.log(`[hwnd] "${sessionId}" resolved OS handle ${best.h} by ${markerW != null ? 'UNIQUE-WIDTH wiggle' : 'window BOUNDS'} — taskbar paint + park can target it`);- return best.h;+ if (matches.size === 1) {+ const h = [...matches][0];+ session._hwnd = h;+ persistSessionHwnd(sessionId, h);+ console.log(`[hwnd] "${sessionId}" resolved OS handle ${h} by ${markerW != null ? 'unique-width wiggle' : 'window bounds'} (exactly 1 match)`);+ return h; }+ console.log(`[hwnd] "${sessionId}" NOT resolved — ${matches.size} candidate windows matched (${markerW != null ? 'wiggle' : 'bounds'}); leaving bare rather than painting a guess`); return null; } catch (e) { if (_releaseWiggle) { try { _releaseWiggle(); } catch (x) {} } console.log(`[hwnd] "${sessionId}" bounds resolve failed: ${e.message}`); return null; } }