← Commit history

v0.16.55 (insiders): nb-bridge#55 experience pass: no second Windows Hello (the first click does work; Hello is a credential window, not a dialog class, so nb no longer decides the click failed and clicks again), the save dialog is filled by value and committed instead of synthetic typing that landed as 'SSSS', a glowing panel is pinned beside the dialog explaining what is happening, the path goes to the clipboard with instructions when Windows refuses the press, and on success the browser window closes before a plain 'Successfully exported' message

John Lauer ·0b5bb33fb1 ·13d ago ·parent 1dec168
6 files changed +55−17
bin/nb-dashboard+1−1
@@ -20,7 +20,7 @@ const { execFile, spawn } = require('child_process'); const fs = require('fs'); const crypto = require('crypto'); -const VERSION = '0.16.54';           // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.55';           // keep in lockstep with package.json (pkg version) // SEND TO AI IS UN-PARKED (John: "yeah, we are re-enabling the send to ai. it seems to be good now"). // The ah build thread certified the crash family dead on the gate-fixed build and the delivery leg is // proven end to end. The park const is DELETED rather than flipped, so no dead branch can resurrect it.
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.16.54\ No newline at end of file+0.16.55\ 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",-  "version": "0.16.54",+  "version": "0.16.55",   "expectedExtensionVersion": "0.2.22",   "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",
bridge/package.json+1−1
@@ -1,6 +1,6 @@ {   "name": "adom-native-browser-bridge",-  "version": "0.16.54",+  "version": "0.16.55",   "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+50−12
@@ -3149,6 +3149,19 @@ function writeSecretHandoff(kind, meta, secret) { const EXPORT_CAPTION = 'nb-export-logins'; const exportSay = (text, opts2) => abCommand(null, 'desktop_caption',   { text, id: EXPORT_CAPTION, position: 'top', size: 'medium', persist: true, ...(opts2 || {}) }).catch(() => {});+// A panel pinned NEXT TO the dialog the user is looking at, rather than a banner somewhere else on the+// screen (John, 2026-10-29: "put a window to the right of it ... tell the user what's happening").+// Positions are 0..1 fractions of the screen, so place it just right of the dialog's own rect.+const exportSayBeside = async (rect, text) => {+  try {+    const disp = await abCommand(null, 'desktop_screenshot_screen', { metaOnly: true }).catch(() => null);+    const sw = (disp && disp.body && (disp.body.fullWidth || (disp.body.display && disp.body.display.width))) || 2560;+    const sh = (disp && disp.body && (disp.body.fullHeight || (disp.body.display && disp.body.display.height))) || 1600;+    const x = Math.min(0.97, Math.max(0.03, ((rect.right || rect.x + rect.w) + 40) / sw));+    const y = Math.min(0.9, Math.max(0.05, ((rect.top || rect.y) + 60) / sh));+    return abCommand(null, 'desktop_caption', { text, id: EXPORT_CAPTION, x, y, size: 'medium', persist: true }).catch(() => {});+  } catch { return exportSay(text); }+}; const exportSayClear = () => abCommand(null, 'desktop_caption', { id: EXPORT_CAPTION, action: 'hide' }).catch(() => {}); // WATCH FOR THE FILE, NOT THE WINDOW (John, 2026-10-29). Window enumeration is not reliable here: the // save dialog is a modal another agent on the same desktop can dismiss between two scans, and we saw it@@ -3241,11 +3254,20 @@ async function exportSavedLogins(opts) {   // verify by OUTCOME: if no new dialog appears, find the button's rect and send a REAL click. The window   // is foregrounded for that moment, which is honest here - we are asking the user to authenticate.   if (invoked) {-    await _sleep(2500);-    const after = await abCommand(null, 'desktop_list_windows', {}).catch(() => null);-    const rows = (() => { try { return (after.body.data || after.body).windows || []; } catch { return []; } })();-    const dialogUp = rows.some((w) => w.className === '#32770' || /windows security|windows hello|verify your identity/i.test(String(w.title || '')));-    if (!dialogUp) {+    // The Hello prompt is a CREDENTIAL HOST window, not a #32770, and it can take seconds to appear.+    // Treating "no #32770 yet" as "the click did nothing" made nb click a second time, so the user got+    // TWO Hello prompts (John, 2026-10-29). Wait for any sign of life before deciding it failed.+    let actuated = false;+    const actDeadline = Date.now() + 15000;+    while (Date.now() < actDeadline && !actuated) {+      await _sleep(500);+      if (newLoginCsvSince(watchSince0, csvPath)) { actuated = true; break; }+      const after = await abCommand(null, 'desktop_list_windows', {}).catch(() => null);+      const rows = (() => { try { return (after.body.data || after.body).windows || []; } catch { return []; } })();+      actuated = rows.some((w) => /^#32770/.test(String(w.className || ''))+        || /credential dialog|windows security|windows hello|verify your identity|export passwords/i.test(`${w.title || ''} ${w.className || ''}`));+    }+    if (!actuated) {       const rect = btnRect;       if (rect) {         await abCommand(null, 'desktop_bring_to_front', { hwnd, reason: 'nb-bridge#55: the page button needs a real click; the user is about to be asked to authenticate' }).catch(() => {});@@ -3283,7 +3305,8 @@ async function exportSavedLogins(opts) {   // should ever see here is the Hello prompt. So poll FAST and fill it the moment it exists, and keep   // waiting the whole budget - the dialog only appears AFTER they answer Hello, which can take a while.   await exportSay(`${brand} is asking you to confirm it is you (Windows Hello). Approve it and Adom finishes the rest - do not close anything.`);-  const watchSince = Date.now();+  const watchSince0 = Date.now();+  const watchSince = watchSince0;   const deadline = Date.now() + timeoutSec * 1000;   let saved = false, foundFile = null, toldToSave = false;   let sawDialogAt = 0, dialogVisibleMs = null;@@ -3309,12 +3332,21 @@ async function exportSavedLogins(opts) {     }     if (!sawDialogAt) {       sawDialogAt = Date.now();-      await exportSay(`${brand} is asking where to save the file. Adom is handling it - you do not need to do anything.`);+      await exportSayBeside(dlg.rect || {}, `${brand} asked where to save your logins. Adom is filling this in and pressing Save for you - the file goes to a folder only you can read, pup imports it, and it is deleted straight after.`);+    }+    // SET the file name by VALUE and press Save. Synthetic typing landed as "SSSS" in the field on+    // John's machine, and it is unnecessary: UIA SetValue writes the whole path atomically. If Windows+    // refuses (the dialog can be owned by an elevated process, which ab will not click into), fall back+    // to putting the path on the clipboard and telling the user exactly what to do, in place.+    await abCommand(null, 'desktop_ui_set', { hwnd: dlg.hwnd, contains: 'File name', value: csvPath,+      reason: 'nb-bridge#55: write the export to a folder only this user can read' }).catch(() => null);+    const sv = await abCommand(null, 'desktop_ui_click', { hwnd: dlg.hwnd, contains: 'Save',+      reason: 'nb-bridge#55: commit the export so the dialog never sits in front of the user' }).catch(() => null);+    const svOk = !!(sv && sv.body && sv.body.success !== false);+    if (!svOk) {+      await abCommand(null, 'desktop_clipboard_set', { text: csvPath, reason: 'nb-bridge#55: give the user the path to paste' }).catch(() => {});+      await exportSay(`Windows will not let Adom press Save here. The path is on your clipboard: press Ctrl+V then Enter, or just press Save - Adom picks the file up either way and deletes it straight after.`, { position: 'top-right', size: 'medium' });     }-    await abCommand(null, 'desktop_bring_to_front', { hwnd: dlg.hwnd, reason: 'nb-bridge#55: fill the export file name' }).catch(() => {});-    await abCommand(null, 'desktop_press_key', { hwnd: dlg.hwnd, keys: 'ctrl+a', reason: 'nb-bridge#55: clear the file name' }).catch(() => {});-    await abCommand(null, 'desktop_type', { hwnd: dlg.hwnd, text: csvPath, reason: 'nb-bridge#55: write the export where only this user can read it' }).catch(() => {});-    await abCommand(null, 'desktop_press_key', { hwnd: dlg.hwnd, keys: 'enter', reason: 'nb-bridge#55: commit the export' }).catch(() => {});     // MEASURE how long that dialog was actually on screen, so "it is filled and gone" is a number the     // user can check, not a claim (John asked to see it work).     const goneDeadline = Date.now() + 8000;@@ -3381,7 +3413,13 @@ async function exportSavedLogins(opts) {   cleanup();   try { if (srcPath !== csvPath) fs.unlinkSync(srcPath); } catch {}   // the browser's own plaintext copy never survives this verb, wherever it landed   steps.push(step('handed it to the local tool and deleted the browser\'s copy', true));-  await exportSay(`Done: ${keep.length} logins handed to pup. ${brand}'s copy of the file was deleted.`, { persist: false, duration: 6000 });+  // Close the window we opened BEFORE the success message, so the last thing the user sees is the+  // result, not a browser page they did not ask for (John: "have that whole browser window go away+  // quickly and then tell the user 'successfully exported'").+  try { await dispatch('nbrowser_close_window', { sessionId: sid }); } catch {}+  await exportSay(`Successfully exported ${keep.length} logins from ${brand} to pup. The file has been deleted - nothing is left on disk.`, { persist: false, duration: 7000, size: 'large' });+  await _notify({ id: 'nb-export-logins', level: 'success', title: 'Successfully exported',+    body: `${keep.length} logins across ${hosts.length} sites are now in pup, and the exported file was deleted.` }).catch(() => {});   logActivity({ verb: 'nbrowser_export_saved_logins', profile: profileKey, ok: true, ops: `${keep.length} login(s) for ${hosts.length} site(s)` });   return done({ ok: true, browser, profile: profileKey, loginCount: keep.length, siteCount: hosts.length, dialogVisibleMs,     sites: hosts.slice(0, 40), filtered: !!wanted, steps, ...h,
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "nb-bridge",-  "version": "0.16.54",+  "version": "0.16.55",   "type": "app",   "title": "NB - Native Browser Bridge",   "description": "This package installs the SKILLS into your container so your AI knows how to drive your real signed-in Chrome and Edge; Bridge loads the bridge runtime itself from the release zip.",