feat(chrome): ChromeLauncher owns Chrome lifecycle + authenticated-profile mode#10
Merged
Merged
Conversation
…ofile mode Consolidate Chrome responsibility that was scattered across DynamicCommand, CdpDriver and Recorder behind ChromeLauncher and a new ChromeSession bridge, add a persistent-profile launch so a real logged-in session can be traced, and switch the journey to always open a fresh tab in the target window instead of reusing an existing one. - ChromeSession (new): the bridge between a held browser (launched or attached) and the CDP transport. Holds the port, knows whether it OWNS the process (so teardown is real for a throwaway and a no-op for an attached window), and exposes Chrome target discovery (pageTargets/openBlankTab). The raw websocket connect stays in CdpDriver (shared with Node). - ChromeLauncher.acquire(spec): one entry point that decides attach vs throwaway headless vs persistent profile and returns a ChromeSession. Spawn is parametrized for headless/headed and ephemeral/persistent profiles; a user-owned profile dir is never deleted on teardown. DynamicCommand's launch-vs-attach branch collapses into a single acquire() + session.kill(). - Authenticated sessions: new --chrome-profile <dir> (headed, persistent --user-data-dir, reuses saved logins/cookies) and --headed flags, with validation. Recorder's inline /json fetch now goes through the bridge too. - No tab targeting: JourneyRunner always opens its OWN tab and drives that, so an attached real window keeps its existing tabs untouched while our tab rides the same profile (same logins). Removed the now-dead urlMatch tab matcher from the Chrome path and ChromeSession. Verified: build clean, 47/48 unit tests pass (1 Postgres skip); end-to-end a --chrome-profile run traces the React bug and the profile persists across two separate CLI invocations; an attach run opens one fresh tab and leaves all pre-existing tabs intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR consolidates Chrome process lifecycle + CDP target discovery behind a new ChromeLauncher/ChromeSession abstraction, adding an authenticated “persistent profile” launch mode and shifting Chrome tracing to always open and drive a fresh tab (instead of attaching to/targeting an existing one).
Changes:
- Introduces
ChromeSessionas the bridge for Chrome target discovery (pageTargets,openBlankTab) and ownership-aware teardown (kill). - Adds
ChromeLauncher.acquire()to unify attach vs throwaway launch vs persistent--chrome-profilelaunch, and threads it throughDynamicCommandandRecorder. - Updates the Chrome journey runner to always open its own tab, removing the prior “tab targeting / urlMatch” behavior from the Chrome path.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/transport/CdpDriver.ts | Adjusts websocket target resolution / candidate selection logic. |
| src/engine/Tracer.ts | Switches Chrome tracing to use a ChromeSession wrapper (attach-by-port) and removes urlMatch-based tab selection. |
| src/engine/Recorder.ts | Uses ChromeLauncher.acquire() + ChromeSession.pageTargets() for render-Chrome discovery. |
| src/engine/JourneyRunner.ts | Refactors runner to operate on ChromeSession and always open/drive a new tab. |
| src/engine/ChromeSession.ts | New bridge object encapsulating Chrome port + ownership and Chrome-specific target ops. |
| src/engine/ChromeLauncher.ts | Centralizes Chrome lifecycle and adds acquire/attach/profile support. |
| src/cli/commands/DynamicCommand.ts | Uses ChromeLauncher.acquire() and ensures only owned Chrome is killed. |
| src/cli/CommandInputs.ts | Adds validated inputs for profileDir and headed. |
| src/cli/Cli.ts | Adds --chrome-profile / --headed flags and validation logic; plumbs into dynamic run. |
| skills/trace/SKILL.md | Updates skill documentation to mention --chrome-profile mode. |
Comments suppressed due to low confidence (1)
src/transport/CdpDriver.ts:85
resolveWsUrlno longer prefers Chrome "page" targets. Chrome’s/jsonoften includes other target types (e.g. service workers) withwebSocketDebuggerUrl, so this can connect to a non-page target and then laterPage.*calls will fail unexpectedly. Prefer filtering totype === "page"forTargetKind.Chrome(with an optional fallback to the full list if there are no pages).
const { kind = TargetKind.Node, urlMatch, titleMatch } = options;
const targets = await CdpDriver.listTargets(port, kind);
const candidates = Array.isArray(targets) ? targets : [];
let target: any;
if (urlMatch) target = candidates.find((candidate) => (candidate.url || "").includes(urlMatch));
if (!target && titleMatch) target = candidates.find((candidate) => (candidate.title || "").includes(titleMatch));
target = target || candidates.find((candidate) => candidate.webSocketDebuggerUrl) || candidates[0];
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
108
to
+112
| if (options.chrome != null && options.node != null) usage("pick one target: --node or --chrome, not both"); | ||
| if (options.chromeProfile && options.node != null) usage("--chrome-profile is a chrome option — don't combine it with --node"); | ||
| // --chrome-profile launches a browser on that profile; an explicit --chrome <port> means attach to a running one. | ||
| if (options.chromeProfile && typeof options.chrome === "string") usage("pick one: --chrome-profile launches a logged-in browser, or --chrome <port> attaches to a running one — not both"); | ||
| if (options.headed && !(options.chrome != null || options.chromeProfile)) usage("--headed only applies when launching Chrome (use with --chrome or --chrome-profile)"); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Chrome responsibility was scattered across
DynamicCommand(launch-vs-attach branch),CdpDriver(Chrome target discovery), andRecorder(inline/jsonfetch). This consolidates it behindChromeLauncherplus a newChromeSessionbridge, adds an authenticated-session launch mode, and removes tab targeting in favor of always opening a fresh tab.Motivated by: "continue everything in a known authenticated Chrome window so it reuses my saved logins."
Changes
ChromeSession(new) — the bridge. One object per--chromerun: holds the port, knows whether it owns the browser (launched) or just attached, and exposes Chrome target discovery (pageTargets/openBlankTab). The raw websocketconnectstays inCdpDriver(shared with Node).kill()tears down only what we launched.ChromeLauncher.acquire(spec)— single entry point. Decides attach (--chrome <port>) vs throwaway headless (--chrome, no port) vs persistent profile (--chrome-profile <dir>), returns aChromeSession. Spawn is parametrized for headless/headed and ephemeral/persistent profiles; a user-owned profile dir is never deleted on teardown.DynamicCommand's branch collapses into oneacquire()+session.kill().--chrome-profile <dir>(headed, persistent--user-data-dir, reuses saved logins/cookies) and--headed, with mutual-exclusion validation.Recordernow discovers its render tab through the bridge too.JourneyRunneralways opens its own tab and drives that, so an attached real window keeps its existing tabs untouched while our tab rides the same profile (same logins). Removed the now-deadurlMatchtab matcher from the Chrome path,ChromeSession, andCdpDriver.resolveWsUrl.No CLI flag was removed (
urlMatchwas never exposed); new flags surface automatically in--help/manifest, so thetraceskill needs no rewrite.Verification
--chrome-profilee2e: traced the planted Reactprice.tsbug (ok:true, 7 hits, recording produced); a marker written into the profile in run A was read back in a fresh process in run B → profile (hence a saved login) persists across separate CLI runs. Profile dir preserved on disk.ok:true, 5 hits; afterward 4 tabs: all 3 originals survived, exactly 1 new tab opened and traced.Usage
```
trace-cli run --chrome-profile ~/chrome-debug-profile
--root --breakpoint src/x.ts:42
--url https://your-authed-app/page --step 'click:...'
```
Use a copy of your Chrome profile (Chrome 136+ blocks remote-debugging on the default dir; one process per dir).
🤖 Generated with Claude Code