Skip to content

feat(chrome): ChromeLauncher owns Chrome lifecycle + authenticated-profile mode#10

Merged
burrows99 merged 1 commit into
masterfrom
feat/chrome-launcher-ownership
Jun 19, 2026
Merged

feat(chrome): ChromeLauncher owns Chrome lifecycle + authenticated-profile mode#10
burrows99 merged 1 commit into
masterfrom
feat/chrome-launcher-ownership

Conversation

@burrows99

Copy link
Copy Markdown
Owner

What & why

Chrome responsibility was scattered across DynamicCommand (launch-vs-attach branch), CdpDriver (Chrome target discovery), and Recorder (inline /json fetch). This consolidates it behind ChromeLauncher plus a new ChromeSession bridge, 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 --chrome run: holds the port, knows whether it owns the browser (launched) or just attached, and exposes Chrome target discovery (pageTargets / openBlankTab). The raw websocket connect stays in CdpDriver (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 a ChromeSession. 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 one acquire() + session.kill().
  • Authenticated sessions. New flags --chrome-profile <dir> (headed, persistent --user-data-dir, reuses saved logins/cookies) and --headed, with mutual-exclusion validation. Recorder now discovers its render tab 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, ChromeSession, and CdpDriver.resolveWsUrl.

No CLI flag was removed (urlMatch was never exposed); new flags surface automatically in --help / manifest, so the trace skill needs no rewrite.

Verification

  • Build clean; 47/48 unit tests pass (1 Postgres skip).
  • --chrome-profile e2e: traced the planted React price.ts bug (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.
  • Attach e2e: a window with 3 pre-existing tabs → attach + trace → 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

…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>
Copilot AI review requested due to automatic review settings June 19, 2026 09:20
@burrows99 burrows99 merged commit b4b01e6 into master Jun 19, 2026
2 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 ChromeSession as 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-profile launch, and threads it through DynamicCommand and Recorder.
  • 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

  • resolveWsUrl no longer prefers Chrome "page" targets. Chrome’s /json often includes other target types (e.g. service workers) with webSocketDebuggerUrl, so this can connect to a non-page target and then later Page.* calls will fail unexpectedly. Prefer filtering to type === "page" for TargetKind.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 thread src/cli/Cli.ts
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)");
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants