← 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)

John Lauer ·096ee3c714 ·2mo ago ·parent 49711ab
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" }