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.50: event-driven background watchdog replaces blind timers; user taskbar clicks always stick; response awaits self-verified park verdict
5 files changed
+159−25
CHANGELOG.md+12@@ -1,5 +1,17 @@ # Changelog — Adom Desktop Puppeteer Bridge +## 1.8.50 — 2026-07-14 — One event-driven watchdog replaces the blind-timer park (John: "waiting 1.2s is for the birds")++The launch park no longer fires at guessed delays. `osBackgroundWatchdog` = ONE hidden PS process:+1. WAIT for the real window to EXIST (EnumWindows @50ms, up to 4s) — react to the event, not a timer.+2. The moment it exists: hand focus back (only if Chrome actually took it), THEN park on-screen at+ z-bottom (deactivate-first = no pop). Once.+3. Stability watch: if Chrome re-grabs the foreground during startup, hand back + re-park within 50ms;+ exit after 500ms of calm (typically <1s total).+4. Exit. Afterwards NOTHING re-parks — a user's taskbar click brings the window up full-size, forever.+The open response awaits the watchdog's SELF-VERIFIED verdict, so `background` is the real outcome,+never a guess mid-park.+ ## 1.8.49 — 2026-07-14 — Kill the input-timing heuristic (it false-fired whenever the user was typing) Opening windows while John was actively using the laptop: 2 of 3 came up `user-foreground`, small
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.8.49",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.49/adom-bridge-puppeteer-v1.8.49.zip",- "sha256": "b9e59ce594a9098585299570a362608e15a4e0ce8e6424b186566ce462c3c1e1",- "size": 164981,+ "version": "1.8.50",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.50/adom-bridge-puppeteer-v1.8.50.zip",+ "sha256": "8c37b03c862439569ef05a4d0e4dbb7d17303a72fc67df93b90bb0f6e9a18424",+ "size": 166222, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.8.49+1.8.50
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.8.49",+ "version": "1.8.50", "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+141−19@@ -17,6 +17,15 @@ function runPsHidden(scriptPath, { timeout = 6000 } = {}) { function runPsHiddenAsync(scriptPath, { timeout = 6000 } = {}) { execFile('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', scriptPath], { timeout, windowsHide: true }, () => {}); }+// Promise variant: resolves with stdout (trimmed) or '' on error. For PS whose RESULT matters+// but which must not block the event loop (e.g. the background watchdog).+function runPsHiddenPromise(scriptPath, { timeout = 8000 } = {}) {+ return new Promise(resolve => {+ execFile('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-WindowStyle', 'Hidden', '-File', scriptPath], { encoding: 'utf8', timeout, windowsHide: true }, (err, stdout) => {+ resolve((stdout || '').trim());+ });+ });+} const puppeteer = require('puppeteer'); const credentialVault = require('./credential_vault'); const chrome = require('./chrome'); // Chrome-for-Testing self-heal: detect / auto-install / readiness (cold-start fix)@@ -1686,6 +1695,122 @@ function osBackgroundWindowByPid(pid, cdpPort, restoreHwnd, respectUser) { } catch (e) { console.error(`[bg] pid ${pid} port ${cdpPort}: ${e.message}`); return 'error'; } } +// v1.8.50: THE background algorithm — ONE event-driven watchdog, no blind timers.+// Replaces the "park + re-park at 300/700/1200ms" kludge. What it does, in order:+// 1. WAIT for the real browser window to EXIST (poll EnumWindows @50ms, up to ~4s — reacting+// to the event "window created", not guessing a delay). The window is OFF-SCREEN, invisible.+// 2. The moment it exists: hand keyboard focus back to the user's window (AttachThreadInput+// dance — only if Chrome actually took the foreground), THEN move it on-screen at z-bottom+// (deactivate-first order = no visible pop). ONE park.+// 3. STABILITY WATCH: for up to 2s, check the foreground @50ms. If Chrome re-grabs it+// (startup self-raise), hand back + re-park INSTANTLY (50ms reaction, not a 400ms timer+// gap). Exit as soon as the foreground has been non-pup for 500ms straight — typically+// well under a second after first paint.+// 4. EXIT. After this process ends, NOTHING ever re-parks the window. A user click on the+// taskbar button brings it up full-size and pup never touches it again. No input-timing+// heuristics anywhere (GetLastInputInfo false-fired while the user typed — removed).+// Returns: 'backgrounded' | 'still-foreground' | 'no-window' | 'error' (self-verified).+async function osBackgroundWatchdog(pid, cdpPort, restoreHwnd) {+ if (process.platform !== 'win32' || (!pid && !cdpPort)) return 'error';+ try {+ const port = Number(cdpPort) || 0;+ const restore = (restoreHwnd && /^-?\d+$/.test(String(restoreHwnd))) ? String(restoreHwnd) : '0';+ const scriptPath = path.join(require('os').tmpdir(), `_adom_bgwd_${pid || port}.ps1`);+ const ps = [+ 'Add-Type @"',+ 'using System; using System.Runtime.InteropServices; using System.Text;',+ 'public class WinWd {',+ ' public delegate bool EnumProc(IntPtr h, IntPtr l);',+ ' [DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, IntPtr l);',+ ' [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);',+ ' [DllImport("user32.dll")] public static extern int GetClassName(IntPtr h, StringBuilder s, int m);',+ ' public struct RECT { public int L; public int T; public int R; public int B; }',+ ' [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);',+ ' [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr h, IntPtr a, int x, int y, int cx, int cy, uint f);',+ ' [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();',+ ' [DllImport("user32.dll")] public static extern int GetSystemMetrics(int i);',+ ' [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);',+ ' [DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a, uint b, bool f);',+ ' [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);',+ ' [DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h);',+ ' [DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();',+ ' public static IntPtr BestWindow(int[] pids) {',+ ' IntPtr best = IntPtr.Zero; long bestArea = -1;',+ ' EnumWindows(delegate(IntPtr h, IntPtr l) {',+ ' if (!IsWindowVisible(h)) return true;',+ ' uint wp; GetWindowThreadProcessId(h, out wp);',+ ' bool m = false; foreach (int p in pids) { if ((uint)p == wp) { m = true; break; } }',+ ' if (!m) return true;',+ ' StringBuilder sb = new StringBuilder(256); GetClassName(h, sb, 256);',+ ' if (sb.ToString() != "Chrome_WidgetWin_1") return true;',+ ' RECT r; GetWindowRect(h, out r); long a = (long)(r.R - r.L) * (r.B - r.T);',+ ' if (a > bestArea) { bestArea = a; best = h; }',+ ' return true;',+ ' }, IntPtr.Zero);',+ ' return best;',+ ' }',+ '}',+ '"@',+ `$targetPid = ${Number(pid) || 0}`,+ `$cdpPort = ${port}`,+ `$restore = [IntPtr]${restore}`,+ '$BOTTOM = [IntPtr]1',+ '$SWP = 0x10', // SWP_NOACTIVATE+ '$scrW = [WinWd]::GetSystemMetrics(0)',+ '$scrH = [WinWd]::GetSystemMetrics(1)',+ '$myTid = [WinWd]::GetCurrentThreadId()',+ // Focus handback: only acts when pup IS the foreground (Chrome stole it); else a no-op.+ 'function HandBack([IntPtr]$pup) {',+ ' $fg = [WinWd]::GetForegroundWindow()',+ ' if ($fg -eq $pup -and $restore -ne [IntPtr]::Zero) {',+ ' $procId = [uint32]0',+ ' $pupTid = [WinWd]::GetWindowThreadProcessId($fg, [ref]$procId)',+ ' [WinWd]::AttachThreadInput($pupTid, $myTid, $true) | Out-Null',+ ' [WinWd]::SetForegroundWindow($restore) | Out-Null',+ ' [WinWd]::BringWindowToTop($restore) | Out-Null',+ ' [WinWd]::AttachThreadInput($pupTid, $myTid, $false) | Out-Null',+ ' }',+ '}',+ // 1. Wait for the window to EXIST (the event, not a guessed delay). Re-resolve the pid+ // list every ~500ms (child browser pids can appear late).+ '$pup = [IntPtr]::Zero',+ '$deadline = [Environment]::TickCount + 4000',+ 'while ([Environment]::TickCount -lt $deadline) {',+ ' $pidList = New-Object System.Collections.ArrayList',+ ' if ($targetPid -ne 0) { [void]$pidList.Add([int]$targetPid) }',+ ' if ($cdpPort -ne 0) { try { $own = Get-NetTCPConnection -LocalPort $cdpPort -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty OwningProcess; if ($own) { [void]$pidList.Add([int]$own) } } catch {} }',+ ' $anchors = @($pidList.ToArray())',+ ' foreach ($tp in $anchors) { Get-CimInstance Win32_Process -Filter "ParentProcessId=$tp" -ErrorAction SilentlyContinue | Where-Object { $_.Name -in @("chrome.exe","msedge.exe") } | ForEach-Object { [void]$pidList.Add([int]$_.ProcessId) } }',+ ' $pup = [WinWd]::BestWindow([int[]]@($pidList.ToArray()))',+ ' if ($pup -ne [IntPtr]::Zero) { break }',+ ' Start-Sleep -Milliseconds 50',+ '}',+ 'if ($pup -eq [IntPtr]::Zero) { Write-Output "no-window"; exit }',+ // 2. Deactivate FIRST (while still off-screen), THEN park on-screen at z-bottom.+ 'HandBack $pup',+ '[WinWd]::SetWindowPos($pup, $BOTTOM, 0, 0, $scrW, $scrH, $SWP) | Out-Null',+ // 3. Stability watch: react to Chrome re-grabs within 50ms; exit after 500ms of calm.+ '$calmSince = [Environment]::TickCount',+ '$watchEnd = [Environment]::TickCount + 2000',+ 'while ([Environment]::TickCount -lt $watchEnd) {',+ ' $fg = [WinWd]::GetForegroundWindow()',+ ' if ($fg -eq $pup) {',+ ' HandBack $pup',+ ' [WinWd]::SetWindowPos($pup, $BOTTOM, 0, 0, $scrW, $scrH, $SWP) | Out-Null',+ ' $calmSince = [Environment]::TickCount',+ ' } elseif (([Environment]::TickCount - $calmSince) -ge 500) { break }',+ ' Start-Sleep -Milliseconds 50',+ '}',+ // 4. Self-verify and exit. Nothing runs after this — the user owns the window now.+ '$fg2 = [WinWd]::GetForegroundWindow()',+ 'if ($fg2 -eq $pup) { Write-Output "still-foreground" } else { Write-Output "backgrounded" }',+ ].join('\r\n');+ fs.writeFileSync(scriptPath, ps);+ const out = await runPsHiddenPromise(scriptPath, { timeout: 9000 });+ return out || 'error';+ } catch (e) { console.error(`[bgwd] pid ${pid} port ${cdpPort}: ${e.message}`); return 'error'; }+}+ // v1.8.47: THE background primitive, rebuilt around MINIMIZE (replaces the on-screen-z-bottom // park + its re-park retry loop, which fought the user's clicks). Why minimize is correct: // • Minimizing a foreground window hands the foreground back to the user's window AUTOMATICALLY@@ -2266,25 +2391,15 @@ 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.49: background = ON-SCREEN at z-bottom (occluded, still renders → screenshots- // work; minimize was tried and REJECTED — visibilityState 'hidden' hangs- // Page.captureScreenshot ~27s even with occlusion disabled). The park deactivates FIRST- // then moves on-screen behind everything (no pop), using the EnumWindows handle (the- // real window, not a phantom). The launch passes ALWAYS park, no user detection: within- // ~1.2s of launch the taskbar button barely exists, so a foreground pup window is- // Chrome's startup self-raise, not the user — and input-timing heuristics false-fire- // whenever the user is typing anywhere (the 1.8.44-48 bug that mis-sized fresh windows).- // After this window ends, pup NEVER auto-re-parks, so a user click is never fought.- b._bgResult = osBackgroundWindowByPid(child.pid, debugPort, priorForeground);- console.log(`[bg] initial park for "${profileName}": ${b._bgResult}`);- for (const delay of [300, 700, 1200]) {- setTimeout(() => {- try {- const r = osBackgroundWindowByPid(child.pid, debugPort, priorForeground);- if (r && r !== 'no-window') b._bgResult = r;- } catch (e) {}- }, delay);- }+ // 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.+ 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')); } return b; };@@ -3340,6 +3455,13 @@ const server = http.createServer(async (req, res) => { // Stash on the session so attachTab uses it for any subsequent // tab open or popup auto-attach in this session. if (session) session._downloadPath = downloadPath;+ // v1.8.50: the background watchdog runs concurrently with navigation. Await its+ // SELF-VERIFIED verdict (bounded) so the response's `background` field is the real+ // outcome ('backgrounded'/'still-foreground'), never a guess mid-park.+ try {+ const bp = session && browsers.get(session.profileName)?.browser?._bgPromise;+ if (bp) await Promise.race([bp, new Promise(r => setTimeout(r, 7000))]);+ } catch (e) {} const info = await getSessionInfo(sessionId, session); // v1.8.0: BACKGROUND BY DEFAULT. pup is the AI's workspace, not the user's — // a window that pops to the foreground and covers what the user is doing is