← Commit history

1.8.44: respect a deliberate user raise so click-to-watch is not forced back to background

John Lauer ·4d460e56c4 · ·parent d0c45bd
5 files changed +82−17
CHANGELOG.md+25
@@ -1,5 +1,30 @@ # Changelog — Adom Desktop Puppeteer Bridge +## 1.8.44 — 2026-07-14 — Respect a deliberate user raise (click-to-watch no longer forced back)++A user clicking a background pup window's taskbar button to WATCH a page load saw it snap+back to the background. Cause: the open-time re-park passes (600ms/1.4s/2.5s) that counter+Chrome's startup self-raise ran `SetWindowPos HWND_BOTTOM` **unconditionally**, so a click+landing inside that 2.5s window got yanked back.++- **`osBackgroundWindowByPid(..., respectUser)`** now distinguishes a real user raise from+  Chrome self-raising: if the pup window is foreground AND `GetLastInputInfo` shows a physical+  input within the last ~1.5s (Chrome's self-raise is programmatic — no input), it returns+  `"user-foreground"` and skips the park.+- The retry driver **latches** on that (`_userForeground`) and **cancels the remaining passes**+  — once you take the window, pup never fights you for it. The post-resize re-park honors the+  same latch. The initial off-screen park is unaffected (runs with `respectUser=0`; you can't+  have clicked an off-screen window).++## 1.8.35 → 1.8.43 — 2026-07-13/14 — Flash routes through AD; taskbar badge; snappy flash; cleanup nudge++- **Taskbar flash + clear route through AD-core** (`desktop_flash_window`) instead of pup's own+  `FlashWindowEx` — one mechanism, native, reliable. `browser_alert_window {stop}` now returns+  AD's real `applied` result (no more fire-and-forget `cleared:true` lies). Snappy: flashes on+  open before the page finishes loading.+- **Adom favicon badge** overlaid on the pup taskbar button via AD's `desktop_taskbar` overlay.+- **`🧹 CLEANUP` hint** in the open response + `browser_status` when windows are ≥30m old or ≥5 open.+ ## 1.8.24 → 1.8.34 — 2026-07-13 — Background-by-default, actually correct (no pop / no cover / no focus steal / no cmd flash)  The long-standing "pup window pops up, covers my work, steals my typing" problem, solved properly and
adom-bridge-puppeteer-manifest.json+6−6
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "1.8.43",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.43/adom-bridge-puppeteer-v1.8.43.zip",-  "sha256": "14435f0ead47ea81a211e5a5f1345df2e108c029d838b96ddc0df42b52ee5d5c",-  "size": 160144,+  "version": "1.8.44",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.8.44/adom-bridge-puppeteer-v1.8.44.zip",+  "sha256": "03faafed505047a9d05bccae21e5368b796b89b2f864c5eccd14affc8c76d21a",+  "size": 160960,   "verbPrefixes": [     "browser_",     "desktop_recorder_",@@ -12,5 +12,5 @@   ],   "healthEndpoint": "/status",   "statusVerb": "browser_readiness",-  "released_at": "2026-07-14T23:54:24Z"-}\ No newline at end of file+  "released_at": "2026-07-14T00:00:00Z"+}
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.8.43+1.8.44\ No newline at end of file
src/bridge.json+6−6
@@ -2,8 +2,8 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "1.8.43",-  "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.",+  "version": "1.8.44",+  "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.",   "license": "MIT",@@ -74,13 +74,13 @@   "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/adom-desktop-puppeteer-bridge/files/adom-bridge-puppeteer-manifest.json",   "dependencies": {     "node": ">=20",-    "browser": "Chromium-family — installed Chrome or Edge (every Windows box ships Edge); if neither exists, Chrome for Testing is auto-fetched as a last resort. No hard Chrome requirement."+    "browser": "Chromium-family \u2014 installed Chrome or Edge (every Windows box ships Edge); if neither exists, Chrome for Testing is auto-fetched as a last resort. No hard Chrome requirement."   },   "runtime": {     "kind": "node",     "version": ">=20",     "preferred": "22",-    "_note": "AD-managed shared-runtime declaration (see PROPOSAL-shared-runtimes.md). Bridges DECLARE their runtime; AD provisions a portable interpreter from its shared adom-runtimes cache and spawns by absolute path — no UAC, no PATH reliance. Additive: older AD ignores this and uses `dependencies` / bare `node` on PATH."+    "_note": "AD-managed shared-runtime declaration (see PROPOSAL-shared-runtimes.md). Bridges DECLARE their runtime; AD provisions a portable interpreter from its shared adom-runtimes cache and spawns by absolute path \u2014 no UAC, no PATH reliance. Additive: older AD ignores this and uses `dependencies` / bare `node` on PATH."   },   "category": "Browser Automation",   "tags": [@@ -96,11 +96,11 @@     },     "macos": {       "supported": false,-      "reason": "macOS port pending — puppeteer-core is cross-platform but the bridge's spawn config uses Windows-only killImageName 'node.exe' and Chrome for Testing path resolution hasn't been tested."+      "reason": "macOS port pending \u2014 puppeteer-core is cross-platform but the bridge's spawn config uses Windows-only killImageName 'node.exe' and Chrome for Testing path resolution hasn't been tested."     },     "linux": {       "supported": false,-      "reason": "Linux port pending — same killImageName + Chrome resolution issues as macOS."+      "reason": "Linux port pending \u2014 same killImageName + Chrome resolution issues as macOS."     }   },   "prewarm": {
src/server.js+44−4
@@ -1564,7 +1564,7 @@ async function badgePupTaskbar(sessionId) { // 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, restoreHwnd) {+function osBackgroundWindowByPid(pid, cdpPort, restoreHwnd, respectUser) {   if (process.platform !== 'win32' || (!pid && !cdpPort)) return 'error';   try {     const port = Number(cdpPort) || 0;@@ -1573,7 +1573,10 @@ function osBackgroundWindowByPid(pid, cdpPort, restoreHwnd) {     const ps = [       'Add-Type @"',       'using System; using System.Runtime.InteropServices;',+      'public struct LASTINPUTINFO { public uint cbSize; public uint dwTime; }',       'public class WinBgP {',+      '  [DllImport("user32.dll")] public static extern bool GetLastInputInfo(ref LASTINPUTINFO plii);',+      '  [DllImport("kernel32.dll")] public static extern uint GetTickCount();',       '  [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);',@@ -1603,6 +1606,24 @@ function osBackgroundWindowByPid(pid, cdpPort, restoreHwnd) {       '  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 }',+      // v1.8.44: if the USER deliberately raised this window (clicked its taskbar button to+      // WATCH a page load), do NOT fight them. A physical input in the last ~1.5s WITH the pup+      // window already foreground means the user took it — not Chrome self-raising (which is+      // programmatic, no input). Skip the re-park and signal the caller to stop retrying.+      // respectUser is 0 for the initial off-screen park (user can't have clicked it yet).+      `$respectUser = ${respectUser ? 1 : 0}`,+      'if ($respectUser -eq 1) {',+      '  $fg0 = [WinBgP]::GetForegroundWindow()',+      '  $isPup0 = $false',+      '  foreach($h in $handles){ if($h -eq $fg0){ $isPup0 = $true } }',+      '  if ($isPup0) {',+      '    $lii = New-Object LASTINPUTINFO',+      '    $lii.cbSize = [uint32][System.Runtime.InteropServices.Marshal]::SizeOf($lii)',+      '    [WinBgP]::GetLastInputInfo([ref]$lii) | Out-Null',+      '    $idle = [WinBgP]::GetTickCount() - $lii.dwTime',+      '    if ($idle -lt 1500) { Write-Output "user-foreground"; exit }',+      '  }',+      '}',       // 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).@@ -2021,9 +2042,25 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer         console.log(`[bg] initial background result for "${profileName}": ${b._bgResult}`);         // 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.+        // these don't flicker. v1.8.44: these passes RESPECT a deliberate user raise — if the+        // user clicks the taskbar button to watch the page load, osBackgroundWindowByPid returns+        // "user-foreground"; we latch it and cancel the remaining passes so we never yank the+        // window out from under them.+        const _bgTimers = [];         for (const delay of [600, 1400, 2500]) {-          setTimeout(() => { try { b._bgResult = osBackgroundWindowByPid(child.pid, debugPort, priorForeground); } catch (e) {} }, delay);+          const t = setTimeout(() => {+            try {+              if (b._userForeground) return; // user already took it — leave it alone+              const r = osBackgroundWindowByPid(child.pid, debugPort, priorForeground, true);+              b._bgResult = r;+              if (r === 'user-foreground') {+                b._userForeground = true;+                for (const tt of _bgTimers) clearTimeout(tt);+                console.log(`[bg] user raised "${profileName}" — respecting it, halting re-park`);+              }+            } catch (e) {}+          }, delay);+          _bgTimers.push(t);         }       }       return b;@@ -3093,7 +3130,10 @@ 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, be.browser._priorForeground);+              // v1.8.44: if the user deliberately raised this window to watch it, don't yank it+              // back on a resize either — honor the same latch the open-time retries set.+              if (be && be.browser && be.browser._userForeground) { /* user is watching — leave it */ }+              else if (be && be.browser) be.browser._bgResult = osBackgroundWindowByPid(be.browser._detachedPid, be.browser._cdpPort, be.browser._priorForeground, true);               else osSendWindowToBack(args.profile);             } catch (e) {}           }