← 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

John Lauer ·dcf1649fe6 ·13d ago ·parent 8a8e134
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\ No newline at end of file+0.16.51\ 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.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.",