← Commit history
0.3.9 + nbe 0.2.2: state enriches url/title via list_tabs (list_windows returns sessionId:null); click change-detection catches small local mutations (#20 was a false negative, not a missed click)
7 files changed
+34−23
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.3.8+0.3.9
bridge/bridge.json+2−2@@ -2,8 +2,8 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Native Browser",- "version": "0.3.8",- "expectedExtensionVersion": "0.2.1",+ "version": "0.3.9",+ "expectedExtensionVersion": "0.2.2", "description": "Drives the user's REAL signed-in Chrome/Edge, in two layers. Layer 1 (nb) needs NOTHING installed and works on any machine: nbrowser_open_os_window (open a URL in the user's real profile, background+flashing, the OAuth/consent path), nbrowser_os_windows, nbrowser_profiles, nbrowser_wake_profile, nbrowser_force_close. Layer 2 (nbe, the Adom Native Browser Extension) unlocks the rest: CDP driving (navigate/click/read/screenshot) inside their logged-in profile. The bridge binds an ephemeral loopback port and the native-messaging host pumps stdio <-> bridge.", "homepage": "https://wiki.adom.inc/adom/nb-bridge", "author": "Adom Inc.",
bridge/package.json+1−1@@ -1,6 +1,6 @@ { "name": "adom-native-browser-bridge",- "version": "0.3.8",+ "version": "0.3.9", "private": true, "description": "Native-browser bridge for Bridge (ab): layer 1 (nb) plus layer 2 via nbe, the Adom Native Browser Extension (Chrome + Edge).", "main": "server.js",
bridge/server.js+19−11@@ -1880,20 +1880,28 @@ function dispatch(verb, args) { // Default view = sessions THIS thread registered that still have a live window. all:true shows // every tracked session (with owner + alive flags) for debugging a shared desktop. const me = (() => { try { const c = callerLabel(); return c && (c.thread || c); } catch { return null; } })();- let live = [];- try {- const w = await callSelf('nbrowser_list_windows', { mine: true });- live = (w.windows || w.sessions || []).filter((r) => r && r.sessionId && r.owner !== 'user');- } catch {}- const liveIds = new Set(live.map((r) => r.sessionId)); const tracked = [...new Set([...sessionHwnd.keys(), ...sessionProfile.keys(), ...sessionThread.keys()])];+ // Enrich with LIVE url/title per session. nbrowser_list_windows cannot be the source: its+ // agent-opened rows carry sessionId:null, so a sessionId join silently yields nothing (state+ // reported hwnd but url/title null). list_tabs {sessionId} is the only per-session view that+ // carries them - and a session whose tabs no longer resolve is, by definition, dead.+ const enrich = {};+ await Promise.all(tracked.map(async (sid) => {+ const t = await Promise.race([+ callSelf('nbrowser_list_tabs', { sessionId: sid }),+ new Promise((r) => setTimeout(() => r({ __timeout: true }), 2500)),+ ]);+ const tabs = (t && t.tabs) || [];+ if (tabs.length) { const act = tabs.find((x) => x.active) || tabs[0]; enrich[sid] = { tabId: act.tabId, url: act.url, title: act.title, tabCount: tabs.length }; }+ })); const rows = tracked.map((sid) => {- const r = live.find((x) => x.sessionId === sid) || {};+ const r = enrich[sid] || {}; const thread = sessionThread.get(sid) || null;- return { sessionId: sid, hwnd: sessionHwnd.get(sid) || r.hwnd || null, windowId: r.windowId ?? null,- tabId: r.tabId ?? r.activeTabId ?? null, url: r.url || null, title: r.title || null,- profile: sessionProfile.get(sid) || r.profile || null, thread, purpose: sessionPurpose.get(sid) || null,- osOpened: osOpened.has(sid), alive: liveIds.has(sid) || !!sessionHwnd.get(sid),+ return { sessionId: sid, hwnd: sessionHwnd.get(sid) || null, windowId: null,+ tabId: r.tabId ?? null, url: r.url || null, title: r.title || null,+ profile: sessionProfile.get(sid) || null, thread, purpose: sessionPurpose.get(sid) || null,+ tabCount: r.tabCount ?? null,+ osOpened: osOpened.has(sid), alive: !!enrich[sid] || !!sessionHwnd.get(sid), yours: !me || !thread || thread === me }; }); const sessions = (sa.all === true ? rows : rows.filter((r) => r.alive && r.yours))
extension/manifest.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Adom Native Browser Extension", "short_name": "Adom",- "version": "0.2.1",+ "version": "0.2.2", "description": "Layer 2 of Adom Native Browser (nb): lets your Adom AI drive your real, signed-in Chrome or Edge, using your own logins.", "minimum_chrome_version": "116", "background": {
extension/src/cdp.js+5−2@@ -879,7 +879,10 @@ async function resizePng(b64, maxWidth) { // so callers can detect "click dispatched but nothing happened" (disabled/covered element, missed // target) and retry. Also scrolls the target into view (resolveCoords already centers it). export async function pageSig(tabId) {- try { const r = await evaluate(tabId, "(function(){return {u:location.href, r:document.readyState, n:(document.body?document.body.innerText.length:0)};})()"); return r || {}; } catch { return {}; }+ // t = a cheap rolling hash of the visible text, so a SMALL local mutation (a button relabelling+ // itself from "Click Me" to "Clicked") is detected. Length alone missed those and reported a+ // landed click as changed:false (nb-bridge #20).+ try { const r = await evaluate(tabId, "(function(){var s=document.body?document.body.innerText:'';var h=0;for(var i=0;i<s.length;i++){h=(h*31+s.charCodeAt(i))|0;}return {u:location.href, r:document.readyState, n:s.length, t:h};})()"); return r || {}; } catch { return {}; } } export async function clickChanged(tabId, args) { const before = await pageSig(tabId);@@ -887,7 +890,7 @@ export async function clickChanged(tabId, args) { if (r && r.ok === false) return r; await sleep(Number(args.settleMs) || 500); let after = await pageSig(tabId);- const didChange = (b, a) => (b.u !== a.u) || Math.abs((a.n || 0) - (b.n || 0)) > 20 || (b.r !== a.r && a.r !== 'complete');+ const didChange = (b, a) => (b.u !== a.u) || (a.t !== undefined && b.t !== undefined && a.t !== b.t) || Math.abs((a.n || 0) - (b.n || 0)) > 3 || (b.r !== a.r && a.r !== 'complete'); let changed = didChange(before, after), retried = false; // SELF-HEAL (nb-bridge #20). A click-by-text that resolves fine and then lands nowhere is the // single most confusing failure this bridge had: ok:true, matchedText set, changed:false, DOM
native-browser-bridge-manifest.json+5−5@@ -1,8 +1,8 @@ { "manifest_version": 1,- "version": "0.3.8",- "url": "https://wiki.adom.inc/download/adom/nb-bridge/0.3.8/native-browser-bridge-v0.3.8.zip",- "sha256": "4ac34fc47c941f90bd6a70152ae3edacedc9e8c634071aa5512b6059cdf689e2",- "size": 108442,- "released_at": "2026-08-19T19:06:40Z"+ "version": "0.3.9",+ "url": "https://wiki.adom.inc/download/adom/nb-bridge/0.3.9/native-browser-bridge-v0.3.9.zip",+ "sha256": "51a1e06f0472ceeb7322450e44782e0a4354c0e6dfc35693ac712da6550fc9e8",+ "size": 108641,+ "released_at": "2026-08-19T19:13:02Z" }