Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clean-dogs-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@browserbasehq/browse-cli": patch
---

feat(cli): add --connect flag for daemon mode with existing Chrome
55 changes: 39 additions & 16 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,11 @@ interface DaemonResponse {
// Default viewport matching Stagehand core
const DEFAULT_VIEWPORT = { width: 1288, height: 711 };

async function runDaemon(session: string, headless: boolean): Promise<void> {
async function runDaemon(
session: string,
headless: boolean,
connectUrl?: string,
): Promise<void> {
// Only clean daemon state files (socket, pid, etc.), not client-written config (context)
await cleanupDaemonStateFiles(session);

Expand Down Expand Up @@ -356,12 +360,18 @@ async function runDaemon(session: string, headless: boolean): Promise<void> {
}
: {}),
}
: {
localBrowserLaunchOptions: {
headless,
viewport: DEFAULT_VIEWPORT,
},
}),
: connectUrl
? {
localBrowserLaunchOptions: {
cdpUrl: connectUrl,
},
}
: {
localBrowserLaunchOptions: {
headless,
viewport: DEFAULT_VIEWPORT,
},
}),
});

// Persist mode so status command can report it
Expand Down Expand Up @@ -1288,6 +1298,7 @@ async function sendCommand(
command: string,
args: unknown[],
headless: boolean = false,
connectUrl?: string,
): Promise<unknown> {
const maxRetries = 3;

Expand Down Expand Up @@ -1318,14 +1329,14 @@ async function sendCommand(

// Attempt 1: Try to restart daemon without cleanup
if (attempt === 1) {
await ensureDaemon(session, headless);
await ensureDaemon(session, headless, connectUrl);
continue;
}

// Final attempt: Full cleanup and restart
await killChromeProcesses(session);
if (!connectUrl) await killChromeProcesses(session);
await cleanupStaleFiles(session);
await ensureDaemon(session, headless);
await ensureDaemon(session, headless, connectUrl);
}
}

Expand All @@ -1344,7 +1355,11 @@ async function stopDaemonAndCleanup(session: string): Promise<void> {
await cleanupStaleFiles(session);
}

async function ensureDaemon(session: string, headless: boolean): Promise<void> {
async function ensureDaemon(
session: string,
headless: boolean,
connectUrl?: string,
): Promise<void> {
const wantMode = await getDesiredMode(session);
assertModeSupported(wantMode);

Expand Down Expand Up @@ -1373,7 +1388,9 @@ async function ensureDaemon(session: string, headless: boolean): Promise<void> {
await stopDaemonAndCleanup(session);
}

const args = ["--session", session, "daemon"];
const args = ["--session", session];
if (connectUrl) args.push("--connect", connectUrl);
args.push("daemon");
if (headless) args.push("--headless");

const child = spawn(process.argv[0], [process.argv[1], ...args], {
Expand Down Expand Up @@ -1432,6 +1449,7 @@ async function ensureDaemon(session: string, headless: boolean): Promise<void> {

interface GlobalOpts {
ws?: string;
connect?: string;
headless?: boolean;
headed?: boolean;
json?: boolean;
Expand Down Expand Up @@ -1460,7 +1478,7 @@ async function runCommand(command: string, args: unknown[]): Promise<unknown> {
const opts = program.opts<GlobalOpts>();
const session = getSession(opts);
const headless = isHeadless(opts);
// If --ws provided, bypass daemon and connect directly
// If --ws provided, bypass daemon and connect directly (stateless, no ref caching)
if (opts.ws) {
const stagehand = new Stagehand({
env: "LOCAL",
Expand All @@ -1478,8 +1496,9 @@ async function runCommand(command: string, args: unknown[]): Promise<unknown> {
}
}

await ensureDaemon(session, headless);
return sendCommand(session, command, args, headless);
// --connect uses daemon mode (persistent refs) but attaches to existing Chrome
await ensureDaemon(session, headless, opts.connect);
return sendCommand(session, command, args, headless, opts.connect);
}

program
Expand All @@ -1490,6 +1509,10 @@ program
"--ws <url>",
"CDP WebSocket URL (bypasses daemon, direct connection)",
)
.option(
"--connect <url>",
"CDP WebSocket URL for daemon to attach to (persistent refs, no Chrome launch)",
)
.option("--headless", "Run Chrome in headless mode")
.option("--headed", "Run Chrome with visible window (default)")
.option("--json", "Output as JSON", false)
Expand Down Expand Up @@ -1644,7 +1667,7 @@ program
.description("Run as daemon (internal use)")
.action(async () => {
const opts = program.opts<GlobalOpts>();
await runDaemon(getSession(opts), isHeadless(opts));
await runDaemon(getSession(opts), isHeadless(opts), opts.connect);
});

// ==================== NAVIGATION ====================
Expand Down
16 changes: 16 additions & 0 deletions packages/core/lib/v3/understudy/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,22 @@ export class V3Context {
});
}

// Close unnavigable chrome:// tabs so the new about:blank tab becomes
// the only visible tab. Without this, --connect mode leaves the original
// chrome://newtab tab visible while the new page hides behind it.
try {
const targets = await this.conn.getTargets();
for (const t of targets) {
if (t.type === "page" && t.url?.startsWith("chrome://")) {
Copy link
Member

@pirate pirate Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (t.type === "page" && t.url?.startsWith("chrome://")) {
if (t.type === "page" && t.url?.startsWith("chrome://")) {
// TODO: consider expanding this list if needed, see chrome://chrome-urls
// many things can cause weird unusable targets to appear at launch.
// e.g. chrome extensions, new feature info pages, chrome error pages

await this.conn
.send("Target.closeTarget", { targetId: t.targetId })
.catch(() => {});
}
}
} catch {
// best effort
}

await this.newPage("about:blank");
}

Expand Down
Loading