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

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 (43) hide show
  1. package/dist/browser.d.ts +33 -5
  2. package/dist/browser.d.ts.map +1 -1
  3. package/dist/browser.js +127 -15
  4. package/dist/browser.js.map +1 -1
  5. package/dist/browserType.d.ts.map +1 -1
  6. package/dist/browserType.js +2 -1
  7. package/dist/browserType.js.map +1 -1
  8. package/dist/mcp/backend/connect.d.ts +1 -0
  9. package/dist/mcp/backend/connect.d.ts.map +1 -1
  10. package/dist/mcp/backend/connect.js +4 -2
  11. package/dist/mcp/backend/connect.js.map +1 -1
  12. package/dist/mcp/backend/network.d.ts.map +1 -1
  13. package/dist/mcp/backend/network.js +30 -4
  14. package/dist/mcp/backend/network.js.map +1 -1
  15. package/dist/mcp/backend/response.d.ts +2 -0
  16. package/dist/mcp/backend/response.d.ts.map +1 -1
  17. package/dist/mcp/backend/response.js +52 -6
  18. package/dist/mcp/backend/response.js.map +1 -1
  19. package/dist/mcp/connectedBrowser.d.ts.map +1 -1
  20. package/dist/mcp/connectedBrowser.js +355 -25
  21. package/dist/mcp/connectedBrowser.js.map +1 -1
  22. package/dist/mcp/runtime.d.ts +3 -0
  23. package/dist/mcp/runtime.d.ts.map +1 -1
  24. package/dist/mcp/runtime.js +62 -8
  25. package/dist/mcp/runtime.js.map +1 -1
  26. package/dist/mcp/transports/inMemory.d.ts.map +1 -1
  27. package/dist/mcp/transports/inMemory.js +1 -0
  28. package/dist/mcp/transports/inMemory.js.map +1 -1
  29. package/dist/mcp/types.d.ts +5 -0
  30. package/dist/mcp/types.d.ts.map +1 -1
  31. package/dist/page.d.ts +2 -0
  32. package/dist/page.d.ts.map +1 -1
  33. package/dist/page.js +12 -0
  34. package/dist/page.js.map +1 -1
  35. package/dist/protocol/bidi/backend.d.ts +1 -1
  36. package/dist/protocol/bidi/backend.d.ts.map +1 -1
  37. package/dist/protocol/bidi/backend.js +6 -2
  38. package/dist/protocol/bidi/backend.js.map +1 -1
  39. package/dist/roxybrowser.bundle.js +516 -60
  40. package/dist/roxybrowser.bundle.js.map +1 -1
  41. package/dist/types/api.d.ts +21 -2
  42. package/dist/types/api.d.ts.map +1 -1
  43. package/package.json +1 -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 = [];
@@ -53334,7 +53444,9 @@ var RoxyBrowserType = class {
53334
53444
  protocol
53335
53445
  });
53336
53446
  await adapter.connect();
53337
- return new RoxyBrowser(await adapter.browser(), adapter, resolveHumanizationOptions(options.human), options.browserName ?? this.browserName);
53447
+ const session = await adapter.browser();
53448
+ const versionStr = await session.version();
53449
+ return new RoxyBrowser(session, adapter, resolveHumanizationOptions(options.human), options.browserName ?? this.browserName, this, versionStr);
53338
53450
  }
53339
53451
  };
