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
v1.8.34: AttachThreadInput focus hand-back — deterministically returns keyboard focus to the user's window after launch (no more keystroke steal); no minimize (no flicker)
4 files changed
+69−32
adom-bridge-puppeteer-manifest.json+5−5@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.8.33",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.33/adom-bridge-puppeteer-v1.8.33.zip",- "sha256": "3bfed9b90c122a3f5259a84699aa4389079a6f4c4374ae915f2ccf88730709e7",- "size": 156427,+ "version": "1.8.34",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.34/adom-bridge-puppeteer-v1.8.34.zip",+ "sha256": "70d25fb8e42555c176b906d9891b34d44b8e5c035cbd8ee0b8f3a7769e9efe4f",+ "size": 157011, "verbPrefixes": [ "browser_", "desktop_recorder_",@@ -12,5 +12,5 @@ ], "healthEndpoint": "/status", "statusVerb": "browser_readiness",- "released_at": "2026-07-14T19:54:32Z"+ "released_at": "2026-07-14T20:05:21Z" }
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.8.33+1.8.34
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.8.33",+ "version": "1.8.34", "description": "Drives a browser ALREADY on the machine — installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) — 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 → Edge → 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, …). 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 — 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 — a separate bridge, not pup's job.", "homepage": "https://wiki.adom.inc/adom/adom-desktop", "author": "Adom Inc.",
src/server.js+62−25@@ -1501,6 +1501,25 @@ function osSendWindowToBack(profileName) { } catch (e) { console.error(`[bg] send-to-back failed for "${profileName}": ${e.message}`); return false; } } +// v1.8.34: capture the user's CURRENT foreground window (decimal HWND string) right+// before we launch a pup browser, so the backgrounder can hand keyboard focus back to it.+function osGetForegroundWindow() {+ if (process.platform !== 'win32') return null;+ try {+ const scriptPath = path.join(require('os').tmpdir(), `_adom_fg_${process.pid}.ps1`);+ const ps = [+ 'Add-Type @"',+ 'using System; using System.Runtime.InteropServices;',+ 'public class WinFgG { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); }',+ '"@',+ 'Write-Output ([int64][WinFgG]::GetForegroundWindow())',+ ].join('\r\n');+ fs.writeFileSync(scriptPath, ps);+ const out = runPsHidden(scriptPath, { timeout: 4000 }).trim();+ return out && out !== '0' ? out : null;+ } catch (e) { return null; }+}+ // v1.8.26: THE real "background by default" enforcement, done RIGHT. A freshly-launched // --start-maximized browser grabs foreground; osSendWindowToBack only lowers z-order and // SetForegroundWindow(userWindow) is BLOCKED by Windows' foreground lock — so the earlier@@ -1513,31 +1532,33 @@ function osSendWindowToBack(profileName) { // foreground window after, so callers (and tests) know for CERTAIN whether it worked. // cdpPort (the browser's CDP debug port) resolves the real browser via its LISTENING // process — reliable across the launcher→browser pid relaunch that Edge/Chrome do.-function osBackgroundWindowByPid(pid, cdpPort) {+function osBackgroundWindowByPid(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_bgpid_${pid || port}.ps1`); const ps = [ 'Add-Type @"', 'using System; using System.Runtime.InteropServices;', 'public class WinBgP {', ' [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 bool ShowWindow(IntPtr h, int n);', ' [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();', ' [DllImport("user32.dll")] public static extern int GetSystemMetrics(int i);',- ' [DllImport("user32.dll")] public static extern int GetWindowLong(IntPtr h, int i);',- ' [DllImport("user32.dll")] public static extern int SetWindowLong(IntPtr h, int i, int v);',+ ' [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();', '}', '"@', `$targetPid = ${Number(pid) || 0}`, `$cdpPort = ${port}`,+ `$restore = [IntPtr]${restore}`, '$BOTTOM = [IntPtr]1', // HWND_BOTTOM '$SWP = 0x10', // SWP_NOACTIVATE (allow move+size) '$scrW = [WinBgP]::GetSystemMetrics(0)', // SM_CXSCREEN (primary monitor) '$scrH = [WinBgP]::GetSystemMetrics(1)', // SM_CYSCREEN- '$SW_MINIMIZE = 6',- '$SW_SHOWNOACTIVATE = 4', '$handles = New-Object System.Collections.ArrayList', '$pids = New-Object System.Collections.ArrayList', 'if ($targetPid -ne 0) { [void]$pids.Add($targetPid) }',@@ -1550,21 +1571,31 @@ function osBackgroundWindowByPid(pid, cdpPort) { ' Get-CimInstance Win32_Process -Filter "ParentProcessId=$tp" -ErrorAction SilentlyContinue | Where-Object { $_.Name -in @("chrome.exe","msedge.exe") } | ForEach-Object { $cp = Get-Process -Id $_.ProcessId -ErrorAction SilentlyContinue; if ($cp -and $cp.MainWindowHandle -ne 0) { [void]$handles.Add($cp.MainWindowHandle) } }', '}', 'if ($handles.Count -eq 0) { Write-Output "no-window"; exit }',- // The window launched OFF-SCREEN (so nothing ever popped up on the user's monitor).- // Now: minimize to drop any foreground it grabbed (Windows hands focus back to the- // user), re-show WITHOUT activating (minimized windows stop compositing → screenshots- // time out, so it must stay shown), then MOVE it ON-SCREEN sized to the monitor at the- // BOTTOM of the z-order. Result: fully behind the user's windows (invisible, never- // covering) BUT reachable — its taskbar button brings it up on click, like any window.- 'foreach($h in $handles){',- ' [WinBgP]::ShowWindow($h, $SW_MINIMIZE) | Out-Null',- ' [WinBgP]::ShowWindow($h, $SW_SHOWNOACTIVATE) | Out-Null',- ' [WinBgP]::SetWindowPos($h, $BOTTOM, 0, 0, $scrW, $scrH, $SWP) | Out-Null',- '}',- // Verify: is any pup window STILL the foreground window?+ // The window launched OFF-SCREEN (nothing popped up). Move it ON-SCREEN, sized to the+ // monitor, at the BOTTOM of the z-order: fully behind the user's windows (invisible,+ // never covering) but reachable via its taskbar button. No minimize (that flickers).+ 'foreach($h in $handles){ [WinBgP]::SetWindowPos($h, $BOTTOM, 0, 0, $scrW, $scrH, $SWP) | Out-Null }',+ // The browser grabbed keyboard focus on launch. Hand it straight back to the user's+ // window. SetForegroundWindow is normally BLOCKED by the foreground lock, so we use the+ // AttachThreadInput trick: attach the (current-foreground = pup) window's input thread+ // to ours, which lets SetForegroundWindow succeed, then detach. This is what makes+ // background-by-default NOT steal keystrokes. '$fg = [WinBgP]::GetForegroundWindow()',+ '$isPup = $false',+ 'foreach($h in $handles){ if($h -eq $fg){ $isPup = $true } }',+ 'if ($isPup -and $restore -ne [IntPtr]::Zero) {',+ ' $procId = [uint32]0',+ ' $pupTid = [WinBgP]::GetWindowThreadProcessId($fg, [ref]$procId)',+ ' $myTid = [WinBgP]::GetCurrentThreadId()',+ ' [WinBgP]::AttachThreadInput($pupTid, $myTid, $true) | Out-Null',+ ' [WinBgP]::SetForegroundWindow($restore) | Out-Null',+ ' [WinBgP]::BringWindowToTop($restore) | Out-Null',+ ' [WinBgP]::AttachThreadInput($pupTid, $myTid, $false) | Out-Null',+ '}',+ // Re-verify after the hand-back.+ '$fg2 = [WinBgP]::GetForegroundWindow()', '$stillFg = $false',- 'foreach($h in $handles){ if($h -eq $fg){ $stillFg = $true } }',+ 'foreach($h in $handles){ if($h -eq $fg2){ $stillFg = $true } }', 'if ($stillFg) { Write-Output "still-foreground" } else { Write-Output "backgrounded" }', ].join('\r\n'); fs.writeFileSync(scriptPath, ps);@@ -1882,6 +1913,9 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer ...(foreground ? [] : ['--window-position=-32000,-32000', '--window-size=1280,800']), ]; console.log(`Chrome launch attempt ${attemptNum} for profile "${profileName}" on CDP port ${debugPort}`);+ // v1.8.34: snapshot the user's foreground window right before launch (background only)+ // so we can hand keyboard focus back to it after the browser grabs it.+ const priorForeground = foreground ? null : osGetForegroundWindow(); const child = spawn(chromePath, chromeArgs, { detached: true, stdio: 'ignore',@@ -1938,6 +1972,7 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer const b = await puppeteer.connect({ browserURL: `http://127.0.0.1:${debugPort}`, defaultViewport: null }); b._detachedPid = child.pid; b._cdpPort = debugPort;+ b._priorForeground = priorForeground; // v1.8.34: user's window to hand focus back to console.log(`Launched detached Chrome (PID ${child.pid}) on CDP port ${debugPort} for profile "${profileName}"`); // v1.8.24: ENFORCE background-by-default the instant the window exists — push it // behind the user's windows AND hand focus back to whatever they were in, so the@@ -1945,12 +1980,14 @@ 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') {- b._bgResult = osBackgroundWindowByPid(child.pid, debugPort);+ b._bgResult = osBackgroundWindowByPid(child.pid, debugPort, priorForeground); console.log(`[bg] initial background result for "${profileName}": ${b._bgResult}`);- // ONE late retry (Chrome/Edge can re-raise itself right after first paint). Keep it- // to a single pass — each pass does a minimize/re-show, which on an on-screen window- // is a visible flicker, so we don't want a burst of them.- setTimeout(() => { try { b._bgResult = osBackgroundWindowByPid(child.pid, debugPort); } catch (e) {} }, 800);+ // A couple of late passes: Chrome/Edge can re-grab foreground right after first paint,+ // so re-run the background+focus-handback until startup settles. No minimize now, so+ // these don't flicker.+ for (const delay of [600, 1400, 2500]) {+ setTimeout(() => { try { b._bgResult = osBackgroundWindowByPid(child.pid, debugPort, priorForeground); } catch (e) {} }, delay);+ } } return b; };@@ -3009,7 +3046,7 @@ const server = http.createServer(async (req, res) => { if (!foreground) { try { const be = browsers.get(args.profile);- if (be && be.browser) be.browser._bgResult = osBackgroundWindowByPid(be.browser._detachedPid, be.browser._cdpPort);+ if (be && be.browser) be.browser._bgResult = osBackgroundWindowByPid(be.browser._detachedPid, be.browser._cdpPort, be.browser._priorForeground); else osSendWindowToBack(args.profile); } catch (e) {} }