@roxybrowser/playwright 2.0.2-beta.1 → 2.0.2-beta.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +30 -7
  2. package/dist/bin/roxybrowser-mcp.js +3 -0
  3. package/dist/bin/roxybrowser-mcp.js.map +1 -1
  4. package/dist/browser.d.ts +33 -5
  5. package/dist/browser.d.ts.map +1 -1
  6. package/dist/browser.js +134 -15
  7. package/dist/browser.js.map +1 -1
  8. package/dist/browserType.d.ts +23 -1
  9. package/dist/browserType.d.ts.map +1 -1
  10. package/dist/browserType.js +33 -5
  11. package/dist/browserType.js.map +1 -1
  12. package/dist/human/bubbleCursor.d.ts +2 -0
  13. package/dist/human/bubbleCursor.d.ts.map +1 -0
  14. package/dist/human/bubbleCursor.js +126 -0
  15. package/dist/human/bubbleCursor.js.map +1 -0
  16. package/dist/human/controller.d.ts +1 -0
  17. package/dist/human/controller.d.ts.map +1 -1
  18. package/dist/human/controller.js +13 -12
  19. package/dist/human/controller.js.map +1 -1
  20. package/dist/human/profile.d.ts.map +1 -1
  21. package/dist/human/profile.js +0 -4
  22. package/dist/human/profile.js.map +1 -1
  23. package/dist/human/types.d.ts +0 -1
  24. package/dist/human/types.d.ts.map +1 -1
  25. package/dist/locator.js +1 -1
  26. package/dist/locator.js.map +1 -1
  27. package/dist/mcp/backend/connect.d.ts +1 -0
  28. package/dist/mcp/backend/connect.d.ts.map +1 -1
  29. package/dist/mcp/backend/connect.js +4 -2
  30. package/dist/mcp/backend/connect.js.map +1 -1
  31. package/dist/mcp/backend/context.d.ts +3 -0
  32. package/dist/mcp/backend/context.d.ts.map +1 -1
  33. package/dist/mcp/backend/context.js +9 -1
  34. package/dist/mcp/backend/context.js.map +1 -1
  35. package/dist/mcp/backend/keyboard.d.ts +28 -0
  36. package/dist/mcp/backend/keyboard.d.ts.map +1 -1
  37. package/dist/mcp/backend/keyboard.js +9 -3
  38. package/dist/mcp/backend/keyboard.js.map +1 -1
  39. package/dist/mcp/backend/network.d.ts.map +1 -1
  40. package/dist/mcp/backend/network.js +30 -4
  41. package/dist/mcp/backend/network.js.map +1 -1
  42. package/dist/mcp/backend/response.d.ts +2 -0
  43. package/dist/mcp/backend/response.d.ts.map +1 -1
  44. package/dist/mcp/backend/response.js +53 -7
  45. package/dist/mcp/backend/response.js.map +1 -1
  46. package/dist/mcp/connectedBrowser.d.ts.map +1 -1
  47. package/dist/mcp/connectedBrowser.js +492 -43
  48. package/dist/mcp/connectedBrowser.js.map +1 -1
  49. package/dist/mcp/output.d.ts +5 -0
  50. package/dist/mcp/output.d.ts.map +1 -1
  51. package/dist/mcp/output.js +17 -1
  52. package/dist/mcp/output.js.map +1 -1
  53. package/dist/mcp/runtime.d.ts +10 -1
  54. package/dist/mcp/runtime.d.ts.map +1 -1
  55. package/dist/mcp/runtime.js +82 -13
  56. package/dist/mcp/runtime.js.map +1 -1
  57. package/dist/mcp/server.d.ts.map +1 -1
  58. package/dist/mcp/server.js +3 -1
  59. package/dist/mcp/server.js.map +1 -1
  60. package/dist/mcp/tools/mouse.d.ts.map +1 -1
  61. package/dist/mcp/tools/mouse.js +5 -2
  62. package/dist/mcp/tools/mouse.js.map +1 -1
  63. package/dist/mcp/transports/inMemory.d.ts.map +1 -1
  64. package/dist/mcp/transports/inMemory.js +1 -0
  65. package/dist/mcp/transports/inMemory.js.map +1 -1
  66. package/dist/mcp/types.d.ts +12 -1
  67. package/dist/mcp/types.d.ts.map +1 -1
  68. package/dist/page.d.ts +2 -0
  69. package/dist/page.d.ts.map +1 -1
  70. package/dist/page.js +12 -0
  71. package/dist/page.js.map +1 -1
  72. package/dist/protocol/adapter.d.ts +1 -0
  73. package/dist/protocol/adapter.d.ts.map +1 -1
  74. package/dist/protocol/bidi/backend.d.ts +1 -1
  75. package/dist/protocol/bidi/backend.d.ts.map +1 -1
  76. package/dist/protocol/bidi/backend.js +6 -2
  77. package/dist/protocol/bidi/backend.js.map +1 -1
  78. package/dist/protocol/cdp/backend.d.ts.map +1 -1
  79. package/dist/protocol/cdp/backend.js +122 -8
  80. package/dist/protocol/cdp/backend.js.map +1 -1
  81. package/dist/roxybrowser.bundle.js +560 -68
  82. package/dist/roxybrowser.bundle.js.map +1 -1
  83. package/dist/types/api.d.ts +23 -4
  84. package/dist/types/api.d.ts.map +1 -1
  85. package/dist/types/options.d.ts +0 -1
  86. package/dist/types/options.d.ts.map +1 -1
  87. package/package.json +2 -1
@@ -5124,6 +5124,7 @@ var RoxyPage = class RoxyPage {
5124
5124
  closed = false;
5125
5125
  closePromise = null;
5126
5126
  closeReason;
5127
+ ownedContext;
5127
5128
  defaultTimeoutMs = DEFAULT_EVENT_TIMEOUT_MS;
5128
5129
  defaultNavigationTimeoutMs = DEFAULT_EVENT_TIMEOUT_MS;
5129
5130
  currentViewportSize = null;
@@ -6111,6 +6112,10 @@ waiting for navigation${navigationTargetDescription} until "${waitUntil}"\n=====
6111
6112
  }
6112
6113
  this.closeReason = options.reason;
6113
6114
  this.closePromise = (async () => {
6115
+ if (this.ownedContext) {
6116
+ await this.ownedContext.close().catch(() => {});
6117
+ return;
6118
+ }
6114
6119
  await this.dismissActiveDialogsForClose();
6115
6120
  await this.finalizeVideoRecording();
6116
6121
  await this.adapter.close(options).catch(() => {});
@@ -6122,6 +6127,9 @@ waiting for navigation${navigationTargetDescription} until "${waitUntil}"\n=====
6122
6127
  this.closePromise = null;
6123
6128
  }
6124
6129
  }
6130
+ setOwnedContext(context) {
6131
+ this.ownedContext = context;
6132
+ }
6125
6133
  defaultTimeout() {
6126
6134
  return this.defaultTimeoutMs;
6127
6135
  }