53340
53452
  var chromium = new RoxyBrowserType("chromium", {
@@ -73317,6 +73429,7 @@ var Response$1 = class {
73317
73429
  includeSnapshot = "none";
73318
73430
  fullSnapshot;
73319
73431
  isClose = false;
73432
+ rawResults = false;
73320
73433
  constructor(context, toolName, toolArgs) {
73321
73434
  this.context = context;
73322
73435
  this.toolName = toolName;
@@ -73340,6 +73453,9 @@ var Response$1 = class {
73340
73453
  setClose() {
73341
73454
  this.isClose = true;
73342
73455
  }
73456
+ setRawResults() {
73457
+ this.rawResults = true;
73458
+ }
73343
73459
  setIncludeSnapshot() {
73344
73460
  this.includeSnapshot = this.context.config.snapshot?.mode ?? "full";
73345
73461
  }
@@ -73355,6 +73471,21 @@ var Response$1 = class {
73355
73471
  async serialize() {
73356
73472
  const sections = [];
73357
73473
  if (this.errors.length) sections.push("### Error", ...this.errors);
73474
+ if (this.rawResults) {
73475
+ if (this.results.length) sections.push(...this.results);
73476
+ return {
73477
+ content: [{
73478
+ type: "text",
73479
+ text: sections.join("\n")
73480
+ }, ...this.images.map((image) => ({
73481
+ type: "image",
73482
+ data: image.data,
73483
+ mimeType: image.mimeType
73484
+ }))],
73485
+ ...this.isClose ? { isClose: true } : {},
73486
+ ...this.errors.length ? { isError: true } : {}
73487
+ };
73488
+ }
73358
73489
  if (this.results.length) {
73359
73490
  if (sections.length) sections.push("");
73360
73491
  sections.push("### Result", ...this.results);
@@ -73364,16 +73495,23 @@ var Response$1 = class {
73364
73495
  sections.push("### Code", "```js", ...this.code, "```");
73365
73496
  }
73366
73497
  if (this.includeSnapshot === "full") {
73367
- const snapshot = await this.context.runtime.snapshot();
73368
- if (sections.length) sections.push("");
73369
- sections.push(formatSnapshot(snapshot));
73498
+ if (!await this.context.runtime.hasDialog()) {
73499
+ const snapshot = await reconcileSnapshotWithTabs(this.context, await this.context.runtime.snapshot());
73500
+ if (sections.length) sections.push("");
73501
+ sections.push(formatSnapshot(snapshot));
73502
+ }
73370
73503
  }
73371
73504
  if (this.fullSnapshot) {
73372
- const snapshot = await this.context.runtime.snapshot({
73505
+ let snapshot = await reconcileSnapshotWithTabs(this.context, await this.context.runtime.snapshot({
73373
73506
  ...this.fullSnapshot.target !== void 0 ? { target: this.fullSnapshot.target } : {},
73374
73507
  ...this.fullSnapshot.depth !== void 0 ? { depth: this.fullSnapshot.depth } : {},
73375
73508
  ...this.fullSnapshot.boxes !== void 0 ? { boxes: this.fullSnapshot.boxes } : {}
73376
- });
73509
+ }));
73510
+ 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({
73511
+ ...this.fullSnapshot.target !== void 0 ? { target: this.fullSnapshot.target } : {},
73512
+ ...this.fullSnapshot.depth !== void 0 ? { depth: this.fullSnapshot.depth } : {},
73513
+ ...this.fullSnapshot.boxes !== void 0 ? { boxes: this.fullSnapshot.boxes } : {}
73514
+ }));
73377
73515
  if (this.fullSnapshot.filename) {
73378
73516
  const resolvedFilename = await this.context.resolveOutputFile(this.fullSnapshot.filename);
73379
73517
  await writeFile(resolvedFilename, snapshot.text);
@@ -73400,6 +73538,15 @@ var Response$1 = class {
73400
73538
  };
73401
73539
  }
73402
73540
  };
73541
+ async function reconcileSnapshotWithTabs(context, snapshot) {
73542
+ const activeTab = (await context.runtime.listTabs()).find((tab) => tab.active);
73543
+ if (!activeTab) return snapshot;
73544
+ return {
73545
+ ...snapshot,
73546
+ title: activeTab.title || snapshot.title,
73547
+ url: activeTab.url || snapshot.url
73548
+ };
73549
+ }
73403
73550
  //#endregion
73404
73551
  //#region src/mcp/backend/tool.ts
73405
73552
  function defineTool$1(tool) {
@@ -73498,7 +73645,8 @@ var connect_default = [defineTool$1({
73498
73645
  description: "Attach to an existing browser and seed the active tab snapshot.",
73499
73646
  inputSchema: object({
73500
73647
  endpoint: string().min(1),
73501
- browser: _enum(["chrome", "firefox"]).default("chrome")
73648
+ browser: _enum(["chrome", "firefox"]).default("chrome"),
73649
+ sessionId: string().min(1).optional()
73502
73650
  }),
73503
73651
  type: "action"
73504
73652
  },
@@ -73507,7 +73655,8 @@ var connect_default = [defineTool$1({
73507
73655
  const result = await context.runtime.connect({
73508
73656
  protocol,
73509
73657
  endpoint: params.endpoint,
73510
- browser: params.browser === "chrome" ? "chromium" : params.browser
73658
+ browser: params.browser === "chrome" ? "chromium" : params.browser,
73659
+ ...params.sessionId ? { sessionId: params.sessionId } : {}
73511
73660
  });
73512
73661
  response.addTextResult(formatConnectResult({
73513
73662
  ...result,
@@ -73952,7 +74101,17 @@ var networkRequest = defineTool$1({
73952
74101
  response.addError(`Request #${args.index} not found. Use browser_network_requests to see available indexes.`);
73953
74102
  return;
73954
74103
  }
73955
- const text = args.part ? renderRequestPart(request, args.part) : renderRequestDetails(request);
74104
+ if (args.part) {
74105
+ response.setRawResults();
74106
+ const partText = args.part === "response-body" ? await context.runtime.fetchResponseBody(args.index) ?? "" : renderRequestPart(request, args.part);
74107
+ if (args.filename) {
74108
+ const resolvedFilename = await context.resolveOutputFile(args.filename);
74109
+ await writeFile(resolvedFilename, partText);
74110
+ response.addTextResult(`Saved network request to "${resolvedFilename}".`);
74111
+ } else response.addTextResult(partText);
74112
+ return;
74113
+ }
74114
+ const text = renderRequestDetails(request);
73956
74115
  if (args.filename) {
73957
74116
  const resolvedFilename = await context.resolveOutputFile(args.filename);
73958
74117
  await writeFile(resolvedFilename, text);
@@ -73986,8 +74145,10 @@ function renderRequestDetails(request) {
73986
74145
  if (request.mimeType) lines.push(` mimeType: ${request.mimeType}`);
73987
74146
  appendHeaders(lines, "Request headers", request.requestHeaders);
73988
74147
  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.`);
74148
+ const hints = [];
74149
+ if (request.requestBody) hints.push(`Call browser_network_request with part="request-body" to read the request body.`);
74150
+ if (canHaveResponseBody(request)) hints.push(`Call browser_network_request with part="response-body" to read the response body.`);
74151
+ if (hints.length) lines.push("", ...hints);
73991
74152
  return lines.join("\n");
73992
74153
  }
73993
74154
  function renderRequestPart(request, part) {
@@ -74006,6 +74167,10 @@ function appendHeaders(lines, title, headers) {
74006
74167
  function renderHeaders(headers) {
74007
74168
  return Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join("\n");
74008
74169
  }
74170
+ function canHaveResponseBody(request) {
74171
+ if (request.failureText || request.status === void 0) return false;
74172
+ return request.status !== 204 && request.status !== 304 && !(request.status >= 100 && request.status < 200);
74173
+ }
74009
74174
  var network_default = [networkRequests, networkRequest];
74010
74175
  var runCode_default = [defineTool$1({
74011
74176
  capability: "devtools",
@@ -74666,13 +74831,13 @@ var CDP_KEY_MAP = {
74666
74831
  async function evaluateBiDi(client, contextId, functionSource, arg) {
74667
74832
  const expression = arg === void 0 ? `(${functionSource})()` : `(${functionSource})(${JSON.stringify(arg)})`;
74668
74833
  const response = await client.scriptEvaluate({
74669
- expression,
74834
+ expression: wrapWithSerializedEvaluationResult(expression),
74670
74835
  target: { context: contextId },
74671
74836
  awaitPromise: true,
74672
74837
  resultOwnership: "none"
74673
74838
  });
74674
74839
  if (response.type === "exception") throw new Error(response.exceptionDetails?.text || "BiDi runtime evaluation failed.");
74675
- return response.result?.value;
74840
+ return parseSerializedEvaluationResult(extractBiDiValue(response.result));
74676
74841
  }
74677
74842
  async function evaluateBiDiRef(client, contextId, functionSource, arg) {
74678
74843
  const expression = arg === void 0 ? `(${functionSource})()` : `(${functionSource})(${JSON.stringify(arg)})`;
@@ -74694,6 +74859,16 @@ async function evaluateBiDiRef(client, contextId, functionSource, arg) {
74694
74859
  ...response.result?.value?.handle !== void 0 ? { handle: response.result.value.handle } : {}
74695
74860
  };
74696
74861
  }
74862
+ function extractBiDiValue(value) {
74863
+ if (!value) return;
74864
+ if (value.type === "array" && Array.isArray(value.value)) return value.value.map((entry) => extractBiDiValue(entry));
74865
+ if (value.type === "object" && Array.isArray(value.value)) {
74866
+ const obj = {};
74867
+ for (const [key, val] of value.value) obj[key] = extractBiDiValue(val);
74868
+ return obj;
74869
+ }
74870
+ return value.value;
74871
+ }
74697
74872
  function toAriaSnapshotPayload(request = {}) {
74698
74873
  return {
74699
74874
  options: normalizeAriaSnapshotOptions({
@@ -74733,6 +74908,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74733
74908
  pageConsoleStates = /* @__PURE__ */ new Map();
74734
74909
  pageNetworkStates = /* @__PURE__ */ new Map();
74735
74910
  pageDialogStates = /* @__PURE__ */ new Map();
74911
+ dialogWaiters = /* @__PURE__ */ new Map();
74736
74912
  activeTabId;
74737
74913
  versionString = "Chromium/unknown";
74738
74914
  constructor(browserClient, connection) {
@@ -74748,7 +74924,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74748
74924
  });
74749
74925
  const session = new CdpConnectedBrowserSession(await chromeRemoteInterface({ target: connection.browserWsEndpoint }), connection);
74750
74926
  session.versionString = version.Browser;
74751
- if ((await session.refreshTabs()).length === 0) await session.newTab();
74927
+ await session.refreshTabs();
74752
74928
  await session.getActivePageClient().catch(() => void 0);
74753
74929
  return session;
74754
74930
  }
@@ -74800,6 +74976,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74800
74976
  async click(target, options) {
74801
74977
  const pageClient = await this.getActivePageClient();
74802
74978
  const contextId = await this.getActiveUtilityContextId(pageClient);
74979
+ const tabId = await this.getActiveTabId();
74803
74980
  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
74981
  if (!point.ok || point.x === void 0 || point.y === void 0) {
74805
74982
  const isSelector = "selector" in target;
@@ -74833,7 +75010,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74833
75010
  modifiers: modifiersMask
74834
75011
  });
74835
75012
  await delay$1(options.clickHoldMs);
74836
- await pageClient.Input.dispatchMouseEvent({
75013
+ const releasePromise = pageClient.Input.dispatchMouseEvent({
74837
75014
  type: "mouseReleased",
74838
75015
  x: point.x,
74839
75016
  y: point.y,
@@ -74841,6 +75018,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74841
75018
  clickCount,
74842
75019
  modifiers: modifiersMask
74843
75020
  });
75021
+ await Promise.race([releasePromise, this.waitForDialog(tabId, options.clickHoldMs + 1e3)]);
74844
75022
  }
74845
75023
  }
74846
75024
  async drag(start, end, options) {
@@ -75096,24 +75274,72 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75096
75274
  }
75097
75275
  }
75098
75276
  async handleDialog(accept, promptText) {
75099
- const tabId = await this.getActiveTabId();
75277
+ const tabId = this.dialogTabId();
75100
75278
  if (!this.pageDialogStates.has(tabId)) throw new McpToolError("no_dialog", "No dialog visible.");
75101
- const pageClient = await this.getActivePageClient();
75279
+ const pageClient = this.pageClients.get(tabId) ?? await this.getActivePageClient();
75102
75280
  this.pageDialogStates.delete(tabId);
75103
75281
  await pageClient.Page.handleJavaScriptDialog({
75104
75282
  accept,
75105
75283
  ...promptText !== void 0 ? { promptText } : {}
75106
75284
  });
75107
75285
  }
75286
+ async hasDialog() {
75287
+ return this.pageDialogStates.size > 0;
75288
+ }
75289
+ waitForDialog(tabId, timeoutMs) {
75290
+ if (this.pageDialogStates.has(tabId)) return Promise.resolve();
75291
+ return new Promise((resolve) => {
75292
+ const waiter = { resolve: () => {
75293
+ if (waiter.timer) clearTimeout(waiter.timer);
75294
+ this.removeDialogWaiter(tabId, waiter);
75295
+ resolve();
75296
+ } };
75297
+ waiter.timer = setTimeout(() => waiter.resolve(), timeoutMs);
75298
+ const waiters = this.dialogWaiters.get(tabId) ?? /* @__PURE__ */ new Set();
75299
+ waiters.add(waiter);
75300
+ this.dialogWaiters.set(tabId, waiters);
75301
+ });
75302
+ }
75303
+ resolveDialogWaiters(tabId) {
75304
+ const waiters = this.dialogWaiters.get(tabId);
75305
+ if (!waiters) return;
75306
+ this.dialogWaiters.delete(tabId);
75307
+ for (const waiter of waiters) waiter.resolve();
75308
+ }
75309
+ removeDialogWaiter(tabId, waiter) {
75310
+ const waiters = this.dialogWaiters.get(tabId);
75311
+ if (!waiters) return;
75312
+ waiters.delete(waiter);
75313
+ if (waiters.size === 0) this.dialogWaiters.delete(tabId);
75314
+ }
75108
75315
  async networkRequests() {
75109
75316
  const tabId = await this.getActiveTabId();
75317
+ await this.hydratePerformanceResourceRequests(tabId);
75110
75318
  return this.ensureNetworkState(tabId).requests.map(cloneNetworkRequest);
75111
75319
  }
75112
75320
  async networkRequest(index) {
75113
75321
  const tabId = await this.getActiveTabId();
75322
+ await this.hydratePerformanceResourceRequests(tabId);
75114
75323
  const request = this.ensureNetworkState(tabId).requests[index - 1];
75115
75324
  return request ? cloneNetworkRequest(request) : void 0;
75116
75325
  }
75326
+ async fetchResponseBody(index) {
75327
+ const tabId = await this.getActiveTabId();
75328
+ const state = this.ensureNetworkState(tabId);
75329
+ const request = state.requests[index - 1];
75330
+ if (!request || !request.requestId) return request?.responseBody;
75331
+ if (!canReadResponseBody(request)) return;
75332
+ if (request.responseBody !== void 0) return request.responseBody;
75333
+ await waitForLoadingDone(state, request.requestId, 5e3).catch(() => void 0);
75334
+ if (request.responseBody !== void 0) return request.responseBody;
75335
+ if (state.bodyRead.has(request.requestId)) return;
75336
+ state.bodyRead.add(request.requestId);
75337
+ const clientNetwork = (this.pageClients.get(tabId) ?? await this.getActivePageClient()).Network;
75338
+ if (!clientNetwork) return;
75339
+ const body = await clientNetwork.getResponseBody({ requestId: request.requestId }).catch(() => void 0);
75340
+ if (body) request.responseBody = body.base64Encoded ? Buffer.from(body.body, "base64").toString("utf8") : body.body;
75341
+ return request.responseBody;
75342
+ }
75117
75343
  async runCodeUnsafe(code) {
75118
75344
  const pageClient = await this.getActivePageClient();
75119
75345
  const contextId = await this.getActiveUtilityContextId(pageClient);
@@ -75173,6 +75399,12 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75173
75399
  if (!activeTab) throw new McpToolError("no_active_tab", "No active tab is available.");
75174
75400
  return activeTab.id;
75175
75401
  }
75402
+ dialogTabId() {
75403
+ if (this.activeTabId && this.pageDialogStates.has(this.activeTabId)) return this.activeTabId;
75404
+ const tabId = this.pageDialogStates.keys().next().value;
75405
+ if (!tabId) throw new McpToolError("no_dialog", "No dialog visible.");
75406
+ return tabId;
75407
+ }
75176
75408
  async getActivePageClient() {
75177
75409
  const activeTab = (await this.refreshTabs()).find((tab) => tab.active);
75178
75410
  if (!activeTab) throw new McpToolError("no_active_tab", "No active tab is available.");
@@ -75254,6 +75486,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75254
75486
  ...event.defaultPrompt !== void 0 ? { defaultPrompt: event.defaultPrompt } : {},
75255
75487
  ...event.url !== void 0 ? { url: event.url } : {}
75256
75488
  });
75489
+ this.resolveDialogWaiters(tabId);
75257
75490
  });
75258
75491
  this.installNetworkCollection(tabId, client);
75259
75492
  }
@@ -75292,20 +75525,29 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75292
75525
  });
75293
75526
  client.Network.loadingFinished(async (event) => {
75294
75527
  const request = state.byRequestId.get(event.requestId);
75295
- if (!request) return;
75528
+ if (!request) {
75529
+ resolveLoadingDone(state, event.requestId, true);
75530
+ return;
75531
+ }
75296
75532
  const startedAt = state.startedAt.get(event.requestId);
75297
75533
  if (startedAt !== void 0 && event.timestamp !== void 0) request.durationMs = Math.round(event.timestamp * 1e3 - startedAt);
75298
- if (canReadResponseBody(request)) {
75534
+ if (canReadResponseBody(request) && !state.bodyRead.has(event.requestId)) {
75535
+ state.bodyRead.add(event.requestId);
75299
75536
  const body = await client.Network?.getResponseBody({ requestId: event.requestId }).catch(() => void 0);
75300
75537
  if (body) request.responseBody = body.base64Encoded ? Buffer.from(body.body, "base64").toString("utf8") : body.body;
75301
75538
  }
75539
+ resolveLoadingDone(state, event.requestId, true);
75302
75540
  });
75303
75541
  client.Network.loadingFailed((event) => {
75304
75542
  const request = state.byRequestId.get(event.requestId);
75305
- if (!request) return;
75543
+ if (!request) {
75544
+ resolveLoadingDone(state, event.requestId, false);
75545
+ return;
75546
+ }
75306
75547
  request.failureText = event.errorText ?? "Unknown error";
75307
75548
  const startedAt = state.startedAt.get(event.requestId);
75308
75549
  if (startedAt !== void 0 && event.timestamp !== void 0) request.durationMs = Math.round(event.timestamp * 1e3 - startedAt);
75550
+ resolveLoadingDone(state, event.requestId, false);
75309
75551
  });
75310
75552
  }
75311
75553
  ensureConsoleState(tabId) {
@@ -75327,7 +75569,10 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75327
75569
  state = {
75328
75570
  requests: [],
75329
75571
  byRequestId: /* @__PURE__ */ new Map(),
75330
- startedAt: /* @__PURE__ */ new Map()
75572
+ startedAt: /* @__PURE__ */ new Map(),
75573
+ hydratedPerformanceResources: false,
75574
+ loadingDone: /* @__PURE__ */ new Map(),
75575
+ bodyRead: /* @__PURE__ */ new Set()
75331
75576
  };
75332
75577
  this.pageNetworkStates.set(tabId, state);
75333
75578
  }
@@ -75343,10 +75588,79 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75343
75588
  this.pageNetworkStates.set(tabId, {
75344
75589
  requests: [],
75345
75590
  byRequestId: /* @__PURE__ */ new Map(),
75346
- startedAt: /* @__PURE__ */ new Map()
75591
+ startedAt: /* @__PURE__ */ new Map(),
75592
+ hydratedPerformanceResources: false,
75593
+ loadingDone: /* @__PURE__ */ new Map(),
75594
+ bodyRead: /* @__PURE__ */ new Set()
75347
75595
  });
75348
75596
  this.pageDialogStates.delete(tabId);
75349
75597
  }
75598
+ async hydratePerformanceResourceRequests(tabId) {
75599
+ const state = this.ensureNetworkState(tabId);
75600
+ if (state.hydratedPerformanceResources) return;
75601
+ state.hydratedPerformanceResources = true;
75602
+ const pageClient = await this.getActivePageClient();
75603
+ const contextId = await this.getActiveUtilityContextId(pageClient);
75604
+ const documentRequest = await evaluateCdp(pageClient, String.raw`() => {
75605
+ const navigation = performance.getEntriesByType("navigation")[0];
75606
+ return {
75607
+ url: String(location.href || ""),
75608
+ duration: navigation ? Math.round(Number(navigation.duration || 0)) : undefined
75609
+ };
75610
+ }`, void 0, contextId).catch(() => void 0);
75611
+ if (documentRequest?.url && !Array.from(state.byRequestId.values()).some((request) => request.url === documentRequest.url)) {
75612
+ const requestId = `performance:navigation:${documentRequest.url}`;
75613
+ const request = {
75614
+ index: state.requests.length + 1,
75615
+ requestId,
75616
+ method: "GET",
75617
+ url: documentRequest.url,
75618
+ resourceType: "document",
75619
+ requestHeaders: {},
75620
+ status: 200,
75621
+ statusText: "OK",
75622
+ ...documentRequest.duration !== void 0 ? { durationMs: documentRequest.duration } : {}
75623
+ };
75624
+ state.requests.push(request);
75625
+ state.byRequestId.set(requestId, request);
75626
+ }
75627
+ const resources = await evaluateCdp(pageClient, String.raw`() => performance.getEntriesByType("resource").map((entry) => ({
75628
+ name: String(entry.name || ""),
75629
+ initiatorType: String(entry.initiatorType || "other"),
75630
+ duration: Math.round(Number(entry.duration || 0)),
75631
+ responseStatus: typeof entry.responseStatus === "number" ? entry.responseStatus : undefined
75632
+ }))`, void 0, contextId).catch(() => []);
75633
+ for (const resource of resources) {
75634
+ if (!resource.name || Array.from(state.byRequestId.values()).some((request) => request.url === resource.name)) continue;
75635
+ const status = resource.responseStatus && resource.responseStatus > 0 ? resource.responseStatus : await this.probeResourceStatus(pageClient, contextId, resource.name);
75636
+ const requestId = `performance:${resource.name}`;
75637
+ const request = {
75638
+ index: state.requests.length + 1,
75639
+ requestId,
75640
+ method: "GET",
75641
+ url: resource.name,
75642
+ resourceType: normalizeResourceType(resource.initiatorType),
75643
+ requestHeaders: {},
75644
+ ...status !== void 0 ? {
75645
+ status,
75646
+ statusText: statusTextForStatus(status)
75647
+ } : {},
75648
+ ...resource.duration !== void 0 ? { durationMs: resource.duration } : {}
75649
+ };
75650
+ state.requests.push(request);
75651
+ state.byRequestId.set(requestId, request);
75652
+ }
75653
+ }
75654
+ async probeResourceStatus(pageClient, contextId, url) {
75655
+ return evaluateCdp(pageClient, String.raw`async (url) => {
75656
+ try {
75657
+ const response = await fetch(url, { method: "HEAD", cache: "no-store" });
75658
+ return response.status;
75659
+ } catch {
75660
+ return undefined;
75661
+ }
75662
+ }`, url, contextId).catch(() => void 0);
75663
+ }
75350
75664
  addConsoleMessage(tabId, message) {
75351
75665
  const state = this.ensureConsoleState(tabId);
75352
75666
  if (!shouldIncludeConsoleMessage(message.type)) return;
@@ -75480,6 +75794,45 @@ function canReadResponseBody(request) {
75480
75794
  if (request.failureText || request.status === void 0) return false;
75481
75795
  return request.status !== 204 && request.status !== 304 && !(request.status >= 100 && request.status < 200);
75482
75796
  }
75797
+ function loadingDoneEntry(state, requestId) {
75798
+ let entry = state.loadingDone.get(requestId);
75799
+ if (!entry) {
75800
+ let resolve;
75801
+ let reject;
75802
+ entry = {
75803
+ promise: new Promise((res, rej) => {
75804
+ resolve = res;
75805
+ reject = rej;
75806
+ }),
75807
+ resolve,
75808
+ reject
75809
+ };
75810
+ state.loadingDone.set(requestId, entry);
75811
+ }
75812
+ return entry;
75813
+ }
75814
+ function resolveLoadingDone(state, requestId, success) {
75815
+ const entry = state.loadingDone.get(requestId);
75816
+ if (!entry) return;
75817
+ state.loadingDone.delete(requestId);
75818
+ if (success) entry.resolve();
75819
+ else entry.reject(/* @__PURE__ */ new Error("Request failed before the response body was available."));
75820
+ }
75821
+ async function waitForLoadingDone(state, requestId, timeoutMs) {
75822
+ const entry = loadingDoneEntry(state, requestId);
75823
+ await Promise.race([entry.promise, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
75824
+ }
75825
+ function statusTextForStatus(status) {
75826
+ if (status === 200) return "OK";
75827
+ if (status === 204) return "No Content";
75828
+ if (status === 304) return "Not Modified";
75829
+ if (status === 400) return "Bad Request";
75830
+ if (status === 401) return "Unauthorized";
75831
+ if (status === 403) return "Forbidden";
75832
+ if (status === 404) return "Not Found";
75833
+ if (status === 500) return "Internal Server Error";
75834
+ return "";
75835
+ }
75483
75836
  function cloneNetworkRequest(request) {
75484
75837
  return {
75485
75838
  ...request,
@@ -75558,6 +75911,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75558
75911
  pageConsoleStates = /* @__PURE__ */ new Map();
75559
75912
  pageNetworkStates = /* @__PURE__ */ new Map();
75560
75913
  pageDialogStates = /* @__PURE__ */ new Map();
75914
+ dialogWaiters = /* @__PURE__ */ new Map();
75561
75915
  bidiListeners = /* @__PURE__ */ new Map();
75562
75916
  responseDataCollector;
75563
75917
  activeTabId;
@@ -75571,12 +75925,12 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75571
75925
  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
75926
  const client = await getBidiClientFactory()({
75573
75927
  browserName: "firefox",
75574
- webSocketUrl: normalizeFirefoxBidiEndpoint(args.endpoint)
75928
+ webSocketUrl: normalizeFirefoxBidiEndpoint(args.endpoint, args.sessionId)
75575
75929
  });
75576
75930
  const session = new BidiConnectedBrowserSession(client);
75577
- session.ownsSession = await ensureMcpBiDiSession(client, args.endpoint);
75931
+ session.ownsSession = await ensureMcpBiDiSession(client, args.endpoint, args.sessionId);
75578
75932
  await session.initialize();
75579
- if ((await session.refreshTabs()).length === 0) await session.newTab();
75933
+ await session.refreshTabs();
75580
75934
  return session;
75581
75935
  }
75582
75936
  async version() {
@@ -75619,10 +75973,13 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75619
75973
  }
75620
75974
  async snapshot(request = {}) {
75621
75975
  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
- });
75976
+ return {
75977
+ ...toBrowserSnapshot(await retryUntilReady(() => evaluateBiDi(this.client, tabId, PLAYWRIGHT_ARIA_SNAPSHOT_EVALUATE_SOURCE, toAriaSnapshotPayload(request))), request, {
75978
+ console: this.consoleSummary(tabId),
75979
+ consoleLink: await this.takeConsoleLink(tabId)
75980
+ }),
75981
+ retryable: true
75982
+ };
75626
75983
  }
75627
75984
  async consoleMessages(level = "info", all = false) {
75628
75985
  const activeTabId = await this.getActiveTabId();
@@ -76029,7 +76386,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76029
76386
  }
76030
76387
  }
76031
76388
  async handleDialog(accept, promptText) {
76032
- const tabId = await this.getActiveTabId();
76389
+ const tabId = this.dialogTabId();
76033
76390
  if (!this.pageDialogStates.has(tabId)) throw new McpToolError("no_dialog", "No dialog visible.");
76034
76391
  this.pageDialogStates.delete(tabId);
76035
76392
  await this.client.browsingContextHandleUserPrompt({
@@ -76038,6 +76395,9 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76038
76395
  ...promptText !== void 0 ? { userText: promptText } : {}
76039
76396
  });
76040
76397
  }
76398
+ async hasDialog() {
76399
+ return this.pageDialogStates.size > 0;
76400
+ }
76041
76401
  async networkRequests() {
76042
76402
  const tabId = await this.getActiveTabId();
76043
76403
  return this.ensureNetworkState(tabId).requests.map(cloneNetworkRequest);
@@ -76047,6 +76407,15 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76047
76407
  const request = this.ensureNetworkState(tabId).requests[index - 1];
76048
76408
  return request ? cloneNetworkRequest(request) : void 0;
76049
76409
  }
76410
+ async fetchResponseBody(index) {
76411
+ const tabId = await this.getActiveTabId();
76412
+ const request = this.ensureNetworkState(tabId).requests[index - 1];
76413
+ if (!request || !request.requestId) return request?.responseBody;
76414
+ if (request.responseBody !== void 0) return request.responseBody;
76415
+ const body = await this.getResponseBody(request.requestId).catch(() => void 0);
76416
+ if (body !== void 0) request.responseBody = body;
76417
+ return request.responseBody;
76418
+ }
76050
76419
  async runCodeUnsafe(code) {
76051
76420
  return this.evaluate(`async () => {
76052
76421
  const fn = eval(${JSON.stringify(`(${code})`)});
@@ -76110,6 +76479,45 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76110
76479
  targetArg(target) {
76111
76480
  return "nodeToken" in target ? { nodeToken: target.nodeToken } : { selector: target.selector };
76112
76481
  }
76482
+ dialogTabId() {
76483
+ if (this.activeTabId && this.pageDialogStates.has(this.activeTabId)) return this.activeTabId;
76484
+ const tabId = this.pageDialogStates.keys().next().value;
76485
+ if (!tabId) throw new McpToolError("no_dialog", "No dialog visible.");
76486
+ return tabId;
76487
+ }
76488
+ waitForDialog(tabId, timeoutMs) {
76489
+ if (this.pageDialogStates.has(tabId)) return Promise.resolve();
76490
+ return new Promise((resolve, reject) => {
76491
+ const waiter = {
76492
+ resolve: () => {
76493
+ if (waiter.timer) clearTimeout(waiter.timer);
76494
+ this.removeDialogWaiter(tabId, waiter);
76495
+ resolve();
76496
+ },
76497
+ reject: (error) => {
76498
+ if (waiter.timer) clearTimeout(waiter.timer);
76499
+ this.removeDialogWaiter(tabId, waiter);
76500
+ reject(error);
76501
+ }
76502
+ };
76503
+ waiter.timer = setTimeout(() => waiter.reject?.(/* @__PURE__ */ new Error("Timed out waiting for dialog.")), timeoutMs);
76504
+ const waiters = this.dialogWaiters.get(tabId) ?? /* @__PURE__ */ new Set();
76505
+ waiters.add(waiter);
76506
+ this.dialogWaiters.set(tabId, waiters);
76507
+ });
76508
+ }
76509
+ resolveDialogWaiters(tabId) {
76510
+ const waiters = this.dialogWaiters.get(tabId);
76511
+ if (!waiters) return;
76512
+ this.dialogWaiters.delete(tabId);
76513
+ for (const waiter of waiters) waiter.resolve();
76514
+ }
76515
+ removeDialogWaiter(tabId, waiter) {
76516
+ const waiters = this.dialogWaiters.get(tabId);
76517
+ if (!waiters) return;
76518
+ waiters.delete(waiter);
76519
+ if (waiters.size === 0) this.dialogWaiters.delete(tabId);
76520
+ }
76113
76521
  async actionPoint(tabId, target) {
76114
76522
  const source = "nodeToken" in target ? ACTION_POINT_EVALUATE_SOURCE : ACTION_POINT_BY_SELECTOR_SOURCE;
76115
76523
  const point = await evaluateBiDi(this.client, tabId, source, this.targetArg(target));
@@ -76268,7 +76676,10 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76268
76676
  state = {
76269
76677
  requests: [],
76270
76678
  byRequestId: /* @__PURE__ */ new Map(),
76271
- startedAt: /* @__PURE__ */ new Map()
76679
+ startedAt: /* @__PURE__ */ new Map(),
76680
+ hydratedPerformanceResources: false,
76681
+ loadingDone: /* @__PURE__ */ new Map(),
76682
+ bodyRead: /* @__PURE__ */ new Set()
76272
76683
  };
76273
76684
  this.pageNetworkStates.set(tabId, state);
76274
76685
  }
@@ -76284,7 +76695,10 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76284
76695
  this.pageNetworkStates.set(tabId, {
76285
76696
  requests: [],
76286
76697
  byRequestId: /* @__PURE__ */ new Map(),
76287
- startedAt: /* @__PURE__ */ new Map()
76698
+ startedAt: /* @__PURE__ */ new Map(),
76699
+ hydratedPerformanceResources: false,
76700
+ loadingDone: /* @__PURE__ */ new Map(),
76701
+ bodyRead: /* @__PURE__ */ new Set()
76288
76702
  });
76289
76703
  this.pageDialogStates.delete(tabId);
76290
76704
  }
@@ -76322,14 +76736,18 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76322
76736
  return `${path.relative(process.cwd(), state.logFile)}${fromLine === state.logLine ? `#L${fromLine}` : `#L${fromLine}-L${state.logLine}`}`;
76323
76737
  }
76324
76738
  };
76325
- function normalizeFirefoxBidiEndpoint(endpoint) {
76739
+ function normalizeFirefoxBidiEndpoint(endpoint, sessionId) {
76326
76740
  const url = new URL(endpoint);
76741
+ if (sessionId) {
76742
+ url.pathname = `/session/${sessionId}`;
76743
+ return url.toString();
76744
+ }
76327
76745
  if (url.pathname === "/" || url.pathname === "") url.pathname = "/session";
76328
76746
  return url.toString();
76329
76747
  }
76330
- async function ensureMcpBiDiSession(client, endpoint) {
76748
+ async function ensureMcpBiDiSession(client, endpoint, sessionId) {
76331
76749
  await client.sessionStatus({});
76332
- if (isSessionSpecificFirefoxBidiEndpoint(endpoint)) return false;
76750
+ if (sessionId || isSessionSpecificFirefoxBidiEndpoint(endpoint)) return false;
76333
76751
  try {
76334
76752
  await client.browsingContextGetTree({});
76335
76753
  return false;
@@ -76414,6 +76832,7 @@ var McpRuntime = class {
76414
76832
  this.invalidateSnapshot();
76415
76833
  this.pendingFileUploadTarget = void 0;
76416
76834
  this.tabs = await session.newTab(url);
76835
+ if (this.snapshotMode === "none") return { tabs: this.tabs };
76417
76836
  const snapshot = this.tabs.some((tab) => tab.active) ? await this.snapshot() : void 0;
76418
76837
  return snapshot ? {
76419
76838
  tabs: this.tabs,
@@ -76427,6 +76846,7 @@ var McpRuntime = class {
76427
76846
  this.invalidateSnapshot();
76428
76847
  this.pendingFileUploadTarget = void 0;
76429
76848
  this.tabs = await session.selectTab(tab.id);
76849
+ if (this.snapshotMode === "none") return { tabs: this.tabs };
76430
76850
  const snapshot = await this.snapshot();
76431
76851
  return {
76432
76852
  tabs: this.tabs,
@@ -76440,6 +76860,7 @@ var McpRuntime = class {
76440
76860
  this.invalidateSnapshot();
76441
76861
  this.pendingFileUploadTarget = void 0;
76442
76862
  this.tabs = await session.closeTab(tab.id);
76863
+ if (this.snapshotMode === "none") return { tabs: this.tabs };
76443
76864
  const snapshot = this.tabs.some((candidate) => candidate.active) ? await this.snapshot() : void 0;
76444
76865
  return snapshot ? {
76445
76866
  tabs: this.tabs,
@@ -76448,26 +76869,49 @@ var McpRuntime = class {
76448
76869
  }
76449
76870
  async snapshot(args = {}) {
76450
76871
  const session = this.requireConnected();
76451
- this.tabs = await session.listTabs();
76452
- const activeTab = this.requireActiveTab();
76453
76872
  const requestKey = this.snapshotRequestKey(args);
76454
76873
  const request = {
76455
76874
  ...args.boxes !== void 0 ? { boxes: args.boxes } : {},
76456
76875
  ...args.depth !== void 0 ? { depth: args.depth } : {},
76457
76876
  ...args.target ? { target: this.resolveSnapshotTarget(args.target) } : {}
76458
76877
  };
76459
- const snapshot = await session.snapshot(request);
76878
+ const { activeTab, currentActiveTab, snapshot } = await this.captureStableSnapshot(session, request);
76460
76879
  this.snapshotCache = {
76461
- tabId: activeTab.id,
76880
+ tabId: currentActiveTab.id,
76462
76881
  requestKey,
76463
76882
  text: snapshot.text,
76464
76883
  refs: { ...snapshot.refs },
76465
- title: snapshot.title,
76466
- url: snapshot.url,
76884
+ title: currentActiveTab.title || snapshot.title,
76885
+ url: currentActiveTab.url || snapshot.url,
76467
76886
  ...snapshot.console ? { console: { ...snapshot.console } } : {},
76468
76887
  ...snapshot.consoleLink ? { consoleLink: snapshot.consoleLink } : {}
76469
76888
  };
76470
- return snapshot;
76889
+ return {
76890
+ ...snapshot,
76891
+ title: currentActiveTab.title || snapshot.title,
76892
+ url: currentActiveTab.url || snapshot.url
76893
+ };
76894
+ }
76895
+ async captureStableSnapshot(session, request) {
76896
+ let lastAttempt;
76897
+ for (let attempt = 0; attempt < 4; attempt += 1) {
76898
+ this.tabs = await session.listTabs();
76899
+ const activeTab = this.requireActiveTab();
76900
+ const snapshot = await session.snapshot(request);
76901
+ const refreshedTabs = await session.listTabs();
76902
+ this.tabs = refreshedTabs;
76903
+ const currentActiveTab = refreshedTabs.find((tab) => tab.active) ?? refreshedTabs.find((tab) => tab.id === activeTab.id) ?? activeTab;
76904
+ const captured = {
76905
+ activeTab,
76906
+ currentActiveTab,
76907
+ snapshot
76908
+ };
76909
+ lastAttempt = captured;
76910
+ if (!snapshot.retryable || snapshot.text.trim().length > 0 || currentActiveTab.url === "about:blank") return captured;
76911
+ await delay(150 * (attempt + 1));
76912
+ }
76913
+ if (!lastAttempt) throw new McpToolError("action_failed", "Unable to capture page snapshot.");
76914
+ return lastAttempt;
76471
76915
  }
76472
76916
  async click(target, opts) {
76473
76917
  const session = this.requireConnected();
@@ -76486,6 +76930,7 @@ var McpRuntime = class {
76486
76930
  this.invalidateSnapshot();
76487
76931
  this.pendingFileUploadTarget = opensFileChooser ? resolved : void 0;
76488
76932
  if (this.snapshotMode === "none") return;
76933
+ if (await session.hasDialog()) return;
76489
76934
  return this.snapshot();
76490
76935
  }
76491
76936
  async hover(target) {
@@ -76495,13 +76940,16 @@ var McpRuntime = class {
76495
76940
  this.invalidateSnapshot();
76496
76941
  this.pendingFileUploadTarget = void 0;
76497
76942
  if (this.snapshotMode === "none") return;
76943
+ if (await session.hasDialog()) return;
76498
76944
  return this.snapshot();
76499
76945
  }
76500
76946
  async navigate(url) {
76501
- await this.requireConnected().navigate(normalizeNavigationUrl(url));
76947
+ const session = this.requireConnected();
76948
+ await session.navigate(normalizeNavigationUrl(url));
76502
76949
  this.invalidateSnapshot();
76503
76950
  this.pendingFileUploadTarget = void 0;
76504
76951
  if (this.snapshotMode === "none") return;
76952
+ if (await session.hasDialog()) return;
76505
76953
  return this.snapshot();
76506
76954
  }
76507
76955
  async type(ref, text, opts) {
@@ -76650,6 +77098,9 @@ var McpRuntime = class {
76650
77098
  async networkRequest(index) {
76651
77099
  return this.requireConnected().networkRequest(index);
76652
77100
  }
77101
+ async fetchResponseBody(index) {
77102
+ return this.requireConnected().fetchResponseBody(index);
77103
+ }
76653
77104
  async runCodeUnsafe(code) {
76654
77105
  const result = await this.requireConnected().runCodeUnsafe(code);
76655
77106
  this.invalidateSnapshot();
@@ -76686,6 +77137,10 @@ var McpRuntime = class {
76686
77137
  hasPendingFileUploadTarget() {
76687
77138
  return !!this.pendingFileUploadTarget;
76688
77139
  }
77140
+ async hasDialog() {
77141
+ if (!this.connection) return false;
77142
+ return this.connection.session.hasDialog();
77143
+ }
76689
77144
  async close() {
76690
77145
  this.invalidateSnapshot();
76691
77146
  this.pendingFileUploadTarget = void 0;
@@ -78306,6 +78761,7 @@ async function createRoxyBrowserMcpInMemory(options = {}) {
78306
78761
  await bundle.server.connect(serverTransport);
78307
78762
  return {
78308
78763
  server: bundle.server,
78764
+ runtimeManager: bundle.runtimeManager,
78309
78765
  serverTransport,
78310
78766
  clientTransport,
78311
78767
  close: async () => {