@runcofactor/cli 0.10.0 → 0.13.0

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 (148) hide show
  1. package/dist/{api-catalog-GFGCLVS4.js → api-catalog-7HLEN7C2.js} +5 -4
  2. package/dist/{api-catalog-GFGCLVS4.js.map → api-catalog-7HLEN7C2.js.map} +1 -1
  3. package/dist/{attachment-SJC7D7FY.js → attachment-OVNQD6Z2.js} +5 -4
  4. package/dist/{attachment-SJC7D7FY.js.map → attachment-OVNQD6Z2.js.map} +1 -1
  5. package/dist/{auth-EN6NQCJY.js → auth-VUHL74FH.js} +3 -2
  6. package/dist/{chunk-IMPZAX6L.js → chunk-5GD47G2D.js} +37 -1
  7. package/dist/{chunk-IMPZAX6L.js.map → chunk-5GD47G2D.js.map} +1 -1
  8. package/dist/{chunk-WOHL4FTB.js → chunk-CQE46HSM.js} +5 -5
  9. package/dist/chunk-CQE46HSM.js.map +1 -0
  10. package/dist/{chunk-52IO7ACJ.js → chunk-CRHPDPRS.js} +39 -3
  11. package/dist/{chunk-52IO7ACJ.js.map → chunk-CRHPDPRS.js.map} +1 -1
  12. package/dist/{chunk-XQXVG7RA.js → chunk-EEN2TZUQ.js} +3 -3
  13. package/dist/{chunk-I4QLI3JO.js → chunk-EJ4U242G.js} +2 -2
  14. package/dist/{chunk-CLAWT7BJ.js → chunk-ENDHMYEJ.js} +3 -3
  15. package/dist/chunk-G4UYGRZR.js +2 -0
  16. package/dist/chunk-JZMJUPZP.js +92 -0
  17. package/dist/chunk-JZMJUPZP.js.map +1 -0
  18. package/dist/{chunk-GKY2KXO5.js → chunk-NL7CVG56.js} +105 -35
  19. package/dist/chunk-NL7CVG56.js.map +1 -0
  20. package/dist/{chunk-A4OFTUVV.js → chunk-PZ4LCN6K.js} +7 -4
  21. package/dist/chunk-PZ4LCN6K.js.map +1 -0
  22. package/dist/{chunk-SJJW77WN.js → chunk-QGI4UCJX.js} +69 -5
  23. package/dist/chunk-QGI4UCJX.js.map +1 -0
  24. package/dist/{chunk-4PNOV7OV.js → chunk-QQVWGWPI.js} +1023 -150
  25. package/dist/chunk-QQVWGWPI.js.map +1 -0
  26. package/dist/{chunk-N7HFALX3.js → chunk-VGTKUOYS.js} +2 -2
  27. package/dist/{chunk-B2VISN2V.js → chunk-YN763NWQ.js} +230 -66
  28. package/dist/chunk-YN763NWQ.js.map +1 -0
  29. package/dist/{chunk-QFDOWABX.js → chunk-Z5LUTUDN.js} +6 -3
  30. package/dist/chunk-Z5LUTUDN.js.map +1 -0
  31. package/dist/{chunk-Q4BO4Z5V.js → chunk-ZTOE3ND3.js} +4 -4
  32. package/dist/{credential-COLTBV6D.js → credential-LO2SEEHU.js} +24 -20
  33. package/dist/credential-LO2SEEHU.js.map +1 -0
  34. package/dist/{credential-template-EEPYFG7R.js → credential-template-ZN7W7734.js} +30 -28
  35. package/dist/credential-template-ZN7W7734.js.map +1 -0
  36. package/dist/{debug-workflow-KOVH5GOP.js → debug-workflow-R3RYYAXK.js} +5 -4
  37. package/dist/{debug-workflow-KOVH5GOP.js.map → debug-workflow-R3RYYAXK.js.map} +1 -1
  38. package/dist/{docs-3SSORHB5.js → docs-DIAFBDXQ.js} +5 -4
  39. package/dist/{docs-3SSORHB5.js.map → docs-DIAFBDXQ.js.map} +1 -1
  40. package/dist/{doctor-OTSIWV6G.js → doctor-NZPUSCRE.js} +9 -5
  41. package/dist/doctor-NZPUSCRE.js.map +1 -0
  42. package/dist/environment-RQVU4P5M.js +221 -0
  43. package/dist/environment-RQVU4P5M.js.map +1 -0
  44. package/dist/{feature-flag-T2LRZME3.js → feature-flag-XG26RURR.js} +72 -10
  45. package/dist/feature-flag-XG26RURR.js.map +1 -0
  46. package/dist/index.js +57 -48
  47. package/dist/index.js.map +1 -1
  48. package/dist/{init-TGWRECT2.js → init-EDNLKGPT.js} +5 -4
  49. package/dist/{init-TGWRECT2.js.map → init-EDNLKGPT.js.map} +1 -1
  50. package/dist/{install-DXSNWZKO.js → install-FYZ3HNZK.js} +7 -7
  51. package/dist/{issues-XIZOPX6T.js → issues-NNRVTJ4L.js} +5 -4
  52. package/dist/{issues-XIZOPX6T.js.map → issues-NNRVTJ4L.js.map} +1 -1
  53. package/dist/{knobs-NBINZIUB.js → knobs-A2BOBH22.js} +5 -4
  54. package/dist/{knobs-NBINZIUB.js.map → knobs-A2BOBH22.js.map} +1 -1
  55. package/dist/{list-3TDHK637.js → list-F33VZ4BJ.js} +5 -4
  56. package/dist/{list-3TDHK637.js.map → list-F33VZ4BJ.js.map} +1 -1
  57. package/dist/{local-HICVD6PU.js → local-DQDTM3XA.js} +7 -6
  58. package/dist/{local-HICVD6PU.js.map → local-DQDTM3XA.js.map} +1 -1
  59. package/dist/{login-UOIUXVUK.js → login-BUMVIB36.js} +5 -4
  60. package/dist/{login-UOIUXVUK.js.map → login-BUMVIB36.js.map} +1 -1
  61. package/dist/{mcp-54IJEOUA.js → mcp-B5Y5CDMQ.js} +23 -24
  62. package/dist/mcp-B5Y5CDMQ.js.map +1 -0
  63. package/dist/{property-UXLOLPTD.js → property-NJ4SRXQG.js} +76 -4
  64. package/dist/property-NJ4SRXQG.js.map +1 -0
  65. package/dist/{psql-BYUUNLGG.js → psql-VBZFCMNR.js} +3 -2
  66. package/dist/{psql-BYUUNLGG.js.map → psql-VBZFCMNR.js.map} +1 -1
  67. package/dist/{record-4TR2UIIS.js → record-WAO36O7G.js} +16 -17
  68. package/dist/record-WAO36O7G.js.map +1 -0
  69. package/dist/{redis-35VBUYW7.js → redis-DHZXHL4S.js} +3 -2
  70. package/dist/{redis-35VBUYW7.js.map → redis-DHZXHL4S.js.map} +1 -1
  71. package/dist/{release-OOLTUBUT.js → release-STJKUKTE.js} +5 -4
  72. package/dist/{release-OOLTUBUT.js.map → release-STJKUKTE.js.map} +1 -1
  73. package/dist/{sandbox-XGSUU6G7.js → sandbox-DWSGLY7B.js} +9 -8
  74. package/dist/{sandbox-XGSUU6G7.js.map → sandbox-DWSGLY7B.js.map} +1 -1
  75. package/dist/{scenarios-GWLJEGHA.js → scenarios-HCIDY725.js} +5 -4
  76. package/dist/{scenarios-GWLJEGHA.js.map → scenarios-HCIDY725.js.map} +1 -1
  77. package/dist/{session-MYFNZPRP.js → session-Y5RHU54D.js} +76 -8
  78. package/dist/session-Y5RHU54D.js.map +1 -0
  79. package/dist/{skill-KM34LDMO.js → skill-23WAHUOB.js} +217 -67
  80. package/dist/skill-23WAHUOB.js.map +1 -0
  81. package/dist/skills/debug-run/SKILL.md +33 -22
  82. package/dist/skills/debug-run/history/1.2.0.md +262 -0
  83. package/dist/skills/debug-run/history/1.3.0.md +273 -0
  84. package/dist/skills/debug-run/version.json +1 -1
  85. package/dist/skills/optimize-cache/SKILL.md +206 -0
  86. package/dist/{spec-RA3CFEKG.js → spec-LM5XHOQ5.js} +54 -25
  87. package/dist/spec-LM5XHOQ5.js.map +1 -0
  88. package/dist/{src-LB4HBTJQ.js → src-22RI5VP2.js} +19 -11
  89. package/dist/src-22RI5VP2.js.map +1 -0
  90. package/dist/src-BGDRMOKC.js +5748 -0
  91. package/dist/src-BGDRMOKC.js.map +1 -0
  92. package/dist/{src-N3TPKGTY.js → src-NCJPUAE6.js} +3 -2
  93. package/dist/{src-UVDX5UP6.js → src-Y3K7UJEF.js} +4 -2
  94. package/dist/{start-PSEQSQA3.js → start-SWY55CZX.js} +5 -4
  95. package/dist/{start-PSEQSQA3.js.map → start-SWY55CZX.js.map} +1 -1
  96. package/dist/{tag-YY6YAABQ.js → tag-YSU2XXPI.js} +6 -5
  97. package/dist/{task-IQC3YR5K.js → task-NLBKLD7D.js} +5 -4
  98. package/dist/{task-IQC3YR5K.js.map → task-NLBKLD7D.js.map} +1 -1
  99. package/dist/{test-suite-DZNJFXDW.js → test-suite-QCPWZ2LS.js} +9 -6
  100. package/dist/{test-suite-DZNJFXDW.js.map → test-suite-QCPWZ2LS.js.map} +1 -1
  101. package/dist/{trace-HXIW7U2D.js → trace-A3ZY6K26.js} +6 -4
  102. package/dist/{triage-ORG45R5W.js → triage-TT4BMDBU.js} +6 -5
  103. package/dist/triage-TT4BMDBU.js.map +1 -0
  104. package/dist/{upgrade-L2VYMP2V.js → upgrade-6ZUFETKX.js} +3 -3
  105. package/dist/{workflow-XYFH5LTP.js → workflow-2CX3ZNOQ.js} +241 -237
  106. package/dist/workflow-2CX3ZNOQ.js.map +1 -0
  107. package/dist/{workflow-cache-NQJQFWMV.js → workflow-cache-GXNE6SU2.js} +140 -7
  108. package/dist/workflow-cache-GXNE6SU2.js.map +1 -0
  109. package/dist/{workflow-debug-BGD4UIXU.js → workflow-debug-PRX3DWGD.js} +8 -7
  110. package/dist/{workflow-debug-BGD4UIXU.js.map → workflow-debug-PRX3DWGD.js.map} +1 -1
  111. package/dist/{workflow-proposals-MTUDX46O.js → workflow-proposals-BW327WGL.js} +5 -4
  112. package/dist/{workflow-proposals-MTUDX46O.js.map → workflow-proposals-BW327WGL.js.map} +1 -1
  113. package/package.json +2 -1
  114. package/dist/chunk-4PNOV7OV.js.map +0 -1
  115. package/dist/chunk-A4OFTUVV.js.map +0 -1
  116. package/dist/chunk-B2VISN2V.js.map +0 -1
  117. package/dist/chunk-GKY2KXO5.js.map +0 -1
  118. package/dist/chunk-JICSYFRJ.js +0 -46
  119. package/dist/chunk-JICSYFRJ.js.map +0 -1
  120. package/dist/chunk-QFDOWABX.js.map +0 -1
  121. package/dist/chunk-SJJW77WN.js.map +0 -1
  122. package/dist/chunk-WOHL4FTB.js.map +0 -1
  123. package/dist/credential-COLTBV6D.js.map +0 -1
  124. package/dist/credential-template-EEPYFG7R.js.map +0 -1
  125. package/dist/doctor-OTSIWV6G.js.map +0 -1
  126. package/dist/feature-flag-T2LRZME3.js.map +0 -1
  127. package/dist/mcp-54IJEOUA.js.map +0 -1
  128. package/dist/property-UXLOLPTD.js.map +0 -1
  129. package/dist/record-4TR2UIIS.js.map +0 -1
  130. package/dist/session-MYFNZPRP.js.map +0 -1
  131. package/dist/skill-KM34LDMO.js.map +0 -1
  132. package/dist/spec-RA3CFEKG.js.map +0 -1
  133. package/dist/src-LB4HBTJQ.js.map +0 -1
  134. package/dist/workflow-XYFH5LTP.js.map +0 -1
  135. package/dist/workflow-cache-NQJQFWMV.js.map +0 -1
  136. /package/dist/{auth-EN6NQCJY.js.map → auth-VUHL74FH.js.map} +0 -0
  137. /package/dist/{chunk-XQXVG7RA.js.map → chunk-EEN2TZUQ.js.map} +0 -0
  138. /package/dist/{chunk-I4QLI3JO.js.map → chunk-EJ4U242G.js.map} +0 -0
  139. /package/dist/{chunk-CLAWT7BJ.js.map → chunk-ENDHMYEJ.js.map} +0 -0
  140. /package/dist/{src-N3TPKGTY.js.map → chunk-G4UYGRZR.js.map} +0 -0
  141. /package/dist/{chunk-N7HFALX3.js.map → chunk-VGTKUOYS.js.map} +0 -0
  142. /package/dist/{chunk-Q4BO4Z5V.js.map → chunk-ZTOE3ND3.js.map} +0 -0
  143. /package/dist/{install-DXSNWZKO.js.map → install-FYZ3HNZK.js.map} +0 -0
  144. /package/dist/{src-UVDX5UP6.js.map → src-NCJPUAE6.js.map} +0 -0
  145. /package/dist/{tag-YY6YAABQ.js.map → src-Y3K7UJEF.js.map} +0 -0
  146. /package/dist/{trace-HXIW7U2D.js.map → tag-YSU2XXPI.js.map} +0 -0
  147. /package/dist/{triage-ORG45R5W.js.map → trace-A3ZY6K26.js.map} +0 -0
  148. /package/dist/{upgrade-L2VYMP2V.js.map → upgrade-6ZUFETKX.js.map} +0 -0
