← Commit history
bin/nb-dashboard+167−82@@ -20,13 +20,29 @@ const { execFile, spawn } = require('child_process'); const fs = require('fs'); const crypto = require('crypto'); -const VERSION = '0.11.2'; // keep in lockstep with package.json (pkg version)-// EMERGENCY PARK (dock thread, 2026-08-24): the Send-to-AI SUCCESS path crashed the ah dev shell on-// ConfRoomROG during kicad's cold-verify - run-prompt is empirically NOT crash-safe (the gate fix does-// not cover it). While parked, /api/say-send answers {ok:false, parked:true} WITHOUT touching the-// control API at all; the page's failure path (error toast + clipboard fallback) carries users, and the-// two-button UI stays. The main ah build thread owns certification; flip to false on their broadcast.-const SAYSEND_PARKED = true;+const VERSION = '0.12.0'; // keep in lockstep with package.json (pkg version)+// SEND TO AI IS UN-PARKED (John: "yeah, we are re-enabling the send to ai. it seems to be good now").+// The ah build thread certified the crash family dead on the gate-fixed build and the delivery leg is+// proven end to end. The park const is DELETED rather than flipped, so no dead branch can resurrect it.+// SHELL-VERSION GATE: published Hydrogen 1.0.141 PREDATES the fix, so a stock 1.0.141 shell still+// carries the defect. Gate on GET /updater/status -> updater.current_version >= 1.0.142, compared+// NUMERICALLY (a lexical compare passes "1.0.99", which is OLDER and still crashes). Deliberately NOT+// /buildinfo: its build_sha lies on incremental builds, and build_epoch would pin the feature to one+// machine's build clock and refuse every user on a stock signed release forever. If /updater/status is+// unreachable we FAIL OPEN and let the call itself be the capability probe (falling back to copy on+// failure) - refusing everyone because a status endpoint hiccuped is the worse error.+const MIN_SHELL = [1, 0, 142];+function cmpVer(a, b) {+ const pa = String(a || '').split('.').map((n) => parseInt(n, 10) || 0);+ for (let i = 0; i < b.length; i++) { const x = pa[i] || 0; if (x !== b[i]) return x - b[i]; }+ return 0;+}+async function shellVersionOk() {+ const r = await httpJson('GET', controlUrl() + '/updater/status');+ const v = r.json && ((r.json.updater && r.json.updater.current_version) || r.json.current_version);+ if (!r.status || !v) return { ok: true, unknown: true }; // fail OPEN: the call is the probe+ return { ok: cmpVer(v, MIN_SHELL) >= 0, version: v };+} 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,@@ -72,6 +88,8 @@ function httpJson(method, url, body) { } async function saySend(phrase) { const base = controlUrl();+ const gate = await shellVersionOk();+ if (!gate.ok) return { ok: false, why: `your Hydrogen is ${gate.version} and Send to AI needs 1.0.142 or newer (1.0.141 carries a crash this would trigger) - update Hydrogen to enable it; the phrase is on your clipboard` }; try { 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);@@ -265,24 +283,29 @@ 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(--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; }+ /* dash-header 1.22.1: 44px, EDGE TO EDGE, padding inside the bar, no version anywhere */+ header { display:flex; align-items:center; gap:10px; height:44px; padding:0 16px; margin:0; border-radius:0;+ border-bottom:1px solid var(--line); background:var(--surface); }+ .brand { display:flex; align-items:center; gap:8px; position:relative; outline:none; }+ header h1 { font:600 15px var(--sans); color:var(--text); }+ .subtitle { font-size:13px; color:var(--muted); margin-left:0; }+ .brand-pop { display:none; position:absolute; top:34px; 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; }+ .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 { display:inline-flex; align-items:center; gap:7px; font:11.5px var(--mono); color:var(--muted);+ background:var(--surface2); 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); } .led.fail { background:var(--red); box-shadow:0 0 8px rgba(239,91,91,.6); } .led.unknown { background:#3a4a5a; }- main { max-width:980px; margin:0 auto; padding:24px 20px 40px; }+ main { padding:20px 20px 24px; } /* the CONTENT REGION owns the padding, not the panel: the gap under the header is identical panel-open and panel-closed */ .cards { display:grid; grid-template-columns:1fr 1fr; gap:16px; } @media (max-width:760px) { .cards { grid-template-columns:1fr; } } .card { background:var(--card); border:1px solid var(--edge); border-radius:12px; padding:18px 18px 16px; }@@ -301,23 +324,46 @@ 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 { 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); }+ /* newbie-panel 1.22.1: ONE full-width band, two columns, prompts as a TABLE (never pills) */+ .panel { background:var(--surface); border:1px solid var(--line); border-left:3px solid var(--teal);+ border-radius:12px; padding:18px 20px 20px; margin:0 0 20px; position:relative; }+ .panel-head { display:flex; align-items:flex-start; }+ .panel h2 { font:600 15px var(--sans); color:var(--text); }+ #panel-x { position:absolute; top:12px; right:12px; }+ .panel-explain { font-size:13.5px; color:var(--muted); margin:4px 0 14px; max-width:70ch; }+ .panel-explain .noun { color:var(--teal); font-weight:700; }+ .panel-cols { display:grid; grid-template-columns:220px 1fr; gap:24px; align-items:start; }+ .panel-right { border-left:1px solid var(--line); padding-left:24px; }+ @media (max-width:760px) { .panel-cols { grid-template-columns:1fr; }+ .panel-right { border-left:0; border-top:1px solid var(--line); padding-left:0; padding-top:16px; } }+ .demo-btn { display:inline-flex; align-items:center; gap:8px; background:var(--teal); color:#05221f;+ border:0; border-radius:8px; padding:10px 18px; font:600 14px var(--sans); cursor:pointer; }+ .demo-btn:hover { filter:brightness(1.07); } .demo-btn:active { transform:translateY(1px); }+ .demo-btn[disabled] { opacity:.6; cursor:default; filter:none; }+ .demo-btn:focus-visible { outline:2px solid var(--teal); outline-offset:2px; }+ .demo-sub { font-size:12px; color:var(--muted); margin-top:8px; }+ /* prompts TABLE: 40px rows, action cell tracks the PHRASE (left-packed), never the window edge */+ .prompts { display:block; }+ .prow { display:grid; grid-template-columns:minmax(0,max-content) 72px; align-items:center;+ height:40px; border-bottom:1px solid var(--line); cursor:pointer; }+ .prow:last-child { border-bottom:0; }+ .prow:hover { background:rgba(255,255,255,.03); }+ .pphrase { font:14px var(--body); color:var(--text); white-space:nowrap; overflow:hidden;+ text-overflow:ellipsis; padding-right:16px; }+ .pphrase em { color:var(--teal); font-style:normal; }+ .pacts { display:flex; gap:6px; }+ .iconbtn { width:28px; height:28px; display:inline-flex; align-items:center; justify-content:center;+ background:none; border:1px solid transparent; border-radius:6px; color:var(--muted); cursor:pointer; padding:0; }+ .iconbtn:hover { color:var(--teal); border-color:var(--line); background:rgba(0,184,177,.08); }+ .iconbtn:focus-visible { outline:2px solid var(--teal); outline-offset:2px; }+ .tip { position:fixed; z-index:60; background:var(--surface2); border:1px solid var(--line);+ border-radius:6px; padding:5px 9px; font-size:12px; color:var(--text); pointer-events:none;+ box-shadow:0 6px 18px rgba(0,0,0,.4); } .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; }+ .icon-btn[aria-pressed="true"] { color:var(--teal); background:rgba(0,184,177,.10); }+ .icon-btn.sm { width:28px; height:28px; } dialog.settings:focus, dialog.settings:focus-visible { outline:none; } /* we focus the dialog on open (open-at-top rule); the default focus ring painted a white border around the whole box */ dialog.settings { margin:auto; border:1px solid #30363d; border-radius:12px; padding:0; background:#161b22; color:#c9d1d9; width:min(880px, 94vw); overflow:hidden; } dialog.settings::backdrop { background:rgba(0,0,0,.25); }@@ -386,8 +432,8 @@ function page() { </style></head><body> <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>+ <img src="icon.svg" width="18" height="18" 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>@@ -395,34 +441,45 @@ function page() { </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>+ <button class="icon-btn" id="help" aria-pressed="true" aria-label="Hide the prompts panel" data-tooltip="Hide the prompts panel" onclick="togglePanel()"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M9.6 9.2a2.5 2.5 0 0 1 4.8.9c0 1.7-2.4 2.2-2.4 3.9"/><path d="M12 17.6h.01"/></svg></button>+ <button class="icon-btn" id="cog" aria-label="Settings" data-tooltip="Settings" onclick="openSettings()"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><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 9 19.4a1.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 4.6 9a1.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.33H9a1.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.82V9a1.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>- <h2><span class="led unknown" id="led-l1"></span>Native Browser</h2>- <p>Your AI opens pages in <strong>your real, signed-in browser</strong> - nothing to install. Real Chrome and Edge windows, your profiles, your logins.</p>- <div class="gain">open a URL in the right profile (work vs personal)</div>- <div class="gain">background by default: your focus is never stolen</div>- <div class="gain">perfect for OAuth consent: you just click Approve</div>- <p class="hintline" id="l1-detail"></p>+ <!-- newbie panel: a SIBLING of the app body, never inside its grid (newbie-panel 1.22.1) -->+ <section class="panel" id="panel">+ <div class="panel-head">+ <h2>Say it</h2>+ <button class="icon-btn sm" id="panel-x" aria-label="Close the prompts panel" data-tooltip="Close the prompts panel" onclick="closePanel()">×</button>+ </div>+ <p class="panel-explain">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>+ <div class="panel-cols">+ <div class="panel-left">+ <button class="demo-btn" id="demo-btn" onclick="demoStart()"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 4.8v14.4a.6.6 0 0 0 .92.5l11.3-7.2a.6.6 0 0 0 0-1l-11.3-7.2a.6.6 0 0 0-.92.5z"/></svg><span>Play the 1-minute demo</span></button>+ <div class="demo-sub">5 steps, about a minute</div>+ </div>+ <div class="panel-right">+ <div class="prompts" id="prompts" role="table" aria-label="Sample AI prompts"></div>+ </div> </div>- <div class="card upsell" id="card-l2">- <div class="layer">Layer 2 · the good stuff</div>- <h2><span class="led unknown" id="led-l2"></span>nbe — the Native Browser Extension</h2>- <div id="l2-body"></div>+ </section>+ <div class="appbody" id="appbody">+ <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>+ <h2><span class="led unknown" id="led-l1"></span>Native Browser</h2>+ <p>Your AI opens pages in <strong>your real, signed-in browser</strong> - nothing to install. Real Chrome and Edge windows, your profiles, your logins.</p>+ <div class="gain">open a URL in the right profile (work vs personal)</div>+ <div class="gain">background by default: your focus is never stolen</div>+ <div class="gain">perfect for OAuth consent: you just click Approve</div>+ <p class="hintline" id="l1-detail"></p>+ </div>+ <div class="card upsell" id="card-l2">+ <div class="layer">Layer 2 · the good stuff</div>+ <h2><span class="led unknown" id="led-l2"></span>nbe — the Native Browser Extension</h2>+ <div id="l2-body"></div>+ </div> </div> </div> </main>@@ -482,6 +539,8 @@ function page() { </div> </dialog> <script>+const ICON_COPY = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12" rx="2.5"/><path d="M6 15H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v1"/></svg>';+const ICON_SEND = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.5 2.5 10.5 13.5"/><path d="M21.5 2.5 14.8 21.5l-4.3-8-8-4.3z"/></svg>'; const UPSELL = '<p><strong>Not installed yet</strong> - and it is the single best upgrade to your Adom experience. With the nbe your AI can:</p>' + '<div class="gain">log into sites with your own saved passwords (pup never can)</div>' + '<div class="gain">click and type with trusted, human-grade input - on a background window</div>'@@ -526,11 +585,13 @@ 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;+ document.getElementById('prompts').innerHTML = window.__say.map((p,i)=>+ '<div class="prow" role="row" onclick="sayCopy('+i+')">'+ + '<div class="pphrase" role="cell" title="'+p.replace(/"/g,'"')+'">“'+hiNoun(p)+'”</div>'+ + '<div class="pacts" role="cell">'+ + '<button class="iconbtn" aria-label="Copy this prompt to your clipboard" data-tooltip="Copy this prompt to your clipboard" onclick="event.stopPropagation();sayCopy('+i+')">'+ICON_COPY+'</button>'+ + '<button class="iconbtn" aria-label="Send this prompt to a new AI thread" data-tooltip="Send this prompt to a new AI thread" onclick="event.stopPropagation();saySendTo('+i+',this)">'+ICON_SEND+'</button>'+ + '</div></div>').join(''); } catch(e){} } async function sayCopy(i){@@ -539,31 +600,53 @@ async function sayCopy(i){ else toast('Could not copy automatically - select the phrase and copy it yourself.', false); } 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 = '...';+ // ONE attempt, defensive: any failure error-toasts AND copies, so the user is never stranded.+ const phrase = window.__say[i]; btn.disabled = true; 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(); }+ btn.disabled = false;+ if (r && r.ok) { toast('Sent to a new AI thread.', true); gradSave({ aiDriven: true }); } else { await writeClipboard(phrase); toast((r && r.why) || 'Could not reach the AI panel; copied instead.', false); } }+// ---- tooltips (600ms, body-appended, viewport-clamped; NEVER a title attr alongside) ----+let _tipEl=null,_tipT=null;+document.addEventListener('mouseover',(e)=>{+ const t=e.target.closest('[data-tooltip]'); if(!t) return;+ clearTimeout(_tipT);+ _tipT=setTimeout(()=>{+ _tipEl=document.createElement('div'); _tipEl.className='tip'; _tipEl.textContent=t.getAttribute('data-tooltip');+ document.body.appendChild(_tipEl);+ const r=t.getBoundingClientRect(), tr=_tipEl.getBoundingClientRect();+ _tipEl.style.left=Math.max(6,Math.min(r.left,innerWidth-tr.width-6))+'px';+ _tipEl.style.top=(r.bottom+6)+'px';+ },600);+});+document.addEventListener('mouseout',(e)=>{ if(e.target.closest('[data-tooltip]')){ clearTimeout(_tipT); if(_tipEl){_tipEl.remove();_tipEl=null;} } }); // ---- 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(){+// The panel is ONE state driven by BOTH the X and the header toggle - they never drift apart.+// The X FULLY closes it (no residual one-liner: that stub is retired), and the app body moves up+// because the content region owns the page padding, not the panel.+let _panelOpen = true;+function panelShouldOpen(){ 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';-}+ if (g.panelClosed === true) return false;+ if (g.panelClosed === false) return true; // an explicit re-open sticks+ return !((g.dismissed||{}).say || g.aiDriven || (g.launches||0) >= 3);+}+function setPanel(open){+ _panelOpen = open;+ document.getElementById('panel').hidden = !open;+ const h = document.getElementById('help');+ h.setAttribute('aria-pressed', open ? 'true' : 'false');+ const label = open ? 'Hide the prompts panel' : 'Show the prompts panel';+ h.setAttribute('aria-label', label); h.setAttribute('data-tooltip', label);+}+function togglePanel(){ const n = !_panelOpen; gradSave({ panelClosed: !n }); setPanel(n); }+function closePanel(){ gradSave({ panelClosed: true }); setPanel(false); }+function applyGrad(){ setPanel(panelShouldOpen()); } let _launchCounted = false; function countLaunch(state){ if (_launchCounted || state !== 'ok') return; // a launch is successful when the row goes green@@ -578,6 +661,8 @@ 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;+ const db = document.getElementById('demo-btn');+ if (db) { db.disabled = true; db.querySelector('span').textContent = 'Demo running'; } document.getElementById('demo-mute').textContent = _demo.muted ? '\uD83D\uDD07' : '\uD83D\uDD0A'; _demo.i = -1; _demo.playing = true; demoAdvance(1); }@@ -622,6 +707,8 @@ async function demoExit(){ clearTimeout(_demo.timer); const au = document.getElementById('demo-audio'); au.pause(); au.src = ''; document.getElementById('demo-bar').hidden = true;+ const db = document.getElementById('demo-btn');+ if (db) { db.disabled = false; db.querySelector('span').textContent = 'Play the 1-minute demo'; } _demo.playing = false; _demo.i = -1; try { await fetch('api/demo/exit', { method:'POST' }); } catch {} }@@ -870,9 +957,7 @@ function startServer() { 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 = SAYSEND_PARKED- ? { ok: false, parked: true, why: 'Send to AI is temporarily paused (a platform fix is being verified) - copied to your clipboard instead' }- : phrase ? await saySend(phrase) : { ok: false, why: 'phrase required' };+ 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;
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "nb-bridge",- "version": "0.11.2",+ "version": "0.12.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.",
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.12.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]"+ }+}