← Commit history
bin/nb-dashboard+359−96
@@ -20,12 +20,7 @@ const { execFile, spawn } = require('child_process'); const fs = require('fs'); const crypto = require('crypto'); -const VERSION = '0.10.4';-// SHIP-NIGHT PARK (dock-bar thread relaying John's ruling, 2026-08-23): /ai-threads/run-prompt has an-// unverified crash fix (it has killed the whole ah process) and ANY /ai-threads call wakes ah's parked-// AI backend - so "Try it" could crash the app the user is standing in. Parked, not removed: flip this-// to false when the dock thread clears the panel's typing path. The Say it strip stays copy-only.-const TRYIT_PARKED = true;           // keep in lockstep with package.json (pkg version)+const VERSION = '0.11.0';           // keep in lockstep with package.json (pkg version) const path = require('path'); // TEACH THE NOUN (John, 2026-08-23): "I say 'open digikey in nb' cuz for that I might know I need nb // since it has my login. Or 'open mouser in nb chrome personal'..." The phrases users should SAY,@@ -45,9 +40,12 @@ function sayPhrases() {   } catch {}   return SAY_FALLBACK; }-// "Try it": stage a phrase into this app's OWN Hydrogen AI thread (tag app:adom/nb-bridge) via the-// control API, unsent (submit:false) so the user stays in control. Control URL comes from the-// discovery file Hydrogen writes each launch; 47084 is only the default.+// SEND TO AI (John's product ruling, 2026-08-24, relayed by the dock thread): the new-thread path of+// the AI-threads API is cleared by ruling (gate fix at the cdp_eval primitive is the engineering basis;+// the formal certification probe never completed). Implement DEFENSIVELY per the dock thread's spec:+// create the thread first, ONE run-prompt attempt in a try/catch, ANY failure = {ok:false, why} so the+// page error-toasts and falls back to copy. No retry loops (the historical failure mode was panel-wide).+// Resume / typing into EXISTING threads stays parked - this backend has no code path that can do it. function controlUrl() {   try { const u = fs.readFileSync(path.join(require('os').homedir(), '.adom', 'hydrogen-control-url'), 'utf8').trim(); if (u) return u.replace(/\/$/, ''); } catch {}   return 'http://127.0.0.1:47084';@@ -57,7 +55,7 @@ function httpJson(method, url, body) {     try {       const u = new URL(url);       const data = body ? JSON.stringify(body) : null;-      const req = http.request({ host: u.hostname, port: u.port, path: u.pathname + u.search, method, timeout: 4000,+      const req = http.request({ host: u.hostname, port: u.port, path: u.pathname + u.search, method, timeout: 5000,         headers: data ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } : {} }, (res) => {         let b = ''; res.on('data', (c) => (b += c)); res.on('end', () => { let j = null; try { j = JSON.parse(b); } catch {} resolve({ status: res.statusCode, json: j, text: b.slice(0, 300) }); });       });@@ -66,36 +64,77 @@ function httpJson(method, url, body) {     } catch (e) { resolve({ status: 0, error: e.message }); }   }); }-async function tryPhrase(prompt) {-  // Lookup order measured against the live control API (dock-bar thread, 2026-08-23):-  //   1. CENSUS  GET /ai-threads -> {ok, groups:[{threads:[{tid, type, label, sid|null, tags:[]}]}]}-  //      (rows live at groups[].threads[]; sid is null for a fresh staged thread -> address by tid)-  //   2. HISTORY GET /ai-threads/history?tag=... -> {ok, items:[{ai, id, title, updated_ms}], open_excluded}-  //      (closed threads only; POST /ai-threads/resume {ai,id} reopens, then run-prompt {sid:id})-  //   3. neither -> honest copy fallback (the page copies the phrase and says why)+async function saySend(phrase) {   const base = controlUrl();-  const TAG = 'app:adom/nb-bridge';-  const census = await httpJson('GET', base + '/ai-threads');-  if (!census.status) return { ok: false, staged: false, reason: 'control API unreachable at ' + base, control: base };-  const rows = [];-  for (const g of ((census.json && census.json.groups) || [])) for (const t of (g.threads || [])) rows.push(t);-  const open = rows.find((t) => Array.isArray(t.tags) && t.tags.includes(TAG));-  if (open) {-    const addr = open.sid ? { sid: open.sid } : { tid: open.tid };-    const r = await httpJson('POST', base + '/ai-threads/run-prompt', { ...addr, prompt, submit: false });-    return { ok: r.status >= 200 && r.status < 300, staged: r.status >= 200 && r.status < 300, via: 'census', ...addr, status: r.status, detail: r.text || r.error, control: base };+  try {+    const mk = await httpJson('POST', base + '/ai-threads/new', { name: 'Native Browser - say it', tags: ['dock:nb-bridge'] });+    const sid = mk.json && (mk.json.sid || mk.json.id);+    if (!(mk.status >= 200 && mk.status < 300) || !sid) return { ok: false, why: mk.status ? ('could not create a thread (status ' + mk.status + ')') : ('AI panel unreachable at ' + base) };+    const run = await httpJson('POST', base + '/ai-threads/run-prompt', { sid, prompt: phrase, submit: true });+    if (run.status >= 200 && run.status < 300) return { ok: true, sid };+    return { ok: false, why: 'thread created but the prompt did not send (status ' + run.status + '); the staged thread expires on its own', sid };+  } catch (e) { return { ok: false, why: String(e && e.message || e) }; }+}+// ---- the guided demo (dash-demo contract) ----------------------------------+// One step list; the page owns pacing/audio, this backend owns ACTIONS and ab CAPTIONS. Captions go+// through desktop_caption with ONLY text+id+duration - never size/position (fleet ruling: the bridge+// default look, identical across every app's demo). The demo drives a real nb window in its own+// session and exit is safe from any step: clear caption, close the session window, forget state.+const DEMO_SID = 'nbdashdemo';+const HARNESS_URL = 'https://wiki.adom.inc/blob/app/nb-bridge/demo/test-harness.html';+const DEMO_STEPS = [+  { id: 'intro', caption: 'This is nb: your AI, driving your real signed-in browser.', audio: 'demo/audio/intro.mp3', min_ms: 5000, action: null },+  { id: 'open', caption: 'Opening DigiKey in your real browser, in the background. Your focus is never stolen.', audio: 'demo/audio/open.mp3', min_ms: 7000, action: 'open' },+  { id: 'shot', caption: 'Taking a screenshot of the page while the window stays behind your work.', audio: 'demo/audio/shot.mp3', min_ms: 6000, action: 'shot' },+  { id: 'click', caption: 'Clicking with trusted, human-grade input on a background window.', audio: 'demo/audio/click.mp3', min_ms: 7000, action: 'click' },+  { id: 'wrap', caption: 'That is the nbe, layer 2 of nb. Ask your AI to install it in any browser you use.', audio: 'demo/audio/wrap.mp3', min_ms: 6000, action: 'wrap' },+];+let _demoTarget = null;+function demoProfileArgs() { return { sessionId: DEMO_SID, thread: 'nb-dashboard', purpose: 'guided dashboard demo' }; }+async function demoCaption(text) {+  const t = _demoTarget; if (!t) return;+  await cli(['--target', t, '--ai-thread', 'nb-dashboard', 'desktop_caption', JSON.stringify({ text, id: 'nb-demo', duration: 30000 })], 10000);+}+async function demoStep(id) {+  const step = DEMO_STEPS.find((x) => x.id === id);+  if (!step) return { ok: false, why: 'unknown step' };+  if (!_demoTarget) {+    const tg = (cache.targets || [])[0];+    if (!tg) return { ok: false, why: 'no desktop connected' };+    _demoTarget = tg.name;   }-  const hist = await httpJson('GET', base + '/ai-threads/history?tag=' + encodeURIComponent(TAG));-  const item = ((hist.json && hist.json.items) || [])[0];-  if (item && item.id) {-    const re = await httpJson('POST', base + '/ai-threads/resume', { ai: item.ai, id: item.id });-    if (re.status >= 200 && re.status < 300) {-      const r = await httpJson('POST', base + '/ai-threads/run-prompt', { sid: item.id, prompt, submit: false });-      return { ok: r.status >= 200 && r.status < 300, staged: r.status >= 200 && r.status < 300, via: 'history-resumed', sid: item.id, status: r.status, detail: r.text || r.error, control: base };+  const t = _demoTarget;+  await demoCaption(step.caption);+  try {+    if (step.action === 'open') {+      const r = await cli(['--target', t, '--ai-thread', 'nb-dashboard', 'nbrowser_open_window', JSON.stringify({ ...demoProfileArgs(), url: 'https://www.digikey.com' })], 30000);+      return { ok: r.ok === true || r.status === 'ok', detail: 'window opened in the background' };     }-    return { ok: false, staged: false, reason: 'nb thread exists in history but could not be resumed (status ' + re.status + ')', control: base };-  }-  return { ok: false, staged: false, reason: 'no nb AI thread yet - open nb from the dock once, then Try it stages into it', control: base };+    if (step.action === 'shot') {+      const r = await cli(['--target', t, '--ai-thread', 'nb-dashboard', 'nbrowser_screenshot', JSON.stringify({ sessionId: DEMO_SID })], 60000);+      return { ok: r.ok === true, detail: 'captured ' + (r.width || '?') + 'x' + (r.height || '?') + ' without foregrounding', shot: r.base64 ? 'data:image/png;base64,' + r.base64 : null };+    }+    if (step.action === 'click') {+      await cli(['--target', t, '--ai-thread', 'nb-dashboard', 'nbrowser_navigate', JSON.stringify({ sessionId: DEMO_SID, url: HARNESS_URL })], 30000);+      await cli(['--target', t, '--ai-thread', 'nb-dashboard', 'nbrowser_wait_for', JSON.stringify({ sessionId: DEMO_SID, selector: '#btn-one' })], 30000);+      const r = await cli(['--target', t, '--ai-thread', 'nb-dashboard', 'nbrowser_click', JSON.stringify({ sessionId: DEMO_SID, selector: '#btn-one' })], 30000);+      return { ok: r.ok === true, detail: 'trusted click dispatched (isTrusted:true), window still background' };+    }+    if (step.action === 'wrap') {+      const r = await cli(['--target', t, '--ai-thread', 'nb-dashboard', 'nbrowser_close_window', JSON.stringify({ sessionId: DEMO_SID })], 20000);+      return { ok: true, detail: 'demo window closed - nothing left behind' };+    }+    return { ok: true };+  } catch (e) { return { ok: false, why: String(e && e.message || e) }; }+}+async function demoExit() {+  const t = _demoTarget; _demoTarget = null;+  if (!t) return { ok: true };+  try {+    await cli(['--target', t, '--ai-thread', 'nb-dashboard', 'desktop_caption', JSON.stringify({ text: '', id: 'nb-demo' })], 8000);+    await cli(['--target', t, '--ai-thread', 'nb-dashboard', 'nbrowser_close_window', JSON.stringify({ sessionId: DEMO_SID })], 15000);+  } catch {}+  return { ok: true }; } // BUILD = hash of this very file. Version-aware reuse compared the VERSION string alone and happily // kept serving a stale process whose code had changed under the same version (2026-08-23) - the@@ -176,9 +215,19 @@ async function refresh() {         : { label: 'Layer 2 - nbe (extension)', state: 'fail', detail: 'no desktop to check', _hint: 'Connect Adom Bridge first.' });   // Rollup is OURS: the dashboard is healthy when layer 1 works; a missing nbe is a warn (that is   // the upsell state, not an outage).+  const prev = new Map((cache.facets || []).map((f) => [f.label, f]));+  for (const f of facets) { const p = prev.get(f.label); f.since = (p && p.state === f.state && p.since) ? p.since : Date.now(); }   cache.facets = facets;   cache.targets = targetRows;   cache.matrix = matrix;+  try {+    const t0 = targets[0];+    if (t0) {+      const act = await cli(['--target', t0.name, '--ai-thread', 'nb-dashboard', 'nbrowser_activity', '{"limit":30}']);+      const rows2 = (act && act.activeSessions) || [];+      cache.aiDriven = rows2.some((r) => r.thread && !/^nb[- ]dashboard$/i.test(String(r.thread)));+    }+  } catch {}   cache.state = l1ok ? (l2ready ? 'ok' : 'warn') : (targets.length ? 'warn' : 'fail');   cache.at = Date.now(); }@@ -191,8 +240,15 @@ function page() { <title>NB Dashboard</title> <link rel="icon" href="icon.svg" type="image/svg+xml"> <style>-  :root { --bg:#0b1117; --card:#121a23; --edge:#1e2a36; --ink:#e8eef4; --dim:#8aa0b4; --teal:#00cec2; --amber:#f3b23e; --red:#ef5b5b; }+  :root { --bg:#0d1117; --surface:#161b22; --surface2:#1c232c; --line:#262d36;+    --text:#e6edf3; --muted:#8b949e; --teal:#00b8b1;+    --led-green:#3fb950; --led-amber:#d29922; --led-red:#f85149;+    --sans:"Familjen Grotesk", system-ui, sans-serif; --body:system-ui,-apple-system,"Segoe UI",sans-serif;+    --mono:"JetBrains Mono", ui-monospace, Consolas, monospace;+    /* legacy aliases so the settings dialog + cards keep rendering */+    --card:var(--surface); --edge:var(--line); --ink:var(--text); --dim:var(--muted); --amber:var(--led-amber); --red:var(--led-red); }   * { box-sizing:border-box; margin:0; }+  [hidden] { display:none !important; }   /* a display:flex class must never beat the hidden attribute */   /* house scrollbar (adom-ui-design scrollbars sub-skill, John 2026-08-18): slim dark pill, teal on hover,      transparent track - consistent with the wiki and the dock rail */   * { scrollbar-width: thin; scrollbar-color: rgba(139,148,158,.38) transparent; }@@ -203,10 +259,18 @@ function page() {   ::-webkit-scrollbar-corner { background: transparent; }   html, body { overflow-x: hidden; }   /* wide content scrolls in its own container, never the page */   body { background:var(--bg); color:var(--ink); font:15px/1.5 system-ui,-apple-system,'Segoe UI',sans-serif; min-height:100vh; }-  header { display:flex; align-items:center; gap:12px; padding:0 20px; height:44px; border-bottom:1px solid var(--edge); background:#0d141c; }-  .mark { font-weight:800; font-size:19px; letter-spacing:-.5px; background:linear-gradient(100deg,#00e6dc,#39b8ff 60%,#8c6bf7); -webkit-background-clip:text; background-clip:text; color:transparent; }-  header h1 { font-size:15px; font-weight:600; }-  header .ver { color:var(--dim); font-size:12px; margin-left:auto; }+  header { display:flex; align-items:center; gap:12px; padding:0 20px; height:44px; border-bottom:1px solid var(--line); background:#10151c; }+  .brand { display:flex; align-items:center; gap:9px; position:relative; outline:none; }+  header h1 { font:600 15px var(--sans); }+  .subtitle { color:var(--muted); font-size:13px; }+  .brand-pop { display:none; position:absolute; top:38px; left:0; z-index:40; background:var(--surface2); border:1px solid var(--line); border-radius:10px; padding:10px 14px; min-width:230px; font-size:12.5px; box-shadow:0 12px 32px rgba(0,0,0,.45); }+  .brand:hover .brand-pop, .brand:focus .brand-pop { display:block; transition-delay:.6s; }+  .brand:hover .brand-pop { animation:popin .01s .6s both; } @keyframes popin { from { opacity:0; } to { opacity:1; } }+  .pop-row { display:flex; justify-content:space-between; gap:18px; color:var(--muted); padding:2px 0; } .pop-row span:last-child { color:var(--text); font-family:var(--mono); font-size:11.5px; }+  .brand-pop a { color:var(--teal); text-decoration:none; font-size:12px; display:block; margin-top:6px; }+  .chips { display:flex; gap:8px; margin-left:auto; }+  .tchip { display:inline-flex; align-items:center; gap:7px; font:11.5px var(--mono); color:var(--muted); background:var(--surface); border:1px solid var(--line); border-radius:999px; padding:3px 11px; }+  .tchip .dot { width:7px; height:7px; border-radius:50%; background:var(--led-green); }   .led { width:10px; height:10px; border-radius:50%; display:inline-block; flex:none; }   .led.ok { background:var(--teal); box-shadow:0 0 8px rgba(0,206,194,.7); }   .led.warn { background:var(--amber); box-shadow:0 0 8px rgba(243,178,62,.6); }@@ -231,16 +295,20 @@ function page() {   .mini { background:transparent; border:1px solid var(--teal); color:var(--teal); border-radius:6px; padding:2px 10px; font-size:12px; cursor:pointer; }   .mini:hover { background:rgba(0,206,194,.12); }   tr.off td { color:var(--dim); opacity:.75; }-  .say { margin:0 0 18px; border:1px solid var(--edge); border-radius:12px; background:linear-gradient(180deg,#121a23,#0f161e); padding:14px 16px 12px; }-  .say-head { font-size:11px; text-transform:uppercase; letter-spacing:.14em; color:var(--teal); margin-bottom:10px; }-  .say-row { display:flex; flex-wrap:wrap; gap:8px; }-  .chip { display:inline-flex; align-items:center; gap:0; border:1px solid var(--edge); border-radius:999px; overflow:hidden; background:#0a0f14; }-  .chip .ph { padding:7px 12px; font:13px/1.2 ui-monospace,Consolas,monospace; color:var(--ink); cursor:pointer; }-  .chip .ph b { color:var(--teal); font-weight:700; }-  .chip .ph:hover { background:rgba(0,206,194,.08); }-  .chip .try { padding:7px 11px; font-size:12px; font-weight:700; color:#04211f; background:var(--teal); border:0; cursor:pointer; }-  .chip .try:hover { filter:brightness(1.08); }-  .say-note { margin-top:9px; font-size:12px; color:var(--dim); } .say-note em { color:var(--ink); font-style:normal; }+  .say { background:var(--surface); border:1px solid var(--line); border-left:3px solid var(--teal); border-radius:10px; padding:14px 16px; margin-bottom:14px; position:relative; }+  .say h2 { font:600 15px var(--sans); margin:0; }+  .say-head p { color:var(--muted); font-size:13.5px; margin:4px 0 0; max-width:70ch; } .say-head p b { color:var(--teal); }+  .say-x { position:absolute; top:8px; right:10px; background:none; border:0; color:var(--muted); font-size:17px; cursor:pointer; border-radius:6px; width:26px; height:26px; } .say-x:hover { color:var(--text); background:rgba(255,255,255,.06); }+  .say-chips { margin-top:10px; }+  .say-chip { display:inline-flex; align-items:center; gap:8px; background:var(--surface2); border:1px solid var(--line); border-radius:999px; padding:5px 6px 5px 12px; font:13.5px var(--body); margin:4px 6px 0 0; cursor:pointer; }+  .say-chip em { color:var(--teal); font-style:normal; }+  .say-chip .copy, .say-chip .send { font:500 11px var(--body); background:none; border:1px solid var(--line); border-radius:999px; padding:2px 9px; cursor:pointer; }+  .say-chip .copy { color:var(--muted); } .say-chip .send { color:var(--teal); border-color:rgba(0,184,177,.45); }+  .say-chip:hover .copy { color:var(--teal); border-color:var(--teal); } .say-chip .send:hover { background:rgba(0,184,177,.12); }+  .say-demo-row { display:flex; align-items:center; gap:12px; margin-top:12px; }+  .say-folded { display:flex; align-items:center; gap:10px; color:var(--muted); font-size:13px; background:var(--surface); border:1px solid var(--line); border-radius:8px; padding:7px 12px; margin-bottom:14px; }+  .say-note { font-size:12px; color:var(--muted); }+   .icon-btn { background:transparent; border:0; color:var(--dim); width:32px; height:32px; border-radius:8px; display:inline-flex; align-items:center; justify-content:center; cursor:pointer; transition:background .12s, color .12s, transform .06s; }   .icon-btn:hover { background:rgba(255,255,255,.07); color:var(--ink); } .icon-btn:active { transform:translateY(1px) scale(.96); } .icon-btn:focus-visible { outline:2px solid var(--teal); outline-offset:2px; }   header #cog { margin-left:10px; }@@ -293,21 +361,48 @@ function page() {   table { width:100%; border-collapse:collapse; font-size:13px; }   th { text-align:left; color:var(--dim); font-weight:500; font-size:11.5px; text-transform:uppercase; letter-spacing:.08em; padding:4px 8px; border-bottom:1px solid var(--edge); }   td { padding:6px 8px; border-bottom:1px solid rgba(30,42,54,.5); }-  .facets { margin-top:20px; display:flex; flex-direction:column; gap:8px; }-  .facet { display:flex; align-items:center; gap:10px; background:var(--card); border:1px solid var(--edge); border-radius:10px; padding:10px 14px; font-size:13.5px; }-  .facet .d { color:var(--dim); margin-left:auto; text-align:right; font-size:12.5px; }+  .leds { display:flex; flex-wrap:wrap; gap:8px; margin-bottom:18px; }+  .led-pill { display:inline-flex; align-items:center; gap:7px; background:var(--surface); border:1px solid var(--line); border-radius:999px; padding:4px 12px; font-size:12.5px; cursor:pointer; }+  .led-pill .dot { width:8px; height:8px; border-radius:50%; background:#3a4a5a; }+  .led-pill[data-state=ok] .dot { background:var(--led-green); }+  .led-pill[data-state=busy] .dot { background:var(--led-amber); }+  .led-pill[data-state=bad] .dot { background:var(--led-red); }+  .led-pill .st { color:var(--muted); }+  .led-pop { position:fixed; z-index:45; background:var(--surface2); border:1px solid var(--line); border-radius:10px; padding:12px 14px; max-width:420px; font-size:12.5px; color:var(--text); box-shadow:0 12px 32px rgba(0,0,0,.5); }+  .led-pop .hint { color:var(--muted); margin-top:6px; } .led-pop code { font-family:var(--mono); font-size:11px; background:rgba(110,118,129,.2); padding:1px 4px; border-radius:4px; }+  .demo-bar { position:fixed; left:50%; bottom:24px; transform:translateX(-50%); display:flex; align-items:center; gap:6px; background:var(--surface2); border:1px solid var(--line); border-radius:999px; padding:6px 10px; z-index:48; box-shadow:0 10px 30px rgba(0,0,0,.5); }+  .dbtn { background:none; border:0; color:var(--text); font-size:15px; width:32px; height:32px; border-radius:50%; cursor:pointer; } .dbtn:hover { background:rgba(255,255,255,.08); } .dbtn:active { transform:translateY(1px); }+  .dsep { width:1px; height:18px; background:var(--line); margin:0 3px; }+  .dstep { font:11.5px var(--mono); color:var(--muted); padding:0 4px; }   footer { text-align:center; color:var(--dim); font-size:12px; padding:18px; }   footer a { color:var(--teal); text-decoration:none; }   .age { font-size:11px; color:var(--dim); } </style></head><body>-<header><span class="mark">nb</span><h1>Native Browser</h1><span class="led unknown" id="topled"></span><span class="ver">v${VERSION} ยท <span class="age" id="age"></span></span>-<button class="icon-btn" id="cog" aria-label="Settings" title="Settings" onclick="openSettings()"><svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true"><path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97 0-.33-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1 0 .33.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66Z"/></svg></button></header>-<main>-  <div class="say" id="say">-    <div class="say-head">Say it to your AI</div>-    <div class="say-row" id="say-row"></div>-    <div class="say-note" id="say-note">nb is the noun. Add the browser and profile when it matters: <em>nb chrome personal</em>, <em>nb edge</em>.</div>+<header>+  <div class="brand" id="brand" tabindex="0">+    <img src="icon.svg" width="20" height="20" alt="">+    <h1>Native Browser</h1><span class="subtitle">&middot; AI browser control</span>+    <div class="brand-pop" id="brand-pop" role="tooltip">+      <div class="pop-row"><span>Dashboard</span><span>v${VERSION}</span></div>+      <div id="pop-boxes"></div>+      <a href="https://wiki.adom.inc/adom/nb-bridge" target="_blank" rel="noopener">adom/nb-bridge on the wiki</a>+    </div>   </div>+  <div class="chips" id="target-chips"></div>+  <button class="icon-btn" id="help" aria-label="Show the getting-started help" title="Show help" onclick="recallScaffolding()">?</button>+  <button class="icon-btn" id="cog" aria-label="Settings" title="Settings" onclick="openSettings()"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><circle cx="12" cy="12" r="3.2"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33h.01a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82v.01a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></button>+</header>+<main>+  <section class="say" id="say">+    <div class="say-head"><h2>Say it</h2>+      <p>You never need this page's buttons: just tell your AI what you want, using the word <b class="noun">nb</b>. Copy a phrase, or send it straight to a new AI thread.</p>+      <button class="say-x" aria-label="Collapse" onclick="dismissSay()">&times;</button></div>+    <div class="say-chips" id="say-chips"></div>+    <div class="say-demo-row" id="demo-entry"><button class="btn" onclick="demoStart()">Play the 1-minute demo</button><span class="say-note" id="say-note"></span></div>+  </section>+  <div class="say-folded" id="say-folded" hidden><span>Say it &middot; <span id="fold-count"></span> phrases</span><button class="mini" onclick="unfoldSay()">Show</button></div>+  <div class="leds" id="leds"></div>+  <div class="led-pop" id="led-pop" hidden></div>   <div class="cards">     <div class="card" id="card-l1">       <div class="layer">Layer 1 &middot; works out of the box</div>@@ -324,9 +419,18 @@ function page() {       <div id="l2-body"></div>     </div>   </div>-  <div class="facets" id="facets"></div>-  <div id="profiles"></div> </main>+<div class="demo-bar" id="demo-bar" hidden>+  <button class="dbtn" onclick="demoPrev()" aria-label="Previous step">&#9198;</button>+  <button class="dbtn" id="demo-play" onclick="demoToggle()" aria-label="Play or pause">&#9208;</button>+  <button class="dbtn" onclick="demoNext()" aria-label="Next step">&#9197;</button>+  <span class="dsep"></span>+  <button class="dbtn" id="demo-mute" onclick="demoMute()" aria-label="Mute narration">&#128266;</button>+  <span class="dstep" id="demo-stepno"></span>+  <span class="dsep"></span>+  <button class="dbtn" onclick="demoExit()" aria-label="Exit the demo">&#10005;</button>+</div>+<audio id="demo-audio"></audio> <footer><a href="https://wiki.adom.inc/adom/nb-bridge" target="_blank" rel="noopener">adom/nb-bridge on the wiki</a></footer> <dialog class="settings" id="settings" tabindex="-1" aria-labelledby="settings-title">   <div class="settings-head"><h2 id="settings-title">NB Settings</h2><button class="icon-btn" aria-label="Close settings" title="Close" onclick="document.getElementById('settings').close()">&#x2715;</button></div>@@ -382,21 +486,10 @@ const UPSELL = '<p><strong>Not installed yet</strong> - and it is the single bes   + '<div class="prompt"><code id="pr">install the adom native browser extension in my chrome</code><button onclick="copyPrompt()">Copy</button></div>'   + '<p class="hintline">Paste that into any Adom AI thread to start.</p>'; function copyPrompt(){ try { navigator.clipboard.writeText(document.getElementById('pr').textContent); } catch(e){} }-function hiNoun(p){ return p.replace(/\bnb\b( chrome| edge)?( personal| work)?/g, (m)=>'<b>'+m+'</b>').replace('adom native browser extension','<b>adom native browser extension</b>'); }-async function loadSay(){-  try {-    const r = await fetch('api/say'); const d = await r.json();-    document.getElementById('say-row').innerHTML = (d.say||[]).map((p,i)=>-      '<span class="chip"><span class="ph" title="click to copy" onclick="copySay('+i+',this)">'+hiNoun(p)+'</span><button class="try" onclick="trySay('+i+',this)">Try it</button></span>').join('');-    window.__say = d.say || [];-  } catch(e){}-}-function copySay(i, el){ try { navigator.clipboard.writeText(window.__say[i]); const o=el.innerHTML; el.textContent='copied'; setTimeout(()=>{el.innerHTML=o;},900); } catch(e){} } function writeClipboard(text){-  // navigator.clipboard needs a secure context + permission, and an UNFOCUSED document makes-  // writeText HANG ("document is not focused" - hit by parts-search 1.1.2 in background windows).-  // So: synchronous hasFocus() check routes straight to the in-gesture execCommand fallback, and the-  // async path races an 800ms timer so a silent hang still resolves to the fallback.+  // navigator.clipboard hangs on an UNFOCUSED document ("document is not focused" - parts-search+  // 1.1.2): synchronous hasFocus() routes to the in-gesture execCommand fallback; the async path+  // races an 800ms timer so a silent hang still resolves.   return new Promise((resolve) => {     if (!document.hasFocus()) { resolve(fallbackCopy(text)); return; }     let done = false;@@ -422,16 +515,112 @@ function toast(msg, ok){   t.classList.add('show');   clearTimeout(_toastT); _toastT = setTimeout(() => t.classList.remove('show'), 4000); }-async function trySay(i, btn){-  // SHIP UX (John, 2026-08-23): Try it copies the prompt and SAYS SO - no AI-thread calls of any kind-  // (that path is parked server-side too; the staging code sleeps behind TRYIT_PARKED).-  const phrase = window.__say[i];-  const ok = await writeClipboard(phrase);+function hiNoun(p){ return p.replace(/\bnb\b( chrome| edge)?( personal| work)?/g, (m)=>'<em>'+m+'</em>').replace('adom native browser extension','<em>adom native browser extension</em>'); }+async function loadSay(){+  try {+    const r = await fetch('api/say'); const d = await r.json();+    window.__say = d.say || [];+    document.getElementById('say-chips').innerHTML = window.__say.map((p,i)=>+      '<span class="say-chip" data-i="'+i+'" onclick="sayCopy('+i+')">&ldquo;'+hiNoun(p)+'&rdquo;'+      + '<button class="copy" onclick="event.stopPropagation();sayCopy('+i+')">Copy</button>'+      + '<button class="send" onclick="event.stopPropagation();saySendTo('+i+',this)">Send to AI</button></span>').join('');+    document.getElementById('fold-count').textContent = window.__say.length;+  } catch(e){}+}+async function sayCopy(i){+  const ok = await writeClipboard(window.__say[i]);   if (ok) toast('Copied to clipboard. Paste it into any AI thread.', true);   else toast('Could not copy automatically - select the phrase and copy it yourself.', false); }-loadSay();-// ---- settings modal ----+async function saySendTo(i, btn){+  // one attempt, defensive (John's 2026-08-24 ruling: new threads only; failure = toast + copy fallback)+  const phrase = window.__say[i]; btn.disabled = true; btn.textContent = '...';+  let r = null;+  try { r = await fetch('api/say-send', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ phrase }) }).then(x=>x.json()); } catch(e){}+  btn.disabled = false; btn.textContent = 'Send to AI';+  if (r && r.ok) { toast('Sent to a new AI thread.', true); gradSave({ aiDriven: true }); applyGrad(); }+  else { await writeClipboard(phrase); toast((r && r.why) || 'Could not reach the AI panel; copied instead.', false); }+}+// ---- newbie graduation (localStorage; this port is its own origin so state is per-app) ----+const GRAD_KEY = 'adom-dash-graduation';+function gradState(){ try { return JSON.parse(localStorage.getItem(GRAD_KEY)) ?? {}; } catch { return {}; } }+function gradSave(patch){ try { localStorage.setItem(GRAD_KEY, JSON.stringify({ ...gradState(), ...patch })); } catch {} }+let _recalled = false; try { _recalled = sessionStorage.getItem('adom-dash-recall') === '1'; } catch {}+function recallScaffolding(){ try { sessionStorage.setItem('adom-dash-recall','1'); } catch {} _recalled = true; applyGrad(); }+function dismissSay(){ gradSave({ dismissed: { ...(gradState().dismissed||{}), say: true } }); _recalled = false; try { sessionStorage.removeItem('adom-dash-recall'); } catch {} applyGrad(); }+function unfoldSay(){ recallScaffolding(); }+function applyGrad(){+  const g = gradState();+  const launches = g.launches || 0;+  const demoFolded = !_recalled && (launches >= 3 || g.aiDriven || (g.dismissed||{}).say);+  const sayFolded = !_recalled && ((g.dismissed||{}).say || g.aiDriven || launches >= 5);   // the say strip folds LAST+  document.getElementById('say').hidden = sayFolded;+  document.getElementById('say-folded').hidden = !sayFolded;+  document.getElementById('demo-entry').style.display = demoFolded && !sayFolded ? 'none' : 'flex';+}+let _launchCounted = false;+function countLaunch(state){+  if (_launchCounted || state !== 'ok') return;   // a launch is successful when the row goes green+  _launchCounted = true;+  gradSave({ launches: (gradState().launches || 0) + 1 });+  applyGrad();+}+// ---- guided demo (dash-demo): page paces steps; backend runs actions + ab captions ----+let _demo = { steps: [], i: -1, playing: false, muted: false, timer: null, actionDone: false, audioDone: false, minDone: false };+try { _demo.muted = localStorage.getItem('adom-demo-muted') === '1'; } catch {}+async function demoStart(){+  try { const d = await fetch('api/demo').then(x=>x.json()); _demo.steps = d.steps || []; } catch { toast('Could not load the demo.', false); return; }+  if (!_demo.steps.length) { toast('Could not load the demo.', false); return; }+  document.getElementById('demo-bar').hidden = false;+  document.getElementById('demo-mute').textContent = _demo.muted ? '\uD83D\uDD07' : '\uD83D\uDD0A';+  _demo.i = -1; _demo.playing = true; demoAdvance(1);+}+function demoStepDone(){+  if (_demo.actionDone && _demo.audioDone && _demo.minDone && _demo.playing) demoAdvance(1);+}+async function demoRun(idx){+  const st = _demo.steps[idx]; if (!st) { demoExit(); return; }+  _demo.i = idx; _demo.actionDone = false; _demo.audioDone = false; _demo.minDone = false;+  document.getElementById('demo-stepno').textContent = 'step ' + (idx+1) + '/' + _demo.steps.length;+  const au = document.getElementById('demo-audio');+  au.pause(); au.src = st.audio; au.muted = _demo.muted; au.currentTime = 0;+  au.onended = () => { _demo.audioDone = true; demoStepDone(); };+  au.play().catch(() => { _demo.audioDone = true; });   // autoplay refusal must not wedge the demo+  clearTimeout(_demo.timer); _demo.timer = setTimeout(() => { _demo.minDone = true; demoStepDone(); }, st.min_ms || 4000);+  try { const r = await fetch('api/demo/step', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ id: st.id }) }).then(x=>x.json());+    if (r && r.ok === false && r.why) toast(r.why, false);+  } catch(e){}+  _demo.actionDone = true; demoStepDone();+}+function demoAdvance(dir){+  const next = _demo.i + dir;+  if (next >= _demo.steps.length) { demoExit(); return; }+  if (next < 0) return;+  demoRun(next);+}+function demoToggle(){+  _demo.playing = !_demo.playing;+  const au = document.getElementById('demo-audio');+  if (_demo.playing) { au.play().catch(()=>{}); document.getElementById('demo-play').textContent = '\u23F8'; demoStepDone(); }+  else { au.pause(); document.getElementById('demo-play').textContent = '\u25B6'; }+}+function demoPrev(){ demoAdvance(-1); }+function demoNext(){ demoAdvance(1); }+function demoMute(){+  _demo.muted = !_demo.muted;+  document.getElementById('demo-audio').muted = _demo.muted;+  document.getElementById('demo-mute').textContent = _demo.muted ? '\uD83D\uDD07' : '\uD83D\uDD0A';+  try { localStorage.setItem('adom-demo-muted', _demo.muted ? '1' : '0'); } catch {}+}+async function demoExit(){+  clearTimeout(_demo.timer);+  const au = document.getElementById('demo-audio'); au.pause(); au.src = '';+  document.getElementById('demo-bar').hidden = true;+  _demo.playing = false; _demo.i = -1;+  try { await fetch('api/demo/exit', { method:'POST' }); } catch {}+}+loadSay(); applyGrad();+// ---- settings modal ----// ---- settings modal ---- const dlg = document.getElementById('settings'); dlg.addEventListener('click', (e) => {   // click OUTSIDE closes; a click on the dialog's own padding does not   if (e.target !== dlg) return;@@ -513,8 +702,9 @@ function led(el, s){ el.className = 'led ' + (s||'unknown'); } async function tick(){   try {     const r = await fetch('api/status'); const d = await r.json();-    led(document.getElementById('topled'), d.state);-    document.getElementById('age').textContent = d.cacheAgeMs < 60000 ? 'live' : 'stale';+    renderPills(d); renderChips(d); renderPop(d);+    countLaunch(d.state);+    if (d.aiDriven && !gradState().aiDriven) { gradSave({ aiDriven: true }); applyGrad(); }     const f = {}; for (const x of d.facets) f[x.label] = x;     const l1 = f['Layer 1 - Native Browser'] || {}; const l2 = f['Layer 2 - nbe (extension)'] || {};     led(document.getElementById('led-l1'), l1.state); led(document.getElementById('led-l2'), l2.state);@@ -551,9 +741,52 @@ async function tick(){         + '<div id="install-explain"></div>';       window.__matrix = m;     }-    document.getElementById('facets').innerHTML = d.facets.map(x =>-      '<div class="facet"><span class="led '+(x.state||'unknown')+'"></span>'+x.label+'<span class="d">'+(x.detail||'')+'</span></div>').join('');-  } catch(e) { led(document.getElementById('topled'), 'unknown'); }+  } catch(e) {}+}+// ---- status pills (status-leds): dot + label + short state; prose only on tap; stuck? past 15 min ----+const PILL_LABELS = { 'Adom Bridge': 'Bridge', 'Layer 1 - Native Browser': 'Layer 1 nb', 'Layer 2 - nbe (extension)': 'Layer 2 nbe' };+function pillState(f){+  if (f.state === 'ok') return 'ok';+  if (f.state === 'warn') return (f.since && Date.now() - f.since > 15*60*1000) ? 'busy-stuck' : 'busy';+  return 'bad';+}+function shortState(f, ps){+  if (ps === 'busy-stuck') return 'stuck?';+  const d = String(f.detail || '');+  return d.length > 34 ? d.slice(0, 32) + '\u2026' : d || f.state;+}+let _facets = [];+function renderPills(d){+  _facets = [{ label: 'Dashboard', state: 'ok', detail: 'v' + d.version + ' serving', _hint: 'This page and its /api/status are served by nb-dashboard on 127.0.0.1:8954.' }, ...(d.facets || [])];+  document.getElementById('leds').innerHTML = _facets.map((f, i) => {+    const ps = pillState(f);+    return '<button class="led-pill" data-state="' + (ps === 'busy-stuck' ? 'busy' : ps) + '" onclick="pillPop(' + i + ',this)"><span class="dot"></span>'+      + (PILL_LABELS[f.label] || f.label) + '<span class="st">' + shortState(f, ps) + '</span></button>';+  }).join('');+}+function pillPop(i, el){+  const f = _facets[i]; const pop = document.getElementById('led-pop');+  if (!pop.hidden && pop.dataset.i == i) { pop.hidden = true; return; }+  const stuck = pillState(f) === 'busy-stuck';+  pop.innerHTML = '<strong>' + f.label + '</strong><div>' + (f.detail || '') + '</div>'+    + (f._hint ? '<div class="hint">' + f._hint + '</div>' : '')+    + (stuck ? '<div class="hint">This has been in this state over 15 minutes - likely stuck. Refresh the bridge from Settings, or ask your AI: <code>check nb readiness</code>.</div>' : '')+    + (f.since ? '<div class="hint">in this state since ' + new Date(f.since).toLocaleTimeString() + '</div>' : '');+  pop.dataset.i = i; pop.hidden = false;+  const r = el.getBoundingClientRect();+  pop.style.left = Math.min(r.left, innerWidth - 440) + 'px'; pop.style.top = (r.bottom + 8) + 'px';+}+document.addEventListener('click', (e) => { const pop = document.getElementById('led-pop'); if (!pop.hidden && !pop.contains(e.target) && !e.target.closest('.led-pill')) pop.hidden = true; });+// ---- header: target chips + version popover content ----+function renderChips(d){+  document.getElementById('target-chips').innerHTML = (d.targets || []).map((t) =>+    '<span class="tchip"><span class="dot"' + (t.state ? '' : ' style="background:var(--led-red)"') + '></span>target: ' + t.name + '</span>').join('');+}+function renderPop(d){+  document.getElementById('pop-boxes').innerHTML = (d.targets || []).map((t) => {+    const vs = Object.entries(t.extensionVersions || {}).map(([p, v]) => p.split(':')[0] + ' v' + v).join(', ');+    return '<div class="pop-row"><span>' + t.name + '</span><span>' + (vs || 'nb bridge') + '</span></div>';+  }).join(''); } tick(); setInterval(tick, 5000); </script>@@ -571,7 +804,7 @@ function startServer() {     if (u.startsWith('/api/') && req.method === 'OPTIONS') { res.writeHead(204, CORS); res.end(); return; }     if (u.startsWith('/api/status')) {       res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', ...CORS });-      res.end(JSON.stringify({ state: cache.state, version: VERSION, build: BUILD, facets: cache.facets, targets: cache.targets, matrix: cache.matrix || [], cacheAgeMs: Date.now() - cache.at }));+      res.end(JSON.stringify({ state: cache.state, version: VERSION, build: BUILD, facets: cache.facets, targets: cache.targets, matrix: cache.matrix || [], aiDriven: !!cache.aiDriven, cacheAgeMs: Date.now() - cache.at }));       return;     }     if (u.startsWith('/icon.svg')) {@@ -628,20 +861,50 @@ function startServer() {       })();       return;     }+    if (u.startsWith('/api/say-send') && req.method === 'POST') {+      let b = ''; req.on('data', (c) => (b += c)); req.on('end', async () => {+        let phrase = ''; try { phrase = String(JSON.parse(b).phrase || ''); } catch {}+        const out = phrase ? await saySend(phrase) : { ok: false, why: 'phrase required' };+        res.writeHead(200, { 'Content-Type': 'application/json', ...CORS }); res.end(JSON.stringify(out));+      });+      return;+    }     if (u.startsWith('/api/say')) {       res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', ...CORS });-      res.end(JSON.stringify({ say: sayPhrases(), tryit: !TRYIT_PARKED }));+      res.end(JSON.stringify({ say: sayPhrases() }));       return;     }-    if (u.startsWith('/api/try') && req.method === 'POST') {-      if (TRYIT_PARKED) { res.writeHead(503, { 'Content-Type': 'application/json', ...CORS }); res.end(JSON.stringify({ ok: false, staged: false, parked: true, reason: 'Try it is parked for tonight\'s ship (the AI-thread path is being repaired dock-side) - copy the phrase instead' })); return; }+    // demo engine: the page drives steps; ACTIONS run here (bridge verbs), captions are ab captions at+    // the bridge DEFAULT size/position (text only - the fleet-consistency ruling), audio ships as+    // pre-created adom-tts mp3s under demo/audio/.+    if (u.includes('/demo/audio/')) {+      const nm = u.split('/demo/audio/')[1].split('?')[0].replace(/[^a-z0-9._-]/gi, '');+      try { const mp3 = fs.readFileSync(path.join(__dirname, '..', 'demo', 'audio', nm)); res.writeHead(200, { 'Content-Type': 'audio/mpeg', 'Cache-Control': 'max-age=600', ...CORS }); res.end(mp3); }+      catch { res.writeHead(404); res.end(); }+      return;+    }+    if (u.startsWith('/api/demo/step') && req.method === 'POST') {       let b = ''; req.on('data', (c) => (b += c)); req.on('end', async () => {-        let phrase = ''; try { phrase = String(JSON.parse(b).phrase || ''); } catch {}-        const out = phrase ? await tryPhrase(phrase) : { ok: false, reason: 'phrase required' };+        let id = ''; try { id = String(JSON.parse(b).id || ''); } catch {}+        const out = await demoStep(id);         res.writeHead(200, { 'Content-Type': 'application/json', ...CORS }); res.end(JSON.stringify(out));       });       return;     }+    if (u.startsWith('/api/demo/exit') && req.method === 'POST') {+      demoExit().then((out) => { res.writeHead(200, { 'Content-Type': 'application/json', ...CORS }); res.end(JSON.stringify(out)); });+      return;+    }+    if (u.startsWith('/api/demo')) {+      res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', ...CORS });+      res.end(JSON.stringify({ steps: DEMO_STEPS.map(({ id, caption, audio, min_ms }) => ({ id, caption, audio, min_ms })) }));+      return;+    }+    if (u.startsWith('/api/health')) {   // status-leds: the dock and the pill row read ONE truth+      res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', ...CORS });+      res.end(JSON.stringify({ state: cache.state, version: VERSION, build: BUILD, facets: cache.facets, cacheAgeMs: Date.now() - cache.at }));+      return;+    }     if (u.startsWith('/api/shutdown') && req.method === 'POST') {       res.writeHead(200, { 'Content-Type': 'application/json', ...CORS });       res.end(JSON.stringify({ ok: true, bye: VERSION }));
demo/audio/click.mp3added
⋯ 1 unchanged line ⋯
demo/audio/intro.mp3added
⋯ 1 unchanged line ⋯
demo/audio/open.mp3added
⋯ 1 unchanged line ⋯
demo/audio/shot.mp3added
⋯ 1 unchanged line ⋯
demo/audio/wrap.mp3added
⋯ 1 unchanged line ⋯
package.json+6−1
@@ -1,6 +1,6 @@ {   "slug": "nb-bridge",-  "version": "0.10.4",+  "version": "0.11.0",   "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.",@@ -26,6 +26,11 @@     "docs/img/settings/activity-progress.png",     "docs/img/settings/activity-flash.png",     "docs/img/settings/activity-off.png",+    "demo/audio/intro.mp3",+    "demo/audio/open.mp3",+    "demo/audio/shot.mp3",+    "demo/audio/click.mp3",+    "demo/audio/wrap.mp3",     "dockbar.json",     "install.sh",     "uninstall.sh",
page.json+75−6
@@ -16,10 +16,10 @@     "chrome",     "edge",     "nbrowser",-    "ai-agent",-    "developer-tools",-    "vibe-coding",-    "native-messaging"+    "pup",+    "native-messaging",+    "credentials",+    "chip-fetcher"   ],   "status": "live",   "discovery_triggers": [@@ -70,5 +70,74 @@   },   "visibility": "public",   "brief": "Your real browser, driven by AI.",-  "discovery_pitch": "nbe (the Adom Native Browser Extension, layer 2 of nb) drives the user's REAL signed-in Chrome/Edge via nbrowser_* verbs: use it for LOGGING IN, filling forms, and downloading behind auth with their OWN saved passwords - pup cannot (it is a fresh Chrome-for-Testing with no logins). Next moves: run 'adom-bridge bridge_list' to check it is installed; if not, OFFER to install it (see the installing-the-extension skill) - never make the user do it. If this container lacks the skills, 'adom-wiki pkg install adom/nb-bridge'. Check readiness with 'adom-bridge nbrowser_readiness' (state:needs-extension means offer the install)."-}+  "discovery_pitch": "nbe (the Adom Native Browser Extension, layer 2 of nb) drives the user's REAL signed-in Chrome/Edge via nbrowser_* verbs: use it for LOGGING IN, filling forms, and downloading behind auth with their OWN saved passwords - pup cannot (it is a fresh Chrome-for-Testing with no logins). Next moves: run 'adom-bridge bridge_list' to check it is installed; if not, OFFER to install it (see the installing-the-extension skill) - never make the user do it. If this container lacks the skills, 'adom-wiki pkg install adom/nb-bridge'. Check readiness with 'adom-bridge nbrowser_readiness' (state:needs-extension means offer the install).",+  "version": "0.11.0",+  "type": "app",+  "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.",+  "install": {+    "binary_name": "nb-bridge"+  },+  "files": [+    "screenshots/hero.png",+    "skills/adom-native-browser/SKILL.md",+    "skills/driving-sites/SKILL.md",+    "skills/driving-the-desktop/SKILL.md",+    "skills/driving-the-extension/SKILL.md",+    "skills/driving-the-extension-demo/SKILL.md",+    "skills/installing-the-extension/SKILL.md",+    "skills/native-browser-recording/SKILL.md",+    "skills/native-browser-safety/SKILL.md",+    "skills/reading-google-messages/SKILL.md",+    "skills/uninstalling-the-extension/SKILL.md",+    "bin/nb-dashboard",+    "docs/img/dock-nb.svg",+    "docs/img/settings/overlay-on.png",+    "docs/img/settings/overlay-off.png",+    "docs/img/settings/activity-progress.png",+    "docs/img/settings/activity-flash.png",+    "docs/img/settings/activity-off.png",+    "demo/audio/intro.mp3",+    "demo/audio/open.mp3",+    "demo/audio/shot.mp3",+    "demo/audio/click.mp3",+    "demo/audio/wrap.mp3",+    "dockbar.json",+    "install.sh",+    "uninstall.sh",+    "package.json"+  ],+  "scripts": {+    "install": "./install.sh",+    "uninstall": "./uninstall.sh"+  },+  "dependencies": {+    "adom/adom-bridge": "^2.0.3"+  },+  "sample_prompts": [+    {+      "label": "Open in my browser",+      "prompt": "open the wiki in my real chrome work profile"+    },+    {+      "label": "Log in for me",+      "prompt": "log into digikey for me using my saved password"+    },+    {+      "label": "Form behind a login",+      "prompt": "fill out this form on the site I'm already signed into"+    },+    {+      "label": "Download behind auth",+      "prompt": "download that datasheet, it's behind a login"+    },+    {+      "label": "Install the extension",+      "prompt": "install the adom native browser extension in my chrome"+    }+  ],+  "org": "adom",+  "author": {+    "name": "John Lauer",+    "email": "[email protected]"+  }+}\ No newline at end of file