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
v2.0.259: Implement John's new placement rules (2026-08-20). R1: a window is BORN at its final on-screen rect via Target.createTarget left/top/width/height, the primary monitor work area inset 10px on all four sides, so it is clickable from its first millisecond. No staging position, no migration. R2: background is a preference, not a precondition; if the window comes up foreground anyway, accept it and stop, because fighting it is what produced every re-park loop. R3: a taskbar click always works, with no help from pup. R4: the hwnd is captured in the same locked moment the window is created, by set-diff over this profile's Chrome PIDs, which needs no unique birth coordinate; when two windows appear under the lock it now REFUSES to guess rather than risk a wrong handle. R5: AUMID and tile work stays async and after. Park is no longer a placement gate: bottoming is 3 bounded attempts instead of 8 slow ones and can never move, hide or off-screen the window when it fails. zConfirmedBelow is deleted; it gated placement on a proof that returned false whenever our hwnd was absent from ab's enumeration, which an off-screen window often is, making the failure self-sustaining.
4 files changed
+103−119
adom-bridge-puppeteer-manifest.json+5−5@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "2.0.258",- "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.258/adom-bridge-puppeteer-v2.0.258.zip",- "sha256": "ea109e762817922ada304d106e5a609a9ff972d0a249d648b4c0aba3729a25e9",- "size": 955822,+ "version": "2.0.259",+ "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.259/adom-bridge-puppeteer-v2.0.259.zip",+ "sha256": "61c2b327f109ef50e91bce8b0874a1e2a59093519cbe3468bb0cb090e504d83b",+ "size": 956135, "verbPrefixes": [ "browser_", "desktop_recorder_",@@ -12,5 +12,5 @@ ], "healthEndpoint": "/status", "statusVerb": "browser_readiness",- "released_at": "2026-08-20T11:46:50.888952Z"+ "released_at": "2026-08-20T12:27:31.602721Z" }
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-2.0.258+2.0.259
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "2.0.258",+ "version": "2.0.259", "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. pup_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. pup_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, pup_alert_window posts a native toast whose click focuses the window, pup_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-bridge", "author": "Adom Inc.",
src/server.js+96−112@@ -438,6 +438,46 @@ const _launchInFlight = new Map(); const _profileOpenChain = new Map(); // profileName -> tail promise let _tabClaimSeq = 0; // monotonic per-call marker for in-window tab adoption (see addTabToSession) let _birthSeq = 0;+// v2.0.259 (John's placement rules, R1): a window is CREATED at its final on-screen rect, so we need+// the primary monitor's work area BEFORE any page exists to ask (screen.availWidth comes from a page,+// which is the chicken-and-egg that pushed the old design into staging windows off-screen and moving+// them later). SystemParametersInfo(SPI_GETWORKAREA) answers without a window. Cached — monitors do+// not change often, and a stale value only costs a slightly-off rect that the park corrects.+let _workAreaCache = null; // { x, y, w, h, at }+async function primaryWorkArea() {+ if (_workAreaCache && Date.now() - _workAreaCache.at < 5 * 60000) return _workAreaCache;+ if (process.platform !== 'win32') return null;+ const ps = `$s=@'+using System; using System.Runtime.InteropServices;+public static class WA {+ [StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; }+ [DllImport("user32.dll")] public static extern bool SystemParametersInfo(uint a, uint b, ref RECT r, uint c);+}+'@; Add-Type -TypeDefinition $s+$r = New-Object WA+RECT+if([WA]::SystemParametersInfo(0x0030, 0, [ref]$r, 0)) { "{0},{1},{2},{3}" -f $r.L,$r.T,($r.R-$r.L),($r.B-$r.T) } else { "ERR" }`;+ return await new Promise((resolve) => {+ try {+ execFile('powershell', ['-NoProfile', '-Command', ps], { windowsHide: true, timeout: 10000 }, (err, stdout) => {+ const m = /^(-?\d+),(-?\d+),(\d+),(\d+)$/.exec(String(stdout || '').trim());+ if (!m) { console.log(`[workarea] could not read the primary work area (${err ? err.message.slice(0, 60) : String(stdout || '').trim().slice(0, 40)}) — birth will fall back to a conservative on-screen box`); return resolve(null); }+ const w = Number(m[3]), h = Number(m[4]);+ if (!(w > 400 && h > 300)) return resolve(null);+ _workAreaCache = { x: Number(m[1]), y: Number(m[2]), w, h, at: Date.now() };+ console.log(`[workarea] primary work area ${w}x${h} at ${m[1]},${m[2]}`);+ resolve(_workAreaCache);+ });+ } catch (e) { resolve(null); }+ });+}+// R1: the birth rect — the primary work area inset 10px on ALL FOUR sides. The inset is deliberate+// and visible: it is how you can tell at a glance that a window was PLACED by pup rather than landing+// somewhere by accident. Falls back to a conservative on-screen box, never off-screen.+async function birthRect() {+ const wa = await primaryWorkArea();+ if (wa) return { left: wa.x + 10, top: wa.y + 10, width: Math.max(400, wa.w - 20), height: Math.max(300, wa.h - 20) };+ return { left: 10, top: 10, width: 1280, height: 800 };+} // v2.0.38: SERIALIZE window creation so the set-diff birth capture always sees EXACTLY ONE new // window handle (position is unreliable — Chrome clamps our -32000 birth coord on-screen, so we // cannot identify the window by where it is; but "the one handle that appeared while I held the@@ -9060,19 +9100,11 @@ async function parkSessionWindowDirect(page, sessionId) { // job is to move the user's window where they cannot click it is a loaded gun in this file. // Z-order truth: our window's z must be strictly below at least one non-pup window, and it // must not be topmost. Returns true only on CONFIRMED not-on-top.- const zConfirmedBelow = async (targetHwnd) => {- try {- const r = await chrome.abCommand('desktop_list_windows', {}, { timeoutMs: 5000 });- const raw = r && (typeof r.output === 'string' && r.output ? JSON.parse(r.output) : r.output || r);- const ws = ((raw && (raw.data || raw)).windows) || [];- if (!ws.length) return false;- const mine = ws.find(w => Number(w.hwnd) === Number(targetHwnd));- if (!mine) return false;- if (Number(mine.z) === 0) return false;- // at least one NON-pup window above ours- return ws.some(w => Number(w.z) < Number(mine.z) && !/\((?:session|ai-thread): /.test(w.title || ''));- } catch (e) { return false; }- };+ // v2.0.259: zConfirmedBelow is GONE. It asked ab's window enumeration to PROVE our window sat+ // below a non-pup window, and placement was gated on that proof. Two fatal properties: it+ // returned false when our hwnd was simply absent from the enumeration (which an off-screen+ // window often is, so the failure was self-sustaining), and there is no correct answer at all+ // when the user has no other windows open. Nothing may gate placement on a proof like that. // hwnd via AD (native) — the title suffix "(session: <id>" is set by the welcome page. // v1.8.59 HARD RULES, learned the expensive way: // 1. The window is NEVER moved on-screen until z-bottom is CONFIRMED ('bottomed'). A@@ -9119,28 +9151,32 @@ async function parkSessionWindowDirect(page, sessionId) { return { ok: !!data.sentToBack, wasForeground: !!data.wasForeground, hwnd: data.hwnd || null, err: data.sentToBack ? null : 'not sent to back' }; } catch (e) { return { ok: false, err: e.message }; } };+ // ── v2.0.259: PARK IS NO LONGER A PLACEMENT GATE (John's rules R1-R3) ──────────────────────+ // The window is BORN at its final on-screen rect, so by the time we get here it is already+ // somewhere the user can click. Park's only remaining job is to ASSERT the background preference.+ // What changed, and why:+ // * Placement no longer waits on proving z-bottom. The old order (bottom -> prove -> only then+ // move on-screen) left the window at -32000 for up to 42 SECONDS (8 attempts x a 5s ab+ // timeout + sleeps) with a live taskbar button. John accepted a "~2s launch gap"; he never+ // agreed to that. R3 makes a working taskbar click a requirement, not a trade.+ // * Bottoming is best-effort and BOUNDED (3 quick attempts, not 8 slow ones). If it fails and+ // the window ends up foreground, R2 says accept it: "just allow that flaw to exist." We log+ // it and stop. Fighting it is what produced every re-park loop, and each one eventually+ // yanked a real user click.+ // * NOTHING here may move, hide, minimize or off-screen the window as a consequence of a+ // failed assertion. That is the whole lesson. let bottomed = false, lastErr = 'error', parkHwnd = null;- for (let attempt = 0; attempt < 8; attempt++) {+ for (let attempt = 0; attempt < 3; attempt++) { const z = await adBottom(); if (z.ok) { bottomed = true; parkHwnd = z.hwnd; break; } lastErr = z.err || 'error';- // Two cases need the legacy PS path (BOTH auto-obsolete as desktops update to AD ≥1.9.115,- // whose force:true is honored above — delete this block once the fleet is there):- // (a) older AD that rejects state:'bottom' entirely;- // (b) AD 1.9.114 exactly: has state:'bottom' but ignores force and SKIPS a foreground- // window (wasForeground:true, sentToBack:false). At LAUNCH, foreground is NOT a user- // signal (post-restart foreground reassignment — the v61-3 case), so force via PS.- if (z.wasForeground || /invalid|unknown|must be one of|maximize\|minimize/i.test(lastErr)) {+ if (z.wasForeground) {+ // The legacy PS path still matters on older ab (state:'bottom' absent, or force ignored). let hwnd = null;- try {- const r = await chrome.abCommand('desktop_find_window', { titleContains: `(${TITLE_LABEL}: ${sessionId}` }, { timeoutMs: 4000 , caller: sessionCaller(sessions.get(sessionId))});- const o = r && abPayload(r);- hwnd = (o && ((o.best && o.best.hwnd) || o.hwnd)) || null;- } catch (e) {}- {- const zr = hwnd ? await osBottomUnlessForeground(hwnd, true, sessions.get(sessionId)) : 'error';+ try { hwnd = await windowHwnd(sessions.get(sessionId), sessionId); } catch (e) {}+ if (hwnd) {+ const zr = await osBottomUnlessForeground(hwnd, true, sessions.get(sessionId)); if (zr === 'bottomed') { bottomed = true; parkHwnd = hwnd; break; }- // v2.0.26: the user raised it while we were parking — respect it, remember it, stop parking. if (zr === 'user-foreground') { const _us = sessions.get(sessionId); if (_us) _us._userActiveAt = Date.now(); console.log(`[park] "${sessionId}": user has this window FOREGROUND (recent input) — leaving it exactly where they put it`);@@ -9150,79 +9186,18 @@ async function parkSessionWindowDirect(page, sessionId) { } await new Promise(r2 => setTimeout(r2, 300)); }+ // Whatever happened above, make sure the geometry is right. This never gates on anything and it+ // never moves the window somewhere unreachable — worst case it is a no-op on an already-correct+ // window (the normal case now, since birth placed it).+ try { await placeOnScreen(); } catch (e) {}+ if (!parkHwnd) { try { parkHwnd = await windowHwnd(sessions.get(sessionId), sessionId); } catch (e) {} }+ if (parkHwnd) { try { const _s = sessions.get(sessionId); if (_s) { _s._hwnd = Number(parkHwnd); persistSessionHwnd(sessionId, Number(parkHwnd)); } } catch (e) {} } if (!bottomed) {- // v2.0.258 (John, twice in one day: "i can't fucking click it to open the pup window"). This- // used to LEAVE THE WINDOW OFF-SCREEN and call that the safe outcome. It is not safe: it is the- // one state that makes a taskbar button LIE, and "self-heal will retry" meant minutes of an- // unclickable window, or forever when the heal could not resolve a handle. The risk being- // avoided (we could not PROVE we are below the user's windows) is possible and transient; the- // harm being caused (the user cannot open their own window, ever) is certain and permanent.- // Never rest off-screen. Place it on-screen, bottom it best-effort, and say so honestly.- console.log(`[park] "${sessionId}": z-bottom NOT confirmed (${lastErr}) — placing ON-SCREEN anyway at best-effort bottom. An unconfirmed z-order may briefly sit above your work; a window you cannot click NEVER becomes clickable, so on-screen always wins.`);- await placeOnScreen();- await adBottom();+ // R2: background is the PREFERENCE, not a precondition. Say so plainly and move on.+ console.log(`[park] "${sessionId}": could not confirm z-bottom (${lastErr}) — the window is ON SCREEN and clickable, which is the requirement; background is only the preference. Not fighting it (John: "just allow that flaw to exist").`); return 'park-unconfirmed'; }- // On-screen full-size WITHOUT activation — AD MoveWindow preserves z-order (we're at bottom).- if (!parkHwnd) {- try {- const r = await chrome.abCommand('desktop_find_window', { titleContains: `(${TITLE_LABEL}: ${sessionId}` }, { timeoutMs: 4000 , caller: sessionCaller(sessions.get(sessionId))});- const o = r && abPayload(r);- parkHwnd = (o && ((o.best && o.best.hwnd) || o.hwnd)) || null;- } catch (e) {}- }- if (!parkHwnd) {- // v2.0.258: same rule — no handle is no excuse to leave a window where a click cannot find it.- // CDP placement needs no hwnd, so put it on-screen and let the sweeps re-bottom it later.- console.log(`[park] "${sessionId}": no hwnd for placement — placing ON-SCREEN via CDP anyway rather than leaving it where a taskbar click reveals nothing`);- await placeOnScreen();- return 'park-unconfirmed';- }- // v1.9.212 — STORE THE HWND ON THE SESSION. This is the fix John has been waiting for me to- // actually make instead of describing. Everything downstream (taskbar paint, park, resolution)- // has been finding windows by their "(session: <id>)" TITLE TAG, and Chrome's PDF viewer cannot- // carry that tag — so on a PDF window every one of those lookups failed: no badge painted,- // park-failed, and the reclaim janitor kept re-adopting the window as yet another session- // (five of them by now, all fighting over one taskbar button). A window handle is stable and- // title-independent, so hold onto it here where we have already resolved it.- try { const _s = sessions.get(sessionId); if (_s) { _s._hwnd = Number(parkHwnd); persistSessionHwnd(sessionId, Number(parkHwnd)); } } catch (e) {}- // Place on-screen full-size (CDP plain bounds — the only move that doesn't raise), then- // RE-BOTTOM (trust nothing) and CONFIRM the z-order. Unconfirmable → back off-screen.- const placed = await placeOnScreen();- if (!placed) {- // v2.0.258: the screen probe failing means we do not know the WORK AREA — it does not mean the- // window should stay somewhere the user can never click. Fall back to a conservative on-screen- // box; a later sweep re-places it to the real work area once the page answers.- console.log(`[park] "${sessionId}": screen-size probe failed — falling back to a conservative ON-SCREEN box (never rest off-screen; a taskbar click must always reveal something)`);- try {- await cdp.send('Browser.setWindowBounds', { windowId, bounds: { windowState: 'normal' } });- await cdp.send('Browser.setWindowBounds', { windowId, bounds: { left: 10, top: 10, width: 1280, height: 800 } });- } catch (e) {}- return 'park-unconfirmed';- }- let zOk = false;- for (let i = 0; i < 2 && !zOk; i++) {- await adBottom(); // re-assert after the move, every time- zOk = await zConfirmedBelow(parkHwnd);- }- if (!zOk) {- // v2.0.258: was `moveOffScreen()`. THIS is the line that stranded John's "ab ralph" window at- // -21845,-21845 with a live taskbar button he could not open. Worse, the confirmation it waits- // for is unreliable in a way that FEEDS this branch: zConfirmedBelow looks our hwnd up in ab's- // window enumeration and returns false when it is absent — and a window parked off-screen may- // not be enumerated at all, so once here, it can never confirm its way out.- console.log(`[park] "${sessionId}": z-order unconfirmed after placement — LEAVING IT ON-SCREEN at best-effort bottom (was: moved back off-screen, which is exactly how a taskbar button ends up revealing nothing).`);- await adBottom();- return 'park-unconfirmed';- }- // Verify POSITION too (a stuck-off-screen window must not read as parked).- let verified = false;- try {- const gb = await cdp.send('Browser.getWindowBounds', { windowId });- const b = gb && gb.bounds;- verified = !!(b && b.left > -1000 && b.top > -1000);- } catch (e) {}- return verified ? 'backgrounded' : 'park-failed';+ return 'backgrounded'; } catch (e) { console.error(`[park] ${sessionId}: ${e.message}`); return 'error';@@ -10184,7 +10159,14 @@ async function createPageInNewWindow(browser, profileName) { // same-profile window is unambiguous. After park it is maximized + shares the parked rect with its // siblings, which is why the width-wiggle could never tell them apart. Captured here, stashed on the // page, trusted by resolveSessionHwndByBounds. This is the fix for multi-window profiles (adom-you).- const birthLeft = -32000 - ((_birthSeq++ % 60) * 220); // v2.0.35: 220px apart (was 40) so set-diff's nearest-match can't cross-wire adjacent births+ // v2.0.259 (John's rule R1): the window is BORN where it will live — primary work area, 10px inset,+ // on screen, clickable from its first millisecond. It used to be born at -32000 purely so the+ // set-diff had a unique coordinate to match on, i.e. IDENTITY dictated PLACEMENT, and that is+ // precisely how windows ended up stranded off-screen behind live taskbar buttons. The lock-based+ // set-diff below never needed the coordinate (see the note on _birthLock: "the one handle that+ // appeared while I held the lock is unambiguous regardless of position"), so nothing is lost.+ _birthSeq++;+ const _br = await birthRect(); try { const before = new Set(await browser.pages()); cdp = await browser.target().createCDPSession();@@ -10209,7 +10191,7 @@ async function createPageInNewWindow(browser, profileName) { invalidateEnumCache(); // v2.0.187: a birth changes the desktop — never diff against a stale snapshot let beforeHwnds = new Set(); if (process.platform === 'win32') { try { beforeHwnds = new Set((await enumChromeWidgetWindows()).map(r => Number(r.h))); } catch (e) {} }- await cdp.send('Target.createTarget', { url: 'about:blank', newWindow: true, left: birthLeft, top: -32000, width: 1280, height: 800 });+ await cdp.send('Target.createTarget', { url: 'about:blank', newWindow: true, left: _br.left, top: _br.top, width: _br.width, height: _br.height }); for (let i = 0; i < 50 && !fresh; i++) { fresh = (await browser.pages()).find(p => !before.has(p)); if (!fresh) await new Promise(r => setTimeout(r, 100));@@ -10228,22 +10210,24 @@ async function createPageInNewWindow(browser, profileName) { && (!_bpid || Number(r.pid) === _bpid)); if (fresh2.length === 1) { capturedHwnd = fresh2[0].h; break; } if (fresh2.length > 1) {- // more than one appeared despite the lock (a sibling thread's stuck-off-screen leftover- // enumerated late): prefer the off-screen one, else the one nearest my birthLeft.- const off = fresh2.filter(r => r.T < -10000);- const pool = off.length ? off : fresh2;- let best = pool[0].h, bestErr = 1e9;- for (const r of pool) for (const sc of [1, 1.25, 1.5, 1.75, 2]) { const e = Math.abs(r.L - Math.round(birthLeft * sc)); if (e < bestErr) { bestErr = e; best = r.h; } }- capturedHwnd = best; break;+ // v2.0.259: more than one appeared despite the lock. The old tie-break matched against the+ // unique off-screen birth coordinate; every window is now born at the SAME on-screen rect,+ // so coordinates cannot break this tie and pretending otherwise would silently mis-assign+ // a handle. R4 says identity must be EXACT, and a wrong hwnd is far worse than a late one+ // (it paints another window's badge and parks the wrong window), so: take none, log loudly,+ // and let the normal resolver settle it once the windows differentiate.+ console.log(`[hwnd] birth capture on "${profileName}" saw ${fresh2.length} new windows under the birth lock (${fresh2.map(r => r.h).join(', ')}) — REFUSING to guess which is mine; the resolver will settle it. If this recurs, the birth lock is not holding.`);+ break; } await new Promise(r => setTimeout(r, 600)); } } });- // Resolve the OS handle NOW, by the unique off-screen birth position, before park moves it on-- // screen. Parked siblings are at positive L; only newborns sit far off-screen, so "the off-screen- // window of this profile at ~birthLeft" is unambiguous. Stash it on the page for the resolver.- if (fresh && capturedHwnd) { fresh.__pupHwnd = capturedHwnd; console.log(`[hwnd] birth-resolved new window on "${profileName}" -> ${capturedHwnd} (locked set-diff, birthLeft ${birthLeft})`); }+ // R4: the handle is resolved in the SAME LOCKED MOMENT the window was created — the one new+ // top-level window owned by THIS profile's Chrome process. No coordinate involved, so it stays+ // exact even though every window is now born at the identical on-screen rect. Stashed on the page+ // for the resolver, which trusts it above everything else.+ if (fresh && capturedHwnd) { fresh.__pupHwnd = capturedHwnd; console.log(`[hwnd] birth-resolved new window on "${profileName}" -> ${capturedHwnd} (locked set-diff; born on-screen at ${_br.left},${_br.top} ${_br.width}x${_br.height})`); } else if (fresh) console.log(`[hwnd] birth capture on "${profileName}" produced no handle — will resolve later`); try { const nowFg = await osGetForegroundWindow();