← Commit history

v0.16.18 (insiders): Window identity is scoped to the window's own browser (adom/nb-bridge#47): Edge, Chrome and Chrome for Testing share one window class, and an unresolved Chrome-for-Testing open fell through a 24 px bounds match onto the user's maximized Edge window and stamped it with another thread's session purpose. Now candidates are only windows of the target browser (title suffix + owning image), Chrome for Testing windows are candidates for chrome profiles instead of being discarded, a bounds match must be unique, an unresolved window is left untouched (hwnd:null with a hint), and the stamp carries the app identity (Adom Native Browser · browser · profile), never a purpose or a thread name.

John Lauer ·1c70b8da28 ·1mo ago ·parent 43698ff
6 files changed +62−25
bin/nb-dashboard+1−1
@@ -20,7 +20,7 @@ const { execFile, spawn } = require('child_process'); const fs = require('fs'); const crypto = require('crypto'); -const VERSION = '0.16.17';           // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.18';           // keep in lockstep with package.json (pkg version) // SEND TO AI IS UN-PARKED (John: "yeah, we are re-enabling the send to ai. it seems to be good now"). // The ah build thread certified the crash family dead on the gate-fixed build and the delivery leg is // proven end to end. The park const is DELETED rather than flipped, so no dead branch can resurrect it.
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.16.17\ No newline at end of file+0.16.18\ No newline at end of file
bridge/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Native Browser",-  "version": "0.16.17",+  "version": "0.16.18",   "expectedExtensionVersion": "0.2.13",   "description": "Drives the user's REAL signed-in Chrome/Edge, in two layers. Layer 1 (nb) needs NOTHING installed and works on any machine: nbrowser_open_os_window (open a URL in the user's real profile, background+flashing, the OAuth/consent path), nbrowser_os_windows, nbrowser_profiles, nbrowser_wake_profile, nbrowser_force_close. Layer 2 (nbe, the Adom Native Browser Extension) unlocks the rest: CDP driving (navigate/click/read/screenshot) inside their logged-in profile. The bridge binds an ephemeral loopback port and the native-messaging host pumps stdio <-> bridge.",   "homepage": "https://wiki.adom.inc/adom/nb-bridge",
bridge/package.json+1−1
@@ -1,6 +1,6 @@ {   "name": "adom-native-browser-bridge",-  "version": "0.16.17",+  "version": "0.16.18",   "private": true,   "description": "Native-browser bridge for Bridge (ab): layer 1 (nb) plus layer 2 via nbe, the Adom Native Browser Extension (Chrome + Edge).",   "main": "server.js",
bridge/server.js+57−20
@@ -937,11 +937,16 @@ function labelWindowOwner(hwnd, sid) {   if (!hwnd) return;   const owner = (sid && sessionCaller.get(sid)) || callerLabel();   if (!owner) return;-  const purpose = (sid && sessionPurpose.get(sid)) || '';+  // The identity is the WINDOW's: the app and the profile it belongs to. Never a session purpose and never+  // a thread name (adom/nb-bridge#47: a jump-list header showed another thread's purpose text). Who is+  // driving stays in the Activity Log and the taskbar badge.+  const key = (sid && sessionProfile.get(sid)) || '';+  const kp = key ? knownProfiles().find((x) => x.key === key) : null;+  const who = kp ? `${kp.browser === 'edge' ? 'Edge' : 'Chrome'} · ${kp.displayName || kp.email || kp.profileDir}` : (key || 'browser');   abCommand('desktop', 'desktop_set_window_identity', {     hwnd,     appId: 'Adom.NativeBrowser',-    displayName: `Adom · ${owner}${purpose ? ' · ' + String(purpose).slice(0, 60) : ''}`,+    displayName: `Adom Native Browser · ${who}`.slice(0, 80),   }).then((r) => {     const ok = !!(r && r.status >= 200 && r.status < 300);     logActivity({ verb: '_window_identity', sessionId: sid || null, hwnd, ops: owner, ok, internal: true });@@ -1091,23 +1096,51 @@ function clearBadge(sid) { const t = badgeIdle.get(sid); if (t) clearTimeout(t); // Capture the driven window's hwnd by DIFFING the real-Chrome window list across the open (the new // hwnd is the one we opened). Robust vs Windows foreground-lock - no GetForegroundWindow guessing. let preOpenHwnds = new Set();-async function chromeHwnds() {+// WHICH BROWSER owns a window (adom/nb-bridge#47). Edge, Chrome and Chrome for Testing all share the+// Chrome_WidgetWin_1 class, so class alone can never say. The title suffix does (" - Microsoft Edge",+// " - Google Chrome", " - Google Chrome for Testing"), and the owning image (msedge.exe / chrome.exe) is the+// tie-break. A window whose browser cannot be told is 'unknown' and never a stamping candidate.+function browserOfWindow(w) {+  const t = String((w && w.title) || ''), img = String((w && w.image) || '').toLowerCase();+  if (/ - Microsoft.?\s*Edge\s*$/i.test(t) || img === 'msedge.exe') return 'edge';+  if (/ - Google Chrome for Testing\s*$/i.test(t) || /for.testing|chrome-for-testing|chrome_for_testing/i.test(img)) return 'chrome-testing';+  if (/ - Google Chrome\s*$/i.test(t) || img === 'chrome.exe') return 'chrome';+  return 'unknown';+}+// The browser a profile key targets: chrome:... -> 'chrome' (Chrome OR Chrome for Testing, since a host like+// pup runs the extension inside CfT), edge:... -> 'edge', pending-N -> null (unknown).+function browserOfKey(key) {+  const k = String(key || '');+  if (k.startsWith('edge:')) return 'edge';+  if (k.startsWith('chrome:')) return 'chrome';+  return null;+}+function windowMatchesBrowser(w, browser) {+  const b = browserOfWindow(w);+  if (!browser) return b !== 'unknown';+  if (browser === 'edge') return b === 'edge';+  return b === 'chrome' || b === 'chrome-testing';+}+async function chromeHwnds(browser) {   const r = await abCommand('desktop', 'desktop_list_windows', {});   let out = r.body;   if (out && typeof out.output === 'string') { try { out = JSON.parse(out.output); } catch {} }   const ws = (out && out.data && out.data.windows) || (out && out.windows) || [];-  return new Set(ws.filter((w) => /Chrome_WidgetWin/.test(w.className || '') && !/for Testing/.test(w.title || '')).map((w) => w.hwnd));+  return new Set(ws.filter((w) => /Chrome_WidgetWin/.test(w.className || '') && windowMatchesBrowser(w, browser)).map((w) => w.hwnd)); } // Full Chrome window list (with rects/titles) so we can correlate a SPAWNED window's bounds/title to its // OS hwnd. Spawns have their own hwnd but no sessionId, so we can't diff them at open like open_window.-async function chromeWindowsDetailed() {+async function chromeWindowsDetailed(browser) {   const r = await abCommand('desktop', 'desktop_list_windows', {});   let out = r.body;   if (out && typeof out.output === 'string') { try { out = JSON.parse(out.output); } catch {} }   const ws = (out && out.data && out.data.windows) || (out && out.windows) || [];-  return ws.filter((w) => /Chrome_WidgetWin/.test(w.className || '') && !/for Testing/.test(w.title || ''));+  return ws.filter((w) => /Chrome_WidgetWin/.test(w.className || '') && windowMatchesBrowser(w, browser)); }-function matchHwnd(list, bounds, title) {+function matchHwnd(list, bounds, title, browser) {+  // Only windows of the TARGET browser are candidates (adom/nb-bridge#47: a bounds match across the shared+  // window class stamped the user's maximized Edge window with a Chrome session's purpose).+  list = (list || []).filter((w) => windowMatchesBrowser(w, browser || null));   // TITLE first — DPI-INDEPENDENT. chrome.windows bounds are LOGICAL px, but the OS window rects are   // PHYSICAL px, so on a scaled display (e.g. 150%) bounds won't match. The OS window title is   // "<page title> - <Browser>", so the session's page title is a PREFIX of it. Prefer that exact prefix@@ -1123,7 +1156,10 @@ function matchHwnd(list, bounds, title) {     if (sub) return sub.hwnd;   }   // Fallback: bounds (only reliable at 100% scaling).-  if (bounds) for (const w of list) { const r = w.rect || {}; if (r.left != null && r.top != null && Math.abs(r.left - bounds.left) <= 24 && Math.abs(r.top - bounds.top) <= 24) return w.hwnd; }+  if (bounds) {+    const near = list.filter((w) => { const r = w.rect || {}; return r.left != null && r.top != null && Math.abs(r.left - bounds.left) <= 24 && Math.abs(r.top - bounds.top) <= 24; });+    if (near.length === 1) return near[0].hwnd;   // two candidates at the same corner (a maximized pair) is a guess, and a guess stamps the wrong window+  }   return null; } // (Re)paint the "Adom is driving" badge from a nbrowser_owned / nbrowser_list_windows payload, correlating@@ -1142,15 +1178,16 @@ async function badgeSpawnsIn(payload, profileKey) {   }   const fresh = spawns.filter((s) => !spawnHwnd.has(s.windowId));   if (!fresh.length && !owned.length) return;-  let list; try { list = await chromeWindowsDetailed(); } catch { return; }-  for (const s of fresh) { const h = matchHwnd(list, s.bounds, s.title); if (h) { spawnHwnd.set(s.windowId, h); paintBadge(h, profileKey); } }+  const brw = browserOfKey(profileKey);+  let list; try { list = await chromeWindowsDetailed(brw); } catch { return; }+  for (const s of fresh) { const h = matchHwnd(list, s.bounds, s.title, brw); if (h) { spawnHwnd.set(s.windowId, h); paintBadge(h, profileKey); } }   // Correlate each owned session to its OWN browser window by its page title (matchHwnd, which prefers   // an exact title prefix and only matches real browser windows). The old "profileHwnd = first window   // whose title contains the profile email" shortcut is REMOVED: it matched the [email protected] "Adom   // Mail"/Gmail PWA window and badged THAT instead of the Chrome window the user was watching — a real   // regression (badge invisible on the driven work-Chrome button; the whole trust signal broke).   for (const o of owned) {-    const h = matchHwnd(list, o.bounds, o.title);+    const h = matchHwnd(list, o.bounds, o.title, brw);     if (!h) continue;     const old = sessionHwnd.get(o.sid);     if (old !== h) { if (old && old !== h) nativeOverlay(old, profileKey); sessionHwnd.set(o.sid, h); persistSessions(); lastHwnd = h; clearBadge(o.sid); applyBadge(o.sid, h); }@@ -2206,7 +2243,7 @@ function dispatch(verb, args) {           const t0 = Date.now();           while (!hwnd && Date.now() - t0 < 8000) {             await new Promise((r) => setTimeout(r, 500));-            try { const list = await chromeWindowsDetailed(); hwnd = list.map((w) => w.hwnd).find((h) => !pre.has(h) && !protectedHwnds.has(h)) || null; } catch {}+            try { const list = await chromeWindowsDetailed(kp.browser); hwnd = list.map((w) => w.hwnd).find((h) => !pre.has(h) && !protectedHwnds.has(h)) || null; } catch {}           }         }         if (a.foreground === true) announceForeground(a.foregroundReason, a.sessionId);@@ -2753,7 +2790,7 @@ function dispatch(verb, args) {     }     // For open_window, snapshot the Chrome window list BEFORE forwarding so the post-open diff reliably     // catches the new window (the old fire-and-forget snapshot raced the window appearing).-    if (verb === 'nbrowser_open_window') chromeHwnds().then((s) => { preOpenHwnds = s; forward(); }).catch(forward);+    if (verb === 'nbrowser_open_window') chromeHwnds(browserOfKey(tgt.key)).then((s) => { preOpenHwnds = s; forward(); }).catch(forward);     else forward();   }); }@@ -2814,18 +2851,18 @@ async function decorateReply(p, frame, payloadIn, setPayload) {         let neu = null;         for (let i = 0; i < 4; i++) {           try {-            const list = await chromeWindowsDetailed();-            // Correlate by BOUNDS first (profile-independent + precise — the new-window DIFF can't tell-            // profiles apart and badged the WRONG profile's taskbar button). Diff only as a fallback.-            // The window the AI JUST created = the new hwnd (set-difference vs the pre-open snapshot). This-            // is reliable, DPI-safe, and PWA-safe: it's always the real browser window we opened, never a-            // Gmail/Chat PWA or some other profile window. Title/bounds are only a fallback.+            const br = browserOfKey(p.profileKey);+            const list = await chromeWindowsDetailed(br);+            // Candidates are windows of the TARGET browser only (adom/nb-bridge#47). The window the AI JUST+            // created = the new hwnd (set-difference vs the pre-open snapshot of the same browser); title,+            // then a UNIQUE bounds match, are the fallbacks. Unresolved = nothing is stamped or badged.             neu = list.map((w) => w.hwnd).find((h) => !preOpenHwnds.has(h)) || null;-            if (!neu) neu = matchHwnd(list, payload.bounds, payload.title);+            if (!neu) neu = matchHwnd(list, payload.bounds, payload.title, br);           } catch {}           if (neu) { lastHwnd = neu; if (sid) { sessionHwnd.set(sid, neu); persistSessions(); } applyBadge(sid, neu, p.profileKey); labelWindowOwner(neu, sid); if (p.args && p.args.userRequestedForeground === true && p.args.foregroundReason) announceForeground(p.args.foregroundReason, sid); break; } // applyBadge = badge (composited onto the profile avatar) + arm idle-expiry           await new Promise((r) => setTimeout(r, 250));         }+        if (!neu) payload = { ...payload, hwnd: null, _hwndHint: 'The OS window for this session could not be resolved among the windows of its own browser, so NOTHING was stamped or badged (nb never stamps a window it did not open). nbrowser_state re-correlates later; nbrowser_screenshot works without an hwnd.' };         payload = { ...payload, ...(neu ? { hwnd: neu } : {}), _taskbarHint: flashHint(),           _screenshotHint: 'To SEE this window without foregrounding it: nbrowser_screenshot {sessionId} = the PAGE bitmap (fast, default). To see the WINDOW FRAME or a native popup/dialog (Chrome password dropdown, print dialog, alert/confirm), use ab desktop_screenshot_window {hwnd:' + (neu || '<from desktop_list_windows by title>') + '} - it captures the window + its OWNED CHILD windows (parent/child hwnds). NEVER a fullscreen/monitor screenshot to inspect a browser window.' };       }
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "nb-bridge",-  "version": "0.16.17",+  "version": "0.16.18",   "type": "app",   "title": "NB - Native Browser Bridge",   "description": "This package installs the SKILLS into your container so your AI knows how to drive your real signed-in Chrome and Edge; Bridge loads the bridge runtime itself from the release zip.",