← Commit history

v0.16.20 (insiders): Every nb window opens at 95% of the primary monitor's work area, centered (John, 2026-09-18): Chrome's own default for an extension-created window is a cascaded ~60% of a big screen and never the user's last hand-set size. The bridge reads the monitor from ab (desktop_list_monitors), passes bounds on every nbrowser_open_window and window-size/position flags on every Layer-1 launch, from a new newWindowSize setting (95|90|85|75|chrome) with a dashboard card; a caller's own bounds always win. Extension 0.2.15 honours left/top/width/height/state.

John Lauer ·2020bcacf0 ·1mo ago ·parent d21da5f
9 files changed +70−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.19';           // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.20';           // 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">New windows: <span class="tt">how big they open</span><span class="card-status" id="st-size"></span></div>+      <p class="card-desc">Every window nb opens is sized to this share of your primary monitor's work area and centered. Without it Chrome sizes an extension-created window with its own cascaded default, about 60 percent of a large screen, and never the size you last set by hand. Chrome default hands that decision back to Chrome. A caller that passes its own bounds always wins.</p>+      <div class="setting-row"><span class="row-label">New window size</span>+        <select id="sel-size" onchange="setSetting('newWindowSize', this.value)"><option value="95">95% of the screen</option><option value="90">90%</option><option value="85">85%</option><option value="75">75%</option><option value="chrome">Chrome default</option></select></div>+      <div class="default-hint">Default: 95%</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>@@ -745,7 +752,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'), ti = agg('toolbarIcon');+  const ov = agg('overlayBadge'), ac = agg('taskbarActivity'), fm = agg('flashMode'), ti = agg('toolbarIcon'), ws = agg('newWindowSize');+  const selS = document.getElementById('sel-size'); if (selS) selS.value = ['95','90','85','75','chrome'].includes(ws) ? ws : '95';+  const stS = document.getElementById('st-size'); if (stS) { stS.textContent = ws === 'chrome' ? 'Chrome default' : (ws + '%'); stS.className = 'card-status' + (ws === '95' ? ' ok' : ''); }   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');@@ -784,7 +793,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', toolbarIcon: 'auto' } }) });+  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', newWindowSize: '95' } }) });   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.19\ No newline at end of file+0.16.20\ No newline at end of file
bridge/bridge.json+2−2
@@ -2,8 +2,8 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Native Browser",-  "version": "0.16.19",-  "expectedExtensionVersion": "0.2.14",+  "version": "0.16.20",+  "expectedExtensionVersion": "0.2.15",   "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.",
bridge/package.json+1−1
@@ -1,6 +1,6 @@ {   "name": "adom-native-browser-bridge",-  "version": "0.16.19",+  "version": "0.16.20",   "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+39−5
@@ -589,8 +589,34 @@ 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', toolbarIcon: 'auto' };-const SETTINGS_ENUMS = { taskbarActivity: ['progress', 'flash', 'off'], flashMode: ['quiet', 'chatty', 'off'], toolbarIcon: ['auto', 'on', 'off'] };+const SETTINGS_DEFAULTS = { overlayBadge: true, taskbarActivity: 'progress', flashMode: 'quiet', toolbarIcon: 'auto', newWindowSize: '95' };+const SETTINGS_ENUMS = { taskbarActivity: ['progress', 'flash', 'off'], flashMode: ['quiet', 'chatty', 'off'], toolbarIcon: ['auto', 'on', 'off'], newWindowSize: ['95', '90', '85', '75', 'chrome'] };+// NEW WINDOW SIZE (John, 2026-09-18: "you should be opening nb windows at like 95% the size of my screen").+// An extension-created Chrome window with no bounds gets Chrome's cascaded default, about 60% of a big+// screen, and never the user's last hand-set size. So every nb open carries bounds: newWindowSize percent+// of the PRIMARY monitor's logical work area (DIPs, what chrome.windows.create takes), centered. 'chrome'+// leaves it to Chrome. A caller's own left/top/width/height/state always wins.+let _monCache = { at: 0, v: null };+async function primaryWorkArea() {+  if (Date.now() - _monCache.at < 60000 && _monCache.v) return _monCache.v;+  try {+    const r = await abCommand('desktop', 'desktop_list_monitors', {});+    let out = r && r.body; if (out && typeof out.output === 'string') { try { out = JSON.parse(out.output); } catch {} }+    const mons = (out && out.monitors) || (out && out.data && out.data.monitors) || [];+    const m = mons.find((x) => x.primary) || mons[0];+    const wa = m && (m.logicalWorkArea || m.logicalBounds);+    if (wa && wa.width > 200 && wa.height > 200) { _monCache = { at: Date.now(), v: { x: wa.x || 0, y: wa.y || 0, width: wa.width, height: wa.height, scale: m.scale || 1 } }; return _monCache.v; }+  } catch {}+  return null;+}+function callerGaveBounds(a) { return !!(a && (a.width != null || a.height != null || a.left != null || a.top != null || a.state)); }+async function defaultWindowBounds() {+  const pct = Number(SETTINGS.newWindowSize);+  if (!Number.isFinite(pct)) return null;                       // 'chrome': leave it to Chrome+  const wa = await primaryWorkArea(); if (!wa) return null;+  const w = Math.round(wa.width * pct / 100), h = Math.round(wa.height * pct / 100);+  return { left: wa.x + Math.round((wa.width - w) / 2), top: wa.y + Math.round((wa.height - h) / 2), width: w, height: h, _from: `${pct}% of the primary monitor's work area (${wa.width}x${wa.height} logical px)` };+} // 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@@ -1659,7 +1685,7 @@ const VERB_META = {   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_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_settings: { hint: 'Read or set the user settings behind the NB Dashboard Settings panel: overlayBadge (bool), taskbarActivity (progress|flash|off), flashMode (quiet|chatty|off), newWindowSize (95|90|85|75|chrome: percent of the primary monitor\'s work area every new nb window opens at, centered; chrome = Chrome\'s own default, which is about 60% of a big screen and never the user\'s last hand-set size), 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'] },@@ -1910,7 +1936,7 @@ function dispatch(verb, args) {         d('nbrowser_install_chrome', 'Install NATIVE Google Chrome for the user (Windows). Downloads Google\'s official installer + runs a quiet user-scoped install (no admin on a normal PC); escalates to a notify-first UAC on a locked-down box. NON-BLOCKING - poll nbrowser_chrome_status. Step 1 of putting the AI onto the user\'s real signed-in browser.', { elevate: 'optional "auto"(default) | true (skip quiet, straight to UAC) | false (quiet only, no prompt)', retryUac: 'optional bool - re-raise the Windows security prompt from the installer already on this PC (no re-download), for a caller with eyes on the screen when the toast could not be seen' }, { ok: 'bool', installing: 'bool', installed: 'bool?', alreadyInstalled: 'bool?', executablePath: 'str?', statusVerb: 'str' }, 30, { statusVerb: 'nbrowser_chrome_status', example: {} }),         d('nbrowser_chrome_status', 'READ-ONLY: is native Google Chrome installed / how is an install going? Poll after nbrowser_install_chrome.', {}, { notifications: '{checked,toastsEnabled,adomBridgeEnabled,toastMayBeMuted,_hint} - Windows toast switches, so a caller knows when the Approve-now toast could not have been seen', canRetryUac: 'bool', installed: 'bool', executablePath: 'str?', phase: '"idle"|"installing"|"awaiting_uac"|"ready"|"failed"', installing: 'bool', awaitingUac: 'bool', errorCode: 'str?' }, 15, { example: {} }),         d('nbrowser_ping', 'Round-trip liveness check through to the extension.', {}, { ok: 'bool', pong: 'bool' }, 15),-        d('nbrowser_open_window', 'Open a NEW window in the BACKGROUND (unfocused) and return a sessionId to drive it. REGISTRATION REQUIRED: sessionId + thread + purpose, or refused (session_unregistered). Never steals focus; a topmost driven window is auto-reverted (focus_violation).', { sessionId: 'required str (your short task id)', thread: 'required str (which AI thread you are)', purpose: 'required str (human-readable, e.g. "finishing John\'s IRS signup")', url: 'required str', profile: P, userRequestedForeground: 'optional bool - raise to foreground ONLY when the user asked or a browser/auth feature requires it (default false)', foregroundReason: 'REQUIRED with userRequestedForeground - human-readable why; without it the window STAYS background. Warn the user via ab notify_user FIRST.' }, { ok: 'bool', sessionId: 'str', tabId: 'str', windowId: 'int', hwnd: 'int (OS window handle - use with desktop_screenshot_window to capture the window+chrome+child popups)', url: 'str', _warning: 'str?', keptBackground: 'bool?' }, 30, { example: { sessionId: 'irs', thread: 'thread-7', purpose: 'finishing John IRS signup', url: 'https://wiki.adom.inc' } }),+        d('nbrowser_open_window', 'Open a NEW window in the BACKGROUND (unfocused) and return a sessionId to drive it. REGISTRATION REQUIRED: sessionId + thread + purpose, or refused (session_unregistered). Never steals focus; a topmost driven window is auto-reverted (focus_violation).', { left: 'optional int (logical px)', top: 'optional int', width: 'optional int (default: the newWindowSize setting, 95% of the primary work area, centered)', height: 'optional int', state: 'optional "maximized"|"normal"', sessionId: 'required str (your short task id)', thread: 'required str (which AI thread you are)', purpose: 'required str (human-readable, e.g. "finishing John\'s IRS signup")', url: 'required str', profile: P, userRequestedForeground: 'optional bool - raise to foreground ONLY when the user asked or a browser/auth feature requires it (default false)', foregroundReason: 'REQUIRED with userRequestedForeground - human-readable why; without it the window STAYS background. Warn the user via ab notify_user FIRST.' }, { ok: 'bool', sessionId: 'str', tabId: 'str', windowId: 'int', hwnd: 'int (OS window handle - use with desktop_screenshot_window to capture the window+chrome+child popups)', url: 'str', _warning: 'str?', keptBackground: 'bool?' }, 30, { example: { sessionId: 'irs', thread: 'thread-7', purpose: 'finishing John IRS signup', url: 'https://wiki.adom.inc' } }),         d('nbrowser_close_window', 'Close a window the agent opened. includeSpawned:true also closes everything that session SPAWNED (composes/popups).', { sessionId: 'required str', includeSpawned: 'optional bool (also close this session\'s spawned windows)', profile: P }, { ok: 'bool', closed: 'bool', sessionId: 'str', alsoClosedSpawned: '[int]' }, 20, { example: { sessionId: 'work', includeSpawned: true } }),         d('nbrowser_owned', 'List everything a session owns: opened windows + adopted SPAWNED windows/tabs (compose/popup/target=_blank a driven page opened). Acknowledges spawns.', { sessionId: 'optional str (all sessions if omitted)', profile: P }, { ok: 'bool', opened: '[{sessionId,windowId,url,title,purpose,ageMs}]', spawned: '[{kind,windowId,tabId,url,ageMs,spawnedBy,opener}]' }, 20, { example: { sessionId: 'irs' } }),         d('nbrowser_cleanup', 'Close ONLY what a session SPAWNED (leaked composes/popups). Never the user\'s windows, never your primary opened window.', { sessionId: 'required str', profile: P }, { ok: 'bool', closed: '[{windowId,url}]', count: 'int', keptOpenedWindow: 'int?' }, 20, { example: { sessionId: 'irs' } }),@@ -2204,6 +2230,8 @@ function dispatch(verb, args) {           const urlArg = url.replace(/['"]/g, '');           const parts = [];           if (kp.profileDir && kp.profileDir !== 'Default') parts.push(`'--profile-directory=${kp.profileDir.replace(/'/g, '')}'`);+          const wb = callerGaveBounds(a) ? null : await defaultWindowBounds();+          if (wb) parts.push(`'--window-size=${wb.width},${wb.height}'`, `'--window-position=${wb.left},${wb.top}'`);           parts.push("'--new-window'", `'${urlArg}'`);           const argArr = `@(${parts.join(',')})`;           const psBody = [@@ -2790,7 +2818,13 @@ function dispatch(verb, args) {     }     // For open_window, snapshot the Chrome window list BEFORE forwarding so the post-open diff reliably     // catches the new window (the old fire-and-forget snapshot raced the window appearing).-    if (verb === 'nbrowser_open_window') chromeHwnds(browserOfKey(tgt.key)).then((s) => { preOpenHwnds = s; forward(); }).catch(forward);+    if (verb === 'nbrowser_open_window') {+      (async () => {+        try { if (!callerGaveBounds(args)) { const b = await defaultWindowBounds(); if (b) { args = { ...(args || {}), left: b.left, top: b.top, width: b.width, height: b.height }; } } } catch {}+        try { preOpenHwnds = await chromeHwnds(browserOfKey(tgt.key)); } catch {}+        forward();+      })();+    }     else forward();   }); }
extension/manifest.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 3,   "name": "Adom Native Browser Extension",   "short_name": "Adom",-  "version": "0.2.14",+  "version": "0.2.15",   "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/native.js+8−1
@@ -522,7 +522,14 @@ export async function openWindow(args = {}) {   // native-browser driving: the user keeps working in their foreground window while we operate another.)   const fg = userWantsForeground(args);   const prev = fg ? null : await currentForeground();-  const win = await chrome.windows.create({ url: args.url, focused: fg });+  // WINDOW SIZE (John, 2026-09-18): with no bounds Chrome sizes an extension-created window with its own+  // cascaded default, about 60% of a large screen, NOT the user's last window size. The bridge passes+  // bounds (95% of the primary monitor's work area by default, a per-desktop setting), and a caller can+  // pass its own left/top/width/height/state. Only numbers we were actually given go through.+  const createArgs = { url: args.url, focused: fg };+  for (const k of ['left', 'top', 'width', 'height']) { const v = Number(args[k]); if (args[k] != null && Number.isFinite(v) && v >= 0) createArgs[k] = Math.round(v); }+  if (['maximized', 'normal'].includes(args.state) && !('width' in createArgs)) createArgs.state = args.state;+  const win = await chrome.windows.create(createArgs);   const sid = args.sessionId;   sessionWindows.set(sid, win.id);   sessionMeta.set(sid, { thread: args.thread, purpose: args.purpose, profile: args.profile || null, createdAt: Date.now() });
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "nb-bridge",-  "version": "0.16.19",+  "version": "0.16.20",   "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+5
@@ -296,6 +296,11 @@ 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.+- **Every nb window opens at 95% of the primary monitor's work area, centered (bridge 0.16.20).** Chrome+  sizes an extension-created window with its own cascaded default, about 60% of a big screen, never the+  user's last hand-set size (John: "annoying as hell"). The bridge now passes bounds on every+  `nbrowser_open_window` and `--window-size/--window-position` on every Layer-1 launch, from the+  `newWindowSize` setting (95|90|85|75|chrome). Your own `left/top/width/height/state` always win. - **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.