@@ -2,6 +2,9 @@ import { createRequire as __cr } from "module"; const require = __cr(import.meta
2
2
  import {
3
3
  getCanaryTmpDir
4
4
  } from "./chunk-JWHYXEA2.js";
5
+ import {
6
+ isVolatileIdentifier
7
+ } from "./chunk-QGI4UCJX.js";
5
8
  import {
6
9
  require_src
7
10
  } from "./chunk-6LEZCKKV.js";
@@ -25583,8 +25586,22 @@ var TIMEOUTS = {
25583
25586
  POST_ACTION_SNAPSHOT: 15e3,
25584
25587
  // Hard cap for the best-effort `window.stop()` dispatched on per-tool-timeout
25585
25588
  // abort. Bounds wedged JS contexts; a healthy page returns in <10ms.
25586
- UNWEDGE_PAGE_CAP: 1e3
25589
+ UNWEDGE_PAGE_CAP: 1e3,
25590
+ // Driver-side grace added on top of `__waitDomQuiet`'s in-page maxWaitMs
25591
+ // before the evaluate itself is abandoned. The in-page cap is a page-side
25592
+ // setTimeout that never fires on a blocked main thread; the driver-side
25593
+ // race (maxWaitMs + this grace) guarantees forward progress regardless.
25594
+ WAIT_DOM_QUIET_EVAL_GRACE: 500,
25595
+ // Driver-side cap for the liveness probe (`page.evaluate(() => 1)`). Short on
25596
+ // purpose: a healthy page answers in <10ms; a blocked main thread caps and is
25597
+ // treated as Unresponsive so the per-tool-timeout recovery can tear it down.
25598
+ LIVENESS_PROBE_CAP: 750,
25599
+ // A captured request still 'pending' (no response headers) past this age is
25600
+ // ignored by the HTTP-settle stabilization signal — a long-poll / hung XHR /
25601
+ // header-less streaming fetch must never block a snapshot indefinitely.
25602
+ HTTP_SETTLE_MAX_AGE: 1e4
25587
25603
  };
25604
+ var LARGE_PAGE_REF_THRESHOLD = 1500;
25588
25605
  var DEFAULTS = {
25589
25606
  MAX_DEPTH: 10,
25590
25607
  MAX_PAGES: 100,
@@ -25911,6 +25928,10 @@ function extractAndStripV2Fields(attributes) {
25911
25928
  cssHints.testIds.push(value);
25912
25929
  sawCssHint = true;
25913
25930
  break;
25931
+ case "~data-id":
25932
+ cssHints.dataId = value;
25933
+ sawCssHint = true;
25934
+ break;
25914
25935
  case "~automation-id":
25915
25936
  cssHints.automationId = value;
25916
25937
  sawCssHint = true;
@@ -25921,6 +25942,12 @@ function extractAndStripV2Fields(attributes) {
25921
25942
  case "~semantic-class":
25922
25943
  out.semanticClass = value;
25923
25944
  break;
25945
+ case "~field":
25946
+ out.dataField = value;
25947
+ break;
25948
+ case "~editable":
25949
+ out.isEditable = value === "true";
25950
+ break;
25924
25951
  case "~v2":
25925
25952
  out.substrateVersion = 2;
25926
25953
  break;
@@ -26338,6 +26365,7 @@ var RESULT_ITEM_SELECTOR = "[role=option],[role=listitem],[role=row],[data-slot=
26338
26365
  var FILTER_QUERY_MIN_LEN = 3;
26339
26366
  var FILTER_MAX_RESULT_ITEMS = 200;
26340
26367
  var FILTER_ANCESTOR_WALK_DEPTH = 4;
26368
+ var HCLIP_ANCESTOR_WALK_DEPTH = 10;
26341
26369
  var MODAL_V2_MAX_ENRICHMENTS = 3;
26342
26370
  var MODAL_V2_FRAMEWORK_SELECTORS = [
26343
26371
  ".cdk-overlay-pane",
@@ -26369,6 +26397,7 @@ var SUB_FRAME_REF_RE = /^f\d+e\d+$/;
26369
26397
  function planChecks(elements, opts) {
26370
26398
  const planByRef = /* @__PURE__ */ new Map();
26371
26399
  const skipGridRefs = opts.skipGridRefs;
26400
+ const hclipCandidates = [];
26372
26401
  const ensure = (ref) => {
26373
26402
  let args = planByRef.get(ref);
26374
26403
  if (!args) {
@@ -26431,9 +26460,32 @@ function planChecks(elements, opts) {
26431
26460
  if (el.role === "switch" || el.role === "checkbox") {
26432
26461
  addCheck(el.ref, "toggle");
26433
26462
  }
26434
- if (opts.v2 && (INTERACTIVE_ROLES3.has(el.role) || el.role === "option" || el.role === "row" || el.role === "listitem" || el.attributes["cursor"] === "pointer")) {
26463
+ const isTargetableForId = INTERACTIVE_ROLES3.has(el.role) || el.role === "option" || el.role === "row" || el.role === "gridcell" || el.role === "listitem" || el.attributes["cursor"] === "pointer";
26464
+ if (opts.v2 && isTargetableForId) {
26435
26465
  addCheck(el.ref, "testId");
26436
26466
  }
26467
+ if (isTargetableForId) {
26468
+ hclipCandidates.push(el.ref);
26469
+ }
26470
+ const CONTAINER_ROLES_FOR_ID = /* @__PURE__ */ new Set([
26471
+ "generic",
26472
+ "group",
26473
+ "region",
26474
+ "article",
26475
+ "section",
26476
+ "listitem",
26477
+ "rowgroup",
26478
+ "list",
26479
+ "navigation",
26480
+ "complementary",
26481
+ "form"
26482
+ ]);
26483
+ if (opts.v2 && (isTargetableForId || CONTAINER_ROLES_FOR_ID.has(el.role) && hasInteractive(el))) {
26484
+ addCheck(el.ref, "dataId");
26485
+ }
26486
+ if (opts.v2 && (el.role === "gridcell" || el.role === "cell")) {
26487
+ addCheck(el.ref, "dataField");
26488
+ }
26437
26489
  if (el.role === "generic" && el.ref && el.text) {
26438
26490
  addCheck(el.ref, "svgPromote");
26439
26491
  }
@@ -26441,6 +26493,9 @@ function planChecks(elements, opts) {
26441
26493
  for (const child of el.children) walk(child, inGridCell || isGridCell);
26442
26494
  };
26443
26495
  for (const el of elements) walk(el, false);
26496
+ if (hclipCandidates.length <= LARGE_PAGE_REF_THRESHOLD) {
26497
+ for (const ref of hclipCandidates) addCheck(ref, "hclip");
26498
+ }
26444
26499
  return [...planByRef].map(([ref, args]) => ({ ref, args }));
26445
26500
  }
26446
26501
  function pageSideFn(el, _ref, args, G) {
@@ -26462,6 +26517,31 @@ function pageSideFn(el, _ref, args, G) {
26462
26517
  if (disabled) readonly = false;
26463
26518
  out.editability = { readonly, disabled };
26464
26519
  }
26520
+ if (checks.has("hclip")) {
26521
+ out.hclip = null;
26522
+ try {
26523
+ const win = el.ownerDocument.defaultView;
26524
+ if (win) {
26525
+ const elRect = el.getBoundingClientRect();
26526
+ if (elRect.width > 0 || elRect.height > 0) {
26527
+ let parent = el.parentElement;
26528
+ for (let i2 = 0; i2 < G.HCLIP_ANCESTOR_DEPTH && parent; i2++, parent = parent.parentElement) {
26529
+ if (parent.scrollWidth <= parent.clientWidth + 1) continue;
26530
+ const ox = win.getComputedStyle(parent).overflowX;
26531
+ if (ox !== "auto" && ox !== "scroll") continue;
26532
+ const cRect = parent.getBoundingClientRect();
26533
+ const bandLeft = Math.max(cRect.left, 0);
26534
+ const bandRight = Math.min(cRect.right, win.innerWidth);
26535
+ if (elRect.right <= bandLeft) out.hclip = "left";
26536
+ else if (elRect.left >= bandRight) out.hclip = "right";
26537
+ break;
26538
+ }
26539
+ }
26540
+ }
26541
+ } catch {
26542
+ out.hclip = null;
26543
+ }
26544
+ }
26465
26545
  if (checks.has("gridCellValue")) {
26466
26546
  let value = null;
26467
26547
  const title = el.getAttribute("title");
@@ -26795,6 +26875,28 @@ function pageSideFn(el, _ref, args, G) {
26795
26875
  out.testId = null;
26796
26876
  }
26797
26877
  }
26878
+ if (checks.has("dataId")) {
26879
+ try {
26880
+ const dataId = el.getAttribute("data-id") || void 0;
26881
+ out.dataId = dataId && dataId.length <= 120 ? dataId : null;
26882
+ } catch {
26883
+ out.dataId = null;
26884
+ }
26885
+ }
26886
+ if (checks.has("dataField")) {
26887
+ out.dataField = null;
26888
+ try {
26889
+ const field = el.getAttribute("data-field");
26890
+ const reserved = field === "__check__" || field === "__row_number__" || field === "__reorder__" || field === "__detail_panel_toggle__" || field === "__tree_data_group__";
26891
+ if (field && !reserved) {
26892
+ const cls = typeof el.className === "string" ? el.className : "";
26893
+ const editable = /(?:^|[\s-])editable(?:[\s-]|$)/i.test(cls) || el.getAttribute("aria-readonly") === "false" || el.querySelector('input, textarea, select, [contenteditable="true"]') !== null;
26894
+ out.dataField = { field: field.slice(0, 120), editable };
26895
+ }
26896
+ } catch {
26897
+ out.dataField = null;
26898
+ }
26899
+ }
26798
26900
  if (checks.has("svgPromote")) {
26799
26901
  try {
26800
26902
  out.svgPromote = el.hasAttribute("data-svg-enriched");
@@ -26943,6 +27045,7 @@ async function runPerRefAugmentations(scope, yaml, logger2, opts = {}) {
26943
27045
  FILTER_MAX_ITEMS: FILTER_MAX_RESULT_ITEMS,
26944
27046
  FILTER_MIN_QUERY_LEN: FILTER_QUERY_MIN_LEN,
26945
27047
  FILTER_ANCESTOR_DEPTH: FILTER_ANCESTOR_WALK_DEPTH,
27048
+ HCLIP_ANCESTOR_DEPTH: HCLIP_ANCESTOR_WALK_DEPTH,
26946
27049
  MODAL_FRAMEWORK_SELECTORS: MODAL_V2_FRAMEWORK_SELECTORS,
26947
27050
  MODAL_CLASS_PATTERNS: MODAL_V2_CLASS_PATTERNS
26948
27051
  };
@@ -26970,6 +27073,15 @@ async function runPerRefAugmentations(scope, yaml, logger2, opts = {}) {
26970
27073
  if (opts.v2 && r2.testId) {
26971
27074
  lines[i2] = appendAttributeIfMissing(lines[i2], "~test-id", encodeV2Value(r2.testId));
26972
27075
  }
27076
+ if (opts.v2 && r2.dataId) {
27077
+ lines[i2] = appendAttributeIfMissing(lines[i2], "~data-id", encodeV2Value(r2.dataId));
27078
+ }
27079
+ if (opts.v2 && r2.dataField) {
27080
+ lines[i2] = appendAttributeIfMissing(lines[i2], "~field", encodeV2Value(r2.dataField.field));
27081
+ if (r2.dataField.editable) {
27082
+ lines[i2] = appendAttributeIfMissing(lines[i2], "~editable", "true");
27083
+ }
27084
+ }
26973
27085
  if (r2.editability) {
26974
27086
  if (r2.editability.readonly) {
26975
27087
  lines[i2] = appendAttributeIfMissing(lines[i2], "readonly", "true");
@@ -26978,6 +27090,9 @@ async function runPerRefAugmentations(scope, yaml, logger2, opts = {}) {
26978
27090
  lines[i2] = appendAttributeIfMissing(lines[i2], "disabled", "true");
26979
27091
  }
26980
27092
  }
27093
+ if (r2.hclip) {
27094
+ lines[i2] = appendAttributeIfMissing(lines[i2], "~hclip", r2.hclip);
27095
+ }
26981
27096
  if (r2.gridCellValue) {
26982
27097
  const escaped = r2.gridCellValue.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
26983
27098
  lines[i2] = appendValueTail(lines[i2], escaped);
@@ -38614,7 +38729,19 @@ function extractGridCell(cellEl) {
38614
38729
  ref: cellEl.ref,
38615
38730
  column: cellEl.text || "",
38616
38731
  value: textValue,
38617
- hasValue: true
38732
+ hasValue: true,
38733
+ dataField: cellEl.dataField,
38734
+ isEditable: cellEl.isEditable
38735
+ };
38736
+ }
38737
+ if (cellEl.isEditable && cellEl.ref) {
38738
+ return {
38739
+ ref: cellEl.ref,
38740
+ column: "",
38741
+ value: "",
38742
+ hasValue: false,
38743
+ dataField: cellEl.dataField,
38744
+ isEditable: true
38618
38745
  };
38619
38746
  }
38620
38747
  return null;
@@ -38642,7 +38769,9 @@ function extractGridCell(cellEl) {
38642
38769
  value,
38643
38770
  hasValue: value !== "",
38644
38771
  gridcellLabel: gridcellLabel || void 0,
38645
- role
38772
+ role,
38773
+ dataField: cellEl.dataField,
38774
+ isEditable: cellEl.isEditable
38646
38775
  };
38647
38776
  }
38648
38777
  function reconcileCellColumn(cell, knownColumns) {
@@ -38956,7 +39085,7 @@ function formatGridOutput(grid, indent, expanded = false) {
38956
39085
  const MAX_GRID_ROWS = 30;
38957
39086
  const displayRows = expanded ? grid.rows : grid.rows.slice(0, MAX_GRID_ROWS);
38958
39087
  for (const row of displayRows) {
38959
- lines.push(formatGridRow(row, indent + 1, knownColumns));
39088
+ lines.push(formatGridRow(row, indent + 1, knownColumns, expanded));
38960
39089
  }
38961
39090
  if (!expanded && grid.rows.length > MAX_GRID_ROWS) {
38962
39091
  lines.push(
@@ -38965,7 +39094,8 @@ function formatGridOutput(grid, indent, expanded = false) {
38965
39094
  }
38966
39095
  return lines.join("\n");
38967
39096
  }
38968
- function formatGridRow(row, indent, knownColumns) {
39097
+ var MAX_COMPACT_EDITABLE_CELLS_PER_ROW = 4;
39098
+ function formatGridRow(row, indent, knownColumns, expanded = false) {
38969
39099
  const indentStr = " ".repeat(indent);
38970
39100
  if (row.expandedContent) {
38971
39101
  const parts = [];
@@ -38980,17 +39110,29 @@ function formatGridRow(row, indent, knownColumns) {
38980
39110
  const marker = row.isSelected ? "ACTIVE \u2192 Row" : "Row";
38981
39111
  const label = row.label ? ` "${row.label}"` : "";
38982
39112
  const actionsPrefix = row.rowActions?.length ? row.rowActions.map((a2) => `[action ${a2.ref}: ${a2.label}]`).join(" ") + " " : "";
38983
- const cellsWithValue = row.cells.filter(
38984
- (c3) => c3.hasValue && (!knownColumns || knownColumns.has(c3.column))
38985
- );
38986
- if (cellsWithValue.length > 0) {
38987
- const cellStrs = cellsWithValue.map((c3) => {
39113
+ const inScope = (c3) => !knownColumns || knownColumns.has(c3.column);
39114
+ const valueCells = row.cells.filter((c3) => c3.hasValue && inScope(c3));
39115
+ const editableEmptyCells = row.cells.filter((c3) => !c3.hasValue && c3.isEditable && inScope(c3));
39116
+ if (valueCells.length > 0 || editableEmptyCells.length > 0) {
39117
+ const renderCell = (c3) => {
39118
+ if (!c3.hasValue && c3.isEditable) {
39119
+ return `${c3.column} [${c3.ref}]: (editable)`;
39120
+ }
38988
39121
  if (c3.column === c3.value) {
38989
39122
  return `${c3.column} [${c3.ref}]`;
38990
39123
  }
38991
39124
  return `${c3.column} [${c3.ref}]: ${c3.value}`;
38992
- });
38993
- return `${indentStr}${actionsPrefix}${marker} [${row.ref}]${label}: ${cellStrs.join(" | ")}`;
39125
+ };
39126
+ const cellStrs = valueCells.map(renderCell);
39127
+ const shownEditable = expanded ? editableEmptyCells : editableEmptyCells.slice(0, MAX_COMPACT_EDITABLE_CELLS_PER_ROW);
39128
+ cellStrs.push(...shownEditable.map(renderCell));
39129
+ const hiddenEditable = editableEmptyCells.length - shownEditable.length;
39130
+ if (hiddenEditable > 0) {
39131
+ cellStrs.push(`(+${hiddenEditable} more editable cells)`);
39132
+ }
39133
+ if (cellStrs.length > 0) {
39134
+ return `${indentStr}${actionsPrefix}${marker} [${row.ref}]${label}: ${cellStrs.join(" | ")}`;
39135
+ }
38994
39136
  }
38995
39137
  return `${indentStr}${actionsPrefix}${marker} [${row.ref}]${label}: (empty row)`;
38996
39138
  }
@@ -40380,9 +40522,6 @@ function isMeaningfulDisabledControlWithoutRef(element) {
40380
40522
  if (!label || label === element.role || label === "(icon)") return false;
40381
40523
  return true;
40382
40524
  }
40383
- function isVolatileTestId(id) {
40384
- return /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/i.test(id) || /\b[0-9A-HJKMNP-TV-Z]{26}\b/.test(id) || /\b\d{6,}\b/.test(id);
40385
- }
40386
40525
  function hasUsefulRenderLabel(label, role) {
40387
40526
  const trimmed = label.trim();
40388
40527
  return Boolean(trimmed && trimmed !== role && trimmed !== "(icon)" && trimmed !== "(no label)");
@@ -40395,7 +40534,7 @@ function renderableTestIdAttributes(element, label) {
40395
40534
  const uniqueStableIds = [];
40396
40535
  for (const id of testIds) {
40397
40536
  const trimmed = id.trim();
40398
- if (!trimmed || trimmed.length > 80 || isVolatileTestId(trimmed)) continue;
40537
+ if (!trimmed || trimmed.length > 80 || isVolatileIdentifier(trimmed)) continue;
40399
40538
  if (!uniqueStableIds.includes(trimmed)) uniqueStableIds.push(trimmed);
40400
40539
  }
40401
40540
  return uniqueStableIds.slice(0, 2).map((id) => `testid=${id}`);
@@ -41686,10 +41825,7 @@ function formatSemanticSnapshot(yaml, options) {
41686
41825
  lines.push("");
41687
41826
  }
41688
41827
  }
41689
- let result = lines.join("\n").trim();
41690
- if (options?.offScreenRefs && options.offScreenRefs.size > 0) {
41691
- result = markOffScreenElements(result, options.offScreenRefs);
41692
- }
41828
+ const result = lines.join("\n").trim();
41693
41829
  return result;
41694
41830
  }
41695
41831
  function expandSection(yaml, sectionRef) {
@@ -41764,15 +41900,6 @@ Section with ref="${sectionRef}" not found in snapshot.
41764
41900
  targetSection.collapsed = false;
41765
41901
  return formatExpandedSectionOutput(targetSection);
41766
41902
  }
41767
- function markOffScreenElements(text, offScreenRefs) {
41768
- if (offScreenRefs.size === 0) return text;
41769
- return text.replace(/\[(e\d+)\](?! \[off-screen\])/g, (match, ref) => {
41770
- if (offScreenRefs.has(ref)) {
41771
- return `[${ref}] [off-screen]`;
41772
- }
41773
- return match;
41774
- });
41775
- }
41776
41903
  function identifyLargeGridRefs(yaml, preParsedElements) {
41777
41904
  if (!yaml || yaml.trim() === "") return [];
41778
41905
  const elements = preParsedElements ?? parseSnapshot(yaml);
@@ -41943,6 +42070,15 @@ var INTERACTIVE_ROLES2 = /* @__PURE__ */ new Set([
41943
42070
  "listbox"
41944
42071
  ]);
41945
42072
  var DIALOG_ROLES2 = /* @__PURE__ */ new Set(["dialog", "alertdialog"]);
42073
+ var CONTENT_ROLES = /* @__PURE__ */ new Set([
42074
+ "text",
42075
+ "paragraph",
42076
+ "heading",
42077
+ "alert",
42078
+ "status",
42079
+ "note"
42080
+ ]);
42081
+ var ALERT_ROLES = /* @__PURE__ */ new Set(["alert", "status"]);
41946
42082
  function extractElementState(el) {
41947
42083
  return {
41948
42084
  ref: el.ref,
@@ -41981,6 +42117,8 @@ function captureSnapshotState(url, yaml) {
41981
42117
  const editingContexts = extractEditingContextsFromElements(elements);
41982
42118
  const elementsByRef = /* @__PURE__ */ new Map();
41983
42119
  const interactiveRefs = /* @__PURE__ */ new Set();
42120
+ const contentTexts = /* @__PURE__ */ new Set();
42121
+ const alertTexts = /* @__PURE__ */ new Set();
41984
42122
  function traverse(el) {
41985
42123
  if (el.ref) {
41986
42124
  const state = extractElementState(el);
@@ -41989,6 +42127,13 @@ function captureSnapshotState(url, yaml) {
41989
42127
  interactiveRefs.add(el.ref);
41990
42128
  }
41991
42129
  }
42130
+ const text = el.text?.trim();
42131
+ if (text && CONTENT_ROLES.has(el.role)) {
42132
+ contentTexts.add(text);
42133
+ if (ALERT_ROLES.has(el.role)) {
42134
+ alertTexts.add(text);
42135
+ }
42136
+ }
41992
42137
  for (const child of el.children) {
41993
42138
  traverse(child);
41994
42139
  }
@@ -42004,9 +42149,16 @@ function captureSnapshotState(url, yaml) {
42004
42149
  dialogStack,
42005
42150
  editingContexts,
42006
42151
  elementsByRef,
42007
- interactiveRefs
42152
+ interactiveRefs,
42153
+ contentTexts,
42154
+ alertTexts
42008
42155
  };
42009
42156
  }
42157
+ function added(before, after) {
42158
+ const out = [];
42159
+ for (const t2 of after) if (!before.has(t2)) out.push(t2);
42160
+ return out;
42161
+ }
42010
42162
  function compareSnapshots(before, after) {
42011
42163
  const urlChange = before.url !== after.url ? { before: before.url, after: after.url, navigated: true } : null;
42012
42164
  const beforeActive = before.activeElement ? extractElementState(before.activeElement) : null;
@@ -42092,6 +42244,10 @@ function compareSnapshots(before, after) {
42092
42244
  before.editingContexts,
42093
42245
  after.editingContexts
42094
42246
  );
42247
+ const addedTexts = added(before.contentTexts, after.contentTexts);
42248
+ const removedTexts = added(after.contentTexts, before.contentTexts);
42249
+ const newAlerts = added(before.alertTexts, after.alertTexts);
42250
+ const contentChange = addedTexts.length > 0 || removedTexts.length > 0 || newAlerts.length > 0 ? { addedTexts, removedTexts, newAlerts } : null;
42095
42251
  return {
42096
42252
  urlChange,
42097
42253
  activeChange,
@@ -42100,11 +42256,35 @@ function compareSnapshots(before, after) {
42100
42256
  removedElements,
42101
42257
  stateChanges,
42102
42258
  editingContextChanges,
42259
+ contentChange,
42103
42260
  elementCountDelta
42104
42261
  };
42105
42262
  }
42263
+ function classifyScreenshotWorthiness(diff2, opts) {
42264
+ const structuralThreshold = opts?.structuralThreshold ?? 8;
42265
+ const contentThreshold = opts?.contentThreshold ?? 3;
42266
+ if (diff2.urlChange?.navigated) {
42267
+ return { warranted: true, reason: "navigation" };
42268
+ }
42269
+ if (diff2.dialogStackChange && diff2.dialogStackChange.after.length > diff2.dialogStackChange.before.length) {
42270
+ return { warranted: true, reason: "dialog-opened" };
42271
+ }
42272
+ const structuralDelta = diff2.newElements.length + diff2.removedElements.length;
42273
+ if (structuralDelta > structuralThreshold) {
42274
+ return { warranted: true, reason: "structural-change" };
42275
+ }
42276
+ if (diff2.contentChange) {
42277
+ if (diff2.contentChange.newAlerts.length > 0) {
42278
+ return { warranted: true, reason: "new-alert" };
42279
+ }
42280
+ if (diff2.contentChange.addedTexts.length > contentThreshold) {
42281
+ return { warranted: true, reason: "new-content" };
42282
+ }
42283
+ }
42284
+ return { warranted: false };
42285
+ }
42106
42286
  function hasDiffChanges(diff2) {
42107
- return diff2.urlChange !== null || diff2.activeChange !== null || diff2.dialogStackChange !== null || diff2.newElements.length > 0 || diff2.removedElements.length > 0 || diff2.stateChanges.length > 0 || diff2.editingContextChanges.length > 0;
42287
+ return diff2.urlChange !== null || diff2.activeChange !== null || diff2.dialogStackChange !== null || diff2.newElements.length > 0 || diff2.removedElements.length > 0 || diff2.stateChanges.length > 0 || diff2.editingContextChanges.length > 0 || diff2.contentChange !== null;
42108
42288
  }
42109
42289
  function formatElementBrief(el) {
42110
42290
  if (!el) return "none";
@@ -42214,7 +42394,7 @@ function formatSemanticDiff(diff2, actionDescription, networkInfo, currentTime)
42214
42394
  const after = diff2.dialogStackChange.after.length > 0 ? diff2.dialogStackChange.after.join(" -> ") : "(none)";
42215
42395
  lines.push(`DIALOGS: [PREVIOUS] ${before} [CURRENT] ${after}`);
42216
42396
  }
42217
- const hasChanges = diff2.newElements.length > 0 || diff2.removedElements.length > 0 || diff2.stateChanges.length > 0 || diff2.editingContextChanges.length > 0;
42397
+ const hasChanges = diff2.newElements.length > 0 || diff2.removedElements.length > 0 || diff2.stateChanges.length > 0 || diff2.editingContextChanges.length > 0 || diff2.contentChange !== null;
42218
42398
  if (hasChanges) {
42219
42399
  lines.push("");
42220
42400
  lines.push("CHANGES DETECTED:");
@@ -42258,6 +42438,25 @@ function formatSemanticDiff(diff2, actionDescription, networkInfo, currentTime)
42258
42438
  ` - REMOVED: ${removedToShow.map((e2) => `${e2.role} "${truncate(e2.text, 15)}" [${e2.ref}]`).join(", ")}${moreText}`
42259
42439
  );
42260
42440
  }
42441
+ if (diff2.contentChange) {
42442
+ for (const alert of diff2.contentChange.newAlerts.slice(0, 5)) {
42443
+ lines.push(` - NEW ALERT: "${truncate(alert, 80)}"`);
42444
+ }
42445
+ const addedToShow = diff2.contentChange.addedTexts.filter((t2) => !diff2.contentChange.newAlerts.includes(t2)).slice(0, 5);
42446
+ if (addedToShow.length > 0) {
42447
+ const moreCount = diff2.contentChange.addedTexts.length - diff2.contentChange.newAlerts.length - addedToShow.length;
42448
+ const moreText = moreCount > 0 ? ` (+${moreCount} more)` : "";
42449
+ lines.push(` - CONTENT: + ${addedToShow.map((t2) => `"${truncate(t2, 60)}"`).join(", ")}${moreText}`);
42450
+ }
42451
+ const removedToShow2 = diff2.contentChange.removedTexts.slice(0, 5);
42452
+ if (removedToShow2.length > 0) {
42453
+ const moreCount = diff2.contentChange.removedTexts.length - removedToShow2.length;
42454
+ const moreText = moreCount > 0 ? ` (+${moreCount} more)` : "";
42455
+ lines.push(
42456
+ ` - CONTENT: - ${removedToShow2.map((t2) => `"${truncate(t2, 60)}"`).join(", ")}${moreText}`
42457
+ );
42458
+ }
42459
+ }
42261
42460
  }
42262
42461
  if (diff2.elementCountDelta !== 0) {
42263
42462
  const sign = diff2.elementCountDelta > 0 ? "+" : "";
@@ -42825,13 +43024,21 @@ function extractFormattedSnapshotRefs(text) {
42825
43024
  }
42826
43025
  return [...refs];
42827
43026
  }
42828
- function markOffScreenRefsInText(text, offScreenRefs) {
42829
- if (offScreenRefs.size === 0) return text;
42830
- return text.replace(/\[(e\d+)\](?! \[off-screen\])/g, (match, ref) => {
42831
- if (offScreenRefs.has(ref)) {
42832
- return `[${ref}] [off-screen]`;
42833
- }
42834
- return match;
43027
+ function parseHorizontalScrollRefs(yaml) {
43028
+ const byRef = /* @__PURE__ */ new Map();
43029
+ for (const line of yaml.split("\n")) {
43030
+ const refMatch = line.match(/\[ref=(e\d+)\]/);
43031
+ if (!refMatch) continue;
43032
+ const dirMatch = line.match(/~hclip=(left|right)\b/);
43033
+ if (dirMatch) byRef.set(refMatch[1], dirMatch[1]);
43034
+ }
43035
+ return byRef;
43036
+ }
43037
+ function markHorizontalScrollRefsInText(text, byRef) {
43038
+ if (byRef.size === 0) return text;
43039
+ return text.replace(/\[(e\d+)\](?! \[scroll-(?:left|right)\])/g, (match, ref) => {
43040
+ const dir = byRef.get(ref);
43041
+ return dir ? `[${ref}] [scroll-${dir}]` : match;
42835
43042
  });
42836
43043
  }
42837
43044
  var BrowserToolExecutor = class {
@@ -42851,12 +43058,20 @@ var BrowserToolExecutor = class {
42851
43058
  * call share one allocation instead of calling `combineSignals` each time.
42852
43059
  */
42853
43060
  cachedCombinedSignal;
43061
+ /**
43062
+ * In-flight forced page-recovery kicked off when a tool call's per-tool
43063
+ * timeout fired on a wedged page. The NEXT `withCallSignal` awaits it before
43064
+ * running, so the next action starts on a fresh, re-navigated page instead of
43065
+ * issuing another orphaning op against the frozen renderer.
43066
+ */
43067
+ recoveryInFlight;
42854
43068
  onScreenshot;
42855
43069
  maxImageDimension;
42856
43070
  clickValidation;
42857
43071
  allowedHosts;
42858
43072
  autoSnapshotAfterAction;
42859
43073
  includeScreenshotWithSnapshot;
43074
+ liveSnapshotEnabled;
42860
43075
  workspaceDir;
42861
43076
  logger;
42862
43077
  spanFactory;
@@ -42896,6 +43111,9 @@ var BrowserToolExecutor = class {
42896
43111
  this.allowedHosts = options?.allowedHosts;
42897
43112
  this.autoSnapshotAfterAction = options?.autoSnapshotAfterAction !== false;
42898
43113
  this.includeScreenshotWithSnapshot = options?.includeScreenshotWithSnapshot !== false;
43114
+ this.liveSnapshotEnabled = options?.liveSnapshot === true;
43115
+ this.client.setLiveSnapshotEnabled?.(this.liveSnapshotEnabled);
43116
+ this.client.setLiveSnapshotRawOnMiss?.(options?.liveSnapshotRawOnMiss === true);
42899
43117
  if (!options?.workspaceDir && !options?.tmpBaseDir) {
42900
43118
  options?.logger?.debug?.(
42901
43119
  "[BrowserToolExecutor] No tmpBaseDir provided, using flat canary dir (not org-scoped)"
@@ -42993,6 +43211,12 @@ var BrowserToolExecutor = class {
42993
43211
  "[BrowserToolExecutor] withCallSignal invoked while another per-call signal was already active. Concurrent tool calls on a single executor are not supported \u2014 verify the session gate is in place."
42994
43212
  );
42995
43213
  }
43214
+ if (this.recoveryInFlight) {
43215
+ const pending = this.recoveryInFlight;
43216
+ this.recoveryInFlight = void 0;
43217
+ await pending.catch(() => {
43218
+ });
43219
+ }
42996
43220
  const prev = this.callSignal;
42997
43221
  const prevCached = this.cachedCombinedSignal;
42998
43222
  this.callSignal = signal;
@@ -43007,6 +43231,7 @@ var BrowserToolExecutor = class {
43007
43231
  error: err instanceof Error ? err.message : String(err)
43008
43232
  });
43009
43233
  }
43234
+ this.scheduleForceRecover("tool_timeout");
43010
43235
  };
43011
43236
  if (signal?.aborted) {
43012
43237
  fireUnwedge();
@@ -43033,6 +43258,56 @@ var BrowserToolExecutor = class {
43033
43258
  if (onAbort && signal) signal.removeEventListener("abort", onAbort);
43034
43259
  }
43035
43260
  }
43261
+ /**
43262
+ * Schedule a liveness-gated forced recovery, CHAINING onto any already-pending
43263
+ * one. Two triggers can fire within a single tool call — the per-tool abort
43264
+ * (`withCallSignal`) and the post-action snapshot timeout — so a bare
43265
+ * assignment would clobber the first (leaving it unawaited) and double-probe.
43266
+ * Chaining makes the next `withCallSignal` await the whole sequence and keeps
43267
+ * the probes serial. This is the single owner of `recoveryInFlight`.
43268
+ */
43269
+ scheduleForceRecover(reason) {
43270
+ this.recoveryInFlight = (this.recoveryInFlight ?? Promise.resolve()).then(
43271
+ () => this.maybeForceRecover(reason)
43272
+ );
43273
+ }
43274
+ /**
43275
+ * Liveness-gated forced recovery. Probes whether the page's main thread is
43276
+ * responsive; only tears it down when it is genuinely wedged, so a
43277
+ * slow-but-progressing page is spared (its orphan settles on its own and the
43278
+ * abort-gated re-checks elsewhere prevent it from corrupting state). Never
43279
+ * throws — recovery is best-effort.
43280
+ */
43281
+ async maybeForceRecover(reason) {
43282
+ const client = this.client;
43283
+ if (typeof client.probeLiveness !== "function" || typeof client.forceRecoverPage !== "function") {
43284
+ return;
43285
+ }
43286
+ try {
43287
+ const liveness = await client.probeLiveness();
43288
+ if (liveness === "alive") {
43289
+ this.logger?.info?.(
43290
+ "[BrowserToolExecutor] page alive after tool timeout; sparing it (no forced recovery)"
43291
+ );
43292
+ return;
43293
+ }
43294
+ this.logger?.warn?.(
43295
+ liveness === "closed" ? "[BrowserToolExecutor] page already closed after tool timeout; recreating" : "[BrowserToolExecutor] page unresponsive after tool timeout; forcing page recovery",
43296
+ { liveness, reason }
43297
+ );
43298
+ const recovered = await client.forceRecoverPage(reason);
43299
+ if (!recovered) {
43300
+ this.logger?.warn?.(
43301
+ "[BrowserToolExecutor] forced recovery did not yield a live page; next action may fail",
43302
+ { liveness, reason }
43303
+ );
43304
+ }
43305
+ } catch (err) {
43306
+ this.logger?.warn?.("[BrowserToolExecutor] forced recovery failed", {
43307
+ error: err instanceof Error ? err.message : String(err)
43308
+ });
43309
+ }
43310
+ }
43036
43311
  // ==================== Navigation ====================
43037
43312
  async navigate(url) {
43038
43313
  ensureAllowedHost(url, this.allowedHosts);
@@ -43239,32 +43514,15 @@ ${effectiveShowCoordinateGrid ? "Coordinate grid enabled - read coordinates from
43239
43514
  matchedHintIds: void 0
43240
43515
  };
43241
43516
  let semanticText = formatSemanticSnapshot(yaml, snapshotOpts);
43242
- try {
43243
- const formattedRefs = extractFormattedSnapshotRefs(semanticText);
43244
- const offScreenRefs = await this.traced("offscreen_detection", "off-screen element scan", async () => {
43245
- if (!this.client.getElementBoundingBoxes || formattedRefs.length === 0) return void 0;
43246
- const boxes = await this.client.getElementBoundingBoxes(formattedRefs, {
43247
- signal: this.currentSignal
43248
- });
43249
- const { width: vw, height: vh } = this.client.getViewportSize();
43250
- const offscreen = /* @__PURE__ */ new Set();
43251
- for (const [ref, box] of boxes) {
43252
- if (box.width === 0 && box.height === 0) continue;
43253
- const right = box.x + box.width;
43254
- const bottom = box.y + box.height;
43255
- if (right < 0 || box.x > vw || bottom < 0 || box.y > vh) {
43256
- offscreen.add(ref);
43257
- }
43258
- }
43259
- return offscreen.size > 0 ? offscreen : void 0;
43260
- });
43261
- if (offScreenRefs) {
43262
- semanticText = markOffScreenRefsInText(semanticText, offScreenRefs);
43517
+ const formattedRefs = extractFormattedSnapshotRefs(semanticText);
43518
+ if (formattedRefs.length > LARGE_PAGE_REF_THRESHOLD) {
43519
+ semanticText = `[Large page: ~${formattedRefs.length.toLocaleString()} interactive elements \u2014 scroll-position hints reduced for speed. Narrow with browser_snapshot search=\u2026 or scroll to a region.]
43520
+ ` + semanticText;
43521
+ } else {
43522
+ const hScrollRefs = parseHorizontalScrollRefs(yaml);
43523
+ if (hScrollRefs.size > 0) {
43524
+ semanticText = markHorizontalScrollRefsInText(semanticText, hScrollRefs);
43263
43525
  }
43264
- } catch (e2) {
43265
- this.logger?.debug?.("[browser_snapshot] Off-screen detection failed", {
43266
- error: e2 instanceof Error ? e2.message : String(e2)
43267
- });
43268
43526
  }
43269
43527
  if (snapshotOpts.matchedHintIds && snapshotOpts.matchedHintIds.length > 0 && this.hintProvider) {
43270
43528
  this.hintProvider.recordMatches(snapshotOpts.matchedHintIds).catch(() => {
@@ -43812,11 +44070,14 @@ ${header}
43812
44070
  const autoEnabled = this.autoSnapshotAfterAction;
43813
44071
  const includeScreenshot = this.includeScreenshotWithSnapshot;
43814
44072
  const beforeUrl = await this.client.getCurrentUrl();
43815
- const beforeResult = await this.client.snapshot({ signal: this.currentSignal, mode: "tree" });
43816
- const beforeYaml = extractSnapshotYaml(beforeResult);
44073
+ let beforeYaml = this.liveSnapshotEnabled ? await this.client.getReusableSnapshotYaml?.() ?? null : null;
44074
+ if (beforeYaml === null) {
44075
+ const beforeResult = await this.client.snapshot({ signal: this.currentSignal, mode: "tree" });
44076
+ beforeYaml = extractSnapshotYaml(beforeResult);
44077
+ }
43817
44078
  const beforeState = captureSnapshotState(beforeUrl, beforeYaml);
43818
44079
  const actionResult = await action();
43819
- const afterMode = autoEnabled && includeScreenshot ? "both" : "tree";
44080
+ const afterMode = !this.liveSnapshotEnabled && autoEnabled && includeScreenshot ? "both" : "tree";
43820
44081
  let afterUrl = beforeUrl;
43821
44082
  let afterResult = "";
43822
44083
  let afterYaml = "";
@@ -43902,6 +44163,7 @@ ${header}
43902
44163
  }
43903
44164
  if (snapshotTimedOut) {
43904
44165
  this.consecutiveSnapshotTimeouts += 1;
44166
+ this.scheduleForceRecover("post_action_snapshot_timeout");
43905
44167
  const now2 = (/* @__PURE__ */ new Date()).toISOString();
43906
44168
  const baseDiff = formatSemanticDiff(
43907
44169
  compareSnapshots(beforeState, beforeState),
@@ -43966,7 +44228,31 @@ ${recoveryHint}`;
43966
44228
  consoleErrors: this.client.getNewConsoleErrors?.() ?? [],
43967
44229
  failedRequests: this.client.getNewFailedRequests?.() ?? []
43968
44230
  });
43969
- const afterImages = isMCPContentWithImages(afterResult) ? afterResult.images : void 0;
44231
+ let afterImages;
44232
+ let withheldNote;
44233
+ if (this.liveSnapshotEnabled) {
44234
+ if (includeScreenshot) {
44235
+ const worthiness = classifyScreenshotWorthiness(diff2);
44236
+ if (worthiness.warranted) {
44237
+ try {
44238
+ const shot = await this.client.snapshot({
44239
+ signal: this.currentSignal,
44240
+ mode: "screenshot",
44241
+ maxImageDimension: this.maxImageDimension
44242
+ });
44243
+ afterImages = isMCPContentWithImages(shot) ? shot.images : void 0;
44244
+ } catch (e2) {
44245
+ this.logger?.warn?.("[withSemanticDiff] On-demand screenshot failed", {
44246
+ error: e2 instanceof Error ? e2.message : String(e2)
44247
+ });
44248
+ }
44249
+ } else {
44250
+ withheldNote = "IMAGE: withheld \u2014 no visual-orienting change (input/focus/state-only). Call browser_screenshot or browser_snapshot mode:'both' for a full image.";
44251
+ }
44252
+ }
44253
+ } else {
44254
+ afterImages = isMCPContentWithImages(afterResult) ? afterResult.images : void 0;
44255
+ }
43970
44256
  if (afterImages?.length && this.onScreenshot) {
43971
44257
  const img = afterImages[0];
43972
44258
  await this.onScreenshot(`data:${img.mimeType};base64,${img.data}`, "auto-snapshot");
@@ -43979,6 +44265,9 @@ ${recoveryHint}`;
43979
44265
  this.logger
43980
44266
  );
43981
44267
  snapshotText = resolutionNote + "\n" + semanticText;
44268
+ } else if (withheldNote) {
44269
+ snapshotText = `${withheldNote}
44270
+ ${semanticText}`;
43982
44271
  }
43983
44272
  autoSnapshot = {
43984
44273
  text: snapshotText,
@@ -43990,6 +44279,7 @@ ${recoveryHint}`;
43990
44279
  });
43991
44280
  }
43992
44281
  }
44282
+ this.client.warmSnapshot?.();
43993
44283
  return { diffText, actionResult, autoSnapshot };
43994
44284
  }
43995
44285
  /**
@@ -44348,7 +44638,7 @@ function getBrowserToolDefinitions() {
44348
44638
  },
44349
44639
  toRef: {
44350
44640
  type: "string",
44351
- description: "Scroll the page so this element is centered in the viewport. Handles both vertical and horizontal scrolling automatically \u2014 works for grid columns that are horizontally off-screen."
44641
+ description: "Scroll the page so this element is centered in the viewport. Handles both vertical and horizontal scrolling automatically \u2014 works for grid columns that are horizontally off-screen. A ref marked [scroll-left]/[scroll-right] in the snapshot is clipped inside a horizontal scroll container; pass it here to bring it into view."
44352
44642
  },
44353
44643
  x: {
44354
44644
  type: "number",
@@ -47302,6 +47592,290 @@ async function augmentBlockingModalSignal(page, yaml, logger2, preParsedElements
47302
47592
  }
47303
47593
  }
47304
47594
 
47595
+ // ../browser-core/src/playwright-client/dom-revision-script.ts
47596
+ var DOM_REVISION_SCRIPT = `
47597
+ (() => {
47598
+ if (window.__domRevisionInstalled) return;
47599
+ window.__domRevisionInstalled = true;
47600
+ window.__domRevision = 0;
47601
+ window.__lastMutationTs = (typeof performance !== 'undefined' && performance.now)
47602
+ ? performance.now()
47603
+ : Date.now();
47604
+
47605
+ var now = function () {
47606
+ return (typeof performance !== 'undefined' && performance.now)
47607
+ ? performance.now()
47608
+ : Date.now();
47609
+ };
47610
+
47611
+ var bump = function () {
47612
+ window.__domRevision = (window.__domRevision || 0) + 1;
47613
+ window.__lastMutationTs = now();
47614
+ };
47615
+
47616
+ var install = function () {
47617
+ try {
47618
+ // Observe the Document node itself (not documentElement): it survives
47619
+ // document.open()/write() \u2014 which replaces documentElement and would
47620
+ // orphan an observer attached to it \u2014 while subtree:true still catches
47621
+ // every descendant childList/attribute/text mutation.
47622
+ var observer = new MutationObserver(bump);
47623
+ observer.observe(document, {
47624
+ childList: true,
47625
+ attributes: true,
47626
+ characterData: true,
47627
+ subtree: true,
47628
+ });
47629
+ window.__domRevisionObserver = observer;
47630
+ return true;
47631
+ } catch (e) {
47632
+ return false;
47633
+ }
47634
+ };
47635
+
47636
+ if (!install()) {
47637
+ document.addEventListener('DOMContentLoaded', install, { once: true });
47638
+ }
47639
+
47640
+ // Input/focus activity doesn't always fire a mutation (typing sets the .value
47641
+ // property), but it IS "the page is changing" for settle purposes \u2014 so count
47642
+ // it toward __lastMutationTs. Capture phase on window so it survives document
47643
+ // replacement and catches events from inside shadow DOM (they're composed).
47644
+ window.addEventListener('input', bump, true);
47645
+ window.addEventListener('change', bump, true);
47646
+ window.addEventListener('focusin', bump, true);
47647
+ window.addEventListener('focusout', bump, true);
47648
+
47649
+ // Resolves when the DOM has been quiet (no mutations / input) for quietMs, or
47650
+ // after maxWaitMs elapses \u2014 whichever comes first. Event-driven (no polling):
47651
+ // a single timer is (re)scheduled off __lastMutationTs.
47652
+ window.__waitDomQuiet = function (quietMs, maxWaitMs) {
47653
+ quietMs = typeof quietMs === 'number' ? quietMs : 100;
47654
+ maxWaitMs = typeof maxWaitMs === 'number' ? maxWaitMs : 2000;
47655
+ return new Promise(function (resolve) {
47656
+ var start = now();
47657
+ var timer = null;
47658
+ var done = false;
47659
+
47660
+ var finish = function (reason) {
47661
+ if (done) return;
47662
+ done = true;
47663
+ if (timer) clearTimeout(timer);
47664
+ resolve(reason);
47665
+ };
47666
+
47667
+ var schedule = function () {
47668
+ if (done) return;
47669
+ var sinceMutation = now() - (window.__lastMutationTs || 0);
47670
+ var sinceStart = now() - start;
47671
+ // Hard cap: never block past maxWaitMs (streaming/websocket pages).
47672
+ if (sinceStart >= maxWaitMs) {
47673
+ finish('maxwait');
47674
+ return;
47675
+ }
47676
+ if (sinceMutation >= quietMs) {
47677
+ finish('quiet');
47678
+ return;
47679
+ }
47680
+ // Wait for the shorter of "quiet window remaining" / "maxwait remaining",
47681
+ // then re-check (a mutation may have landed since, moving __lastMutationTs).
47682
+ var waitForQuiet = quietMs - sinceMutation;
47683
+ var waitForCap = maxWaitMs - sinceStart;
47684
+ timer = setTimeout(schedule, Math.max(0, Math.min(waitForQuiet, waitForCap)));
47685
+ };
47686
+
47687
+ schedule();
47688
+ });
47689
+ };
47690
+ })();
47691
+ `;
47692
+
47693
+ // ../browser-core/src/playwright-client/evaluate-with-cap.ts
47694
+ var EVALUATE_CAPPED = /* @__PURE__ */ Symbol("browser-core.evaluate-capped");
47695
+ async function evaluateWithCap(capMs, op) {
47696
+ let timer;
47697
+ let capped = false;
47698
+ const cap = new Promise((resolve) => {
47699
+ timer = setTimeout(() => {
47700
+ capped = true;
47701
+ resolve(EVALUATE_CAPPED);
47702
+ }, capMs);
47703
+ });
47704
+ const evaluation = op().catch(
47705
+ (err) => {
47706
+ if (capped) return EVALUATE_CAPPED;
47707
+ throw err;
47708
+ }
47709
+ );
47710
+ try {
47711
+ return await Promise.race([evaluation, cap]);
47712
+ } finally {
47713
+ if (timer) clearTimeout(timer);
47714
+ }
47715
+ }
47716
+
47717
+ // ../browser-core/src/playwright-client/http-settle.ts
47718
+ var STREAM_RESOURCE_TYPES = /* @__PURE__ */ new Set(["eventsource", "websocket"]);
47719
+ function countUnsettledRequests(requests, now, opts) {
47720
+ let n2 = 0;
47721
+ for (const req of requests) {
47722
+ if (req.status !== "pending") continue;
47723
+ if (req.resourceType !== void 0 && STREAM_RESOURCE_TYPES.has(req.resourceType)) continue;
47724
+ if (opts.noisePatterns.some((p2) => p2.test(req.url))) continue;
47725
+ if (req.startedAt !== void 0 && now - req.startedAt > opts.maxAgeMs) continue;
47726
+ n2++;
47727
+ }
47728
+ return n2;
47729
+ }
47730
+
47731
+ // ../browser-core/src/playwright-client/liveness-probe.ts
47732
+ var Liveness = {
47733
+ Alive: "alive",
47734
+ Unresponsive: "unresponsive",
47735
+ Closed: "closed"
47736
+ };
47737
+ var CLOSURE_HINTS = [
47738
+ "target closed",
47739
+ "has been closed",
47740
+ "browser has been closed",
47741
+ "execution context was destroyed",
47742
+ "browser closed",
47743
+ "context closed"
47744
+ ];
47745
+ function looksClosed(err) {
47746
+ const message = (err instanceof Error ? err.message : String(err)).toLowerCase();
47747
+ return CLOSURE_HINTS.some((h) => message.includes(h));
47748
+ }
47749
+ async function probePageLiveness(page, capMs) {
47750
+ if (!page || page.isClosed()) return Liveness.Closed;
47751
+ try {
47752
+ const result = await evaluateWithCap(capMs, () => page.evaluate(() => 1));
47753
+ if (result === EVALUATE_CAPPED) return Liveness.Unresponsive;
47754
+ return Liveness.Alive;
47755
+ } catch (err) {
47756
+ return looksClosed(err) ? Liveness.Closed : Liveness.Unresponsive;
47757
+ }
47758
+ }
47759
+
47760
+ // ../browser-core/src/live-snapshot.ts
47761
+ function mintedYaml(snap) {
47762
+ return snap.augmentedYaml ?? snap.rawYaml;
47763
+ }
47764
+ var LiveSnapshotManager = class {
47765
+ constructor(deps) {
47766
+ this.deps = deps;
47767
+ this.now = deps.now ?? (() => Date.now());
47768
+ }
47769
+ deps;
47770
+ current = null;
47771
+ generationCounter = 0;
47772
+ /** Serializes captures so two never run against the page at once. */
47773
+ chain = Promise.resolve();
47774
+ now;
47775
+ /** The current mint without re-checking the live page (may be stale). */
47776
+ peek() {
47777
+ return this.current;
47778
+ }
47779
+ /** Drop the cached snapshot (e.g. on navigation / new page). */
47780
+ reset() {
47781
+ this.current = null;
47782
+ }
47783
+ /** Run `fn` after any in-flight capture settles; never overlap captures. */
47784
+ enqueue(fn) {
47785
+ const run = this.chain.then(fn, fn);
47786
+ this.chain = run.then(
47787
+ () => void 0,
47788
+ () => void 0
47789
+ );
47790
+ return run;
47791
+ }
47792
+ /**
47793
+ * Produce a current snapshot, reusing the augmented one when the raw substrate
47794
+ * is unchanged. Always takes a fresh (cheap) raw substrate first, so it never
47795
+ * serves a stale representation.
47796
+ */
47797
+ mint(opts) {
47798
+ return this.enqueue(() => this.doMint(opts));
47799
+ }
47800
+ async doMint(opts) {
47801
+ const raw = await this.deps.captureRaw(opts?.signal);
47802
+ if (!raw) {
47803
+ if (this.current) return this.current;
47804
+ throw new Error("[LiveSnapshot] captureRaw returned null and there is no current snapshot");
47805
+ }
47806
+ if (!opts?.force && this.current && this.current.augmentedYaml !== null && this.current.rawYaml === raw.rawYaml) {
47807
+ return this.current;
47808
+ }
47809
+ if (opts?.rawOnMiss) {
47810
+ const snap = this.store(raw.url, raw.rawYaml, null);
47811
+ this.scheduleAugment(snap, opts?.signal);
47812
+ return snap;
47813
+ }
47814
+ const augmentedYaml = await this.deps.augment(raw.rawYaml, opts?.signal);
47815
+ return this.store(raw.url, raw.rawYaml, augmentedYaml);
47816
+ }
47817
+ store(url, rawYaml, augmentedYaml) {
47818
+ const snap = {
47819
+ generation: ++this.generationCounter,
47820
+ url,
47821
+ rawYaml,
47822
+ augmentedYaml,
47823
+ capturedAtMs: this.now()
47824
+ };
47825
+ this.current = snap;
47826
+ return snap;
47827
+ }
47828
+ /** Background-augment a raw-only mint, upgrading `current` in place if still current. */
47829
+ scheduleAugment(snap, signal) {
47830
+ void this.enqueue(async () => {
47831
+ if (this.current !== snap) return;
47832
+ try {
47833
+ const augmentedYaml = await this.deps.augment(snap.rawYaml, signal);
47834
+ if (this.current === snap) {
47835
+ this.current = { ...snap, augmentedYaml };
47836
+ }
47837
+ } catch (err) {
47838
+ this.deps.logger?.debug?.("[LiveSnapshot] background augment failed (ignored)", {
47839
+ error: err instanceof Error ? err.message : String(err)
47840
+ });
47841
+ }
47842
+ }).catch(() => void 0);
47843
+ }
47844
+ /**
47845
+ * Return the current AUGMENTED snapshot if its raw substrate still matches the
47846
+ * live page, else null — used to reuse the warm snapshot as a diff baseline.
47847
+ * Takes a fresh (cheap) raw substrate to verify; never augments.
47848
+ */
47849
+ getReusable(opts) {
47850
+ return this.enqueue(async () => {
47851
+ if (!this.current || this.current.augmentedYaml === null) return null;
47852
+ const raw = await this.deps.captureRaw(opts?.signal);
47853
+ if (!raw) return null;
47854
+ return raw.rawYaml === this.current.rawYaml ? this.current : null;
47855
+ });
47856
+ }
47857
+ /**
47858
+ * Fire-and-forget background refresh. Waits for the DOM to settle, then mints
47859
+ * (reusing or augmenting) so the next request hits the reuse path. Errors are
47860
+ * swallowed — warming must never surface to the agent.
47861
+ */
47862
+ warm(opts) {
47863
+ void this.warmAsync(opts).catch((err) => {
47864
+ this.deps.logger?.debug?.("[LiveSnapshot] warm failed (ignored)", {
47865
+ error: err instanceof Error ? err.message : String(err)
47866
+ });
47867
+ });
47868
+ }
47869
+ /** Test seam: the awaitable warm body. */
47870
+ async warmAsync(opts) {
47871
+ if (this.deps.waitQuiet) {
47872
+ await this.deps.waitQuiet(opts?.quietMs ?? 100, opts?.maxWaitMs ?? 2e3);
47873
+ }
47874
+ if (opts?.signal?.aborted) return;
47875
+ await this.mint({ signal: opts?.signal, rawOnMiss: opts?.rawOnMiss });
47876
+ }
47877
+ };
47878
+
47305
47879
  // ../browser-core/src/playwright-client/response-format.ts
47306
47880
  var TOAST_SNAPSHOT_TTL = 5;
47307
47881
  function formatMcpResponse(input) {
@@ -47565,6 +48139,7 @@ var PlaywrightClient = class _PlaywrightClient {
47565
48139
  screencastEncoderFactory;
47566
48140
  tmpBaseDir;
47567
48141
  spanFactory;
48142
+ stabilizeDomMs;
47568
48143
  /** In-flight latch for `attemptUnwedgePage` so close-succession aborts
47569
48144
  * don't dispatch redundant `window.stop()` evaluates. */
47570
48145
  unwedgeInFlight = false;
@@ -47572,6 +48147,7 @@ var PlaywrightClient = class _PlaywrightClient {
47572
48147
  this.browserLease = options?.browserLease ?? null;
47573
48148
  this.logger = options?.logger ?? consoleLogger;
47574
48149
  this.spanFactory = options?.spanFactory ?? null;
48150
+ this.stabilizeDomMs = options?.stabilizeDomMs ?? TIMEOUTS.STABILIZE_DOM;
47575
48151
  this.screencastEncoderFactory = options?.screencastEncoderFactory ?? null;
47576
48152
  this.cursorOverlay = new CursorOverlay({ enabled: false });
47577
48153
  if (!options?.tmpBaseDir) {
@@ -47580,6 +48156,95 @@ var PlaywrightClient = class _PlaywrightClient {
47580
48156
  );
47581
48157
  }
47582
48158
  this.tmpBaseDir = options?.tmpBaseDir ?? getCanaryTmpDir();
48159
+ this.liveSnapshot = new LiveSnapshotManager({
48160
+ logger: this.logger,
48161
+ // CHEAP tier: the raw substrate (the freshness ground truth).
48162
+ captureRaw: async (signal) => {
48163
+ const page = this.page;
48164
+ if (!page || page.isClosed()) return null;
48165
+ const rawYaml = await this.captureRawSubstrate(signal).catch(() => "");
48166
+ if (signal?.aborted || !rawYaml) return null;
48167
+ return { rawYaml, url: page.url() };
48168
+ },
48169
+ // EXPENSIVE tier: augment the raw substrate.
48170
+ augment: (rawYaml, signal) => this.augmentSubstrate(rawYaml, signal),
48171
+ waitQuiet: (quietMs, maxWaitMs) => this.waitDomQuiet(quietMs, maxWaitMs)
48172
+ });
48173
+ }
48174
+ /** Enable/disable the live-snapshot (warm + minted) path. Off = legacy v1. */
48175
+ setLiveSnapshotEnabled(enabled) {
48176
+ this.liveSnapshotEnabled = enabled;
48177
+ }
48178
+ /**
48179
+ * On a freshness miss, return the raw substrate immediately and augment in the
48180
+ * background (instead of blocking). Default off → block on augmentation
48181
+ * (matches legacy behavior and the snapshot formatter's expectations).
48182
+ */
48183
+ setLiveSnapshotRawOnMiss(rawOnMiss) {
48184
+ this.liveSnapshotRawOnMiss = rawOnMiss;
48185
+ }
48186
+ /**
48187
+ * Capture the accessibility-tree YAML, routing through the live-snapshot
48188
+ * manager when enabled (raw substrate compared exactly; augmentation reused
48189
+ * when unchanged). `force` re-augments unconditionally (used by navigation).
48190
+ * Falls back to the legacy full capture when disabled.
48191
+ */
48192
+ captureTreeYaml(signal, opts) {
48193
+ if (this.liveSnapshotEnabled) {
48194
+ return this.liveSnapshot.mint({ signal, force: opts?.force, rawOnMiss: this.liveSnapshotRawOnMiss }).then((s2) => mintedYaml(s2));
48195
+ }
48196
+ return this.traced(
48197
+ "snapshot_for_ai",
48198
+ "capture accessibility tree",
48199
+ () => this.captureSnapshotInternal(signal)
48200
+ );
48201
+ }
48202
+ /**
48203
+ * Wait until the DOM has been quiet for `quietMs` (bounded by `maxWaitMs`),
48204
+ * using the page-side `__waitDomQuiet` helper. If the helper is missing or
48205
+ * unreadable (init-script race, or a pre-existing CDP document), fall back to
48206
+ * a fixed `quietMs` delay — NOT an immediate return, which would drop the
48207
+ * stabilize wait and risk capturing a half-rendered SPA. Soft — never throws.
48208
+ *
48209
+ * The evaluate is bounded by a DRIVER-side race (`maxWaitMs` + grace): the
48210
+ * in-page cap is a page-side setTimeout that never fires when the main
48211
+ * thread is blocked, so without the race a single stalled page hangs this
48212
+ * await forever and wedges every subsequent tool call (June 2026
48213
+ * live-snapshot suite wedge). On cap we return immediately — the stabilize
48214
+ * budget is already spent; stacking the quietMs fallback on top would only
48215
+ * delay the caller further on a page that can't get healthier by waiting.
48216
+ */
48217
+ async waitDomQuiet(quietMs, maxWaitMs) {
48218
+ const page = this.page;
48219
+ if (!page || page.isClosed()) return;
48220
+ let ran = false;
48221
+ try {
48222
+ const result = await evaluateWithCap(
48223
+ maxWaitMs + TIMEOUTS.WAIT_DOM_QUIET_EVAL_GRACE,
48224
+ () => page.evaluate(
48225
+ async ({ q, m }) => {
48226
+ const w = window;
48227
+ if (typeof w.__waitDomQuiet !== "function") return false;
48228
+ await w.__waitDomQuiet(q, m);
48229
+ return true;
48230
+ },
48231
+ { q: quietMs, m: maxWaitMs }
48232
+ )
48233
+ );
48234
+ if (result === EVALUATE_CAPPED) {
48235
+ this.logger.warn(
48236
+ "[DirectPlaywright] waitDomQuiet evaluate exceeded driver-side cap (page main thread may be blocked)",
48237
+ { maxWaitMs, graceMs: TIMEOUTS.WAIT_DOM_QUIET_EVAL_GRACE }
48238
+ );
48239
+ return;
48240
+ }
48241
+ ran = result;
48242
+ } catch {
48243
+ ran = false;
48244
+ }
48245
+ if (!ran) {
48246
+ await page.waitForTimeout(quietMs).catch(() => void 0);
48247
+ }
47583
48248
  }
47584
48249
  /**
47585
48250
  * Race a Playwright op against `signal`:
@@ -47646,7 +48311,7 @@ var PlaywrightClient = class _PlaywrightClient {
47646
48311
  this.logger.info("[DirectPlaywright] attemptUnwedgePage dispatched");
47647
48312
  let timer;
47648
48313
  void Promise.race([
47649
- page.evaluate("window.stop()").then(() => this.logger.info("[DirectPlaywright] attemptUnwedgePage completed")).catch((err) => {
48314
+ page.evaluate(() => window.stop()).then(() => this.logger.info("[DirectPlaywright] attemptUnwedgePage completed")).catch((err) => {
47650
48315
  this.logger.warn("[DirectPlaywright] attemptUnwedgePage evaluate failed", {
47651
48316
  error: err instanceof Error ? err.message : String(err)
47652
48317
  });
@@ -47665,6 +48330,65 @@ var PlaywrightClient = class _PlaywrightClient {
47665
48330
  this.unwedgeInFlight = false;
47666
48331
  });
47667
48332
  }
48333
+ /**
48334
+ * Cheap, driver-bounded liveness check (`page.evaluate(() => 1)` under a
48335
+ * short cap). Used by the per-tool-timeout recovery path to decide whether a
48336
+ * timed-out page is genuinely wedged (force-recover) or just slow (spare it).
48337
+ */
48338
+ async probeLiveness() {
48339
+ return probePageLiveness(this.page, TIMEOUTS.LIVENESS_PROBE_CAP);
48340
+ }
48341
+ /**
48342
+ * Force-recover the active page so orphaned Playwright ops die with the
48343
+ * renderer. `ariaSnapshot`/`screenshot` have no timeout and cannot be
48344
+ * cancelled; when the per-tool timeout fires they keep running for minutes on
48345
+ * a jammed renderer. Closing the page rejects every in-flight op bound to its
48346
+ * CDP session — the only thing that truly kills the orphans — then we recreate
48347
+ * the page and re-navigate to `lastKnownUrl` via the existing crash-recovery
48348
+ * path. Never throws; returns whether a live page is available afterward.
48349
+ */
48350
+ async forceRecoverPage(reason) {
48351
+ if (this.pageClosedIntentionally) return false;
48352
+ if (this.restoreInProgress && this.restorePromise) {
48353
+ await this.restorePromise.catch(() => {
48354
+ });
48355
+ return !!this.page && !this.page.isClosed();
48356
+ }
48357
+ const startedAt = Date.now();
48358
+ try {
48359
+ const page = this.page;
48360
+ if (page && !page.isClosed()) {
48361
+ this.forcedTeardownInProgress = true;
48362
+ try {
48363
+ await this.withOperationTimeout(
48364
+ page.close({ runBeforeUnload: false }),
48365
+ 2e3,
48366
+ "forceRecover:pageClose"
48367
+ ).catch(() => {
48368
+ });
48369
+ } finally {
48370
+ this.forcedTeardownInProgress = false;
48371
+ }
48372
+ }
48373
+ this._pendingCrashUrl = this.lastKnownUrl;
48374
+ this._pendingCrashType = "page_crash";
48375
+ await this.restoreBrowser(reason);
48376
+ const recovered = !!this.page && !this.page.isClosed();
48377
+ this.logger.info("[DirectPlaywright] forceRecoverPage completed", {
48378
+ reason,
48379
+ recovered,
48380
+ durationMs: Date.now() - startedAt,
48381
+ restoredUrl: this.lastKnownUrl
48382
+ });
48383
+ return recovered;
48384
+ } catch (err) {
48385
+ this.logger.warn("[DirectPlaywright] forceRecoverPage failed", {
48386
+ reason,
48387
+ error: err instanceof Error ? err.message : String(err)
48388
+ });
48389
+ return false;
48390
+ }
48391
+ }
47668
48392
  /**
47669
48393
  * Set the span factory for sub-operation tracing after construction.
47670
48394
  * Called by the API layer to propagate Sentry span context.
@@ -47710,12 +48434,20 @@ var PlaywrightClient = class _PlaywrightClient {
47710
48434
  * Used to guard against hung Playwright operations (launch, connect, newContext).
47711
48435
  */
47712
48436
  async withOperationTimeout(operation, timeoutMs, label) {
47713
- return Promise.race([
47714
- operation,
47715
- new Promise(
47716
- (_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs)
47717
- )
47718
- ]);
48437
+ let timer;
48438
+ try {
48439
+ return await Promise.race([
48440
+ operation,
48441
+ new Promise((_, reject) => {
48442
+ timer = setTimeout(
48443
+ () => reject(new Error(`${label} timed out after ${timeoutMs}ms`)),
48444
+ timeoutMs
48445
+ );
48446
+ })
48447
+ ]);
48448
+ } finally {
48449
+ if (timer) clearTimeout(timer);
48450
+ }
47719
48451
  }
47720
48452
  isRecoverableClosureError(error) {
47721
48453
  const message = error instanceof Error ? error.message : String(error);
@@ -47728,6 +48460,10 @@ var PlaywrightClient = class _PlaywrightClient {
47728
48460
  contextClosedIntentionally = false;
47729
48461
  lastConnectOptions = null;
47730
48462
  pageClosedIntentionally = false;
48463
+ // Set while `forceRecoverPage` is deliberately closing the page so the
48464
+ // page-close handler doesn't log it as an unexpected crash / re-stamp crash
48465
+ // state. Distinct from `pageClosedIntentionally` (which makes getPage throw).
48466
+ forcedTeardownInProgress = false;
47731
48467
  restoreInProgress = false;
47732
48468
  restorePromise = null;
47733
48469
  // State tracking (MCP does this internally via Tab class)
@@ -47748,6 +48484,11 @@ var PlaywrightClient = class _PlaywrightClient {
47748
48484
  // Last snapshot for reference (used by some methods)
47749
48485
  lastSnapshotYaml = "";
47750
48486
  lastSnapshotTimestamp = 0;
48487
+ // Live-snapshot (warm/minted) state. Gated by `liveSnapshotEnabled`; when off,
48488
+ // snapshot() behaves exactly as before. See live-snapshot.ts.
48489
+ liveSnapshotEnabled = false;
48490
+ liveSnapshotRawOnMiss = false;
48491
+ liveSnapshot;
47751
48492
  // Last resolved element metadata (for recording layer to avoid snapshot re-parsing)
47752
48493
  lastResolvedElementMetadata = /* @__PURE__ */ new Map();
47753
48494
  // Tracing state
@@ -48554,7 +49295,7 @@ var PlaywrightClient = class _PlaywrightClient {
48554
49295
  this.highlightStylesInjected = false;
48555
49296
  this.cursorOverlay.resetInjectionState();
48556
49297
  const [snapshotYaml, title] = await Promise.all([
48557
- this.captureSnapshotInternal(opts?.signal),
49298
+ this.captureTreeYaml(opts?.signal, { force: true }),
48558
49299
  this.getPage().then((p2) => p2.title())
48559
49300
  ]);
48560
49301
  return this.formatMCPResponse(
@@ -48576,7 +49317,7 @@ var PlaywrightClient = class _PlaywrightClient {
48576
49317
  this.highlightStylesInjected = false;
48577
49318
  this.cursorOverlay.resetInjectionState();
48578
49319
  const [snapshotYaml, title] = await Promise.all([
48579
- this.captureSnapshotInternal(opts?.signal),
49320
+ this.captureTreeYaml(opts?.signal, { force: true }),
48580
49321
  this.getPage().then((p2) => p2.title())
48581
49322
  ]);
48582
49323
  return this.formatMCPResponse("Navigated back", snapshotYaml, "await page.goBack();", title);
@@ -48593,14 +49334,21 @@ var PlaywrightClient = class _PlaywrightClient {
48593
49334
  } else if (opts?.includeScreenshot) {
48594
49335
  effectiveMode = "both";
48595
49336
  }
48596
- await this.traced("stabilize", "DOM stabilize", () => this.stabilize("dom-only"));
49337
+ await this.traced(
49338
+ "stabilize",
49339
+ "DOM stabilize",
49340
+ () => this.stabilize("dom-only", opts?.signal)
49341
+ );
48597
49342
  if (effectiveMode === "screenshot") {
48598
49343
  try {
48599
49344
  const title2 = await page.title();
48600
49345
  const buffer = await page.screenshot({
48601
49346
  type: "jpeg",
48602
49347
  quality: 90,
48603
- fullPage: false
49348
+ fullPage: false,
49349
+ // Hard cap: a screenshot blocks on a composited frame, which a jammed
49350
+ // renderer can't produce — without a timeout it orphans for minutes.
49351
+ timeout: TIMEOUTS.SCREENSHOT
48604
49352
  });
48605
49353
  const base64 = buffer.toString("base64");
48606
49354
  const downsampledBase642 = await downsampleImage(base64, opts?.maxImageDimension);
@@ -48622,26 +49370,41 @@ Use coordinate-based clicking (x, y) to interact with elements visible in the sc
48622
49370
  throw new Error("Failed to capture screenshot in screenshot-only mode");
48623
49371
  }
48624
49372
  }
48625
- const captureScreenshot = effectiveMode === "both" ? this.traced("screenshot", "capture + downsample screenshot", async () => {
48626
- const buffer = await page.screenshot({
48627
- type: "jpeg",
48628
- quality: 90,
48629
- fullPage: false
48630
- });
48631
- return downsampleImage(buffer.toString("base64"), opts?.maxImageDimension);
48632
- }).catch((err) => {
49373
+ const captureScreenshot = effectiveMode === "both" ? this.traced(
49374
+ "screenshot",
49375
+ "capture + downsample screenshot",
49376
+ () => (
49377
+ // Raced against the tool signal AND given a hard Playwright timeout:
49378
+ // a viewport screenshot still waits for a composited frame, which a
49379
+ // jammed renderer can't hand back, so without both bounds it orphans
49380
+ // for minutes behind the wedge.
49381
+ this.raceAgainstSignal("snapshot:screenshot", opts?.signal, async () => {
49382
+ const buffer = await page.screenshot({
49383
+ type: "jpeg",
49384
+ quality: 90,
49385
+ fullPage: false,
49386
+ timeout: TIMEOUTS.SCREENSHOT
49387
+ });
49388
+ return downsampleImage(buffer.toString("base64"), opts?.maxImageDimension);
49389
+ })
49390
+ )
49391
+ ).catch((err) => {
48633
49392
  this.logger.warn("[DirectPlaywright] Failed to capture screenshot for snapshot", {
48634
49393
  error: err instanceof Error ? err.message : String(err)
48635
49394
  });
48636
49395
  return null;
48637
49396
  }) : Promise.resolve(null);
48638
49397
  const [rawSnapshotYaml, title, downsampledBase64] = await Promise.all([
48639
- this.traced(
48640
- "snapshot_for_ai",
48641
- "capture accessibility tree",
48642
- () => this.captureSnapshotInternal(opts?.signal)
48643
- ),
48644
- this.getPage().then((p2) => p2.title()),
49398
+ this.captureTreeYaml(opts?.signal),
49399
+ this.raceAgainstSignal(
49400
+ "snapshot:title",
49401
+ opts?.signal,
49402
+ () => this.withOperationTimeout(
49403
+ this.getPage().then((p2) => p2.title()),
49404
+ TIMEOUTS.GET_TITLE,
49405
+ "snapshot:title"
49406
+ )
49407
+ ).catch(() => ""),
48645
49408
  captureScreenshot
48646
49409
  ]);
48647
49410
  const snapshotYaml = this.filterSnapshot(
@@ -48874,15 +49637,7 @@ ${warning}` : ""}`,
48874
49637
  // tracking pixels
48875
49638
  ];
48876
49639
  async waitForNetworkIdle(timeoutMs = TIMEOUTS.STABILIZE_INTERACTION) {
48877
- const page = this.page;
48878
- if (!page || page.isClosed()) return;
48879
- try {
48880
- await page.waitForLoadState("networkidle", { timeout: timeoutMs });
48881
- } catch {
48882
- this.logger.debug("[DirectPlaywright] waitForNetworkIdle timed out, proceeding", {
48883
- timeoutMs
48884
- });
48885
- }
49640
+ await this.waitForHttpSettle(timeoutMs);
48886
49641
  }
48887
49642
  getPendingNetworkInfo() {
48888
49643
  const pendingRequests = [];
@@ -48903,6 +49658,40 @@ ${warning}` : ""}`,
48903
49658
  });
48904
49659
  return { pendingCount: pendingRequests.length, sampleUrls };
48905
49660
  }
49661
+ /**
49662
+ * Stream-aware HTTP-settle wait (replaces Playwright `networkidle` in
49663
+ * stabilization). Resolves when no non-stream HTTP request is awaiting
49664
+ * response headers, or after `maxWaitMs`. SSE/WebSocket connections never
49665
+ * block it — their response headers land immediately so they're non-pending,
49666
+ * and `eventsource`/`websocket` resource types are excluded outright. A
49667
+ * request stuck pending past `HTTP_SETTLE_MAX_AGE` is aged out. Best-effort:
49668
+ * the cap always wins.
49669
+ */
49670
+ async waitForHttpSettle(maxWaitMs) {
49671
+ const page = this.page;
49672
+ if (!page || page.isClosed()) return;
49673
+ const deadline = Date.now() + maxWaitMs;
49674
+ const POLL_MS = 50;
49675
+ while (this.countUnsettledRequests() > 0) {
49676
+ if (Date.now() >= deadline) {
49677
+ const pending = this.getPendingNetworkInfo();
49678
+ this.logger.debug("[DirectPlaywright] HTTP-settle deadline reached, proceeding", {
49679
+ maxWaitMs,
49680
+ pendingCount: pending.pendingCount,
49681
+ sampleUrls: pending.sampleUrls
49682
+ });
49683
+ return;
49684
+ }
49685
+ await new Promise((r2) => setTimeout(r2, POLL_MS));
49686
+ }
49687
+ }
49688
+ /** Pending non-stream HTTP requests younger than the age guard. 0 ⇒ settled. */
49689
+ countUnsettledRequests() {
49690
+ return countUnsettledRequests(this._capturedNetworkRequests.values(), Date.now(), {
49691
+ noisePatterns: _PlaywrightClient.NOISE_URL_PATTERNS,
49692
+ maxAgeMs: TIMEOUTS.HTTP_SETTLE_MAX_AGE
49693
+ });
49694
+ }
48906
49695
  async networkRequests(_opts) {
48907
49696
  if (this._capturedNetworkRequests.size === 0) {
48908
49697
  return this.formatMCPResponse("No network requests captured");
@@ -50127,17 +50916,30 @@ ${warning}` : ""}`,
50127
50916
  * - Link URLs, heading levels, cursor styles
50128
50917
  * - Same role vocabulary (generic, paragraph, etc.)
50129
50918
  */
50919
+ /**
50920
+ * Full capture = raw substrate (cheap) + augmentation (expensive). Legacy
50921
+ * path (flag off) and any internal caller that wants the complete substrate.
50922
+ * Returns the previous YAML on abort (unchanged behavior).
50923
+ */
50130
50924
  async captureSnapshotInternal(signal) {
50131
50925
  await this.traceGroup("Canary Internal: Capture Snapshot");
50132
50926
  try {
50133
- return await this._captureSnapshotInternalBody(signal);
50927
+ const previousYaml = this.lastSnapshotYaml;
50928
+ const rawYaml = await this.captureRawSubstrate(signal);
50929
+ if (signal?.aborted || !rawYaml) return previousYaml;
50930
+ return await this.augmentSubstrate(rawYaml, signal);
50134
50931
  } finally {
50135
50932
  await this.traceGroupEnd();
50136
50933
  }
50137
50934
  }
50138
- async _captureSnapshotInternalBody(signal) {
50935
+ /**
50936
+ * CHEAP tier: read toasts, run the pre-snapshot DOM enrichers, and take the
50937
+ * `ariaSnapshot`. Returns the raw (un-augmented) substrate YAML — the
50938
+ * ground-truth representation used for freshness comparison — or '' if the
50939
+ * capture was aborted mid-way.
50940
+ */
50941
+ async captureRawSubstrate(signal) {
50139
50942
  const page = await this.getPage();
50140
- const previousYaml = this.lastSnapshotYaml;
50141
50943
  let capturedToasts = [];
50142
50944
  try {
50143
50945
  capturedToasts = await this.raceAgainstSignal(
@@ -50152,7 +50954,7 @@ ${warning}` : ""}`,
50152
50954
  );
50153
50955
  } catch {
50154
50956
  }
50155
- if (signal?.aborted) return previousYaml;
50957
+ if (signal?.aborted) return "";
50156
50958
  if (capturedToasts.length > 0) {
50157
50959
  for (const t2 of capturedToasts) {
50158
50960
  t2.snapshotsRemaining = TOAST_SNAPSHOT_TTL;
@@ -50161,14 +50963,14 @@ ${warning}` : ""}`,
50161
50963
  for (const t2 of capturedToasts) {
50162
50964
  existingByText.set(t2.text, t2);
50163
50965
  }
50164
- if (signal?.aborted) return previousYaml;
50966
+ if (signal?.aborted) return "";
50165
50967
  this._lastCapturedToasts = Array.from(existingByText.values());
50166
50968
  this.logger.debug("[DirectPlaywright] Captured ephemeral toasts", {
50167
50969
  count: capturedToasts.length,
50168
50970
  texts: capturedToasts.map((t2) => t2.text)
50169
50971
  });
50170
50972
  }
50171
- if (signal?.aborted) return previousYaml;
50973
+ if (signal?.aborted) return "";
50172
50974
  await this.traced(
50173
50975
  "enrichment",
50174
50976
  "DOM enrichment (SVG, hidden, modals)",
@@ -50182,43 +50984,56 @@ ${warning}` : ""}`,
50182
50984
  ]).then(() => void 0)
50183
50985
  )
50184
50986
  );
50185
- if (signal?.aborted) return previousYaml;
50186
- let snapshot;
50187
- const maxRetries = 3;
50188
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
50189
- if (signal?.aborted) return previousYaml;
50190
- try {
50191
- snapshot = await this.raceAgainstSignal(
50192
- "snapshot:aria",
50193
- signal,
50194
- () => page.ariaSnapshot({ mode: "ai" })
50195
- );
50196
- break;
50197
- } catch (err) {
50198
- if (signal?.aborted) return previousYaml;
50199
- if (attempt < maxRetries) {
50200
- this.logger.debug("[DirectPlaywright] Snapshot attempt failed, retrying", {
50201
- attempt,
50202
- maxRetries,
50203
- error: err instanceof Error ? err.message : String(err)
50204
- });
50205
- await this.raceAgainstSignal(
50206
- "snapshot:retry-backoff",
50987
+ if (signal?.aborted) return "";
50988
+ const snapshot = await this.traced("snapshot_for_ai", "capture accessibility tree", async () => {
50989
+ let result;
50990
+ const maxRetries = 3;
50991
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
50992
+ if (signal?.aborted) return void 0;
50993
+ try {
50994
+ result = await this.raceAgainstSignal(
50995
+ "snapshot:aria",
50207
50996
  signal,
50208
- () => new Promise((resolve) => setTimeout(resolve, 200 * attempt))
50997
+ () => page.ariaSnapshot({ mode: "ai" })
50209
50998
  );
50210
- } else {
50211
- this.logger.warn(
50212
- "[DirectPlaywright] All snapshot attempts failed, using empty fallback",
50213
- {
50214
- attempts: maxRetries,
50999
+ break;
51000
+ } catch (err) {
51001
+ if (signal?.aborted) return void 0;
51002
+ if (attempt < maxRetries) {
51003
+ this.logger.debug("[DirectPlaywright] Snapshot attempt failed, retrying", {
51004
+ attempt,
51005
+ maxRetries,
50215
51006
  error: err instanceof Error ? err.message : String(err)
50216
- }
50217
- );
51007
+ });
51008
+ await this.raceAgainstSignal(
51009
+ "snapshot:retry-backoff",
51010
+ signal,
51011
+ () => new Promise((resolve) => setTimeout(resolve, 200 * attempt))
51012
+ );
51013
+ } else {
51014
+ this.logger.warn(
51015
+ "[DirectPlaywright] All snapshot attempts failed, using empty fallback",
51016
+ {
51017
+ attempts: maxRetries,
51018
+ error: err instanceof Error ? err.message : String(err)
51019
+ }
51020
+ );
51021
+ }
50218
51022
  }
50219
51023
  }
50220
- }
50221
- const rawYaml = snapshot || "- page: (empty)";
51024
+ return result;
51025
+ });
51026
+ if (signal?.aborted) return "";
51027
+ return snapshot || "- page: (empty)";
51028
+ }
51029
+ /**
51030
+ * EXPENSIVE tier: augment a raw substrate into the full substrate (per-ref
51031
+ * DOM-derived hints, v2 attrs, frame selectors). Publishes `lastSnapshotYaml`.
51032
+ * Returns the previous YAML on abort.
51033
+ */
51034
+ async augmentSubstrate(rawYaml, signal) {
51035
+ const page = await this.getPage();
51036
+ const previousYaml = this.lastSnapshotYaml;
50222
51037
  if (signal?.aborted) return previousYaml;
50223
51038
  const augmentedYaml = await this.traced("augmentation", "YAML augmentation (labels, grids, forms, selects)", () => this.raceAgainstSignal("snapshot:augmentation", signal, async () => {
50224
51039
  const elements = parseSnapshot(rawYaml);
@@ -50384,6 +51199,27 @@ ${warning}` : ""}`,
50384
51199
  if (Date.now() - this.lastSnapshotTimestamp > maxAgeMs) return null;
50385
51200
  return this.lastSnapshotYaml;
50386
51201
  }
51202
+ /**
51203
+ * Return the warm (augmented) snapshot YAML if its raw substrate still matches
51204
+ * the live page, else null — verified with a cheap raw capture, never
51205
+ * augmenting. Used by the executor to reuse the warm snapshot as a diff
51206
+ * baseline. Returns null when live-snapshot is disabled (caller falls back to
51207
+ * a fresh capture).
51208
+ */
51209
+ async getReusableSnapshotYaml() {
51210
+ if (!this.liveSnapshotEnabled) return null;
51211
+ const reusable = await this.liveSnapshot.getReusable();
51212
+ return reusable ? mintedYaml(reusable) : null;
51213
+ }
51214
+ /**
51215
+ * Fire-and-forget background *preparation* of the next snapshot. Waits for the
51216
+ * DOM to settle, then re-captures and re-augments on change so the next
51217
+ * request hits the reuse path. No-op when live-snapshot is disabled.
51218
+ */
51219
+ warmSnapshot(opts) {
51220
+ if (!this.liveSnapshotEnabled) return;
51221
+ this.liveSnapshot.warm({ ...opts, rawOnMiss: this.liveSnapshotRawOnMiss });
51222
+ }
50387
51223
  // ================ CDP SCREENCAST ================
50388
51224
  async startScreencast(onFrame, options) {
50389
51225
  if (!this.page) {
@@ -50499,22 +51335,51 @@ ${warning}` : ""}`,
50499
51335
  * - 'interaction': Brief DOM delay + network idle (after click/submit)
50500
51336
  * - 'dom-only': Just DOM/animation settling (before snapshot, after hover)
50501
51337
  */
50502
- async stabilize(type) {
51338
+ async stabilize(type, signal) {
50503
51339
  const page = this.page;
50504
51340
  if (!page || page.isClosed()) return;
50505
51341
  try {
50506
51342
  switch (type) {
50507
51343
  case "navigation":
50508
- await page.waitForLoadState("networkidle", { timeout: TIMEOUTS.STABILIZE_NAV }).catch(() => {
51344
+ await this.raceAgainstSignal(
51345
+ "stabilize:dom",
51346
+ signal,
51347
+ () => this.waitDomQuiet(this.stabilizeDomMs, TIMEOUTS.STABILIZE_NAV)
51348
+ ).catch(() => {
51349
+ });
51350
+ await this.raceAgainstSignal(
51351
+ "stabilize:http",
51352
+ signal,
51353
+ () => this.waitForHttpSettle(TIMEOUTS.STABILIZE_NAV)
51354
+ ).catch(() => {
50509
51355
  });
50510
51356
  break;
50511
51357
  case "interaction":
50512
- await page.waitForTimeout(TIMEOUTS.STABILIZE_DOM);
50513
- await page.waitForLoadState("networkidle", { timeout: TIMEOUTS.STABILIZE_INTERACTION }).catch(() => {
51358
+ await this.raceAgainstSignal(
51359
+ "stabilize:dom-delay",
51360
+ signal,
51361
+ () => page.waitForTimeout(this.stabilizeDomMs)
51362
+ ).catch(() => {
51363
+ });
51364
+ await this.raceAgainstSignal(
51365
+ "stabilize:http",
51366
+ signal,
51367
+ () => this.waitForHttpSettle(TIMEOUTS.STABILIZE_INTERACTION)
51368
+ ).catch(() => {
50514
51369
  });
50515
51370
  break;
50516
51371
  case "dom-only":
50517
- await page.waitForTimeout(TIMEOUTS.STABILIZE_DOM);
51372
+ if (this.liveSnapshotEnabled) {
51373
+ await this.raceAgainstSignal(
51374
+ "stabilize:dom",
51375
+ signal,
51376
+ () => this.waitDomQuiet(this.stabilizeDomMs, TIMEOUTS.STABILIZE_INTERACTION)
51377
+ ).catch(() => {
51378
+ });
51379
+ } else {
51380
+ await page.waitForTimeout(this.stabilizeDomMs).catch(() => {
51381
+ });
51382
+ }
50518
51383
  break;
50519
51384
  }
50520
51385
  } catch (err) {
@@ -50740,7 +51605,7 @@ Legacy two-step (only if you need to change the action between calls): browser_h
50740
51605
  });
50741
51606
  });
50742
51607
  page.on("close", () => {
50743
- if (this.pageClosedIntentionally || this.contextClosedIntentionally || this.disconnectInProgress) {
51608
+ if (this.pageClosedIntentionally || this.contextClosedIntentionally || this.disconnectInProgress || this.forcedTeardownInProgress) {
50744
51609
  this.logger.debug("[DirectPlaywright] Page closed intentionally");
50745
51610
  return;
50746
51611
  }
@@ -50783,7 +51648,9 @@ Legacy two-step (only if you need to change the action between calls): browser_h
50783
51648
  method: request2.method(),
50784
51649
  url: request2.url(),
50785
51650
  status: "pending",
50786
- statusText: ""
51651
+ statusText: "",
51652
+ resourceType: request2.resourceType(),
51653
+ startedAt: Date.now()
50787
51654
  });
50788
51655
  });
50789
51656
  page.on("response", (response) => {
@@ -50929,6 +51796,7 @@ Legacy two-step (only if you need to change the action between calls): browser_h
50929
51796
  this._pendingDialogs = [];
50930
51797
  this.setPendingArm(null, "createPageWithListeners");
50931
51798
  this.lastSnapshotYaml = "";
51799
+ this.liveSnapshot.reset();
50932
51800
  this.highlightStylesInjected = false;
50933
51801
  this.cursorOverlay.resetInjectionState();
50934
51802
  this.setupPageEventListeners(page);
@@ -50949,6 +51817,11 @@ Legacy two-step (only if you need to change the action between calls): browser_h
50949
51817
  error: err instanceof Error ? err.message : String(err)
50950
51818
  });
50951
51819
  });
51820
+ void context.addInitScript(DOM_REVISION_SCRIPT).catch((err) => {
51821
+ this.logger.warn("[DirectPlaywright] Failed to install dom-revision init script", {
51822
+ error: err instanceof Error ? err.message : String(err)
51823
+ });
51824
+ });
50952
51825
  void context.grantPermissions(["microphone", "camera"]).catch((err) => {
50953
51826
  this.logger.warn("[DirectPlaywright] Failed to grant media permissions", {
50954
51827
  error: err instanceof Error ? err.message : String(err)
@@ -51977,4 +52850,4 @@ playwright-extra/dist/index.esm.js:
51977
52850
  * @license MIT
51978
52851
  *)
51979
52852
  */
51980
- //# sourceMappingURL=chunk-4PNOV7OV.js.map
52853
+ //# sourceMappingURL=chunk-QQVWGWPI.js.map