← Commit history
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.1.11+0.1.12\ 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.11",+  "version": "0.1.12",   "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.11",+  "version": "0.1.12",   "private": true,   "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)",   "main": "server.js",
bridge/server.js+133−27
@@ -26,6 +26,7 @@ const net = require('net'); const fs = require('fs'); const os = require('os'); const path = require('path');+const { execFile } = require('child_process');  const BRIDGE = 'native-browser'; let VERSION = '0.0.1';@@ -448,7 +449,7 @@ const badgeIdle = new Map();  // sessionId -> clear timer const badgeOn = new Set();    // sessionIds currently badged function armBadgeIdle(sid) {   const prev = badgeIdle.get(sid); if (prev) clearTimeout(prev);-  badgeIdle.set(sid, setTimeout(() => { const h = sessionHwnd.get(sid); if (h) taskbar(h, { overlay: { badge: 'none' } }).catch(() => {}); badgeOn.delete(sid); badgeIdle.delete(sid); }, BADGE_IDLE_MS));+  badgeIdle.set(sid, setTimeout(() => { const h = sessionHwnd.get(sid); if (h) nativeOverlay(h, sessionProfile.get(sid)); badgeOn.delete(sid); badgeIdle.delete(sid); }, BADGE_IDLE_MS)); } // Map a driven profile → its Chrome/Edge profile DIRECTORY, so AD (>=1.9.110) can composite the Adom mark // onto that profile's AVATAR instead of evicting it (the taskbar overlay has one slot = the avatar's slot).@@ -480,21 +481,101 @@ function avatarForProfile(profileKey) {   _avatarCache.set(profileKey, result);   return result; }-// TEMPORARILY OFF (John, 2026-07-15): every current overlay variant looks wrong — the plain badge covers-// the profile avatar, and AD's composite uses the RAW (square) profile PNG + a too-small mark, so it-// replaces Chrome's circular avatar with a square one. Keeping the badge off (icons stay clean/native)-// until the composite looks right: a CIRCLE-cropped avatar + a clearly-visible Adom mark. The startup-// sweep clears any existing overlays, so flipping this off leaves the taskbar clean.-const BADGE_ENABLED = false;+const BADGE_ENABLED = true;+// The BRIDGE is the browser expert, so WE build the FINAL taskbar overlay icon ourselves and hand AD a+// ready PNG via overlay.icon (AD stays generic - no browser/avatar/folder knowledge). The icon is the+// profile's own avatar CIRCLE-cropped (the way Chrome/Edge draw it) with the Adom favicon sitting flush+// in the lower-right corner. Compositing runs natively on Windows via PowerShell + System.Drawing (GDI+):+// zero npm deps (the bridge has none), antialiased circle-crop + HighQualityBicubic resize, reads any PNG.+// Two variants per profile: WITH the favicon (driving) and WITHOUT it (the plain avatar = the native look,+// painted back when driving stops so the taskbar returns to exactly how Chrome had it - no blanked slot).+const FAVICON_PNG = path.join(__dirname, 'adom-favicon.png');+const ICON_DIR = path.join(os.tmpdir(), 'adom-nbrowser-icons');+const PS1_PATH = path.join(ICON_DIR, 'compose-overlay.ps1');+const PS1_BODY = [+  'param([string]$Avatar,[string]$Favicon,[string]$Out,[int]$Size,[int]$Badge)',+  '$ErrorActionPreference="Stop"',+  'Add-Type -AssemblyName System.Drawing',+  '$S=$Size; $W=$S*4',                                   // supersample 4x then downscale = clean edges+  '$av=[System.Drawing.Image]::FromFile($Avatar)',+  '$avSq=New-Object System.Drawing.Bitmap($W,$W,[System.Drawing.Imaging.PixelFormat]::Format32bppArgb)',+  '$g=[System.Drawing.Graphics]::FromImage($avSq)',+  '$g.InterpolationMode=[System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic',+  '$g.PixelOffsetMode=[System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality',+  '$aw=$av.Width;$ah=$av.Height;$m=[Math]::Min($aw,$ah);$sx=($aw-$m)/2;$sy=($ah-$m)/2',   // center-crop to square+  '$g.DrawImage($av,(New-Object System.Drawing.Rectangle(0,0,$W,$W)),$sx,$sy,$m,$m,[System.Drawing.GraphicsUnit]::Pixel)',+  '$g.Dispose()',+  '$big=New-Object System.Drawing.Bitmap($W,$W,[System.Drawing.Imaging.PixelFormat]::Format32bppArgb)',+  '$g2=[System.Drawing.Graphics]::FromImage($big)',+  '$g2.SmoothingMode=[System.Drawing.Drawing2D.SmoothingMode]::AntiAlias',+  '$g2.InterpolationMode=[System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic',+  '$brush=New-Object System.Drawing.TextureBrush($avSq)',                  // circle-crop = antialiased FillEllipse+  '$g2.FillEllipse($brush,0,0,($W-1),($W-1))',+  '$brush.Dispose()',+  'if($Badge -ne 0){',+  '  $fav=[System.Drawing.Image]::FromFile($Favicon)',+  '  $fs=[int][Math]::Round($W*0.46)',                                     // favicon ~46% of the icon+  '  $g2.DrawImage($fav,($W-$fs),($W-$fs),$fs,$fs)',                       // flush lower-right corner (no inset)+  '  $fav.Dispose()',+  '}',+  '$g2.Dispose()',+  '$fin=New-Object System.Drawing.Bitmap($S,$S,[System.Drawing.Imaging.PixelFormat]::Format32bppArgb)',+  '$g3=[System.Drawing.Graphics]::FromImage($fin)',+  '$g3.InterpolationMode=[System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic',+  '$g3.PixelOffsetMode=[System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality',+  '$g3.SmoothingMode=[System.Drawing.Drawing2D.SmoothingMode]::HighQuality',+  '$g3.DrawImage($big,(New-Object System.Drawing.Rectangle(0,0,$S,$S)),0,0,$W,$W,[System.Drawing.GraphicsUnit]::Pixel)',+  '$g3.Dispose()',+  '$fin.Save($Out,[System.Drawing.Imaging.ImageFormat]::Png)',+  '$fin.Dispose();$big.Dispose();$avSq.Dispose();$av.Dispose()'+].join('\r\n');+try { fs.mkdirSync(ICON_DIR, { recursive: true }); fs.writeFileSync(PS1_PATH, PS1_BODY); } catch {}+const _iconCache = new Map(); // profileKey -> { mtime, badge: path|null, plain: path|null }+const _sanitizeKey = (k) => String(k).replace(/[^a-z0-9]+/gi, '_').slice(0, 60);+// Build (or return cached) the overlay PNG for a profile. withBadge=true → avatar + Adom favicon; false →+// plain circle avatar (the native look). Cache keyed on the avatar file's mtime, so a changed profile pic+// regenerates. Resolves to a file PATH (AD reads it - same machine) or null (no avatar photo for this+// profile → caller falls back to AD's built-in plain badge / clears).+function buildOverlayIcon(profileKey, withBadge) {+  return new Promise((resolve) => {+    try {+      const av = avatarForProfile(profileKey);+      if (!av || !av.avatarPath) return resolve(null);+      let mtime = 0; try { mtime = Math.round(fs.statSync(av.avatarPath).mtimeMs); } catch {}+      const slot = withBadge ? 'badge' : 'plain';+      const cached = _iconCache.get(profileKey);+      if (cached && cached.mtime === mtime && cached[slot] && fs.existsSync(cached[slot])) return resolve(cached[slot]);+      const out = path.join(ICON_DIR, `ov_${_sanitizeKey(profileKey)}_${slot}_${mtime}.png`);+      const record = (p) => { const c = (cached && cached.mtime === mtime) ? cached : { mtime }; c.mtime = mtime; c[slot] = p; _iconCache.set(profileKey, c); };+      if (fs.existsSync(out)) { record(out); return resolve(out); }+      execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', PS1_PATH,+        '-Avatar', av.avatarPath, '-Favicon', FAVICON_PNG, '-Out', out, '-Size', '48', '-Badge', withBadge ? '1' : '0'],+        { timeout: 15000, windowsHide: true }, (err) => {+          if (err || !fs.existsSync(out)) return resolve(null);+          record(out); resolve(out);+        });+    } 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) {+  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' } });+  });+}+// Return a window to its NATIVE look: the plain circle avatar (same image Chrome shows), NOT a blanked+// overlay slot. If we have no avatar photo, clear to none (best we can do).+function nativeOverlay(hwnd, profileKey) {+  if (!hwnd) return;+  buildOverlayIcon(profileKey, false).then((icon) => {+    taskbar(hwnd, icon ? { overlay: { icon } } : { overlay: { badge: 'none' } });+  });+} function applyBadge(sid, hwnd, profileKey) {   if (!BADGE_ENABLED || !sid || !hwnd) return;-  const overlay = { badge: 'adom', tooltip: 'Adom is driving this browser' };-  // 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);+  badgeOn.add(sid); armBadgeIdle(sid);+  paintBadge(hwnd, profileKey || sessionProfile.get(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@@ -557,7 +638,7 @@ 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); taskbar(h, { overlay: { badge: 'adom', tooltip: 'Adom is driving this browser' } }); } }+  for (const s of fresh) { const h = matchHwnd(list, s.bounds, s.title); if (h) { spawnHwnd.set(s.windowId, h); paintBadge(h, profileKey); } }   // PROFILE-LEVEL badge: the OS window title for Google pages contains the profile email, and Windows   // taskbar buttons group by profile — so badging ANY window of the driven profile lights the right   // button. This is STABLE while pages navigate (unlike a per-window title), which is why per-window@@ -1588,16 +1669,41 @@ function startHttp(p) { } startHttp(argPort()); -// On startup, clear any ORPHANED "Adom is driving" overlays a PREVIOUS bridge instance left on Chrome-// windows. The badge idle-expiry timers are in-memory, so a bridge restart (redeploy/crash) LOSES them-// while the OS overlay persists — so without this sweep, a badge applied before a restart would linger-// forever (exactly the "personal profile still badged, nothing driving it" bug). Active driving re-applies-// the badge on its next verb (touchBadge), so the only cost is a brief gap on a currently-driven window.-async function sweepBadgesOnStartup(attempt = 0) {-  try {-    const list = await chromeWindowsDetailed();-    if (list && list.length) { for (const w of list) taskbar(w.hwnd, { overlay: { badge: 'none' } }).catch(() => {}); return; }-  } catch {}-  if (attempt < 4) setTimeout(() => sweepBadgesOnStartup(attempt + 1), 3000); // AD connection may not be ready at boot+// Enumerate all Chrome + Edge profiles from each browser's Local State (info_cache: dir -> {user_name}),+// so we can map a live window (its OS title carries the signed-in email on Google pages) back to a KEY.+function knownProfiles() {+  const out = [];+  const local = process.env.LOCALAPPDATA;+  if (!local) return out;+  const roots = [['chrome', path.join(local, 'Google', 'Chrome', 'User Data')], ['edge', path.join(local, 'Microsoft', 'Edge', 'User Data')]];+  for (const [browser, root] of roots) {+    try {+      const j = JSON.parse(fs.readFileSync(path.join(root, 'Local State'), 'utf8'));+      const cache = (j.profile && j.profile.info_cache) || {};+      for (const info of Object.values(cache)) {+        const email = String((info && info.user_name) || '');+        if (email.includes('@')) out.push({ email, key: `${browser}:${email}` });+      }+    } catch {}+  }+  return out;+}+// On startup, undo any STALE "Adom is driving" badge a PRIOR bridge left behind (bridges are KILLED, not+// gracefully stopped, so in-memory idle timers can't clear badges across a redeploy). We must NOT blank the+// overlay slot — that evicts Chrome's OWN profile avatar (the destructive regression we had). Instead, for+// each live browser window we can map to a known profile (title carries the signed-in email), we repaint the+// NATIVE look: the plain circle avatar (same image Chrome uses, minus the Adom mark). Undriven profiles look+// unchanged; any stale badge is wiped. Unmappable windows are left untouched; a currently-driven window gets+// its badge back on the thread's next verb.+async function resetStaleBadgesOnStartup(attempt = 0) {+  let list; try { list = await chromeWindowsDetailed(); } catch { list = null; }+  if (!list || !list.length) { if (attempt < 4) setTimeout(() => resetStaleBadgesOnStartup(attempt + 1), 3000); return; }+  const known = knownProfiles();+  const seen = new Set();+  for (const w of list) {+    const t = String(w.title || '');+    const hit = known.find((p) => p.email && t.includes(p.email));+    if (hit && !seen.has(w.hwnd)) { seen.add(w.hwnd); nativeOverlay(w.hwnd, hit.key); }+  } }-setTimeout(() => sweepBadgesOnStartup(), 4000);+setTimeout(() => resetStaleBadgesOnStartup(), 4000);
native-browser-bridge-manifest.json+6−6
@@ -1,8 +1,8 @@ {   "manifest_version": 1,-  "version": "0.1.11",-  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.11/native-browser-bridge-v0.1.11.bin",-  "sha256": "1aefff8bb9fcf61560f1ffa88a1a007078bf5f39747f8924bd5cf9ecebf7e6a1",-  "size": 58063,-  "released_at": "2026-07-15T00:00:00Z"-}\ No newline at end of file+  "version": "0.1.12",+  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.12/native-browser-bridge-v0.1.12.bin",+  "sha256": "83a61cd98a57ac14e225960bed5e680697366d3f2bef5f26997e91614a1084d1",+  "size": 67439,+  "released_at": "2026-07-14T00:00:00Z"+}