← Commit history
bin/nb-dashboard+197−2
@@ -20,7 +20,7 @@ const { execFile, spawn } = require('child_process'); const fs = require('fs'); const crypto = require('crypto'); -const VERSION = '0.9.1';           // keep in lockstep with package.json (pkg version)+const VERSION = '0.10.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,@@ -227,6 +227,47 @@ function page() {   .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; }+  .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; }+  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); }+  .settings-head { display:flex; align-items:center; justify-content:space-between; padding:12px 14px 12px 18px; border-bottom:1px solid #30363d; }+  .settings-head h2 { font-size:15px; font-weight:700; color:#e6edf3; }+  .settings-body { max-height:70vh; overflow:auto; padding:18px; scrollbar-width:thin; scrollbar-color:rgba(255,255,255,.18) transparent; }+  .settings-body::-webkit-scrollbar { width:8px; } .settings-body::-webkit-scrollbar-thumb { background:rgba(255,255,255,.18); border-radius:8px; } .settings-body::-webkit-scrollbar-thumb:hover { background:var(--teal); }+  .settings-card { background:#1c2128; border:1px solid #30363d; border-left:3px solid color-mix(in srgb, #00b8b1 45%, transparent); border-radius:10px; padding:.9rem 1.1rem 1rem; margin-bottom:1rem; }+  .card-title { font-size:13px; font-weight:600; color:#e6edf3; display:flex; align-items:center; gap:10px; } .card-title .tt { color:#8fd3d4; }+  .card-status { font-size:11px; color:#8b949e; font-weight:400; margin-left:auto; } .card-status.ok { color:#3fb950; }+  .card-desc { font-size:12.5px; line-height:1.55; color:#8b949e; max-width:60ch; margin:.5rem 0 .75rem; } .card-desc code { font-size:11px; background:rgba(110,118,129,.2); padding:1px 4px; border-radius:4px; }+  .shots { display:flex; gap:14px; margin:0 0 .75rem; flex-wrap:wrap; }+  .setting-shot { margin:0; } .setting-shot img { display:block; image-rendering:pixelated; border:1px solid #30363d; border-radius:8px; background:#161b22; }+  .setting-shot figcaption { margin-top:.35rem; font-size:.75rem; color:#8b949e; text-align:center; }+  .setting-row { display:flex; align-items:center; gap:10px; padding:.4rem 0; }+  .row-label { font-size:.875rem; color:#c9d1d9; }+  .default-hint { font-size:.75rem; color:#8b949e; margin-top:.15rem; }+  .toggle { background:none; border:none; padding:2px; cursor:pointer; flex-shrink:0; transition:transform .06s; }+  .toggle-track { display:block; width:32px; height:18px; background:#21262d; border:1px solid #30363d; border-radius:9px; position:relative; transition:background .15s, border-color .15s; }+  .toggle:hover .toggle-track { border-color:#00b8b1; } .toggle:active { transform:translateY(1px) scale(.97); }+  .toggle[aria-checked="true"] .toggle-track { background:#00b8b1; border-color:#00b8b1; }+  .toggle-thumb { position:absolute; top:2px; left:2px; width:12px; height:12px; background:#fff; border-radius:50%; transition:transform .18s, width .12s; }+  .toggle[aria-checked="true"] .toggle-thumb { transform:translateX(14px); } .toggle:active .toggle-thumb { width:16px; } .toggle[aria-checked="true"]:active .toggle-thumb { transform:translateX(10px); }+  .toggle:focus-visible { outline:2px solid #00b8b1; outline-offset:2px; border-radius:10px; }+  .choice-set { border:none; margin:0; padding:0; display:flex; flex-wrap:wrap; gap:.75rem; }+  .choice-set legend { margin-bottom:.5rem; }+  .choice { flex:1 1 200px; display:flex; flex-direction:column; gap:.35rem; padding:.6rem; cursor:pointer; background:#161b22; border:1px solid #30363d; border-radius:8px; position:relative; transition:border-color .15s, background .15s, transform .06s; }+  .choice input { position:absolute; opacity:0; pointer-events:none; }+  .choice img { display:block; image-rendering:pixelated; border-radius:4px; background:#0b1117; }+  .choice-name { font-size:.875rem; font-weight:600; color:#e6edf3; } .choice-desc { font-size:.75rem; color:#8b949e; line-height:1.5; }+  .choice:hover { border-color:#00b8b1; background:#1c2128; } .choice:active { transform:translateY(1px) scale(.995); }+  .choice[data-selected="true"] { border-color:#00b8b1; box-shadow:inset 0 0 0 1px #00b8b1; } .choice:focus-within { outline:2px solid #00b8b1; outline-offset:2px; }+  select { background:#21262d; color:#e6edf3; border:1px solid #30363d; border-radius:6px; padding:5px 8px; font-size:13px; }+  .btn { background:#00b8b1; color:#06231f; border:0; border-radius:6px; padding:6px 12px; font-weight:600; font-size:.875rem; cursor:pointer; margin-left:auto; } .btn:hover { filter:brightness(1.08); } .btn:active { transform:translateY(1px); } .btn[disabled] { opacity:.6; cursor:default; }+  .console { background:#0a0f14; border:1px solid #30363d; border-radius:8px; padding:10px 12px; font:12px/1.5 ui-monospace,Consolas,monospace; color:#c9d1d9; max-height:200px; overflow:auto; white-space:pre-wrap; margin-top:.5rem; }+  .settings-footer { margin-top:1.5rem; padding-top:1rem; border-top:1px solid #30363d; display:flex; flex-direction:column; gap:.5rem; }+  .footer-note { font-size:.75rem; color:#8b949e; } .footer-note code { font-size:11px; }+  .restore-btn { width:fit-content; padding:.4rem .85rem; background:transparent; border:1px solid #30363d; border-radius:.375rem; color:#c9d1d9; font-size:.8125rem; font-weight:600; cursor:pointer; }+  .restore-btn:hover { background:#1c2128; } .restore-btn.armed { color:#f85149; border-color:rgba(255,107,107,.5); background:rgba(255,107,107,.08); }   .explain { margin-top:12px; border:1px solid rgba(0,206,194,.35); border-radius:10px; padding:12px 14px; background:rgba(0,206,194,.05); }   table { width:100%; border-collapse:collapse; margin-top:10px; 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); }@@ -238,7 +279,8 @@ function page() {   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></header>+<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>@@ -265,6 +307,49 @@ function page() {   <div id="profiles"></div> </main> <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>+  <div class="settings-body" id="settings-body">+    <div class="settings-card">+      <div class="card-title">Taskbar: <span class="tt">&ldquo;Adom is driving&rdquo; badge</span><span class="card-status" id="st-overlay"></span></div>+      <p class="card-desc">While nb is driving one of your browser windows, the bridge composites your own profile avatar with the Adom mark in its corner and paints it as the overlay icon on that window's Windows taskbar button. It is the trust tell: a glance at the taskbar says which windows an AI is working in and which profile it is using. The overlay goes on when a window is opened or driven and fades back to your plain avatar after about three minutes of no driving; it never blanks the overlay slot, so your profile avatar is always visible. Turning this off restores the native avatar on every window immediately.</p>+      <div class="shots">+        <figure class="setting-shot"><img src="assets/settings/overlay-on.png" alt="The Chrome taskbar button with the profile avatar carrying the small Adom mark in its corner" loading="lazy"><figcaption>On</figcaption></figure>+        <figure class="setting-shot"><img src="assets/settings/overlay-off.png" alt="The same button with the plain profile avatar and no Adom mark" loading="lazy"><figcaption>Off</figcaption></figure>+      </div>+      <div class="setting-row"><button class="toggle" id="tg-overlay" role="switch" aria-checked="true" aria-label="Adom is driving badge" onclick="setSetting('overlayBadge', !(this.getAttribute('aria-checked')==='true'))"><span class="toggle-track"><span class="toggle-thumb"></span></span></button><span class="row-label">Show the badge while nb is driving</span></div>+      <div class="default-hint">Default: On</div>+    </div>+    <div class="settings-card">+      <div class="card-title">Taskbar: <span class="tt">activity while a verb runs</span><span class="card-status" id="st-activity"></span></div>+      <p class="card-desc">What the driven window's taskbar button does from the moment nb dispatches a verb until the browser answers. The progress bar is determinate: the bridge keeps a rolling average of how long each verb usually takes on your machine and fills the bar over that estimate, never past 95% until the reply lands, so it cannot claim to be done early. Open-ended work (waiting for a page, recording, a login the AI is walking through) shows an indeterminate marquee instead. The orange flash is the older cue: it tells you work started, not how far along it is.</p>+      <fieldset class="choice-set" role="radiogroup" aria-labelledby="act-label" id="choice-activity">+        <legend id="act-label" class="row-label">Taskbar activity</legend>+        <label class="choice" data-value="progress"><input type="radio" name="taskbarActivity" value="progress" onchange="setSetting('taskbarActivity','progress')"><img src="assets/settings/activity-progress.png" alt="The taskbar button's underline filled about two thirds with a blue progress bar" loading="lazy"><span class="choice-name">Progress bar</span><span class="choice-desc">Fills over the verb's usual duration, so you can see how far along it is.</span></label>+        <label class="choice" data-value="flash"><input type="radio" name="taskbarActivity" value="flash" onchange="setSetting('taskbarActivity','flash')"><img src="assets/settings/activity-flash.png" alt="The same button tinted salmon with a highlighted underline" loading="lazy"><span class="choice-name">Orange flash</span><span class="choice-desc">Highlights the button when work starts. Tells you something happened, not how long is left.</span></label>+        <label class="choice" data-value="off"><input type="radio" name="taskbarActivity" value="off" onchange="setSetting('taskbarActivity','off')"><img src="assets/settings/activity-off.png" alt="The same button with no bar and no highlight" loading="lazy"><span class="choice-name">Off</span><span class="choice-desc">The button never changes while a verb runs.</span></label>+      </fieldset>+      <div class="default-hint">Default: Progress bar</div>+    </div>+    <div class="settings-card">+      <div class="card-title">Taskbar: <span class="tt">auto-flash on come-look moments</span><span class="card-status" id="st-flash"></span></div>+      <p class="card-desc">Separate from the activity above: nb flashes a window's button orange at the moments you would want to look (a page finished loading, a screenshot was taken, something was refused, a login or 2FA step needs you, a page spawned a window). Quiet flashes only at those moments. Chatty flashes on every action, for when you want to watch the AI work. Off never auto-flashes; the AI can still flash deliberately at a human gate.</p>+      <div class="setting-row"><span class="row-label">Auto-flash</span>+        <select id="sel-flash" onchange="setSetting('flashMode', this.value)"><option value="quiet">Quiet</option><option value="chatty">Chatty</option><option value="off">Off</option></select></div>+      <div class="default-hint">Default: Quiet</div>+    </div>+    <div class="settings-card">+      <div class="card-title">nb bridge <span class="tt">on your desktops</span><span class="card-status" id="st-bridge"></span></div>+      <p class="card-desc">The nb bridge is a small Node runtime Adom Bridge keeps in its cache on each desktop and updates from the wiki release. Refresh asks Bridge to re-check the published release, swap in a newer runtime if there is one, and restart the bridge process; the extension reconnects on its own within a few seconds. This runs on every connected desktop and streams what each one said.</p>+      <div class="setting-row"><span class="row-label" id="bridge-rows">Connected desktops</span><button class="btn" id="btn-refresh" onclick="refreshBridge(this)">Refresh bridge</button></div>+      <pre class="console" id="console-refresh" hidden></pre>+    </div>+    <footer class="settings-footer">+      <p class="footer-note">Settings are stored on each desktop in <code>~/.adom/native-browser/settings.json</code> and applied to every desktop connected right now. Restoring returns every option above to its default.</p>+      <button class="restore-btn" id="restore" onclick="restoreDefaults(this)">Restore defaults</button>+    </footer>+  </div>+</dialog> <script> 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>'@@ -298,6 +383,67 @@ async function trySay(i, btn){   setTimeout(()=>{ btn.textContent='Try it'; }, 2500); } loadSay();+// ---- 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;+  const r = dlg.getBoundingClientRect();+  const inside = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;+  if (!inside) dlg.close();+});+function openSettings(){+  dlg.showModal(); dlg.focus();+  const body = document.getElementById('settings-body'); body.scrollTop = 0; requestAnimationFrame(() => { body.scrollTop = 0; });+  sizeShots(); loadSettings();+}+function sizeShots(){ // render at device pixels: naturalWidth / devicePixelRatio, so the picture is the real physical size+  for (const img of dlg.querySelectorAll('img')) { const f = () => { if (img.naturalWidth) { img.style.width = (img.naturalWidth / devicePixelRatio) + 'px'; } }; if (img.complete) f(); else img.onload = f; }+}+let _settingsPer = [];+function paintSettings(per){+  _settingsPer = per || [];+  const live = _settingsPer.filter(p => p.settings);+  const agg = (k) => { const vals = [...new Set(live.map(p => String(p.settings[k])))]; return vals.length === 1 ? vals[0] : (vals.length ? 'mixed' : 'unknown'); };+  const ov = agg('overlayBadge'), ac = agg('taskbarActivity'), fm = agg('flashMode');+  const tg = document.getElementById('tg-overlay'); tg.setAttribute('aria-checked', ov === 'true' ? 'true' : 'false');+  const stO = document.getElementById('st-overlay'); stO.textContent = ov === 'true' ? 'on' : ov === 'false' ? 'off' : ov; stO.className = 'card-status' + (ov === 'true' ? ' ok' : '');+  for (const c of document.querySelectorAll('#choice-activity .choice')) { const on = c.dataset.value === ac; c.dataset.selected = on ? 'true' : 'false'; c.querySelector('input').checked = on; }+  const stA = document.getElementById('st-activity'); stA.textContent = ({progress:'Progress bar', flash:'Orange flash', off:'Off'})[ac] || ac; stA.className = 'card-status' + (ac === 'progress' ? ' ok' : '');+  document.getElementById('sel-flash').value = ['quiet','chatty','off'].includes(fm) ? fm : 'quiet';+  const stF = document.getElementById('st-flash'); stF.textContent = fm; stF.className = 'card-status' + (fm === 'quiet' ? ' ok' : '');+  document.getElementById('st-bridge').textContent = _settingsPer.map(p => p.target + (p.settings ? ' ok' : ' no answer')).join(' · ');+  document.getElementById('bridge-rows').textContent = 'Connected desktops: ' + (_settingsPer.map(p => p.target).join(', ') || 'none');+}+async function loadSettings(){+  try { const r = await fetch('api/settings'); const d = await r.json(); paintSettings(d.per); } catch(e) {}+}+async function setSetting(key, value){+  // optimistic: reflect the click now, then read the STORED literal back from every desktop+  if (key === 'overlayBadge') document.getElementById('tg-overlay').setAttribute('aria-checked', value ? 'true' : 'false');+  if (key === 'taskbarActivity') for (const c of document.querySelectorAll('#choice-activity .choice')) c.dataset.selected = (c.dataset.value === value) ? 'true' : 'false';+  const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 5000);+  try {+    const r = await fetch('api/settings', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ set: { [key]: value } }), signal: ctrl.signal });+    const d = await r.json(); paintSettings(d.per);+  } catch(e) { console.warn('settings save did not complete', e); loadSettings(); }+  clearTimeout(t);+}+async function refreshBridge(btn){+  const out = document.getElementById('console-refresh'); out.hidden = false; out.textContent = ''; btn.disabled = true;+  try {+    const r = await fetch('api/action/refresh-bridge', { method:'POST' });+    const rd = r.body.getReader(); const dec = new TextDecoder();+    while (true) { const { value, done } = await rd.read(); if (done) break; out.textContent += dec.decode(value); out.scrollTop = out.scrollHeight; }+  } catch(e) { out.textContent += '\\nerror: ' + e.message; }+  btn.disabled = false; loadSettings();+}+let _armed = 0;+async function restoreDefaults(btn){+  if (!_armed) { _armed = setTimeout(() => { _armed = 0; btn.classList.remove('armed'); btn.textContent = 'Restore defaults'; }, 4000); btn.classList.add('armed'); btn.textContent = 'Click again to confirm'; return; }+  clearTimeout(_armed); _armed = 0; btn.classList.remove('armed'); btn.textContent = 'Restore defaults';+  const r = await fetch('api/settings', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ set: { overlayBadge: true, taskbarActivity: 'progress', flashMode: 'quiet' } }) });+  const d = await r.json(); paintSettings(d.per);+} function copyPrompt2(btn){ try { navigator.clipboard.writeText(document.getElementById('pr2').textContent); btn.textContent = 'Copied!'; } catch(e){} } function installPrompt(i){   const r = (window.__matrix||[])[i]; if (!r) return;@@ -385,6 +531,55 @@ function startServer() {       catch { res.writeHead(404); res.end(); }       return;     }+    if (u.includes('/assets/settings/')) {+      // by SUFFIX, so it also works behind the proxy sub-path+      const nm = u.split('/assets/settings/')[1].split('?')[0].replace(/[^a-z0-9._-]/gi, '');+      try { const png = fs.readFileSync(path.join(__dirname, '..', 'docs', 'img', 'settings', nm)); res.writeHead(200, { 'Content-Type': 'image/png', 'Cache-Control': 'max-age=600' }); res.end(png); }+      catch { res.writeHead(404); res.end(); }+      return;+    }+    if (u.startsWith('/api/settings') && req.method === 'GET') {+      (async () => {+        const per = [];+        for (const t of (cache.targets || [])) {+          const r = await cli(['--target', t.name, '--ai-thread', 'nb-dashboard', 'nbrowser_settings', '{}']);+          per.push({ target: t.name, settings: r.settings || null, defaults: r.defaults || null, enums: r.enums || null, error: r.settings ? null : (r.error || r._err || 'no answer') });+        }+        res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', ...CORS });+        res.end(JSON.stringify({ ok: true, per }));+      })();+      return;+    }+    if (u.startsWith('/api/settings') && req.method === 'POST') {+      let b = ''; req.on('data', (c) => (b += c)); req.on('end', async () => {+        let set = {}; try { set = JSON.parse(b).set || {}; } catch {}+        // settings are PER DESKTOP: apply to every connected one, return each STORED literal+        const per = [];+        for (const t of (cache.targets || [])) {+          const r = await cli(['--target', t.name, '--ai-thread', 'nb-dashboard', 'nbrowser_settings', JSON.stringify({ set })]);+          per.push({ target: t.name, settings: r.settings || null, changed: r.changed || [], error: r.settings && r.ok !== false ? null : (r.error || r._err || 'no answer') });+        }+        res.writeHead(200, { 'Content-Type': 'application/json', ...CORS }); res.end(JSON.stringify({ ok: per.every((p) => !p.error), per }));+      });+      return;+    }+    if (u.startsWith('/api/action/refresh-bridge') && req.method === 'POST') {+      // LIVE CONSOLE: chunked text, one line per step, exit line, then a verify line+      res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', ...CORS });+      (async () => {+        for (const t of (cache.targets || [])) {+          res.write(`$ adom-bridge --target ${t.name} refresh_bridges {"name":"native-browser"}\n`);+          const r = await cli(['--target', t.name, '--ai-thread', 'nb-dashboard', 'refresh_bridges', '{"name":"native-browser"}'], 60000);+          res.write(`  ${r.status || (r._err ? 'error' : 'ok')} ${r._err || ''}\n`);+          await new Promise((k) => setTimeout(k, 4000));+          const rd = await cli(['--target', t.name, '--ai-thread', 'nb-dashboard', 'nbrowser_readiness', '{}']);+          res.write(`  verify: state=${rd.state || '?'} expectedExtension=${rd.expectedExtensionVersion || '?'}\n`);+        }+        res.write('done\n'); res.end();+        refresh().catch(() => {});+      })();+      return;+    }     if (u.startsWith('/api/say')) {       res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', ...CORS });       res.end(JSON.stringify({ say: sayPhrases() }));
docs/img/settings/activity-flash.pngadded
⋯ 1 unchanged line ⋯
docs/img/settings/activity-off.pngadded
⋯ 1 unchanged line ⋯
docs/img/settings/activity-progress.pngadded
⋯ 1 unchanged line ⋯
docs/img/settings/overlay-off.pngadded
⋯ 1 unchanged line ⋯
docs/img/settings/overlay-on.pngadded
⋯ 1 unchanged line ⋯
package.json+6−1
@@ -1,6 +1,6 @@ {   "slug": "nb-bridge",-  "version": "0.9.1",+  "version": "0.10.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.",@@ -21,6 +21,11 @@     "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",     "dockbar.json",     "install.sh",     "uninstall.sh",
page.json+70−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,69 @@   },   "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.10.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",+    "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