← 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)

John Lauer ·b893c92cda · ·parent 94679f0
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" }\ No newline at end of file
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) {}           }