@roxybrowser/playwright 2.0.2-beta.2 → 2.0.2-beta.5

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",
@@ -74249,6 +74398,16 @@ var allTools = [...mouse_default, ...form_default];
74249
74398
  function delay$1(ms) {
74250
74399
  return new Promise((resolve) => setTimeout(resolve, ms));
74251
74400
  }
74401
+ async function withBiDiTimeout(promise, timeoutMs) {
74402
+ let timer;
74403
+ try {
74404
+ return await Promise.race([promise, new Promise((_, reject) => {
74405
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Timed out after ${timeoutMs}ms.`)), timeoutMs);
74406
+ })]);
74407
+ } finally {
74408
+ if (timer) clearTimeout(timer);
74409
+ }
74410
+ }
74252
74411
  var chromeRemoteInterface = "default" in import_chrome_remote_interface ? import_chrome_remote_interface.default : import_chrome_remote_interface;
74253
74412
  function buildConnectionFromWsEndpoint(browserWsEndpoint) {
74254
74413
  const parsed = new URL(browserWsEndpoint);
@@ -74759,6 +74918,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74759
74918
  pageConsoleStates = /* @__PURE__ */ new Map();
74760
74919
  pageNetworkStates = /* @__PURE__ */ new Map();
74761
74920
  pageDialogStates = /* @__PURE__ */ new Map();
74921
+ dialogWaiters = /* @__PURE__ */ new Map();
74762
74922
  activeTabId;
74763
74923
  versionString = "Chromium/unknown";
74764
74924
  constructor(browserClient, connection) {
@@ -74774,7 +74934,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74774
74934
  });
74775
74935
  const session = new CdpConnectedBrowserSession(await chromeRemoteInterface({ target: connection.browserWsEndpoint }), connection);
74776
74936
  session.versionString = version.Browser;
74777
- if ((await session.refreshTabs()).length === 0) await session.newTab();
74937
+ await session.refreshTabs();
74778
74938
  await session.getActivePageClient().catch(() => void 0);
74779
74939
  return session;
74780
74940
  }
@@ -74826,6 +74986,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74826
74986
  async click(target, options) {
74827
74987
  const pageClient = await this.getActivePageClient();
74828
74988
  const contextId = await this.getActiveUtilityContextId(pageClient);
74989
+ const tabId = await this.getActiveTabId();
74829
74990
  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
74991
  if (!point.ok || point.x === void 0 || point.y === void 0) {
74831
74992
  const isSelector = "selector" in target;
@@ -74859,7 +75020,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74859
75020
  modifiers: modifiersMask
74860
75021
  });
74861
75022
  await delay$1(options.clickHoldMs);
74862
- await pageClient.Input.dispatchMouseEvent({
75023
+ const releasePromise = pageClient.Input.dispatchMouseEvent({
74863
75024
  type: "mouseReleased",
74864
75025
  x: point.x,
74865
75026
  y: point.y,
@@ -74867,6 +75028,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74867
75028
  clickCount,
74868
75029
  modifiers: modifiersMask
74869
75030
  });
75031
+ await Promise.race([releasePromise, this.waitForDialog(tabId, options.clickHoldMs + 1e3)]);
74870
75032
  }
74871
75033
  }
74872
75034
  async drag(start, end, options) {
@@ -75122,24 +75284,72 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75122
75284
  }
75123
75285
  }
75124
75286
  async handleDialog(accept, promptText) {
75125
- const tabId = await this.getActiveTabId();
75287
+ const tabId = this.dialogTabId();
75126
75288
  if (!this.pageDialogStates.has(tabId)) throw new McpToolError("no_dialog", "No dialog visible.");
75127
- const pageClient = await this.getActivePageClient();
75289
+ const pageClient = this.pageClients.get(tabId) ?? await this.getActivePageClient();
75128
75290
  this.pageDialogStates.delete(tabId);
75129
75291
  await pageClient.Page.handleJavaScriptDialog({
75130
75292
  accept,
75131
75293
  ...promptText !== void 0 ? { promptText } : {}
75132
75294
  });
75133
75295
  }
75296
+ async hasDialog() {
75297
+ return this.pageDialogStates.size > 0;
75298
+ }
75299
+ waitForDialog(tabId, timeoutMs) {
75300
+ if (this.pageDialogStates.has(tabId)) return Promise.resolve();
75301
+ return new Promise((resolve) => {
75302
+ const waiter = { resolve: () => {
75303
+ if (waiter.timer) clearTimeout(waiter.timer);
75304
+ this.removeDialogWaiter(tabId, waiter);
75305
+ resolve();
75306
+ } };
75307
+ waiter.timer = setTimeout(() => waiter.resolve(), timeoutMs);
75308
+ const waiters = this.dialogWaiters.get(tabId) ?? /* @__PURE__ */ new Set();
75309
+ waiters.add(waiter);
75310
+ this.dialogWaiters.set(tabId, waiters);
75311
+ });
75312
+ }
75313
+ resolveDialogWaiters(tabId) {
75314
+ const waiters = this.dialogWaiters.get(tabId);
75315
+ if (!waiters) return;
75316
+ this.dialogWaiters.delete(tabId);
75317
+ for (const waiter of waiters) waiter.resolve();
75318
+ }
75319
+ removeDialogWaiter(tabId, waiter) {
75320
+ const waiters = this.dialogWaiters.get(tabId);
75321
+ if (!waiters) return;
75322
+ waiters.delete(waiter);
75323
+ if (waiters.size === 0) this.dialogWaiters.delete(tabId);
75324
+ }
75134
75325
  async networkRequests() {
75135
75326
  const tabId = await this.getActiveTabId();
75327
+ await this.hydratePerformanceResourceRequests(tabId);
75136
75328
  return this.ensureNetworkState(tabId).requests.map(cloneNetworkRequest);
75137
75329
  }
75138
75330
  async networkRequest(index) {
75139
75331
  const tabId = await this.getActiveTabId();
75332
+ await this.hydratePerformanceResourceRequests(tabId);
75140
75333
  const request = this.ensureNetworkState(tabId).requests[index - 1];
75141
75334
  return request ? cloneNetworkRequest(request) : void 0;
75142
75335
  }
75336
+ async fetchResponseBody(index) {
75337
+ const tabId = await this.getActiveTabId();
75338
+ const state = this.ensureNetworkState(tabId);
75339
+ const request = state.requests[index - 1];
75340
+ if (!request || !request.requestId) return request?.responseBody;
75341
+ if (!canReadResponseBody(request)) return;
75342
+ if (request.responseBody !== void 0) return request.responseBody;
75343
+ await waitForLoadingDone(state, request.requestId, 5e3).catch(() => void 0);
75344
+ if (request.responseBody !== void 0) return request.responseBody;
75345
+ if (state.bodyRead.has(request.requestId)) return;
75346
+ state.bodyRead.add(request.requestId);
75347
+ const clientNetwork = (this.pageClients.get(tabId) ?? await this.getActivePageClient()).Network;
75348
+ if (!clientNetwork) return;
75349
+ const body = await clientNetwork.getResponseBody({ requestId: request.requestId }).catch(() => void 0);
75350
+ if (body) request.responseBody = body.base64Encoded ? Buffer.from(body.body, "base64").toString("utf8") : body.body;
75351
+ return request.responseBody;
75352
+ }
75143
75353
  async runCodeUnsafe(code) {
75144
75354
  const pageClient = await this.getActivePageClient();
75145
75355
  const contextId = await this.getActiveUtilityContextId(pageClient);
@@ -75199,6 +75409,12 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75199
75409
  if (!activeTab) throw new McpToolError("no_active_tab", "No active tab is available.");
75200
75410
  return activeTab.id;
75201
75411
  }
75412
+ dialogTabId() {
75413
+ if (this.activeTabId && this.pageDialogStates.has(this.activeTabId)) return this.activeTabId;
75414
+ const tabId = this.pageDialogStates.keys().next().value;
75415
+ if (!tabId) throw new McpToolError("no_dialog", "No dialog visible.");
75416
+ return tabId;
75417
+ }
75202
75418
  async getActivePageClient() {
75203
75419
  const activeTab = (await this.refreshTabs()).find((tab) => tab.active);
75204
75420
  if (!activeTab) throw new McpToolError("no_active_tab", "No active tab is available.");
@@ -75280,6 +75496,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75280
75496
  ...event.defaultPrompt !== void 0 ? { defaultPrompt: event.defaultPrompt } : {},
75281
75497
  ...event.url !== void 0 ? { url: event.url } : {}
75282
75498
  });
75499
+ this.resolveDialogWaiters(tabId);
75283
75500
  });
75284
75501
  this.installNetworkCollection(tabId, client);
75285
75502
  }
@@ -75318,20 +75535,29 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75318
75535
  });
75319
75536
  client.Network.loadingFinished(async (event) => {
75320
75537
  const request = state.byRequestId.get(event.requestId);
75321
- if (!request) return;
75538
+ if (!request) {
75539
+ resolveLoadingDone(state, event.requestId, true);
75540
+ return;
75541
+ }
75322
75542
  const startedAt = state.startedAt.get(event.requestId);
75323
75543
  if (startedAt !== void 0 && event.timestamp !== void 0) request.durationMs = Math.round(event.timestamp * 1e3 - startedAt);
75324
- if (canReadResponseBody(request)) {
75544
+ if (canReadResponseBody(request) && !state.bodyRead.has(event.requestId)) {
75545
+ state.bodyRead.add(event.requestId);
75325
75546
  const body = await client.Network?.getResponseBody({ requestId: event.requestId }).catch(() => void 0);
75326
75547
  if (body) request.responseBody = body.base64Encoded ? Buffer.from(body.body, "base64").toString("utf8") : body.body;
75327
75548
  }
75549
+ resolveLoadingDone(state, event.requestId, true);
75328
75550
  });
75329
75551
  client.Network.loadingFailed((event) => {
75330
75552
  const request = state.byRequestId.get(event.requestId);
75331
- if (!request) return;
75553
+ if (!request) {
75554
+ resolveLoadingDone(state, event.requestId, false);
75555
+ return;
75556
+ }
75332
75557
  request.failureText = event.errorText ?? "Unknown error";
75333
75558
  const startedAt = state.startedAt.get(event.requestId);
75334
75559
  if (startedAt !== void 0 && event.timestamp !== void 0) request.durationMs = Math.round(event.timestamp * 1e3 - startedAt);
75560
+ resolveLoadingDone(state, event.requestId, false);
75335
75561
  });
75336
75562
  }
75337
75563
  ensureConsoleState(tabId) {
@@ -75353,7 +75579,10 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75353
75579
  state = {
75354
75580
  requests: [],
75355
75581
  byRequestId: /* @__PURE__ */ new Map(),
75356
- startedAt: /* @__PURE__ */ new Map()
75582
+ startedAt: /* @__PURE__ */ new Map(),
75583
+ hydratedPerformanceResources: false,
75584
+ loadingDone: /* @__PURE__ */ new Map(),
75585
+ bodyRead: /* @__PURE__ */ new Set()
75357
75586
  };
75358
75587
  this.pageNetworkStates.set(tabId, state);
75359
75588
  }
@@ -75369,10 +75598,79 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75369
75598
  this.pageNetworkStates.set(tabId, {
75370
75599
  requests: [],
75371
75600
  byRequestId: /* @__PURE__ */ new Map(),
75372
- startedAt: /* @__PURE__ */ new Map()
75601
+ startedAt: /* @__PURE__ */ new Map(),
75602
+ hydratedPerformanceResources: false,
75603
+ loadingDone: /* @__PURE__ */ new Map(),
75604
+ bodyRead: /* @__PURE__ */ new Set()
75373
75605
  });
75374
75606
  this.pageDialogStates.delete(tabId);
75375
75607
  }
75608
+ async hydratePerformanceResourceRequests(tabId) {
75609
+ const state = this.ensureNetworkState(tabId);
75610
+ if (state.hydratedPerformanceResources) return;
75611
+ state.hydratedPerformanceResources = true;
75612
+ const pageClient = await this.getActivePageClient();
75613
+ const contextId = await this.getActiveUtilityContextId(pageClient);
75614
+ const documentRequest = await evaluateCdp(pageClient, String.raw`() => {
75615
+ const navigation = performance.getEntriesByType("navigation")[0];
75616
+ return {
75617
+ url: String(location.href || ""),
75618
+ duration: navigation ? Math.round(Number(navigation.duration || 0)) : undefined
75619
+ };
75620
+ }`, void 0, contextId).catch(() => void 0);
75621
+ if (documentRequest?.url && !Array.from(state.byRequestId.values()).some((request) => request.url === documentRequest.url)) {
75622
+ const requestId = `performance:navigation:${documentRequest.url}`;
75623
+ const request = {
75624
+ index: state.requests.length + 1,
75625
+ requestId,
75626
+ method: "GET",
75627
+ url: documentRequest.url,
75628
+ resourceType: "document",
75629
+ requestHeaders: {},
75630
+ status: 200,
75631
+ statusText: "OK",
75632
+ ...documentRequest.duration !== void 0 ? { durationMs: documentRequest.duration } : {}
75633
+ };
75634
+ state.requests.push(request);
75635
+ state.byRequestId.set(requestId, request);
75636
+ }
75637
+ const resources = await evaluateCdp(pageClient, String.raw`() => performance.getEntriesByType("resource").map((entry) => ({
75638
+ name: String(entry.name || ""),
75639
+ initiatorType: String(entry.initiatorType || "other"),
75640
+ duration: Math.round(Number(entry.duration || 0)),
75641
+ responseStatus: typeof entry.responseStatus === "number" ? entry.responseStatus : undefined
75642
+ }))`, void 0, contextId).catch(() => []);
75643
+ for (const resource of resources) {
75644
+ if (!resource.name || Array.from(state.byRequestId.values()).some((request) => request.url === resource.name)) continue;
75645
+ const status = resource.responseStatus && resource.responseStatus > 0 ? resource.responseStatus : await this.probeResourceStatus(pageClient, contextId, resource.name);
75646
+ const requestId = `performance:${resource.name}`;
75647
+ const request = {
75648
+ index: state.requests.length + 1,
75649
+ requestId,
75650
+ method: "GET",
75651
+ url: resource.name,
75652
+ resourceType: normalizeResourceType(resource.initiatorType),
75653
+ requestHeaders: {},
75654
+ ...status !== void 0 ? {
75655
+ status,
75656
+ statusText: statusTextForStatus(status)
75657
+ } : {},
75658
+ ...resource.duration !== void 0 ? { durationMs: resource.duration } : {}
75659
+ };
75660
+ state.requests.push(request);
75661
+ state.byRequestId.set(requestId, request);
75662
+ }
75663
+ }
75664
+ async probeResourceStatus(pageClient, contextId, url) {
75665
+ return evaluateCdp(pageClient, String.raw`async (url) => {
75666
+ try {
75667
+ const response = await fetch(url, { method: "HEAD", cache: "no-store" });
75668
+ return response.status;
75669
+ } catch {
75670
+ return undefined;
75671
+ }
75672
+ }`, url, contextId).catch(() => void 0);
75673
+ }
75376
75674
  addConsoleMessage(tabId, message) {
75377
75675
  const state = this.ensureConsoleState(tabId);
75378
75676
  if (!shouldIncludeConsoleMessage(message.type)) return;
@@ -75506,6 +75804,45 @@ function canReadResponseBody(request) {
75506
75804
  if (request.failureText || request.status === void 0) return false;
75507
75805
  return request.status !== 204 && request.status !== 304 && !(request.status >= 100 && request.status < 200);
75508
75806
  }
75807
+ function loadingDoneEntry(state, requestId) {
75808
+ let entry = state.loadingDone.get(requestId);
75809
+ if (!entry) {
75810
+ let resolve;
75811
+ let reject;
75812
+ entry = {
75813
+ promise: new Promise((res, rej) => {
75814
+ resolve = res;
75815
+ reject = rej;
75816
+ }),
75817
+ resolve,
75818
+ reject
75819
+ };
75820
+ state.loadingDone.set(requestId, entry);
75821
+ }
75822
+ return entry;
75823
+ }
75824
+ function resolveLoadingDone(state, requestId, success) {
75825
+ const entry = state.loadingDone.get(requestId);
75826
+ if (!entry) return;
75827
+ state.loadingDone.delete(requestId);
75828
+ if (success) entry.resolve();
75829
+ else entry.reject(/* @__PURE__ */ new Error("Request failed before the response body was available."));
75830
+ }
75831
+ async function waitForLoadingDone(state, requestId, timeoutMs) {
75832
+ const entry = loadingDoneEntry(state, requestId);
75833
+ await Promise.race([entry.promise, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
75834
+ }
75835
+ function statusTextForStatus(status) {
75836
+ if (status === 200) return "OK";
75837
+ if (status === 204) return "No Content";
75838
+ if (status === 304) return "Not Modified";
75839
+ if (status === 400) return "Bad Request";
75840
+ if (status === 401) return "Unauthorized";
75841
+ if (status === 403) return "Forbidden";
75842
+ if (status === 404) return "Not Found";
75843
+ if (status === 500) return "Internal Server Error";
75844
+ return "";
75845
+ }
75509
75846
  function cloneNetworkRequest(request) {
75510
75847
  return {
75511
75848
  ...request,
@@ -75584,6 +75921,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75584
75921
  pageConsoleStates = /* @__PURE__ */ new Map();
75585
75922
  pageNetworkStates = /* @__PURE__ */ new Map();
75586
75923
  pageDialogStates = /* @__PURE__ */ new Map();
75924
+ dialogWaiters = /* @__PURE__ */ new Map();
75587
75925
  bidiListeners = /* @__PURE__ */ new Map();
75588
75926
  responseDataCollector;
75589
75927
  activeTabId;
@@ -75602,7 +75940,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75602
75940
  const session = new BidiConnectedBrowserSession(client);
75603
75941
  session.ownsSession = await ensureMcpBiDiSession(client, args.endpoint, args.sessionId);
75604
75942
  await session.initialize();
75605
- if ((await session.refreshTabs()).length === 0) await session.newTab();
75943
+ await session.refreshTabs();
75606
75944
  return session;
75607
75945
  }
75608
75946
  async version() {
@@ -75645,10 +75983,13 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75645
75983
  }
75646
75984
  async snapshot(request = {}) {
75647
75985
  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
- });
75986
+ return {
75987
+ ...toBrowserSnapshot(await retryUntilReady(() => evaluateBiDi(this.client, tabId, PLAYWRIGHT_ARIA_SNAPSHOT_EVALUATE_SOURCE, toAriaSnapshotPayload(request))), request, {
75988
+ console: this.consoleSummary(tabId),
75989
+ consoleLink: await this.takeConsoleLink(tabId)
75990
+ }),
75991
+ retryable: true
75992
+ };
75652
75993
  }
75653
75994
  async consoleMessages(level = "info", all = false) {
75654
75995
  const activeTabId = await this.getActiveTabId();
@@ -75751,10 +76092,12 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75751
76092
  parameters: { pointerType: "mouse" },
75752
76093
  actions: pointerActions
75753
76094
  });
75754
- await this.client.inputPerformActions({
76095
+ const performPromise = this.client.inputPerformActions({
75755
76096
  context: tabId,
75756
76097
  actions
75757
76098
  });
76099
+ await Promise.race([performPromise, this.waitForDialog(tabId, options.clickHoldMs + 5e3)]);
76100
+ performPromise.catch(() => {});
75758
76101
  await this.client.inputReleaseActions({ context: tabId }).catch(() => {});
75759
76102
  }
75760
76103
  async drag(start, end, options) {
@@ -76055,14 +76398,17 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76055
76398
  }
76056
76399
  }
76057
76400
  async handleDialog(accept, promptText) {
76058
- const tabId = await this.getActiveTabId();
76401
+ const tabId = this.dialogTabId();
76059
76402
  if (!this.pageDialogStates.has(tabId)) throw new McpToolError("no_dialog", "No dialog visible.");
76060
76403
  this.pageDialogStates.delete(tabId);
76061
- await this.client.browsingContextHandleUserPrompt({
76404
+ await withBiDiTimeout(this.client.browsingContextHandleUserPrompt({
76062
76405
  context: tabId,
76063
76406
  accept,
76064
76407
  ...promptText !== void 0 ? { userText: promptText } : {}
76065
- });
76408
+ }), 5e3);
76409
+ }
76410
+ async hasDialog() {
76411
+ return this.pageDialogStates.size > 0;
76066
76412
  }
76067
76413
  async networkRequests() {
76068
76414
  const tabId = await this.getActiveTabId();
@@ -76073,6 +76419,15 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76073
76419
  const request = this.ensureNetworkState(tabId).requests[index - 1];
76074
76420
  return request ? cloneNetworkRequest(request) : void 0;
76075
76421
  }
76422
+ async fetchResponseBody(index) {
76423
+ const tabId = await this.getActiveTabId();
76424
+ const request = this.ensureNetworkState(tabId).requests[index - 1];
76425
+ if (!request || !request.requestId) return request?.responseBody;
76426
+ if (request.responseBody !== void 0) return request.responseBody;
76427
+ const body = await this.getResponseBody(request.requestId).catch(() => void 0);
76428
+ if (body !== void 0) request.responseBody = body;
76429
+ return request.responseBody;
76430
+ }
76076
76431
  async runCodeUnsafe(code) {
76077
76432
  return this.evaluate(`async () => {
76078
76433
  const fn = eval(${JSON.stringify(`(${code})`)});
@@ -76088,6 +76443,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76088
76443
  }`);
76089
76444
  }
76090
76445
  async initialize() {
76446
+ this.attachBiDiListeners();
76091
76447
  await this.client.sessionSubscribe({ events: [
76092
76448
  "browsingContext.userPromptOpened",
76093
76449
  "log.entryAdded",
@@ -76101,7 +76457,6 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76101
76457
  maxEncodedDataSize: 1e7
76102
76458
  }).catch(() => void 0);
76103
76459
  this.responseDataCollector = collectorResult?.collector;
76104
- this.attachBiDiListeners();
76105
76460
  }
76106
76461
  attachBiDiListeners() {
76107
76462
  this.attachBiDiListener("log.entryAdded", (payload) => this.handleLogEntry(payload));
@@ -76136,6 +76491,45 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76136
76491
  targetArg(target) {
76137
76492
  return "nodeToken" in target ? { nodeToken: target.nodeToken } : { selector: target.selector };
76138
76493
  }
76494
+ dialogTabId() {
76495
+ if (this.activeTabId && this.pageDialogStates.has(this.activeTabId)) return this.activeTabId;
76496
+ const tabId = this.pageDialogStates.keys().next().value;
76497
+ if (!tabId) throw new McpToolError("no_dialog", "No dialog visible.");
76498
+ return tabId;
76499
+ }
76500
+ waitForDialog(tabId, timeoutMs) {
76501
+ if (this.pageDialogStates.has(tabId)) return Promise.resolve();
76502
+ return new Promise((resolve, reject) => {
76503
+ const waiter = {
76504
+ resolve: () => {
76505
+ if (waiter.timer) clearTimeout(waiter.timer);
76506
+ this.removeDialogWaiter(tabId, waiter);
76507
+ resolve();
76508
+ },
76509
+ reject: (error) => {
76510
+ if (waiter.timer) clearTimeout(waiter.timer);
76511
+ this.removeDialogWaiter(tabId, waiter);
76512
+ reject(error);
76513
+ }
76514
+ };
76515
+ waiter.timer = setTimeout(() => waiter.reject?.(/* @__PURE__ */ new Error("Timed out waiting for dialog.")), timeoutMs);
76516
+ const waiters = this.dialogWaiters.get(tabId) ?? /* @__PURE__ */ new Set();
76517
+ waiters.add(waiter);
76518
+ this.dialogWaiters.set(tabId, waiters);
76519
+ });
76520
+ }
76521
+ resolveDialogWaiters(tabId) {
76522
+ const waiters = this.dialogWaiters.get(tabId);
76523
+ if (!waiters) return;
76524
+ this.dialogWaiters.delete(tabId);
76525
+ for (const waiter of waiters) waiter.resolve();
76526
+ }
76527
+ removeDialogWaiter(tabId, waiter) {
76528
+ const waiters = this.dialogWaiters.get(tabId);
76529
+ if (!waiters) return;
76530
+ waiters.delete(waiter);
76531
+ if (waiters.size === 0) this.dialogWaiters.delete(tabId);
76532
+ }
76139
76533
  async actionPoint(tabId, target) {
76140
76534
  const source = "nodeToken" in target ? ACTION_POINT_EVALUATE_SOURCE : ACTION_POINT_BY_SELECTOR_SOURCE;
76141
76535
  const point = await evaluateBiDi(this.client, tabId, source, this.targetArg(target));
@@ -76192,6 +76586,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76192
76586
  type: event.type ?? "alert",
76193
76587
  ...event.defaultValue !== void 0 ? { defaultPrompt: event.defaultValue } : {}
76194
76588
  });
76589
+ this.resolveDialogWaiters(event.context);
76195
76590
  }
76196
76591
  handleBeforeRequestSent(payload) {
76197
76592
  const event = parseBidiNetworkEvent(payload);
@@ -76294,7 +76689,10 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76294
76689
  state = {
76295
76690
  requests: [],
76296
76691
  byRequestId: /* @__PURE__ */ new Map(),
76297
- startedAt: /* @__PURE__ */ new Map()
76692
+ startedAt: /* @__PURE__ */ new Map(),
76693
+ hydratedPerformanceResources: false,
76694
+ loadingDone: /* @__PURE__ */ new Map(),
76695
+ bodyRead: /* @__PURE__ */ new Set()
76298
76696
  };
76299
76697
  this.pageNetworkStates.set(tabId, state);
76300
76698
  }
@@ -76310,7 +76708,10 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76310
76708
  this.pageNetworkStates.set(tabId, {
76311
76709
  requests: [],
76312
76710
  byRequestId: /* @__PURE__ */ new Map(),
76313
- startedAt: /* @__PURE__ */ new Map()
76711
+ startedAt: /* @__PURE__ */ new Map(),
76712
+ hydratedPerformanceResources: false,
76713
+ loadingDone: /* @__PURE__ */ new Map(),
76714
+ bodyRead: /* @__PURE__ */ new Set()
76314
76715
  });
76315
76716
  this.pageDialogStates.delete(tabId);
76316
76717
  }
@@ -76519,7 +76920,7 @@ var McpRuntime = class {
76519
76920
  snapshot
76520
76921
  };
76521
76922
  lastAttempt = captured;
76522
- if (snapshot.text.trim().length > 0 || currentActiveTab.url === "about:blank") return captured;
76923
+ if (!snapshot.retryable || snapshot.text.trim().length > 0 || currentActiveTab.url === "about:blank") return captured;
76523
76924
  await delay(150 * (attempt + 1));
76524
76925
  }
76525
76926
  if (!lastAttempt) throw new McpToolError("action_failed", "Unable to capture page snapshot.");
@@ -76542,6 +76943,7 @@ var McpRuntime = class {
76542
76943
  this.invalidateSnapshot();
76543
76944
  this.pendingFileUploadTarget = opensFileChooser ? resolved : void 0;
76544
76945
  if (this.snapshotMode === "none") return;
76946
+ if (await session.hasDialog()) return;
76545
76947
  return this.snapshot();
76546
76948
  }
76547
76949
  async hover(target) {
@@ -76551,13 +76953,16 @@ var McpRuntime = class {
76551
76953
  this.invalidateSnapshot();
76552
76954
  this.pendingFileUploadTarget = void 0;
76553
76955
  if (this.snapshotMode === "none") return;
76956
+ if (await session.hasDialog()) return;
76554
76957
  return this.snapshot();
76555
76958
  }
76556
76959
  async navigate(url) {
76557
- await this.requireConnected().navigate(normalizeNavigationUrl(url));
76960
+ const session = this.requireConnected();
76961
+ await session.navigate(normalizeNavigationUrl(url));
76558
76962
  this.invalidateSnapshot();
76559
76963
  this.pendingFileUploadTarget = void 0;
76560
76964
  if (this.snapshotMode === "none") return;
76965
+ if (await session.hasDialog()) return;
76561
76966
  return this.snapshot();
76562
76967
  }
76563
76968
  async type(ref, text, opts) {
@@ -76706,6 +77111,9 @@ var McpRuntime = class {
76706
77111
  async networkRequest(index) {
76707
77112
  return this.requireConnected().networkRequest(index);
76708
77113
  }
77114
+ async fetchResponseBody(index) {
77115
+ return this.requireConnected().fetchResponseBody(index);
77116
+ }
76709
77117
  async runCodeUnsafe(code) {
76710
77118
  const result = await this.requireConnected().runCodeUnsafe(code);
76711
77119
  this.invalidateSnapshot();
@@ -76742,6 +77150,10 @@ var McpRuntime = class {
76742
77150
  hasPendingFileUploadTarget() {
76743
77151
  return !!this.pendingFileUploadTarget;
76744
77152
  }
77153
+ async hasDialog() {
77154
+ if (!this.connection) return false;
77155
+ return this.connection.session.hasDialog();
77156
+ }
76745
77157
  async close() {
76746
77158
  this.invalidateSnapshot();
76747
77159
  this.pendingFileUploadTarget = void 0;