← Commit history
v0.1.10: correlation - badge ONLY the window open_window created (new-hwnd diff; PWA-safe); remove profile-email re-correlation that hit the Gmail PWA. Bridge finds the avatar file + hands AD the path (overlay.avatar); AD stays generic (no folder reading)
5 files changed
+31−27
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.1.9+0.1.10
bridge/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Native Browser (Chrome + Edge)",- "version": "0.1.9",+ "version": "0.1.10", "expectedExtensionVersion": "0.1.2", "description": "Drives the user's REAL signed-in Chrome/Edge via an installed extension (native-messaging transport). Mirrors the puppeteer bridge's verb surface as nbrowser_* but runs inside the actual logged-in profile, so cookies/SSO/saved-logins/captcha human-trust all work. The bridge binds an ephemeral loopback port (PORTS.md compliant) and writes a discovery file; the native-messaging host pumps stdio <-> this bridge; the extension never touches a TCP port.", "homepage": "https://wiki.adom.inc/adom/adom-browser-extension",
bridge/package.json+1−1@@ -1,6 +1,6 @@ { "name": "adom-native-browser-bridge",- "version": "0.1.9",+ "version": "0.1.10", "private": true, "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)", "main": "server.js",
bridge/server.js+24−20@@ -469,7 +469,11 @@ function avatarForProfile(profileKey) { const j = JSON.parse(fs.readFileSync(path.join(root, 'Local State'), 'utf8')); const cache = (j.profile && j.profile.info_cache) || {}; for (const [dir, info] of Object.entries(cache)) {- if (String((info && info.user_name) || '').toLowerCase() === email.toLowerCase()) { result = { userDataDir: root, profileDir: dir }; break; }+ if (String((info && info.user_name) || '').toLowerCase() === email.toLowerCase()) {+ const pic = path.join(root, dir, 'Google Profile Picture.png');+ result = { userDataDir: root, profileDir: dir, avatarPath: fs.existsSync(pic) ? pic : null };+ break;+ } } } } catch {}@@ -479,30 +483,28 @@ function avatarForProfile(profileKey) { function applyBadge(sid, hwnd, profileKey) { if (!sid || !hwnd) return; const overlay = { badge: 'adom', tooltip: 'Adom is driving this browser' };- const av = avatarForProfile(profileKey || sessionProfile.get(sid)); // composite onto the profile avatar if we can find it- if (av) { overlay.profileDir = av.profileDir; overlay.userDataDir = av.userDataDir; }+ // The BRIDGE is the browser expert: WE find the profile's avatar file and hand AD the path; AD just+ // composites the Adom mark onto it (AD stays generic - no browser/folder knowledge). If we can't find+ // an avatar, AD falls back to the plain badge.+ const av = avatarForProfile(profileKey || sessionProfile.get(sid));+ if (av && av.avatarPath) overlay.avatar = av.avatarPath; taskbar(hwnd, { overlay }); badgeOn.add(sid); armBadgeIdle(sid); } // Called on every DRIVE verb: the badge tracks ACTIVE driving, not mere ownership. On the first activity // (or after a bridge restart) it lazily correlates the driven profile's taskbar window; then it just // re-arms the idle timer. An IDLE owned window (a thread opened it then went quiet) is NEVER badged here, // so it fades and stays faded — fixing the "personal profile badged when nothing's driving it" bug.-const badgeTry = new Map(); // sid -> last correlate-attempt ts (throttles the desktop_list_windows call)-async function touchBadge(sid, profileKey) {+function touchBadge(sid, profileKey) { if (!sid) return;- let h = sessionHwnd.get(sid);- if (!h) {- const last = badgeTry.get(sid) || 0;- if (Date.now() - last < 8000) return; // throttle correlation attempts- badgeTry.set(sid, Date.now());- const email = emailOf(profileKey);- if (!email.includes('@')) return;- try { const list = await chromeWindowsDetailed(); const m = list.find((w) => (w.title || '').includes(email)); if (m) { h = m.hwnd; sessionHwnd.set(sid, h); } } catch {}- if (!h) return;- }+ // ONLY the window we captured at open_window (the real browser window the AI created). We deliberately+ // do NOT re-correlate by profile/title here - that is exactly what reached the wrong window (a Gmail PWA+ // that merely had the profile email in its title). If the captured hwnd is gone (e.g. bridge restarted),+ // no badge until the thread opens a fresh window. Correctness over persistence.+ const h = sessionHwnd.get(sid);+ if (!h) return; if (!badgeOn.has(sid)) applyBadge(sid, h, profileKey); else armBadgeIdle(sid); }-function clearBadge(sid) { const t = badgeIdle.get(sid); if (t) clearTimeout(t); badgeIdle.delete(sid); badgeOn.delete(sid); badgeTry.delete(sid); }+function clearBadge(sid) { const t = badgeIdle.get(sid); if (t) clearTimeout(t); badgeIdle.delete(sid); badgeOn.delete(sid); } // 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();@@ -1401,9 +1403,11 @@ async function onHostFrame(frame) { 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.- neu = matchHwnd(list, payload.bounds, payload.title); // title-first (DPI-safe), then bounds- if (!neu) neu = list.map((w) => w.hwnd).find((h) => !preOpenHwnds.has(h)) || null; // diff fallback (title empty while loading)- if (!neu) { const email = emailOf(p.profileKey); if (email.includes('@')) { const m = list.find((w) => (w.title || '').includes(email)); if (m) neu = m.hwnd; } } // profile-level 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.+ neu = list.map((w) => w.hwnd).find((h) => !preOpenHwnds.has(h)) || null;+ if (!neu) neu = matchHwnd(list, payload.bounds, payload.title); } catch {} if (neu) { lastHwnd = neu; if (sid) sessionHwnd.set(sid, neu); applyBadge(sid, neu, p.profileKey); break; } // applyBadge = badge (composited onto the profile avatar) + arm idle-expiry await new Promise((r) => setTimeout(r, 250));@@ -1425,7 +1429,7 @@ async function onHostFrame(frame) { } } maybeAutoFlash(p.verb, payload);- if (payload && payload.sessionId && ACTIVITY.has(p.verb)) touchBadge(payload.sessionId, p.profileKey).catch(() => {}); // badge = ACTIVE driving; lazily correlates, idle-expires when commands stop+ if (payload && payload.sessionId && ACTIVITY.has(p.verb)) touchBadge(payload.sessionId, p.profileKey); // keep the open-captured badge alive while driving; idle-expires when commands stop const sid = payload && payload.sessionId; logActivity({ verb: p.verb, sessionId: sid || null, thread: sessionThread.get(sid) || null, profile: p.profileKey || null, ok: payload && payload.ok !== false, errorCode: (payload && payload.errorCode) || null }); resolve_ok(p.resolve, payload);
native-browser-bridge-manifest.json+4−4@@ -1,8 +1,8 @@ { "manifest_version": 1,- "version": "0.1.9",- "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.9/native-browser-bridge-v0.1.9.bin",- "sha256": "c2753b101d02ea99a3d819a47e1967cb8088026b833746bf3a1d8e8bd48687c4",- "size": 57677,+ "version": "0.1.10",+ "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.10/native-browser-bridge-v0.1.10.bin",+ "sha256": "1d1f216598c539f8f77cfb0d787bc807fe781e4b99680597c9b21bec1b021089",+ "size": 57819, "released_at": "2026-07-15T00:00:00Z" }