← Commit history

v2.0.276: Off-screen birth restored, moved on-screen within 50ms, handback deleted (John). Root cause of tonight's regression: 2.0.259 moved birth on-screen, which lets Chrome activate the window and steal the foreground on every open; 2.0.260's handback then called desktop_bring_to_front(prevFg), and on a machine full of pup windows prevFg is usually ANOTHER pup window, so it foregrounded pup windows with no caption (the parade John saw). Chrome/CDP expose no no-activate create option and WS_EX_NOACTIVATE is a documented dead-end, so the only lever is WHERE the window is born: Chrome does not activate a window that is off-screen. So birth is off-screen again (zero focus steal), then a single local CDP getWindowForTarget+setWindowBounds repositions it on-screen WITHOUT changing z-order in ~20ms (budget 50ms), so it is clickable from one frame in and can never be stranded. The handback is gone; the remaining focus check is a diagnostic that fires only if off-screen birth ever fails to suppress the activation.

John Lauer ·4df4960977 ·2mo ago ·parent b41bfe4
4 files changed +35−25
adom-bridge-puppeteer-manifest.json+5−5
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "puppeteer",-  "version": "2.0.275",-  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.275/adom-bridge-puppeteer-v2.0.275.zip",-  "sha256": "0da881f99341d0cd09a1bb59f15b413d9160c2bbdb26f7521ee32481b330661b",-  "size": 964259,+  "version": "2.0.276",+  "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.276/adom-bridge-puppeteer-v2.0.276.zip",+  "sha256": "a438158e6b1f3f7930773bd6c929b90447bd33550f4e5fe743f118e2cbd524f0",+  "size": 964921,   "verbPrefixes": [     "browser_",     "desktop_recorder_",@@ -12,5 +12,5 @@   ],   "healthEndpoint": "/status",   "statusVerb": "browser_readiness",-  "released_at": "2026-08-21T23:17:23.044195Z"+  "released_at": "2026-08-22T16:29:16.144842Z" }
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-2.0.275\ No newline at end of file+2.0.276\ No newline at end of file
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "puppeteer",   "displayName": "Puppeteer (Chrome for Testing + Native)",-  "version": "2.0.275",+  "version": "2.0.276",   "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. pup_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. pup_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. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, pup_alert_window posts a native toast whose click focuses the window, pup_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.",   "homepage": "https://wiki.adom.inc/adom/adom-bridge",   "author": "Adom Inc.",
src/server.js+28−18
@@ -10335,7 +10335,26 @@ async function createPageInNewWindow(browser, profileName) {       invalidateEnumCache();   // v2.0.187: a birth changes the desktop — never diff against a stale snapshot       let beforeHwnds = new Set();       if (process.platform === 'win32') { try { beforeHwnds = new Set((await enumChromeWidgetWindows()).map(r => Number(r.h))); } catch (e) {} }-      await cdp.send('Target.createTarget', { url: 'about:blank', newWindow: true, left: _br.left, top: _br.top, width: _br.width, height: _br.height });+      // v2.0.276 (John: "off-screen birth could work as long as you move it to the main screen+      // within 50ms"). BORN OFF-SCREEN, because that is the ONLY lever Chrome/CDP leave us to stop+      // Chrome activating its own window: Chrome does not bother giving the foreground to a window+      // that is nowhere visible (measured). CDP createTarget has no no-activate option, and+      // WS_EX_NOACTIVATE is a dead-end (Chrome re-activates anyway + breaks the taskbar click).+      // So: create off-screen (zero focus steal), then IMMEDIATELY reposition on-screen with a single+      // local CDP setWindowBounds — which moves WITHOUT changing z-order, so the window lands on+      // screen, clickable, and still NOT foreground. This is ~20ms of local CDP, well inside the 50ms+      // budget, so the off-screen state lasts one frame and the "stranded unclickable window" can+      // never happen. No handback exists anymore because there is no steal to undo.+      const _OFFX = -32000, _OFFY = -32000;+      const _ct = await cdp.send('Target.createTarget', { url: 'about:blank', newWindow: true, left: _OFFX, top: _OFFY, width: _br.width, height: _br.height });+      const _tid = _ct && _ct.targetId;+      const _moveStart = Date.now();+      try {+        const { windowId: _wid } = await cdp.send('Browser.getWindowForTarget', _tid ? { targetId: _tid } : {});+        await cdp.send('Browser.setWindowBounds', { windowId: _wid, bounds: { left: _br.left, top: _br.top, width: _br.width, height: _br.height } });+        const _moveMs = Date.now() - _moveStart;+        console.log(`[birth] window moved off-screen -> on-screen (${_br.left},${_br.top}) in ${_moveMs}ms (budget 50ms; born off-screen so Chrome never activated it -> no focus steal)`);+      } catch (e) { console.log(`[birth] on-screen reposition failed (${e && e.message}) — the placement sweep will land it; the window is briefly off-screen`); }       for (let i = 0; i < 50 && !fresh; i++) {         fresh = (await browser.pages()).find(p => !before.has(p));         if (!fresh) await new Promise(r => setTimeout(r, 100));@@ -10375,24 +10394,15 @@ async function createPageInNewWindow(browser, profileName) {     else if (fresh) console.log(`[hwnd] birth capture on "${profileName}" produced no handle — will resolve later`);     try {       const nowFg = await osGetForegroundWindow();-      // v2.0.259, MEASURED on ConfRoomROG the moment birth moved on-screen: `--no-startup-window`-      // was NOT the only thing suppressing the foreground grab — the off-screen birth was doing half-      // the work, because Chrome does not bother activating a window that is nowhere. Born at a real-      // rect, Chrome activates it, and this logged a steal on EVERY open.-      //-      // R2 says accept a foreground window rather than fight it, and we do: the WINDOW is left where-      // it is (nothing re-parks it, nothing moves it). But the user's KEYBOARD focus is a different-      // thing from the window's z-order, and letting their typing land in a browser that just-      // appeared is not a flaw worth accepting when handing it back costs one call. This is the-      // v1.9.253 handback, restored: it was deleted only because the off-screen birth made it-      // unreachable. One shot, at birth, never on a timer — it is not a re-park loop.+      // v2.0.276: THE HANDBACK IS DELETED. It existed only because 2.0.259's on-screen birth let+      // Chrome steal the foreground on every open — and it was a bug: it called+      // desktop_bring_to_front(prevFg), and on a machine full of pup windows prevFg is usually+      // ANOTHER pup window, so it FOREGROUNDED pup windows (with no caption). Off-screen birth means+      // there is no steal to undo, so the whole mechanism is gone. This is only a DIAGNOSTIC now: if+      // it ever logs, off-screen birth failed to suppress the activation and that is a real bug to+      // investigate — never something to paper over with a bring_to_front.       if (prevFg && nowFg && String(nowFg) !== String(prevFg)) {-        console.log(`[focus] createTarget took the foreground (${prevFg} -> ${nowFg}); the window STAYS where it is (R2), handing your keyboard focus straight back to ${prevFg}`);-        try { await chrome.abCommand('desktop_bring_to_front', { hwnd: Number(prevFg) }, { timeoutMs: 4000 }); } catch (e) {}-        try {-          const after = await osGetForegroundWindow();-          if (after && String(after) !== String(prevFg)) console.log(`[focus] handback did not take (foreground is ${after}) — reporting it rather than pretending; the window is still on-screen and clickable, which is the requirement`);-        } catch (e) {}+        console.log(`[focus] UNEXPECTED: a new window took the foreground (${prevFg} -> ${nowFg}) despite off-screen birth — off-screen birth is supposed to make this impossible; investigate, do NOT add a handback.`);       }     } catch (e) {}     return fresh;