@roxybrowser/playwright 2.0.2-beta.2 → 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.
@@ -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);
@@ -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,9 +73495,11 @@ var Response$1 = class {
73364
73495
  sections.push("### Code", "```js", ...this.code, "```");
73365
73496
  }
73366
73497
  if (this.includeSnapshot === "full") {
73367
- const snapshot = await reconcileSnapshotWithTabs(this.context, 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
73505
  let snapshot = await reconcileSnapshotWithTabs(this.context, await this.context.runtime.snapshot({
@@ -73374,7 +73507,7 @@ var Response$1 = class {
73374
73507
  ...this.fullSnapshot.depth !== void 0 ? { depth: this.fullSnapshot.depth } : {},
73375
73508
  ...this.fullSnapshot.boxes !== void 0 ? { boxes: this.fullSnapshot.boxes } : {}
73376
73509
  }));
73377
- if (!this.fullSnapshot.filename && snapshot.text.trim().length === 0 && snapshot.url && snapshot.url !== "about:blank") snapshot = await reconcileSnapshotWithTabs(this.context, await this.context.runtime.snapshot({
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({
73378
73511
  ...this.fullSnapshot.target !== void 0 ? { target: this.fullSnapshot.target } : {},
73379
73512
  ...this.fullSnapshot.depth !== void 0 ? { depth: this.fullSnapshot.depth } : {},
73380
73513
  ...this.fullSnapshot.boxes !== void 0 ? { boxes: this.fullSnapshot.boxes } : {}
@@ -73968,7 +74101,17 @@ var networkRequest = defineTool$1({
73968
74101
  response.addError(`Request #${args.index} not found. Use browser_network_requests to see available indexes.`);
73969
74102
  return;
73970
74103
  }
73971
- 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);
73972
74115
  if (args.filename) {
73973
74116
  const resolvedFilename = await context.resolveOutputFile(args.filename);
73974
74117
  await writeFile(resolvedFilename, text);
@@ -74002,8 +74145,10 @@ function renderRequestDetails(request) {
74002
74145
  if (request.mimeType) lines.push(` mimeType: ${request.mimeType}`);
74003
74146
  appendHeaders(lines, "Request headers", request.requestHeaders);
74004
74147
  if (request.responseHeaders) appendHeaders(lines, "Response headers", request.responseHeaders);
74005
- if (request.requestBody) lines.push("", `Call browser_network_request with part="request-body" to read the request body.`);
74006
- 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);
74007
74152
  return lines.join("\n");
74008
74153
  }
74009
74154
  function renderRequestPart(request, part) {
@@ -74022,6 +74167,10 @@ function appendHeaders(lines, title, headers) {
74022
74167
  function renderHeaders(headers) {
74023
74168
  return Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join("\n");
74024
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
+ }
74025
74174
  var network_default = [networkRequests, networkRequest];
74026
74175
  var runCode_default = [defineTool$1({
74027
74176
  capability: "devtools",
@@ -74759,6 +74908,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74759
74908
  pageConsoleStates = /* @__PURE__ */ new Map();
74760
74909
  pageNetworkStates = /* @__PURE__ */ new Map();
74761
74910
  pageDialogStates = /* @__PURE__ */ new Map();
74911
+ dialogWaiters = /* @__PURE__ */ new Map();
74762
74912
  activeTabId;
74763
74913
  versionString = "Chromium/unknown";
74764
74914
  constructor(browserClient, connection) {
@@ -74774,7 +74924,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74774
74924
  });
74775
74925
  const session = new CdpConnectedBrowserSession(await chromeRemoteInterface({ target: connection.browserWsEndpoint }), connection);
74776
74926
  session.versionString = version.Browser;
74777
- if ((await session.refreshTabs()).length === 0) await session.newTab();
74927
+ await session.refreshTabs();
74778
74928
  await session.getActivePageClient().catch(() => void 0);
74779
74929
  return session;
74780
74930
  }
@@ -74826,6 +74976,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74826
74976
  async click(target, options) {
74827
74977
  const pageClient = await this.getActivePageClient();
74828
74978
  const contextId = await this.getActiveUtilityContextId(pageClient);
74979
+ const tabId = await this.getActiveTabId();
74829
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);
74830
74981
  if (!point.ok || point.x === void 0 || point.y === void 0) {
74831
74982
  const isSelector = "selector" in target;
@@ -74859,7 +75010,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74859
75010
  modifiers: modifiersMask
74860
75011
  });
74861
75012
  await delay$1(options.clickHoldMs);
74862
- await pageClient.Input.dispatchMouseEvent({
75013
+ const releasePromise = pageClient.Input.dispatchMouseEvent({
74863
75014
  type: "mouseReleased",
74864
75015
  x: point.x,
74865
75016
  y: point.y,
@@ -74867,6 +75018,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74867
75018
  clickCount,
74868
75019
  modifiers: modifiersMask
74869
75020
  });
75021
+ await Promise.race([releasePromise, this.waitForDialog(tabId, options.clickHoldMs + 1e3)]);
74870
75022
  }
74871
75023
  }
74872
75024
  async drag(start, end, options) {
@@ -75122,24 +75274,72 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75122
75274
  }
75123
75275
  }
75124
75276
  async handleDialog(accept, promptText) {
75125
- const tabId = await this.getActiveTabId();
75277
+ const tabId = this.dialogTabId();
75126
75278
  if (!this.pageDialogStates.has(tabId)) throw new McpToolError("no_dialog", "No dialog visible.");
75127
- const pageClient = await this.getActivePageClient();
75279
+ const pageClient = this.pageClients.get(tabId) ?? await this.getActivePageClient();
75128
75280
  this.pageDialogStates.delete(tabId);
75129
75281
  await pageClient.Page.handleJavaScriptDialog({
75130
75282
  accept,
75131
75283
  ...promptText !== void 0 ? { promptText } : {}
75132
75284
  });
75133
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
+ }
75134
75315
  async networkRequests() {
75135
75316
  const tabId = await this.getActiveTabId();
75317
+ await this.hydratePerformanceResourceRequests(tabId);
75136
75318
  return this.ensureNetworkState(tabId).requests.map(cloneNetworkRequest);
75137
75319
  }
75138
75320
  async networkRequest(index) {
75139
75321
  const tabId = await this.getActiveTabId();
75322
+ await this.hydratePerformanceResourceRequests(tabId);
75140
75323
  const request = this.ensureNetworkState(tabId).requests[index - 1];
75141
75324
  return request ? cloneNetworkRequest(request) : void 0;
75142
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
+ }
75143
75343
  async runCodeUnsafe(code) {
75144
75344
  const pageClient = await this.getActivePageClient();
75145
75345
  const contextId = await this.getActiveUtilityContextId(pageClient);
@@ -75199,6 +75399,12 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75199
75399
  if (!activeTab) throw new McpToolError("no_active_tab", "No active tab is available.");
75200
75400
  return activeTab.id;
75201
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
+ }
75202
75408
  async getActivePageClient() {
75203
75409
  const activeTab = (await this.refreshTabs()).find((tab) => tab.active);
75204
75410
  if (!activeTab) throw new McpToolError("no_active_tab", "No active tab is available.");
@@ -75280,6 +75486,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75280
75486
  ...event.defaultPrompt !== void 0 ? { defaultPrompt: event.defaultPrompt } : {},
75281
75487
  ...event.url !== void 0 ? { url: event.url } : {}
75282
75488
  });
75489
+ this.resolveDialogWaiters(tabId);
75283
75490
  });
75284
75491
  this.installNetworkCollection(tabId, client);
75285
75492
  }
@@ -75318,20 +75525,29 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75318
75525
  });
75319
75526
  client.Network.loadingFinished(async (event) => {
75320
75527
  const request = state.byRequestId.get(event.requestId);
75321
- if (!request) return;
75528
+ if (!request) {
75529
+ resolveLoadingDone(state, event.requestId, true);
75530
+ return;
75531
+ }
75322
75532
  const startedAt = state.startedAt.get(event.requestId);
75323
75533
  if (startedAt !== void 0 && event.timestamp !== void 0) request.durationMs = Math.round(event.timestamp * 1e3 - startedAt);
75324
- if (canReadResponseBody(request)) {
75534
+ if (canReadResponseBody(request) && !state.bodyRead.has(event.requestId)) {
75535
+ state.bodyRead.add(event.requestId);
75325
75536
  const body = await client.Network?.getResponseBody({ requestId: event.requestId }).catch(() => void 0);
75326
75537
  if (body) request.responseBody = body.base64Encoded ? Buffer.from(body.body, "base64").toString("utf8") : body.body;
75327
75538
  }
75539
+ resolveLoadingDone(state, event.requestId, true);
75328
75540
  });
75329
75541
  client.Network.loadingFailed((event) => {
75330
75542
  const request = state.byRequestId.get(event.requestId);
75331
- if (!request) return;
75543
+ if (!request) {
75544
+ resolveLoadingDone(state, event.requestId, false);
75545
+ return;
75546
+ }
75332
75547
  request.failureText = event.errorText ?? "Unknown error";
75333
75548
  const startedAt = state.startedAt.get(event.requestId);
75334
75549
  if (startedAt !== void 0 && event.timestamp !== void 0) request.durationMs = Math.round(event.timestamp * 1e3 - startedAt);
75550
+ resolveLoadingDone(state, event.requestId, false);
75335
75551
  });
75336
75552
  }
75337
75553
  ensureConsoleState(tabId) {
@@ -75353,7 +75569,10 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75353
75569
  state = {
75354
75570
  requests: [],
75355
75571
  byRequestId: /* @__PURE__ */ new Map(),
75356
- startedAt: /* @__PURE__ */ new Map()
75572
+ startedAt: /* @__PURE__ */ new Map(),
75573
+ hydratedPerformanceResources: false,
75574
+ loadingDone: /* @__PURE__ */ new Map(),
75575
+ bodyRead: /* @__PURE__ */ new Set()
75357
75576
  };
75358
75577
  this.pageNetworkStates.set(tabId, state);
75359
75578
  }
@@ -75369,10 +75588,79 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75369
75588
  this.pageNetworkStates.set(tabId, {
75370
75589
  requests: [],
75371
75590
  byRequestId: /* @__PURE__ */ new Map(),
75372
- startedAt: /* @__PURE__ */ new Map()
75591
+ startedAt: /* @__PURE__ */ new Map(),
75592
+ hydratedPerformanceResources: false,
75593
+ loadingDone: /* @__PURE__ */ new Map(),
75594
+ bodyRead: /* @__PURE__ */ new Set()
75373
75595
  });
75374
75596
  this.pageDialogStates.delete(tabId);
75375
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
+ }
75376
75664
  addConsoleMessage(tabId, message) {
75377
75665
  const state = this.ensureConsoleState(tabId);
75378
75666
  if (!shouldIncludeConsoleMessage(message.type)) return;
@@ -75506,6 +75794,45 @@ function canReadResponseBody(request) {
75506
75794
  if (request.failureText || request.status === void 0) return false;
75507
75795
  return request.status !== 204 && request.status !== 304 && !(request.status >= 100 && request.status < 200);
75508
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
+ }
75509
75836
  function cloneNetworkRequest(request) {
75510
75837
  return {
75511
75838
  ...request,
@@ -75584,6 +75911,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75584
75911
  pageConsoleStates = /* @__PURE__ */ new Map();
75585
75912
  pageNetworkStates = /* @__PURE__ */ new Map();
75586
75913
  pageDialogStates = /* @__PURE__ */ new Map();
75914
+ dialogWaiters = /* @__PURE__ */ new Map();
75587
75915
  bidiListeners = /* @__PURE__ */ new Map();
75588
75916
  responseDataCollector;
75589
75917
  activeTabId;
@@ -75602,7 +75930,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75602
75930
  const session = new BidiConnectedBrowserSession(client);
75603
75931
  session.ownsSession = await ensureMcpBiDiSession(client, args.endpoint, args.sessionId);
75604
75932
  await session.initialize();
75605
- if ((await session.refreshTabs()).length === 0) await session.newTab();
75933
+ await session.refreshTabs();
75606
75934
  return session;
75607
75935
  }
75608
75936
  async version() {
@@ -75645,10 +75973,13 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75645
75973
  }
75646
75974
  async snapshot(request = {}) {
75647
75975
  const tabId = await this.getActiveTabId();
75648
- return toBrowserSnapshot(await retryUntilReady(() => evaluateBiDi(this.client, tabId, PLAYWRIGHT_ARIA_SNAPSHOT_EVALUATE_SOURCE, toAriaSnapshotPayload(request))), request, {
75649
- console: this.consoleSummary(tabId),
75650
- consoleLink: await this.takeConsoleLink(tabId)
75651
- });
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
+ };
75652
75983
  }
75653
75984
  async consoleMessages(level = "info", all = false) {
75654
75985
  const activeTabId = await this.getActiveTabId();
@@ -76055,7 +76386,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76055
76386
  }
76056
76387
  }
76057
76388
  async handleDialog(accept, promptText) {
76058
- const tabId = await this.getActiveTabId();
76389
+ const tabId = this.dialogTabId();
76059
76390
  if (!this.pageDialogStates.has(tabId)) throw new McpToolError("no_dialog", "No dialog visible.");
76060
76391
  this.pageDialogStates.delete(tabId);
76061
76392
  await this.client.browsingContextHandleUserPrompt({
@@ -76064,6 +76395,9 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76064
76395
  ...promptText !== void 0 ? { userText: promptText } : {}
76065
76396
  });
76066
76397
  }
76398
+ async hasDialog() {
76399
+ return this.pageDialogStates.size > 0;
76400
+ }
76067
76401
  async networkRequests() {
76068
76402
  const tabId = await this.getActiveTabId();
76069
76403
  return this.ensureNetworkState(tabId).requests.map(cloneNetworkRequest);
@@ -76073,6 +76407,15 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76073
76407
  const request = this.ensureNetworkState(tabId).requests[index - 1];
76074
76408
  return request ? cloneNetworkRequest(request) : void 0;
76075
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
+ }
76076
76419
  async runCodeUnsafe(code) {
76077
76420
  return this.evaluate(`async () => {
76078
76421
  const fn = eval(${JSON.stringify(`(${code})`)});
@@ -76136,6 +76479,45 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76136
76479
  targetArg(target) {
76137
76480
  return "nodeToken" in target ? { nodeToken: target.nodeToken } : { selector: target.selector };
76138
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
+ }
76139
76521
  async actionPoint(tabId, target) {
76140
76522
  const source = "nodeToken" in target ? ACTION_POINT_EVALUATE_SOURCE : ACTION_POINT_BY_SELECTOR_SOURCE;
76141
76523
  const point = await evaluateBiDi(this.client, tabId, source, this.targetArg(target));
@@ -76294,7 +76676,10 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76294
76676
  state = {
76295
76677
  requests: [],
76296
76678
  byRequestId: /* @__PURE__ */ new Map(),
76297
- startedAt: /* @__PURE__ */ new Map()
76679
+ startedAt: /* @__PURE__ */ new Map(),
76680
+ hydratedPerformanceResources: false,
76681
+ loadingDone: /* @__PURE__ */ new Map(),
76682
+ bodyRead: /* @__PURE__ */ new Set()
76298
76683
  };
76299
76684
  this.pageNetworkStates.set(tabId, state);
76300
76685
  }
@@ -76310,7 +76695,10 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76310
76695
  this.pageNetworkStates.set(tabId, {
76311
76696
  requests: [],
76312
76697
  byRequestId: /* @__PURE__ */ new Map(),
76313
- startedAt: /* @__PURE__ */ new Map()
76698
+ startedAt: /* @__PURE__ */ new Map(),
76699
+ hydratedPerformanceResources: false,
76700
+ loadingDone: /* @__PURE__ */ new Map(),
76701
+ bodyRead: /* @__PURE__ */ new Set()
76314
76702
  });
76315
76703
  this.pageDialogStates.delete(tabId);
76316
76704
  }
@@ -76519,7 +76907,7 @@ var McpRuntime = class {
76519
76907
  snapshot
76520
76908
  };
76521
76909
  lastAttempt = captured;
76522
- if (snapshot.text.trim().length > 0 || currentActiveTab.url === "about:blank") return captured;
76910
+ if (!snapshot.retryable || snapshot.text.trim().length > 0 || currentActiveTab.url === "about:blank") return captured;
76523
76911
  await delay(150 * (attempt + 1));
76524
76912
  }
76525
76913
  if (!lastAttempt) throw new McpToolError("action_failed", "Unable to capture page snapshot.");
@@ -76542,6 +76930,7 @@ var McpRuntime = class {
76542
76930
  this.invalidateSnapshot();
76543
76931
  this.pendingFileUploadTarget = opensFileChooser ? resolved : void 0;
76544
76932
  if (this.snapshotMode === "none") return;
76933
+ if (await session.hasDialog()) return;
76545
76934
  return this.snapshot();
76546
76935
  }
76547
76936
  async hover(target) {
@@ -76551,13 +76940,16 @@ var McpRuntime = class {
76551
76940
  this.invalidateSnapshot();
76552
76941
  this.pendingFileUploadTarget = void 0;
76553
76942
  if (this.snapshotMode === "none") return;
76943
+ if (await session.hasDialog()) return;
76554
76944
  return this.snapshot();
76555
76945
  }
76556
76946
  async navigate(url) {
76557
- await this.requireConnected().navigate(normalizeNavigationUrl(url));
76947
+ const session = this.requireConnected();
76948
+ await session.navigate(normalizeNavigationUrl(url));
76558
76949
  this.invalidateSnapshot();
76559
76950
  this.pendingFileUploadTarget = void 0;
76560
76951
  if (this.snapshotMode === "none") return;
76952
+ if (await session.hasDialog()) return;
76561
76953
  return this.snapshot();
76562
76954
  }
76563
76955
  async type(ref, text, opts) {
@@ -76706,6 +77098,9 @@ var McpRuntime = class {
76706
77098
  async networkRequest(index) {
76707
77099
  return this.requireConnected().networkRequest(index);
76708
77100
  }
77101
+ async fetchResponseBody(index) {
77102
+ return this.requireConnected().fetchResponseBody(index);
77103
+ }
76709
77104
  async runCodeUnsafe(code) {
76710
77105
  const result = await this.requireConnected().runCodeUnsafe(code);
76711
77106
  this.invalidateSnapshot();
@@ -76742,6 +77137,10 @@ var McpRuntime = class {
76742
77137
  hasPendingFileUploadTarget() {
76743
77138
  return !!this.pendingFileUploadTarget;
76744
77139
  }
77140
+ async hasDialog() {
77141
+ if (!this.connection) return false;
77142
+ return this.connection.session.hasDialog();
77143
+ }
76745
77144
  async close() {
76746
77145
  this.invalidateSnapshot();
76747
77146
  this.pendingFileUploadTarget = void 0;