← 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.

John Lauer ·44a53f2949 ·2mo ago ·parent daf2054
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\ No newline at end of file+0.16.7\ 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.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.",