feat: Support external CDP WebSocket URL for connecting to existing browsers#396
feat: Support external CDP WebSocket URL for connecting to existing browsers#396warmshao wants to merge 2 commits intobrowser-use:mainfrom
Conversation
…rowsers This commit adds the ability to connect to an already-running local Chrome/Chromium instance via its CDP WebSocket endpoint, instead of spawning a new embedded WebContentsView for every session. ## Motivation When driving a browser that the user already has open (e.g. with extensions, cookies, or specific profiles loaded), it is more convenient to attach to that existing instance rather than launching a fresh one. This is especially useful for workflows that require a logged-in state or specific browser configuration. ## How to use 1. Open Chrome/Chromium and navigate to `chrome://inspect/#remote-debugging`. 2. Enable "Discover network targets" or start Chrome with `--remote-debugging-port=9222`. 3. Copy the browser-level WebSocket URL (e.g. `ws://127.0.0.1:9222/devtools/browser/<id>`). 4. Open **Settings → Browser** in the app. 5. Paste the URL into the **CDP WebSocket URL** field. 6. (Optional) Check **Always allow** to keep a single global daemon alive across all sessions, avoiding Chrome's "Allow remote debugging?" dialog on every follow-up. ## How it works - A long-running `daemon.js` process holds a single CDP WebSocket connection. - Agent scripts talk to the daemon over a local named pipe / Unix socket (`\.\pipe\browser-use-bh-global` on Windows, `/tmp/bh-global.sock` on Unix). - The daemon reuses the same WebSocket for every agent turn, so Chrome only prompts once (or never if already allowed). - If the WebSocket drops, the daemon attempts to reconnect automatically. - If reconnection fails, the daemon exits and the main process starts a fresh one on the next session. ## Files changed - **Adapter layer** (`claude-code/adapter.ts`, `codex/adapter.ts`, `runEngine.ts`, `types.ts`): Pass `cdpUrl` and `daemonSocket` through the engine spawn pipeline. - **Harness** (`helpers.js`, `daemon.js`, `harness.ts`): Add daemon IPC client, WebSocket reconnection logic, and stock-file bootstrap for `daemon.js`. - **Main process** (`index.ts`): Manage per-session and global daemon lifecycle, CDP health checks before reusing a daemon, and settings-aware cleanup. - **Settings UI** (`SettingsPane.tsx`, `cdpUrlIpc.ts`, `cdpUrlStore.ts`, `SettingsWindow.ts`): Add CDP URL input, test connection button, and "Always allow" toggle. - **Renderer** (`AgentPane.tsx`, `useSessionsQuery.ts`, `types.ts`): Surface `externalBrowser` flag in session lists. - **Shell/Logs** (`shell.ts`, `logsPill.ts`): Adjust logs overlay behavior when no embedded view is present. ## Notes - Chrome 144+ may return 404 on HTTP `/json/*` endpoints when remote debugging is toggled via the UI. The daemon reads `DevToolsActivePort` from the Chrome profile as a fallback. - On Windows, npm-installed CLI shims (`.cmd`, `.ps1`) are handled correctly via `cmd.exe /d /s /c` or `powershell.exe`.
There was a problem hiding this comment.
4 issues found across 17 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/hl/stock/helpers.js">
<violation number="1" location="app/src/main/hl/stock/helpers.js:214">
P2: `createContext` returns a daemon-backed context even when the daemon never becomes ready, instead of throwing after the retry loop.</violation>
</file>
<file name="app/src/main/settings/cdpUrlStore.ts">
<violation number="1" location="app/src/main/settings/cdpUrlStore.ts:75">
P2: Change notifications fire even when persistence fails, so runtime observers can react to an unsaved CDP setting.</violation>
</file>
<file name="app/src/main/hl/engines/claude-code/adapter.ts">
<violation number="1" location="app/src/main/hl/engines/claude-code/adapter.ts:139">
P2: Connection-mode env vars are added without clearing conflicting inherited vars, so stale BU_DAEMON_SOCKET / BU_CDP_WS / BU_TARGET_ID / BU_CDP_PORT values can force the helper into the wrong browser-connection mode.</violation>
<violation number="2" location="app/src/main/hl/engines/claude-code/adapter.ts:140">
P3: Prompt mentions the wrong env var for external CDP WebSocket mode; it should refer to `BU_CDP_WS`, not `BU_DAEMON_SOCKET`.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.
| await client.send('Runtime.evaluate', { expression: '1' }).catch(() => {}); | ||
| } | ||
| } | ||
| return { |
There was a problem hiding this comment.
P2: createContext returns a daemon-backed context even when the daemon never becomes ready, instead of throwing after the retry loop.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/hl/stock/helpers.js, line 214:
<comment>`createContext` returns a daemon-backed context even when the daemon never becomes ready, instead of throwing after the retry loop.</comment>
<file context>
@@ -102,19 +120,118 @@ class CdpSession {
+ await client.send('Runtime.evaluate', { expression: '1' }).catch(() => {});
+ }
+ }
+ return {
+ targetId: targetId || 'external',
+ port,
</file context>
| return { | |
| if (info.product == null) { | |
| throw new Error(`createContext: daemon socket ${daemonSocket} is unreachable or not ready`); | |
| } | |
| return { |
| @@ -0,0 +1,94 @@ | |||
| /** | |||
There was a problem hiding this comment.
P2: Change notifications fire even when persistence fails, so runtime observers can react to an unsaved CDP setting.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/settings/cdpUrlStore.ts, line 75:
<comment>Change notifications fire even when persistence fails, so runtime observers can react to an unsaved CDP setting.</comment>
<file context>
@@ -0,0 +1,94 @@
+ } catch (err) {
+ mainLogger.error('cdpUrl.set-failed', { error: (err as Error).message });
+ }
+ onChange?.(next);
+ return next;
+}
</file context>
| 'You are driving a specific Chromium browser view on this machine.', | ||
| `Your target is CDP target_id=${ctx.targetId} on port ${ctx.cdpPort} (env BU_TARGET_ID / BU_CDP_PORT).`, | ||
| ]; | ||
| if (ctx.cdpUrl) { |
There was a problem hiding this comment.
P2: Connection-mode env vars are added without clearing conflicting inherited vars, so stale BU_DAEMON_SOCKET / BU_CDP_WS / BU_TARGET_ID / BU_CDP_PORT values can force the helper into the wrong browser-connection mode.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/hl/engines/claude-code/adapter.ts, line 139:
<comment>Connection-mode env vars are added without clearing conflicting inherited vars, so stale BU_DAEMON_SOCKET / BU_CDP_WS / BU_TARGET_ID / BU_CDP_PORT values can force the helper into the wrong browser-connection mode.</comment>
<file context>
@@ -135,10 +135,16 @@ const claudeCodeAdapter: EngineAdapter = {
'You are driving a specific Chromium browser view on this machine.',
- `Your target is CDP target_id=${ctx.targetId} on port ${ctx.cdpPort} (env BU_TARGET_ID / BU_CDP_PORT).`,
+ ];
+ if (ctx.cdpUrl) {
+ lines.push(`You are connected to an external browser via CDP (env BU_DAEMON_SOCKET).`);
+ } else {
</file context>
| `Your target is CDP target_id=${ctx.targetId} on port ${ctx.cdpPort} (env BU_TARGET_ID / BU_CDP_PORT).`, | ||
| ]; | ||
| if (ctx.cdpUrl) { | ||
| lines.push(`You are connected to an external browser via CDP (env BU_DAEMON_SOCKET).`); |
There was a problem hiding this comment.
P3: Prompt mentions the wrong env var for external CDP WebSocket mode; it should refer to BU_CDP_WS, not BU_DAEMON_SOCKET.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/hl/engines/claude-code/adapter.ts, line 140:
<comment>Prompt mentions the wrong env var for external CDP WebSocket mode; it should refer to `BU_CDP_WS`, not `BU_DAEMON_SOCKET`.</comment>
<file context>
@@ -135,10 +135,16 @@ const claudeCodeAdapter: EngineAdapter = {
- `Your target is CDP target_id=${ctx.targetId} on port ${ctx.cdpPort} (env BU_TARGET_ID / BU_CDP_PORT).`,
+ ];
+ if (ctx.cdpUrl) {
+ lines.push(`You are connected to an external browser via CDP (env BU_DAEMON_SOCKET).`);
+ } else {
+ lines.push(`Your target is CDP target_id=${ctx.targetId} on port ${ctx.cdpPort} (env BU_TARGET_ID / BU_CDP_PORT).`);
</file context>
| lines.push(`You are connected to an external browser via CDP (env BU_DAEMON_SOCKET).`); | |
| + lines.push(`You are connected to an external browser via CDP WebSocket (env BU_CDP_WS).`); |
The new CdpUrlSection component calls settings.cdpUrl.get() on mount. The existing test mock did not include this API, causing a TypeError (\"Cannot read properties of undefined (reading 'get')\") and failing CI.
|
this is really cool, though I think the incentive behind the app is that this should be a complete replacement of that flow, because you can copy all auth cookies in this environment. But I think we should support the capability regardless. I will review later! |
This commit adds the ability to connect to an already-running local Chrome/Chromium instance via its CDP WebSocket endpoint, instead of spawning a new embedded WebContentsView for every session.
Motivation
When driving a browser that the user already has open (e.g. with extensions, cookies, or specific profiles loaded), it is more convenient to attach to that existing instance rather than launching a fresh one. This is especially useful for workflows that require a logged-in state or specific browser configuration.
How to use
chrome://inspect/#remote-debugging.--remote-debugging-port=9222.ws://127.0.0.1:9222).How it works
daemon.jsprocess holds a single CDP WebSocket connection.\.\pipe\browser-use-bh-globalon Windows,/tmp/bh-global.sockon Unix).Files changed
claude-code/adapter.ts,codex/adapter.ts,runEngine.ts,types.ts): PasscdpUrlanddaemonSocketthrough the engine spawn pipeline.helpers.js,daemon.js,harness.ts): Add daemon IPC client, WebSocket reconnection logic, and stock-file bootstrap fordaemon.js.index.ts): Manage per-session and global daemon lifecycle, CDP health checks before reusing a daemon, and settings-aware cleanup.SettingsPane.tsx,cdpUrlIpc.ts,cdpUrlStore.ts,SettingsWindow.ts): Add CDP URL input, test connection button, and "Always allow" toggle.AgentPane.tsx,useSessionsQuery.ts,types.ts): SurfaceexternalBrowserflag in session lists.shell.ts,logsPill.ts): Adjust logs overlay behavior when no embedded view is present.Notes
/json/*endpoints when remote debugging is toggled via the UI. The daemon readsDevToolsActivePortfrom the Chrome profile as a fallback..cmd,.ps1) are handled correctly viacmd.exe /d /s /corpowershell.exe.Summary
Test plan
npm run lintpassesnpm run typecheckpassesnpm run testpasses (unit + integration)src/main/src/sharedmodules)pytest+ruff check+mypypassnpm run devfor UI-facing changesScreenshots / recordings
Risk + rollback