← Commit history
v0.16.15 (insiders): The nbe's browser toolbar icon is now a setting (toolbarIcon auto|on|off, default auto = Chrome's unpinned default, the small surface inside a user's own browser): on enables the icon with an nb badge and title for a host that pins it (pup loading the nbe into its window), off hides it. Pushed to every connected extension on hello and on change; dashboard settings card added; extension 0.2.11.
11 files changed
+96−15
bin/nb-dashboard+12−3@@ -20,7 +20,7 @@ const { execFile, spawn } = require('child_process'); const fs = require('fs'); const crypto = require('crypto'); -const VERSION = '0.16.14'; // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.15'; // 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.@@ -571,6 +571,13 @@ function page() { <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">Browser toolbar: <span class="tt">the nbe icon</span><span class="card-status" id="st-icon"></span></div>+ <p class="card-desc">What the extension shows in the browser's toolbar slot. Auto keeps Chrome's default for an unpacked extension: the icon exists but stays unpinned under the puzzle menu, a small surface, so a user's own browser does not advertise that the nbe is there. On enables the icon with an nb badge and a clear title, for a host that pins it (pup pins the extensions it loads into its own window). Off hides it as far as an extension can hide itself. Pinning is the profile's own preference; this setting only decides what the slot shows.</p>+ <div class="setting-row"><span class="row-label">Toolbar icon</span>+ <select id="sel-icon" onchange="setSetting('toolbarIcon', this.value)"><option value="auto">Auto (unpinned, Chrome default)</option><option value="on">On (icon + nb badge)</option><option value="off">Off (hidden)</option></select></div>+ <div class="default-hint">Default: Auto</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>@@ -738,7 +745,9 @@ 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 ov = agg('overlayBadge'), ac = agg('taskbarActivity'), fm = agg('flashMode'), ti = agg('toolbarIcon');+ const selI = document.getElementById('sel-icon'); if (selI) selI.value = ['auto','on','off'].includes(ti) ? ti : 'auto';+ const stI = document.getElementById('st-icon'); if (stI) { stI.textContent = ({auto:'auto', on:'on', off:'off'})[ti] || ti; stI.className = 'card-status' + (ti === 'auto' ? ' ok' : ''); } 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; }@@ -775,7 +784,7 @@ 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 r = await fetch('api/settings', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ set: { overlayBadge: true, taskbarActivity: 'progress', flashMode: 'quiet', toolbarIcon: 'auto' } }) }); const d = await r.json(); paintSettings(d.per); } function copyPrompt2(btn){ try { navigator.clipboard.writeText(document.getElementById('pr2').textContent); btn.textContent = 'Copied!'; } catch(e){} }
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.16.14+0.16.15
bridge/bridge.json+3−2@@ -2,8 +2,8 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Native Browser",- "version": "0.16.14",- "expectedExtensionVersion": "0.2.10",+ "version": "0.16.15",+ "expectedExtensionVersion": "0.2.11", "description": "Drives the user's REAL signed-in Chrome/Edge, in two layers. Layer 1 (nb) needs NOTHING installed and works on any machine: nbrowser_open_os_window (open a URL in the user's real profile, background+flashing, the OAuth/consent path), nbrowser_os_windows, nbrowser_profiles, nbrowser_wake_profile, nbrowser_force_close. Layer 2 (nbe, the Adom Native Browser Extension) unlocks the rest: CDP driving (navigate/click/read/screenshot) inside their logged-in profile. The bridge binds an ephemeral loopback port and the native-messaging host pumps stdio <-> bridge.", "homepage": "https://wiki.adom.inc/adom/nb-bridge", "author": "Adom Inc.",@@ -87,6 +87,7 @@ "nbrowser_record_list", "nbrowser_flash", "nbrowser_flash_stop",+ "nbrowser_apply_settings", "nbrowser_settings", "nbrowser_flash_mode", "nbrowser_taskbar",
bridge/package.json+1−1@@ -1,6 +1,6 @@ { "name": "adom-native-browser-bridge",- "version": "0.16.14",+ "version": "0.16.15", "private": true, "description": "Native-browser bridge for Bridge (ab): layer 1 (nb) plus layer 2 via nbe, the Adom Native Browser Extension (Chrome + Edge).", "main": "server.js",
bridge/server.js+18−3@@ -255,6 +255,7 @@ function registerConn(sock, profile, browser, version) { // Keep the version the extension reported in its hello (used by readiness to flag a stale build). // Fall back to a same-profile prior value if this hello somehow omitted it. conns.set(key, { sock, profile: profile || {}, since: Date.now(), version: version || (prev && prev.version) || null });+ setTimeout(() => { const c = conns.get(key); if (c && c.sock === sock) pushExtSettings(c, key); }, 800); // toolbarIcon etc. reach a fresh service worker if (!activeProfile || !liveConns().some(([k]) => k === activeProfile)) activeProfile = isBlocked(key) ? activeProfile : key; dlog(`hello: profile ${key} registered (${conns.size} total)`); }@@ -588,8 +589,19 @@ let flashMode = 'quiet'; // quiet | chatty | off (mirrors SETTINGS.fl // taskbarActivity enum progress | flash | off - what the button does WHILE a verb runs // flashMode enum quiet | chatty | off - auto-flash verbosity on come-look moments // verbDurations (rolling averages, ms) ride in the same file so the progress bar can be sized.-const SETTINGS_DEFAULTS = { overlayBadge: true, taskbarActivity: 'progress', flashMode: 'quiet' };-const SETTINGS_ENUMS = { taskbarActivity: ['progress', 'flash', 'off'], flashMode: ['quiet', 'chatty', 'off'] };+const SETTINGS_DEFAULTS = { overlayBadge: true, taskbarActivity: 'progress', flashMode: 'quiet', toolbarIcon: 'auto' };+const SETTINGS_ENUMS = { taskbarActivity: ['progress', 'flash', 'off'], flashMode: ['quiet', 'chatty', 'off'], toolbarIcon: ['auto', 'on', 'off'] };+// toolbarIcon (John, 2026-09-12): what the nbe's toolbar slot shows. auto = Chrome's default (icon exists,+// unpinned, the small surface for a user's own browser); on = icon + "nb" badge + a clear title (for a host+// that pins it, e.g. pup loading the nbe into its window); off = transparent and disabled. Pushed to every+// connected extension on hello and on change; pinning itself belongs to whoever owns the profile.+function pushExtSettings(conn, key) {+ try {+ const id = ++seq;+ pending.set(id, { resolve: () => {}, timer: setTimeout(() => pending.delete(id), 4000), verb: 'nbrowser_apply_settings', profileKey: key, _internal: true });+ conn.sock.write(JSON.stringify({ id, verb: 'nbrowser_apply_settings', args: { toolbarIcon: SETTINGS.toolbarIcon } }) + '\n');+ } catch {}+} const SETTINGS_FILE = path.join(os.homedir(), '.adom', 'native-browser', 'settings.json'); let SETTINGS = { ...SETTINGS_DEFAULTS }; let VERB_DUR = {}; // verb -> { avgMs, n }@@ -1609,7 +1621,8 @@ const VERB_META = { nbrowser_record_list: { hint: 'List recent recordings.', related: ['nbrowser_record_status'], pitfalls: ['paths are on the LAPTOP - pull them with adom-bridge pull_file, do not expect them in the container'] }, nbrowser_flash: { hint: 'Flash the driven window\'s taskbar button - the "come look" cue (e.g. at a captcha/2FA/login human-gate).', related: ['nbrowser_flash_stop', 'nbrowser_flash_mode'], pitfalls: ['auto-clears on focus; nbrowser_flash_stop to clear early'] }, nbrowser_flash_stop: { hint: 'Stop the taskbar flash.', related: ['nbrowser_flash'], pitfalls: ['the flash already auto-clears when the user focuses the window - only stop it early if you no longer need their attention'] },- nbrowser_settings: { hint: 'Read or set the user settings behind the NB Dashboard Settings panel: overlayBadge (bool), taskbarActivity (progress|flash|off), flashMode (quiet|chatty|off). {} reads; {set:{...}} writes, validated by type, persisted on the desktop, and returns the STORED literal.', related: ['nbrowser_flash_mode', 'nbrowser_taskbar'], pitfalls: ['settings are PER DESKTOP (stored on the machine the bridge runs on) - set them on every --target the user uses', 'a rejected key changes nothing; read the returned settings rather than assuming your write landed', 'overlayBadge:false also restores the native avatar on windows currently badged'] },+ nbrowser_apply_settings: { hint: 'INTERNAL push from the bridge to a connected extension (toolbarIcon and future per-extension settings); sent on every hello and on nbrowser_settings changes. Not for callers: set settings with nbrowser_settings.', related: ['nbrowser_settings'], pitfalls: ['calling it directly changes nothing durable - the bridge re-pushes the stored settings on the next hello'] },+ nbrowser_settings: { hint: 'Read or set the user settings behind the NB Dashboard Settings panel: overlayBadge (bool), taskbarActivity (progress|flash|off), flashMode (quiet|chatty|off), toolbarIcon (auto|on|off: what the nbe\'s browser toolbar slot shows; auto = Chrome default, unpinned; on = icon with an nb badge for a host that PINS it, such as pup; off = transparent and disabled). {} reads; {set:{...}} writes, validated by type, persisted on the desktop, and returns the STORED literal.', related: ['nbrowser_flash_mode', 'nbrowser_taskbar'], pitfalls: ['settings are PER DESKTOP (stored on the machine the bridge runs on) - set them on every --target the user uses', 'a rejected key changes nothing; read the returned settings rather than assuming your write landed', 'overlayBadge:false also restores the native avatar on windows currently badged'] }, nbrowser_flash_mode: { hint: 'Set auto-flash verbosity: quiet (come-look moments: page load, screenshot, error/refusal, human-gate, spawned window) | chatty (every action) | off (no auto-flash at all). Flash manually with nbrowser_flash at task-done.', related: ['nbrowser_flash'], pitfalls: ['off is the user opt-out - if the user says the orange flashing is annoying, set flash_mode:"off" and it stops entirely', 'quiet is the default; the flash is a "come look" cue, so keep it for human-gates even if you dial the rest down'] }, nbrowser_taskbar: { hint: 'Paint progress/overlay/flash on the window taskbar button (e.g. paused=yellow for a human gate). The "Adom is driving" badge is AUTOMATIC: the bridge composites the profile\'s circle avatar + the Adom favicon at open/drive, and idle-expires it (~3 min) back to the plain native avatar - you rarely need to touch overlay yourself.', related: ['nbrowser_flash'], pitfalls: ['clear progress+flash at task end: {progress:{state:"none"},flash:"stop"} - the badge clears itself (idle-expiry / close)', 'overlay.badge:"none" RESTORES the native avatar look (the bridge never blanks the overlay slot - blanking would erase the user\'s profile avatar)'] }, nbrowser_describe: { hint: 'Returns per-verb schemas + hint/related/pitfalls for ab\'s Verbs tab.', related: ['nbrowser_help'], pitfalls: ['this is the catalog - a few extension-only verbs (e.g. nbrowser_login_form) are real but resolved in the extension, not listed here'] },@@ -1920,6 +1933,7 @@ function dispatch(verb, args) { d('nbrowser_record_list', 'List recent recordings.', {}, { ok: 'bool', recordings: '[...]' }, 20, { example: {} }), d('nbrowser_flash', "Flash the driven window's taskbar button ('come look' cue).", { sessionId: 'required str' }, { ok: 'bool' }, 15, { example: { sessionId: 'work' } }), d('nbrowser_flash_stop', 'Stop the taskbar flash.', { sessionId: 'required str' }, { ok: 'bool' }, 15, { example: { sessionId: 'work' } }),+ d('nbrowser_apply_settings', 'INTERNAL: the bridge pushes per-extension settings (toolbarIcon) to a connected extension on hello and on change. Callers use nbrowser_settings instead.', { toolbarIcon: 'optional "auto"|"on"|"off"' }, { ok: 'bool', applied: 'obj', toolbarIcon: 'str' }, 10, { example: { toolbarIcon: 'on' } }), d('nbrowser_settings', 'Read/set the dashboard settings (overlayBadge, taskbarActivity, flashMode); writes are validated and the stored literal is returned.', { set: 'optional {overlayBadge?:bool, taskbarActivity?:"progress"|"flash"|"off", flashMode?:"quiet"|"chatty"|"off"}' }, { ok: 'bool', settings: 'obj', defaults: 'obj', enums: 'obj', changed: '[str]', file: 'str' }, 15, { example: { set: { overlayBadge: false } } }), d('nbrowser_flash_mode', 'Set auto-flash verbosity.', { mode: 'required "quiet"|"chatty"|"off"' }, { ok: 'bool', mode: 'str' }, 15, { example: { mode: 'quiet' } }), d('nbrowser_taskbar', 'Paint progress/overlay/flash on the window taskbar button.', { sessionId: 'required str', progress: 'optional {state:"normal"|"paused"|"error"|"indeterminate"|"none", value?: PERCENT 0..100 (a 0..1 value paints an invisible bar)}', overlay: 'optional {badge}', flash: 'optional "until_focused"|"stop"' }, { ok: 'bool', applied: '[str]' }, 15, { example: { sessionId: 'work', progress: { state: 'indeterminate' } } }),@@ -2529,6 +2543,7 @@ function dispatch(verb, args) { const rejected = Object.keys(set).filter((k) => !(k in SETTINGS_DEFAULTS) || (SETTINGS_ENUMS[k] && !SETTINGS_ENUMS[k].includes(String(set[k])))); SETTINGS = merged; flashMode = SETTINGS.flashMode; saveSettings(); changed = Object.keys(SETTINGS_DEFAULTS).filter((k) => before[k] !== SETTINGS[k]);+ if (changed.includes('toolbarIcon')) for (const [k, c] of liveConns()) pushExtSettings(c, k); if (changed.includes('overlayBadge') && SETTINGS.overlayBadge === false) { // turning the badge OFF restores the native avatar on every window we are badging right now for (const sid of [...badgeOn]) { const h = sessionHwnd.get(sid); if (h) taskbar(h, { overlay: { badge: 'none' } }); badgeOn.delete(sid); }
extension/manifest.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Adom Native Browser Extension", "short_name": "Adom",- "version": "0.2.10",+ "version": "0.2.11", "description": "Layer 2 of Adom Native Browser (nb): lets your Adom AI drive your real, signed-in Chrome or Edge, using your own logins.", "minimum_chrome_version": "116", "background": {
extension/src/sw.js+48−1@@ -11,6 +11,47 @@ import * as record from './record.js'; const HOST = 'inc.adom.native_browser'; const VERSION = chrome.runtime.getManifest().version; +// TOOLBAR ICON (John, 2026-09-12). Three modes, set by the bridge (nbrowser_settings {set:{toolbarIcon}})+// and persisted here so a service-worker restart keeps it:+// auto (default): Chrome's own default for an unpacked extension, the icon exists but is UNPINNED (under the+// puzzle menu) - the "small surface" John wants when nb loads the nbe into a user's browser.+// on: the icon is enabled, carries an "nb" badge and a clear title, so a host that PINS it (pup+// pins the extensions it loads into its own profile) shows a live Adom presence in the toolbar.+// off: transparent icon and disabled action, as invisible as an extension can make itself.+// An extension cannot pin itself: pinning is the profile's extensions.pinned_extensions preference, which the+// launcher that owns the profile (pup) sets. This switch decides what the pinned slot SHOWS.+const ICON_DEFAULT = { 16: 'icons/icon16.png', 48: 'icons/icon48.png', 128: 'icons/icon128.png' };+const ICON_BLANK = { 16: 'icons/blank16.png', 48: 'icons/blank48.png' };+let _toolbarIcon = 'auto';+async function applyToolbarIcon(mode) {+ mode = ['auto', 'on', 'off'].includes(mode) ? mode : 'auto';+ _toolbarIcon = mode;+ try { await chrome.storage.local.set({ nb_toolbar_icon: mode }); } catch {}+ try {+ if (mode === 'off') {+ await chrome.action.setIcon({ path: ICON_BLANK });+ await chrome.action.setBadgeText({ text: '' });+ await chrome.action.setTitle({ title: 'Adom Native Browser Extension (hidden)' });+ await chrome.action.disable();+ } else {+ await chrome.action.enable();+ await chrome.action.setIcon({ path: ICON_DEFAULT });+ if (mode === 'on') {+ await chrome.action.setBadgeBackgroundColor({ color: '#14b8a6' });+ await chrome.action.setBadgeTextColor({ color: '#04201d' }).catch(() => {});+ await chrome.action.setBadgeText({ text: 'nb' });+ await chrome.action.setTitle({ title: 'Adom Native Browser Extension (nbe): your Adom AI can drive this browser' });+ } else {+ await chrome.action.setBadgeText({ text: '' });+ await chrome.action.setTitle({ title: 'Adom' });+ }+ }+ } catch (e) { /* action API missing (no action in manifest) or a race on startup: harmless */ }+ return { ok: true, toolbarIcon: mode };+}+(async () => { try { const { nb_toolbar_icon = 'auto' } = await chrome.storage.local.get('nb_toolbar_icon'); await applyToolbarIcon(nb_toolbar_icon); } catch {} })();++ let port = null; let connecting = false; let gotMessage = false;@@ -150,7 +191,7 @@ async function dispatch(verb, args) { // --- meta ------------------------------------------------------------- case 'nbrowser_status': { const own = native.ownershipSummary(); // dirty_session nag: surfaced while owned/spawned windows remain- return { ok: true, ext: 'nbe', version: VERSION, engine: 'native', browser: detectBrowser(), ownedWindows: own.ownedWindows, spawnedWindows: own.spawnedWindows, sessions: own.sessions, _hint: own._nag };+ return { ok: true, ext: 'nbe', version: VERSION, engine: 'native', browser: detectBrowser(), toolbarIcon: _toolbarIcon, ownedWindows: own.ownedWindows, spawnedWindows: own.spawnedWindows, sessions: own.sessions, _hint: own._nag }; } case 'nbrowser_whoami': { // Force a fresh Google-email resolve + RE-announce, so the bridge re-keys this connection to@@ -165,6 +206,12 @@ async function dispatch(verb, args) { } case 'nbrowser_ping': return { ok: true, pong: true };+ case 'nbrowser_apply_settings': {+ // Pushed by the bridge on every hello and whenever nbrowser_settings changes.+ const r = {};+ if (args && args.toolbarIcon !== undefined) Object.assign(r, await applyToolbarIcon(String(args.toolbarIcon)));+ return { ok: true, applied: r, toolbarIcon: _toolbarIcon };+ } case 'nbrowser_dev_reload': { // HARD-GATED. chrome.runtime.reload() restarts the SW, which re-launches the native host. This is // a bridge-DISPATCHABLE verb, so a looping/buggy bridge could call it repeatedly → hosts
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "nb-bridge",- "version": "0.16.14",+ "version": "0.16.15", "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.",
skills/driving-the-extension/SKILL.md+8@@ -296,6 +296,14 @@ This is **not** screenshot→eyeball-a-pixel→click automation, so it does **no the app state. **An unknown `input_dispatch` type is REFUSED (`unknown_input_type`)**; before 0.2.10 it fell through to a click, which on a canvas editor with a tool armed placed four clicks for four zoom attempts. Valid types: click | dblclick | rightclick | move | hover | wheel | type | key.+- **The nbe's browser TOOLBAR icon is a setting (`nbrowser_settings {set:{toolbarIcon:"auto"|"on"|"off"}}`,+ ext 0.2.11 / bridge 0.16.15).** `auto` (default) is Chrome's own default for an unpacked extension: the icon+ exists but stays UNPINNED under the puzzle menu, the small surface John wants inside a user's own browser.+ `on` enables it with an "nb" badge and a clear title, for a host that PINS it: pup pins the extensions it+ loads into its own window, so a pinned nbe shows a live Adom presence there. `off` is a transparent icon+ and a disabled action. An extension cannot pin itself; pinning is the profile's `extensions.pinned_extensions`+ preference (the nbe's id is `nncjgceobmlenfliamnibncjojpedbcc`, fixed by the manifest key), set by whoever+ owns the profile. The bridge pushes the setting to every connected extension on hello and on change. - **Taskbar badge and progress on a COMBINED taskbar (2026-09-04, proven at native resolution on ConfRoomROG):** ab paints the Adom badge and the progress bar correctly (a KiCad button showed both), but a Chrome or Edge GROUP button shows only its most recently ACTIVE window's overlay and progress, and
tools/verb-battery.mjs+1−1@@ -260,7 +260,7 @@ const T = { // ─── coverage map: EVERY VERB_META verb decides its fate here ─────────────── export const COVERAGE = {- status: 'live', readiness: 'live', ping: 'live', progress: 'live', scroll: 'live', input_unknown_type: 'guard', hover_raw_xy: 'live', whoami: 'live', profiles: 'live',+ status: 'live', readiness: 'live', ping: 'live', progress: 'live', scroll: 'live', input_unknown_type: 'guard', hover_raw_xy: 'live', apply_settings: 'skip:internal bridge-to-extension push, exercised by the settings test through nbrowser_settings', whoami: 'live', profiles: 'live', resolve_profiles: 'live', chrome_status: 'live', describe: 'live', activity: 'live', os_windows: 'live', wake_profile: 'live', rescan: 'live', resync_sessions: 'live', open_window: 'live', pin_session: 'live', use_profile: 'live', list_windows: 'live',
tools/verbs-policy.json+2−1@@ -57,5 +57,6 @@ "eval": "mutating", "evaluate": "alias", "set_file_input": "mutating",- "scroll": "mutating"+ "scroll": "mutating",+ "apply_settings": "meta" }