← 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
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+0.16.58
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.",