← Commit history
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.1.44\ No newline at end of file+0.1.45\ No newline at end of file
bridge/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Browser Extension",-  "version": "0.1.44",+  "version": "0.1.45",   "expectedExtensionVersion": "0.1.3",   "description": "Drives the user's REAL signed-in Chrome/Edge. MOST verbs require the Adom extension installed in that browser profile (CDP driving: navigate/click/read/screenshot). TWO verbs need NO extension at all and work on any machine: nbrowser_open_os_window (open a URL in the user's real profile, background+flashing - the OAuth/consent path) and nbrowser_os_windows (enumerate browser windows). Also extension-free: nbrowser_profiles, nbrowser_wake_profile, nbrowser_force_close. The bridge binds an ephemeral loopback port and the native-messaging host pumps stdio <-> bridge.",   "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.44",+  "version": "0.1.45",   "private": true,   "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)",   "main": "server.js",
bridge/server.js+30−11
@@ -614,7 +614,8 @@ const BADGE_ENABLED = true; // 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 FAVICON_PNG = path.join(__dirname, 'adom-favicon.png');            // LAYER 2: solid = "Adom is DRIVING this window"+const FAVICON_HOLLOW_PNG = path.join(__dirname, 'adom-favicon-hollow.png'); // LAYER 1: hollow = "Adom OPENED it, you're driving" const ICON_DIR = path.join(os.tmpdir(), 'adom-nbrowser-icons'); const PS1_PATH = path.join(ICON_DIR, 'compose-overlay.ps1'); const PS1_BODY = [@@ -661,20 +662,21 @@ const _sanitizeKey = (k) => String(k).replace(/[^a-z0-9]+/gi, '_').slice(0, 60); // 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) {+function buildOverlayIcon(profileKey, withBadge, style) {   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 hollow = style === 'hollow';+      const slot = withBadge ? (hollow ? 'badge_hollow' : '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'],+        '-Avatar', av.avatarPath, '-Favicon', hollow ? FAVICON_HOLLOW_PNG : 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);@@ -701,10 +703,12 @@ function resolvePendingProfiles() { // 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) {+function paintBadge(hwnd, profileKey, sid, style) {   if (!hwnd) return;-  buildOverlayIcon(profileKey, true).then((icon) => {-    if (icon) { taskbar(hwnd, { overlay: { icon, tooltip: 'Adom is driving this browser' } }); if (sid) badgeFallback.delete(sid); }+  const hollow = style === 'hollow';+  const tip = hollow ? 'Adom opened this window - you are in control' : 'Adom is driving this browser';+  buildOverlayIcon(profileKey, true, style).then((icon) => {+    if (icon) { taskbar(hwnd, { overlay: { icon, tooltip: tip } }); if (sid) badgeFallback.delete(sid); }     else { if (sid) badgeFallback.add(sid); resolvePendingProfiles(); }   }); }@@ -716,10 +720,20 @@ function nativeOverlay(hwnd, profileKey) {     taskbar(hwnd, icon ? { overlay: { icon } } : { overlay: { badge: 'none' } });   }); }-function applyBadge(sid, hwnd, profileKey) {+function applyBadge(sid, hwnd, profileKey, style) {   if (!BADGE_ENABLED || !sid || !hwnd) return;   badgeOn.add(sid); armBadgeIdle(sid);-  paintBadge(hwnd, profileKey || sessionProfile.get(sid), sid);+  paintBadge(hwnd, profileKey || sessionProfile.get(sid), sid, style);+}+// ── FOREGROUND ANNOUNCE (John, 2026-07-22): background is the hard default and foreground already+// REQUIRES a written reason — but that reason only ever reached the AI. When a foreground grant is+// actually exercised, SHOW THE HUMAN why, full-screen, for 3s. A takeover you can't explain is exactly+// what erodes trust in an agent that can touch your desktop.+function announceForeground(reason, sid) {+  const why = String(reason || '').trim().slice(0, 160) || 'no reason given';+  adCommand(null, 'desktop_caption', { text: `Adom is bringing a window to the front - ${why}`, id: 'adom-foreground', position: 'top', duration: 3000 })+    .then((r) => { const ok = !!(r && r.status >= 200 && r.status < 300); logActivity({ verb: '_foreground_announce', sessionId: sid || null, ok, ops: why.slice(0, 80), errorCode: ok ? null : `http_${(r && r.status) || 0}`, internal: true }); })+    .catch(() => {}); } // ── 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@@ -1674,6 +1688,11 @@ function dispatch(verb, args) {             try { const list = await chromeWindowsDetailed(); 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);+        // LAYER-1 BADGE (hollow): mark the window Adom OPENED even though it cannot drive it - provenance+        // matters most on an OAuth consent page. Uses applyBadge so the normal 3-min idle expiry restores+        // the native avatar; nothing lingers.+        if (hwnd) applyBadge(a.sessionId, hwnd, kp.key, 'hollow');         logActivity({ verb: 'nbrowser_open_os_window', sessionId: a.sessionId, thread: a.thread, profile: kp.key, ok: !!hwnd, errorCode: hwnd ? null : 'hwnd_not_found' });         sessionThread.set(a.sessionId, a.thread); sessionPurpose.set(a.sessionId, a.purpose); sessionProfile.set(a.sessionId, kp.key); if (hwnd) sessionHwnd.set(a.sessionId, hwnd); osOpened.add(a.sessionId); persistSessions();         const upsell = extSt && extSt.installed@@ -1985,7 +2004,7 @@ function dispatch(verb, args) {         pending.delete(id);         resolve({ success: false, error: `verb ${verb} timed out`, _hint: 'extension did not reply in time' });       }, timeoutFor(verb));-      pending.set(id, { resolve, timer, verb, toFile: !!(args && args.toFile), profileKey: tgt.key });+      pending.set(id, { resolve, timer, verb, toFile: !!(args && args.toFile), profileKey: tgt.key, args });       sock.write(JSON.stringify({ id, verb, args: args || {} }) + '\n');     };     // PIN the session to whatever profile this window opened in, so every later verb carrying this@@ -2061,7 +2080,7 @@ async function onHostFrame(frame) {             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); persistSessions(); } applyBadge(sid, neu, p.profileKey); break; } // applyBadge = badge (composited onto the profile avatar) + arm idle-expiry+          if (neu) { lastHwnd = neu; if (sid) { sessionHwnd.set(sid, neu); persistSessions(); } applyBadge(sid, neu, p.profileKey); 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));         }         payload = { ...payload, ...(neu ? { hwnd: neu } : {}), _taskbarHint: flashHint(),
native-browser-bridge-manifest.json+5−5
@@ -1,8 +1,8 @@ {   "manifest_version": 1,-  "version": "0.1.44",-  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.44/native-browser-bridge-v0.1.44.zip",-  "sha256": "e5b361dec85b84b21db7526770629d4de33bf9f216cfeedea8d56f43b4ee5ad5",-  "size": 88361,-  "released_at": "2026-07-21T00:00:00Z"+  "version": "0.1.45",+  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.45/native-browser-bridge-v0.1.45.zip",+  "sha256": "123bfbecf98722eb394ae84a03fc5a2d8d2112bcb999cff19fe35a6cb8150fbe",+  "size": 94856,+  "released_at": "2026-07-22T00:00:00Z" }