← Commit history
Update 5 file(s)
5 files changed
+38−19
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.1.44+0.1.45
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" }