@@ -10650,12 +10658,74 @@ var RoxyBrowser = class {
10650
10658
  session;
10651
10659
  adapter;
10652
10660
  humanDefaults;
10653
- browserName;
10654
- constructor(session, adapter, humanDefaults, browserName = "chromium") {
10661
+ _browserName;
10662
+ _browserType;
10663
+ _version;
10664
+ _contexts = [];
10665
+ _listeners = /* @__PURE__ */ new Map();
10666
+ _connected = true;
10667
+ constructor(session, adapter, humanDefaults, _browserName, _browserType, _version) {
10655
10668
  this.session = session;
10656
10669
  this.adapter = adapter;
10657
10670
  this.humanDefaults = humanDefaults;
10658
- this.browserName = browserName;
10671
+ this._browserName = _browserName;
10672
+ this._browserType = _browserType;
10673
+ this._version = _version;
10674
+ }
10675
+ on(event, listener) {
10676
+ return this._addListenerInternal(event, listener);
10677
+ }
10678
+ once(event, listener) {
10679
+ const wrapped = (payload) => {
10680
+ this._removeListenerInternal(event, listener);
10681
+ listener(payload);
10682
+ };
10683
+ this._ensureListenerSet(event).add({
10684
+ original: listener,
10685
+ wrapped
10686
+ });
10687
+ return this;
10688
+ }
10689
+ addListener(event, listener) {
10690
+ return this._addListenerInternal(event, listener);
10691
+ }
10692
+ removeListener(event, listener) {
10693
+ return this._removeListenerInternal(event, listener);
10694
+ }
10695
+ off(event, listener) {
10696
+ return this._removeListenerInternal(event, listener);
10697
+ }
10698
+ prependListener(event, listener) {
10699
+ const entry = {
10700
+ original: listener,
10701
+ wrapped: listener
10702
+ };
10703
+ const existing = this._listeners.get(event);
10704
+ if (!existing) {
10705
+ const s = new Set([entry]);
10706
+ this._listeners.set(event, s);
10707
+ } else {
10708
+ const s = new Set([entry, ...Array.from(existing)]);
10709
+ this._listeners.set(event, s);
10710
+ }
10711
+ return this;
10712
+ }
10713
+ removeAllListeners(type) {
10714
+ if (type === void 0) this._listeners.clear();
10715
+ else this._listeners.delete(type);
10716
+ return this;
10717
+ }
10718
+ browserType() {
10719
+ return this._browserType;
10720
+ }
10721
+ contexts() {
10722
+ return [...this._contexts];
10723
+ }
10724
+ isConnected() {
10725
+ return this._connected;
10726
+ }
10727
+ version() {
10728
+ return this._version;
10659
10729
  }
10660
10730
  async newContext(options = {}) {
10661
10731
  const normalizedOptions = {
@@ -10666,26 +10736,66 @@ var RoxyBrowser = class {
10666
10736
  ...options.recordVideo.dir ? { dir: resolve(options.recordVideo.dir) } : {}
10667
10737
  } } : {}
10668
10738
  };
10669
- return new RoxyBrowserContext(await this.session.newContext(normalizedOptions), resolveHumanizationOptions(normalizedOptions.human, this.humanDefaults), normalizedOptions, this.browserName);
10670
- }
10671
- async version() {
10672
- return this.session.version();
10739
+ const context = new RoxyBrowserContext(await this.session.newContext(normalizedOptions), resolveHumanizationOptions(normalizedOptions.human, this.humanDefaults), normalizedOptions, this._browserName);
10740
+ this._contexts.push(context);
10741
+ context.on("close", () => {
10742
+ const index = this._contexts.indexOf(context);
10743
+ if (index !== -1) this._contexts.splice(index, 1);
10744
+ });
10745
+ this._emit("context", context);
10746
+ return context;
10747
+ }
10748
+ async newPage(options) {
10749
+ const context = await this.newContext(options);
10750
+ const page = await context.newPage();
10751
+ const roxyPage = page;
10752
+ if (typeof roxyPage.setOwnedContext === "function") roxyPage.setOwnedContext(context);
10753
+ else page.once("close", () => context.close().catch(() => {}));
10754
+ return page;
10673
10755
  }
10674
- async close() {
10756
+ async close(options) {
10757
+ if (!this._connected) return;
10758
+ this._connected = false;
10675
10759
  try {
10676
10760
  await withCloseTimeout$1(this.session.close(), BROWSER_SESSION_CLOSE_TIMEOUT_MS);
10677
10761
  } finally {
10678
10762
  await this.adapter.close();
10763
+ this._emit("disconnected", this);
10679
10764
  }
10680
10765
  }
10766
+ _addListenerInternal(event, listener) {
10767
+ this._ensureListenerSet(event).add({
10768
+ original: listener,
10769
+ wrapped: listener
10770
+ });
10771
+ return this;
10772
+ }
10773
+ _removeListenerInternal(event, listener) {
10774
+ const entries = this._listeners.get(event);
10775
+ if (!entries) return this;
10776
+ for (const entry of Array.from(entries)) if (entry.original === listener) entries.delete(entry);
10777
+ if (entries.size === 0) this._listeners.delete(event);
10778
+ return this;
10779
+ }
10780
+ _ensureListenerSet(event) {
10781
+ const existing = this._listeners.get(event);
10782
+ if (existing) return existing;
10783
+ const created = /* @__PURE__ */ new Set();
10784
+ this._listeners.set(event, created);
10785
+ return created;
10786
+ }
10787
+ _emit(event, payload) {
10788
+ const entries = this._listeners.get(event);
10789
+ if (!entries?.size) return false;
10790
+ for (const entry of Array.from(entries)) entry.wrapped(payload);
10791
+ return true;
10792
+ }
10681
10793
  };
10682
10794
  async function withCloseTimeout$1(promise, timeoutMs) {
10683
10795
  let timer;
10684
10796
  try {
10685
10797
  return await Promise.race([promise, new Promise((_, reject) => {
10686
- timer = setTimeout(() => {
10687
- reject(/* @__PURE__ */ new Error(`Timed out closing browser session after ${timeoutMs}ms.`));
10688
- }, timeoutMs);
10798
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Timed out closing browser session after ${timeoutMs}ms.`)), timeoutMs);
10689
10799
  })]);
10690
10800
  } finally {
10691
10801
  if (timer) clearTimeout(timer);
@@ -16005,7 +16115,7 @@ var BidiPageAdapter = class BidiPageAdapter {
16005
16115
  resultOwnership: "none"
16006
16116
  }));
16007
16117
  if (response.type === "exception") throw new Error(response.exceptionDetails.text || "BiDi evaluation failed.");
16008
- return parseSerializedEvaluationResult(extractBiDiValue(response.result));
16118
+ return parseSerializedEvaluationResult(extractBiDiValue$1(response.result));
16009
16119
  }
16010
16120
  async evaluateFunction(expression, arg) {
16011
16121
  const serializedArg = arg === void 0 ? "" : serializeForEvaluation$1(arg);
@@ -17249,11 +17359,11 @@ var BidiElementHandleAdapter = class BidiElementHandleAdapter {
17249
17359
  return this.page.selectOptionReference(this.reference(), values, options);
17250
17360
  }
17251
17361
  };
17252
- function extractBiDiValue(value) {
17253
- if (value.type === "array" && Array.isArray(value.value)) return value.value.map((entry) => extractBiDiValue(entry));
17362
+ function extractBiDiValue$1(value) {
17363
+ if (value.type === "array" && Array.isArray(value.value)) return value.value.map((entry) => extractBiDiValue$1(entry));
17254
17364
  if (value.type === "object" && Array.isArray(value.value)) {
17255
17365
  const obj = {};
17256
- for (const [key, val] of value.value) obj[key] = extractBiDiValue(val);
17366
+ for (const [key, val] of value.value) obj[key] = extractBiDiValue$1(val);
17257
17367
  return obj;
17258
17368
  }
17259
17369
  return value.value;
@@ -17645,9 +17755,9 @@ function buildFirefoxLaunchArgs(options, userDataDir, port) {
17645
17755
  ...options.args ?? []
17646
17756
  ];
17647
17757
  }
17648
- function resolveFirefoxExecutableCandidates(options, platform = currentPlatform$1(), fileExistsFn = fileExists$1) {
17758
+ function resolveFirefoxExecutableCandidates(options, platform = currentPlatform$1(), playwrightFirefoxExecutablePath, fileExistsFn = fileExists$1) {
17649
17759
  if (options.executablePath) return [options.executablePath];
17650
- return filterExistingFirefoxExecutableCandidates(defaultFirefoxExecutableCandidates(platform), platform, fileExistsFn);
17760
+ return filterExistingFirefoxExecutableCandidates([...playwrightFirefoxExecutablePath ? [playwrightFirefoxExecutablePath] : [], ...defaultFirefoxExecutableCandidates(platform)], platform, fileExistsFn);
17651
17761
  }
17652
17762
  function defaultFirefoxExecutableCandidates(platform) {
17653
17763
  const candidates = [];
@@ -53298,11 +53408,34 @@ var RoxyBrowserType = class {
53298
53408
  protocol: options.protocol ?? (this.browserName === "firefox" ? "bidi" : "cdp")
53299
53409
  });
53300
53410
  }
53301
- async connect(options) {
53411
+ /**
53412
+ * Connect to an already-running browser over a WebSocket endpoint.
53413
+ *
53414
+ * ⚠️ DIVERGENCE FROM PLAYWRIGHT — DO NOT "FIX" THIS BACK TO CDP-ONLY:
53415
+ *
53416
+ * In upstream Playwright, `BrowserType.connect()` is a CDP-only operation: it
53417
+ * connects to a running Chromium over the DevTools Protocol. Firefox/WebKit are
53418
+ * not supported here (Playwright routes BiDi through `connectOverCDP`-style
53419
+ * helpers or its own internal plumbing, not this method).
53420
+ *
53421
+ * Roxy intentionally changes that semantics. This method dispatches on
53422
+ * `browserName` so a single entry point serves both browser families:
53423
+ * - chromium → CDP (ws:// DevTools endpoint)
53424
+ * - firefox → BiDi (ws:// WebDriver BiDi endpoint)
53425
+ *
53426
+ * This is deliberate, not a bug. If you are an agent "normalizing" this code to
53427
+ * match Playwright upstream, STOP — the dual-protocol dispatch is the feature.
53428
+ * To force CDP specifically, call `connectOverCDP()` instead, which remains
53429
+ * chromium-only and CDP-only by design (see the guard below).
53430
+ *
53431
+ * `options.protocol` may still be passed to override the per-browser default.
53432
+ */
53433
+ async connect(endpointURL, options) {
53302
53434
  return this.connectBrowser({
53303
- ...options,
53304
- browserName: options.browserName ?? this.browserName,
53305
- protocol: options.protocol ?? (this.browserName === "firefox" ? "bidi" : "cdp")
53435
+ browserName: this.browserName,
53436
+ protocol: this.browserName === "chromium" ? "cdp" : "bidi",
53437
+ wsEndpoint: endpointURL,
53438
+ ...options
53306
53439
  });
53307
53440
  }
53308
53441
  async connectOverCDP(progressOrEndpointURL, endpointURLOrOptions, maybeOptions = {}) {
@@ -53334,7 +53467,9 @@ var RoxyBrowserType = class {
53334
53467
  protocol
53335
53468
  });
53336
53469
  await adapter.connect();
53337
- return new RoxyBrowser(await adapter.browser(), adapter, resolveHumanizationOptions(options.human), options.browserName ?? this.browserName);
53470
+ const session = await adapter.browser();
53471
+ const versionStr = await session.version();
53472
+ return new RoxyBrowser(session, adapter, resolveHumanizationOptions(options.human), options.browserName ?? this.browserName, this, versionStr);
53338
53473
  }
53339
53474
  };
53340
53475
  var chromium = new RoxyBrowserType("chromium", {
@@ -73317,6 +73452,7 @@ var Response$1 = class {
73317
73452
  includeSnapshot = "none";
73318
73453
  fullSnapshot;
73319
73454
  isClose = false;
73455
+ rawResults = false;
73320
73456
  constructor(context, toolName, toolArgs) {
73321
73457
  this.context = context;
73322
73458
  this.toolName = toolName;
@@ -73340,6 +73476,9 @@ var Response$1 = class {
73340
73476
  setClose() {
73341
73477
  this.isClose = true;
73342
73478
  }
73479
+ setRawResults() {
73480
+ this.rawResults = true;
73481
+ }
73343
73482
  setIncludeSnapshot() {
73344
73483
  this.includeSnapshot = this.context.config.snapshot?.mode ?? "full";
73345
73484
  }
@@ -73355,6 +73494,21 @@ var Response$1 = class {
73355
73494
  async serialize() {
73356
73495
  const sections = [];
73357
73496
  if (this.errors.length) sections.push("### Error", ...this.errors);
73497
+ if (this.rawResults) {
73498
+ if (this.results.length) sections.push(...this.results);
73499
+ return {
73500
+ content: [{
73501
+ type: "text",
73502
+ text: sections.join("\n")
73503
+ }, ...this.images.map((image) => ({
73504
+ type: "image",
73505
+ data: image.data,
73506
+ mimeType: image.mimeType
73507
+ }))],
73508
+ ...this.isClose ? { isClose: true } : {},
73509
+ ...this.errors.length ? { isError: true } : {}
73510
+ };
73511
+ }
73358
73512
  if (this.results.length) {
73359
73513
  if (sections.length) sections.push("");
73360
73514
  sections.push("### Result", ...this.results);
@@ -73364,16 +73518,23 @@ var Response$1 = class {
73364
73518
  sections.push("### Code", "```js", ...this.code, "```");
73365
73519
  }
73366
73520
  if (this.includeSnapshot === "full") {
73367
- const snapshot = await this.context.runtime.snapshot();
73368
- if (sections.length) sections.push("");
73369
- sections.push(formatSnapshot(snapshot));
73521
+ if (!await this.context.runtime.hasDialog()) {
73522
+ const snapshot = await reconcileSnapshotWithTabs(this.context, await this.context.runtime.snapshot());
73523
+ if (sections.length) sections.push("");
73524
+ sections.push(formatSnapshot(snapshot));
73525
+ }
73370
73526
  }
73371
73527
  if (this.fullSnapshot) {
73372
- const snapshot = await this.context.runtime.snapshot({
73528
+ let snapshot = await reconcileSnapshotWithTabs(this.context, await this.context.runtime.snapshot({
73373
73529
  ...this.fullSnapshot.target !== void 0 ? { target: this.fullSnapshot.target } : {},
73374
73530
  ...this.fullSnapshot.depth !== void 0 ? { depth: this.fullSnapshot.depth } : {},
73375
73531
  ...this.fullSnapshot.boxes !== void 0 ? { boxes: this.fullSnapshot.boxes } : {}
73376
- });
73532
+ }));
73533
+ if (!this.fullSnapshot.filename && snapshot.retryable && snapshot.text.trim().length === 0 && snapshot.url && snapshot.url !== "about:blank") snapshot = await reconcileSnapshotWithTabs(this.context, await this.context.runtime.snapshot({
73534
+ ...this.fullSnapshot.target !== void 0 ? { target: this.fullSnapshot.target } : {},
73535
+ ...this.fullSnapshot.depth !== void 0 ? { depth: this.fullSnapshot.depth } : {},
73536
+ ...this.fullSnapshot.boxes !== void 0 ? { boxes: this.fullSnapshot.boxes } : {}
73537
+ }));
73377
73538
  if (this.fullSnapshot.filename) {
73378
73539
  const resolvedFilename = await this.context.resolveOutputFile(this.fullSnapshot.filename);
73379
73540
  await writeFile(resolvedFilename, snapshot.text);
@@ -73400,6 +73561,15 @@ var Response$1 = class {
73400
73561
  };
73401
73562
  }
73402
73563
  };
73564
+ async function reconcileSnapshotWithTabs(context, snapshot) {
73565
+ const activeTab = (await context.runtime.listTabs()).find((tab) => tab.active);
73566
+ if (!activeTab) return snapshot;
73567
+ return {
73568
+ ...snapshot,
73569
+ title: activeTab.title || snapshot.title,
73570
+ url: activeTab.url || snapshot.url
73571
+ };
73572
+ }
73403
73573
  //#endregion
73404
73574
  //#region src/mcp/backend/tool.ts
73405
73575
  function defineTool$1(tool) {
@@ -73498,7 +73668,8 @@ var connect_default = [defineTool$1({
73498
73668
  description: "Attach to an existing browser and seed the active tab snapshot.",
73499
73669
  inputSchema: object({
73500
73670
  endpoint: string().min(1),
73501
- browser: _enum(["chrome", "firefox"]).default("chrome")
73671
+ browser: _enum(["chrome", "firefox"]).default("chrome"),
73672
+ sessionId: string().min(1).optional()
73502
73673
  }),
73503
73674
  type: "action"
73504
73675
  },
@@ -73507,7 +73678,8 @@ var connect_default = [defineTool$1({
73507
73678
  const result = await context.runtime.connect({
73508
73679
  protocol,
73509
73680
  endpoint: params.endpoint,
73510
- browser: params.browser === "chrome" ? "chromium" : params.browser
73681
+ browser: params.browser === "chrome" ? "chromium" : params.browser,
73682
+ ...params.sessionId ? { sessionId: params.sessionId } : {}
73511
73683
  });
73512
73684
  response.addTextResult(formatConnectResult({
73513
73685
  ...result,
@@ -73952,7 +74124,17 @@ var networkRequest = defineTool$1({
73952
74124
  response.addError(`Request #${args.index} not found. Use browser_network_requests to see available indexes.`);
73953
74125
  return;
73954
74126
  }
73955
- const text = args.part ? renderRequestPart(request, args.part) : renderRequestDetails(request);
74127
+ if (args.part) {
74128
+ response.setRawResults();
74129
+ const partText = args.part === "response-body" ? await context.runtime.fetchResponseBody(args.index) ?? "" : renderRequestPart(request, args.part);
74130
+ if (args.filename) {
74131
+ const resolvedFilename = await context.resolveOutputFile(args.filename);
74132
+ await writeFile(resolvedFilename, partText);
74133
+ response.addTextResult(`Saved network request to "${resolvedFilename}".`);
74134
+ } else response.addTextResult(partText);
74135
+ return;
74136
+ }
74137
+ const text = renderRequestDetails(request);
73956
74138
  if (args.filename) {
73957
74139
  const resolvedFilename = await context.resolveOutputFile(args.filename);
73958
74140
  await writeFile(resolvedFilename, text);
@@ -73986,8 +74168,10 @@ function renderRequestDetails(request) {
73986
74168
  if (request.mimeType) lines.push(` mimeType: ${request.mimeType}`);
73987
74169
  appendHeaders(lines, "Request headers", request.requestHeaders);
73988
74170
  if (request.responseHeaders) appendHeaders(lines, "Response headers", request.responseHeaders);
73989
- if (request.requestBody) lines.push("", `Call browser_network_request with part="request-body" to read the request body.`);
73990
- if (request.responseBody) lines.push("", `Call browser_network_request with part="response-body" to read the response body.`);
74171
+ const hints = [];
74172
+ if (request.requestBody) hints.push(`Call browser_network_request with part="request-body" to read the request body.`);
74173
+ if (canHaveResponseBody(request)) hints.push(`Call browser_network_request with part="response-body" to read the response body.`);
74174
+ if (hints.length) lines.push("", ...hints);
73991
74175
  return lines.join("\n");
73992
74176
  }
73993
74177
  function renderRequestPart(request, part) {
@@ -74006,6 +74190,10 @@ function appendHeaders(lines, title, headers) {
74006
74190
  function renderHeaders(headers) {
74007
74191
  return Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join("\n");
74008
74192
  }
74193
+ function canHaveResponseBody(request) {
74194
+ if (request.failureText || request.status === void 0) return false;
74195
+ return request.status !== 204 && request.status !== 304 && !(request.status >= 100 && request.status < 200);
74196
+ }
74009
74197
  var network_default = [networkRequests, networkRequest];
74010
74198
  var runCode_default = [defineTool$1({
74011
74199
  capability: "devtools",
@@ -74233,6 +74421,16 @@ var allTools = [...mouse_default, ...form_default];
74233
74421
  function delay$1(ms) {
74234
74422
  return new Promise((resolve) => setTimeout(resolve, ms));
74235
74423
  }
74424
+ async function withBiDiTimeout(promise, timeoutMs) {
74425
+ let timer;
74426
+ try {
74427
+ return await Promise.race([promise, new Promise((_, reject) => {
74428
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Timed out after ${timeoutMs}ms.`)), timeoutMs);
74429
+ })]);
74430
+ } finally {
74431
+ if (timer) clearTimeout(timer);
74432
+ }
74433
+ }
74236
74434
  var chromeRemoteInterface = "default" in import_chrome_remote_interface ? import_chrome_remote_interface.default : import_chrome_remote_interface;
74237
74435
  function buildConnectionFromWsEndpoint(browserWsEndpoint) {
74238
74436
  const parsed = new URL(browserWsEndpoint);
@@ -74666,13 +74864,13 @@ var CDP_KEY_MAP = {
74666
74864
  async function evaluateBiDi(client, contextId, functionSource, arg) {
74667
74865
  const expression = arg === void 0 ? `(${functionSource})()` : `(${functionSource})(${JSON.stringify(arg)})`;
74668
74866
  const response = await client.scriptEvaluate({
74669
- expression,
74867
+ expression: wrapWithSerializedEvaluationResult(expression),
74670
74868
  target: { context: contextId },
74671
74869
  awaitPromise: true,
74672
74870
  resultOwnership: "none"
74673
74871
  });
74674
74872
  if (response.type === "exception") throw new Error(response.exceptionDetails?.text || "BiDi runtime evaluation failed.");
74675
- return response.result?.value;
74873
+ return parseSerializedEvaluationResult(extractBiDiValue(response.result));
74676
74874
  }
74677
74875
  async function evaluateBiDiRef(client, contextId, functionSource, arg) {
74678
74876
  const expression = arg === void 0 ? `(${functionSource})()` : `(${functionSource})(${JSON.stringify(arg)})`;
@@ -74694,6 +74892,16 @@ async function evaluateBiDiRef(client, contextId, functionSource, arg) {
74694
74892
  ...response.result?.value?.handle !== void 0 ? { handle: response.result.value.handle } : {}
74695
74893
  };
74696
74894
  }
74895
+ function extractBiDiValue(value) {
74896
+ if (!value) return;
74897
+ if (value.type === "array" && Array.isArray(value.value)) return value.value.map((entry) => extractBiDiValue(entry));
74898
+ if (value.type === "object" && Array.isArray(value.value)) {
74899
+ const obj = {};
74900
+ for (const [key, val] of value.value) obj[key] = extractBiDiValue(val);
74901
+ return obj;
74902
+ }
74903
+ return value.value;
74904
+ }
74697
74905
  function toAriaSnapshotPayload(request = {}) {
74698
74906
  return {
74699
74907
  options: normalizeAriaSnapshotOptions({
@@ -74733,6 +74941,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74733
74941
  pageConsoleStates = /* @__PURE__ */ new Map();
74734
74942
  pageNetworkStates = /* @__PURE__ */ new Map();
74735
74943
  pageDialogStates = /* @__PURE__ */ new Map();
74944
+ dialogWaiters = /* @__PURE__ */ new Map();
74736
74945
  activeTabId;
74737
74946
  versionString = "Chromium/unknown";
74738
74947
  constructor(browserClient, connection) {
@@ -74748,7 +74957,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74748
74957
  });
74749
74958
  const session = new CdpConnectedBrowserSession(await chromeRemoteInterface({ target: connection.browserWsEndpoint }), connection);
74750
74959
  session.versionString = version.Browser;
74751
- if ((await session.refreshTabs()).length === 0) await session.newTab();
74960
+ await session.refreshTabs();
74752
74961
  await session.getActivePageClient().catch(() => void 0);
74753
74962
  return session;
74754
74963
  }
@@ -74800,6 +75009,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74800
75009
  async click(target, options) {
74801
75010
  const pageClient = await this.getActivePageClient();
74802
75011
  const contextId = await this.getActiveUtilityContextId(pageClient);
75012
+ const tabId = await this.getActiveTabId();
74803
75013
  const point = await evaluateCdp(pageClient, "nodeToken" in target ? ACTION_POINT_EVALUATE_SOURCE : ACTION_POINT_BY_SELECTOR_SOURCE, "nodeToken" in target ? { nodeToken: target.nodeToken } : { selector: target.selector }, contextId);
74804
75014
  if (!point.ok || point.x === void 0 || point.y === void 0) {
74805
75015
  const isSelector = "selector" in target;
@@ -74833,7 +75043,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74833
75043
  modifiers: modifiersMask
74834
75044
  });
74835
75045
  await delay$1(options.clickHoldMs);
74836
- await pageClient.Input.dispatchMouseEvent({
75046
+ const releasePromise = pageClient.Input.dispatchMouseEvent({
74837
75047
  type: "mouseReleased",
74838
75048
  x: point.x,
74839
75049
  y: point.y,
@@ -74841,6 +75051,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74841
75051
  clickCount,
74842
75052
  modifiers: modifiersMask
74843
75053
  });
75054
+ await Promise.race([releasePromise, this.waitForDialog(tabId, options.clickHoldMs + 1e3)]);
74844
75055
  }
74845
75056
  }
74846
75057
  async drag(start, end, options) {
@@ -75096,24 +75307,72 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75096
75307
  }
75097
75308
  }
75098
75309
  async handleDialog(accept, promptText) {
75099
- const tabId = await this.getActiveTabId();
75310
+ const tabId = this.dialogTabId();
75100
75311
  if (!this.pageDialogStates.has(tabId)) throw new McpToolError("no_dialog", "No dialog visible.");
75101
- const pageClient = await this.getActivePageClient();
75312
+ const pageClient = this.pageClients.get(tabId) ?? await this.getActivePageClient();
75102
75313
  this.pageDialogStates.delete(tabId);
75103
75314
  await pageClient.Page.handleJavaScriptDialog({
75104
75315
  accept,
75105
75316
  ...promptText !== void 0 ? { promptText } : {}
75106
75317
  });
75107
75318
  }
75319
+ async hasDialog() {
75320
+ return this.pageDialogStates.size > 0;
75321
+ }
75322
+ waitForDialog(tabId, timeoutMs) {
75323
+ if (this.pageDialogStates.has(tabId)) return Promise.resolve();
75324
+ return new Promise((resolve) => {
75325
+ const waiter = { resolve: () => {
75326
+ if (waiter.timer) clearTimeout(waiter.timer);
75327
+ this.removeDialogWaiter(tabId, waiter);
75328
+ resolve();
75329
+ } };
75330
+ waiter.timer = setTimeout(() => waiter.resolve(), timeoutMs);
75331
+ const waiters = this.dialogWaiters.get(tabId) ?? /* @__PURE__ */ new Set();
75332
+ waiters.add(waiter);
75333
+ this.dialogWaiters.set(tabId, waiters);
75334
+ });
75335
+ }
75336
+ resolveDialogWaiters(tabId) {
75337
+ const waiters = this.dialogWaiters.get(tabId);
75338
+ if (!waiters) return;
75339
+ this.dialogWaiters.delete(tabId);
75340
+ for (const waiter of waiters) waiter.resolve();
75341
+ }
75342
+ removeDialogWaiter(tabId, waiter) {
75343
+ const waiters = this.dialogWaiters.get(tabId);
75344
+ if (!waiters) return;
75345
+ waiters.delete(waiter);
75346
+ if (waiters.size === 0) this.dialogWaiters.delete(tabId);
75347
+ }
75108
75348
  async networkRequests() {
75109
75349
  const tabId = await this.getActiveTabId();
75350
+ await this.hydratePerformanceResourceRequests(tabId);
75110
75351
  return this.ensureNetworkState(tabId).requests.map(cloneNetworkRequest);
75111
75352
  }
75112
75353
  async networkRequest(index) {
75113
75354
  const tabId = await this.getActiveTabId();
75355
+ await this.hydratePerformanceResourceRequests(tabId);
75114
75356
  const request = this.ensureNetworkState(tabId).requests[index - 1];
75115
75357
  return request ? cloneNetworkRequest(request) : void 0;
75116
75358
  }
75359
+ async fetchResponseBody(index) {
75360
+ const tabId = await this.getActiveTabId();
75361
+ const state = this.ensureNetworkState(tabId);
75362
+ const request = state.requests[index - 1];
75363
+ if (!request || !request.requestId) return request?.responseBody;
75364
+ if (!canReadResponseBody(request)) return;
75365
+ if (request.responseBody !== void 0) return request.responseBody;
75366
+ await waitForLoadingDone(state, request.requestId, 5e3).catch(() => void 0);
75367
+ if (request.responseBody !== void 0) return request.responseBody;
75368
+ if (state.bodyRead.has(request.requestId)) return;
75369
+ state.bodyRead.add(request.requestId);
75370
+ const clientNetwork = (this.pageClients.get(tabId) ?? await this.getActivePageClient()).Network;
75371
+ if (!clientNetwork) return;
75372
+ const body = await clientNetwork.getResponseBody({ requestId: request.requestId }).catch(() => void 0);
75373
+ if (body) request.responseBody = body.base64Encoded ? Buffer.from(body.body, "base64").toString("utf8") : body.body;
75374
+ return request.responseBody;
75375
+ }
75117
75376
  async runCodeUnsafe(code) {
75118
75377
  const pageClient = await this.getActivePageClient();
75119
75378
  const contextId = await this.getActiveUtilityContextId(pageClient);
@@ -75173,6 +75432,12 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75173
75432
  if (!activeTab) throw new McpToolError("no_active_tab", "No active tab is available.");
75174
75433
  return activeTab.id;
75175
75434
  }
75435
+ dialogTabId() {
75436
+ if (this.activeTabId && this.pageDialogStates.has(this.activeTabId)) return this.activeTabId;
75437
+ const tabId = this.pageDialogStates.keys().next().value;
75438
+ if (!tabId) throw new McpToolError("no_dialog", "No dialog visible.");
75439
+ return tabId;
75440
+ }
75176
75441
  async getActivePageClient() {
75177
75442
  const activeTab = (await this.refreshTabs()).find((tab) => tab.active);
75178
75443
  if (!activeTab) throw new McpToolError("no_active_tab", "No active tab is available.");
@@ -75254,6 +75519,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75254
75519
  ...event.defaultPrompt !== void 0 ? { defaultPrompt: event.defaultPrompt } : {},
75255
75520
  ...event.url !== void 0 ? { url: event.url } : {}
75256
75521
  });
75522
+ this.resolveDialogWaiters(tabId);
75257
75523
  });
75258
75524
  this.installNetworkCollection(tabId, client);
75259
75525
  }
@@ -75292,20 +75558,29 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75292
75558
  });
75293
75559
  client.Network.loadingFinished(async (event) => {
75294
75560
  const request = state.byRequestId.get(event.requestId);
75295
- if (!request) return;
75561
+ if (!request) {
75562
+ resolveLoadingDone(state, event.requestId, true);
75563
+ return;
75564
+ }
75296
75565
  const startedAt = state.startedAt.get(event.requestId);
75297
75566
  if (startedAt !== void 0 && event.timestamp !== void 0) request.durationMs = Math.round(event.timestamp * 1e3 - startedAt);
75298
- if (canReadResponseBody(request)) {
75567
+ if (canReadResponseBody(request) && !state.bodyRead.has(event.requestId)) {
75568
+ state.bodyRead.add(event.requestId);
75299
75569
  const body = await client.Network?.getResponseBody({ requestId: event.requestId }).catch(() => void 0);
75300
75570
  if (body) request.responseBody = body.base64Encoded ? Buffer.from(body.body, "base64").toString("utf8") : body.body;
75301
75571
  }
75572
+ resolveLoadingDone(state, event.requestId, true);
75302
75573
  });
75303
75574
  client.Network.loadingFailed((event) => {
75304
75575
  const request = state.byRequestId.get(event.requestId);
75305
- if (!request) return;
75576
+ if (!request) {
75577
+ resolveLoadingDone(state, event.requestId, false);
75578
+ return;
75579
+ }
75306
75580
  request.failureText = event.errorText ?? "Unknown error";
75307
75581
  const startedAt = state.startedAt.get(event.requestId);
75308
75582
  if (startedAt !== void 0 && event.timestamp !== void 0) request.durationMs = Math.round(event.timestamp * 1e3 - startedAt);
75583
+ resolveLoadingDone(state, event.requestId, false);
75309
75584
  });
75310
75585
  }
75311
75586
  ensureConsoleState(tabId) {
@@ -75327,7 +75602,10 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75327
75602
  state = {
75328
75603
  requests: [],
75329
75604
  byRequestId: /* @__PURE__ */ new Map(),
75330
- startedAt: /* @__PURE__ */ new Map()
75605
+ startedAt: /* @__PURE__ */ new Map(),
75606
+ hydratedPerformanceResources: false,
75607
+ loadingDone: /* @__PURE__ */ new Map(),
75608
+ bodyRead: /* @__PURE__ */ new Set()
75331
75609
  };
75332
75610
  this.pageNetworkStates.set(tabId, state);
75333
75611
  }
@@ -75343,10 +75621,79 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75343
75621
  this.pageNetworkStates.set(tabId, {
75344
75622
  requests: [],
75345
75623
  byRequestId: /* @__PURE__ */ new Map(),
75346
- startedAt: /* @__PURE__ */ new Map()
75624
+ startedAt: /* @__PURE__ */ new Map(),
75625
+ hydratedPerformanceResources: false,
75626
+ loadingDone: /* @__PURE__ */ new Map(),
75627
+ bodyRead: /* @__PURE__ */ new Set()
75347
75628
  });
75348
75629
  this.pageDialogStates.delete(tabId);
75349
75630
  }
75631
+ async hydratePerformanceResourceRequests(tabId) {
75632
+ const state = this.ensureNetworkState(tabId);
75633
+ if (state.hydratedPerformanceResources) return;
75634
+ state.hydratedPerformanceResources = true;
75635
+ const pageClient = await this.getActivePageClient();
75636
+ const contextId = await this.getActiveUtilityContextId(pageClient);
75637
+ const documentRequest = await evaluateCdp(pageClient, String.raw`() => {
75638
+ const navigation = performance.getEntriesByType("navigation")[0];
75639
+ return {
75640
+ url: String(location.href || ""),
75641
+ duration: navigation ? Math.round(Number(navigation.duration || 0)) : undefined
75642
+ };
75643
+ }`, void 0, contextId).catch(() => void 0);
75644
+ if (documentRequest?.url && !Array.from(state.byRequestId.values()).some((request) => request.url === documentRequest.url)) {
75645
+ const requestId = `performance:navigation:${documentRequest.url}`;
75646
+ const request = {
75647
+ index: state.requests.length + 1,
75648
+ requestId,
75649
+ method: "GET",
75650
+ url: documentRequest.url,
75651
+ resourceType: "document",
75652
+ requestHeaders: {},
75653
+ status: 200,
75654
+ statusText: "OK",
75655
+ ...documentRequest.duration !== void 0 ? { durationMs: documentRequest.duration } : {}
75656
+ };
75657
+ state.requests.push(request);
75658
+ state.byRequestId.set(requestId, request);
75659
+ }
75660
+ const resources = await evaluateCdp(pageClient, String.raw`() => performance.getEntriesByType("resource").map((entry) => ({
75661
+ name: String(entry.name || ""),
75662
+ initiatorType: String(entry.initiatorType || "other"),
75663
+ duration: Math.round(Number(entry.duration || 0)),
75664
+ responseStatus: typeof entry.responseStatus === "number" ? entry.responseStatus : undefined
75665
+ }))`, void 0, contextId).catch(() => []);
75666
+ for (const resource of resources) {
75667
+ if (!resource.name || Array.from(state.byRequestId.values()).some((request) => request.url === resource.name)) continue;
75668
+ const status = resource.responseStatus && resource.responseStatus > 0 ? resource.responseStatus : await this.probeResourceStatus(pageClient, contextId, resource.name);
75669
+ const requestId = `performance:${resource.name}`;
75670
+ const request = {
75671
+ index: state.requests.length + 1,
75672
+ requestId,
75673
+ method: "GET",
75674
+ url: resource.name,
75675
+ resourceType: normalizeResourceType(resource.initiatorType),
75676
+ requestHeaders: {},
75677
+ ...status !== void 0 ? {
75678
+ status,
75679
+ statusText: statusTextForStatus(status)
75680
+ } : {},
75681
+ ...resource.duration !== void 0 ? { durationMs: resource.duration } : {}
75682
+ };
75683
+ state.requests.push(request);
75684
+ state.byRequestId.set(requestId, request);
75685
+ }
75686
+ }
75687
+ async probeResourceStatus(pageClient, contextId, url) {
75688
+ return evaluateCdp(pageClient, String.raw`async (url) => {
75689
+ try {
75690
+ const response = await fetch(url, { method: "HEAD", cache: "no-store" });
75691
+ return response.status;
75692
+ } catch {
75693
+ return undefined;
75694
+ }
75695
+ }`, url, contextId).catch(() => void 0);
75696
+ }
75350
75697
  addConsoleMessage(tabId, message) {
75351
75698
  const state = this.ensureConsoleState(tabId);
75352
75699
  if (!shouldIncludeConsoleMessage(message.type)) return;
@@ -75480,6 +75827,45 @@ function canReadResponseBody(request) {
75480
75827
  if (request.failureText || request.status === void 0) return false;
75481
75828
  return request.status !== 204 && request.status !== 304 && !(request.status >= 100 && request.status < 200);
75482
75829
  }
75830
+ function loadingDoneEntry(state, requestId) {
75831
+ let entry = state.loadingDone.get(requestId);
75832
+ if (!entry) {
75833
+ let resolve;
75834
+ let reject;
75835
+ entry = {
75836
+ promise: new Promise((res, rej) => {
75837
+ resolve = res;
75838
+ reject = rej;
75839
+ }),
75840
+ resolve,
75841
+ reject
75842
+ };
75843
+ state.loadingDone.set(requestId, entry);
75844
+ }
75845
+ return entry;
75846
+ }
75847
+ function resolveLoadingDone(state, requestId, success) {
75848
+ const entry = state.loadingDone.get(requestId);
75849
+ if (!entry) return;
75850
+ state.loadingDone.delete(requestId);
75851
+ if (success) entry.resolve();
75852
+ else entry.reject(/* @__PURE__ */ new Error("Request failed before the response body was available."));
75853
+ }
75854
+ async function waitForLoadingDone(state, requestId, timeoutMs) {
75855
+ const entry = loadingDoneEntry(state, requestId);
75856
+ await Promise.race([entry.promise, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
75857
+ }
75858
+ function statusTextForStatus(status) {
75859
+ if (status === 200) return "OK";
75860
+ if (status === 204) return "No Content";
75861
+ if (status === 304) return "Not Modified";
75862
+ if (status === 400) return "Bad Request";
75863
+ if (status === 401) return "Unauthorized";
75864
+ if (status === 403) return "Forbidden";
75865
+ if (status === 404) return "Not Found";
75866
+ if (status === 500) return "Internal Server Error";
75867
+ return "";
75868
+ }
75483
75869
  function cloneNetworkRequest(request) {
75484
75870
  return {
75485
75871
  ...request,
@@ -75558,6 +75944,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75558
75944
  pageConsoleStates = /* @__PURE__ */ new Map();
75559
75945
  pageNetworkStates = /* @__PURE__ */ new Map();
75560
75946
  pageDialogStates = /* @__PURE__ */ new Map();
75947
+ dialogWaiters = /* @__PURE__ */ new Map();
75561
75948
  bidiListeners = /* @__PURE__ */ new Map();
75562
75949
  responseDataCollector;
75563
75950
  activeTabId;
@@ -75571,12 +75958,12 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75571
75958
  if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") throw new McpToolError("unsupported_protocol_input", `BiDi endpoint must be a ws(s) URL. Received "${parsed.protocol}".`);
75572
75959
  const client = await getBidiClientFactory()({
75573
75960
  browserName: "firefox",
75574
- webSocketUrl: normalizeFirefoxBidiEndpoint(args.endpoint)
75961
+ webSocketUrl: normalizeFirefoxBidiEndpoint(args.endpoint, args.sessionId)
75575
75962
  });
75576
75963
  const session = new BidiConnectedBrowserSession(client);
75577
- session.ownsSession = await ensureMcpBiDiSession(client, args.endpoint);
75964
+ session.ownsSession = await ensureMcpBiDiSession(client, args.endpoint, args.sessionId);
75578
75965
  await session.initialize();
75579
- if ((await session.refreshTabs()).length === 0) await session.newTab();
75966
+ await session.refreshTabs();
75580
75967
  return session;
75581
75968
  }
75582
75969
  async version() {
@@ -75619,10 +76006,13 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75619
76006
  }
75620
76007
  async snapshot(request = {}) {
75621
76008
  const tabId = await this.getActiveTabId();
75622
- return toBrowserSnapshot(await evaluateBiDi(this.client, tabId, PLAYWRIGHT_ARIA_SNAPSHOT_EVALUATE_SOURCE, toAriaSnapshotPayload(request)), request, {
75623
- console: this.consoleSummary(tabId),
75624
- consoleLink: await this.takeConsoleLink(tabId)
75625
- });
76009
+ return {
76010
+ ...toBrowserSnapshot(await retryUntilReady(() => evaluateBiDi(this.client, tabId, PLAYWRIGHT_ARIA_SNAPSHOT_EVALUATE_SOURCE, toAriaSnapshotPayload(request))), request, {
76011
+ console: this.consoleSummary(tabId),
76012
+ consoleLink: await this.takeConsoleLink(tabId)
76013
+ }),
76014
+ retryable: true
76015
+ };
75626
76016
  }
75627
76017
  async consoleMessages(level = "info", all = false) {
75628
76018
  const activeTabId = await this.getActiveTabId();
@@ -75725,10 +76115,12 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75725
76115
  parameters: { pointerType: "mouse" },
75726
76116
  actions: pointerActions
75727
76117
  });
75728
- await this.client.inputPerformActions({
76118
+ const performPromise = this.client.inputPerformActions({
75729
76119
  context: tabId,
75730
76120
  actions
75731
76121
  });
76122
+ await Promise.race([performPromise, this.waitForDialog(tabId, options.clickHoldMs + 5e3)]);
76123
+ performPromise.catch(() => {});
75732
76124
  await this.client.inputReleaseActions({ context: tabId }).catch(() => {});
75733
76125
  }
75734
76126
  async drag(start, end, options) {
@@ -76029,14 +76421,17 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76029
76421
  }
76030
76422
  }
76031
76423
  async handleDialog(accept, promptText) {
76032
- const tabId = await this.getActiveTabId();
76424
+ const tabId = this.dialogTabId();
76033
76425
  if (!this.pageDialogStates.has(tabId)) throw new McpToolError("no_dialog", "No dialog visible.");
76034
76426
  this.pageDialogStates.delete(tabId);
76035
- await this.client.browsingContextHandleUserPrompt({
76427
+ await withBiDiTimeout(this.client.browsingContextHandleUserPrompt({
76036
76428
  context: tabId,
76037
76429
  accept,
76038
76430
  ...promptText !== void 0 ? { userText: promptText } : {}
76039
- });
76431
+ }), 5e3);
76432
+ }
76433
+ async hasDialog() {
76434
+ return this.pageDialogStates.size > 0;
76040
76435
  }
76041
76436
  async networkRequests() {
76042
76437
  const tabId = await this.getActiveTabId();
@@ -76047,6 +76442,15 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76047
76442
  const request = this.ensureNetworkState(tabId).requests[index - 1];
76048
76443
  return request ? cloneNetworkRequest(request) : void 0;
76049
76444
  }
76445
+ async fetchResponseBody(index) {
76446
+ const tabId = await this.getActiveTabId();
76447
+ const request = this.ensureNetworkState(tabId).requests[index - 1];
76448
+ if (!request || !request.requestId) return request?.responseBody;
76449
+ if (request.responseBody !== void 0) return request.responseBody;
76450
+ const body = await this.getResponseBody(request.requestId).catch(() => void 0);
76451
+ if (body !== void 0) request.responseBody = body;
76452
+ return request.responseBody;
76453
+ }
76050
76454
  async runCodeUnsafe(code) {
76051
76455
  return this.evaluate(`async () => {
76052
76456
  const fn = eval(${JSON.stringify(`(${code})`)});
@@ -76062,6 +76466,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76062
76466
  }`);
76063
76467
  }
76064
76468
  async initialize() {
76469
+ this.attachBiDiListeners();
76065
76470
  await this.client.sessionSubscribe({ events: [
76066
76471
  "browsingContext.userPromptOpened",
76067
76472
  "log.entryAdded",
@@ -76075,7 +76480,6 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76075
76480
  maxEncodedDataSize: 1e7
76076
76481
  }).catch(() => void 0);
76077
76482
  this.responseDataCollector = collectorResult?.collector;
76078
- this.attachBiDiListeners();
76079
76483
  }
76080
76484
  attachBiDiListeners() {
76081
76485
  this.attachBiDiListener("log.entryAdded", (payload) => this.handleLogEntry(payload));
@@ -76110,6 +76514,45 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76110
76514
  targetArg(target) {
76111
76515
  return "nodeToken" in target ? { nodeToken: target.nodeToken } : { selector: target.selector };
76112
76516
  }
76517
+ dialogTabId() {
76518
+ if (this.activeTabId && this.pageDialogStates.has(this.activeTabId)) return this.activeTabId;
76519
+ const tabId = this.pageDialogStates.keys().next().value;
76520
+ if (!tabId) throw new McpToolError("no_dialog", "No dialog visible.");
76521
+ return tabId;
76522
+ }
76523
+ waitForDialog(tabId, timeoutMs) {
76524
+ if (this.pageDialogStates.has(tabId)) return Promise.resolve();
76525
+ return new Promise((resolve, reject) => {
76526
+ const waiter = {
76527
+ resolve: () => {
76528
+ if (waiter.timer) clearTimeout(waiter.timer);
76529
+ this.removeDialogWaiter(tabId, waiter);
76530
+ resolve();
76531
+ },
76532
+ reject: (error) => {
76533
+ if (waiter.timer) clearTimeout(waiter.timer);
76534
+ this.removeDialogWaiter(tabId, waiter);
76535
+ reject(error);
76536
+ }
76537
+ };
76538
+ waiter.timer = setTimeout(() => waiter.reject?.(/* @__PURE__ */ new Error("Timed out waiting for dialog.")), timeoutMs);
76539
+ const waiters = this.dialogWaiters.get(tabId) ?? /* @__PURE__ */ new Set();
76540
+ waiters.add(waiter);
76541
+ this.dialogWaiters.set(tabId, waiters);
76542
+ });
76543
+ }
76544
+ resolveDialogWaiters(tabId) {
76545
+ const waiters = this.dialogWaiters.get(tabId);
76546
+ if (!waiters) return;
76547
+ this.dialogWaiters.delete(tabId);
76548
+ for (const waiter of waiters) waiter.resolve();
76549
+ }
76550
+ removeDialogWaiter(tabId, waiter) {
76551
+ const waiters = this.dialogWaiters.get(tabId);
76552
+ if (!waiters) return;
76553
+ waiters.delete(waiter);
76554
+ if (waiters.size === 0) this.dialogWaiters.delete(tabId);
76555
+ }
76113
76556
  async actionPoint(tabId, target) {
76114
76557
  const source = "nodeToken" in target ? ACTION_POINT_EVALUATE_SOURCE : ACTION_POINT_BY_SELECTOR_SOURCE;
76115
76558
  const point = await evaluateBiDi(this.client, tabId, source, this.targetArg(target));
@@ -76166,6 +76609,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76166
76609
  type: event.type ?? "alert",
76167
76610
  ...event.defaultValue !== void 0 ? { defaultPrompt: event.defaultValue } : {}
76168
76611
  });
76612
+ this.resolveDialogWaiters(event.context);
76169
76613
  }
76170
76614
  handleBeforeRequestSent(payload) {
76171
76615
  const event = parseBidiNetworkEvent(payload);
@@ -76268,7 +76712,10 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76268
76712
  state = {
76269
76713
  requests: [],
76270
76714
  byRequestId: /* @__PURE__ */ new Map(),
76271
- startedAt: /* @__PURE__ */ new Map()
76715
+ startedAt: /* @__PURE__ */ new Map(),
76716
+ hydratedPerformanceResources: false,
76717
+ loadingDone: /* @__PURE__ */ new Map(),
76718
+ bodyRead: /* @__PURE__ */ new Set()
76272
76719
  };
76273
76720
  this.pageNetworkStates.set(tabId, state);
76274
76721
  }
@@ -76284,7 +76731,10 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76284
76731
  this.pageNetworkStates.set(tabId, {
76285
76732
  requests: [],
76286
76733
  byRequestId: /* @__PURE__ */ new Map(),
76287
- startedAt: /* @__PURE__ */ new Map()
76734
+ startedAt: /* @__PURE__ */ new Map(),
76735
+ hydratedPerformanceResources: false,
76736
+ loadingDone: /* @__PURE__ */ new Map(),
76737
+ bodyRead: /* @__PURE__ */ new Set()
76288
76738
  });
76289
76739
  this.pageDialogStates.delete(tabId);
76290
76740
  }
@@ -76322,14 +76772,18 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76322
76772
  return `${path.relative(process.cwd(), state.logFile)}${fromLine === state.logLine ? `#L${fromLine}` : `#L${fromLine}-L${state.logLine}`}`;
76323
76773
  }
76324
76774
  };
76325
- function normalizeFirefoxBidiEndpoint(endpoint) {
76775
+ function normalizeFirefoxBidiEndpoint(endpoint, sessionId) {
76326
76776
  const url = new URL(endpoint);
76777
+ if (sessionId) {
76778
+ url.pathname = `/session/${sessionId}`;
76779
+ return url.toString();
76780
+ }
76327
76781
  if (url.pathname === "/" || url.pathname === "") url.pathname = "/session";
76328
76782
  return url.toString();
76329
76783
  }
76330
- async function ensureMcpBiDiSession(client, endpoint) {
76784
+ async function ensureMcpBiDiSession(client, endpoint, sessionId) {
76331
76785
  await client.sessionStatus({});
76332
- if (isSessionSpecificFirefoxBidiEndpoint(endpoint)) return false;
76786
+ if (sessionId || isSessionSpecificFirefoxBidiEndpoint(endpoint)) return false;
76333
76787
  try {
76334
76788
  await client.browsingContextGetTree({});
76335
76789
  return false;
@@ -76414,6 +76868,7 @@ var McpRuntime = class {
76414
76868
  this.invalidateSnapshot();
76415
76869
  this.pendingFileUploadTarget = void 0;
76416
76870
  this.tabs = await session.newTab(url);
76871
+ if (this.snapshotMode === "none") return { tabs: this.tabs };
76417
76872
  const snapshot = this.tabs.some((tab) => tab.active) ? await this.snapshot() : void 0;
76418
76873
  return snapshot ? {
76419
76874
  tabs: this.tabs,
@@ -76427,6 +76882,7 @@ var McpRuntime = class {
76427
76882
  this.invalidateSnapshot();
76428
76883
  this.pendingFileUploadTarget = void 0;
76429
76884
  this.tabs = await session.selectTab(tab.id);
76885
+ if (this.snapshotMode === "none") return { tabs: this.tabs };
76430
76886
  const snapshot = await this.snapshot();
76431
76887
  return {
76432
76888
  tabs: this.tabs,
@@ -76440,6 +76896,7 @@ var McpRuntime = class {
76440
76896
  this.invalidateSnapshot();
76441
76897
  this.pendingFileUploadTarget = void 0;
76442
76898
  this.tabs = await session.closeTab(tab.id);
76899
+ if (this.snapshotMode === "none") return { tabs: this.tabs };
76443
76900
  const snapshot = this.tabs.some((candidate) => candidate.active) ? await this.snapshot() : void 0;
76444
76901
  return snapshot ? {
76445
76902
  tabs: this.tabs,
@@ -76448,26 +76905,49 @@ var McpRuntime = class {
76448
76905
  }
76449
76906
  async snapshot(args = {}) {
76450
76907
  const session = this.requireConnected();
76451
- this.tabs = await session.listTabs();
76452
- const activeTab = this.requireActiveTab();
76453
76908
  const requestKey = this.snapshotRequestKey(args);
76454
76909
  const request = {
76455
76910
  ...args.boxes !== void 0 ? { boxes: args.boxes } : {},
76456
76911
  ...args.depth !== void 0 ? { depth: args.depth } : {},
76457
76912
  ...args.target ? { target: this.resolveSnapshotTarget(args.target) } : {}
76458
76913
  };
76459
- const snapshot = await session.snapshot(request);
76914
+ const { activeTab, currentActiveTab, snapshot } = await this.captureStableSnapshot(session, request);
76460
76915
  this.snapshotCache = {
76461
- tabId: activeTab.id,
76916
+ tabId: currentActiveTab.id,
76462
76917
  requestKey,
76463
76918
  text: snapshot.text,
76464
76919
  refs: { ...snapshot.refs },
76465
- title: snapshot.title,
76466
- url: snapshot.url,
76920
+ title: currentActiveTab.title || snapshot.title,
76921
+ url: currentActiveTab.url || snapshot.url,
76467
76922
  ...snapshot.console ? { console: { ...snapshot.console } } : {},
76468
76923
  ...snapshot.consoleLink ? { consoleLink: snapshot.consoleLink } : {}
76469
76924
  };
76470
- return snapshot;
76925
+ return {
76926
+ ...snapshot,
76927
+ title: currentActiveTab.title || snapshot.title,
76928
+ url: currentActiveTab.url || snapshot.url
76929
+ };
76930
+ }
76931
+ async captureStableSnapshot(session, request) {
76932
+ let lastAttempt;
76933
+ for (let attempt = 0; attempt < 4; attempt += 1) {
76934
+ this.tabs = await session.listTabs();
76935
+ const activeTab = this.requireActiveTab();
76936
+ const snapshot = await session.snapshot(request);
76937
+ const refreshedTabs = await session.listTabs();
76938
+ this.tabs = refreshedTabs;
76939
+ const currentActiveTab = refreshedTabs.find((tab) => tab.active) ?? refreshedTabs.find((tab) => tab.id === activeTab.id) ?? activeTab;
76940
+ const captured = {
76941
+ activeTab,
76942
+ currentActiveTab,
76943
+ snapshot
76944
+ };
76945
+ lastAttempt = captured;
76946
+ if (!snapshot.retryable || snapshot.text.trim().length > 0 || currentActiveTab.url === "about:blank") return captured;
76947
+ await delay(150 * (attempt + 1));
76948
+ }
76949
+ if (!lastAttempt) throw new McpToolError("action_failed", "Unable to capture page snapshot.");
76950
+ return lastAttempt;
76471
76951
  }
76472
76952
  async click(target, opts) {
76473
76953
  const session = this.requireConnected();
@@ -76486,6 +76966,7 @@ var McpRuntime = class {
76486
76966
  this.invalidateSnapshot();
76487
76967
  this.pendingFileUploadTarget = opensFileChooser ? resolved : void 0;
76488
76968
  if (this.snapshotMode === "none") return;
76969
+ if (await session.hasDialog()) return;
76489
76970
  return this.snapshot();
76490
76971
  }
76491
76972
  async hover(target) {
@@ -76495,13 +76976,16 @@ var McpRuntime = class {
76495
76976
  this.invalidateSnapshot();
76496
76977
  this.pendingFileUploadTarget = void 0;
76497
76978
  if (this.snapshotMode === "none") return;
76979
+ if (await session.hasDialog()) return;
76498
76980
  return this.snapshot();
76499
76981
  }
76500
76982
  async navigate(url) {
76501
- await this.requireConnected().navigate(normalizeNavigationUrl(url));
76983
+ const session = this.requireConnected();
76984
+ await session.navigate(normalizeNavigationUrl(url));
76502
76985
  this.invalidateSnapshot();
76503
76986
  this.pendingFileUploadTarget = void 0;
76504
76987
  if (this.snapshotMode === "none") return;
76988
+ if (await session.hasDialog()) return;
76505
76989
  return this.snapshot();
76506
76990
  }
76507
76991
  async type(ref, text, opts) {
@@ -76650,6 +77134,9 @@ var McpRuntime = class {
76650
77134
  async networkRequest(index) {
76651
77135
  return this.requireConnected().networkRequest(index);
76652
77136
  }
77137
+ async fetchResponseBody(index) {
77138
+ return this.requireConnected().fetchResponseBody(index);
77139
+ }
76653
77140
  async runCodeUnsafe(code) {
76654
77141
  const result = await this.requireConnected().runCodeUnsafe(code);
76655
77142
  this.invalidateSnapshot();
@@ -76686,6 +77173,10 @@ var McpRuntime = class {
76686
77173
  hasPendingFileUploadTarget() {
76687
77174
  return !!this.pendingFileUploadTarget;
76688
77175
  }
77176
+ async hasDialog() {
77177
+ if (!this.connection) return false;
77178
+ return this.connection.session.hasDialog();
77179
+ }
76689
77180
  async close() {
76690
77181
  this.invalidateSnapshot();
76691
77182
  this.pendingFileUploadTarget = void 0;
@@ -78306,6 +78797,7 @@ async function createRoxyBrowserMcpInMemory(options = {}) {
78306
78797
  await bundle.server.connect(serverTransport);
78307
78798
  return {
78308
78799
  server: bundle.server,
78800
+ runtimeManager: bundle.runtimeManager,
78309
78801
  serverTransport,
78310
78802
  clientTransport,
78311
78803
  close: async () => {