← Commit history
v0.16.51 (insiders): the export narrates itself on the user's screen (what is happening, that Hello is theirs to approve, that the save dialog is nb's to handle) and always clears that caption; if nb cannot fill the dialog it says so in place and tells them to click Cancel; it also measures how long the dialog was on screen so 'filled and gone' is a number
6 files changed
+59−12
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.50'; // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.51'; // 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.50+0.16.51
bridge/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Native Browser",- "version": "0.16.50",+ "version": "0.16.51", "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.50",+ "version": "0.16.51", "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+54−7@@ -3142,6 +3142,14 @@ function writeSecretHandoff(kind, meta, secret) { // 5. move the CSV into the owner-only handoff dir, parse it for COUNTS only, strip the note column, // and delete every other copy (Chrome's own, and the temp). // Fails loudly with a screenshot path rather than guessing when the page moves under us.+// NARRATE IT ON THE SCREEN (John, 2026-10-29: "you don't even give them a caption or window to help+// them through what's next?"). A person who suddenly sees a Windows Hello prompt, or a file picker, must+// be told what it is, who asked for it, and what to do. The caption follows the flow and is always+// cleared at the end - a stale banner over someone's desktop is its own bug.+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(() => {});+const exportSayClear = () => abCommand(null, 'desktop_caption', { id: EXPORT_CAPTION, action: 'hide' }).catch(() => {}); async function exportSavedLogins(opts) { const { profileKey, browser, sites, reason, timeoutSec } = opts; const scheme = browser === 'edge' ? 'edge' : 'chrome';@@ -3152,9 +3160,11 @@ async function exportSavedLogins(opts) { const steps = []; const cleanup = () => { try { fs.unlinkSync(csvPath); } catch {} }; + const brand = scheme === 'edge' ? 'Edge' : 'Chrome'; await _notify({ id: 'nb-export-logins', level: 'warning', scenario: 'reminder',- title: 'Chrome will ask you to confirm',- body: `Adom is handing your saved ${scheme === 'edge' ? 'Edge' : 'Chrome'} logins to pup so it can sign you in. Chrome asks for Windows Hello once - approve it and this finishes by itself.` }).catch(() => {});+ title: `${brand} will ask you to confirm`,+ body: `Adom is handing your saved ${brand} logins to pup so it can sign you in. ${brand} asks for Windows Hello once - approve it and this finishes by itself.` }).catch(() => {});+ await exportSay(`Adom is loading pup with your saved ${brand} logins. ${brand} will ask you to confirm it is you - approve that prompt. Nothing else is needed from you.`); steps.push(step('told the user a Hello prompt is coming', true)); // The page lives in the user's own browser, so open OUR OWN window on it (never reuse theirs).@@ -3165,7 +3175,7 @@ async function exportSavedLogins(opts) { try { hwnd = JSON.parse(open.output).hwnd; } catch {} if (!hwnd) { cleanup(); return { ok: false, errorCode: 'no_window', error: 'could not open a window to drive the export in', steps }; } steps.push(step('opened a background window nb owns', true, `hwnd ${hwnd}`));- const done = async (res) => { try { await dispatch('nbrowser_close_window', { sessionId: sid }); } catch {} return res; };+ const done = async (res) => { await exportSayClear(); try { await dispatch('nbrowser_close_window', { sessionId: sid }); } catch {} return res; }; const nav = await abCommand(null, 'desktop_navigate', { hwnd, url: `${scheme}://password-manager/settings`, reason: 'nb-bridge#55: open the browser\'s own password export page' }).catch(() => null);@@ -3240,25 +3250,60 @@ async function exportSavedLogins(opts) { // download file save dialog? that's a horrible user experience"). He is right: the only thing a person // 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 deadline = Date.now() + timeoutSec * 1000; let saved = false;+ let sawDialogAt = 0, dialogVisibleMs = null; while (Date.now() < deadline) { await _sleep(300); const wins = await abCommand(null, 'desktop_list_windows', {}).catch(() => null); const list = (() => { try { return (wins.body.data || wins.body).windows || []; } catch { return []; } })(); const dlg = list.find((w) => (w.className === '#32770' || /^#32770/.test(String(w.className || ''))) && /save|export|passwords/i.test(String(w.title || '')));- if (!dlg) continue;+ if (!dlg) {+ // If the dialog is up but we have not matched it, SAY SO rather than leaving the person guessing+ // at a file picker that appeared out of nowhere.+ continue;+ }+ 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 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;+ while (Date.now() < goneDeadline) {+ await _sleep(150);+ const w2 = await abCommand(null, 'desktop_list_windows', {}).catch(() => null);+ const rows2 = (() => { try { return (w2.body.data || w2.body).windows || []; } catch { return []; } })();+ if (!rows2.some((x) => x.hwnd === dlg.hwnd)) break;+ }+ dialogVisibleMs = Date.now() - sawDialogAt; saved = true; break; }- steps.push(step('answered the save dialog', saved, saved ? null : 'no save dialog appeared'));+ steps.push(step('answered the save dialog', saved, saved ? `the save dialog was on screen for ${dialogVisibleMs}ms` : 'no save dialog appeared')); if (!saved) {- return done({ ok: false, errorCode: 'awaiting_user', steps,+ // Is a dialog sitting in front of them RIGHT NOW that nb failed to fill? Then the person is staring+ // at a file picker with no idea what it is. Tell them, in place, what it is and what to do.+ let stuck = null;+ try {+ const w = await abCommand(null, 'desktop_list_windows', {});+ const rows = (w.body.data || w.body).windows || [];+ stuck = rows.find((x) => /^#32770/.test(String(x.className || '')) && /save|export|passwords/i.test(String(x.title || ''))) || null;+ } catch {}+ if (stuck) {+ await exportSay(`${brand} is asking where to save your logins and Adom could not fill it in. Click Cancel to stop - nothing is exported. (Adom asked for this to load pup with your logins.)`, { persist: true });+ await _notify({ id: 'nb-export-logins', level: 'warning', scenario: 'reminder', title: 'A save dialog is waiting on your screen',+ body: `${brand} is asking where to save your exported logins. Adom could not fill it in, so click Cancel - nothing will be exported and you can retry later.` }).catch(() => {});+ } else {+ await exportSay(`Adom stopped waiting for the ${brand} confirmation. Nothing was exported.`, { persist: false, duration: 8000 });+ }+ return done({ ok: false, errorCode: 'awaiting_user', steps, dialogLeftOnScreen: !!stuck, error: `the export was started but no save dialog appeared within ${timeoutSec}s - the Windows Hello prompt was probably not answered`, _hint: 'RELAY TO THE USER: a Windows Hello prompt is (or was) on their PC asking them to confirm the password export; it cannot be answered for them. Once they approve, retry this verb.' }); }@@ -3268,6 +3313,7 @@ async function exportSavedLogins(opts) { const wdl = Date.now() + 45000; while (Date.now() < wdl) { await _sleep(800); try { const st = fs.statSync(csvPath); if (st.size > 0) { bytes = st.size; break; } } catch {} } if (!bytes) {+ await exportSay(`Nothing was exported - ${brand} did not write the file. You can close this.`, { persist: false, duration: 8000 }); return done({ ok: false, errorCode: 'no_export_file', steps, error: 'the save dialog was answered but no file appeared', _hint: 'The user may have cancelled, or Hello failed. Nothing was exported; retry when they are at the machine.' }); }@@ -3289,8 +3335,9 @@ async function exportSavedLogins(opts) { { logins: keep.map((r) => ({ url: r.url, username: r.username, password: r.password })) }); // note column deliberately absent cleanup(); // the browser's own plaintext copy never survives this verb 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 }); 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,+ return done({ ok: true, browser, profile: profileKey, loginCount: keep.length, siteCount: hosts.length, dialogVisibleMs, sites: hosts.slice(0, 40), filtered: !!wanted, steps, ...h, _hint: `Chrome exported ${keep.length} login(s) across ${hosts.length} site(s), and the values are NOT in this reply: they are in a file on the USER'S machine, readable only by them. ` + `Hand handoffPath to pup (it runs on that machine) and call nbrowser_drop_handoff {handoffId} the moment it has imported. The file self-deletes in ${h.expiresInSec}s either way. ` +
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "nb-bridge",- "version": "0.16.50",+ "version": "0.16.51", "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.",