stagehand 4.2.0a0.dev1561__tar.gz → 4.2.0a0.dev1563__tar.gz

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 (36) hide show
  1. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/PKG-INFO +1 -1
  2. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/pyproject.toml +1 -1
  3. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/_extension/service-worker.js +187 -128
  4. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/_generated/input_types.py +1 -0
  5. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/_generated/models.py +2 -0
  6. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/_generated/protocol_version.py +1 -1
  7. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/page.py +10 -3
  8. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/rpc_client.py +3 -2
  9. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/README.md +0 -0
  10. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/__init__.py +0 -0
  11. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/_extension/blank.html +0 -0
  12. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/_extension/content-script.js +0 -0
  13. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/_extension/manifest.json +0 -0
  14. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/_extension/offscreen/service-worker-heartbeat.html +0 -0
  15. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/_extension/offscreen/service-worker-heartbeat.js +0 -0
  16. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/_extension/wake-service-worker.html +0 -0
  17. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/_extension/wake-service-worker.js +0 -0
  18. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/_generated/__init__.py +0 -0
  19. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/_sdk_identity.py +0 -0
  20. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/_validation.py +0 -0
  21. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/browser.py +0 -0
  22. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/browser_clipboard.py +0 -0
  23. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/browser_context.py +0 -0
  24. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/browserbase_services.py +0 -0
  25. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/browserbase_session.py +0 -0
  26. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/cdp_client.py +0 -0
  27. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/client_models.py +0 -0
  28. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/client_types.py +0 -0
  29. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/extension_assets.py +0 -0
  30. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/file_upload.py +0 -0
  31. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/locator.py +0 -0
  32. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/py.typed +0 -0
  33. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/response.py +0 -0
  34. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/stagehand.py +0 -0
  35. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/timeouts.py +0 -0
  36. {stagehand-4.2.0a0.dev1561 → stagehand-4.2.0a0.dev1563}/src/stagehand/webmcp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stagehand
3
- Version: 4.2.0a0.dev1561
3
+ Version: 4.2.0a0.dev1563
4
4
  Summary: Browserbase's SDK for building browser agents
5
5
  Keywords: ai,browser,automation,web-scraping,testing
6
6
  Author: Browserbase
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stagehand"
3
- version = "4.2.0a0.dev1561"
3
+ version = "4.2.0a0.dev1563"
4
4
  description = "Browserbase's SDK for building browser agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -7363,7 +7363,7 @@ function isRecord$1(value) {
7363
7363
  }
