← Commit history
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.0.17\ No newline at end of file+0.0.18\ No newline at end of file
bridge/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Native Browser (Chrome + Edge)",-  "version": "0.0.17",+  "version": "0.0.18",   "description": "Drives the user's REAL signed-in Chrome/Edge via an installed extension (native-messaging transport). Mirrors the puppeteer bridge's verb surface as nbrowser_* but runs inside the actual logged-in profile, so cookies/SSO/saved-logins/captcha human-trust all work. The bridge binds an ephemeral loopback port (PORTS.md compliant) and writes a discovery file; the native-messaging host pumps stdio <-> this bridge; the extension never touches a TCP port.",   "homepage": "https://wiki.adom.inc/adom/adom-browser-extension",   "author": "Adom Inc.",
bridge/package.json+1−1
@@ -1,6 +1,6 @@ {   "name": "adom-native-browser-bridge",-  "version": "0.0.17",+  "version": "0.0.18",   "private": true,   "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)",   "main": "server.js",
bridge/server.js+63−63
@@ -3,7 +3,7 @@ /*  * adom-browser-extension :: bridge  * --------------------------------- * AD-spawned Node server. Two listeners, BOTH ephemeral (per adom-desktop/skills/PORTS.md — a fixed+ * AD-spawned Node server. Two listeners, BOTH ephemeral (per adom-desktop/skills/PORTS.md - a fixed  * or scan-range port dies silently on HNS-reserved Windows machines):  *  *   - HTTP server  : AD talks /status + /command here (the standard bridge contract). AD allocates@@ -62,7 +62,7 @@ const conns = new Map();   // profileKey -> { sock, profile, since } let activeProfile = null;  // default target for action verbs (instantly switchable, no window-closing) let provSeq = 0;           // provisional keys for hosts that haven't sent their hello yet // Connections are keyed by BROWSER + PROFILE so the same account signed into Chrome AND Edge (or any-// browser/profile combo) are DISTINCT, independently-addressable connections that coexist — letting+// browser/profile combo) are DISTINCT, independently-addressable connections that coexist - letting // multiple AI threads each drive a different browser/profile in parallel without colliding. e.g. // "chrome:[email protected]", "edge:[email protected]", "chrome:[email protected]". function profileKey(p) {@@ -88,7 +88,7 @@ function targetConn(args) {   const wantBrowser = args && args.browser; // optional: disambiguate the SAME email across browsers   if (want) {     if (isBlocked(String(want))) return { blocked: true, key: String(want) };-    // `want` may be a full key ("edge:[email protected]") or just an email — match either, and if a browser+    // `want` may be a full key ("edge:[email protected]") or just an email - match either, and if a browser     // is also given require it (so parallel threads can pin "this email in THIS browser").     for (const [k, c] of conns) {       if (!(c.sock && !c.sock.destroyed)) continue;@@ -107,7 +107,7 @@ function targetConn(args) {   return { conn: c, key: k }; } // Register a freshly-connected host BEFORE its hello, under a provisional key, so even an old extension-// (no hello) is drivable — and so we can dev_reload it to pick up the hello-sending build. The hello+// (no hello) is drivable - and so we can dev_reload it to pick up the hello-sending build. The hello // then re-keys it to the real profile (registerConn). function registerProvisional(sock) {   const key = 'pending-' + (++provSeq);@@ -116,9 +116,9 @@ function registerProvisional(sock) {   return key; } // ---- credential QUERY LAYER (reads the BROWSER's own saved-login store) ------// We do NOT store or replace anything — Chrome/Edge remain the source of truth for passwords. This is a+// We do NOT store or replace anything - Chrome/Edge remain the source of truth for passwords. This is a // read-only layer that QUERIES the browser's native credential store so the AI can tell which vendors a-// user already has a login for (origins + usernames; NEVER passwords — those stay encrypted and Chrome/+// user already has a login for (origins + usernames; NEVER passwords - those stay encrypted and Chrome/ // Edge autofill them at login time). A browser EXTENSION can't read this (sandbox); the bridge runs // natively, so it reads the `Login Data` SQLite for BOTH Chromium browsers. Dependency-free leaf reader. const LOCALAPP = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');@@ -173,7 +173,7 @@ function queryCredentials({ browser, profile, match } = {}) {   return out; } -// (Path-A bridge-side password decryption removed — v20 app-bound passwords can't be decrypted by any+// (Path-A bridge-side password decryption removed - v20 app-bound passwords can't be decrypted by any // external process; nbrowser_autologin defers legacy-v10 decryption to the in-Chrome host. See skill.)  function registerConn(sock, profile, browser) {@@ -191,7 +191,7 @@ function registerConn(sock, profile, browser) {   dlog(`hello: profile ${key} registered (${conns.size} total)`); } -// Bridge-self-reported chip status (led/summary/tooltip). AD is a pure renderer — ALL the reasoning+// Bridge-self-reported chip status (led/summary/tooltip). AD is a pure renderer - ALL the reasoning // about what green/yellow/red means lives HERE, since only this bridge knows its own domain. AD just // shows the color + prints the tooltip on hover; it owns only the offline/gray state (a dead bridge // can't self-report). See /status.@@ -231,21 +231,21 @@ function timeoutFor(verb) { const HELP = {   overview: 'Drive the user\'s REAL logged-in Chrome/Edge. Verbs mirror pup\'s browser_* as nbrowser_*.',   pitfalls: [-    'The extension lives in ONE browser PROFILE and DIES if that profile has no open windows — if you get "not connected", ask the user to open a window in that profile (or open one yourself once reconnected). Never close your LAST window in that profile.',+    'The extension lives in ONE browser PROFILE and DIES if that profile has no open windows - if you get "not connected", ask the user to open a window in that profile (or open one yourself once reconnected). Never close your LAST window in that profile.',     'Multiple Adom Desktops on the relay → pass `--target <name>` (e.g. AdomLapper) or you get ambiguous_target.',-    'Screenshots are large base64 and can TIME OUT over the relay — retry, or prefer AD desktop_screenshot_window for proof shots.',+    'Screenshots are large base64 and can TIME OUT over the relay - retry, or prefer AD desktop_screenshot_window for proof shots.',     'nbrowser_eval runs in the PAGE world: chrome.* APIs are NOT available there (only DOM/window).',-    'ALWAYS clean up windows you open (nbrowser_close_window) — abandoned windows annoy the user.',+    'ALWAYS clean up windows you open (nbrowser_close_window) - abandoned windows annoy the user.',     'Poll nbrowser_events to learn if the USER closed your windows/tabs (window_closed {byUser:true}).',   ],   bestPractices: [-    '⛔ NEVER reuse a window the user already has open. Do not nbrowser_navigate / reload it, and do NOT desktop_navigate it either — that yanks their live page out from under them. For ANY page YOU need (driving, reading, or screenshotting a chrome://extensions / edge://extensions page for a guide), open a NEW window (nbrowser_open_window, or a fresh OS window you then desktop_navigate), work in THAT window, and close it when done. The only thing you may do to one of their existing windows is desktop_bring_to_front to surface it for them.',-    '⛔ BACKGROUND BY DEFAULT — NEVER foreground a window unless the user explicitly asked to watch. nbrowser_open_window opens UNFOCUSED, open_tab opens active:false, switch_window does NOT raise — keep it that way. To capture a chrome:// / edge:// page (e.g. chrome://extensions for a guide), DO NOT launch the browser yourself (msedge/chrome `--new-window` via launch/launch_process RAISES the new window to the foreground). Instead: nbrowser_open_window {url:"about:blank"} (background) → desktop_navigate that window\'s hwnd to the chrome:// URL (background, no foreground) → desktop_screenshot_window (WGC captures a background/occluded window) → nbrowser_close_window. Same for recording (WGC) and screenshots (nbrowser_screenshot CDP) — both capture occluded windows, so foregrounding is never needed. Foregrounding is the USER\'s choice; do it only on explicit request (desktop_bring_to_front).',+    '⛔ NEVER reuse a window the user already has open. Do not nbrowser_navigate / reload it, and do NOT desktop_navigate it either - that yanks their live page out from under them. For ANY page YOU need (driving, reading, or screenshotting a chrome://extensions / edge://extensions page for a guide), open a NEW window (nbrowser_open_window, or a fresh OS window you then desktop_navigate), work in THAT window, and close it when done. The only thing you may do to one of their existing windows is desktop_bring_to_front to surface it for them.',+    '⛔ BACKGROUND BY DEFAULT - NEVER foreground a window unless the user explicitly asked to watch. nbrowser_open_window opens UNFOCUSED, open_tab opens active:false, switch_window does NOT raise - keep it that way. To capture a chrome:// / edge:// page (e.g. chrome://extensions for a guide), DO NOT launch the browser yourself (msedge/chrome `--new-window` via launch/launch_process RAISES the new window to the foreground). Instead: nbrowser_open_window {url:"about:blank"} (background) → desktop_navigate that window\'s hwnd to the chrome:// URL (background, no foreground) → desktop_screenshot_window (WGC captures a background/occluded window) → nbrowser_close_window. Same for recording (WGC) and screenshots (nbrowser_screenshot CDP) - both capture occluded windows, so foregrounding is never needed. Foregrounding is the USER\'s choice; do it only on explicit request (desktop_bring_to_front).',     'Before driving, nbrowser_status to confirm connected. After a dev_reload, expect a ~3-6s reconnect.',-    '⭐ BACKGROUND IS THE WHOLE POINT — NEVER foreground the driven window to screenshot OR record. The user must keep working while a demo records behind their other windows; yanking Chrome forward makes the feature useless. nbrowser_screenshot (CDP) captures a background/occluded window unconditionally. WGC recording captures a background/occluded window TOO — but only when the recorded Chrome was launched with the occlusion-disable flags (see Recording); without them an occluded window paints blank. Foregrounding is the USER\'s choice, made by them when they want to watch — it is never something the AI should do on its own.',-    'For demos: nbrowser_caption (in-page subtitle/alert) OR AD desktop_caption (fullscreen; use position:bottom — top covers the URL bar + Chrome warning).',+    '⭐ BACKGROUND IS THE WHOLE POINT - NEVER foreground the driven window to screenshot OR record. The user must keep working while a demo records behind their other windows; yanking Chrome forward makes the feature useless. nbrowser_screenshot (CDP) captures a background/occluded window unconditionally. WGC recording captures a background/occluded window TOO - but only when the recorded Chrome was launched with the occlusion-disable flags (see Recording); without them an occluded window paints blank. Foregrounding is the USER\'s choice, made by them when they want to watch - it is never something the AI should do on its own.',+    'For demos: nbrowser_caption (in-page subtitle/alert) OR AD desktop_caption (fullscreen; use position:bottom - top covers the URL bar + Chrome warning).',     'Mouse verbs: pass cursor:true to show the gliding Adom cursor, and intent:"Clicking the Pay button" so the cursor LABEL narrates the action (builds user trust).',-    '⭐ Recording a browser window: the BEST tool is the CORE adom-desktop verb desktop_record_window_start (WGC) — or nbrowser_record_start {method:"wgc"}, which delegates to it. Background, up to 60fps, NO source picker, NO banner, captures OCCLUDED windows, never steals focus. This bridge\'s own getDisplayMedia/tabcapture recording is only a LAST-RESORT FALLBACK and is REFUSED unless you pass fallbackConfirmed:true, because a browser extension CANNOT silently screen-record (Chrome forces a manual share-source picker a human must click + a persistent banner, and won\'t reliably capture a background window). Prefer WGC every time; the extension is the fallback. ⚠️ For WGC background capture of an OCCLUDED Chrome window, Chrome must be launched with `--disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-background-timer-throttling --disable-features=CalculateNativeWinOcclusion` (pup\'s Chrome ships these; a default user Chrome does NOT — without them Chrome stops painting the occluded window and WGC records a blank surface). Stop with nbrowser_record_stop {method:"wgc",recordingId}.',+    '⭐ Recording a browser window: the BEST tool is the CORE adom-desktop verb desktop_record_window_start (WGC) - or nbrowser_record_start {method:"wgc"}, which delegates to it. Background, up to 60fps, NO source picker, NO banner, captures OCCLUDED windows, never steals focus. This bridge\'s own getDisplayMedia/tabcapture recording is only a LAST-RESORT FALLBACK and is REFUSED unless you pass fallbackConfirmed:true, because a browser extension CANNOT silently screen-record (Chrome forces a manual share-source picker a human must click + a persistent banner, and won\'t reliably capture a background window). Prefer WGC every time; the extension is the fallback. ⚠️ For WGC background capture of an OCCLUDED Chrome window, Chrome must be launched with `--disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-background-timer-throttling --disable-features=CalculateNativeWinOcclusion` (pup\'s Chrome ships these; a default user Chrome does NOT - without them Chrome stops painting the occluded window and WGC records a blank surface). Stop with nbrowser_record_stop {method:"wgc",recordingId}.',   ],   verbs: {     'open_window/open_tab/close_window/close_tab/switch_window/switch_tab/list_windows/list_tabs/rescan': 'Sessions = a real Chrome window (durable across reloads). tabId = real chrome tab id. close what you open.',@@ -255,7 +255,7 @@ const HELP = {     'fetch_url/download': 'fetch_url uses the user\'s REAL cookies (authed). download via chrome.downloads.',     'errors/events': 'errors = console/page errors for a tab. events = window/tab closes incl. byUser.',     'record_start/stop/status/list': 'method: wgc|getdisplaymedia|tabcapture. record_start is non-blocking for getdisplaymedia (poll record_status). base64 return is for SHORT clips only.',-    'caption/focus': 'caption{style:subtitle|alert,position}. focus activates the tab + flashes its taskbar button (does NOT raise the window — by design, never steals the user\'s focus). Capture in place; only desktop_bring_to_front when the user explicitly wants to watch live.',+    'caption/focus': 'caption{style:subtitle|alert,position}. focus activates the tab + flashes its taskbar button (does NOT raise the window - by design, never steals the user\'s focus). Capture in place; only desktop_bring_to_front when the user explicitly wants to watch live.',     'flash/flash_stop/flash_mode/taskbar': 'Paint glanceable status on the driven browser window\'s Windows TASKBAR button (hwnd captured at open_window; an "Adom is driving" badge goes on automatically). flash = orange "come look" cue (auto-clears on focus). flash_mode{quiet|chatty|off}: quiet(default)=flash on come-look moments (load/screenshot/error/done); chatty=every action (users who want to watch); off=no auto-flash. nbrowser_taskbar{progress:{state:"normal"|"paused"|"error"|"indeterminate"|"none",value?},overlay:{badge:"adom"|"none"}} = progress bar (green/YELLOW-for-gates/red/marquee) + overlay badge for long tasks. Use paused(yellow)+flash on captcha/2FA/login. Clears on close.',   }, };@@ -263,8 +263,8 @@ const HELP = { // ---- WGC recording: bridge -> AD (standalone, NO HD dependency) ----------------------------------- // WGC per-window capture can't be done in a browser, so it lives in AD (which is always present with // this bridge). We delegate to AD's direct API. AD writes the mp4 to a WINDOWS path (so pull_file-// grabs it directly — no WSL distro). NEVER call HD here: AD must run standalone without HD.-// AD 1.8.149 control-API discovery contract: env var first (AD injects it into every spawned bridge —+// grabs it directly - no WSL distro). NEVER call HD here: AD must run standalone without HD.+// AD 1.8.149 control-API discovery contract: env var first (AD injects it into every spawned bridge - // no file race, no stale port), then the host:port discovery file, then AD's fixed 47200..47209 range. function discoverAdBase() {   if (process.env.ADOM_DIRECT_API_URL) return process.env.ADOM_DIRECT_API_URL.replace(/\/+$/, '');@@ -288,27 +288,27 @@ function adCommand(app, command, args) {   }); } // ⭐ The hint we hand back whenever an AI tries to record WITHOUT choosing WGC. The AI is not assumed-// to know the core adom-desktop window recorder exists, so we spell it out and explain — in detail —+// to know the core adom-desktop window recorder exists, so we spell it out and explain - in detail - // why the extension's own screen-capture is a heinous last resort. (Lesson from a real thread that // blundered into the getDisplayMedia picker before discovering desktop_record_window_start on its own.) const RECORD_WGC_HINT =-  '⭐ BEST PATH — record the window with WGC, NOT the extension. Use the CORE adom-desktop verb ' +-  'desktop_record_window_start {"titleContains":"<window title>"} (or {"hwnd":<n>}) — or call THIS verb ' ++  '⭐ BEST PATH - record the window with WGC, NOT the extension. Use the CORE adom-desktop verb ' ++  'desktop_record_window_start {"titleContains":"<window title>"} (or {"hwnd":<n>}) - or call THIS verb ' +   'with method:"wgc", which delegates straight to it. WGC (Windows Graphics Capture) records ONE specific ' +   'window in the BACKGROUND at HIGH frame rate (up to 60fps), with NO source picker, NO "sharing" banner, ' +   'captures the window even while it is OCCLUDED/behind other windows, and NEVER steals the user\'s focus. ' +   'Stop with desktop_record_window_stop (or nbrowser_record_stop {"method":"wgc","recordingId":...}); poll ' +-  'desktop_record_window_status. This is the right tool for recording a browser window — reach for it first.\n' ++  'desktop_record_window_status. This is the right tool for recording a browser window - reach for it first.\n' +   '⛔ The extension\'s OWN recording (method:"getdisplaymedia"/"tabcapture") is a LAST-RESORT FALLBACK, used ' +   'ONLY if WGC is genuinely unavailable. By Chrome\'s security design a browser extension CANNOT silently ' +   'screen-record: getDisplayMedia FORCES a share-source PICKER that a HUMAN must click to choose the window/ ' +-  'screen (yes — heinous: the whole point of a logged-in browser extension is that it should never need a ' ++  'screen (yes - heinous: the whole point of a logged-in browser extension is that it should never need a ' +   'manual picker), it paints a persistent recording banner, and it does NOT reliably capture a background/ ' +   'occluded window. I am the fallback, not the default. If WGC truly will not work, re-call with ' +   'method:"getdisplaymedia" AND fallbackConfirmed:true to acknowledge the picker + banner tradeoff.';  // AD's native per-window recorder shipped in adom-desktop 1.8.148 (standalone Rust WGC + Media-// Foundation — no picker, no banner, background-capturable, real .mp4 under %TEMP%\adom-desktop-+// Foundation - no picker, no banner, background-capturable, real .mp4 under %TEMP%\adom-desktop- // recordings\, pullable with pull_file). We delegate method:"wgc" straight to it. async function wgcRecord(verb, args) {   const map = {@@ -321,7 +321,7 @@ async function wgcRecord(verb, args) {   delete adArgs.method; // 'wgc' is the bridge-side selector, not an AD arg   const r = await adCommand('desktop', map[verb], adArgs);   let out = r.body;-  // AD wraps results differently across surfaces — unwrap {output:"<json>"} and/or {data:{...}} so+  // AD wraps results differently across surfaces - unwrap {output:"<json>"} and/or {data:{...}} so   // recordingId/path/fileExists are reachable no matter which shape the direct API returns.   if (out && typeof out.output === 'string') { try { out = JSON.parse(out.output); } catch {} }   if (out && out.data && typeof out.data === 'object') out = out.data;@@ -330,12 +330,12 @@ async function wgcRecord(verb, args) {     return {       success: false,       error: 'AD is too old for native window recording',-      _hint: 'desktop_record_window_* shipped in adom-desktop 1.8.148 — upgrade AD (adom-desktop --version must be >= 1.8.148; older builds mis-route to the puppeteer bridge). Meanwhile use method:"getdisplaymedia" or the "Record this tab" toolbar button.',+      _hint: 'desktop_record_window_* shipped in adom-desktop 1.8.148 - upgrade AD (adom-desktop --version must be >= 1.8.148; older builds mis-route to the puppeteer bridge). Meanwhile use method:"getdisplaymedia" or the "Record this tab" toolbar button.',     };   }   if (r.status >= 200 && r.status < 300 && out && out.success !== false && !out.error) {-    const stopHint = 'mp4 is on the laptop at out.path (under %TEMP%\\adom-desktop-recordings) — fetch it with: adom-desktop pull_file {"filePaths":["<path>"]}';-    const startHint = 'recording started in the BACKGROUND — the user keeps working; do NOT foreground the window (foregrounding is the user\'s choice, never the AI\'s). ⚠️ If frames come out BLANK, the recorded Chrome was NOT launched with the occlusion-disable flags (--disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-background-timer-throttling --disable-features=CalculateNativeWinOcclusion) — Chrome stops painting an occluded window without them. pup\'s Chrome already has these (so it background-records fine); a default user Chrome must be relaunched with them. Poll nbrowser_record_status {method:"wgc",recordingId}; stop with nbrowser_record_stop {method:"wgc",recordingId}.';+    const stopHint = 'mp4 is on the laptop at out.path (under %TEMP%\\adom-desktop-recordings) - fetch it with: adom-desktop pull_file {"filePaths":["<path>"]}';+    const startHint = 'recording started in the BACKGROUND - the user keeps working; do NOT foreground the window (foregrounding is the user\'s choice, never the AI\'s). ⚠️ If frames come out BLANK, the recorded Chrome was NOT launched with the occlusion-disable flags (--disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-background-timer-throttling --disable-features=CalculateNativeWinOcclusion) - Chrome stops painting an occluded window without them. pup\'s Chrome already has these (so it background-records fine); a default user Chrome must be relaunched with them. Poll nbrowser_record_status {method:"wgc",recordingId}; stop with nbrowser_record_stop {method:"wgc",recordingId}.';     return { success: true, output: JSON.stringify({       ok: true, method: 'wgc',       recordingId: out.recordingId, path: out.path, fileExists: out.fileExists,@@ -343,7 +343,7 @@ async function wgcRecord(verb, args) {       _hint: verb === 'nbrowser_record_stop' ? stopHint : startHint,     }) };   }-  // a real AD-side failure (window not found, capture error, …) — surface it, don't pretend it's missing+  // a real AD-side failure (window not found, capture error, …) - surface it, don't pretend it's missing   return {     success: false,     error: (out && out.error) || `WGC ${verb} failed`,@@ -356,7 +356,7 @@ async function wgcRecord(verb, args) { // We paint a "come look" cue on the REAL browser window's Windows taskbar button. The hwnd is captured // at control-start (the just-opened window is foreground) and cached, so flashes never hit the wrong // window. Flash works today via run_script FlashWindowEx; overlay/progress need AD's desktop_taskbar-// (ITaskbarList3 — can't be driven cleanly from PowerShell). See TASKBAR-FEATURE.md.+// (ITaskbarList3 - can't be driven cleanly from PowerShell). See TASKBAR-FEATURE.md. let lastHwnd = 0;                  // most-recently driven window const sessionHwnd = new Map();     // sessionId -> hwnd let flashMode = 'quiet';           // quiet | chatty | off@@ -372,7 +372,7 @@ function taskbar(hwnd, opts) { } function flashHwnd(hwnd, stop) { taskbar(hwnd, { flash: stop ? 'stop' : 'until_focused' }); } // Capture the driven window's hwnd by DIFFING the real-Chrome window list across the open (the new-// hwnd is the one we opened). Robust vs Windows foreground-lock — no GetForegroundWindow guessing.+// hwnd is the one we opened). Robust vs Windows foreground-lock - no GetForegroundWindow guessing. let preOpenHwnds = new Set(); async function chromeHwnds() {   const r = await adCommand('desktop', 'desktop_list_windows', {});@@ -391,7 +391,7 @@ function maybeAutoFlash(verb, payload) { } const FLASH_HINT = 'This window\'s Windows taskbar can signal the user. flashMode=' ; function flashHint() {-  return `${FLASH_HINT}${flashMode}. Get the user's attention: nbrowser_flash {sessionId}. The USER may prefer more/less noise — nbrowser_flash_mode {mode:"quiet"|"chatty"|"off"} (quiet=come-look moments only [default]; chatty=every action, for users who want to watch; off=silent). On a human-in-the-loop gate (captcha/2FA/login), nbrowser_flash so they come help. Never leave it strobing — it auto-clears on focus, or nbrowser_flash_stop.`;+  return `${FLASH_HINT}${flashMode}. Get the user's attention: nbrowser_flash {sessionId}. The USER may prefer more/less noise - nbrowser_flash_mode {mode:"quiet"|"chatty"|"off"} (quiet=come-look moments only [default]; chatty=every action, for users who want to watch; off=silent). On a human-in-the-loop gate (captcha/2FA/login), nbrowser_flash so they come help. Never leave it strobing - it auto-clears on focus, or nbrowser_flash_stop.`; }  function dispatch(verb, args) {@@ -399,7 +399,7 @@ function dispatch(verb, args) {     if (verb === 'nbrowser_help') {       return resolve({ success: true, output: JSON.stringify({ ok: true, bridge: BRIDGE, version: VERSION, extConnected: anyConnected(), flashMode, ...HELP }) });     }-    // nbrowser_describe — per-verb schemas for AD's Verbs tab + inline runner (AD v1.9.23+).+    // nbrowser_describe - per-verb schemas for AD's Verbs tab + inline runner (AD v1.9.23+).     // AD POSTs {command:"nbrowser_describe",args:{}} and renders each verb expandable with these I/O     // docs, prefilling the runner with `example`. One entry per declared verb (+ describe itself).     if (verb === 'nbrowser_describe') {@@ -458,7 +458,7 @@ function dispatch(verb, args) {     // --- multi-profile management (bridge-local) ---     if (verb === 'nbrowser_profiles') {       const list = [...conns.entries()].map(([k, c]) => ({ profile: k, label: profileLabel(c.profile), email: c.profile.email || '', browser: c.profile.browser || 'chrome', active: k === activeProfile, blocked: isBlocked(k), live: !!(c.sock && !c.sock.destroyed) }));-      return resolve({ success: true, output: JSON.stringify({ ok: true, profiles: list, active: activeProfile, blocklist: [...blocklist], _hint: 'Each entry is one BROWSER×PROFILE the AI can drive — `profile` is the addressable key like "edge:[email protected]" (browser:email). Target any of them in PARALLEL by passing profile:"<key>" (or profile:"<email>"+browser:"edge") on any verb — different AI threads can each drive a different one at once. ▶active is only the default when no target is passed. Switch the default with nbrowser_use_profile; exclude one with nbrowser_profile_block.' }) });+      return resolve({ success: true, output: JSON.stringify({ ok: true, profiles: list, active: activeProfile, blocklist: [...blocklist], _hint: 'Each entry is one BROWSER×PROFILE the AI can drive - `profile` is the addressable key like "edge:[email protected]" (browser:email). Target any of them in PARALLEL by passing profile:"<key>" (or profile:"<email>"+browser:"edge") on any verb - different AI threads can each drive a different one at once. ▶active is only the default when no target is passed. Switch the default with nbrowser_use_profile; exclude one with nbrowser_profile_block.' }) });     }     if (verb === 'nbrowser_use_profile') {       const want = String((args && (args.profile || args.email)) || '');@@ -476,7 +476,7 @@ function dispatch(verb, args) {       saveBlocklist();       return resolve({ success: true, output: JSON.stringify({ ok: true, blocklist: [...blocklist], active: activeProfile, _hint: verb === 'nbrowser_profile_block' ? 'Adom will NEVER drive this profile until unblocked (security whitelist). Persisted across restarts.' : 'Profile is drivable again.' }) });     }-    // The CREDENTIAL LIBRARY — which vendors this user has a saved login for (origins+usernames; NEVER+    // The CREDENTIAL LIBRARY - which vendors this user has a saved login for (origins+usernames; NEVER     // passwords). Chrome's extension API can't read this; the bridge reads Chrome's Login Data DB     // natively. Use it to know up-front which sites you can auto-login to, then drive the login.     if (verb === 'nbrowser_credentials') {@@ -484,20 +484,20 @@ function dispatch(verb, args) {       const browser = args && args.browser;   // optional: "chrome" | "edge"       const profile = args && (args.profile || args.email); // optional: a specific profile email       const rows = queryCredentials({ browser, profile, match });-      return resolve({ success: true, output: JSON.stringify({ ok: true, count: rows.length, credentials: rows, _hint: 'Read-only query of the BROWSER\'s own saved-login store (Chrome + Edge) — we do NOT store or replace passwords; the browser stays the source of truth. Each row = {browser, profile, host, username} you CAN auto-login to (password is NOT exposed — Chrome/Edge autofill it when you focus the login field). Flow: credentials {match:"digikey"} -> if present, navigate to the sign-in page -> click the user/email field (autofills) -> click submit -> login_state. Filters: match:"<vendor>", browser:"chrome"|"edge", profile:"<email>".' }) });+      return resolve({ success: true, output: JSON.stringify({ ok: true, count: rows.length, credentials: rows, _hint: 'Read-only query of the BROWSER\'s own saved-login store (Chrome + Edge) - we do NOT store or replace passwords; the browser stays the source of truth. Each row = {browser, profile, host, username} you CAN auto-login to (password is NOT exposed - Chrome/Edge autofill it when you focus the login field). Flow: credentials {match:"digikey"} -> if present, navigate to the sign-in page -> click the user/email field (autofills) -> click submit -> login_state. Filters: match:"<vendor>", browser:"chrome"|"edge", profile:"<email>".' }) });     }     // AUTOLOGIN (path A): decrypt the saved password LOCALLY and forward it DOWNSTREAM to the extension,     // which injects it into the password field via CDP. Opt-in (confirm:true) + host-scoped. The-    // plaintext is NEVER returned, logged, or sent up the relay — only {ok,loggedIn,needs2FA} comes back.+    // plaintext is NEVER returned, logged, or sent up the relay - only {ok,loggedIn,needs2FA} comes back.     if (verb === 'nbrowser_autologin') {       const tgt0 = targetConn(args);       if (!tgt0 || tgt0.blocked || !tgt0.conn) return resolve({ success: false, error: 'native browser extension not connected', _hint: 'Open a window in the target profile first; nbrowser_profiles lists connected profiles.' });-      if (!args || args.confirm !== true) return resolve({ success: false, error: 'autologin is gated — pass confirm:true to authorize using the saved password for this host', _hint: 'Opt-in per call (so it can never fire silently for arbitrary sites). The AI never sees the password: it is decrypted on the laptop and injected by the extension. Also pass host:"login.<vendor>.com" and optionally username.' });+      if (!args || args.confirm !== true) return resolve({ success: false, error: 'autologin is gated - pass confirm:true to authorize using the saved password for this host', _hint: 'Opt-in per call (so it can never fire silently for arbitrary sites). The AI never sees the password: it is decrypted on the laptop and injected by the extension. Also pass host:"login.<vendor>.com" and optionally username.' });       const host = String(args.host || '').replace(/^https?:\/\//, '').split('/')[0].toLowerCase();-      if (!host) return resolve({ success: false, error: 'host required (e.g. host:"login.gusto.com")', _hint: 'The host whose saved password to inject — must be explicit (no wildcard).' });+      if (!host) return resolve({ success: false, error: 'host required (e.g. host:"login.gusto.com")', _hint: 'The host whose saved password to inject - must be explicit (no wildcard).' });       const browser = (tgt0.conn.profile && tgt0.conn.profile.browser) || 'chrome';       const profileEmail = (tgt0.conn.profile && tgt0.conn.profile.email) || '';-      // The bridge is sandboxed (its DPAPI scope can't decrypt Chrome's blobs — verified 2026-06-23), so we+      // The bridge is sandboxed (its DPAPI scope can't decrypt Chrome's blobs - verified 2026-06-23), so we       // DEFER decryption to the native host, which runs in Chrome's real context. Flag it + forward; the       // host attaches __secret downstream. The bridge NEVER sees the password.       dlog(`autologin: host=${host} profile=${browser}:${profileEmail} (decrypt deferred to in-Chrome host)`);@@ -518,7 +518,7 @@ function dispatch(verb, args) {       const stop = verb === 'nbrowser_flash_stop';       const hwnd = (args && args.sessionId && sessionHwnd.get(args.sessionId)) || lastHwnd;       if (!hwnd) {-        return resolve({ success: false, error: 'no driven window captured yet', _hint: 'Open/drive a browser window first (nbrowser_open_window) — the bridge captures its taskbar hwnd at that moment. Then nbrowser_flash works.' });+        return resolve({ success: false, error: 'no driven window captured yet', _hint: 'Open/drive a browser window first (nbrowser_open_window) - the bridge captures its taskbar hwnd at that moment. Then nbrowser_flash works.' });       }       flashHwnd(hwnd, stop);       return resolve({ success: true, output: JSON.stringify({ ok: true, hwnd, stopped: stop, _hint: stop ? 'Taskbar flash cleared.' : 'Flashing the browser taskbar ORANGE until the user focuses that window (auto-clears on focus). nbrowser_flash_stop clears it early.' }) });@@ -530,10 +530,10 @@ function dispatch(verb, args) {       if (args && args.progress) opts.progress = args.progress;       if (args && args.overlay) opts.overlay = args.overlay;       if (args && args.flash) opts.flash = args.flash;-      taskbar(hwnd, opts).then(() => resolve({ success: true, output: JSON.stringify({ ok: true, hwnd, applied: Object.keys(opts), _hint: 'progress.state: normal(green) | paused(YELLOW — use for captcha/2FA/login human-gates) | error(red) | indeterminate(marquee) | none. overlay.badge: "adom" (the driven-by-Adom badge) | "none". Clear ALL at task-end: {progress:{state:"none"},overlay:{badge:"none"},flash:"stop"}.' }) }));+      taskbar(hwnd, opts).then(() => resolve({ success: true, output: JSON.stringify({ ok: true, hwnd, applied: Object.keys(opts), _hint: 'progress.state: normal(green) | paused(YELLOW - use for captcha/2FA/login human-gates) | error(red) | indeterminate(marquee) | none. overlay.badge: "adom" (the driven-by-Adom badge) | "none". Clear ALL at task-end: {progress:{state:"none"},overlay:{badge:"none"},flash:"stop"}.' }) }));       return;     }-    // WGC recording is handled HERE (bridge -> AD direct API, standalone — never HD), not in the+    // WGC recording is handled HERE (bridge -> AD direct API, standalone - never HD), not in the     // extension (a browser can't capture another window). method:"wgc".     if (verb.startsWith('nbrowser_record_') && args && args.method === 'wgc') {       return wgcRecord(verb, args).then(resolve);@@ -549,8 +549,8 @@ function dispatch(verb, args) {         return resolve({           success: false,           error: m-            ? `method:"${m}" is the inferior fallback — WGC is strongly preferred for recording a browser window`-            : 'no recording method chosen — WGC is strongly recommended (this is a fallback recorder)',+            ? `method:"${m}" is the inferior fallback - WGC is strongly preferred for recording a browser window`+            : 'no recording method chosen - WGC is strongly recommended (this is a fallback recorder)',           recommended: { method: 'wgc', coreVerb: 'desktop_record_window_start' },           _hint: RECORD_WGC_HINT,         });@@ -558,10 +558,10 @@ function dispatch(verb, args) {       // acknowledged fallback → fall through to the extension's getDisplayMedia/tabCapture path below.     }     // OS-level window raise/focus is owned by the CORE adom-desktop verb desktop_bring_to_front. The-    // extension deliberately does NOT implement a focus/raise-window verb — it must never steal the+    // extension deliberately does NOT implement a focus/raise-window verb - it must never steal the     // user's focus. So we don't expose a phantom verb; but an AI coming from pup will guess     // browser_focus_window -> nbrowser_focus_window. Rather than fail opaquely in the extension,-    // redirect it to the core verb (with THIS session's captured hwnd) — it would find it on its own.+    // redirect it to the core verb (with THIS session's captured hwnd) - it would find it on its own.     const RAISE_REDIRECT = {       nbrowser_focus_window: 'OS window raise/focus',       nbrowser_raise_os_window: 'OS window raise',@@ -573,16 +573,16 @@ function dispatch(verb, args) {       const target = hwnd ? `{"hwnd":${hwnd}}` : '{"titleContains":"<the driven window\'s title>"}';       return resolve({         success: false,-        error: `${verb} is not an extension verb — ${RAISE_REDIRECT[verb]} is owned by the core adom-desktop verb desktop_bring_to_front`,-        _hint: `Use the CORE AD verb instead: desktop_bring_to_front ${target}. The extension intentionally never raises or steals the user's focus — nbrowser_focus only activates the tab + flashes its taskbar button; desktop_bring_to_front is the explicit "bring it to the front so the user can watch" action.`,+        error: `${verb} is not an extension verb - ${RAISE_REDIRECT[verb]} is owned by the core adom-desktop verb desktop_bring_to_front`,+        _hint: `Use the CORE AD verb instead: desktop_bring_to_front ${target}. The extension intentionally never raises or steals the user's focus - nbrowser_focus only activates the tab + flashes its taskbar button; desktop_bring_to_front is the explicit "bring it to the front so the user can watch" action.`,       });     }-    // Other pup verbs with no 1:1 extension verb — redirect to the right one instead of failing opaquely.+    // Other pup verbs with no 1:1 extension verb - redirect to the right one instead of failing opaquely.     if (verb === 'nbrowser_alert_window') {       return resolve({ success: false, error: 'nbrowser_alert_window is not an extension verb', _hint: 'Flash the driven window\'s taskbar button with nbrowser_flash {sessionId} (orange "come look" cue; nbrowser_flash_stop to clear). For an arbitrary OS window, the core AD verb is desktop_flash_window {hwnd}.' });     }     if (verb === 'nbrowser_close') {-      return resolve({ success: false, error: 'nbrowser_close is not an extension verb', _hint: 'Close a specific window with nbrowser_close_window {sessionId} or a tab with nbrowser_close_tab {sessionId,tabId}. There is no global "detach all" — the extension idle-detaches the debugger automatically when no verb is running.' });+      return resolve({ success: false, error: 'nbrowser_close is not an extension verb', _hint: 'Close a specific window with nbrowser_close_window {sessionId} or a tab with nbrowser_close_tab {sessionId,tabId}. There is no global "detach all" - the extension idle-detaches the debugger automatically when no verb is running.' });     }     const tgt = targetConn(args);     if (!tgt || tgt.blocked || !tgt.conn) {@@ -622,7 +622,7 @@ function onHostFrame(frame) {       let payload = frame.payload || {};       // Screenshot file-save: huge base64 over the relay times out, so when the caller passes       // toFile:true we write the PNG to a laptop temp file here and return its path instead of the-      // base64. Pull it with `adom-desktop pull_file` (binary chunked) — fast + no relay timeout.+      // base64. Pull it with `adom-desktop pull_file` (binary chunked) - fast + no relay timeout.       const isShot = /screenshot/.test(p.verb);       const isRec = p.verb === 'nbrowser_record_stop';       if (payload.base64 && ((isShot && p.toFile) || isRec)) {@@ -638,7 +638,7 @@ function onHostFrame(frame) {         } catch (e) { payload._fileError = String((e && e.message) || e); }       }       // Finder debug ({debug}/{resolveOnly}): spill annotatedShot + shots[] base64 to laptop files so the-      // (possibly several) PNGs never bloat the relay. Always spill when present — debug is opt-in already.+      // (possibly several) PNGs never bloat the relay. Always spill when present - debug is opt-in already.       if (payload.finder && typeof payload.finder === 'object') {         try {           const dir = path.join(process.env.TEMP || os.tmpdir(), 'adom-nbrowser-shots');@@ -654,11 +654,11 @@ function onHostFrame(frame) {           const f = payload.finder;           if (f.annotatedShot) spill(f.annotatedShot, 'annotated');           if (Array.isArray(f.shots)) f.shots.forEach((s, i) => spill(s, (s.label || 'shot') + '-' + i));-          f._hint = 'Pull annotatedShot.path / shots[].path with `adom-desktop pull_file {filePaths:[...]}` (binary, no relay timeout). In the annotated overlay each candidate is a numbered box — ★=chosen, ⛔=occluded — and the blue crosshair is the click point.';+          f._hint = 'Pull annotatedShot.path / shots[].path with `adom-desktop pull_file {filePaths:[...]}` (binary, no relay timeout). In the annotated overlay each candidate is a numbered box - ★=chosen, ⛔=occluded - and the blue crosshair is the click point.';         } catch (e) { payload._finderFileError = String((e && e.message) || e); }       }       // taskbar: capture the driven window's hwnd at control-start, auto-flash per mode, and TEACH the-      // feature on open (a fresh AI thread must discover it — it isn't obvious).+      // feature on open (a fresh AI thread must discover it - it isn't obvious).       if (p.verb === 'nbrowser_open_window') {         const sid = payload.sessionId;         chromeHwnds().then((after) => {@@ -666,7 +666,7 @@ function onHostFrame(frame) {           if (neu) {             lastHwnd = neu;             if (sid) sessionHwnd.set(sid, neu);-            // the "Adom is driving your browser" overlay badge — on for the window's whole driven life+            // the "Adom is driving your browser" overlay badge - on for the window's whole driven life             taskbar(neu, { overlay: { badge: 'adom', tooltip: 'Adom is driving this browser' } });           }         }).catch(() => {});@@ -716,7 +716,7 @@ const httpSrv = http.createServer((req, res) => {  // ---- circuit breaker ------------------------------------------------------- // Backstop: if hosts connect in a tight storm (the runaway-accumulation signature), STOP feeding the-// loop — log loudly and refuse new host connections for a cooldown. Even if the host/SW backoff ever+// loop - log loudly and refuse new host connections for a cooldown. Even if the host/SW backoff ever // regressed, the bridge alone caps the damage. Healthy operation is ~1 connect per host lifetime, so a // burst of 20 connects in 10s is unambiguously pathological. const CB_WINDOW_MS = 10000;@@ -728,13 +728,13 @@ let cbTrippedUntil = 0; // ---- raw-TCP server for the native-messaging host -------------------------- const hostSrv = net.createServer((sock) => {   const now = Date.now();-  if (now < cbTrippedUntil) { try { sock.destroy(); } catch {} return; } // breaker open — refuse+  if (now < cbTrippedUntil) { try { sock.destroy(); } catch {} return; } // breaker open - refuse   cbConnects = cbConnects.filter((t) => now - t < CB_WINDOW_MS);   cbConnects.push(now);   if (cbConnects.length > CB_MAX_CONNECTS) {     cbTrippedUntil = now + CB_COOLDOWN_MS;     cbConnects = [];-    process.stderr.write(`[native-browser] CIRCUIT BREAKER TRIPPED: >${CB_MAX_CONNECTS} host connects in ${CB_WINDOW_MS}ms — refusing new host connections for ${CB_COOLDOWN_MS}ms (runaway native-host loop suspected)\n`);+    process.stderr.write(`[native-browser] CIRCUIT BREAKER TRIPPED: >${CB_MAX_CONNECTS} host connects in ${CB_WINDOW_MS}ms - refusing new host connections for ${CB_COOLDOWN_MS}ms (runaway native-host loop suspected)\n`);     try { sock.destroy(); } catch {}     return;   }@@ -767,7 +767,7 @@ function writeHostFile(hostPort) { } // PID-aware cleanup: ONLY delete host.json if it still points at *this* process. Otherwise a churning // instance (AD respawn, a manual run, a crash-restart) deletes the discovery file that another LIVE-// bridge owns — exactly what stranded the extension (host.json vanished while a bridge was up). Never+// bridge owns - exactly what stranded the extension (host.json vanished while a bridge was up). Never // blow away a sibling's file. function cleanup() {   try {@@ -786,14 +786,14 @@ function argPort() { }  // Resilient bind. If AD hands us a --port that's already held (HNS reservation, a zombie bridge, a-// double-spawn), DON'T die silently — fall back to an ephemeral port and keep serving. The hostSrv ++// double-spawn), DON'T die silently - fall back to an ephemeral port and keep serving. The hostSrv + // discovery file get written regardless, so the extension always has a live port to dial. let httpFellBack = false; function startHttp(p) {   httpSrv.once('error', (e) => {     if (e && e.code === 'EADDRINUSE' && !httpFellBack) {       httpFellBack = true;-      process.stderr.write(`[native-browser] http :${p} in use (${e.code}) — falling back to ephemeral\n`);+      process.stderr.write(`[native-browser] http :${p} in use (${e.code}) - falling back to ephemeral\n`);       try { httpSrv.close(); } catch {}       setTimeout(() => startHttp(0), 50);     } else {
native-browser-bridge-manifest.json+5−5
@@ -1,8 +1,8 @@ {   "manifest_version": 1,-  "version": "0.0.17",-  "url": "native-browser-bridge-v0.0.17.bin",-  "sha256": "6fab19e58b2900cdea321af74346310743018edaad99a60e45d1f661f664f08e",-  "size": 24334,-  "released_at": "2026-06-27T19:47:28Z"+  "version": "0.0.18",+  "url": "native-browser-bridge-v0.0.18.bin",+  "sha256": "b6af866e5f4b1967b24f264a9d669389743770964574b933ed9aaf5261bb9a6d",+  "size": 24297,+  "released_at": "2026-06-27T19:49:25Z" }
skills/driving-the-extension-demo/SKILL.md+16−16
@@ -18,7 +18,7 @@ capability reference (CDP-vs-native, the debug bar, xy-finding, verbs), read the  ## ⭐ The golden rules for a watchable demo (learned the hard way) 1. **Foreground the window before EVERY step**, or the user can't see it and your screenshots are-   wrong: `desktop_bring_to_front {"hwnd":<HWND>}` — **omit `state`** so it just raises and+   wrong: `desktop_bring_to_front {"hwnd":<HWND>}` - **omit `state`** so it just raises and    **preserves** the current maximized/fullscreen state. (`state:"restore"` would un-maximize it.) 2. **Most users mean MAXIMIZED, not immersive fullscreen.** Run the majority of a demo maximized    (`window_state {state:"maximized"}`). Show true fullscreen only as a quick "it can also do this".@@ -30,7 +30,7 @@ capability reference (CDP-vs-native, the debug bar, xy-finding, verbs), read the      a white frame** (compositor race) → retry, and if it keeps failing, capture each window with      `desktop_screenshot_window` and **montage** them (see the side-by-side recipe). 4. **Caption every step** so the viewer knows what's happening:-   `desktop_caption {"text":"…","position":"bottom","id":"nbd","durationMs":120000}` (bottom — top+   `desktop_caption {"text":"…","position":"bottom","id":"nbd","durationMs":120000}` (bottom - top    covers the URL bar + debug banner). Clear with `{"text":"","id":"nbd","clear":true}`. 5. **shotlog every frame AND surface the viewer** (don't just paste a URL): `shotlog open --channel    <ch>` then `adom-cli hydrogen workspace active-tab --name "Shots: <ch>"`, then per frame@@ -41,11 +41,11 @@ capability reference (CDP-vs-native, the debug bar, xy-finding, verbs), read the 7. **Clean up after yourself:** `nbrowser_flash_stop`, `nbrowser_taskbar {progress:{state:"none"},    overlay:{badge:"none"}}`, close extra windows, clear the caption. -## Recipe — guided interaction demo (the dark demo page)-A self-contained dark-mode page (good for video — not too bright) with a Pay button, a name field,+## Recipe - guided interaction demo (the dark demo page)+A self-contained dark-mode page (good for video - not too bright) with a Pay button, a name field, a hover-tooltip, and a right-click box. Write it once to the user's box, then drive it. ```bash-# (page source is in this repo: demo/demo3.html — write it to C:\Users\john\.adom\demo3.html)+# (page source is in this repo: demo/demo3.html - write it to C:\Users\john\.adom\demo3.html) nbrowser_navigate    {"sessionId":"demo","url":"file:///C:/Users/<u>/.adom/demo3.html"} nbrowser_window_state {"sessionId":"demo","state":"maximized"} nbrowser_click  {"sessionId":"demo","text":"Pay","cursor":true,"intent":"Clicking the Pay button"}   # click BY TEXT + Adom cursor@@ -54,8 +54,8 @@ nbrowser_hover  {"sessionId":"demo","text":"Hover me","cursor":true,"intent":"Ho nbrowser_right_click {"sessionId":"demo","selector":".ctx","cursor":true,"intent":"Right-clicking the box"} ``` -## Recipe — window state: maximize / fullscreen / un-fullscreen / move / resize-Native `chrome.windows` — **no debug bar**. (A synthetic **F11 does NOT work** — the browser+## Recipe - window state: maximize / fullscreen / un-fullscreen / move / resize+Native `chrome.windows` - **no debug bar**. (A synthetic **F11 does NOT work** - the browser intercepts browser-chrome shortcuts before the page sees the CDP key event; use this verb instead.) ```bash nbrowser_window_state {"sessionId":"demo","state":"maximized"}    # what users usually want@@ -65,7 +65,7 @@ nbrowser_window_state {"sessionId":"demo","left":60,"top":50,"width":1050,"heigh # alias: nbrowser_fullscreen {"sessionId":"demo"} ``` -## Recipe — two browser windows tiled SIDE-BY-SIDE+## Recipe - two browser windows tiled SIDE-BY-SIDE ```bash # read the work area (chrome's own coordinate units, DPI-correct) from a maximized window: nbrowser_window_state {"sessionId":"demo","state":"maximized"}@@ -79,7 +79,7 @@ To shotlog the side-by-side, prefer a montage of two `desktop_screenshot_window (reliable) over `desktop_screenshot_screen` (white-frame prone). Find each hwnd with `desktop_find_window {"className":"Chrome_WidgetWin_1"}` and pick by `rect.left`. -## Recipe — open tabs in a window+## Recipe - open tabs in a window ```bash nbrowser_open_tab  {"sessionId":"demo","url":"https://www.mouser.com"} nbrowser_open_tab  {"sessionId":"demo","url":"https://www.ultralibrarian.com"}@@ -87,9 +87,9 @@ nbrowser_list_tabs {"sessionId":"demo"}                          # -> [{tabId,ur nbrowser_switch_tab {"sessionId":"demo","tabId":"<id>"}          # bring a tab forward ``` -## Recipe — taskbar status: Adom overlay badge, progress, flash alert+## Recipe - taskbar status: Adom overlay badge, progress, flash alert Paints on the real browser window's **taskbar button** (the hwnd is captured at control-start). The-window must be windowed/maximized (taskbar visible) — not immersive fullscreen.+window must be windowed/maximized (taskbar visible) - not immersive fullscreen. ```bash nbrowser_taskbar {"sessionId":"demo","progress":{"state":"normal","value":45},"overlay":{"badge":"adom","tooltip":"Adom is driving your browser"}}   # GREEN + Adom badge nbrowser_taskbar {"sessionId":"demo","progress":{"state":"paused","value":70}}      # YELLOW = human-gate (captcha/2FA/login)@@ -100,27 +100,27 @@ Verified to render: the **teal Adom badge sits on the Chrome taskbar button's co green/yellow **progress bar** runs along the button's bottom. To prove it in a frame, full-screen capture then crop+zoom the button (`desktop_screenshot_screen` → crop the bottom taskbar strip). -## Recipe — "am I logged in to <site>?" + drive logout/login+## Recipe - "am I logged in to <site>?" + drive logout/login ```bash nbrowser_login_state {"url":"https://www.mouser.com"} # -> {loggedIn:true, confidence:"high", authCookieNames:[ASP.NET_SessionId, …]} ``` - Detects a live session from the site's **auth/session cookies** (`chrome.cookies`).-- **Chrome does NOT expose saved passwords to extensions** (security) — you can't read the stored+- **Chrome does NOT expose saved passwords to extensions** (security) - you can't read the stored   credential. To **re-login after a logout**: drive the login form and let **Chrome autofill** the   saved credential, then submit (Adom never sees the secret). Or snapshot the auth cookies *before*   logout and restore them (only works if logout didn't invalidate them server-side). -## Recipe — the killer use case: fetch a chip's CAD (chip-fetcher) in the REAL browser+## Recipe - the killer use case: fetch a chip's CAD (chip-fetcher) in the REAL browser EE CAD sources (manufacturer → SnapMagic/SnapEDA → Mouser → DigiKey → Arrow → UltraLibrarian → LCSC-last) gate downloads behind **login + captcha**; `pup`'s empty profile hits the wall. The native browser uses the engineer's **real signed-in session**, so the STEP / KiCad / Altium / Fusion-bundles just download. `chip-fetcher` is the Rust CLI that walks that ladder — point it at the+bundles just download. `chip-fetcher` is the Rust CLI that walks that ladder - point it at the native browser instead of pup. Demo by navigating the real sites (`nbrowser_navigate https://www.mouser.com` → `…ultralibrarian.com`) and showing `login_state` = signed in where pup would be signed out. `chip-fetcher sources` prints the canonical ladder.  ## Screenshot/shotlog helper pattern (cloud-side)-`desktop_screenshot_screen` **auto-pulls** both PNGs to the cloud (`localFullPath` in the result) —+`desktop_screenshot_screen` **auto-pulls** both PNGs to the cloud (`localFullPath` in the result) - shotlog that path directly. `desktop_screenshot_window` returns base64 in `.output.data.image` (decode then shotlog). Resize busy frames ≤1400px for the model.
skills/driving-the-extension/SKILL.md+79−79
@@ -1,39 +1,39 @@ --- name: driving-the-extension-description: How to drive the Adom browser extension (nbrowser_* verbs) — control the user's REAL logged-in Chrome/Edge from the cloud. Covers the CDP-vs-native split (and the "debugging this browser" bar), the full verb surface, isTrusted input (click-by-text + xy-finding, type/press_key, hover, right-click), the gliding Adom cursor with intent narration, screenshots, the taskbar status feature (flash/progress/Adom-overlay), background-safe capture (never stealing the user's focus), captions, the chip-fetcher use case, and every pitfall learned building it.+description: How to drive the Adom browser extension (nbrowser_* verbs) - control the user's REAL logged-in Chrome/Edge from the cloud. Covers the CDP-vs-native split (and the "debugging this browser" bar), the full verb surface, isTrusted input (click-by-text + xy-finding, type/press_key, hover, right-click), the gliding Adom cursor with intent narration, screenshots, the taskbar status feature (flash/progress/Adom-overlay), background-safe capture (never stealing the user's focus), captions, the chip-fetcher use case, and every pitfall learned building it. ---  # Driving the Adom Browser Extension (`nbrowser_*`) -This extension turns the user's **real, logged-in Chrome/Edge** into something the agent drives — the+This extension turns the user's **real, logged-in Chrome/Edge** into something the agent drives - the native-browser counterpart to `pup`. Because it runs **inside the user's actual profile**, their cookies, SSO, saved logins and human-trust signals all apply, so it sails through login walls and captchas that block pup's empty controlled-Chromium. That is the whole point. -## ⛔ Golden rule: NEVER reuse a window the user is in — always open a fresh one+## ⛔ Golden rule: NEVER reuse a window the user is in - always open a fresh one  The user is working in their browser. **Never navigate, reload, or repurpose a window/tab they-already have open** — not with `nbrowser_navigate`, and **not with the OS-level `desktop_navigate`+already have open** - not with `nbrowser_navigate`, and **not with the OS-level `desktop_navigate` either.** That yanks their live page out from under them and is rightly seen as hostile. This applies-to **everything you do for your own needs** — driving a workflow, reading a doc, and especially+to **everything you do for your own needs** - driving a workflow, reading a doc, and especially **grabbing a screenshot** (e.g. of a `chrome://extensions` / `edge://extensions` page for a guide).  The rule, every time: **open a NEW window** (`nbrowser_open_window {url}`, or a fresh OS window you then `desktop_navigate`), do your work and capture in THAT window, and **close it when done.** The only thing you may do to one of their existing windows is `desktop_bring_to_front` to surface it for-them — never to drive or navigate it.+them - never to drive or navigate it. -## ⛔ Background by default — NEVER foreground unless the user asks+## ⛔ Background by default - NEVER foreground unless the user asks  The user is working in other windows. **Never raise a window to the foreground** for your own needs. The verbs are built for this: `nbrowser_open_window` opens **unfocused**, `open_tab` opens-`active:false`, `switch_window` does **not** raise — leave those defaults alone. Capture without+`active:false`, `switch_window` does **not** raise - leave those defaults alone. Capture without foregrounding: **`nbrowser_screenshot` (CDP) and `desktop_screenshot_window` (WGC) both capture a background/occluded window**, and WGC records one at full fps (with the occlusion flags). So there is never a reason to pull a window forward to grab a shot or a recording.  **Capturing a `chrome://` / `edge://` page** (e.g. `chrome://extensions` for a guide) is the classic-trap: do **NOT** launch the browser yourself — `msedge` / `chrome --new-window` (via `launch` /+trap: do **NOT** launch the browser yourself - `msedge` / `chrome --new-window` (via `launch` / `launch_process`) **raises the new window to the foreground.** Instead:  1. `nbrowser_open_window {url:"about:blank"}` → a fresh **background** window (note its `hwnd`).@@ -42,7 +42,7 @@ trap: do **NOT** launch the browser yourself — `msedge` / `chrome --new-window 3. `desktop_screenshot_window {hwnd}` → WGC captures it where it sits. 4. `nbrowser_close_window {sessionId}`. -Foregrounding is the **user's** choice — only `desktop_bring_to_front`, and only when they explicitly+Foregrounding is the **user's** choice - only `desktop_bring_to_front`, and only when they explicitly ask to watch live.  **Path:** `cloud → relay → adom-desktop (laptop) → native-browser bridge → native-messaging host →@@ -59,11 +59,11 @@ tab id. The session→window map persists to `chrome.storage`, so sessions survi  ## ⭐ The #1 thing to understand: CDP vs native (and the "debugging this browser" bar) The extension is a **CDP-primary hybrid** with two engines:-- **`chrome.debugger` (CDP)** — needed for *real* `isTrusted` input, full-page screenshots, and page+- **`chrome.debugger` (CDP)** - needed for *real* `isTrusted` input, full-page screenshots, and page   `eval`. **Attaching it is what makes Chrome show the yellow `"Adom for Chrome & Edge" started   debugging this browser` bar.** The bridge **lazy-attaches** only for CDP verbs and **idle-detaches   ~8s** later, so the bar appears during active clicking/typing and clears when idle.-- **Native `chrome.*` APIs** — windows/tabs/navigation/downloads. **No debugger → no bar.**+- **Native `chrome.*` APIs** - windows/tabs/navigation/downloads. **No debugger → no bar.**  | 🟡 Shows the bar (CDP) | ⚪ No bar (native) | |---|---|@@ -80,11 +80,11 @@ read/navigate, prefer native verbs and the bar never shows. - **meta:** `status`, `ping`, `help` (full inline reference), `dev_reload` (self-completing 2-pass; throttled 1/90s) - **windows/sessions:** `open_window`, `close_window`, `switch_window`, `list_windows` (returns   `bounds`+`state`), `window_state`/`fullscreen` (maximize·fullscreen·normal·minimize, or-  `left/top/width/height` to move+resize — native, no bar; F11-via-key does NOT work), `rescan`+  `left/top/width/height` to move+resize - native, no bar; F11-via-key does NOT work), `rescan` - **account / credentials:** `login_state {url}` (signed-in NOW? via `chrome.cookies`) ·-  `credentials {match?, browser?, profile?}` — a **read-only QUERY of the browser's OWN saved-login+  `credentials {match?, browser?, profile?}` - a **read-only QUERY of the browser's OWN saved-login   store** (we don't store or replace anything; Chrome/Edge stay the source of truth). Returns-  `{browser, profile, host, username}` rows across **Chrome AND Edge**, all profiles — i.e. which+  `{browser, profile, host, username}` rows across **Chrome AND Edge**, all profiles - i.e. which   vendors the user can auto-login to. Never passwords (the browser autofills those). The extension   can't read this (sandbox); the bridge reads each browser's `Login Data` SQLite natively. @@ -96,67 +96,67 @@ password:    library, the user has a saved login and Chrome will autofill it. (No match → ask the user to save it    once, or pick another rung of the sourcing ladder.) 2. **Drive the login:** `nbrowser_navigate` to the sign-in page → `nbrowser_click` the email/username-   field (Chrome **autofills** username+password — you never see the secret) → `nbrowser_click` "Sign+   field (Chrome **autofills** username+password - you never see the secret) → `nbrowser_click` "Sign    In"/"Log In" → `nbrowser_login_state` to confirm. Proven live on Mouser (`johnAdom`) + CSE. 3. **Signed in = no bot wall:** a real logged-in session also skips the anti-bot "security slider"    captchas that block pup's empty profile. That's the whole point of driving the real browser.-Mouser uses a **Username** field (not email) — check `input` by name, not just `type=email`.+Mouser uses a **Username** field (not email) - check `input` by name, not just `type=email`. - **tabs:** `open_tab`, `close_tab`, `switch_tab`, `list_tabs` - **navigation:** `navigate`, `reload`, `back`, `forward`, `wait{ms}` - **input (isTrusted, CDP):** `click`, `double_click`, `right_click`, `hover`, `type`, `press_key`,   `input_dispatch`, `cursor` - **capture/eval:** `screenshot`, `screenshot_full_res`, `eval`/`evaluate`-- **data (native advantage):** `fetch_url` (uses the user's REAL cookies — authed), `download`+- **data (native advantage):** `fetch_url` (uses the user's REAL cookies - authed), `download` - **status/errors:** `errors`, `events` (window/tab closes incl. `byUser:true`) - **demo/UX:** `caption`, `focus`, **`flash`/`flash_stop`/`flash_mode`/`taskbar`**-- **recording:** `record_start`/`stop`/`status`/`list` — ⭐ use `method:"wgc"` (delegates to the core AD verb `desktop_record_window_start`: background, 60fps, no picker, no banner, captures occluded). `getdisplaymedia`/`tabcapture` are a refused-by-default fallback (Chrome forces a manual share picker) — need `fallbackConfirmed:true`. See **native-browser-recording**.+- **recording:** `record_start`/`stop`/`status`/`list` - ⭐ use `method:"wgc"` (delegates to the core AD verb `desktop_record_window_start`: background, 60fps, no picker, no banner, captures occluded). `getdisplaymedia`/`tabcapture` are a refused-by-default fallback (Chrome forces a manual share picker) - need `fallbackConfirmed:true`. See **native-browser-recording**.  ## Parallel multi-browser / multi-profile control Every **BROWSER × PROFILE is its own independently-addressable connection**, keyed `browser:email` (e.g. `chrome:[email protected]`, `edge:[email protected]`, `chrome:[email protected]`). The same account signed into-Chrome AND Edge are DISTINCT connections that coexist — the host singleton AND the bridge both key by+Chrome AND Edge are DISTINCT connections that coexist - the host singleton AND the bridge both key by `browser:profile`, so the 2nd browser's host no longer exits with *"Native host has exited."*  - **List them:** `nbrowser_profiles` → each entry's `profile` field is its addressable key, plus   `browser`, `email`, `live`, `active`.-- **Target any of them, in PARALLEL:** pass `profile:"<key>"` on ANY verb —+- **Target any of them, in PARALLEL:** pass `profile:"<key>"` on ANY verb -   `nbrowser_navigate {profile:"edge:[email protected]", url:…}`. Also accepts `profile:"<email>"`+   `browser:"edge"`, or `browser:"edge"` alone (that browser's active profile). A wrong/absent target   no-routes cleanly (doesn't fall through to the wrong browser). - **Multiple AI threads at once:** separate Claude/Codex threads can each drive their own-  browser/profile simultaneously — every verb carries its own explicit target, so there's no shared+  browser/profile simultaneously - every verb carries its own explicit target, so there's no shared   default to clobber. `nbrowser_use_profile` only sets the convenience default for verbs that omit a   target; for parallel work, ALWAYS pass an explicit `profile`/`browser`.-- **Exclude one:** `nbrowser_profile_block {profile}` (persisted) — a security whitelist that keeps a+- **Exclude one:** `nbrowser_profile_block {profile}` (persisted) - a security whitelist that keeps a   browser/profile from ever being driven.  For a browser to appear, that profile needs an open window AND the Adom extension loaded in THAT browser (`chrome://extensions` / `edge://extensions` → Developer mode → Load unpacked → the extension folder). The hello carries the browser (Chrome vs Edge via the UA), so each connection self-keys. -## Input — click, type, hover, right-click, and how xy is found+## Input - click, type, hover, right-click, and how xy is found You don't guess pixels. The **smart finder** scores the DOM, takes the matched element's-`getBoundingClientRect`, and computes its center xy — then CDP dispatches the `isTrusted` event there.+`getBoundingClientRect`, and computes its center xy - then CDP dispatches the `isTrusted` event there. Targeting is **DOM-truth**, not pixel-guessing.  ### Why DPI / window-resize never breaks targeting (the question every AI asks) This is **not** screenshot→eyeball-a-pixel→click automation, so it does **not** suffer that class of bug: - **High-DPI / display scaling is handled by Chrome, not you.** `getBoundingClientRect` and CDP-  `Input.dispatchMouseEvent` are BOTH in **CSS pixels, viewport-relative** — the same space. The browser+  `Input.dispatchMouseEvent` are BOTH in **CSS pixels, viewport-relative** - the same space. The browser   maps CSS→physical internally. A 4K@200% screen and a 1080p screen give the identical xy for the same   button. There is **zero** DPI-translation code, hence zero DPI bugs. Never touch `devicePixelRatio`.-- **Screen resolution / window size / window position are irrelevant** — targeting is page-relative, not+- **Screen resolution / window size / window position are irrelevant** - targeting is page-relative, not   screen-relative. Drag Chrome to the other monitor mid-task; it doesn't matter. - **No stale-coordinate / "dirty flag" problem.** Measure and act are **one atomic verb, sub-second,-  with no AI loop between them** — the finder re-measures `getBoundingClientRect` fresh on *every* click/+  with no AI loop between them** - the finder re-measures `getBoundingClientRect` fresh on *every* click/   type/hover. A user resizing the window 100ms before your click is fine: the next call reads the live   rect. Nothing is cached across a gap. **So you can reassure the user that resizing/scrolling while Adom-  drives is SAFE** (a nice `nbrowser_caption`) — the opposite of the usual "don't touch anything" fear.+  drives is SAFE** (a nice `nbrowser_caption`) - the opposite of the usual "don't touch anything" fear. - **The only path that CAN go stale** is passing **raw `{x,y}`** to `input_dispatch` (a literal pixel,-  no re-resolve). Prefer `text`/`selector` — they re-resolve every call. AD is **not** involved in click+  no re-resolve). Prefer `text`/`selector` - they re-resolve every call. AD is **not** involved in click   xy at all (it only does OS-level foreground/taskbar/window-screenshots). ```bash-# click by VISIBLE TEXT (finder computes xy) — returns {x,y,matchedText}+# click by VISIBLE TEXT (finder computes xy) - returns {x,y,matchedText} adom-desktop -t AdomLapper nbrowser_click '{"sessionId":"s1","text":"Pay","cursor":true,"intent":"Clicking the Pay button"}' adom-desktop -t AdomLapper nbrowser_click '{"sessionId":"s1","selector":"#download"}'         # or by selector adom-desktop -t AdomLapper nbrowser_type  '{"sessionId":"s1","selector":"#search","text":"STM32F103C8T6"}'@@ -173,7 +173,7 @@ settle → ~600ms dwell (tune with `hoverDwellMs`). When in doubt for hover, **a  **The gliding Adom cursor:** pass `cursor:true` + `intent:"…"` on any input verb and the gorgeous Adom cursor (teal arrow + pulsing ring + "Adom" label) glides to the target **while narrating its intention**-("Clicking the Pay button", "Hovering the card to reveal the tooltip"). Builds user trust — they always+("Clicking the Pay button", "Hovering the card to reveal the tooltip"). Builds user trust - they always see what the AI is doing and why. Mimics Hydrogen Desktop's cursor.  ## Screenshots@@ -181,75 +181,75 @@ see what the AI is doing and why. Mimics Hydrogen Desktop's cursor. adom-desktop -t AdomLapper nbrowser_screenshot '{"sessionId":"s1","toFile":true}'   # writes a laptop PNG, returns its path # then: adom-desktop -t AdomLapper pull_file '{"filePaths":["<path>"],"saveTo":"/tmp"}' ```-Screenshots are large base64 — over the relay they can time out, so **prefer `toFile:true` + `pull_file`**+Screenshots are large base64 - over the relay they can time out, so **prefer `toFile:true` + `pull_file`** (binary, chunked). `screenshot_full_res` is unresized; default resizes ≤1568 for the model.  > **🚫 NEVER foreground the window to take a screenshot.** `nbrowser_screenshot` (CDP) captures the > page bitmap with **zero OS-window interaction**, and AD `desktop_screenshot_window` (WGC) captures a-> **background / occluded** window fine. The user is usually working *in* their real browser — yanking+> **background / occluded** window fine. The user is usually working *in* their real browser - yanking > it forward to grab a shot disrupts them. See [Do NOT foreground the window](#do-not-foreground-the-window-to-look-at-it--capture-in-place). -## Taskbar status — flash, progress, the "Adom is driving" overlay badge+## Taskbar status - flash, progress, the "Adom is driving" overlay badge Paint glanceable status on the **real browser window's Windows taskbar button** (the hwnd is captured at-`open_window`; an **Adom overlay badge** goes on automatically while driving — a trust tell). Driven via+`open_window`; an **Adom overlay badge** goes on automatically while driving - a trust tell). Driven via AD's `desktop_taskbar` (1.8.152+).-- `nbrowser_flash {sessionId}` / `nbrowser_flash_stop` — orange "come look" cue (auto-clears on focus).-- `nbrowser_flash_mode {mode}` — **`quiet`** (default; flash only on come-look moments: load/screenshot/+- `nbrowser_flash {sessionId}` / `nbrowser_flash_stop` - orange "come look" cue (auto-clears on focus).+- `nbrowser_flash_mode {mode}` - **`quiet`** (default; flash only on come-look moments: load/screenshot/   error/done) · **`chatty`** (flash every action, for users who want to watch) · **`off`**.-- `nbrowser_taskbar {progress:{state,value?}, overlay:{badge,tooltip?}}` — progress states:-  `normal`(green) · **`paused`(YELLOW — use for captcha/2FA/login human-gates)** · `error`(red) ·+- `nbrowser_taskbar {progress:{state,value?}, overlay:{badge,tooltip?}}` - progress states:+  `normal`(green) · **`paused`(YELLOW - use for captcha/2FA/login human-gates)** · `error`(red) ·   `indeterminate`(marquee) · `none`. Overlay `badge:"adom"|"none"`.  Play it like a status light: indeterminate green while working → yellow + flash on a human-gate → clear + one flash on done. **Respect the user's mode** (anxiety-driven users want `chatty`; trusting users want `quiet`). Never leave it strobing. -## Do NOT foreground the window to look at it — capture in place-**The user is usually working IN their real browser. Stealing its focus disrupts them — this has been+## Do NOT foreground the window to look at it - capture in place+**The user is usually working IN their real browser. Stealing its focus disrupts them - this has been called out directly ("that's disrupting my work massively").** The whole point of native-browser driving is that the user keeps working in *their* foreground window while Adom operates one of their *other* windows in the background. So: -- **Screenshots NEVER need foregrounding** — `nbrowser_screenshot` (CDP) captures a **background / occluded**+- **Screenshots NEVER need foregrounding** - `nbrowser_screenshot` (CDP) captures a **background / occluded**   window unconditionally (it reads the page bitmap, not the OS window). Capture where it sits; never raise it.-- **⭐ Recording in the BACKGROUND is the whole point — and it works, with one setup requirement.** The WGC+- **⭐ Recording in the BACKGROUND is the whole point - and it works, with one setup requirement.** The WGC   recorder (and `desktop_screenshot_window`'s WGC path) capture the OS window's composited surface. Chrome,   by default, **stops painting a fully-occluded window** (its occlusion detector), so WGC then records a   **blank** surface. The fix is a one-time **launch-flag** requirement on the Chrome being recorded:   `--disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-background-timer-throttling --disable-features=CalculateNativeWinOcclusion`.   **pup's Chrome already ships these** (which is why pup background-records reliably); a default user Chrome-  does **not** — so for the extension to record the user's real Chrome in the background, that Chrome must be+  does **not** - so for the extension to record the user's real Chrome in the background, that Chrome must be   launched with these flags (bake them into the Adom-managed Chrome launch / golden image). **Never** work-  around a blank background recording by foregrounding the window — that defeats the entire feature (the user+  around a blank background recording by foregrounding the window - that defeats the entire feature (the user   must be able to keep working while a demo records). Foregrounding is the **user's** choice, when *they* want-  to watch — never the AI's.+  to watch - never the AI's. - **`open_window` opens in the BACKGROUND** (focus is not stolen). Pass `focused:true` *only* when the   user explicitly asked to watch it live. - `nbrowser_focus` activates the right *tab* and flashes its taskbar button, but **by design does NOT-  raise the window** above other apps (Windows foreground-lock) — so it never yanks the user's focus.+  raise the window** above other apps (Windows foreground-lock) - so it never yanks the user's focus.   To say "come look" without forcing the window forward, use the taskbar **flash / overlay** above. - **Only** raise a window when the user EXPLICITLY asks to see it live ("show me", "let me watch"): ```bash adom-desktop -t AdomLapper desktop_bring_to_front '{"titleContains":"<page title>","state":"restore"}'  # or {hwnd} ``` -## 🎥 Recording doctrine — full-framerate background recording of the NATIVE browser+## 🎥 Recording doctrine - full-framerate background recording of the NATIVE browser -**Goal:** demos must look **amazing** → full-resolution, **high framerate (20–60 fps)**. **The enemy is+**Goal:** demos must look **amazing** → full-resolution, **high framerate (20-60 fps)**. **The enemy is PAINT-THROTTLING, not any codec.** Chrome correctly stops painting occluded / background / static windows (its battery-saving default), and screencast-style capture is event-driven, so a throttled page silently drops to ~5 fps ("frozen" / sped-up video). Every technique exists to keep frames flowing. *(The full-cross-surface reference — Hydrogen vs pup, the foreground-rebump trick, page-side animation, the keyframe-re-encode — lives in the **demo-recording** skill's `recording-mechanics.md`. CDP screencast is NOT garbage —-it's pup's `browser_record_start` engine and does 20–30 fps when the page paints.)*+cross-surface reference - Hydrogen vs pup, the foreground-rebump trick, page-side animation, the keyframe+re-encode - lives in the **demo-recording** skill's `recording-mechanics.md`. CDP screencast is NOT garbage -+it's pup's `browser_record_start` engine and does 20-30 fps when the page paints.)*  **When to use the NATIVE browser (this extension) to record:** only when the demo needs the **user's real session / cookies / logins** (a logged-in tool). If no login is needed, record via **Hydrogen or pup** (see demo-recording) and don't touch the user's real Chrome.  **Native-browser recording = `nbrowser_record_start {method:"wgc"|"getdisplaymedia"}`** (the native path-does NOT use CDP screencast — that's pup's). Both are real high-fps engines: **`wgc`** (60 fps, Windows,-picker-free) and **`getdisplaymedia`** (WebRTC/Meet pipeline, 30–60 fps; picker auto-skipped with+does NOT use CDP screencast - that's pup's). Both are real high-fps engines: **`wgc`** (60 fps, Windows,+picker-free) and **`getdisplaymedia`** (WebRTC/Meet pipeline, 30-60 fps; picker auto-skipped with `--auto-select-desktop-capture-source` when *we* launch Chrome).  **The key constraint that makes this different from pup:** you must **NOT foreground-rebump the user's real@@ -257,87 +257,87 @@ working browser** (pup does that on a throwaway window, but on the user's real C Instead, make the real Chrome **paint while occluded** by launching it with the occlusion/throttle-disable flags `--disable-features=CalculateNativeWinOcclusion --disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-background-timer-throttling`. Default Chrome lacks them (correctly-— battery). So for a logged-in background recording:+- battery). So for a logged-in background recording: 1. Check whether native Chrome already runs with the flags. 2. If not → **ASK + WARN the user we must fully restart their Chrome**, then **kill the entire `chrome.exe`-   process tree** (many helper/background processes — closing the window is NOT enough; the flags don't apply+   process tree** (many helper/background processes - closing the window is NOT enough; the flags don't apply    until every `chrome.exe` is gone), wait for full shutdown, and **relaunch with their real profile** (logins    survive; tabs restore) **plus the flags**. 3. Record full-framerate **in the background** while they keep working.-4. **Later OFFER TO REVERT** (battery): "Chrome's in full-background recording mode — back to normal?" → if+4. **Later OFFER TO REVERT** (battery): "Chrome's in full-background recording mode - back to normal?" → if    yes, fully restart Chrome again **without** the flags. Real session + amazing recordings + normal battery.  **Hard requirement under both transitions:** a robust **full-restart of native Chrome** (kill the whole tree,-confirm down, relaunch with profile + flags — and again to revert).+confirm down, relaunch with profile + flags - and again to revert).  **⛔ NEVER:** foreground the user's *real* Chrome to fix framerate (use the flags) · leave the flags on globally/permanently (battery drain) · upload a raw recording to the wiki without the `-g 30 -keyint_min 30` keyframe re-encode (see recording-mechanics.md).  ## Captions (two systems)-- **In-page** `nbrowser_caption {text, style, position}` — `style:"subtitle"` (calm white-on-dark, the+- **In-page** `nbrowser_caption {text, style, position}` - `style:"subtitle"` (calm white-on-dark, the   HD/web-hydrogen look) or `"alert"` (red **pulsing** banner). Tied to a page (cleared on navigation).-- **Fullscreen** AD `desktop_caption {text, position:"bottom", id}` — Win32 always-on-top over every app,-  truly unmissable; auto-expires. (`position:"bottom"` — top covers the URL bar + the debugger banner.)+- **Fullscreen** AD `desktop_caption {text, position:"bottom", id}` - Win32 always-on-top over every app,+  truly unmissable; auto-expires. (`position:"bottom"` - top covers the URL bar + the debugger banner.)  ## ⭐ The killer use case: chip-fetcher through the native browser EE CAD sources (**SnapEDA/SnapMagic, Ultralibrarian, Mouser, DigiKey**) gate the actual download behind a **login** and often a **captcha**. `pup`'s empty controlled-Chromium hits the wall. The native browser uses the engineer's **real logged-in session**, so the STEP / KiCad symbol+footprint / Altium / Fusion `.lbr` bundles just download. Drive the chip-fetcher sourcing ladder (manufacturer-first → SnapMagic →-Mouser → DigiKey → … → LCSC last) with `nbrowser_*` instead of `browser_*` — same flow, real session, no+Mouser → DigiKey → … → LCSC last) with `nbrowser_*` instead of `browser_*` - same flow, real session, no wall. This is what changes the EE workflow.  ## Pitfalls + best practices - **Confirm connected first** (`nbrowser_status`). After `dev_reload` OR an **AD restart**, the native-  host re-launches and re-dials the bridge — expect a reconnect delay (see RECONNECT below).+  host re-launches and re-dials the bridge - expect a reconnect delay (see RECONNECT below). - **The extension dies if its profile has NO open windows** (MV3 SW sleeps) → `"not connected"`. Open a   window in that profile; never close your LAST window there.-- **Always clean up windows you open** (`nbrowser_close_window`) — abandoned windows annoy the user, and+- **Always clean up windows you open** (`nbrowser_close_window`) - abandoned windows annoy the user, and   closing clears that window's taskbar status. - **Poll `nbrowser_events`** to learn if the USER closed your windows/tabs (`window_closed{byUser:true}`).-- **`nbrowser_eval` runs in the PAGE world** — `chrome.*` is NOT available there (only DOM/window).-- **`fetch_url` is the authed shortcut** — real cookies; grabs a file behind a login without clicking.-- **Run `nbrowser_help`** anytime — the bridge self-documents every verb + the flash/taskbar playbook.+- **`nbrowser_eval` runs in the PAGE world** - `chrome.*` is NOT available there (only DOM/window).+- **`fetch_url` is the authed shortcut** - real cookies; grabs a file behind a login without clicking.+- **Run `nbrowser_help`** anytime - the bridge self-documents every verb + the flash/taskbar playbook. - **Driving AD itself:** prefer the first-class file verbs (`read_file`/`write_file`/`list_dir`/-  `delete_file`, `process_list`, `net_stats`, `registry_*`) over `shell_execute` — cleaner, no quoting.-  (AD 1.8.165 fixed `shell_execute`'s quote-mangling, so quoted Windows paths with spaces now work too —+  `delete_file`, `process_list`, `net_stats`, `registry_*`) over `shell_execute` - cleaner, no quoting.+  (AD 1.8.165 fixed `shell_execute`'s quote-mangling, so quoted Windows paths with spaces now work too -   the old `ADOMDE~1` 8.3-short-name workaround is no longer needed.)  ## RECONNECT (the one operational gotcha) Each bridge restart (deploying new bridge code) or AD restart drops the extension; the native host then-re-dials and reconnects (currently 30–60s — being fixed AD-side with a stable host port). If+re-dials and reconnects (currently 30-60s - being fixed AD-side with a stable host port). If `nbrowser_status` says "not connected" but you didn't change anything, AD probably restarted to deploy a-fix — wait, then retry. If it persists, check `process_list` for **stray bridge instances** (kill all but+fix - wait, then retry. If it persists, check `process_list` for **stray bridge instances** (kill all but one) and that `bridge_list` runtimePort matches the live bridge's port.  ## Deploying a code change to the laptop-The `extension/` and `bridge/` here are **source** — they run from the *laptop*, so an edit takes effect+The `extension/` and `bridge/` here are **source** - they run from the *laptop*, so an edit takes effect only after you push it there and reload:  - **Extension** (`src/*.js`, `manifest.json`) loads unpacked from   `C:\Users\john\adom-browser-extension\extension`:   1. `write_file` each changed file there (real quoted Windows paths work as of AD 1.8.165).-  2. `nbrowser_dev_reload` — **self-completing 2-pass**: it does a soft reload, then arms a one-shot flag+  2. `nbrowser_dev_reload` - **self-completing 2-pass**: it does a soft reload, then arms a one-shot flag      so the SW does a second *hard* reload on restart that re-reads disk. A single call now reliably picks      up your changes (older builds needed two manual calls because the soft reload served cached files).      Throttled 1/90s. Verified: a fresh version loads in ~7s under clean conditions. **Gotcha:** if you-     ALSO restart the bridge in the same beat, the SW's reconnect churn can delay the 2nd pass — wait+     ALSO restart the bridge in the same beat, the SW's reconnect churn can delay the 2nd pass - wait      ~10s and don't read `nbrowser_status` until it settles (the version eventually lands; it's not stuck).   3. Verify: `nbrowser_status` → `version` should reflect your `manifest.json` bump (bump it so you can tell).-- **Bridge** (`server.js`, `bridge.json`) runs from `…\bridges-cache\native-browser\` — the **single**+- **Bridge** (`server.js`, `bridge.json`) runs from `…\bridges-cache\native-browser\` - the **single**   runtime (`bridge_list` shows each bridge's `entrypoint`/`source`). `write_file` the changes there, then   restart it; if it's a wiki-managed (`source:cache`) install, keep `BRIDGE_VERSION`/`.bridge-version` in   sync with the deployed code so a future re-stream stays a no-op.  ## Managing the extension's lifecycle / driving chrome:// pages → see **driving-the-desktop** CDP/`nbrowser_*` **cannot** reach `chrome://`/`edge://` pages, native OS dialogs, browser chrome, or an-extension that **isn't loaded yet** — so the extension's own **install ("Load unpacked") / uninstall+extension that **isn't loaded yet** - so the extension's own **install ("Load unpacked") / uninstall ("Remove") / reload**, plus anything on `edge://extensions`, is driven through AD's `desktop_*` verbs-instead. That whole playbook — the **zero-math image-space click** path (`coordMap` + `space:"image"`),+instead. That whole playbook - the **zero-math image-space click** path (`coordMap` + `space:"image"`), **background UI Automation** (`desktop_find_control` → `desktop_ui_click`/`desktop_ui_set`, no foreground), **full-screen capture that SEES popup bubbles**, modifier chords, the ghost-cursor TTL, and the-proven uninstall→reinstall recipe (validated on John's Edge 2026-06-23, AD 1.8.175) — lives in the+proven uninstall→reinstall recipe (validated on John's Edge 2026-06-23, AD 1.8.175) - lives in the **[driving-the-desktop](../driving-the-desktop/SKILL.md)** sub-skill. Reach for it whenever the user wants to install/uninstall/reload the extension or you need to touch a surface CDP can't. 
skills/native-browser-recording/SKILL.md+16−16
@@ -1,16 +1,16 @@ --- name: native-browser-recording-description: How to record full-resolution, high-framerate demo videos of a browser window in the BACKGROUND so the user keeps working. THE BEST PATH is the core adom-desktop verb desktop_record_window_start (WGC) — or nbrowser_record_start {method:"wgc"}, which delegates to it: no source picker, no banner, captures occluded windows, no focus theft. The extension's own getDisplayMedia/tabcapture recording is a LAST-RESORT FALLBACK (Chrome forces a manual share-source picker a human must click) and is refused unless you pass fallbackConfirmed:true. Use the native browser ONLY when a demo needs the user's real session/cookies/logins; otherwise record via pup or Hydrogen (see the demo-recording skill). Covers WGC vs the heinous getDisplayMedia picker, the paint-throttling problem, the occlusion/throttle-disable launch flags, the restart-native-Chrome-into-background-recording-mode flow (ask + warn + full process-tree kill + relaunch with profile + flags), and the offer-to-revert-for-battery step. Match on: record a browser window, record the native browser, record a logged-in demo, background recording of Chrome, nbrowser_record, desktop_record_window_start, WGC, source picker, share picker, full-framerate native recording. Part of the adom-browser-extension.+description: How to record full-resolution, high-framerate demo videos of a browser window in the BACKGROUND so the user keeps working. THE BEST PATH is the core adom-desktop verb desktop_record_window_start (WGC) - or nbrowser_record_start {method:"wgc"}, which delegates to it: no source picker, no banner, captures occluded windows, no focus theft. The extension's own getDisplayMedia/tabcapture recording is a LAST-RESORT FALLBACK (Chrome forces a manual share-source picker a human must click) and is refused unless you pass fallbackConfirmed:true. Use the native browser ONLY when a demo needs the user's real session/cookies/logins; otherwise record via pup or Hydrogen (see the demo-recording skill). Covers WGC vs the heinous getDisplayMedia picker, the paint-throttling problem, the occlusion/throttle-disable launch flags, the restart-native-Chrome-into-background-recording-mode flow (ask + warn + full process-tree kill + relaunch with profile + flags), and the offer-to-revert-for-battery step. Match on: record a browser window, record the native browser, record a logged-in demo, background recording of Chrome, nbrowser_record, desktop_record_window_start, WGC, source picker, share picker, full-framerate native recording. Part of the adom-browser-extension. --- Part of **adom-browser-extension** (see **driving-the-extension** for the verb surface). The broad demo pipeline (Hydrogen vs pup, scripting, TTS, storyboard, mux, wiki upload) lives in the **demo-recording**-skill — this page is ONLY the part that's unique to recording the user's **real, logged-in native browser**.+skill - this page is ONLY the part that's unique to recording the user's **real, logged-in native browser**.  ## When to use the native browser to record (and when NOT to)-- **Use it ONLY when the demo needs the user's real session** — cookies, SSO, logins (e.g. a tool gated+- **Use it ONLY when the demo needs the user's real session** - cookies, SSO, logins (e.g. a tool gated   behind their account). Only the user's real Chrome has those. - **Otherwise, don't.** If the app is reachable without the user's login, record via **pup** or **Hydrogen**-  (demo-recording skill) — those are throwaway/managed surfaces you can drive freely without disturbing the+  (demo-recording skill) - those are throwaway/managed surfaces you can drive freely without disturbing the   user's real browser.  ## ⭐ FIRST: record with WGC (the core adom-desktop window recorder), NOT the extension@@ -28,7 +28,7 @@ captures the window **even when it is occluded**, and **never steals the user's the WGC hint). Why they're bad, and why being a "full browser extension" does NOT save you: - **Chrome forbids silent screen capture.** Even a full extension cannot record without `getDisplayMedia`   popping a **share-source PICKER a human must click** to choose the window/screen. You do **not** get to-  override that — it is a hard browser-security gate. (This is the exact trap to avoid: an AI that blindly+  override that - it is a hard browser-security gate. (This is the exact trap to avoid: an AI that blindly   calls record_start lands on a picker that just sits there waiting for a human.) - It paints a **persistent recording banner**, and it does **not** reliably capture a **background/occluded**   window. WGC has none of these problems because it captures at the OS level, below the browser.@@ -45,47 +45,47 @@ fix is the occlusion/throttle-disable launch flags below, NOT foregrounding the  ## Why the native path is different from pup: you must NOT foreground the user's real Chrome pup beats paint-throttling with the **foreground-rebump** trick (raise the throwaway window before each-action). **You must NOT do that to the user's real working browser** — it wrecks their flow. Instead, make+action). **You must NOT do that to the user's real working browser** - it wrecks their flow. Instead, make the real Chrome **keep painting while occluded** by launching it with the occlusion/throttle-disable flags: ``` --disable-features=CalculateNativeWinOcclusion --disable-backgrounding-occluded-windows --disable-renderer-backgrounding --disable-background-timer-throttling ```-A default Chrome does **not** have these (correctly — they cost battery: every occluded window keeps+A default Chrome does **not** have these (correctly - they cost battery: every occluded window keeps rendering). So enabling them is a **temporary, opt-in mode** we enter for recording and **leave afterward**. -## The flow — record a logged-in demo in the background+## The flow - record a logged-in demo in the background 1. **Confirm you actually need the real session.** If not, go use pup/Hydrogen.-2. **Check whether native Chrome already runs with the flags** — read its launch command line+2. **Check whether native Chrome already runs with the flags** - read its launch command line    (`adom-desktop process_list {nameContains:"chrome"}` → the main `chrome.exe` with no `--type=`). pup's    "Chrome for Testing" already has them; the user's real Chrome usually does not. 3. **If the flags are missing → ASK + WARN, then fully restart Chrome into background-recording mode:**    - Tell the user plainly: *"To record at full framerate while you keep working, I need to fully restart      your Chrome once. Your tabs and logins come back."*-   - **Kill the ENTIRE `chrome.exe` process tree** — native Chrome spawns many helper/background processes+   - **Kill the ENTIRE `chrome.exe` process tree** - native Chrome spawns many helper/background processes      (GPU, utility, crashpad, the `--no-startup-window` background-mode launcher); closing the window is NOT      enough and the flags won't apply until *every* `chrome.exe` is gone. Kill them all, confirm shutdown.    - **Relaunch with their real profile + the flags** (default `--user-data-dir`/`--profile-directory`, so      **logins survive** and tabs return via session-restore), e.g. via `launch_process`.    - Wait for the extension to reconnect (`nbrowser_status`).-4. **Record in the background** — `nbrowser_record_start {method:"wgc", hwnd:<window>}` (or `getdisplaymedia`+4. **Record in the background** - `nbrowser_record_start {method:"wgc", hwnd:<window>}` (or `getdisplaymedia`    with `--auto-select-desktop-capture-source`). Drive the app (CDP, background) + `nbrowser_caption`. The    occluded window now paints, so the capture is full-framerate and not blank. **Never foreground it.**-5. **Stop + fetch** — `nbrowser_record_stop {method:"wgc", recordingId}` (wgc is bridge-side; the stop needs+5. **Stop + fetch** - `nbrowser_record_stop {method:"wgc", recordingId}` (wgc is bridge-side; the stop needs    the `recordingId`, not just a sessionId). The mp4 lands on the laptop under `%TEMP%\adom-desktop-recordings`;    pull it with `pull_file {filePaths:["C:\\...\\window-….mp4"]}` (it saves into the container `/tmp`). 6. **OFFER TO REVERT (battery):** when the recording session is done, ask *"Chrome's in full-background-   recording mode, which uses more battery — want me to put it back to normal?"* → if yes, **fully restart+   recording mode, which uses more battery - want me to put it back to normal?"* → if yes, **fully restart    Chrome again WITHOUT the flags**. Best of all worlds: real session + amazing recordings + normal battery.  ## The hard requirement under both transitions A **robust full-restart of native Chrome**: enumerate + terminate the whole `chrome.exe` process tree, confirm it's down, relaunch with the real profile (+ flags to enter, or without to revert). This is the load-bearing-primitive — get it solid and the rest is easy.+primitive - get it solid and the rest is easy.  ## ⛔ Never-- Foreground the user's **real** Chrome to "fix" framerate or a blank recording — use the flags instead.-- Leave the flags on **globally/permanently** — it drains laptop battery. Enter the mode, record, offer to revert.+- Foreground the user's **real** Chrome to "fix" framerate or a blank recording - use the flags instead.+- Leave the flags on **globally/permanently** - it drains laptop battery. Enter the mode, record, offer to revert. - Upload a raw recording to the wiki without the `-g 30 -keyint_min 30` keyframe re-encode (see demo-recording).  ## Hand-off