← Commit history
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.1.18\ No newline at end of file+0.1.19\ 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 (Chrome + Edge)",-  "version": "0.1.18",+  "version": "0.1.19",   "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.18",+  "version": "0.1.19",   "private": true,   "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)",   "main": "server.js",
bridge/server.js+41−10
@@ -525,12 +525,23 @@ function armBadgeIdle(sid) { const _avatarCache = new Map(); // profileKey -> {userDataDir, profileDir} | null function avatarForProfile(profileKey) {   if (!profileKey) return null;-  if (_avatarCache.has(profileKey)) return _avatarCache.get(profileKey);+  // pending-N / id keys carry NO email (fresh announce after a bridge restart, not yet resolved). Try+  // the LIVE connection's announced profile; if still unresolved, return null WITHOUT caching - the+  // caller must not paint a destructive fallback, and a later call may succeed once whoami resolves.+  let keyStr = String(profileKey);+  if (!keyStr.includes('@')) {+    const c = conns.get(keyStr);+    const em = c && c.profile && c.profile.email;+    if (em && String(em).includes('@')) keyStr = `${(c.profile.browser) || 'chrome'}:${em}`;+    else return null;+  }+  if (_avatarCache.has(keyStr)) return _avatarCache.get(keyStr);+  const profileKeyResolved = keyStr;   let result = null;   try {-    const i = String(profileKey).indexOf(':');-    const browser = i >= 0 ? profileKey.slice(0, i) : 'chrome';-    const email = i >= 0 ? profileKey.slice(i + 1) : profileKey;+    const i = profileKeyResolved.indexOf(':');+    const browser = i >= 0 ? profileKeyResolved.slice(0, i) : 'chrome';+    const email = i >= 0 ? profileKeyResolved.slice(i + 1) : profileKeyResolved;     const local = process.env.LOCALAPPDATA;     if (local && email.includes('@')) {       const root = browser === 'edge' ? path.join(local, 'Microsoft', 'Edge', 'User Data') : path.join(local, 'Google', 'Chrome', 'User Data');@@ -549,7 +560,7 @@ function avatarForProfile(profileKey) {       }     }   } catch {}-  _avatarCache.set(profileKey, result);+  _avatarCache.set(profileKeyResolved, result);   return result; } const BADGE_ENABLED = true;@@ -628,11 +639,30 @@ function buildOverlayIcon(profileKey, withBadge) {     } catch { resolve(null); }   }); }-// Paint the "Adom is driving" badge on a window: our composite icon if we can build it, else AD's plain badge.-function paintBadge(hwnd, profileKey) {+// Fire whoami at every live conn that hasn't resolved its email yet (pending-N keys), so avatar lookups+// start succeeding. Throttled; response rides the normal announce/whoami plumbing.+let _lastResolveNudge = 0;+function resolvePendingProfiles() {+  const now = Date.now(); if (_lastResolveNudge + 10000 > now) return; _lastResolveNudge = now;+  for (const [k, c] of conns) {+    if (!c.sock || c.sock.destroyed) continue;+    const em = c.profile && c.profile.email;+    if (em && String(em).includes('@')) continue;+    try { const id = ++seq; pending.set(id, { resolve: () => {}, timer: setTimeout(() => pending.delete(id), 4000), verb: 'nbrowser_whoami', profileKey: k, _internal: true }); c.sock.write(JSON.stringify({ id, verb: 'nbrowser_whoami', args: {} }) + '\n'); } catch {}+  }+}+// Paint the "Adom is driving" badge: our composite (circle avatar + favicon corner) or NOTHING yet.+// ⛔ NEVER fall back to the bare Adom badge on a real browser button - the overlay slot holds the+// user's PROFILE AVATAR, and stomping it with a full favicon is exactly what this feature exists to+// prevent (John hit it live, 2026-07-18: a pending-N key -> no avatar -> plain badge evicted his work+// avatar). If the composite can't be built yet (unresolved profile / no avatar file), we skip the+// paint, nudge profile resolution, and the caller's badgeFallback retry upgrades it on a later touch.+const badgeFallback = new Set(); // sids whose badge paint is deferred until the composite is buildable+function paintBadge(hwnd, profileKey, sid) {   if (!hwnd) return;   buildOverlayIcon(profileKey, true).then((icon) => {-    taskbar(hwnd, icon ? { overlay: { icon, tooltip: 'Adom is driving this browser' } } : { overlay: { badge: 'adom', tooltip: 'Adom is driving this browser' } });+    if (icon) { taskbar(hwnd, { overlay: { icon, tooltip: 'Adom is driving this browser' } }); if (sid) badgeFallback.delete(sid); }+    else { if (sid) badgeFallback.add(sid); resolvePendingProfiles(); }   }); } // Return a window to its NATIVE look: the plain circle avatar (same image Chrome shows), NOT a blanked@@ -646,7 +676,7 @@ function nativeOverlay(hwnd, profileKey) { function applyBadge(sid, hwnd, profileKey) {   if (!BADGE_ENABLED || !sid || !hwnd) return;   badgeOn.add(sid); armBadgeIdle(sid);-  paintBadge(hwnd, profileKey || sessionProfile.get(sid));+  paintBadge(hwnd, profileKey || sessionProfile.get(sid), sid); } // ── SESSION PERSISTENCE — sessionHwnd/sessionProfile are the badge's memory, and they used to be // in-memory only: every bridge redeploy WIPED them, so a thread that kept driving after a restart never@@ -735,7 +765,8 @@ function touchBadge(sid, profileKey) {     if (pk) { const now = Date.now(); if ((_profCorrAt.get(pk) || 0) + 30000 <= now) { _profCorrAt.set(pk, now); extQuery(pk, 'nbrowser_list_windows', {}).then((r) => { let pl = r; if (r && typeof r.output === 'string') { try { pl = JSON.parse(r.output); } catch { pl = null; } } if (pl) badgeSpawnsIn(pl, pk).catch(() => {}); }); } }     return;   }-  if (!badgeOn.has(sid)) applyBadge(sid, h, profileKey); else armBadgeIdle(sid);+  if (!badgeOn.has(sid)) applyBadge(sid, h, profileKey);+  else { armBadgeIdle(sid); if (badgeFallback.has(sid)) applyBadge(sid, h, profileKey); } // deferred paint: retry the composite now that the profile may have resolved } 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
native-browser-bridge-manifest.json+4−4
@@ -1,8 +1,8 @@ {   "manifest_version": 1,-  "version": "0.1.18",-  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.18/native-browser-bridge-v0.1.18.bin",-  "sha256": "9f21c9e3693ffc40f9decc282f7e187246b7f8a5285b27baf4ff01b1c7f35f56",-  "size": 71636,+  "version": "0.1.19",+  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.19/native-browser-bridge-v0.1.19.bin",+  "sha256": "8d8dfd178452020e1387fee4052b9af7e58dd3d90117d18e8d9099de5a697ffd",+  "size": 72398,   "released_at": "2026-07-18T00:00:00Z" }