7364
7364
  var package_default$1 = {
7365
7365
  name: "@browserbasehq/stagehand-protocol",
7366
- version: "2.1.0-alpha-836a0639f5bfbf2e011acc525da6a9515c544b8e",
7366
+ version: "2.1.0-alpha-5788121e60c2648654c2b36ec7755030f0015fab",
7367
7367
  "private": true,
7368
7368
  type: "module",
7369
7369
  exports: {
@@ -8324,7 +8324,10 @@ var SnapshotResultSchema = strictObject({
8324
8324
  xpathMap: record(string(), string()),
8325
8325
  urlMap: record(string(), string())
8326
8326
  }).meta({ id: "SnapshotResult" });
8327
- var PageSnapshotOptionsSchema = strictObject({ includeIframes: boolean().optional() }).meta({ id: "PageSnapshotOptions" });
8327
+ var PageSnapshotOptionsSchema = strictObject({
8328
+ timeout: number$1().nonnegative().optional().describe("Milliseconds for the whole snapshot call. Defaults to 20000. Zero disables the timeout."),
8329
+ includeIframes: boolean().optional()
8330
+ }).meta({ id: "PageSnapshotOptions" });
8328
8331
  var PageRefSchema = strictObject({
8329
8332
  pageId: string(),
8330
8333
  url: string().optional(),
@@ -14073,9 +14076,10 @@ var ExecutionContextRegistry = class {
14073
14076
  return this.fallbackByFrame.get(session)?.get(frameId) ?? null;
14074
14077
  }
14075
14078
  /** With progress, timeout limits each extension probe, not the whole wait.
14076
- * Frame traversal disables retries here so it can recheck session ownership.
14079
+ * Callers that handle failure themselves can disable retries without losing
14080
+ * the caller's deadline. Without progress, readiness makes a single attempt.
14077
14081
  */
14078
- async waitForLocatorWorld(session, frameId, timeout = 1e3, progress, retryUntilDeadline = true) {
14082
+ async waitForLocatorWorld(session, frameId, timeout = 1e3, progress, { readinessRetries = "until-deadline" } = {}) {
14079
14083
  while (true) {
14080
14084
  progress?.throwIfStopped();
14081
14085
  const extensionContextId = this.getExtensionWorld(session, frameId);
@@ -14088,7 +14092,7 @@ var ExecutionContextRegistry = class {
14088
14092
  progress?.throwIfStopped();
14089
14093
  if (progress && isCdpClosedError(extensionError)) throw extensionError;
14090
14094
  if (await this.isFallbackEligible(session, frameId, progress)) return this.fallbackWorld(await runLocatorStep(progress, "installing locator helpers", () => this.createFallbackWorld(session, frameId)));
14091
- if (!progress || !retryUntilDeadline) throw extensionError;
14095
+ if (!progress || readinessRetries === "none") throw extensionError;
14092
14096
  }
14093
14097
  }
14094
14098
  }
@@ -14389,8 +14393,9 @@ function buildLocatorInvocation(name, args) {
14389
14393
  //#endregion
14390
14394
  //#region understudy/selectorResolver.ts
14391
14395
  var FrameSelectorResolver = class {
14392
- constructor(frame) {
14396
+ constructor(frame, readinessOptions = {}) {
14393
14397
  this.frame = frame;
14398
+ this.readinessOptions = readinessOptions;
14394
14399
  }
14395
14400
  static parseSelector(raw) {
14396
14401
  const trimmed = raw.trim();
@@ -14466,7 +14471,7 @@ var FrameSelectorResolver = class {
14466
14471
  async resolveElements(helper, value, limit, progress) {
14467
14472
  progress?.throwIfStopped();
14468
14473
  if (limit <= 0) return [];
14469
- const { contextId } = await executionContexts.waitForLocatorWorld(this.frame.session, this.frame.frameId, 1e3, progress);
14474
+ const { contextId } = await executionContexts.waitForLocatorWorld(this.frame.session, this.frame.frameId, 1e3, progress, this.readinessOptions);
14470
14475
  const results = [];
14471
14476
  try {
14472
14477
  for (let index = 0; index < limit; index += 1) {
@@ -14488,13 +14493,13 @@ var FrameSelectorResolver = class {
14488
14493
  }
14489
14494
  async countCss(selector, progress) {
14490
14495
  const session = this.frame.session;
14491
- const { contextId } = await executionContexts.waitForLocatorWorld(session, this.frame.frameId, 1e3, progress);
14496
+ const { contextId } = await executionContexts.waitForLocatorWorld(session, this.frame.frameId, 1e3, progress, this.readinessOptions);
14492
14497
  const primaryExpr = buildLocatorInvocation("countCssMatchesPrimary", [JSON.stringify(selector)]);
14493
14498
  return this.evaluateCount(primaryExpr, contextId, progress);
14494
14499
  }
14495
14500
  async countText(value, progress) {
14496
14501
  const session = this.frame.session;
14497
- const { contextId: ctxId } = await executionContexts.waitForLocatorWorld(session, this.frame.frameId, 1e3, progress);
14502
+ const { contextId: ctxId } = await executionContexts.waitForLocatorWorld(session, this.frame.frameId, 1e3, progress, this.readinessOptions);
14498
14503
  const expr = buildLocatorInvocation("countTextMatches", [JSON.stringify(value)]);
14499
14504
  try {
14500
14505
  const evalRes = await runLocatorStep(progress, "counting elements", () => session.send("Runtime.evaluate", {
@@ -14525,7 +14530,7 @@ var FrameSelectorResolver = class {
14525
14530
  }
14526
14531
  async countXPath(value, progress) {
14527
14532
  const session = this.frame.session;
14528
- const { contextId: ctxId } = await executionContexts.waitForLocatorWorld(session, this.frame.frameId, 1e3, progress);
14533
+ const { contextId: ctxId } = await executionContexts.waitForLocatorWorld(session, this.frame.frameId, 1e3, progress, this.readinessOptions);
14529
14534
  const expr = buildLocatorInvocation("countXPathMatchesMainWorld", [JSON.stringify(value)]);
14530
14535
  try {
14531
14536
  const evalRes = await runLocatorStep(progress, "counting elements", () => session.send("Runtime.evaluate", {
@@ -15533,7 +15538,7 @@ async function ensureChildFrameReady(page, childFrameId, budgetMs, progress) {
15533
15538
  const remaining = progress?.remainingMs() ?? deadline - Date.now();
15534
15539
  if (remaining <= 0) break;
15535
15540
  try {
15536
- await executionContexts.waitForLocatorWorld(session, childFrameId, Math.min(remaining, LOCATOR_WORLD_ATTEMPT_TIMEOUT_MS), progress, false);
15541
+ await executionContexts.waitForLocatorWorld(session, childFrameId, Math.min(remaining, LOCATOR_WORLD_ATTEMPT_TIMEOUT_MS), progress, { readinessRetries: "none" });
15537
15542
  progress?.throwIfStopped();
15538
15543
  if (page.getSessionForFrame(childFrameId) === session) return;
15539
15544
  } catch (error) {
@@ -15638,7 +15643,8 @@ var IFRAME_STEP_RE = /^i?frame(?:\[\d+])?$/i;
15638
15643
  * - the tail XPath (within the target frame)
15639
15644
  * - the absolute XPath prefix up to the iframe element hosting that frame
15640
15645
  */
15641
- async function resolveFocusFrameAndTail(page, absoluteXPath, parentByFrame, rootId) {
15646
+ async function resolveFocusFrameAndTail(page, absoluteXPath, parentByFrame, rootId, progress) {
15647
+ progress?.throwIfStopped();
15642
15648
  const steps = parseXPathToSteps(absoluteXPath);
15643
15649
  let ctxFrameId = rootId;
15644
15650
  let buf = [];
@@ -15647,33 +15653,36 @@ async function resolveFocusFrameAndTail(page, absoluteXPath, parentByFrame, root
15647
15653
  if (!buf.length) return;
15648
15654
  const selectorForIframe = buildXPathFromSteps$1(buf);
15649
15655
  const parentSess = page.getSessionForFrame(ctxFrameId);
15650
- const objectId = await resolveObjectIdForXPath(parentSess, selectorForIframe, ctxFrameId);
15656
+ const objectId = await resolveObjectIdForXPath(parentSess, selectorForIframe, ctxFrameId, 0, progress);
15651
15657
  if (!objectId) throw iframeResolutionError(selectorForIframe, "Failed to resolve iframe element by XPath");
15652
15658
  try {
15653
- await parentSess.send("DOM.enable").catch(() => {});
15654
- const iframeBackendNodeId = (await parentSess.send("DOM.describeNode", { objectId })).node.backendNodeId;
15659
+ await runLocatorStep(progress, "enabling DOM", () => parentSess.send("DOM.enable").catch(() => {}));
15660
+ const iframeBackendNodeId = (await runLocatorStep(progress, "reading snapshot iframe", () => parentSess.send("DOM.describeNode", { objectId }))).node.backendNodeId;
15655
15661
  let childFrameId;
15656
15662
  for (const fid of listChildrenOf(parentByFrame, ctxFrameId)) try {
15657
- const { backendNodeId } = await parentSess.send("DOM.getFrameOwner", { frameId: fid });
15663
+ const { backendNodeId } = await runLocatorStep(progress, "finding snapshot iframe owner", () => parentSess.send("DOM.getFrameOwner", { frameId: fid }));
15658
15664
  if (backendNodeId === iframeBackendNodeId) {
15659
15665
  childFrameId = fid;
15660
15666
  break;
15661
15667
  }
15662
15668
  } catch {
15669
+ progress?.throwIfStopped();
15663
15670
  continue;
15664
15671
  }
15665
15672
  if (!childFrameId) throw iframeResolutionError(selectorForIframe, "Could not map iframe to child frameId");
15666
15673
  absPrefix = prefixXPath(absPrefix || "/", selectorForIframe);
15667
15674
  ctxFrameId = childFrameId;
15668
15675
  } finally {
15669
- await parentSess.send("Runtime.releaseObject", { objectId }).catch(() => {});
15676
+ await releaseSnapshotObject(parentSess, objectId, progress);
15670
15677
  }
15671
15678
  buf = [];
15672
15679
  };
15673
15680
  for (const st of steps) {
15681
+ progress?.throwIfStopped();
15674
15682
  buf.push(st);
15675
15683
  if (IFRAME_STEP_RE.test(st.name)) await flushIntoChild();
15676
15684
  }
15685
+ progress?.throwIfStopped();
15677
15686
  const tailXPath = buildXPathFromSteps$1(buf);
15678
15687
  return {
15679
15688
  targetFrameId: ctxFrameId,
@@ -15682,33 +15691,36 @@ async function resolveFocusFrameAndTail(page, absoluteXPath, parentByFrame, root
15682
15691
  };
15683
15692
  }
15684
15693
  /** Resolve focus frame and tail CSS selector using '>>' to hop iframes. */
15685
- async function resolveCssFocusFrameAndTail(page, rawSelector, parentByFrame, rootId) {
15694
+ async function resolveCssFocusFrameAndTail(page, rawSelector, parentByFrame, rootId, progress) {
15695
+ progress?.throwIfStopped();
15686
15696
  const parts = rawSelector.split(">>").map((s) => s.trim()).filter(Boolean);
15687
15697
  let ctxFrameId = rootId;
15688
15698
  const absPrefix = "";
15689
15699
  for (let i = 0; i < Math.max(0, parts.length - 1); i++) {
15690
15700
  const parentSess = page.getSessionForFrame(ctxFrameId);
15691
- const objectId = await resolveObjectIdForCss(parentSess, parts[i], ctxFrameId);
15701
+ const objectId = await resolveObjectIdForCss(parentSess, parts[i], ctxFrameId, 0, progress);
15692
15702
  if (!objectId) throw iframeResolutionError(parts[i], "Failed to resolve iframe via CSS hop");
15693
15703
  try {
15694
- await parentSess.send("DOM.enable").catch(() => {});
15695
- const iframeBackendNodeId = (await parentSess.send("DOM.describeNode", { objectId })).node.backendNodeId;
15704
+ await runLocatorStep(progress, "enabling DOM", () => parentSess.send("DOM.enable").catch(() => {}));
15705
+ const iframeBackendNodeId = (await runLocatorStep(progress, "reading snapshot iframe", () => parentSess.send("DOM.describeNode", { objectId }))).node.backendNodeId;
15696
15706
  let childFrameId;
15697
15707
  for (const fid of listChildrenOf(parentByFrame, ctxFrameId)) try {
15698
- const { backendNodeId } = await parentSess.send("DOM.getFrameOwner", { frameId: fid });
15708
+ const { backendNodeId } = await runLocatorStep(progress, "finding snapshot iframe owner", () => parentSess.send("DOM.getFrameOwner", { frameId: fid }));
15699
15709
  if (backendNodeId === iframeBackendNodeId) {
15700
15710
  childFrameId = fid;
15701
15711
  break;
15702
15712
  }
15703
15713
  } catch {
15714
+ progress?.throwIfStopped();
15704
15715
  continue;
15705
15716
  }
15706
15717
  if (!childFrameId) throw iframeResolutionError(parts[i], "Could not map CSS iframe hop to child frameId");
15707
15718
  ctxFrameId = childFrameId;
15708
15719
  } finally {
15709
- await parentSess.send("Runtime.releaseObject", { objectId }).catch(() => {});
15720
+ await releaseSnapshotObject(parentSess, objectId, progress);
15710
15721
  }
15711
15722
  }
15723
+ progress?.throwIfStopped();
15712
15724
  const tailSelector = parts[parts.length - 1] ?? "*";
15713
15725
  return {
15714
15726
  targetFrameId: ctxFrameId,
@@ -15720,35 +15732,36 @@ function iframeResolutionError(frameUrl, message) {
15720
15732
  return /* @__PURE__ */ new Error(`Unable to resolve frameId for iframe with URL: ${frameUrl} Full error: ${message}`);
15721
15733
  }
15722
15734
  /** Resolve an XPath to a Runtime remoteObjectId in the given CDP session. */
15723
- async function resolveObjectIdForXPath(session, xpath, frameId, index = 0) {
15724
- let contextId;
15725
- if (frameId) contextId = (await executionContexts.waitForLocatorWorld(session, frameId, 800)).contextId;
15726
- const expr = buildLocatorInvocation("resolveXPathMainWorld", [JSON.stringify(xpath), JSON.stringify(index)]);
15727
- const { result, exceptionDetails } = await session.send("Runtime.evaluate", {
15728
- expression: expr,
15735
+ async function resolveObjectIdForXPath(session, xpath, frameId, index = 0, progress) {
15736
+ return resolveObjectId(session, buildLocatorInvocation("resolveXPathMainWorld", [JSON.stringify(xpath), JSON.stringify(index)]), frameId, progress);
15737
+ }
15738
+ /** Resolve a CSS selector (supports '>>' within the same frame only) to a Runtime objectId. */
15739
+ async function resolveObjectIdForCss(session, selector, frameId, index = 0, progress) {
15740
+ return resolveObjectId(session, buildLocatorInvocation("resolveCssSelector", [JSON.stringify(selector), JSON.stringify(index)]), frameId, progress);
15741
+ }
15742
+ async function resolveObjectId(session, expression, frameId, progress) {
15743
+ progress?.throwIfStopped();
15744
+ const contextId = frameId ? (await executionContexts.waitForLocatorWorld(session, frameId, 800, progress, { readinessRetries: "none" })).contextId : void 0;
15745
+ const release = (response) => releaseSnapshotObject(session, response.result?.objectId, progress);
15746
+ const response = await runLocatorStep(progress, "resolving snapshot focus", () => session.send("Runtime.evaluate", {
15747
+ expression,
15729
15748
  returnByValue: false,
15730
15749
  contextId,
15731
15750
  awaitPromise: true
15732
- });
15733
- if (exceptionDetails) return null;
15734
- return result?.objectId ?? null;
15751
+ }), release);
15752
+ if (response.exceptionDetails) {
15753
+ await release(response);
15754
+ progress?.throwIfStopped();
15755
+ return null;
15756
+ }
15757
+ return response.result?.objectId ?? null;
15735
15758
  }
15736
- /** Resolve a CSS selector (supports '>>' within the same frame only) to a Runtime objectId. */
15737
- async function resolveObjectIdForCss(session, selector, frameId, index = 0) {
15738
- let contextId;
15739
- if (frameId) contextId = (await executionContexts.waitForLocatorWorld(session, frameId, 800)).contextId;
15740
- const expression = buildLocatorInvocation("resolveCssSelector", [JSON.stringify(selector), JSON.stringify(index)]);
15741
- const evaluate = async (expression) => {
15742
- const { result, exceptionDetails } = await session.send("Runtime.evaluate", {
15743
- expression,
15744
- returnByValue: false,
15745
- contextId,
15746
- awaitPromise: true
15747
- });
15748
- if (exceptionDetails) return null;
15749
- return result?.objectId ?? null;
15750
- };
15751
- return evaluate(expression);
15759
+ /** Release snapshot references even after expiry, without replacing the read error. */
15760
+ async function releaseSnapshotObject(session, objectId, progress) {
15761
+ if (!objectId) return;
15762
+ const release = () => session.send("Runtime.releaseObject", { objectId }).catch(() => {});
15763
+ if (progress) await progress.cleanup(release);
15764
+ else await release();
15752
15765
  }
15753
15766
  function listChildrenOf(parentByFrame, parentId) {
15754
15767
  const out = [];
@@ -17307,31 +17320,33 @@ function normaliseSpaces(s) {
17307
17320
  * Fetch and prune the accessibility tree for a frame, optionally scoping the
17308
17321
  * output to a selector root for faster targeted snapshots.
17309
17322
  */
17310
- async function a11yForFrame(session, frameId, opts) {
17311
- await session.send("Accessibility.enable").catch(() => {});
17312
- await session.send("Runtime.enable").catch(() => {});
17313
- await session.send("DOM.enable").catch(() => {});
17323
+ async function a11yForFrame(session, frameId, opts, progress) {
17324
+ await runLocatorStep(progress, "enabling Accessibility", () => session.send("Accessibility.enable").catch(() => {}));
17325
+ await runLocatorStep(progress, "enabling Runtime", () => session.send("Runtime.enable").catch(() => {}));
17326
+ await runLocatorStep(progress, "enabling DOM", () => session.send("DOM.enable").catch(() => {}));
17314
17327
  let nodes = [];
17315
17328
  try {
17316
17329
  const params = frameId ? { frameId } : {};
17317
- ({nodes} = await session.send("Accessibility.getFullAXTree", params));
17330
+ ({nodes} = await runLocatorStep(progress, "reading snapshot accessibility tree", () => session.send("Accessibility.getFullAXTree", params)));
17318
17331
  } catch (e) {
17332
+ progress?.throwIfStopped();
17319
17333
  const msg = String(e?.message ?? e ?? "");
17320
17334
  if (!(msg.includes("Frame with the given") || msg.includes("does not belong to the target") || msg.includes("is not found")) || !frameId) throw e;
17321
- ({nodes} = await session.send("Accessibility.getFullAXTree"));
17335
+ ({nodes} = await runLocatorStep(progress, "reading snapshot accessibility tree", () => session.send("Accessibility.getFullAXTree")));
17322
17336
  }
17323
17337
  let scopeApplied = false;
17324
- const filteredNodes = (await (async () => {
17338
+ const nodesForOutline = await (async () => {
17325
17339
  const locator = opts.focusLocator;
17326
17340
  if (!locator) return nodes;
17327
17341
  const sel = locator.selector.trim();
17328
17342
  if (!sel) return nodes;
17343
+ let objectId = null;
17329
17344
  try {
17330
17345
  const looksLikeXPath = /^xpath=/i.test(sel) || sel.startsWith("/");
17331
17346
  const nth = locator.nth ?? 0;
17332
- const objectId = looksLikeXPath ? await resolveObjectIdForXPath(session, sel, frameId, nth) : await resolveObjectIdForCss(session, sel, frameId, nth);
17347
+ objectId = looksLikeXPath ? await resolveObjectIdForXPath(session, sel, frameId, nth, progress) : await resolveObjectIdForCss(session, sel, frameId, nth, progress);
17333
17348
  if (!objectId) return nodes;
17334
- const be = (await session.send("DOM.describeNode", { objectId })).node?.backendNodeId;
17349
+ const be = (await runLocatorStep(progress, "reading snapshot focus", () => session.send("DOM.describeNode", { objectId }))).node?.backendNodeId;
17335
17350
  if (typeof be !== "number") return nodes;
17336
17351
  const target = nodes.find((n) => n.backendDOMNodeId === be);
17337
17352
  if (!target) return nodes;
@@ -17352,9 +17367,14 @@ async function a11yForFrame(session, frameId, opts) {
17352
17367
  parentId: void 0
17353
17368
  } : n);
17354
17369
  } catch {
17370
+ progress?.throwIfStopped();
17355
17371
  return nodes;
17372
+ } finally {
17373
+ await releaseSnapshotObject(session, objectId ?? void 0, progress);
17356
17374
  }
17357
- })()).filter((node) => {
17375
+ })();
17376
+ progress?.throwIfStopped();
17377
+ const filteredNodes = nodesForOutline.filter((node) => {
17358
17378
  const be = node.backendDOMNodeId;
17359
17379
  return typeof be !== "number" || !opts.isIgnoredBackendNode?.(be);
17360
17380
  });
@@ -17368,6 +17388,7 @@ async function a11yForFrame(session, frameId, opts) {
17368
17388
  urlMap[enc] = url;
17369
17389
  }
17370
17390
  const { tree } = await buildHierarchicalTree(decorateRoles(filteredNodes, opts), opts);
17391
+ progress?.throwIfStopped();
17371
17392
  return {
17372
17393
  outline: tree.map((n) => formatTreeLine(n)).join("\n").trimEnd(),
17373
17394
  urlMap,
@@ -17529,11 +17550,12 @@ function collectDomTraversalTargets(node) {
17529
17550
  * Rehydrate a truncated DOM tree by repeatedly calling DOM.describeNode with
17530
17551
  * decreasing depths. Any non-CBOR failure is surfaced directly.
17531
17552
  */
17532
- async function hydrateDomTree(session, root, pierce) {
17553
+ async function hydrateDomTree(session, root, pierce, progress) {
17533
17554
  const stack = [root];
17534
17555
  const expandedNodeIds = /* @__PURE__ */ new Set();
17535
17556
  const expandedBackendIds = /* @__PURE__ */ new Set();
17536
17557
  while (stack.length) {
17558
+ progress?.throwIfStopped();
17537
17559
  const node = stack.pop();
17538
17560
  const nodeId = typeof node.nodeId === "number" && node.nodeId > 0 ? node.nodeId : void 0;
17539
17561
  const backendId = typeof node.backendNodeId === "number" && node.backendNodeId > 0 ? node.backendNodeId : void 0;
@@ -17547,11 +17569,11 @@ async function hydrateDomTree(session, root, pierce) {
17547
17569
  let expanded = false;
17548
17570
  let lastRetryError;
17549
17571
  for (const depth of DESCRIBE_DEPTH_ATTEMPTS) try {
17550
- const described = await session.send("DOM.describeNode", {
17572
+ const described = await runLocatorStep(progress, "expanding snapshot DOM", () => session.send("DOM.describeNode", {
17551
17573
  ...describeParamsBase,
17552
17574
  depth,
17553
17575
  pierce
17554
- });
17576
+ }));
17555
17577
  mergeDomNodes(node, described.node);
17556
17578
  if (!nodeId && described.node.nodeId && described.node.nodeId > 0) {
17557
17579
  node.nodeId = described.node.nodeId;
@@ -17560,6 +17582,7 @@ async function hydrateDomTree(session, root, pierce) {
17560
17582
  expanded = true;
17561
17583
  break;
17562
17584
  } catch (err) {
17585
+ progress?.throwIfStopped();
17563
17586
  if (isCborStackError(err instanceof Error ? err.message : String(err))) {
17564
17587
  lastRetryError = err;
17565
17588
  continue;
@@ -17579,16 +17602,17 @@ async function hydrateDomTree(session, root, pierce) {
17579
17602
  * complaining. When a shallower snapshot is returned we hydrate the missing
17580
17603
  * branches so downstream DOM traversals see the full tree shape.
17581
17604
  */
17582
- async function getDomTreeWithFallback(session, pierce) {
17605
+ async function getDomTreeWithFallback(session, pierce, progress) {
17583
17606
  let lastRetryError;
17584
17607
  for (const depth of DOM_DEPTH_ATTEMPTS) try {
17585
- const { root } = await session.send("DOM.getDocument", {
17608
+ const { root } = await runLocatorStep(progress, "reading snapshot DOM", () => session.send("DOM.getDocument", {
17586
17609
  depth,
17587
17610
  pierce
17588
- });
17589
- if (depth !== -1) await hydrateDomTree(session, root, pierce);
17611
+ }));
17612
+ if (depth !== -1) await hydrateDomTree(session, root, pierce, progress);
17590
17613
  return root;
17591
17614
  } catch (err) {
17615
+ progress?.throwIfStopped();
17592
17616
  if (isCborStackError(err instanceof Error ? err.message : String(err))) {
17593
17617
  lastRetryError = err;
17594
17618
  continue;
@@ -17602,17 +17626,19 @@ async function getDomTreeWithFallback(session, pierce) {
17602
17626
  * Build tag name and XPath maps for a single frame session.
17603
17627
  * EncodedId is produced by a frame-aware encoder provided by the caller.
17604
17628
  */
17605
- async function domMapsForSession(session, frameId, pierce, encode, attemptOwnerLookup = true) {
17606
- await session.send("DOM.enable").catch(() => {});
17607
- const root = await getDomTreeWithFallback(session, pierce);
17629
+ async function domMapsForSession(session, frameId, pierce, encode, attemptOwnerLookup = true, progress) {
17630
+ await runLocatorStep(progress, "enabling DOM", () => session.send("DOM.enable").catch(() => {}));
17631
+ const root = await getDomTreeWithFallback(session, pierce, progress);
17608
17632
  let startNode = root;
17609
17633
  if (attemptOwnerLookup) try {
17610
- const ownerBackendId = (await session.send("DOM.getFrameOwner", { frameId })).backendNodeId;
17634
+ const ownerBackendId = (await runLocatorStep(progress, "finding snapshot iframe owner", () => session.send("DOM.getFrameOwner", { frameId }))).backendNodeId;
17611
17635
  if (typeof ownerBackendId === "number") {
17612
17636
  const ownerEl = findNodeByBackendId(root, ownerBackendId);
17613
17637
  if (ownerEl?.contentDocument) startNode = ownerEl.contentDocument;
17614
17638
  }
17615
- } catch {}
17639
+ } catch {
17640
+ progress?.throwIfStopped();
17641
+ }
17616
17642
  const tagNameMap = {};
17617
17643
  const xpathMap = {};
17618
17644
  const scrollableMap = {};
@@ -17621,6 +17647,7 @@ async function domMapsForSession(session, frameId, pierce, encode, attemptOwnerL
17621
17647
  xpath: ""
17622
17648
  }];
17623
17649
  while (stack.length) {
17650
+ progress?.throwIfStopped();
17624
17651
  const { node, xpath } = stack.pop();
17625
17652
  if (node.backendNodeId) {
17626
17653
  const encId = encode(frameId, node.backendNodeId);
@@ -17645,6 +17672,7 @@ async function domMapsForSession(session, frameId, pierce, encode, attemptOwnerL
17645
17672
  xpath: joinXPath(xpath, "//")
17646
17673
  });
17647
17674
  }
17675
+ progress?.throwIfStopped();
17648
17676
  return {
17649
17677
  tagNameMap,
17650
17678
  xpathMap,
@@ -17656,9 +17684,9 @@ async function domMapsForSession(session, frameId, pierce, encode, attemptOwnerL
17656
17684
  * Once the index is cached, per-frame slices are derived without extra DOM
17657
17685
  * calls, which keeps snapshot capture linear in the number of frames.
17658
17686
  */
17659
- async function buildSessionDomIndex(session, pierce) {
17660
- await session.send("DOM.enable").catch(() => {});
17661
- const root = await getDomTreeWithFallback(session, true);
17687
+ async function buildSessionDomIndex(session, pierce, progress) {
17688
+ await runLocatorStep(progress, "enabling DOM", () => session.send("DOM.enable").catch(() => {}));
17689
+ const root = await getDomTreeWithFallback(session, true, progress);
17662
17690
  const absByBe = /* @__PURE__ */ new Map();
17663
17691
  const tagByBe = /* @__PURE__ */ new Map();
17664
17692
  const scrollByBe = /* @__PURE__ */ new Map();
@@ -17675,6 +17703,7 @@ async function buildSessionDomIndex(session, pierce) {
17675
17703
  }];
17676
17704
  let dfsIndex = 0;
17677
17705
  while (stack.length) {
17706
+ progress?.throwIfStopped();
17678
17707
  const { node, xp, docRootBe, phase } = stack.pop();
17679
17708
  if (phase === "exit") {
17680
17709
  if (node.backendNodeId) exitByBe.set(node.backendNodeId, dfsIndex++);
@@ -17724,6 +17753,7 @@ async function buildSessionDomIndex(session, pierce) {
17724
17753
  });
17725
17754
  }
17726
17755
  }
17756
+ progress?.throwIfStopped();
17727
17757
  return {
17728
17758
  rootBackend: rootBe,
17729
17759
  absByBe,
@@ -17820,7 +17850,8 @@ var toWellFormed = (value) => value.toWellFormed();
17820
17850
  *
17821
17851
  * Each numbered block below references the step above for easier debugging.
17822
17852
  */
17823
- async function captureHybridSnapshot(page, options, logger = page.logger) {
17853
+ async function captureHybridSnapshot(page, options, progress, logger = page.logger) {
17854
+ progress.throwIfStopped();
17824
17855
  const pierce = options?.pierceShadow ?? true;
17825
17856
  const includeIframes = options?.includeIframes !== false;
17826
17857
  const hasIgnoreLocators = (options?.ignoreLocators?.length ?? 0) > 0;
@@ -17828,17 +17859,18 @@ async function captureHybridSnapshot(page, options, logger = page.logger) {
17828
17859
  const framesInScope = includeIframes ? [...context.frames] : [context.rootId];
17829
17860
  if (!framesInScope.includes(context.rootId)) framesInScope.unshift(context.rootId);
17830
17861
  if (!hasIgnoreLocators) {
17831
- const scopedSnapshot = await tryScopedSnapshot(page, options, context, pierce, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), logger);
17862
+ const scopedSnapshot = await tryScopedSnapshot(page, options, context, pierce, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), logger, progress);
17832
17863
  if (scopedSnapshot) return scopedSnapshot;
17833
17864
  }
17834
- const sessionToIndex = await buildSessionIndexes(page, framesInScope, pierce);
17835
- const exclusionIntervalsByFrame = await buildFrameExclusionIntervals(page, context, sessionToIndex, await resolveIgnoredNodes(page, options?.ignoreLocators, context, sessionToIndex));
17865
+ const sessionToIndex = await buildSessionIndexes(page, framesInScope, pierce, progress);
17866
+ const exclusionIntervalsByFrame = await buildFrameExclusionIntervals(page, context, sessionToIndex, await resolveIgnoredNodes(page, options?.ignoreLocators, context, sessionToIndex, progress), progress);
17836
17867
  if (hasIgnoreLocators) {
17837
- const scopedSnapshot = await tryScopedSnapshot(page, options, context, pierce, sessionToIndex, exclusionIntervalsByFrame, logger);
17868
+ const scopedSnapshot = await tryScopedSnapshot(page, options, context, pierce, sessionToIndex, exclusionIntervalsByFrame, logger, progress);
17838
17869
  if (scopedSnapshot) return scopedSnapshot;
17839
17870
  }
17840
- const { perFrameMaps, perFrameOutlines } = await collectPerFrameMaps(page, context, sessionToIndex, options, pierce, framesInScope, exclusionIntervalsByFrame);
17841
- const { absPrefix, iframeHostEncByChild } = await computeFramePrefixes(page, context, perFrameMaps, framesInScope);
17871
+ const { perFrameMaps, perFrameOutlines } = await collectPerFrameMaps(page, context, sessionToIndex, options, pierce, framesInScope, exclusionIntervalsByFrame, progress);
17872
+ const { absPrefix, iframeHostEncByChild } = await computeFramePrefixes(page, context, perFrameMaps, framesInScope, progress);
17873
+ progress.throwIfStopped();
17842
17874
  return mergeFramesIntoSnapshot(context, perFrameMaps, perFrameOutlines, absPrefix, iframeHostEncByChild, framesInScope);
17843
17875
  }
17844
17876
  /**
@@ -17869,7 +17901,8 @@ function buildFrameContext(page) {
17869
17901
  * Returns `null` when scoping fails (e.g., locator miss) so the caller can
17870
17902
  * fall back to the full multi-frame snapshot.
17871
17903
  */
17872
- async function tryScopedSnapshot(page, options, context, pierce, sessionToIndex, exclusionIntervalsByFrame, logger) {
17904
+ async function tryScopedSnapshot(page, options, context, pierce, sessionToIndex, exclusionIntervalsByFrame, logger, progress) {
17905
+ progress?.throwIfStopped();
17873
17906
  const focusLocator = options?.focusLocator;
17874
17907
  if (!focusLocator) return null;
17875
17908
  const requestedFocus = focusLocator.selector.trim();
@@ -17883,12 +17916,12 @@ async function tryScopedSnapshot(page, options, context, pierce, sessionToIndex,
17883
17916
  let tailSelector;
17884
17917
  let absPrefix;
17885
17918
  if (/^xpath=/i.test(requestedFocus) || requestedFocus.startsWith("/")) {
17886
- const hit = await resolveFocusFrameAndTail(page, normalizeXPath(requestedFocus), context.parentByFrame, context.rootId);
17919
+ const hit = await resolveFocusFrameAndTail(page, normalizeXPath(requestedFocus), context.parentByFrame, context.rootId, progress);
17887
17920
  targetFrameId = hit.targetFrameId;
17888
17921
  tailSelector = hit.tailXPath || void 0;
17889
17922
  absPrefix = hit.absPrefix;
17890
17923
  } else {
17891
- const cssHit = await resolveCssFocusFrameAndTail(page, requestedFocus, context.parentByFrame, context.rootId);
17924
+ const cssHit = await resolveCssFocusFrameAndTail(page, requestedFocus, context.parentByFrame, context.rootId, progress);
17892
17925
  targetFrameId = cssHit.targetFrameId;
17893
17926
  tailSelector = cssHit.tailSelector || void 0;
17894
17927
  absPrefix = cssHit.absPrefix;
@@ -17896,7 +17929,7 @@ async function tryScopedSnapshot(page, options, context, pierce, sessionToIndex,
17896
17929
  const owningSess = ownerSession(page, targetFrameId);
17897
17930
  const parentId = context.parentByFrame.get(targetFrameId);
17898
17931
  const sameSessionAsParent = !!parentId && ownerSession(page, parentId) === ownerSession(page, targetFrameId);
17899
- const { tagNameMap, xpathMap, scrollableMap } = await domMapsForSession(owningSess, targetFrameId, pierce, (fid, be) => `${page.getOrdinal(fid)}-${be}`, sameSessionAsParent);
17932
+ const { tagNameMap, xpathMap, scrollableMap } = await domMapsForSession(owningSess, targetFrameId, pierce, (fid, be) => `${page.getOrdinal(fid)}-${be}`, sameSessionAsParent, progress);
17900
17933
  const { outline, urlMap, scopeApplied } = await a11yForFrame(owningSess, targetFrameId, {
17901
17934
  focusLocator: tailSelector ? {
17902
17935
  selector: tailSelector,
@@ -17906,7 +17939,7 @@ async function tryScopedSnapshot(page, options, context, pierce, sessionToIndex,
17906
17939
  tagNameMap,
17907
17940
  scrollableMap,
17908
17941
  encode: (backendNodeId) => `${page.getOrdinal(targetFrameId)}-${backendNodeId}`
17909
- });
17942
+ }, progress);
17910
17943
  const scopedXpathMap = {};
17911
17944
  const isIgnoredBackendNode = makeIsIgnoredBackendNode(targetFrameId, ownerSessionIndexForFrame(page, targetFrameId, sessionToIndex), exclusionIntervalsByFrame);
17912
17945
  const abs = absPrefix ?? "";
@@ -17936,6 +17969,7 @@ async function tryScopedSnapshot(page, options, context, pierce, sessionToIndex,
17936
17969
  if (scopeApplied) return snapshot;
17937
17970
  logScopeFallback();
17938
17971
  } catch {
17972
+ progress?.throwIfStopped();
17939
17973
  logScopeFallback();
17940
17974
  }
17941
17975
  return null;
@@ -17945,16 +17979,18 @@ async function tryScopedSnapshot(page, options, context, pierce, sessionToIndex,
17945
17979
  * result so per-frame slices can share the structure. We key by session id
17946
17980
  * because same process iframes live inside the same session.
17947
17981
  */
17948
- async function buildSessionIndexes(page, frames, pierce) {
17982
+ async function buildSessionIndexes(page, frames, pierce, progress) {
17949
17983
  const sessionToIndex = /* @__PURE__ */ new Map();
17950
17984
  const sessionById = /* @__PURE__ */ new Map();
17951
17985
  for (const frameId of frames) {
17986
+ progress?.throwIfStopped();
17952
17987
  const sess = ownerSession(page, frameId);
17953
17988
  const sid = sess.id ?? "root";
17954
17989
  if (!sessionById.has(sid)) sessionById.set(sid, sess);
17955
17990
  }
17956
17991
  for (const [sid, sess] of sessionById.entries()) {
17957
- const idx = await buildSessionDomIndex(sess, pierce);
17992
+ progress?.throwIfStopped();
17993
+ const idx = await buildSessionDomIndex(sess, pierce, progress);
17958
17994
  sessionToIndex.set(sid, idx);
17959
17995
  }
17960
17996
  return sessionToIndex;
@@ -17967,20 +18003,21 @@ async function buildSessionIndexes(page, frames, pierce) {
17967
18003
  * - collects tag/xpath/scrollability maps for DOM-based lookups
17968
18004
  * - fetches its AX tree to produce outlines and URL maps
17969
18005
  */
17970
- async function collectPerFrameMaps(page, context, sessionToIndex, options, pierce, frameIds, exclusionIntervalsByFrame) {
18006
+ async function collectPerFrameMaps(page, context, sessionToIndex, options, pierce, frameIds, exclusionIntervalsByFrame, progress) {
17971
18007
  const perFrameMaps = /* @__PURE__ */ new Map();
17972
18008
  const perFrameOutlines = [];
17973
18009
  for (const frameId of frameIds) {
18010
+ progress?.throwIfStopped();
17974
18011
  const sess = ownerSession(page, frameId);
17975
18012
  const sid = sess.id ?? "root";
17976
18013
  let idx = sessionToIndex.get(sid);
17977
18014
  if (!idx) {
17978
- idx = await buildSessionDomIndex(sess, pierce);
18015
+ idx = await buildSessionDomIndex(sess, pierce, progress);
17979
18016
  sessionToIndex.set(sid, idx);
17980
18017
  }
17981
18018
  const parentId = context.parentByFrame.get(frameId);
17982
18019
  const sameSessionAsParent = !!parentId && ownerSession(page, parentId) === sess;
17983
- const docRootBe = await resolveFrameDocRootBackendId(page, frameId, idx, sameSessionAsParent);
18020
+ const docRootBe = await resolveFrameDocRootBackendId(page, frameId, idx, sameSessionAsParent, progress);
17984
18021
  if (docRootBe === void 0) continue;
17985
18022
  const tagNameMap = {};
17986
18023
  const xpathMap = {};
@@ -18004,7 +18041,7 @@ async function collectPerFrameMaps(page, context, sessionToIndex, options, pierc
18004
18041
  tagNameMap,
18005
18042
  scrollableMap,
18006
18043
  encode: (backendNodeId) => `${page.getOrdinal(frameId)}-${backendNodeId}`
18007
- });
18044
+ }, progress);
18008
18045
  perFrameOutlines.push({
18009
18046
  frameId,
18010
18047
  outline
@@ -18021,38 +18058,40 @@ async function collectPerFrameMaps(page, context, sessionToIndex, options, pierc
18021
18058
  perFrameOutlines
18022
18059
  };
18023
18060
  }
18024
- async function resolveIgnoredNodes(page, ignoreLocators, context, sessionToIndex) {
18061
+ async function resolveIgnoredNodes(page, ignoreLocators, context, sessionToIndex, progress) {
18025
18062
  const ignoredNodesByFrame = /* @__PURE__ */ new Map();
18026
18063
  for (const locator of ignoreLocators ?? []) {
18064
+ progress?.throwIfStopped();
18027
18065
  const selector = locator.selector.trim();
18028
18066
  if (!selector) continue;
18029
18067
  try {
18030
18068
  const resolved = await resolveIgnoredNodesForLocator(page, {
18031
18069
  ...locator,
18032
18070
  selector
18033
- }, context, sessionToIndex);
18071
+ }, context, sessionToIndex, progress);
18034
18072
  for (const match of resolved) {
18035
18073
  const nodes = ignoredNodesByFrame.get(match.frameId) ?? /* @__PURE__ */ new Set();
18036
18074
  nodes.add(match.backendNodeId);
18037
18075
  ignoredNodesByFrame.set(match.frameId, nodes);
18038
18076
  }
18039
18077
  } catch {
18078
+ progress?.throwIfStopped();
18040
18079
  continue;
18041
18080
  }
18042
18081
  }
18043
18082
  return ignoredNodesByFrame;
18044
18083
  }
18045
- async function resolveIgnoredNodesForLocator(page, locator, context, sessionToIndex) {
18084
+ async function resolveIgnoredNodesForLocator(page, locator, context, sessionToIndex, progress) {
18046
18085
  const selector = locator.selector;
18047
18086
  if (/^xpath=/i.test(selector) || selector.startsWith("/")) {
18048
- const hit = await resolveFocusFrameAndTail(page, normalizeXPath(selector), context.parentByFrame, context.rootId);
18087
+ const hit = await resolveFocusFrameAndTail(page, normalizeXPath(selector), context.parentByFrame, context.rootId, progress);
18049
18088
  const targetFrameId = hit.targetFrameId;
18050
18089
  const tailXPath = hit.tailXPath || "/";
18051
18090
  if (tailXPath === "/") {
18052
18091
  const idx = ownerSessionIndexForFrame(page, targetFrameId, sessionToIndex);
18053
18092
  if (!idx) return [];
18054
18093
  const parentId = context.parentByFrame.get(targetFrameId);
18055
- const backendNodeId = await resolveFrameDocRootBackendId(page, targetFrameId, idx, !!parentId && ownerSession(page, parentId) === ownerSession(page, targetFrameId));
18094
+ const backendNodeId = await resolveFrameDocRootBackendId(page, targetFrameId, idx, !!parentId && ownerSession(page, parentId) === ownerSession(page, targetFrameId), progress);
18056
18095
  if (backendNodeId === void 0) return [];
18057
18096
  return [{
18058
18097
  frameId: targetFrameId,
@@ -18062,41 +18101,44 @@ async function resolveIgnoredNodesForLocator(page, locator, context, sessionToIn
18062
18101
  return resolveIgnoredNodesInFrame(page, targetFrameId, {
18063
18102
  kind: "xpath",
18064
18103
  value: tailXPath
18065
- }, locator.nth);
18104
+ }, locator.nth, progress);
18066
18105
  }
18067
- const hit = await resolveCssFocusFrameAndTail(page, selector, context.parentByFrame, context.rootId);
18106
+ const hit = await resolveCssFocusFrameAndTail(page, selector, context.parentByFrame, context.rootId, progress);
18068
18107
  const targetFrameId = hit.targetFrameId;
18069
18108
  return resolveIgnoredNodesInFrame(page, targetFrameId, {
18070
18109
  kind: "css",
18071
18110
  value: hit.tailSelector || selector
18072
- }, locator.nth);
18111
+ }, locator.nth, progress);
18073
18112
  }
18074
- async function resolveIgnoredNodesInFrame(page, frameId, query, nth) {
18113
+ async function resolveIgnoredNodesInFrame(page, frameId, query, nth, progress) {
18075
18114
  const session = ownerSession(page, frameId);
18076
- const resolver = new FrameSelectorResolver(new Frame(session, frameId, "", false, page.logger));
18077
- const resolvedNodes = nth === void 0 ? await resolver.resolveAll(query) : [await resolver.resolveAtIndex(query, nth)].filter((node) => !!node);
18115
+ const resolver = new FrameSelectorResolver(new Frame(session, frameId, "", false, page.logger), { readinessRetries: "none" });
18116
+ const resolvedNodes = nth === void 0 ? await resolver.resolveAll(query, {}, progress) : [await resolver.resolveAtIndex(query, nth, progress)].filter((node) => !!node);
18078
18117
  if (!resolvedNodes.length) return [];
18079
- return (await describeResolvedNodes(session, resolvedNodes)).map((backendNodeId) => ({
18118
+ return (await describeResolvedNodes(session, resolvedNodes, progress)).map((backendNodeId) => ({
18080
18119
  frameId,
18081
18120
  backendNodeId
18082
18121
  }));
18083
18122
  }
18084
- async function describeResolvedNodes(session, resolvedNodes) {
18123
+ async function describeResolvedNodes(session, resolvedNodes, progress) {
18085
18124
  const backendNodeIds = /* @__PURE__ */ new Set();
18086
18125
  try {
18087
18126
  for (const resolvedNode of resolvedNodes) {
18088
- const backendNodeId = (await session.send("DOM.describeNode", { objectId: resolvedNode.objectId })).node.backendNodeId;
18127
+ const backendNodeId = (await runLocatorStep(progress, "reading ignored snapshot nodes", () => session.send("DOM.describeNode", { objectId: resolvedNode.objectId }))).node.backendNodeId;
18089
18128
  if (typeof backendNodeId === "number") backendNodeIds.add(backendNodeId);
18090
18129
  }
18091
18130
  } finally {
18092
- await Promise.all(resolvedNodes.map((resolvedNode) => session.send("Runtime.releaseObject", { objectId: resolvedNode.objectId }).catch(() => {})));
18131
+ const release = () => Promise.all(resolvedNodes.map((resolvedNode) => session.send("Runtime.releaseObject", { objectId: resolvedNode.objectId }).catch(() => {})));
18132
+ if (progress) await progress.cleanup(release);
18133
+ else await release();
18093
18134
  }
18135
+ progress?.throwIfStopped();
18094
18136
  return [...backendNodeIds];
18095
18137
  }
18096
- async function buildFrameExclusionIntervals(page, context, sessionToIndex, ignoredNodesByFrame) {
18138
+ async function buildFrameExclusionIntervals(page, context, sessionToIndex, ignoredNodesByFrame, progress) {
18097
18139
  const intervalsByFrame = /* @__PURE__ */ new Map();
18098
18140
  if (!ignoredNodesByFrame.size) return intervalsByFrame;
18099
- const childFramesByParent = await resolveChildFramesByParent(page, context);
18141
+ const childFramesByParent = await resolveChildFramesByParent(page, context, progress);
18100
18142
  const excludedFrames = /* @__PURE__ */ new Set();
18101
18143
  const pushInterval = (frameId, start, end) => {
18102
18144
  const intervals = intervalsByFrame.get(frameId) ?? [];
@@ -18107,12 +18149,13 @@ async function buildFrameExclusionIntervals(page, context, sessionToIndex, ignor
18107
18149
  intervalsByFrame.set(frameId, intervals);
18108
18150
  };
18109
18151
  const excludeFrameSubtree = async (frameId) => {
18152
+ progress?.throwIfStopped();
18110
18153
  if (excludedFrames.has(frameId)) return;
18111
18154
  excludedFrames.add(frameId);
18112
18155
  const idx = ownerSessionIndexForFrame(page, frameId, sessionToIndex);
18113
18156
  if (!idx) return;
18114
18157
  const parentId = context.parentByFrame.get(frameId);
18115
- const docRootBe = await resolveFrameDocRootBackendId(page, frameId, idx, !!parentId && ownerSession(page, parentId) === ownerSession(page, frameId));
18158
+ const docRootBe = await resolveFrameDocRootBackendId(page, frameId, idx, !!parentId && ownerSession(page, parentId) === ownerSession(page, frameId), progress);
18116
18159
  if (docRootBe !== void 0) {
18117
18160
  const start = idx.enterByBe.get(docRootBe);
18118
18161
  const end = idx.exitByBe.get(docRootBe);
@@ -18121,6 +18164,7 @@ async function buildFrameExclusionIntervals(page, context, sessionToIndex, ignor
18121
18164
  for (const childFrameId of listChildrenOf(context.parentByFrame, frameId)) await excludeFrameSubtree(childFrameId);
18122
18165
  };
18123
18166
  const excludeIgnoredNode = async (frameId, backendNodeId) => {
18167
+ progress?.throwIfStopped();
18124
18168
  const idx = ownerSessionIndexForFrame(page, frameId, sessionToIndex);
18125
18169
  if (!idx) return;
18126
18170
  const start = idx.enterByBe.get(backendNodeId);
@@ -18150,15 +18194,16 @@ async function buildFrameExclusionIntervals(page, context, sessionToIndex, ignor
18150
18194
  }
18151
18195
  return intervalsByFrame;
18152
18196
  }
18153
- async function resolveChildFramesByParent(page, context) {
18197
+ async function resolveChildFramesByParent(page, context, progress) {
18154
18198
  const childFramesByParent = /* @__PURE__ */ new Map();
18155
18199
  for (const frameId of context.frames) {
18200
+ progress?.throwIfStopped();
18156
18201
  const parentId = context.parentByFrame.get(frameId);
18157
18202
  if (!parentId) continue;
18158
18203
  const session = parentSession(page, context.parentByFrame, frameId);
18159
18204
  if (!session) continue;
18160
18205
  try {
18161
- const { backendNodeId } = await session.send("DOM.getFrameOwner", { frameId });
18206
+ const { backendNodeId } = await runLocatorStep(progress, "finding snapshot iframe owner", () => session.send("DOM.getFrameOwner", { frameId }));
18162
18207
  if (typeof backendNodeId !== "number") continue;
18163
18208
  const childFrames = childFramesByParent.get(parentId) ?? [];
18164
18209
  childFrames.push({
@@ -18167,6 +18212,7 @@ async function resolveChildFramesByParent(page, context) {
18167
18212
  });
18168
18213
  childFramesByParent.set(parentId, childFrames);
18169
18214
  } catch {
18215
+ progress?.throwIfStopped();
18170
18216
  continue;
18171
18217
  }
18172
18218
  }
@@ -18201,23 +18247,26 @@ function ownerSessionIndexForFrame(page, frameId, sessionToIndex) {
18201
18247
  const session = ownerSession(page, frameId);
18202
18248
  return sessionToIndex.get(session.id ?? "root");
18203
18249
  }
18204
- async function resolveFrameDocRootBackendId(page, frameId, idx, sameSessionAsParent) {
18250
+ async function resolveFrameDocRootBackendId(page, frameId, idx, sameSessionAsParent, progress) {
18251
+ progress?.throwIfStopped();
18205
18252
  if (!sameSessionAsParent) return idx.rootBackend;
18206
18253
  const session = ownerSession(page, frameId);
18207
18254
  try {
18208
- const { backendNodeId } = await session.send("DOM.getFrameOwner", { frameId });
18255
+ const { backendNodeId } = await runLocatorStep(progress, "finding snapshot iframe owner", () => session.send("DOM.getFrameOwner", { frameId }));
18209
18256
  if (typeof backendNodeId === "number") {
18210
18257
  const docRootBe = idx.contentDocRootByIframe.get(backendNodeId);
18211
18258
  if (typeof docRootBe === "number") return docRootBe;
18212
18259
  }
18213
- } catch {}
18260
+ } catch {
18261
+ progress?.throwIfStopped();
18262
+ }
18214
18263
  }
18215
18264
  /**
18216
18265
  * Step 4 – walk the frame tree (parent-first) to compute absolute prefixes for
18217
18266
  * every frame. The prefix is the absolute XPath of the iframe element hosting
18218
18267
  * the frame, so we can later convert relative XPaths into cross-frame ones.
18219
18268
  */
18220
- async function computeFramePrefixes(page, context, perFrameMaps, frameIds) {
18269
+ async function computeFramePrefixes(page, context, perFrameMaps, frameIds, progress) {
18221
18270
  const absPrefix = /* @__PURE__ */ new Map();
18222
18271
  const iframeHostEncByChild = /* @__PURE__ */ new Map();
18223
18272
  absPrefix.set(context.rootId, "");
@@ -18225,6 +18274,7 @@ async function computeFramePrefixes(page, context, perFrameMaps, frameIds) {
18225
18274
  const queue = [];
18226
18275
  if (included.has(context.rootId)) queue.push(context.rootId);
18227
18276
  while (queue.length) {
18277
+ progress?.throwIfStopped();
18228
18278
  const parent = queue.shift();
18229
18279
  const parentAbs = absPrefix.get(parent);
18230
18280
  for (const child of context.frames) {
@@ -18234,9 +18284,10 @@ async function computeFramePrefixes(page, context, perFrameMaps, frameIds) {
18234
18284
  const parentSess = parentSession(page, context.parentByFrame, child);
18235
18285
  const ownerBackendNodeId = await (async () => {
18236
18286
  try {
18237
- const { backendNodeId } = await parentSess.send("DOM.getFrameOwner", { frameId: child });
18287
+ const { backendNodeId } = await runLocatorStep(progress, "finding snapshot iframe owner", () => parentSess.send("DOM.getFrameOwner", { frameId: child }));
18238
18288
  return backendNodeId;
18239
18289
  } catch {
18290
+ progress?.throwIfStopped();
18240
18291
  return;
18241
18292
  }
18242
18293
  })();
@@ -54319,7 +54370,7 @@ var WebTracerProvider = class extends BasicTracerProvider {
54319
54370
  };
54320
54371
  var package_default = {
54321
54372
  name: "@browserbasehq/stagehand-extension",
54322
- version: "1.1.0-alpha-836a0639f5bfbf2e011acc525da6a9515c544b8e",
54373
+ version: "1.1.0-alpha-5788121e60c2648654c2b36ec7755030f0015fab",
54323
54374
  "private": true,
54324
54375
  type: "module",
54325
54376
  scripts: {
@@ -57623,19 +57674,27 @@ var Page = class Page {
57623
57674
  throw error;
57624
57675
  }
57625
57676
  }
57626
- async captureSnapshot(options) {
57627
- return await captureHybridSnapshot(this, options, this.logger);
57677
+ async captureSnapshot(options, parentProgress) {
57678
+ return await runWithProgress(parentProgress ?? {
57679
+ name: "snapshot",
57680
+ timeout: 0
57681
+ }, (progress) => captureHybridSnapshot(this, options, progress, this.logger));
57628
57682
  }
57629
- async snapshot(options) {
57630
- const { combinedTree, combinedXpathMap, combinedUrlMap } = await this.captureSnapshot({
57631
- pierceShadow: true,
57632
- includeIframes: options?.includeIframes
57683
+ async snapshot(options, parentProgress) {
57684
+ return await runWithProgress(parentProgress ?? {
57685
+ name: "snapshot",
57686
+ timeout: options?.timeout ?? 2e4
57687
+ }, async (progress) => {
57688
+ const { combinedTree, combinedXpathMap, combinedUrlMap } = await this.captureSnapshot({
57689
+ pierceShadow: true,
57690
+ includeIframes: options?.includeIframes
57691
+ }, progress);
57692
+ return {
57693
+ formattedTree: combinedTree,
57694
+ xpathMap: combinedXpathMap,
57695
+ urlMap: combinedUrlMap
57696
+ };
57633
57697
  });
57634
- return {
57635
- formattedTree: combinedTree,
57636
- xpathMap: combinedXpathMap,
57637
- urlMap: combinedUrlMap
57638
- };
57639
57698
  }
57640
57699
  /** Press a key down without releasing it */
57641
57700
  async keyDown(key) {
@@ -916,6 +916,7 @@ class PageSetViewportSizeParams(TypedDict):
916
916
 
917
917
 
918
918
  class PageSnapshotOptions(TypedDict):
919
+ timeout: NotRequired[float]
919
920
  include_iframes: NotRequired[bool]
920
921
 
921
922
 
@@ -1899,6 +1899,8 @@ class PageSnapshotOptions(WireModel):
1899
1899
  extra="forbid",
1900
1900
  validate_by_name=True,
1901
1901
  )
1902
+ timeout: Annotated[Optional[StrictFloat], Field(ge=0.0)] = None
1903
+ """Milliseconds for the whole snapshot call. Defaults to 20000. Zero disables the timeout."""
1902
1904
  include_iframes: Optional[StrictBool] = None
1903
1905
 
1904
1906
 
@@ -1,4 +1,4 @@
1
1
  """Generated from packages/protocol/package.json. Do not edit."""
2
2
 
3
3
  PROTOCOL_SEMVER_PATTERN = '^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\\+([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?$'
4
- STAGEHAND_PROTOCOL_VERSION = '2.1.0-alpha-836a0639f5bfbf2e011acc525da6a9515c544b8e'
4
+ STAGEHAND_PROTOCOL_VERSION = '2.1.0-alpha-5788121e60c2648654c2b36ec7755030f0015fab'
@@ -665,10 +665,17 @@ class Page:
665
665
  Path(path).write_bytes(data)
666
666
  return data
667
667
 
668
- async def snapshot(self, *, include_iframes: bool | None = None) -> SnapshotResult:
668
+ async def snapshot(
669
+ self, *, include_iframes: bool | None = None, timeout: float | None = None
670
+ ) -> SnapshotResult:
669
671
  params = PageSnapshotParams(page_id=self.page_id)
670
- if include_iframes is not None:
671
- params.options = PageSnapshotOptions(include_iframes=include_iframes)
672
+ options = PageSnapshotOptions.model_validate({
673
+ key: value
674
+ for key, value in {"include_iframes": include_iframes, "timeout": timeout}.items()
675
+ if value is not None
676
+ })
677
+ if options.model_fields_set:
678
+ params.options = options
672
679
  return await self._rpc_client.send("page.snapshot", params, SnapshotResult)
673
680
 
674
681
  async def tools(self, *, timeout: float | None = None) -> list[WebMCPTool]:
@@ -31,6 +31,7 @@ _DEFAULT_OPERATION_TIMEOUT_MS = {
31
31
  "page.wait_for_load_state": 15_000,
32
32
  "page.wait_for_selector": 30_000,
33
33
  "page.pdf": 30_000,
34
+ "page.snapshot": 20_000,
34
35
  "page.webmcp_tools": 1_000,
35
36
  }
36
37
  _UNBOUNDED_BY_DEFAULT_METHODS = {
@@ -56,7 +57,6 @@ _UNBOUNDED_BY_DEFAULT_METHODS = {
56
57
  "page.close",
57
58
  "page.evaluate",
58
59
  "page.screenshot",
59
- "page.snapshot",
60
60
  "page.webmcp_invocation_result",
61
61
  }
62
62
 
@@ -557,6 +557,7 @@ def _rpc_response_timeout_seconds(method: str, params: BaseModel) -> float | Non
557
557
  "page.go_forward",
558
558
  "page.screenshot",
559
559
  "page.pdf",
560
+ "page.snapshot",
560
561
  "page.wait_for_selector",
561
562
  "page.webmcp_tools",
562
563
  "page.webmcp_invocation_result",
@@ -568,7 +569,7 @@ def _rpc_response_timeout_seconds(method: str, params: BaseModel) -> float | Non
568
569
  operation_timeout_ms = _numeric_property(params, "ms")
569
570
 
570
571
  if operation_timeout_ms is not None:
571
- if method == "page.pdf" and operation_timeout_ms == 0:
572
+ if method in {"page.pdf", "page.snapshot"} and operation_timeout_ms == 0:
572
573
  return None
573
574
  return (_RPC_RESPONSE_GRACE_MS + max(0, operation_timeout_ms)) / 1_000
574
575