← Commit history
Publish 0.11.0
8 files changed
+440−103
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">· 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()">×</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 · <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 · 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">⏮</button>+ <button class="dbtn" id="demo-play" onclick="demoToggle()" aria-label="Play or pause">⏸</button>+ <button class="dbtn" onclick="demoNext()" aria-label="Next step">⏭</button>+ <span class="dsep"></span>+ <button class="dbtn" id="demo-mute" onclick="demoMute()" aria-label="Mute narration">🔊</button>+ <span class="dstep" id="demo-stepno"></span>+ <span class="dsep"></span>+ <button class="dbtn" onclick="demoExit()" aria-label="Exit the demo">✕</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()">✕</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+')">“'+hiNoun(p)+'”'+ + '<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]"+ }+}