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.53: direct-API park via CDP windowId and AD find_window; position-verified; instant badge; PS reduced to one z-bottom call
5 files changed
+148−18
CHANGELOG.md+22@@ -1,5 +1,27 @@ # Changelog — Adom Desktop Puppeteer Bridge +## 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+"backgrounded" — the PS park moved the wrong window and its focus-only self-check passed anyway.+And he asked the right question: "why are you using powershell? that's heavy. why aren't you just+calling direct api's?" Rebuilt the park on direct APIs:++- **Geometry via CDP `Browser.setWindowBounds`** — addresses the EXACT window by CDP windowId, so+ parking the wrong window is impossible. ~10ms, no process spawn.+- **Window lookup via AD `desktop_find_window`** (native, title-suffix match) — no EnumWindows PS,+ no pid guessing.+- **The park runs where the window is BORN** (right after `newPage()` + welcome page in+ launchSession) — no wait loop at all; the `await` IS the event. `tryDetachedLaunch` just marks+ `_needsPark` (reused windows are never re-parked).+- **Verification checks POSITION** (CDP `getWindowBounds`), not just focus — the exact lie catch-2+ told is now detected as `park-failed`.+- **One residual Win32 call**: z-bottom (`osSendHwndToBottom`, single P/Invoke) — CDP has no+ z-order and AD has no send-to-back verb yet (AD issue filed). Runs while the window is still+ off-screen so its latency is invisible.+- **Badge fires the instant the park lands** (title suffix already set) instead of blind+ 1.5s/3.5s timers — the teal Adom favicon appears with the window, not seconds later.+ ## 1.8.52 — 2026-07-14 — Close the launch gap: a click during the first ~2s now shows the window With 1.8.51, a taskbar click during the launch gap (window still at its off-screen coords, park PS
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.8.52",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.52/adom-bridge-puppeteer-v1.8.52.zip",- "sha256": "cd1726eee1858edccc62c7f154d85efa7cb5d63436cabcc4a5bba83181c570b0",- "size": 166799,+ "version": "1.8.53",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.53/adom-bridge-puppeteer-v1.8.53.zip",+ "sha256": "0393446988db76888ec3e3e6ffb77b53f1ada772b3f66ec55ccf8a3c7dd6b19d",+ "size": 168311, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.8.52+1.8.53
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.8.52",+ "version": "1.8.53", "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+120−12@@ -1921,6 +1921,93 @@ function osMinimizeToBackground(pid, cdpPort, respectUser) { } catch (e) { console.error(`[min] pid ${pid} port ${cdpPort}: ${e.message}`); return 'error'; } } +// v1.8.53: send ONE hwnd to the bottom of the z-order. This is the ONLY Win32 call left with+// no direct API (CDP has no z-order; AD has no send-to-back verb yet — issue filed). Minimal+// PS: one P/Invoke signature, no window-finding, no pid guessing, hwnd comes from AD's native+// desktop_find_window. Everything else in the park is CDP/AD direct.+function osSendHwndToBottom(hwnd) {+ if (process.platform !== 'win32' || !hwnd) return false;+ try {+ const scriptPath = path.join(require('os').tmpdir(), `_adom_zbot_${hwnd}.ps1`);+ const ps = [+ 'Add-Type @"',+ 'using System; using System.Runtime.InteropServices;',+ 'public class ZBot { [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr h, IntPtr a, int x, int y, int cx, int cy, uint f); }',+ '"@',+ `[ZBot]::SetWindowPos([IntPtr]${Number(hwnd)}, [IntPtr]1, 0, 0, 0, 0, 0x13) | Out-Null`, // HWND_BOTTOM; SWP_NOSIZE|NOMOVE|NOACTIVATE+ ].join('\r\n');+ fs.writeFileSync(scriptPath, ps);+ runPsHidden(scriptPath, { timeout: 5000 });+ return true;+ } catch (e) { return false; }+}++// v1.8.53: THE park — direct APIs end to end (John: "why powershell? that's heavy. why aren't+// you just calling direct api's?"). No EnumWindows, no pid guessing, no focus handback (with+// --no-startup-window nothing can steal focus), no wait loop (called AFTER newPage resolved, so+// the window exists — the await IS the event). Steps:+// 1. Escape hatch: CDP `document.hasFocus()` — can ONLY be true if the USER clicked the+// taskbar during the launch gap (Chrome can't self-focus anymore) → move on-screen, leave+// them on it, report 'user-foreground'.+// 2. Z-bottom while still OFF-SCREEN (invisible): hwnd via AD desktop_find_window (native,+// title-suffix match), then osSendHwndToBottom (the one residual Win32 call).+// 3. Re-check focus (user may have clicked mid-park) → if so, on-screen + AD bring_to_front.+// 4. CDP Browser.setWindowBounds → on-screen full-size. CDP addresses the EXACT window by+// windowId, so the catch-2 failure mode (parked the wrong window) is impossible.+// 5. Verify POSITION via CDP getWindowBounds (catch-2's park "verified" by focus alone and+// lied) → 'backgrounded' | 'park-failed'.+async function parkSessionWindowDirect(page, sessionId) {+ if (process.platform !== 'win32') return null;+ let cdp = null;+ try {+ const hasFocus = async () => { try { return await page.evaluate(() => document.hasFocus()); } catch (e) { return false; } };+ cdp = await page.target().createCDPSession();+ const { windowId } = await cdp.send('Browser.getWindowForTarget');+ let scr = { w: 1280, h: 800 };+ try { scr = await page.evaluate(() => ({ w: screen.availWidth, h: screen.availHeight })); } catch (e) {}+ const onScreenBounds = { left: 0, top: 0, width: scr.w, height: scr.h };+ if (await hasFocus()) {+ await cdp.send('Browser.setWindowBounds', { windowId, bounds: { windowState: 'normal' } });+ await cdp.send('Browser.setWindowBounds', { windowId, bounds: onScreenBounds });+ return 'user-foreground';+ }+ // hwnd via AD (native) — the title suffix "(session: <id>" is set by the welcome page.+ let hwnd = null;+ for (let i = 0; i < 4 && !hwnd; i++) {+ try {+ const r = await chrome.adCommand('desktop_find_window', { titleContains: `(session: ${sessionId}` }, { timeoutMs: 4000 });+ const o = r && (typeof r.output === 'string' ? JSON.parse(r.output) : r.output || r);+ hwnd = (o && ((o.best && o.best.hwnd) || o.hwnd)) || null;+ } catch (e) {}+ if (!hwnd) await new Promise(r2 => setTimeout(r2, 250));+ }+ if (hwnd) osSendHwndToBottom(hwnd);+ if (await hasFocus()) {+ // User clicked while we were parking — give them the window, foregrounded, on-screen.+ await cdp.send('Browser.setWindowBounds', { windowId, bounds: { windowState: 'normal' } });+ await cdp.send('Browser.setWindowBounds', { windowId, bounds: onScreenBounds });+ if (hwnd) { try { await chrome.adCommand('desktop_bring_to_front', { hwnd }, { timeoutMs: 3000 }); } catch (e) {} }+ return 'user-foreground';+ }+ // On-screen full-size WITHOUT activation — CDP move preserves z-order (we're at bottom).+ await cdp.send('Browser.setWindowBounds', { windowId, bounds: { windowState: 'normal' } });+ await cdp.send('Browser.setWindowBounds', { windowId, bounds: onScreenBounds });+ // Verify POSITION, not just focus.+ let verified = false;+ try {+ const gb = await cdp.send('Browser.getWindowBounds', { windowId });+ const b = gb && gb.bounds;+ verified = !!(b && b.left > -5000 && b.top > -5000);+ } catch (e) {}+ return verified ? 'backgrounded' : 'park-failed';+ } catch (e) {+ console.error(`[park] ${sessionId}: ${e.message}`);+ return 'error';+ } finally {+ if (cdp) { try { await cdp.detach(); } catch (e) {} }+ }+}+ // 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@@ -2405,15 +2492,11 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer // can re-raise itself during startup (first paint, restore prompts). Resolve the // window via the CDP-port owner (reliable across launcher→browser pid relaunches). if (!foreground && process.platform === 'win32') {- // v1.8.50: ONE event-driven watchdog (see osBackgroundWatchdog) — waits for the window- // to EXIST, parks it once (deactivate-first, on-screen z-bottom so screenshots keep- // working), reacts to Chrome startup re-grabs within 50ms, exits after 500ms of calm.- // After it exits NOTHING re-parks — a user's taskbar click brings the window up- // full-size and pup never touches it again. No blind timers, no input heuristics.+ // v1.8.53: with --no-startup-window there is NO window yet (it's created by newPage()+ // in launchSession) — so the park happens THERE, via parkSessionWindowDirect (CDP ++ // AD direct APIs, no PS window-hunting). Just mark that this fresh browser needs it. b._bgResult = 'parking';- b._bgPromise = osBackgroundWatchdog(child.pid, debugPort, priorForeground)- .then(r => { b._bgResult = r; console.log(`[bgwd] "${profileName}": ${r}`); return r; })- .catch(() => (b._bgResult = 'error'));+ b._needsPark = true; } return b; };@@ -2638,6 +2721,31 @@ async function launchSession(sessionId, url, { freshProfile = false, profile, st console.log(`Welcome page failed to load (non-fatal): ${e.message}`); } + // v1.8.53: PARK NOW — the window exists (newPage resolved) and the welcome page just set+ // the "(session: <id>)" title suffix. Direct APIs (CDP geometry + AD find_window); the open+ // handler awaits _bgPromise so the response carries the real, position-verified verdict.+ // Only for a freshly launched browser (_needsPark) — a reused window is already placed, and+ // re-parking it could hide a window the user has up.+ if (!foreground && process.platform === 'win32') {+ try {+ const be = browsers.get(profileName);+ if (be && be.browser && be.browser._needsPark) {+ be.browser._needsPark = false;+ be.browser._bgResult = 'parking';+ be.browser._bgPromise = parkSessionWindowDirect(page, sessionId)+ .then(r => {+ be.browser._bgResult = r;+ console.log(`[park] "${sessionId}": ${r}`);+ // v1.8.53: badge IMMEDIATELY after the park (the title suffix already exists —+ // no more blind 1.5s/3.5s waits John noticed) + one safety retry.+ badgePupTaskbar(sessionId).catch(() => {});+ setTimeout(() => { badgePupTaskbar(sessionId).catch(() => {}); }, 1200);+ return r;+ })+ .catch(e => (be.browser._bgResult = 'error'));+ }+ } catch (e) {}+ } // v1.8.36: SNAPPY auto-flash. The window already exists + is backgrounded here (the // welcome page loaded from a local file), so flash the taskbar NOW — do not wait for the // real URL's domcontentloaded, which for heavy sites is another 1–3s. Set _flashState so@@ -3522,11 +3630,11 @@ const server = http.createServer(async (req, res) => { recordAgentUpdate(session, 'browser_open_window', args, session.activeTabId); if (!foreground) scheduleAutoFlash(session, { silent: !!args.silent }); }- // v1.8.40: badge the taskbar button with the Adom leaves (once the window + its- // "(session: id)" title suffix exist and AD can enumerate it). Fire-and-forget +- // one retry, via AD-core desktop_taskbar. Never blocks the open response.+ // v1.8.53: badge timing moved into the park (launchSession) — it fires the moment the+ // park verdict lands instead of blind 1.5s/3.5s timers. This is only a fallback for+ // the REUSED-window path (no park runs there, but the badge may have been cleared). if (process.platform === 'win32') {- for (const d of [1500, 3500]) setTimeout(() => { badgePupTaskbar(sessionId).catch(() => {}); }, d);+ setTimeout(() => { badgePupTaskbar(sessionId).catch(() => {}); }, 300); } // Verbose browser report: which browser we actually drove, what else is available. const brep = browserPickReport();