feat(cli): add --connect flag for daemon mode with existing Chrome#1830
feat(cli): add --connect flag for daemon mode with existing Chrome#1830peytoncasper wants to merge 4 commits intobrowserbase:mainfrom
Conversation
Adds a --connect <url> option that tells the browse daemon to attach to an existing Chrome instance via CDP WebSocket URL instead of launching its own Chrome. The daemon persists between commands (refs from snapshot are cached), but Chrome lifecycle is managed externally. Use case: remote node management where Chrome is launched with custom flags (anti-detection, profiles, specific ports) and the browse CLI needs to interact with it while preserving accessibility tree refs. --ws (stateless, no ref cache) remains unchanged. --connect (daemon mode, persistent refs, external Chrome) is new. Made-with: Cursor
🦋 Changeset detectedLatest commit: dbdd604 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
This PR is from an external contributor and must be approved by a stagehand team member with write access before CI can run. |
|
@peytoncasper changeset for release |
Greptile SummaryAdds a
Confidence Score: 3/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["browse --connect <url> <command>"] --> B{"opts.ws set?"}
B -->|Yes| C["Direct stateless connection\n(bypasses daemon)"]
B -->|No| D["ensureDaemon(session, headless, connectUrl)"]
D --> E{"Daemon running\nfor session?"}
E -->|Yes| F{"Mode matches?"}
F -->|Yes| G["Reuse existing daemon\n⚠️ connectUrl not checked"]
F -->|No| H["stopDaemonAndCleanup → restart"]
E -->|No| I["Spawn daemon with\n--connect <url> flag"]
I --> J["runDaemon(session, headless, connectUrl)"]
J --> K["ensureBrowserInitialized"]
K --> L{"connectUrl set?"}
L -->|Yes| M["Stagehand with\ncdpUrl: connectUrl\n(no Chrome launch)"]
L -->|No| N["Stagehand with\nheadless + viewport\n(launches Chrome)"]
G --> O["sendCommand via Unix socket"]
H --> O
M --> O
N --> O
|
There was a problem hiding this comment.
No issues found across 1 file
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Architecture diagram
sequenceDiagram
participant User
participant CLI as CLI Process
participant FS as Session Files (PID/Socket)
participant Daemon as Daemon Process (Background)
participant Core as Stagehand Core
participant Chrome as Chrome (CDP Host)
User->>CLI: browse --connect <url> <command>
CLI->>FS: Check for existing daemon socket
alt Daemon NOT running
CLI->>Daemon: NEW: Spawn daemon with --connect <url>
Daemon->>Core: NEW: Initialize with cdpUrl (No local launch)
Core->>Chrome: Connect to existing CDP WebSocket
Daemon-->>FS: Write socket/PID
end
CLI->>Daemon: sendCommand(session, command, args)
alt Standard execution
Daemon->>Core: Execute command
Core->>Chrome: CDP Interactions
Chrome-->>Core: Result + Accessibility Tree
Core->>Core: Cache Accessibility Refs
Daemon-->>CLI: Command Response
else CHANGED: Command failure / Retry
CLI->>Daemon: Attempt restart
opt NEW: if --connect is active
CLI->>CLI: Skip killChromeProcesses (External)
end
CLI->>FS: cleanupStaleFiles()
CLI->>Daemon: ensureDaemon(session, connectUrl)
end
CLI-->>User: Display Result
Note over Daemon,Chrome: Daemon & CDP Connection persist after CLI exits
User->>CLI: browse --connect <url> click @0-5
CLI->>Daemon: sendCommand("click", ["@0-5"])
Note over Daemon,Core: Uses cached refs from previous snapshot
Daemon->>Core: Resolve @0-5 to CDP node
Core->>Chrome: Dispatch Click
Daemon-->>CLI: Success
CLI-->>User: Clicked element
Since this is your first cubic review, here's how it works:
- cubic automatically reviews your code and comments on bugs and improvements
- Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
- Add one-off context when rerunning by tagging
@cubic-dev-aiwith guidance or docs links (includingllms.txt) - Ask questions if you need clarification on any suggestion
Made-with: Cursor
|
The latest approval by @miguelg719 could not refresh the mirrored PR automatically (missing-previous-source). The external PR stays open, and the mirrored PR should be updated manually before work continues. |
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
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=".changeset/clean-dogs-chew.md">
<violation number="1" location=".changeset/clean-dogs-chew.md:2">
P2: This changeset uses `patch` but the change introduces a new CLI feature (`--connect`). Per semver, new backward-compatible features should be a `minor` bump so users tracking versions can discover new capabilities.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
When connecting to an existing Chrome via CDP, the default chrome://newtab tab is not injectable by Stagehand (filtered by hasInjectableDOM). The ensureFirstTopLevelPage fallback creates a new about:blank tab, but the chrome://newtab tab remains visible in front of it. Fix: close any chrome:// tabs before creating the about:blank tab so the new tab becomes the only visible one. Combined with the sidecar's pre-navigation of chrome://newtab to about:blank, this ensures the user always sees the automated page. Made-with: Cursor
| try { | ||
| const targets = await this.conn.getTargets(); | ||
| for (const t of targets) { | ||
| if (t.type === "page" && t.url?.startsWith("chrome://")) { |
There was a problem hiding this comment.
| 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 |
|
The latest approval by @pirate could not refresh the mirrored PR automatically (missing-previous-source). The external PR stays open, and the mirrored PR should be updated manually before work continues. |
Summary
--connect <url>CLI option that starts the daemon attached to an existing Chrome instance via CDP WebSocket URL--ws(stateless, no ref caching),--connectgives you persistent daemon mode without launching ChromeUse case
Remote node management where Chrome is launched externally with custom flags (anti-bot detection, persistent profiles, specific ports) and the browse CLI needs to interact with it while preserving refs between commands.
Example:
Changes
packages/cli/src/index.ts: Added--connectoption toGlobalOpts, program options,runDaemon,ensureDaemon,sendCommand, andrunCommand--connectis set,ensureBrowserInitializeduseslocalBrowserLaunchOptions: { cdpUrl: connectUrl }instead of launching ChromesendCommandskipskillChromeProcesseswhen using--connect(Chrome is externally managed)Test plan
browse --connect <ws_url> open <url>navigates successfullybrowse --connect <ws_url> snapshot -creturns accessibility tree with refsbrowse --connect <ws_url> click @0-1uses cached refs from previous snapshot--wsbehavior unchangedMade with Cursor