← Commit history
v0.16.7 (insiders): Send to AI uses Hydrogen's AI picker (adom/nb-bridge#44): no type on a new thread, so the human picks the AI; the shared module's request is rewritten on the way through (type stripped, app:adom/nb-bridge tag added, reply verbatim); the dashboard's own say-send sends background:true and polls the run-prompt job to terminal, returning Hydrogen's human line, the picked AI and picked_by, and treating cancelled as final.
5 files changed
+62−24
bin/nb-dashboard+58−20@@ -20,7 +20,7 @@ const { execFile, spawn } = require('child_process'); const fs = require('fs'); const crypto = require('crypto'); -const VERSION = '0.16.6'; // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.7'; // 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.@@ -74,6 +74,27 @@ function controlUrl() { } // Raw pass-through to the control API: bytes in, bytes out. `u` is the path+query exactly as the page // asked for it. Only the transport is ours; the payload is Hydrogen's.+// AI PICKER (adom/nb-bridge#44, Hydrogen 1.0.213+): when a NEW thread is asked for from this dashboard,+// the HUMAN chooses the AI in Hydrogen's picker (5 s countdown to their default, Escape cancels). That+// happens when the request names no `type`, so the shared module's request is rewritten on the way+// through: `type` stripped, the `app:adom/nb-bridge` tag added (the picker's "remember for this app"+// keys on it; the dock: tag stays for the dock). Only the REQUEST body changes; the reply still passes+// through byte for byte, so `cancelled:true`, `job`, `state` reach the page exactly as Hydrogen sent them.+const APP_TAG = 'app:adom/nb-bridge';+function sendBodyForPicker(u, method, body) {+ if (method !== 'POST' || !u.startsWith('/ai-threads/run-prompt') || u.startsWith('/ai-threads/run-prompt/')) return body;+ try {+ const j = JSON.parse(body || '{}');+ if (j && typeof j === 'object' && j.new === true) {+ delete j.type;+ const tags = Array.isArray(j.tags) ? j.tags.slice() : [];+ if (!tags.includes(APP_TAG)) tags.push(APP_TAG);+ j.tags = tags;+ return JSON.stringify(j);+ }+ } catch {}+ return body;+} function proxyRaw(method, u, body, res) { let up; try { up = new URL(controlUrl() + u); } catch { up = null; } const fail = (why) => {@@ -112,36 +133,53 @@ async function saySend(phrase) { // ONE CALL composite. NOT the two-step new-then-send-by-sid flow: /ai-threads/new has NEVER // returned an id (a thread has no sid until its first submitted prompt), so a client that reads // sid||id from /new always aborts and ships a Send button that opens a tab and delivers nothing.- // I shipped exactly that in 0.12.0 and it was caught in review before John pressed it. // `new:true` inside run-prompt creates the thread; name and tags stage on the SAME request.+ //+ // AI PICKER + JOB STATUS (adom/nb-bridge#44, Hydrogen 1.0.213/1.0.218+): no `type`, so the human picks+ // the AI in Hydrogen's picker; `background:true` returns {job, state:"accepted"} at once and the rest+ // runs detached, so the picker's 5 to 45 s never sits inside a request timeout. Then poll+ // GET /ai-threads/run-prompt/status?job=<job> once a second until terminal:true. Every answer carries+ // `human`, the ready toast line for that phase; `cancelled` (Escape in the picker) is final: never+ // retry, never fall back to naming an AI. Older Hydrogen (no job in the answer) keeps the pre-1.0.187+ // delivered_to reading below. const base = controlUrl(); const gate = await shellVersionOk(); if (!gate.ok) return { ok: false, why: `your Hydrogen is ${gate.version} and Send to AI needs 1.0.142 or newer (1.0.141 carries a crash this would trigger) - update Hydrogen to enable it; the phrase is on your clipboard` }; try {- // 120s: the tab CREATE alone can take 60s on a loaded workbench, and a short client timeout- // turns a working send into a spurious "could not reach the AI panel". const r = await httpJson('POST', base + '/ai-threads/run-prompt', {- new: true, prompt: phrase, submit: true,- name: 'NB - say it', tags: ['dock:nb-bridge'],+ new: true, prompt: phrase, submit: true, background: true,+ name: 'NB - say it', tags: ['dock:nb-bridge', APP_TAG], }, 120000); if (!(r.status >= 200 && r.status < 300)) { const hint = r.json && (r.json._hints ? (typeof r.json._hints === 'string' ? r.json._hints : JSON.stringify(r.json._hints)) : r.json.error); return { ok: false, why: hint || (r.status ? `the AI panel refused the prompt (status ${r.status})` : `AI panel unreachable at ${base}`) }; }- // THREE states, not two (adom-ui-design 1.28.0). delivered_to is "a diff, not a guess" and is- // EMPTY when verification has not caught up, so reading it alone reports FAILURE on a send that- // actually worked and then copy-fallbacks on top of a prompt the user already received (kicad hit- // exactly this on its first live send: delivered_to empty, inject.delivered_in_conversation true).- const inj = (r.json && r.json.inject) || {};- const delivered = (r.json && r.json.delivered_to) || null;- const confirmed = (delivered && String(delivered).length) || inj.delivered_in_conversation === true;- if (confirmed) return { ok: true, state: 'delivered', delivered_to: delivered, inject: inj, body: r.json };- if (r.json && r.json.ok === true) {- // OPENED but UNCONFIRMED: never an error, never a copy-fallback - the user may already have it.- return { ok: true, state: 'unconfirmed', delivered_to: delivered, inject: inj, tid: r.json.tid || null, body: r.json };+ const j = r.json || {};+ if (j.cancelled === true || j.state === 'cancelled') return { ok: false, cancelled: true, state: 'cancelled', why: j.human || 'you dismissed the AI picker; nothing was opened or sent', job: j.job || null };+ if (!j.job) {+ // Pre-1.0.187 shape: delivered_to is a diff, not a guess, and EMPTY when verification has not caught up.+ const inj = j.inject || {};+ const delivered = j.delivered_to || null;+ const confirmed = (delivered && String(delivered).length) || inj.delivered_in_conversation === true;+ if (confirmed) return { ok: true, state: 'delivered', delivered_to: delivered, inject: inj, body: j };+ if (j.ok === true) return { ok: true, state: 'unconfirmed', delivered_to: delivered, inject: inj, tid: j.tid || null, body: j };+ return { ok: false, state: 'failed', why: 'the AI panel did not confirm the send', body: j };+ }+ const started = Date.now(); const timeline = []; let last = j;+ while (Date.now() - started < 120000) {+ await new Promise((res) => setTimeout(res, 1000));+ const st = await httpJson('GET', base + '/ai-threads/run-prompt/status?job=' + encodeURIComponent(j.job), null, 8000);+ if (!st.json) continue;+ last = st.json;+ const state = last.state || (last.job && last.job.state) || '';+ if (!timeline.length || timeline[timeline.length - 1].state !== state) timeline.push({ t: Math.round((Date.now() - started) / 100) / 10, state, human: last.human || null });+ if (last.terminal === true || state === 'cancelled') break; }- const hint2 = r.json && r.json._hints ? (typeof r.json._hints === 'string' ? r.json._hints : JSON.stringify(r.json._hints)) : null;- return { ok: false, state: 'failed', why: hint2 || 'the AI panel did not confirm the send', body: r.json };+ const state = last.state || (last.job && last.job.state) || 'unknown';+ const picked = last.job ? { type: last.job.type || null, picked_by: last.job.picked_by || null } : null;+ const ok = !(['cancelled', 'rejected', 'open_failed', 'open_unconfirmed', 'tab_not_found', 'inject_failed', 'submit_failed', 'failed', 'timeout'].includes(state));+ return { ok, cancelled: state === 'cancelled', state, human: last.human || null, picked, job: j.job, terminal: last.terminal === true, timeline,+ why: ok ? undefined : (last.human || state) }; } catch (e) { return { ok: false, why: String(e && e.message || e) }; } } // ---- the guided demo (dash-demo contract) ----------------------------------@@ -951,7 +989,7 @@ function startServer() { if ((u.startsWith('/updater/status') && req.method === 'GET') || (u.startsWith('/ai-threads/run-prompt/status') && req.method === 'GET') || (u.startsWith('/ai-threads/run-prompt') && !u.startsWith('/ai-threads/run-prompt/') && req.method === 'POST')) {- let b = ''; req.on('data', (c) => (b += c)); req.on('end', () => proxyRaw(req.method, u, b, res));+ let b = ''; req.on('data', (c) => (b += c)); req.on('end', () => proxyRaw(req.method, u, sendBodyForPicker(u, req.method, b), res)); return; } if (u.startsWith('/api/say-send') && req.method === 'POST') {
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.16.6+0.16.7
bridge/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Native Browser",- "version": "0.16.6",+ "version": "0.16.7", "expectedExtensionVersion": "0.2.10", "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.6",+ "version": "0.16.7", "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",
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "nb-bridge",- "version": "0.16.6",+ "version": "0.16.7", "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.",