When we built $B connect, the plan was to connect to the user's real Chrome browser — the one with their cookies, sessions, extensions, and open tabs. No more cookie import. The design called for:
chromium.connectOverCDP(wsUrl) connecting to a running Chrome via CDP--remote-debugging-port=9222This is why chrome-launcher.ts existed (361 LOC of browser binary discovery, CDP port probing, and runtime detection) and why the method was called connectCDP().
Real Chrome silently blocks --load-extension when launched via Playwright's channel: 'chrome'. The extension wouldn't load. We needed the extension for the side panel (activity feed, refs, chat).
The implementation fell back to chromium.launchPersistentContext() with Playwright's bundled Chromium — which reliably loads extensions via --load-extension and --disable-extensions-except. But the naming stayed: connectCDP(), connectionMode: 'cdp', BROWSE_CDP_URL, chrome-launcher.ts.
The original vision (access user's real browser state) was never implemented. We launched a fresh browser every time — functionally identical to Playwright's Chromium, but with 361 lines of dead code and misleading names.
During a /office-hours design session, we traced the architecture and discovered:
connectCDP() doesn't use CDP — it calls launchPersistentContext()connectionMode: 'cdp' is misleading — it's just "headed mode"chrome-launcher.ts is dead code — its only import was in an unreachable attemptReconnect() methodpreExistingTabIds was designed for protecting real Chrome tabs we never connect to$B handoff (headless → headed) used a different API (launch() + newContext()) that couldn't load extensions, creating two different "headed" experiencesconnectCDP() → launchHeaded()connectionMode: 'cdp' → connectionMode: 'headed'BROWSE_CDP_URL → BROWSE_HEADEDchrome-launcher.ts (361 LOC)attemptReconnect() (dead method)preExistingTabIds (dead concept)reconnecting field (dead state)cdp-connect.test.ts (tests for deleted code)$B handoff now uses launchPersistentContext() + extension loading (same as $B connect)--chat flag$B connect (default): activity feed + refs only$B connect --chat: + experimental standalone chat agentBrowser States:
HEADLESS (default) ←→ HEADED ($B connect or $B handoff)
Playwright Playwright (same engine)
launch() launchPersistentContext()
invisible visible + extension + side panel
Sidebar (orthogonal add-on, headed only):
Activity tab — always on, shows live browse commands
Refs tab — always on, shows @ref overlays
Chat tab — opt-in via --chat, experimental standalone agent
Data Bridge (sidebar → workspace):
Sidebar writes to .context/sidebar-inbox/*.json
Workspace reads via $B inbox
Real Chrome blocks --load-extension when launched by Playwright. This is a Chrome security feature — extensions loaded via command-line args are restricted in Chromium-based browsers to prevent malicious extension injection.
Playwright's bundled Chromium doesn't have this restriction because it's designed for testing and automation. The ignoreDefaultArgs option lets us bypass Playwright's own extension-blocking flags.
If we ever want to access the user's real cookies/sessions, the path is:
$B cookie-import)Not reconnecting to real Chrome.