Skip to content

feat: adopt an existing browser/page instead of launching a new one#2

Open
felixtrz wants to merge 1 commit into
drawcall-ai:mainfrom
felixtrz:feat/adopt-existing-browser
Open

feat: adopt an existing browser/page instead of launching a new one#2
felixtrz wants to merge 1 commit into
drawcall-ai:mainfrom
felixtrz:feat/adopt-existing-browser

Conversation

@felixtrz

@felixtrz felixtrz commented May 31, 2026

Copy link
Copy Markdown

Add an opt-in way to run a snippet in a browser/page the caller already owns, so a host (e.g. a dev server with a visible, instrumented tab) can run a vitexec check in that same browser — no second window. Fully backward compatible: with none of the new options, behavior is unchanged.

API: new RunVitexecOptions page, adoptPage (a thunk resolved AFTER the Vite server is listening, so a host that launches a browser in reaction to the server can hand it over), context, browser, and keepBrowserOpen; new export runVitexecOnPage(); new CLI flag/env --keep-browser-open / VITEXEC_KEEP_BROWSER_OPEN.

Internals: split browser acquisition from context/page creation and gate teardown on ownership — vitexec only closes handles it created itself, and always closes its own Vite server. A single idempotent __vitexecReport binding per page allows reusing one adopted page across sequential runs. When adopting an already-loaded page the injected runtime is 'armed' so the snippet runs exactly once (on vitexec's navigation, after the report hook is bound), never on the page's prior load.

Tests: page/context/browser adoption, sequential reuse, and runVitexecOnPage.

This contribution is funded by Meta Platforms, Inc.

Summary by CodeRabbit

  • New Features

    • Added --keep-browser-open CLI flag and VITEXEC_KEEP_BROWSER_OPEN environment variable to maintain browser sessions.
    • Added support for reusing existing Playwright pages, contexts, and browsers with vitexec.
    • Added keepBrowserOpen option to programmatic API.
  • Documentation

    • Added guide for running vitexec with existing Playwright resources.
  • Tests

    • Added coverage for page and browser adoption scenarios.

Add an opt-in way to run a snippet in a browser/page the caller already owns, so a host (e.g. a dev server with a visible, instrumented tab) can run a vitexec check in that same browser — no second window. Fully backward compatible: with none of the new options, behavior is unchanged.

API: new RunVitexecOptions `page`, `adoptPage` (a thunk resolved AFTER the Vite server is listening, so a host that launches a browser in reaction to the server can hand it over), `context`, `browser`, and `keepBrowserOpen`; new export `runVitexecOnPage()`; new CLI flag/env `--keep-browser-open` / VITEXEC_KEEP_BROWSER_OPEN.

Internals: split browser acquisition from context/page creation and gate teardown on ownership — vitexec only closes handles it created itself, and always closes its own Vite server. A single idempotent `__vitexecReport` binding per page allows reusing one adopted page across sequential runs. When adopting an already-loaded page the injected runtime is 'armed' so the snippet runs exactly once (on vitexec's navigation, after the report hook is bound), never on the page's prior load.

Tests: page/context/browser adoption, sequential reuse, and runVitexecOnPage.
@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds page adoption and keep-browser-open support to vitexec. Callers can now supply existing Playwright pages, contexts, or browsers to vitexec, which tracks ownership and avoids closing adopted resources. The --keep-browser-open CLI flag and VITEXEC_KEEP_BROWSER_OPEN env var control whether vitexec leaves connected browsers running after execution. An "armed" injection mode ensures the runtime snippet executes exactly once on adopted pages.

Changes

Page Adoption and Keep-Browser-Open

Layer / File(s) Summary
Type definitions and options
packages/vitexec/src/index.ts, packages/vitexec/src/cli.ts
VitexecPluginOptions gains armed?: boolean to control conditional injection. RunVitexecOptions extends with adoption options (page, context, browser) and keepBrowserOpen. VITEXEC_KEEP_BROWSER_OPEN env var is added, and Frame import is included for frame-navigation support.
Vite plugin armed injection mechanism
packages/vitexec/src/index.ts
runtimeScript generator now accepts armed parameter and wraps the dynamic import in a conditional check of globalThis.__vitexecArm when armed. transformIndexHtml passes armed flag through to the generator.
Ownership tracking and adoption handling
packages/vitexec/src/cli.ts
runVitexecInServerTask refactored to track ownership of browser, context, and page. Conditional context creation for recordings when vitexec owns the context. Stable per-page report-binding management via WeakSet/WeakMap prevents "function already registered" errors on page reuse. Named event handlers are registered and detached in finally blocks. Arm init script via page.addInitScript enables the injected snippet to execute once on adopted-page loads. Cleanup conditionally saves recordings, detaches handlers, removes bindings, and closes resources only when vitexec owns them.
CLI flag and integration
packages/vitexec/src/cli.ts
--keep-browser-open CLI flag and CliOptions type updated. createRunOptions threads keepBrowserOpen into RunVitexecOptions. armed flag is determined from page adoption and passed to Vite plugin invocation.
Tests and documentation
packages/vitexec/test/cli.test.ts, README.md
New imports: chromium and runVitexecOnPage. README documents --keep-browser-open flag and env var. New "page adoption" section explains lifecycle and navigation behavior when reusing Playwright resources. Comprehensive test suite covers page adoption, page reuse across sequential runs, context adoption, browser adoption, and confirms adopted resources remain open while vitexec-created resources are properly cleaned up.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A rabbit learns to share its warren,
Adopting burrows, not just borrowing,
Each page a home to keep or let go—
Ownership tracked, clean as fresh snow.
The snippet arms once, no double-play,
A keeper of browsers, in every way! 🏠✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main feature added: enabling vitexec to run in an existing browser/page that the caller provides, rather than always launching a new one.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: da367ca8e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +358 to +360
await page.addInitScript((armId) => {
(globalThis as Record<string, unknown>).__vitexecArm = armId;
}, id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid accumulating arm scripts on reused pages

When the same adopted Page is used for multiple sequential runs, this installs another init script each time and there is no removal before the next navigation. All prior scripts will continue to run on future navigations and each writes a different __vitexecArm; since Playwright does not guarantee ordering for multiple init scripts, an older run's id can end up being the value checked by the injected runtime, causing the current snippet to be skipped and the run to time out. Use a single stable init script that reads the current run id from mutable state instead of appending a per-run script.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/vitexec/src/cli.ts (1)

413-423: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Close the page vitexec creates inside an adopted context.

When options.context is provided, vitexec opens a fresh page and marks ownsPage = true, but this cleanup path only closes owned contexts/browsers. Reusing one caller-owned context across runs will keep leaking tabs.

Possible fix
       if (page) {
         VITEXEC_REPORT_RESOLVERS.delete(page);
         page.off("console", collectConsoleLog);
         page.off("pageerror", onPageError);
         page.off("requestfailed", onRequestFailed);
         page.off("response", onResponse);
         page.off("framenavigated", onFrameNavigated);
       }
       if (cdp) await cdp.detach().catch(() => undefined);
+      if (ownsPage && !ownsContext && page && !page.isClosed()) {
+        await page.close().catch(() => undefined);
+      }
       if (ownsContext && context) {
         await context.close().then(
           () => {
             if (options.networkTracePath && !signal.aborted) {
               log(`[network-trace] ${options.networkTracePath}`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vitexec/src/cli.ts` around lines 413 - 423, The cleanup currently
only closes owned contexts and browsers, leaking pages when vitexec created a
page in a caller-provided context; add a branch to close the owned page: when
ownsPage is true and page exists, await page.close().catch(() => undefined)
(before or independent of closing the context), so reference and update the
cleanup around ownsContext/context and ownsBrowser/browser/keepOpen to also
handle ownsPage/page closure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/vitexec/src/index.ts`:
- Around line 76-93: The armed guard in runtimeScript only checks
globalThis.__vitexecArm (which resets on navigation) so imports can run once per
navigation; change runtimeScript to persist the armed state across reloads
(e.g., use sessionStorage/localStorage keyed by the id) by checking both
globalThis.__vitexecArm and a persistent flag (set the persistent flag before
performing the import and clear/leave it after success/failure), reference
runtimeScript, globalThis.__vitexecArm, globalThis.__vitexecRuns and the
codeRouteForBase(...) import expression when making the check and when setting
the persistent marker so the import truly runs exactly once across reloads.

---

Outside diff comments:
In `@packages/vitexec/src/cli.ts`:
- Around line 413-423: The cleanup currently only closes owned contexts and
browsers, leaking pages when vitexec created a page in a caller-provided
context; add a branch to close the owned page: when ownsPage is true and page
exists, await page.close().catch(() => undefined) (before or independent of
closing the context), so reference and update the cleanup around
ownsContext/context and ownsBrowser/browser/keepOpen to also handle
ownsPage/page closure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4265871-67f5-4387-a602-7668832a84a5

📥 Commits

Reviewing files that changed from the base of the PR and between 729d772 and da367ca.

📒 Files selected for processing (4)
  • README.md
  • packages/vitexec/src/cli.ts
  • packages/vitexec/src/index.ts
  • packages/vitexec/test/cli.test.ts

Comment on lines +76 to 93
function runtimeScript(id: string, base: string, armed: boolean): string {
const run = `globalThis.__vitexecRuns[${JSON.stringify(id)}] = import(${JSON.stringify(codeRouteForBase(base))} + "/" + ${JSON.stringify(encodeURIComponent(id))})
.catch(console.error)
.finally(() => globalThis.__vitexecReport?.());`;

if (!armed) {
return `
globalThis.__vitexecRuns ??= {};
${run}
`;
}

return `
globalThis.__vitexecRuns ??= {};
globalThis.__vitexecRuns[${JSON.stringify(id)}] = import(${JSON.stringify(codeRouteForBase(base))} + "/" + ${JSON.stringify(encodeURIComponent(id))})
.catch(console.error)
.finally(() => globalThis.__vitexecReport?.());
if (globalThis.__vitexecArm === ${JSON.stringify(id)}) {
${run}
}
`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist the armed guard across reloads.

On adopted pages this only guards the current document. If the page reloads during the run, globalThis.__vitexecArm is reinitialized and the same snippet imports again, so the "exactly once" contract turns into "once per navigation".

Possible fix
 function runtimeScript(id: string, base: string, armed: boolean): string {
   const run = `globalThis.__vitexecRuns[${JSON.stringify(id)}] = import(${JSON.stringify(codeRouteForBase(base))} + "/" + ${JSON.stringify(encodeURIComponent(id))})
   .catch(console.error)
   .finally(() => globalThis.__vitexecReport?.());`;

   if (!armed) {
     return `
 globalThis.__vitexecRuns ??= {};
 ${run}
 `;
   }

   return `
 globalThis.__vitexecRuns ??= {};
-if (globalThis.__vitexecArm === ${JSON.stringify(id)}) {
+if (
+  globalThis.__vitexecArm === ${JSON.stringify(id)} &&
+  sessionStorage.getItem("__vitexecArmedRun") !== ${JSON.stringify(id)}
+) {
+  sessionStorage.setItem("__vitexecArmedRun", ${JSON.stringify(id)});
   ${run}
 }
 `;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function runtimeScript(id: string, base: string, armed: boolean): string {
const run = `globalThis.__vitexecRuns[${JSON.stringify(id)}] = import(${JSON.stringify(codeRouteForBase(base))} + "/" + ${JSON.stringify(encodeURIComponent(id))})
.catch(console.error)
.finally(() => globalThis.__vitexecReport?.());`;
if (!armed) {
return `
globalThis.__vitexecRuns ??= {};
${run}
`;
}
return `
globalThis.__vitexecRuns ??= {};
globalThis.__vitexecRuns[${JSON.stringify(id)}] = import(${JSON.stringify(codeRouteForBase(base))} + "/" + ${JSON.stringify(encodeURIComponent(id))})
.catch(console.error)
.finally(() => globalThis.__vitexecReport?.());
if (globalThis.__vitexecArm === ${JSON.stringify(id)}) {
${run}
}
`;
function runtimeScript(id: string, base: string, armed: boolean): string {
const run = `globalThis.__vitexecRuns[${JSON.stringify(id)}] = import(${JSON.stringify(codeRouteForBase(base))} + "/" + ${JSON.stringify(encodeURIComponent(id))})
.catch(console.error)
.finally(() => globalThis.__vitexecReport?.());`;
if (!armed) {
return `
globalThis.__vitexecRuns ??= {};
${run}
`;
}
return `
globalThis.__vitexecRuns ??= {};
if (
globalThis.__vitexecArm === ${JSON.stringify(id)} &&
sessionStorage.getItem("__vitexecArmedRun") !== ${JSON.stringify(id)}
) {
sessionStorage.setItem("__vitexecArmedRun", ${JSON.stringify(id)});
${run}
}
`;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/vitexec/src/index.ts` around lines 76 - 93, The armed guard in
runtimeScript only checks globalThis.__vitexecArm (which resets on navigation)
so imports can run once per navigation; change runtimeScript to persist the
armed state across reloads (e.g., use sessionStorage/localStorage keyed by the
id) by checking both globalThis.__vitexecArm and a persistent flag (set the
persistent flag before performing the import and clear/leave it after
success/failure), reference runtimeScript, globalThis.__vitexecArm,
globalThis.__vitexecRuns and the codeRouteForBase(...) import expression when
making the check and when setting the persistent marker so the import truly runs
exactly once across reloads.

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.

1 participant