← Commit history
Parallel browser-aware keying: connections keyed browser:profile so Chrome+Edge (+work/personal) coexist & are driven in parallel by separate threads (pkg 0.3.2, ext 0.0.5)
6 files changed
+60−11
bridge/server.js+29−7@@ -61,7 +61,15 @@ let lastVerb = null, lastActivityAt = 0; 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-function profileKey(p) { return (p && (p.email || p.id)) || 'unknown'; }+// 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+// 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) {+ const b = (p && p.browser) || 'chrome';+ const id = (p && (p.email || p.id)) || 'unknown';+ return `${b}:${id}`;+} function profileLabel(p) { return (p && (p.email || p.name || p.id)) || 'unknown'; } // Blocklist: profiles the user told us to NEVER drive. Persisted so it survives bridge respawns.@@ -77,9 +85,19 @@ function anyConnected() { return liveConns().length > 0; } // else the only/most-recent live one. Returns null if none drivable (or the target is blocklisted). function targetConn(args) { const want = args && (args.profile || args.profileEmail);+ const wantBrowser = args && args.browser; // optional: disambiguate the SAME email across browsers if (want) { if (isBlocked(String(want))) return { blocked: true, key: String(want) };- for (const [k, c] of conns) if ((k === want || c.profile.email === want) && c.sock && !c.sock.destroyed) return { conn: c, key: k };+ // `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;+ if ((k === want || c.profile.email === want) && (!wantBrowser || c.profile.browser === wantBrowser)) return { conn: c, key: k };+ }+ return null;+ }+ if (wantBrowser) { // browser-only target → that browser's active/first live profile+ for (const [k, c] of conns) if (c.profile.browser === wantBrowser && c.sock && !c.sock.destroyed && !isBlocked(k)) return { conn: c, key: k }; return null; } const live = liveConns();@@ -155,8 +173,12 @@ function queryCredentials({ browser, profile, match } = {}) { return out; } -function registerConn(sock, profile) {- const key = profileKey(profile);+function registerConn(sock, profile, browser) {+ // Normalize to an object + tag the browser so chrome:me and edge:me are DISTINCT coexisting conns.+ const p = (profile && typeof profile === 'object') ? { ...profile } : { email: String(profile || ''), id: String(profile || '') };+ p.browser = browser || p.browser || 'chrome';+ const key = profileKey(p);+ profile = p; // Re-key any provisional/old entry for THIS socket to the real profile. for (const [k, c] of [...conns]) if (c.sock === sock && k !== key) { conns.delete(k); if (activeProfile === k) activeProfile = null; } const prev = conns.get(key);@@ -354,8 +376,8 @@ 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 || '', 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 signed-in browser PROFILE the AI can drive. Default verbs target the ▶active one (or pass profile:"<email>" on any verb). Switch default with nbrowser_use_profile. Exclude one forever with nbrowser_profile_block. By default ALL non-blocked profiles are drivable.' }) });+ 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.' }) }); } if (verb === 'nbrowser_use_profile') { const want = String((args && (args.profile || args.email)) || '');@@ -564,7 +586,7 @@ const hostSrv = net.createServer((sock) => { if (!line.trim()) continue; let frame; try { frame = JSON.parse(line); } catch { continue; }- if (frame && frame.type === 'hello') { registerConn(sock, frame.profile); continue; }+ if (frame && frame.type === 'hello') { registerConn(sock, frame.profile, frame.browser); continue; } onHostFrame(frame); } });
extension/manifest.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 3, "name": "Adom for Chrome & Edge", "short_name": "Adom",- "version": "0.0.4",+ "version": "0.0.5", "description": "Drive your real, logged-in Chrome/Edge from the Adom ecosystem — the native-browser counterpart to pup.", "minimum_chrome_version": "116", "background": {
extension/src/sw.js+1−1@@ -88,7 +88,7 @@ async function sendHello() { ]); if (info && info.email) profile.email = info.email; } catch {}- try { if (port) { port.postMessage({ type: 'hello', profile, version: VERSION }); helloSent = true; } } catch {}+ try { if (port) { port.postMessage({ type: 'hello', profile, version: VERSION, browser: detectBrowser() }); helloSent = true; } } catch {} } function reply(id, ok, payload, error, hint) {
host/native-host.js+5−1@@ -117,7 +117,11 @@ function pumpStdin() { // the bridge registers the connection under this profile. if (msg && msg.type === 'hello') { clearTimeout(helloFallback);- acquireSingleton((msg.profile && (msg.profile.email || msg.profile.id)) || 'default');+ // Key the singleton by BROWSER + profile so the same account in Chrome AND Edge each get their+ // own host (instead of the 2nd browser's host exiting on a shared lock — the "Native host has+ // exited" collision). The bridge keys connections the same way, so all coexist in parallel.+ const _id = (msg.profile && (msg.profile.email || msg.profile.id)) || 'default';+ acquireSingleton(`${msg.browser || 'chrome'}:${_id}`); } sendToBridge(msg); }
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-browser-extension",- "version": "0.3.1",+ "version": "0.3.2", "type": "app", "title": "Adom Browser Extension (Chrome + Edge)", "description": "Drive your real, logged-in Chrome/Edge from the Adom ecosystem \u2014 the nbrowser_* native-browser bridge (counterpart to pup). Multi-profile driving, a read-only credential-library query over the browser's own saved logins (Chrome + Edge), and hands-free vendor login automation, so chip CAD / datasheets download past every login wall in your real signed-in session.",
skills/driving-the-extension/SKILL.md+23@@ -76,6 +76,29 @@ Mouser uses a **Username** field (not email) — check `input` by name, not just - **demo/UX:** `caption`, `focus`, **`flash`/`flash_stop`/`flash_mode`/`taskbar`** - **recording:** `record_start`/`stop`/`status`/`list` — `method: wgc|getdisplaymedia|tabcapture` +## 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+`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 —+ `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+ 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+ 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 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.