← 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.
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+0.16.18
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.",