← Commit history

v0.16.58 (insiders): nb-bridge#55 experience: the export path is staged on the clipboard up front and pasted into the save dialog with ctrl+a/ctrl+v/enter via SendInput (paste beats synthetic typing, which landed as SSSS); a Win32 stage scanner watches which window is actually on screen (Hello, the export page, the save dialog, including elevated ones ab's list omits) and drives step-by-step ab captions; if Windows blocks the input because the browser is elevated, the path is already on the clipboard and the caption tells the user to paste and press Enter

John Lauer ·00de1567d6 ·13d ago ·parent 9e2376c
6 files changed +84−65
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.57';           // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.58';           // 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.57\ No newline at end of file+0.16.58\ 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.57",+  "version": "0.16.58",   "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.57",+  "version": "0.16.58",   "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+79−60
@@ -3171,6 +3171,45 @@ const exportSayClear = () => abCommand(null, 'desktop_caption', { id: EXPORT_CAP // ab's window list does not return a dialog owned by an ELEVATED process (it will not click into one, so // it does not list it), which is why six runs reported "no save dialog appeared" while one sat in front // of the user. A plain Win32 enumeration sees it regardless, so ask the OS directly (John, 2026-10-29).+// ONE Win32 enumeration that classifies every window this flow cares about, so the caption can track+// the real stage (John, 2026-10-29: "watch the hwnds via ab to know what windows and steps are+// appearing so you can immediately update the ab captions"). ab's own list omits elevated windows;+// EnumWindows does not. Returns { hello, dialog:{hwnd,rect}, page:bool }.+async function scanExportStageWin32() {+  const ps = [+    'Add-Type @"',+    'using System;using System.Text;using System.Runtime.InteropServices;',+    'public class NBS{',+    ' [DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc f, IntPtr l);',+    ' [DllImport("user32.dll")] public static extern int GetWindowTextW(IntPtr h, StringBuilder s, int n);',+    ' [DllImport("user32.dll")] public static extern int GetClassNameW(IntPtr h, StringBuilder s, int n);',+    ' [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);',+    ' [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out R r);',+    ' public struct R{public int left,top,right,bottom;}',+    ' public delegate bool EnumProc(IntPtr h, IntPtr l);',+    '}',+    '"@',+    '$res = @{ hello=$false; page=$false; dialog=$null }',+    '[void][NBS]::EnumWindows({ param($h,$l)',+    '  if (-not [NBS]::IsWindowVisible($h)) { return $true }',+    '  $tb = New-Object System.Text.StringBuilder 300; [void][NBS]::GetWindowTextW($h,$tb,300); $t=$tb.ToString()',+    '  $cb = New-Object System.Text.StringBuilder 200; [void][NBS]::GetClassNameW($h,$cb,200); $c=$cb.ToString()',+    '  if ($t -match "Windows Security" -or $c -match "Credential Dialog Xaml Host") { $res.hello=$true }',+    '  if ($t -match "Google Password Manager|Password Manager - ") { $res.page=$true }',+    '  if ($t -match "Export Passwords|^Save As$" -and $c -match "#32770|Dialog") {',+    '    $r = New-Object NBS+R; [void][NBS]::GetWindowRect($h,[ref]$r)',+    '    $res.dialog = @{ hwnd=[int64]$h; left=$r.left; top=$r.top; right=$r.right; bottom=$r.bottom }',+    '  }',+    '  return $true }, [IntPtr]::Zero)',+    '$res | ConvertTo-Json -Compress -Depth 4',+  ].join('; ');+  try {+    const r = await abCommand(null, 'run_script', { interpreter: 'powershell', scriptB64: Buffer.from(ps, 'utf8').toString('base64'),+      reason: 'nb-bridge#55: see which export window is on screen right now, to narrate the step', timeoutSeconds: 20 });+    const out = String((r && r.body && (r.body.stdout || (r.body.data && r.body.data.stdout))) || '').trim();+    return JSON.parse(out || '{}');+  } catch { return {}; }+} async function findSaveDialogWin32() {   const ps = [     'Add-Type @"',@@ -3334,73 +3373,53 @@ async function exportSavedLogins(opts) {   }   steps.push(step('started the export', true, invoked)); -  // Chrome now raises Windows Hello, then a Save As dialog. Fill the path, commit, wait for bytes.-  // THE SAVE DIALOG IS NB'S JOB, NOT THE USER'S (John, 2026-10-29: "are you seriously showing me a csv-  // 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.`);+  // Set the target path on the clipboard NOW, before any dialog exists, so the paste is instant when it+  // does (John, 2026-10-29: "put the path you want ... into my clipboard, then just trigger a ctrl-v to+  // paste it in, then mimic hitting the enter key"). Paste beats synthetic typing, which landed as "SSSS".+  await abCommand(null, 'desktop_clipboard_set', { text: csvPath, reason: 'nb-bridge#55: stage the export path for a one-paste fill' }).catch(() => {});++  await exportSay(`Step 1 of 3: ${brand} is asking you to confirm it is you (Windows Hello). Approve it. Adom does the rest - do not close anything.`);   const watchSince = watchSince0;   const deadline = Date.now() + timeoutSec * 1000;-  let saved = false, foundFile = null, toldToSave = false;-  let sawDialogAt = 0, dialogVisibleMs = null, lastWin32Probe = 0;+  let saved = false, foundFile = null;+  let sawDialogAt = 0, dialogVisibleMs = null, lastScan = 0, stage = 'hello', filledAt = 0, fillTries = 0;   while (Date.now() < deadline) {-    await _sleep(300);-    // THE AUTHORITATIVE SIGNAL: did a passwords CSV appear? Whoever pressed Save, we are done.+    await _sleep(250);+    // The file appearing is the one true "done" - whoever or whatever pressed Save.     foundFile = newLoginCsvSince(watchSince, csvPath);     if (foundFile) { saved = true; if (sawDialogAt) dialogVisibleMs = Date.now() - sawDialogAt; break; }-    // After a few seconds with the dialog still up, stop pretending and give the user the one useful-    // instruction: press Save. nb takes it from there wherever Chrome puts it.-    if (sawDialogAt && !toldToSave && Date.now() - sawDialogAt > 4000) {-      toldToSave = true;-      await exportSay(`${brand} is asking where to save. Just press Save - Adom picks the file up from wherever it lands and deletes it straight after.`);-    }-    const wins = await abCommand(null, 'desktop_list_windows', {}).catch(() => null);-    const list = (() => { try { return (wins.body.data || wins.body).windows || []; } catch { return []; } })();-    let dlg = list.find((w) => (w.className === '#32770' || /^#32770/.test(String(w.className || '')))-      && /save|export|passwords/i.test(String(w.title || '')));-    // Not in ab's list? It may be elevated. Ask Win32 directly, but only every ~2s: this costs a-    // PowerShell spawn, and polling it five times a second would be its own problem.-    if (!dlg && Date.now() - lastWin32Probe > 2000) {-      lastWin32Probe = Date.now();-      const w32 = await findSaveDialogWin32();-      if (w32) dlg = { hwnd: w32.hwnd, title: w32.title, elevatedOrHidden: true,-        rect: { x: w32.left, y: w32.top, w: w32.right - w32.left, h: w32.bottom - w32.top, right: w32.right, top: w32.top } };-    }-    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 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' });++    // Scan the real OS windows every ~1.2s (one PowerShell spawn) so the caption tracks the actual step,+    // including windows ab's own list omits because they are elevated.+    if (Date.now() - lastScan > 1200) {+      lastScan = Date.now();+      const st = await scanExportStageWin32();+      if (st && st.dialog && st.dialog.hwnd) {+        if (!sawDialogAt) { sawDialogAt = Date.now(); stage = 'dialog'; }+        const d = st.dialog;+        const rect = { x: d.left, y: d.top, w: d.right - d.left, h: d.bottom - d.top, right: d.right, top: d.top };+        await exportSayBeside(rect, `Step 3 of 3: ${brand} is asking where to save. Adom is pasting the path and pressing Save for you - nothing for you to do. The file lands where only you can read it, pup imports it, then it is deleted.`);+        // Paste the staged path and commit, via SendInput. If Chrome is NOT elevated this lands; if it+        // IS elevated Windows blocks our input and the file simply never appears, which we handle below.+        fillTries++;+        await abCommand(null, 'desktop_bring_to_front', { hwnd: d.hwnd, reason: 'nb-bridge#55: focus the save dialog to paste the path' }).catch(() => {});+        await _sleep(250);+        await abCommand(null, 'desktop_press_key', { hwnd: d.hwnd, keys: 'ctrl+a', reason: 'nb-bridge#55: select the file name field' }).catch(() => {});+        await abCommand(null, 'desktop_press_key', { hwnd: d.hwnd, keys: 'ctrl+v', reason: 'nb-bridge#55: paste the staged export path' }).catch(() => {});+        await abCommand(null, 'desktop_press_key', { hwnd: d.hwnd, keys: 'enter', reason: 'nb-bridge#55: commit the export' }).catch(() => {});+        filledAt = Date.now();+      } else if (st && st.hello && stage === 'hello') {+        await exportSay(`Step 1 of 3: approve the ${brand} / Windows Hello prompt on your screen - that is the only thing Adom needs from you.`);+      } else if (st && st.page && stage === 'hello' && !sawDialogAt) {+        stage = 'exporting';+        await exportSay(`Step 2 of 3: Adom asked ${brand} to export your logins. The Windows Hello prompt is coming - approve it.`);+      }     }-    // 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;+    // If the dialog has been up for a few seconds and our paste-and-enter has not produced a file,+    // Windows is blocking our input (an elevated browser). Tell the user plainly and let them finish it.+    if (sawDialogAt && fillTries >= 2 && !foundFile && Date.now() - sawDialogAt > 5000) {+      await exportSay(`${brand} is asking where to save, and Windows is not letting Adom press the button (your browser is running elevated). The path is on your clipboard: press Ctrl+V then Enter, or just press Save. Adom picks the file up and deletes it.`, { persist: true });     }-    dialogVisibleMs = Date.now() - sawDialogAt;-    saved = true;-    break;   }   steps.push(step('answered the save dialog', saved, saved ? `the save dialog was on screen for ${dialogVisibleMs}ms` : 'no save dialog appeared'));   if (!saved) {
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "nb-bridge",-  "version": "0.16.57",+  "version": "0.16.58",   "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.",