@qontinui/ui-bridge 0.2.0 → 0.3.1

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 (140) hide show
  1. package/dist/ai/index.d.mts +312 -155
  2. package/dist/ai/index.d.ts +312 -155
  3. package/dist/ai/index.js +2363 -67
  4. package/dist/ai/index.js.map +1 -1
  5. package/dist/ai/index.mjs +2328 -68
  6. package/dist/ai/index.mjs.map +1 -1
  7. package/dist/annotations/index.d.mts +218 -0
  8. package/dist/annotations/index.d.ts +218 -0
  9. package/dist/annotations/index.js +246 -0
  10. package/dist/annotations/index.js.map +1 -0
  11. package/dist/annotations/index.mjs +241 -0
  12. package/dist/annotations/index.mjs.map +1 -0
  13. package/dist/assertions-BSR3afVr.d.ts +161 -0
  14. package/dist/assertions-CTw1hfOx.d.mts +161 -0
  15. package/dist/babel-plugin/index.js +504 -0
  16. package/dist/babel-plugin/index.js.map +1 -0
  17. package/dist/babel-plugin/index.mjs +488 -0
  18. package/dist/babel-plugin/index.mjs.map +1 -0
  19. package/dist/browser-capture-Bms60T6f.d.mts +47 -0
  20. package/dist/browser-capture-CsTU29mb.d.ts +47 -0
  21. package/dist/control/index.d.mts +26 -7
  22. package/dist/control/index.d.ts +26 -7
  23. package/dist/control/index.js +276 -48
  24. package/dist/control/index.js.map +1 -1
  25. package/dist/control/index.mjs +276 -48
  26. package/dist/control/index.mjs.map +1 -1
  27. package/dist/core/index.d.mts +115 -44
  28. package/dist/core/index.d.ts +115 -44
  29. package/dist/core/index.js +0 -1560
  30. package/dist/core/index.js.map +1 -1
  31. package/dist/core/index.mjs +1 -1549
  32. package/dist/core/index.mjs.map +1 -1
  33. package/dist/debug/index.d.mts +5 -3
  34. package/dist/debug/index.d.ts +5 -3
  35. package/dist/debug/index.js +925 -1
  36. package/dist/debug/index.js.map +1 -1
  37. package/dist/debug/index.mjs +924 -2
  38. package/dist/debug/index.mjs.map +1 -1
  39. package/dist/index.d.mts +13 -9
  40. package/dist/index.d.ts +13 -9
  41. package/dist/index.js +8310 -3777
  42. package/dist/index.js.map +1 -1
  43. package/dist/index.mjs +8246 -3766
  44. package/dist/index.mjs.map +1 -1
  45. package/dist/{metrics-NC3csD0R.d.mts → metrics-DuA2qIIz.d.mts} +2 -2
  46. package/dist/{metrics-C9XRi_mL.d.ts → metrics-KFAAKNEB.d.ts} +2 -2
  47. package/dist/native/control/index.js +448 -0
  48. package/dist/native/control/index.js.map +1 -0
  49. package/dist/native/control/index.mjs +445 -0
  50. package/dist/native/control/index.mjs.map +1 -0
  51. package/dist/native/core/index.js +486 -0
  52. package/dist/native/core/index.js.map +1 -0
  53. package/dist/native/core/index.mjs +475 -0
  54. package/dist/native/core/index.mjs.map +1 -0
  55. package/dist/native/debug/index.js +408 -0
  56. package/dist/native/debug/index.js.map +1 -0
  57. package/dist/native/debug/index.mjs +406 -0
  58. package/dist/native/debug/index.mjs.map +1 -0
  59. package/dist/native/index.js +2232 -0
  60. package/dist/native/index.js.map +1 -0
  61. package/dist/native/index.mjs +2204 -0
  62. package/dist/native/index.mjs.map +1 -0
  63. package/dist/native/react/index.js +1377 -0
  64. package/dist/native/react/index.js.map +1 -0
  65. package/dist/native/react/index.mjs +1365 -0
  66. package/dist/native/react/index.mjs.map +1 -0
  67. package/dist/native/server/index.js +440 -0
  68. package/dist/native/server/index.js.map +1 -0
  69. package/dist/native/server/index.mjs +435 -0
  70. package/dist/native/server/index.mjs.map +1 -0
  71. package/dist/react/index.d.mts +121 -9
  72. package/dist/react/index.d.ts +121 -9
  73. package/dist/react/index.js +2239 -91
  74. package/dist/react/index.js.map +1 -1
  75. package/dist/react/index.mjs +2239 -92
  76. package/dist/react/index.mjs.map +1 -1
  77. package/dist/{registry-CIEDjbQ9.d.ts → registry-C6dDtn1v.d.ts} +34 -15
  78. package/dist/{registry-SsSDq46X.d.mts → registry-POtcxnal.d.mts} +34 -15
  79. package/dist/render-log/index.d.mts +1 -1
  80. package/dist/render-log/index.d.ts +1 -1
  81. package/dist/server/express.d.mts +37 -0
  82. package/dist/server/express.d.ts +37 -0
  83. package/dist/server/express.js +298 -0
  84. package/dist/server/express.js.map +1 -0
  85. package/dist/server/express.mjs +294 -0
  86. package/dist/server/express.mjs.map +1 -0
  87. package/dist/server/handlers.d.mts +124 -0
  88. package/dist/server/handlers.d.ts +124 -0
  89. package/dist/server/handlers.js +7183 -0
  90. package/dist/server/handlers.js.map +1 -0
  91. package/dist/server/handlers.mjs +7180 -0
  92. package/dist/server/handlers.mjs.map +1 -0
  93. package/dist/server/index.d.mts +12 -0
  94. package/dist/server/index.d.ts +12 -0
  95. package/dist/server/index.js +8384 -0
  96. package/dist/server/index.js.map +1 -0
  97. package/dist/server/index.mjs +8369 -0
  98. package/dist/server/index.mjs.map +1 -0
  99. package/dist/server/nextjs.d.mts +128 -0
  100. package/dist/server/nextjs.d.ts +128 -0
  101. package/dist/server/nextjs.js +390 -0
  102. package/dist/server/nextjs.js.map +1 -0
  103. package/dist/server/nextjs.mjs +385 -0
  104. package/dist/server/nextjs.mjs.map +1 -0
  105. package/dist/server/standalone.d.mts +7 -0
  106. package/dist/server/standalone.d.ts +7 -0
  107. package/dist/server/standalone.js +845 -0
  108. package/dist/server/standalone.js.map +1 -0
  109. package/dist/server/standalone.mjs +841 -0
  110. package/dist/server/standalone.mjs.map +1 -0
  111. package/dist/specs/index.d.mts +365 -0
  112. package/dist/specs/index.d.ts +365 -0
  113. package/dist/specs/index.js +2809 -0
  114. package/dist/specs/index.js.map +1 -0
  115. package/dist/specs/index.mjs +2786 -0
  116. package/dist/specs/index.mjs.map +1 -0
  117. package/dist/standalone-B6GLIEmR.d.ts +216 -0
  118. package/dist/standalone-CjdYqj3P.d.mts +216 -0
  119. package/dist/swc-plugin/index.d.mts +79 -0
  120. package/dist/swc-plugin/index.d.ts +79 -0
  121. package/dist/swc-plugin/index.js +15 -0
  122. package/dist/swc-plugin/index.js.map +1 -0
  123. package/dist/swc-plugin/index.mjs +9 -0
  124. package/dist/swc-plugin/index.mjs.map +1 -0
  125. package/dist/types-B2EfvEaq.d.ts +236 -0
  126. package/dist/{types-Dr6tH-bm.d.mts → types-C7gVYRnF.d.ts} +72 -2
  127. package/dist/{types-oCTrRxSw.d.ts → types-CJGrBEhC.d.mts} +72 -2
  128. package/dist/types-CebMQj76.d.ts +1275 -0
  129. package/dist/types-D_ypYl3T.d.mts +1275 -0
  130. package/dist/types-UBtp7R0u.d.mts +132 -0
  131. package/dist/types-UBtp7R0u.d.ts +132 -0
  132. package/dist/types-gO696T_t.d.mts +236 -0
  133. package/dist/{types-CPMbN_Iw.d.mts → types-suaYwWWg.d.mts} +519 -152
  134. package/dist/{types-CPMbN_Iw.d.ts → types-suaYwWWg.d.ts} +519 -152
  135. package/package.json +123 -4
  136. package/swc-plugin-wasm/ui_bridge_swc_plugin.wasm +0 -0
  137. package/dist/types-BvCfFuEV.d.ts +0 -534
  138. package/dist/types-CFT3Dnx4.d.mts +0 -534
  139. package/dist/websocket-client-CX4QJesI.d.ts +0 -124
  140. package/dist/websocket-client-C_Na0OSp.d.mts +0 -124
@@ -552,7 +552,9 @@ function generateDescription(input) {
552
552
  }
553
553
  parts.push(`"${name}"`);
554
554
  }
555
- const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [input.elementType || "element"];
555
+ const typeWords = ELEMENT_ACTION_WORDS[input.elementType || ""] || [
556
+ input.elementType || "element"
557
+ ];
556
558
  parts.push(typeWords[0]);
557
559
  if (input.inputType && input.inputType !== "text") {
558
560
  parts.push(`(${input.inputType})`);
@@ -773,12 +775,38 @@ var UIBridgeRegistry = class {
773
775
  getState: () => getElementState(element),
774
776
  getIdentifier: () => createElementIdentifier(element),
775
777
  registeredAt: Date.now(),
776
- mounted: true
778
+ mounted: true,
779
+ category: options.category ?? "interactive",
780
+ contentMetadata: options.contentMetadata
777
781
  };
778
782
  this.elements.set(actualId, registered);
779
783
  this.emit("element:registered", { id: actualId, type, label: options.label });
780
784
  return registered;
781
785
  }
786
+ /**
787
+ * Register a content (non-interactive) element
788
+ */
789
+ registerContentElement(id, element, options) {
790
+ return this.registerElement(id, element, {
791
+ type: options.contentType,
792
+ label: options.label,
793
+ actions: [],
794
+ category: "content",
795
+ contentMetadata: options.contentMetadata
796
+ });
797
+ }
798
+ /**
799
+ * Get all content (non-interactive) elements
800
+ */
801
+ getAllContentElements() {
802
+ return Array.from(this.elements.values()).filter((el) => el.category === "content");
803
+ }
804
+ /**
805
+ * Get all interactive elements
806
+ */
807
+ getAllInteractiveElements() {
808
+ return Array.from(this.elements.values()).filter((el) => el.category !== "content");
809
+ }
782
810
  /**
783
811
  * Unregister an element
784
812
  */
@@ -875,7 +903,9 @@ var UIBridgeRegistry = class {
875
903
  scores.accessibility = result.similarity;
876
904
  if (result.similarity > maxScore) {
877
905
  maxScore = result.similarity;
878
- matchReasons.push(`accessible name similarity: ${(result.similarity * 100).toFixed(0)}%`);
906
+ matchReasons.push(
907
+ `accessible name similarity: ${(result.similarity * 100).toFixed(0)}%`
908
+ );
879
909
  }
880
910
  }
881
911
  }
@@ -1035,7 +1065,9 @@ var UIBridgeRegistry = class {
1035
1065
  })) ?? [],
1036
1066
  elementIds: options.elementIds,
1037
1067
  registeredAt: Date.now(),
1038
- mounted: true
1068
+ mounted: true,
1069
+ getState: options.getState,
1070
+ getComputed: options.getComputed
1039
1071
  };
1040
1072
  this.components.set(id, registered);
1041
1073
  this.emit("component:registered", { id, name: options.name });
@@ -1066,6 +1098,20 @@ var UIBridgeRegistry = class {
1066
1098
  getAllComponents() {
1067
1099
  return Array.from(this.components.values());
1068
1100
  }
1101
+ /**
1102
+ * Get the current state and computed properties of a component
1103
+ */
1104
+ getComponentState(id) {
1105
+ const component = this.components.get(id);
1106
+ if (!component || !component.mounted) {
1107
+ return null;
1108
+ }
1109
+ return {
1110
+ state: component.getState?.() ?? {},
1111
+ computed: component.getComputed?.() ?? {},
1112
+ timestamp: Date.now()
1113
+ };
1114
+ }
1069
1115
  /**
1070
1116
  * Register a workflow
1071
1117
  */
@@ -1455,8 +1501,56 @@ var UIBridgeRegistry = class {
1455
1501
  identifier: el.getIdentifier(),
1456
1502
  state: el.getState(),
1457
1503
  actions: el.actions,
1458
- customActions: el.customActions ? Object.keys(el.customActions) : void 0
1504
+ customActions: el.customActions ? Object.keys(el.customActions) : void 0,
1505
+ category: el.category,
1506
+ contentMetadata: el.contentMetadata
1507
+ })),
1508
+ components: this.getAllComponents().map((comp) => ({
1509
+ id: comp.id,
1510
+ name: comp.name,
1511
+ description: comp.description,
1512
+ actions: comp.actions.map((a) => a.id),
1513
+ elementIds: comp.elementIds
1459
1514
  })),
1515
+ workflows: this.getAllWorkflows().map((wf) => ({
1516
+ id: wf.id,
1517
+ name: wf.name,
1518
+ description: wf.description,
1519
+ stepCount: wf.steps.length
1520
+ }))
1521
+ };
1522
+ }
1523
+ /**
1524
+ * Create a snapshot asynchronously, processing elements in batches to avoid
1525
+ * blocking the main thread. This prevents "Page Unresponsive" dialogs when
1526
+ * there are many registered elements (200-500+), since getState() and
1527
+ * getIdentifier() force layout/style recalculation for each element.
1528
+ */
1529
+ async createSnapshotAsync(batchSize = 50) {
1530
+ const allElements = this.getAllElements();
1531
+ const elementSnapshots = [];
1532
+ for (let i = 0; i < allElements.length; i += batchSize) {
1533
+ const batch = allElements.slice(i, i + batchSize);
1534
+ for (const el of batch) {
1535
+ elementSnapshots.push({
1536
+ id: el.id,
1537
+ type: el.type,
1538
+ label: el.label,
1539
+ identifier: el.getIdentifier(),
1540
+ state: el.getState(),
1541
+ actions: el.actions,
1542
+ customActions: el.customActions ? Object.keys(el.customActions) : void 0,
1543
+ category: el.category,
1544
+ contentMetadata: el.contentMetadata
1545
+ });
1546
+ }
1547
+ if (i + batchSize < allElements.length) {
1548
+ await new Promise((resolve) => setTimeout(resolve, 0));
1549
+ }
1550
+ }
1551
+ return {
1552
+ timestamp: Date.now(),
1553
+ elements: elementSnapshots,
1460
1554
  components: this.getAllComponents().map((comp) => ({
1461
1555
  id: comp.id,
1462
1556
  name: comp.name,
@@ -1981,6 +2075,10 @@ function getElementState2(element) {
1981
2075
  pointerEvents: style.pointerEvents
1982
2076
  }
1983
2077
  };
2078
+ const rawText = element.textContent?.trim();
2079
+ if (rawText) {
2080
+ state.textContent = rawText.replace(/\s+/g, " ").slice(0, 500);
2081
+ }
1984
2082
  if (element instanceof HTMLInputElement) {
1985
2083
  state.value = element.value;
1986
2084
  if (element.type === "checkbox" || element.type === "radio") {
@@ -2022,9 +2120,25 @@ function createMouseEvent(type, element, options) {
2022
2120
  clientY: rect.top + y
2023
2121
  });
2024
2122
  }
2123
+ function elementFromPointSafe(x, y) {
2124
+ if (typeof document.elementFromPoint === "function") {
2125
+ return document.elementFromPoint(x, y);
2126
+ }
2127
+ return null;
2128
+ }
2129
+ function createMouseEventAt(type, clientX, clientY) {
2130
+ return new MouseEvent(type, {
2131
+ bubbles: true,
2132
+ cancelable: true,
2133
+ button: 0,
2134
+ clientX,
2135
+ clientY
2136
+ });
2137
+ }
2025
2138
  var DefaultActionExecutor = class {
2026
- constructor(registry) {
2139
+ constructor(registry, consoleCapture) {
2027
2140
  this.registry = registry;
2141
+ this.consoleCapture = consoleCapture;
2028
2142
  }
2029
2143
  /**
2030
2144
  * Execute an action on an element
@@ -2061,11 +2175,19 @@ var DefaultActionExecutor = class {
2061
2175
  };
2062
2176
  }
2063
2177
  }
2178
+ const actionStartTime = Date.now();
2064
2179
  const result = await this.performAction(element, request.action, request.params);
2180
+ let consoleErrors;
2181
+ if (this.consoleCapture) {
2182
+ await sleep(50);
2183
+ const errors = this.consoleCapture.getConsoleSince(actionStartTime);
2184
+ if (errors.length > 0) consoleErrors = errors;
2185
+ }
2065
2186
  return {
2066
2187
  success: true,
2067
2188
  elementState: getElementState2(element),
2068
2189
  result,
2190
+ consoleErrors,
2069
2191
  durationMs: performance.now() - startTime,
2070
2192
  timestamp: Date.now(),
2071
2193
  requestId: request.requestId,
@@ -2157,47 +2279,76 @@ var DefaultActionExecutor = class {
2157
2279
  const rootEl = document.querySelector(options.root);
2158
2280
  if (rootEl) root = rootEl;
2159
2281
  }
2160
- const interactiveSelectors = [
2161
- "a[href]",
2162
- "button",
2163
- "input",
2164
- "select",
2165
- "textarea",
2166
- "[onclick]",
2167
- '[role="button"]',
2168
- '[role="link"]',
2169
- '[role="checkbox"]',
2170
- '[role="radio"]',
2171
- '[role="menuitem"]',
2172
- '[role="tab"]',
2173
- '[role="switch"]',
2174
- '[tabindex]:not([tabindex="-1"])',
2175
- '[contenteditable="true"]',
2176
- "[data-ui-id]",
2177
- "[data-testid]"
2178
- ];
2179
- const selector = options?.selector || interactiveSelectors.join(", ");
2180
- const foundElements = root.querySelectorAll(selector);
2181
- for (const el of foundElements) {
2182
- if (options?.limit && elements.length >= options.limit) break;
2183
- const state = getElementState2(el);
2184
- if (!options?.includeHidden && !state.visible) continue;
2185
- if (options?.types) {
2186
- const type = this.inferElementType(el);
2187
- if (!options.types.includes(type)) continue;
2188
- }
2189
- const registered = this.registry.findByDOMElement(el);
2190
- elements.push({
2191
- id: registered?.id || this.getElementId(el),
2192
- type: registered?.type || this.inferElementType(el),
2193
- label: registered?.label || this.getElementLabel(el),
2194
- tagName: el.tagName.toLowerCase(),
2195
- role: el.getAttribute("role") || void 0,
2196
- accessibleName: this.getAccessibleName(el),
2197
- actions: registered?.actions || this.inferActions(el),
2198
- state,
2199
- registered: !!registered
2200
- });
2282
+ if (!options?.contentOnly) {
2283
+ const interactiveSelectors = [
2284
+ "a[href]",
2285
+ "button",
2286
+ "input",
2287
+ "select",
2288
+ "textarea",
2289
+ "[onclick]",
2290
+ '[role="button"]',
2291
+ '[role="link"]',
2292
+ '[role="checkbox"]',
2293
+ '[role="radio"]',
2294
+ '[role="menuitem"]',
2295
+ '[role="tab"]',
2296
+ '[role="switch"]',
2297
+ '[tabindex]:not([tabindex="-1"])',
2298
+ '[contenteditable="true"]',
2299
+ "[data-ui-element]",
2300
+ "[data-ui-id]",
2301
+ "[data-testid]"
2302
+ ];
2303
+ const selector = options?.selector || interactiveSelectors.join(", ");
2304
+ const foundElements = root.querySelectorAll(selector);
2305
+ for (const el of foundElements) {
2306
+ if (options?.limit && elements.length >= options.limit) break;
2307
+ const state = getElementState2(el);
2308
+ if (!options?.includeHidden && !state.visible) continue;
2309
+ if (options?.types) {
2310
+ const type = this.inferElementType(el);
2311
+ if (!options.types.includes(type)) continue;
2312
+ }
2313
+ const registered = this.registry.findByDOMElement(el);
2314
+ elements.push({
2315
+ id: registered?.id || this.getElementId(el),
2316
+ type: registered?.type || this.inferElementType(el),
2317
+ label: registered?.label || this.getElementLabel(el),
2318
+ tagName: el.tagName.toLowerCase(),
2319
+ role: el.getAttribute("role") || void 0,
2320
+ accessibleName: this.getAccessibleName(el),
2321
+ actions: registered?.actions || this.inferActions(el),
2322
+ state,
2323
+ registered: !!registered,
2324
+ category: registered?.category || "interactive",
2325
+ contentMetadata: registered?.contentMetadata
2326
+ });
2327
+ }
2328
+ }
2329
+ if (options?.includeContent || options?.contentOnly) {
2330
+ const contentElements = this.registry.getAllContentElements();
2331
+ for (const el of contentElements) {
2332
+ if (options?.limit && elements.length >= options.limit) break;
2333
+ const state = el.getState();
2334
+ if (!options?.includeHidden && !state.visible) continue;
2335
+ if (options?.contentRole && el.contentMetadata?.contentRole !== options.contentRole) {
2336
+ continue;
2337
+ }
2338
+ elements.push({
2339
+ id: el.id,
2340
+ type: el.type,
2341
+ label: el.label,
2342
+ tagName: el.element.tagName.toLowerCase(),
2343
+ role: el.element.getAttribute("role") || void 0,
2344
+ accessibleName: el.label || state.textContent?.trim(),
2345
+ actions: [],
2346
+ state,
2347
+ registered: true,
2348
+ category: "content",
2349
+ contentMetadata: el.contentMetadata
2350
+ });
2351
+ }
2201
2352
  }
2202
2353
  return {
2203
2354
  elements,
@@ -2227,7 +2378,9 @@ var DefaultActionExecutor = class {
2227
2378
  type: el.type,
2228
2379
  label: el.label,
2229
2380
  actions: [...el.actions, ...el.customActions ? Object.keys(el.customActions) : []],
2230
- state: el.getState()
2381
+ state: el.getState(),
2382
+ category: el.category,
2383
+ contentMetadata: el.contentMetadata
2231
2384
  })),
2232
2385
  components: components.map((comp) => ({
2233
2386
  id: comp.id,
@@ -2311,6 +2464,14 @@ var DefaultActionExecutor = class {
2311
2464
  return this.performCheck(element, false);
2312
2465
  case "toggle":
2313
2466
  return this.performToggle(element);
2467
+ case "drag":
2468
+ return this.performDrag(element, params);
2469
+ case "setValue":
2470
+ return this.performSetValue(element, params);
2471
+ case "submit":
2472
+ return this.performSubmit(element);
2473
+ case "reset":
2474
+ return this.performReset(element);
2314
2475
  default: {
2315
2476
  const registered = this.registry.findByDOMElement(element);
2316
2477
  if (registered?.customActions?.[action]) {
@@ -2340,15 +2501,26 @@ var DefaultActionExecutor = class {
2340
2501
  if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
2341
2502
  throw new Error("Type action requires an input or textarea element");
2342
2503
  }
2504
+ const proto = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
2505
+ const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
2343
2506
  element.focus();
2344
2507
  if (options?.clear) {
2345
- element.value = "";
2508
+ if (nativeSetter) {
2509
+ nativeSetter.call(element, "");
2510
+ } else {
2511
+ element.value = "";
2512
+ }
2346
2513
  element.dispatchEvent(new Event("input", { bubbles: true }));
2347
2514
  }
2348
2515
  const text = options?.text || "";
2349
2516
  const delay = options?.delay || 0;
2350
2517
  for (const char of text) {
2351
- element.value += char;
2518
+ const current = element.value;
2519
+ if (nativeSetter) {
2520
+ nativeSetter.call(element, current + char);
2521
+ } else {
2522
+ element.value = current + char;
2523
+ }
2352
2524
  if (options?.triggerEvents !== false) {
2353
2525
  element.dispatchEvent(new Event("input", { bubbles: true }));
2354
2526
  }
@@ -2362,7 +2534,13 @@ var DefaultActionExecutor = class {
2362
2534
  }
2363
2535
  performClear(element) {
2364
2536
  if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
2365
- element.value = "";
2537
+ const proto = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
2538
+ const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
2539
+ if (nativeSetter) {
2540
+ nativeSetter.call(element, "");
2541
+ } else {
2542
+ element.value = "";
2543
+ }
2366
2544
  element.dispatchEvent(new Event("input", { bubbles: true }));
2367
2545
  element.dispatchEvent(new Event("change", { bubbles: true }));
2368
2546
  }
@@ -2444,6 +2622,150 @@ var DefaultActionExecutor = class {
2444
2622
  element.dispatchEvent(new Event("change", { bubbles: true }));
2445
2623
  }
2446
2624
  }
2625
+ performSetValue(element, params) {
2626
+ const value = params?.value;
2627
+ if (value === void 0) {
2628
+ throw new Error('setValue requires a "value" parameter');
2629
+ }
2630
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
2631
+ const nativeSetter = Object.getOwnPropertyDescriptor(
2632
+ element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype,
2633
+ "value"
2634
+ )?.set;
2635
+ if (nativeSetter) {
2636
+ nativeSetter.call(element, value);
2637
+ } else {
2638
+ element.value = value;
2639
+ }
2640
+ element.dispatchEvent(new Event("input", { bubbles: true }));
2641
+ element.dispatchEvent(new Event("change", { bubbles: true }));
2642
+ } else if (element instanceof HTMLSelectElement) {
2643
+ element.value = value;
2644
+ element.dispatchEvent(new Event("change", { bubbles: true }));
2645
+ }
2646
+ }
2647
+ performSubmit(element) {
2648
+ const form = element instanceof HTMLFormElement ? element : element.closest("form");
2649
+ if (form) {
2650
+ const submitEvent = new Event("submit", { bubbles: true, cancelable: true });
2651
+ if (form.dispatchEvent(submitEvent)) {
2652
+ form.requestSubmit();
2653
+ }
2654
+ } else {
2655
+ throw new Error("No form found for submit action");
2656
+ }
2657
+ }
2658
+ performReset(element) {
2659
+ const form = element instanceof HTMLFormElement ? element : element.closest("form");
2660
+ if (form) {
2661
+ form.reset();
2662
+ form.dispatchEvent(new Event("reset", { bubbles: true }));
2663
+ } else {
2664
+ throw new Error("No form found for reset action");
2665
+ }
2666
+ }
2667
+ /**
2668
+ * Perform a drag operation by dispatching a sequence of mouse events.
2669
+ *
2670
+ * Follows the same composite pattern as the qontinui core library:
2671
+ * mousedown on source → wait → mousemove × N along path → mouseup on target.
2672
+ *
2673
+ * Optionally dispatches HTML5 drag events (dragstart/dragover/drop/dragend)
2674
+ * for apps that use the HTML5 Drag and Drop API instead of mouse events.
2675
+ */
2676
+ async performDrag(sourceElement, options) {
2677
+ const sourceRect = sourceElement.getBoundingClientRect();
2678
+ const sourceX = sourceRect.left + (options?.sourceOffset?.x ?? sourceRect.width / 2);
2679
+ const sourceY = sourceRect.top + (options?.sourceOffset?.y ?? sourceRect.height / 2);
2680
+ let targetX;
2681
+ let targetY;
2682
+ if (options?.targetPosition) {
2683
+ targetX = options.targetPosition.x;
2684
+ targetY = options.targetPosition.y;
2685
+ } else if (options?.target) {
2686
+ const targetElement = this.resolveTargetElement(options.target);
2687
+ if (!targetElement) {
2688
+ throw new Error(`Drag target element not found: ${JSON.stringify(options.target)}`);
2689
+ }
2690
+ const targetRect = targetElement.getBoundingClientRect();
2691
+ targetX = targetRect.left + (options?.targetOffset?.x ?? targetRect.width / 2);
2692
+ targetY = targetRect.top + (options?.targetOffset?.y ?? targetRect.height / 2);
2693
+ } else {
2694
+ throw new Error("Drag requires either target or targetPosition");
2695
+ }
2696
+ const steps = options?.steps ?? 10;
2697
+ const holdDelay = options?.holdDelay ?? 100;
2698
+ const releaseDelay = options?.releaseDelay ?? 50;
2699
+ sourceElement.dispatchEvent(createMouseEventAt("mousedown", sourceX, sourceY));
2700
+ const canHTML5 = options?.html5 && typeof DragEvent !== "undefined";
2701
+ if (canHTML5) {
2702
+ sourceElement.dispatchEvent(
2703
+ new DragEvent("dragstart", {
2704
+ bubbles: true,
2705
+ cancelable: true,
2706
+ clientX: sourceX,
2707
+ clientY: sourceY
2708
+ })
2709
+ );
2710
+ }
2711
+ if (holdDelay > 0) {
2712
+ await sleep(holdDelay);
2713
+ }
2714
+ for (let i = 1; i <= steps; i++) {
2715
+ const progress = i / steps;
2716
+ const currentX = sourceX + (targetX - sourceX) * progress;
2717
+ const currentY = sourceY + (targetY - sourceY) * progress;
2718
+ const dispatchTarget = elementFromPointSafe(currentX, currentY) || sourceElement;
2719
+ dispatchTarget.dispatchEvent(createMouseEventAt("mousemove", currentX, currentY));
2720
+ if (canHTML5) {
2721
+ dispatchTarget.dispatchEvent(
2722
+ new DragEvent("dragover", {
2723
+ bubbles: true,
2724
+ cancelable: true,
2725
+ clientX: currentX,
2726
+ clientY: currentY
2727
+ })
2728
+ );
2729
+ }
2730
+ }
2731
+ const dropTarget = elementFromPointSafe(targetX, targetY) || sourceElement;
2732
+ dropTarget.dispatchEvent(createMouseEventAt("mouseup", targetX, targetY));
2733
+ if (canHTML5) {
2734
+ dropTarget.dispatchEvent(
2735
+ new DragEvent("drop", {
2736
+ bubbles: true,
2737
+ cancelable: true,
2738
+ clientX: targetX,
2739
+ clientY: targetY
2740
+ })
2741
+ );
2742
+ sourceElement.dispatchEvent(
2743
+ new DragEvent("dragend", {
2744
+ bubbles: true,
2745
+ cancelable: true,
2746
+ clientX: targetX,
2747
+ clientY: targetY
2748
+ })
2749
+ );
2750
+ }
2751
+ if (releaseDelay > 0) {
2752
+ await sleep(releaseDelay);
2753
+ }
2754
+ }
2755
+ /**
2756
+ * Resolve a drag target element from a target descriptor.
2757
+ */
2758
+ resolveTargetElement(target) {
2759
+ if (target.elementId) {
2760
+ const registered = this.registry.getElement(target.elementId);
2761
+ if (registered?.element) return registered.element;
2762
+ return findElementByIdentifier(target.elementId);
2763
+ }
2764
+ if (target.selector) {
2765
+ return document.querySelector(target.selector);
2766
+ }
2767
+ return null;
2768
+ }
2447
2769
  getElementId(element) {
2448
2770
  return element.getAttribute("data-ui-id") || element.getAttribute("data-testid") || element.id || `${element.tagName.toLowerCase()}-${Math.random().toString(36).substr(2, 8)}`;
2449
2771
  }
@@ -2539,8 +2861,442 @@ var DefaultActionExecutor = class {
2539
2861
  }
2540
2862
  }
2541
2863
  };
2542
- function createActionExecutor(registry) {
2543
- return new DefaultActionExecutor(registry);
2864
+ function createActionExecutor(registry, consoleCapture) {
2865
+ return new DefaultActionExecutor(registry, consoleCapture);
2866
+ }
2867
+
2868
+ // src/specs/types.ts
2869
+ var SPEC_CONFIG_VERSION = "1.0.0";
2870
+ var VALID_ASSERTION_TYPES = [
2871
+ "visible",
2872
+ "hidden",
2873
+ "enabled",
2874
+ "disabled",
2875
+ "focused",
2876
+ "checked",
2877
+ "unchecked",
2878
+ "hasText",
2879
+ "containsText",
2880
+ "hasValue",
2881
+ "hasClass",
2882
+ "exists",
2883
+ "notExists",
2884
+ "count",
2885
+ "attribute",
2886
+ "cssProperty"
2887
+ ];
2888
+ var VALID_SPEC_CATEGORIES = [
2889
+ "element-presence",
2890
+ "accessibility",
2891
+ "form-validation",
2892
+ "state-consistency",
2893
+ "modal-dialog",
2894
+ "navigation",
2895
+ "cross-page-consistency",
2896
+ "custom"
2897
+ ];
2898
+ var VALID_SPEC_SEVERITIES = [
2899
+ "critical",
2900
+ "warning",
2901
+ "info"
2902
+ ];
2903
+ var VALID_SPEC_SOURCES = [
2904
+ "auto",
2905
+ "manual",
2906
+ "ai-generated"
2907
+ ];
2908
+
2909
+ // src/specs/validator.ts
2910
+ function isValidAssertionType(value) {
2911
+ return typeof value === "string" && VALID_ASSERTION_TYPES.includes(value);
2912
+ }
2913
+ function isValidSpecCategory(value) {
2914
+ return typeof value === "string" && VALID_SPEC_CATEGORIES.includes(value);
2915
+ }
2916
+ function isValidSpecSeverity(value) {
2917
+ return typeof value === "string" && VALID_SPEC_SEVERITIES.includes(value);
2918
+ }
2919
+ function isValidSpecSource(value) {
2920
+ return typeof value === "string" && VALID_SPEC_SOURCES.includes(value);
2921
+ }
2922
+ function validateSpecAssertion(data, path = "assertion") {
2923
+ const errors = [];
2924
+ if (!data || typeof data !== "object") {
2925
+ errors.push({ path, message: "must be an object" });
2926
+ return errors;
2927
+ }
2928
+ const obj = data;
2929
+ if (typeof obj.id !== "string" || obj.id.length === 0) {
2930
+ errors.push({ path: `${path}.id`, message: "must be a non-empty string" });
2931
+ }
2932
+ if (typeof obj.description !== "string") {
2933
+ errors.push({ path: `${path}.description`, message: "must be a string" });
2934
+ }
2935
+ if (!isValidSpecCategory(obj.category)) {
2936
+ errors.push({
2937
+ path: `${path}.category`,
2938
+ message: `must be one of: ${VALID_SPEC_CATEGORIES.join(", ")}`
2939
+ });
2940
+ }
2941
+ if (!isValidSpecSeverity(obj.severity)) {
2942
+ errors.push({
2943
+ path: `${path}.severity`,
2944
+ message: `must be one of: ${VALID_SPEC_SEVERITIES.join(", ")}`
2945
+ });
2946
+ }
2947
+ if (!obj.target || typeof obj.target !== "object") {
2948
+ errors.push({ path: `${path}.target`, message: "must be an object" });
2949
+ } else {
2950
+ const target = obj.target;
2951
+ if (target.type === "elementId") {
2952
+ if (typeof target.elementId !== "string" || target.elementId.length === 0) {
2953
+ errors.push({ path: `${path}.target.elementId`, message: "must be a non-empty string" });
2954
+ }
2955
+ } else if (target.type === "search") {
2956
+ if (!target.criteria || typeof target.criteria !== "object") {
2957
+ errors.push({ path: `${path}.target.criteria`, message: "must be an object" });
2958
+ }
2959
+ } else {
2960
+ errors.push({ path: `${path}.target.type`, message: 'must be "elementId" or "search"' });
2961
+ }
2962
+ }
2963
+ if (!isValidAssertionType(obj.assertionType)) {
2964
+ errors.push({
2965
+ path: `${path}.assertionType`,
2966
+ message: `must be one of: ${VALID_ASSERTION_TYPES.join(", ")}`
2967
+ });
2968
+ }
2969
+ if (!isValidSpecSource(obj.source)) {
2970
+ errors.push({
2971
+ path: `${path}.source`,
2972
+ message: `must be one of: ${VALID_SPEC_SOURCES.join(", ")}`
2973
+ });
2974
+ }
2975
+ if (typeof obj.reviewed !== "boolean") {
2976
+ errors.push({ path: `${path}.reviewed`, message: "must be a boolean" });
2977
+ }
2978
+ if (typeof obj.enabled !== "boolean") {
2979
+ errors.push({ path: `${path}.enabled`, message: "must be a boolean" });
2980
+ }
2981
+ if (obj.timeout !== void 0 && (typeof obj.timeout !== "number" || obj.timeout < 0)) {
2982
+ errors.push({ path: `${path}.timeout`, message: "must be a non-negative number" });
2983
+ }
2984
+ return errors;
2985
+ }
2986
+ function validateSpecGroup(data, path = "group") {
2987
+ const errors = [];
2988
+ if (!data || typeof data !== "object") {
2989
+ errors.push({ path, message: "must be an object" });
2990
+ return errors;
2991
+ }
2992
+ const obj = data;
2993
+ if (typeof obj.id !== "string" || obj.id.length === 0) {
2994
+ errors.push({ path: `${path}.id`, message: "must be a non-empty string" });
2995
+ }
2996
+ if (typeof obj.name !== "string") {
2997
+ errors.push({ path: `${path}.name`, message: "must be a string" });
2998
+ }
2999
+ if (typeof obj.description !== "string") {
3000
+ errors.push({ path: `${path}.description`, message: "must be a string" });
3001
+ }
3002
+ if (!isValidSpecCategory(obj.category)) {
3003
+ errors.push({
3004
+ path: `${path}.category`,
3005
+ message: `must be one of: ${VALID_SPEC_CATEGORIES.join(", ")}`
3006
+ });
3007
+ }
3008
+ if (!isValidSpecSource(obj.source)) {
3009
+ errors.push({
3010
+ path: `${path}.source`,
3011
+ message: `must be one of: ${VALID_SPEC_SOURCES.join(", ")}`
3012
+ });
3013
+ }
3014
+ if (!Array.isArray(obj.assertions)) {
3015
+ errors.push({ path: `${path}.assertions`, message: "must be an array" });
3016
+ } else {
3017
+ for (let i = 0; i < obj.assertions.length; i++) {
3018
+ errors.push(...validateSpecAssertion(obj.assertions[i], `${path}.assertions[${i}]`));
3019
+ }
3020
+ }
3021
+ return errors;
3022
+ }
3023
+ function validateSpecConfig(data) {
3024
+ const errors = [];
3025
+ if (!data || typeof data !== "object") {
3026
+ return { valid: false, errors: [{ path: "", message: "must be an object" }] };
3027
+ }
3028
+ const obj = data;
3029
+ if (obj.version !== SPEC_CONFIG_VERSION) {
3030
+ errors.push({ path: "version", message: `must be "${SPEC_CONFIG_VERSION}"` });
3031
+ }
3032
+ if (obj.description !== void 0 && typeof obj.description !== "string") {
3033
+ errors.push({ path: "description", message: "must be a string if provided" });
3034
+ }
3035
+ if (!Array.isArray(obj.groups)) {
3036
+ errors.push({ path: "groups", message: "must be an array" });
3037
+ } else {
3038
+ for (let i = 0; i < obj.groups.length; i++) {
3039
+ errors.push(...validateSpecGroup(obj.groups[i], `groups[${i}]`));
3040
+ }
3041
+ }
3042
+ if (obj.assertions !== void 0) {
3043
+ if (!Array.isArray(obj.assertions)) {
3044
+ errors.push({ path: "assertions", message: "must be an array if provided" });
3045
+ } else {
3046
+ for (let i = 0; i < obj.assertions.length; i++) {
3047
+ errors.push(...validateSpecAssertion(obj.assertions[i], `assertions[${i}]`));
3048
+ }
3049
+ }
3050
+ }
3051
+ if (obj.metadata !== void 0 && (typeof obj.metadata !== "object" || obj.metadata === null)) {
3052
+ errors.push({ path: "metadata", message: "must be an object if provided" });
3053
+ }
3054
+ return { valid: errors.length === 0, errors };
3055
+ }
3056
+
3057
+ // src/specs/store.ts
3058
+ var SpecStore = class {
3059
+ constructor() {
3060
+ this.configs = /* @__PURE__ */ new Map();
3061
+ this.listeners = /* @__PURE__ */ new Set();
3062
+ }
3063
+ // ---------------------------------------------------------------------------
3064
+ // CRUD — Config Level
3065
+ // ---------------------------------------------------------------------------
3066
+ load(specId, config) {
3067
+ this.configs.set(specId, config);
3068
+ this.emit({ type: "spec:loaded", specId, timestamp: Date.now() });
3069
+ }
3070
+ unload(specId) {
3071
+ const existed = this.configs.delete(specId);
3072
+ if (existed) {
3073
+ this.emit({ type: "spec:unloaded", specId, timestamp: Date.now() });
3074
+ }
3075
+ return existed;
3076
+ }
3077
+ get(specId) {
3078
+ return this.configs.get(specId);
3079
+ }
3080
+ has(specId) {
3081
+ return this.configs.has(specId);
3082
+ }
3083
+ getIds() {
3084
+ return Array.from(this.configs.keys());
3085
+ }
3086
+ getAll() {
3087
+ return new Map(this.configs);
3088
+ }
3089
+ get count() {
3090
+ return this.configs.size;
3091
+ }
3092
+ clear() {
3093
+ this.configs.clear();
3094
+ this.emit({ type: "spec:cleared", timestamp: Date.now() });
3095
+ }
3096
+ // ---------------------------------------------------------------------------
3097
+ // CRUD — Group Level
3098
+ // ---------------------------------------------------------------------------
3099
+ addGroup(specId, group) {
3100
+ const config = this.configs.get(specId);
3101
+ if (!config) return false;
3102
+ config.groups.push(group);
3103
+ this.emit({ type: "spec:group-added", specId, groupId: group.id, timestamp: Date.now() });
3104
+ return true;
3105
+ }
3106
+ removeGroup(specId, groupId) {
3107
+ const config = this.configs.get(specId);
3108
+ if (!config) return false;
3109
+ const idx = config.groups.findIndex((g) => g.id === groupId);
3110
+ if (idx === -1) return false;
3111
+ config.groups.splice(idx, 1);
3112
+ this.emit({ type: "spec:group-removed", specId, groupId, timestamp: Date.now() });
3113
+ return true;
3114
+ }
3115
+ getGroup(specId, groupId) {
3116
+ const config = this.configs.get(specId);
3117
+ if (!config) return void 0;
3118
+ return config.groups.find((g) => g.id === groupId);
3119
+ }
3120
+ // ---------------------------------------------------------------------------
3121
+ // CRUD — Assertion Level
3122
+ // ---------------------------------------------------------------------------
3123
+ addAssertion(specId, groupId, assertion) {
3124
+ const config = this.configs.get(specId);
3125
+ if (!config) return false;
3126
+ if (groupId) {
3127
+ const group = config.groups.find((g) => g.id === groupId);
3128
+ if (!group) return false;
3129
+ group.assertions.push(assertion);
3130
+ } else {
3131
+ if (!config.assertions) config.assertions = [];
3132
+ config.assertions.push(assertion);
3133
+ }
3134
+ this.emit({
3135
+ type: "spec:assertion-added",
3136
+ specId,
3137
+ groupId: groupId ?? void 0,
3138
+ assertionId: assertion.id,
3139
+ timestamp: Date.now()
3140
+ });
3141
+ return true;
3142
+ }
3143
+ removeAssertion(specId, groupId, assertionId) {
3144
+ const config = this.configs.get(specId);
3145
+ if (!config) return false;
3146
+ let removed = false;
3147
+ if (groupId) {
3148
+ const group = config.groups.find((g) => g.id === groupId);
3149
+ if (group) {
3150
+ const idx = group.assertions.findIndex((a) => a.id === assertionId);
3151
+ if (idx !== -1) {
3152
+ group.assertions.splice(idx, 1);
3153
+ removed = true;
3154
+ }
3155
+ }
3156
+ } else if (config.assertions) {
3157
+ const idx = config.assertions.findIndex((a) => a.id === assertionId);
3158
+ if (idx !== -1) {
3159
+ config.assertions.splice(idx, 1);
3160
+ removed = true;
3161
+ }
3162
+ }
3163
+ if (removed) {
3164
+ this.emit({
3165
+ type: "spec:assertion-removed",
3166
+ specId,
3167
+ groupId: groupId ?? void 0,
3168
+ assertionId,
3169
+ timestamp: Date.now()
3170
+ });
3171
+ }
3172
+ return removed;
3173
+ }
3174
+ toggleAssertion(specId, groupId, assertionId) {
3175
+ const assertion = this.findAssertion(specId, groupId, assertionId);
3176
+ if (!assertion) return false;
3177
+ assertion.enabled = !assertion.enabled;
3178
+ this.emit({ type: "spec:updated", specId, timestamp: Date.now() });
3179
+ return true;
3180
+ }
3181
+ markReviewed(specId, groupId, assertionId) {
3182
+ const assertion = this.findAssertion(specId, groupId, assertionId);
3183
+ if (!assertion) return false;
3184
+ assertion.reviewed = !assertion.reviewed;
3185
+ this.emit({ type: "spec:updated", specId, timestamp: Date.now() });
3186
+ return true;
3187
+ }
3188
+ // ---------------------------------------------------------------------------
3189
+ // Queries
3190
+ // ---------------------------------------------------------------------------
3191
+ getAllAssertions() {
3192
+ const result = [];
3193
+ for (const config of this.configs.values()) {
3194
+ for (const group of config.groups) {
3195
+ result.push(...group.assertions);
3196
+ }
3197
+ if (config.assertions) {
3198
+ result.push(...config.assertions);
3199
+ }
3200
+ }
3201
+ return result;
3202
+ }
3203
+ filterAssertions(opts) {
3204
+ return this.getAllAssertions().filter((a) => {
3205
+ if (opts.categories && !opts.categories.includes(a.category)) return false;
3206
+ if (opts.severities && !opts.severities.includes(a.severity)) return false;
3207
+ if (opts.enabledOnly && !a.enabled) return false;
3208
+ if (opts.reviewedOnly && !a.reviewed) return false;
3209
+ return true;
3210
+ });
3211
+ }
3212
+ // ---------------------------------------------------------------------------
3213
+ // Coverage
3214
+ // ---------------------------------------------------------------------------
3215
+ getCoverage(allElementIds) {
3216
+ const specifiedIdSet = /* @__PURE__ */ new Set();
3217
+ for (const assertion of this.getAllAssertions()) {
3218
+ if (assertion.target.type === "elementId") {
3219
+ specifiedIdSet.add(assertion.target.elementId);
3220
+ }
3221
+ }
3222
+ const specifiedIds = [];
3223
+ const unspecifiedIds = [];
3224
+ for (const id of allElementIds) {
3225
+ if (specifiedIdSet.has(id)) {
3226
+ specifiedIds.push(id);
3227
+ } else {
3228
+ unspecifiedIds.push(id);
3229
+ }
3230
+ }
3231
+ const total = allElementIds.length;
3232
+ return {
3233
+ totalElements: total,
3234
+ specifiedElements: specifiedIds.length,
3235
+ coveragePercent: total > 0 ? specifiedIds.length / total * 100 : 0,
3236
+ specifiedIds,
3237
+ unspecifiedIds,
3238
+ timestamp: Date.now()
3239
+ };
3240
+ }
3241
+ // ---------------------------------------------------------------------------
3242
+ // Import / Export
3243
+ // ---------------------------------------------------------------------------
3244
+ importConfig(specId, config) {
3245
+ const result = validateSpecConfig(config);
3246
+ if (!result.valid) return false;
3247
+ this.configs.set(specId, config);
3248
+ this.emit({ type: "spec:loaded", specId, timestamp: Date.now() });
3249
+ return true;
3250
+ }
3251
+ exportConfig(specId) {
3252
+ const config = this.configs.get(specId);
3253
+ if (!config) return void 0;
3254
+ return {
3255
+ ...config,
3256
+ version: SPEC_CONFIG_VERSION,
3257
+ metadata: {
3258
+ ...config.metadata,
3259
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3260
+ }
3261
+ };
3262
+ }
3263
+ // ---------------------------------------------------------------------------
3264
+ // Events
3265
+ // ---------------------------------------------------------------------------
3266
+ on(listener) {
3267
+ this.listeners.add(listener);
3268
+ return () => {
3269
+ this.listeners.delete(listener);
3270
+ };
3271
+ }
3272
+ emit(event) {
3273
+ for (const listener of this.listeners) {
3274
+ try {
3275
+ listener(event);
3276
+ } catch {
3277
+ }
3278
+ }
3279
+ }
3280
+ // ---------------------------------------------------------------------------
3281
+ // Private Helpers
3282
+ // ---------------------------------------------------------------------------
3283
+ findAssertion(specId, groupId, assertionId) {
3284
+ const config = this.configs.get(specId);
3285
+ if (!config) return void 0;
3286
+ if (groupId) {
3287
+ const group = config.groups.find((g) => g.id === groupId);
3288
+ if (!group) return void 0;
3289
+ return group.assertions.find((a) => a.id === assertionId);
3290
+ }
3291
+ return config.assertions?.find((a) => a.id === assertionId);
3292
+ }
3293
+ };
3294
+ var globalStore = null;
3295
+ function getGlobalSpecStore() {
3296
+ if (!globalStore) {
3297
+ globalStore = new SpecStore();
3298
+ }
3299
+ return globalStore;
2544
3300
  }
2545
3301
 
2546
3302
  // src/control/workflow-engine.ts
@@ -3572,36 +4328,692 @@ var MetricsCollector = class {
3572
4328
  function createMetricsCollector(options) {
3573
4329
  return new MetricsCollector(options);
3574
4330
  }
3575
- var UIBridgeContext = react.createContext(null);
3576
- function UIBridgeProvider({
3577
- children,
3578
- features = {},
3579
- config = {},
3580
- onEvent
3581
- }) {
3582
- const registryRef = react.useRef(null);
3583
- const renderLogRef = react.useRef(null);
3584
- const metricsRef = react.useRef(null);
3585
- const wsClientRef = react.useRef(null);
3586
- const [wsConnectionState, setWsConnectionState] = react.useState("disconnected");
3587
- if (!registryRef.current) {
3588
- registryRef.current = new UIBridgeRegistry({
3589
- verbose: config.verbose,
3590
- onEvent
3591
- });
3592
- setGlobalRegistry(registryRef.current);
3593
- if (features.renderLog) {
3594
- renderLogRef.current = createRenderLogManager({
3595
- maxEntries: config.maxLogEntries
3596
- });
3597
- }
3598
- if (features.debug) {
3599
- metricsRef.current = createMetricsCollector();
4331
+
4332
+ // src/debug/browser-capture-types.ts
4333
+ var DEFAULT_CAPTURE_CONFIG = {
4334
+ console: true,
4335
+ network: true,
4336
+ navigation: true,
4337
+ longTasks: true,
4338
+ longAnimationFrames: true,
4339
+ resourceErrors: true,
4340
+ wsDisconnections: true,
4341
+ hmr: true,
4342
+ webVitals: false,
4343
+ memory: false,
4344
+ memoryIntervalMs: 3e4,
4345
+ maxEntries: 200
4346
+ };
4347
+
4348
+ // src/debug/captures/console.ts
4349
+ function argsToMessage(args) {
4350
+ return args.map((a) => {
4351
+ if (a instanceof Error) return a.message;
4352
+ if (typeof a === "string") return a;
4353
+ try {
4354
+ return JSON.stringify(a);
4355
+ } catch {
4356
+ return String(a);
3600
4357
  }
3601
- if (config.websocket) {
3602
- const wsPort = config.websocketPort || config.serverPort || 9876;
3603
- const wsUrl = `ws://localhost:${wsPort}`;
3604
- wsClientRef.current = createWSClient({
4358
+ }).join(" ");
4359
+ }
4360
+ function extractStack(args) {
4361
+ const err = args.find((a) => a instanceof Error);
4362
+ return err?.stack;
4363
+ }
4364
+ function makeEvent(level, message, stack) {
4365
+ return {
4366
+ type: "console",
4367
+ timestamp: Date.now(),
4368
+ url: typeof window !== "undefined" ? window.location.href : "",
4369
+ level,
4370
+ message,
4371
+ stack
4372
+ };
4373
+ }
4374
+ function installConsoleCapture(emit) {
4375
+ const originalError = console.error;
4376
+ const originalWarn = console.warn;
4377
+ console.error = (...args) => {
4378
+ emit(makeEvent("error", argsToMessage(args), extractStack(args)));
4379
+ originalError.apply(console, args);
4380
+ };
4381
+ console.warn = (...args) => {
4382
+ emit(makeEvent("warn", argsToMessage(args), extractStack(args)));
4383
+ originalWarn.apply(console, args);
4384
+ };
4385
+ const rejectionHandler = (event) => {
4386
+ const reason = event.reason;
4387
+ const message = reason instanceof Error ? reason.message : String(reason ?? "Unhandled rejection");
4388
+ const stack = reason instanceof Error ? reason.stack : void 0;
4389
+ emit(makeEvent("unhandledrejection", message, stack));
4390
+ };
4391
+ if (typeof window !== "undefined") {
4392
+ window.addEventListener("unhandledrejection", rejectionHandler);
4393
+ }
4394
+ return () => {
4395
+ console.error = originalError;
4396
+ console.warn = originalWarn;
4397
+ if (typeof window !== "undefined") {
4398
+ window.removeEventListener("unhandledrejection", rejectionHandler);
4399
+ }
4400
+ };
4401
+ }
4402
+
4403
+ // src/debug/captures/network.ts
4404
+ var DEFAULT_IGNORE = ["/api/dev-debug/", "/api/ui-bridge/", "localhost:9876"];
4405
+ function installNetworkCapture(emit, options) {
4406
+ if (typeof window === "undefined" || typeof window.fetch !== "function") {
4407
+ return () => {
4408
+ };
4409
+ }
4410
+ const originalFetch = window.fetch;
4411
+ const ignorePatterns = options?.ignorePatterns ?? DEFAULT_IGNORE;
4412
+ function shouldIgnore(url) {
4413
+ return ignorePatterns.some((p) => url.includes(p));
4414
+ }
4415
+ function getMethod(input, init) {
4416
+ if (init?.method) return init.method.toUpperCase();
4417
+ if (input instanceof Request) return input.method.toUpperCase();
4418
+ return "GET";
4419
+ }
4420
+ function getUrl(input) {
4421
+ if (typeof input === "string") return input;
4422
+ if (input instanceof URL) return input.href;
4423
+ if (input instanceof Request) return input.url;
4424
+ return String(input);
4425
+ }
4426
+ window.fetch = async function patchedFetch(input, init) {
4427
+ const requestUrl = getUrl(input);
4428
+ if (shouldIgnore(requestUrl)) {
4429
+ return originalFetch.call(window, input, init);
4430
+ }
4431
+ const method = getMethod(input, init);
4432
+ const start = performance.now();
4433
+ try {
4434
+ const response = await originalFetch.call(window, input, init);
4435
+ const durationMs = Math.round(performance.now() - start);
4436
+ if (response.status >= 400) {
4437
+ const event = {
4438
+ type: "network",
4439
+ timestamp: Date.now(),
4440
+ url: typeof window !== "undefined" ? window.location.href : "",
4441
+ method,
4442
+ requestUrl,
4443
+ status: response.status,
4444
+ statusText: response.statusText,
4445
+ durationMs,
4446
+ kind: "http-error"
4447
+ };
4448
+ emit(event);
4449
+ }
4450
+ return response;
4451
+ } catch (err) {
4452
+ const durationMs = Math.round(performance.now() - start);
4453
+ const errorMessage = err instanceof Error ? err.message : String(err);
4454
+ let kind = "network-error";
4455
+ if (err instanceof DOMException && err.name === "AbortError") {
4456
+ kind = "abort";
4457
+ } else if (errorMessage.includes("CORS") || errorMessage.includes("cross-origin")) {
4458
+ kind = "cors";
4459
+ } else if (errorMessage.includes("timeout") || errorMessage.includes("timed out")) {
4460
+ kind = "timeout";
4461
+ }
4462
+ const event = {
4463
+ type: "network",
4464
+ timestamp: Date.now(),
4465
+ url: typeof window !== "undefined" ? window.location.href : "",
4466
+ method,
4467
+ requestUrl,
4468
+ durationMs,
4469
+ kind,
4470
+ errorMessage
4471
+ };
4472
+ emit(event);
4473
+ throw err;
4474
+ }
4475
+ };
4476
+ return () => {
4477
+ window.fetch = originalFetch;
4478
+ };
4479
+ }
4480
+
4481
+ // src/debug/captures/navigation.ts
4482
+ function installNavigationCapture(emit) {
4483
+ if (typeof window === "undefined" || typeof history === "undefined") {
4484
+ return () => {
4485
+ };
4486
+ }
4487
+ let lastUrl = window.location.href;
4488
+ function emitNav(to, trigger) {
4489
+ const from = lastUrl;
4490
+ lastUrl = to;
4491
+ if (from === to) return;
4492
+ emit({
4493
+ type: "navigation",
4494
+ timestamp: Date.now(),
4495
+ url: to,
4496
+ from,
4497
+ to,
4498
+ trigger
4499
+ });
4500
+ }
4501
+ const originalPushState = history.pushState;
4502
+ const originalReplaceState = history.replaceState;
4503
+ history.pushState = function(...args) {
4504
+ originalPushState.apply(this, args);
4505
+ emitNav(window.location.href, "pushState");
4506
+ };
4507
+ history.replaceState = function(...args) {
4508
+ originalReplaceState.apply(this, args);
4509
+ emitNav(window.location.href, "replaceState");
4510
+ };
4511
+ const popstateHandler = () => {
4512
+ emitNav(window.location.href, "popstate");
4513
+ };
4514
+ window.addEventListener("popstate", popstateHandler);
4515
+ return () => {
4516
+ history.pushState = originalPushState;
4517
+ history.replaceState = originalReplaceState;
4518
+ window.removeEventListener("popstate", popstateHandler);
4519
+ };
4520
+ }
4521
+
4522
+ // src/debug/captures/long-tasks.ts
4523
+ function installLongTaskCapture(emit) {
4524
+ if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") {
4525
+ return () => {
4526
+ };
4527
+ }
4528
+ try {
4529
+ const observer = new PerformanceObserver((list) => {
4530
+ for (const entry of list.getEntries()) {
4531
+ emit({
4532
+ type: "long-task",
4533
+ timestamp: Date.now(),
4534
+ url: window.location.href,
4535
+ durationMs: Math.round(entry.duration)
4536
+ });
4537
+ }
4538
+ });
4539
+ observer.observe({ type: "longtask", buffered: true });
4540
+ return () => {
4541
+ observer.disconnect();
4542
+ };
4543
+ } catch {
4544
+ return () => {
4545
+ };
4546
+ }
4547
+ }
4548
+
4549
+ // src/debug/captures/resource-errors.ts
4550
+ var TRACKED_TAGS = /* @__PURE__ */ new Set(["IMG", "SCRIPT", "LINK"]);
4551
+ function installResourceErrorCapture(emit) {
4552
+ if (typeof window === "undefined") {
4553
+ return () => {
4554
+ };
4555
+ }
4556
+ const handler = (event) => {
4557
+ const target = event.target;
4558
+ if (!target || !target.tagName) return;
4559
+ if (!TRACKED_TAGS.has(target.tagName)) return;
4560
+ const resourceUrl = target.src || target.src || target.href || "";
4561
+ if (!resourceUrl) return;
4562
+ emit({
4563
+ type: "resource-error",
4564
+ timestamp: Date.now(),
4565
+ url: window.location.href,
4566
+ resourceUrl,
4567
+ tagName: target.tagName
4568
+ });
4569
+ };
4570
+ window.addEventListener("error", handler, true);
4571
+ return () => {
4572
+ window.removeEventListener("error", handler, true);
4573
+ };
4574
+ }
4575
+
4576
+ // src/debug/captures/web-vitals.ts
4577
+ function installWebVitalsCapture(emit) {
4578
+ if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") {
4579
+ return () => {
4580
+ };
4581
+ }
4582
+ const observers = [];
4583
+ try {
4584
+ const lcpObserver = new PerformanceObserver((list) => {
4585
+ const entries = list.getEntries();
4586
+ const last = entries[entries.length - 1];
4587
+ if (last) {
4588
+ emit({
4589
+ type: "web-vital",
4590
+ timestamp: Date.now(),
4591
+ url: window.location.href,
4592
+ metric: "LCP",
4593
+ value: Math.round(last.startTime)
4594
+ });
4595
+ }
4596
+ });
4597
+ lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
4598
+ observers.push(lcpObserver);
4599
+ } catch {
4600
+ }
4601
+ try {
4602
+ let clsValue = 0;
4603
+ const clsObserver = new PerformanceObserver((list) => {
4604
+ for (const entry of list.getEntries()) {
4605
+ if (!entry.hadRecentInput) {
4606
+ clsValue += entry.value ?? 0;
4607
+ }
4608
+ }
4609
+ emit({
4610
+ type: "web-vital",
4611
+ timestamp: Date.now(),
4612
+ url: window.location.href,
4613
+ metric: "CLS",
4614
+ value: Math.round(clsValue * 1e3) / 1e3
4615
+ });
4616
+ });
4617
+ clsObserver.observe({ type: "layout-shift", buffered: true });
4618
+ observers.push(clsObserver);
4619
+ } catch {
4620
+ }
4621
+ return () => {
4622
+ for (const obs of observers) {
4623
+ obs.disconnect();
4624
+ }
4625
+ };
4626
+ }
4627
+
4628
+ // src/debug/captures/memory.ts
4629
+ function installMemoryCapture(emit, intervalMs = 3e4) {
4630
+ if (typeof window === "undefined") {
4631
+ return () => {
4632
+ };
4633
+ }
4634
+ const perf = performance;
4635
+ if (!perf.memory) {
4636
+ return () => {
4637
+ };
4638
+ }
4639
+ const tick = () => {
4640
+ const mem = perf.memory;
4641
+ if (!mem) return;
4642
+ emit({
4643
+ type: "memory",
4644
+ timestamp: Date.now(),
4645
+ url: window.location.href,
4646
+ usedJSHeapSize: mem.usedJSHeapSize,
4647
+ totalJSHeapSize: mem.totalJSHeapSize,
4648
+ jsHeapSizeLimit: mem.jsHeapSizeLimit
4649
+ });
4650
+ };
4651
+ tick();
4652
+ const id = setInterval(tick, intervalMs);
4653
+ return () => {
4654
+ clearInterval(id);
4655
+ };
4656
+ }
4657
+
4658
+ // src/debug/captures/hmr.ts
4659
+ var HMR_PATH_PATTERNS = ["/_next/webpack-hmr", "/__turbopack_hmr", "/_next/turbopack-hmr"];
4660
+ function isHmrUrl(url) {
4661
+ return HMR_PATH_PATTERNS.some((p) => url.includes(p));
4662
+ }
4663
+ function makeEvent2(level, message, moduleName, loc) {
4664
+ return {
4665
+ type: "hmr",
4666
+ level,
4667
+ message,
4668
+ moduleName,
4669
+ loc,
4670
+ timestamp: Date.now(),
4671
+ url: typeof window !== "undefined" ? window.location.href : ""
4672
+ };
4673
+ }
4674
+ function processHmrMessage(data, emit) {
4675
+ try {
4676
+ const msg = JSON.parse(data);
4677
+ if (Array.isArray(msg.errors)) {
4678
+ for (const err of msg.errors) {
4679
+ emit(
4680
+ makeEvent2(
4681
+ "error",
4682
+ typeof err === "string" ? err : err.message ?? String(err),
4683
+ err.moduleName ?? err.moduleIdentifier,
4684
+ err.loc ? String(err.loc) : void 0
4685
+ )
4686
+ );
4687
+ }
4688
+ }
4689
+ if (Array.isArray(msg.warnings)) {
4690
+ for (const warn of msg.warnings) {
4691
+ emit(
4692
+ makeEvent2(
4693
+ "warning",
4694
+ typeof warn === "string" ? warn : warn.message ?? String(warn),
4695
+ warn.moduleName ?? warn.moduleIdentifier,
4696
+ warn.loc ? String(warn.loc) : void 0
4697
+ )
4698
+ );
4699
+ }
4700
+ }
4701
+ if (msg.action === "serverError" && msg.errorJSON) {
4702
+ try {
4703
+ const err = JSON.parse(msg.errorJSON);
4704
+ emit(makeEvent2("error", err.message ?? String(err)));
4705
+ } catch {
4706
+ emit(makeEvent2("error", msg.errorJSON));
4707
+ }
4708
+ }
4709
+ if ((msg.action === "turbopack-message" || msg.type === "turbopack-message") && msg.data?.diagnostics) {
4710
+ for (const diag of msg.data.diagnostics) {
4711
+ emit(
4712
+ makeEvent2(
4713
+ diag.category === "warning" ? "warning" : "error",
4714
+ diag.message ?? String(diag),
4715
+ diag.filePath,
4716
+ diag.line != null ? `${diag.line}:${diag.column ?? 0}` : void 0
4717
+ )
4718
+ );
4719
+ }
4720
+ }
4721
+ } catch {
4722
+ }
4723
+ }
4724
+ function installWebSocketCapture(emit, cleanups) {
4725
+ if (!window.WebSocket) return;
4726
+ const OriginalWebSocket = window.WebSocket;
4727
+ const trackedSockets = [];
4728
+ const PatchedWebSocket = function(url, protocols) {
4729
+ const ws = new OriginalWebSocket(url, protocols);
4730
+ const urlStr = typeof url === "string" ? url : url.toString();
4731
+ if (isHmrUrl(urlStr)) {
4732
+ ws.addEventListener("message", (event) => {
4733
+ if (typeof event.data === "string") {
4734
+ processHmrMessage(event.data, emit);
4735
+ }
4736
+ });
4737
+ trackedSockets.push(ws);
4738
+ }
4739
+ return ws;
4740
+ };
4741
+ PatchedWebSocket.prototype = OriginalWebSocket.prototype;
4742
+ Object.defineProperty(PatchedWebSocket, "CONNECTING", { value: OriginalWebSocket.CONNECTING });
4743
+ Object.defineProperty(PatchedWebSocket, "OPEN", { value: OriginalWebSocket.OPEN });
4744
+ Object.defineProperty(PatchedWebSocket, "CLOSING", { value: OriginalWebSocket.CLOSING });
4745
+ Object.defineProperty(PatchedWebSocket, "CLOSED", { value: OriginalWebSocket.CLOSED });
4746
+ window.WebSocket = PatchedWebSocket;
4747
+ cleanups.push(() => {
4748
+ window.WebSocket = OriginalWebSocket;
4749
+ for (const ws of trackedSockets) {
4750
+ ws.close();
4751
+ }
4752
+ trackedSockets.length = 0;
4753
+ });
4754
+ }
4755
+ function installEventSourceCapture(emit, cleanups) {
4756
+ if (!window.EventSource) return;
4757
+ const OriginalEventSource = window.EventSource;
4758
+ const trackedSources = [];
4759
+ const messageHandler = (event) => {
4760
+ if (typeof event.data === "string") {
4761
+ processHmrMessage(event.data, emit);
4762
+ }
4763
+ };
4764
+ const PatchedEventSource = function(url, init) {
4765
+ const es = new OriginalEventSource(url, init);
4766
+ const urlStr = typeof url === "string" ? url : url.toString();
4767
+ if (isHmrUrl(urlStr)) {
4768
+ es.addEventListener("message", messageHandler);
4769
+ trackedSources.push(es);
4770
+ }
4771
+ return es;
4772
+ };
4773
+ PatchedEventSource.prototype = OriginalEventSource.prototype;
4774
+ Object.defineProperty(PatchedEventSource, "CONNECTING", {
4775
+ value: OriginalEventSource.CONNECTING
4776
+ });
4777
+ Object.defineProperty(PatchedEventSource, "OPEN", { value: OriginalEventSource.OPEN });
4778
+ Object.defineProperty(PatchedEventSource, "CLOSED", { value: OriginalEventSource.CLOSED });
4779
+ window.EventSource = PatchedEventSource;
4780
+ cleanups.push(() => {
4781
+ window.EventSource = OriginalEventSource;
4782
+ for (const es of trackedSources) {
4783
+ es.close();
4784
+ }
4785
+ trackedSources.length = 0;
4786
+ });
4787
+ }
4788
+ function installHmrCapture(emit) {
4789
+ if (typeof window === "undefined") return () => {
4790
+ };
4791
+ const cleanups = [];
4792
+ installWebSocketCapture(emit, cleanups);
4793
+ installEventSourceCapture(emit, cleanups);
4794
+ return () => {
4795
+ for (const cleanup of cleanups) {
4796
+ cleanup();
4797
+ }
4798
+ };
4799
+ }
4800
+
4801
+ // src/debug/captures/long-animation-frames.ts
4802
+ function installLoafCapture(emit) {
4803
+ if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") {
4804
+ return () => {
4805
+ };
4806
+ }
4807
+ try {
4808
+ const observer = new PerformanceObserver((list) => {
4809
+ for (const entry of list.getEntries()) {
4810
+ const scripts = (entry.scripts ?? []).map((s) => {
4811
+ const script = s;
4812
+ return {
4813
+ invoker: script.invoker ?? "",
4814
+ sourceURL: script.sourceURL ?? "",
4815
+ sourceFunctionName: script.sourceFunctionName ?? "",
4816
+ sourceCharPosition: script.sourceCharPosition ?? 0,
4817
+ duration: Math.round(script.duration ?? 0)
4818
+ };
4819
+ });
4820
+ emit({
4821
+ type: "long-animation-frame",
4822
+ timestamp: Date.now(),
4823
+ url: window.location.href,
4824
+ durationMs: Math.round(entry.duration),
4825
+ blockingDurationMs: Math.round(
4826
+ entry.blockingDuration ?? 0
4827
+ ),
4828
+ scripts
4829
+ });
4830
+ }
4831
+ });
4832
+ observer.observe({ type: "long-animation-frame", buffered: true });
4833
+ return () => {
4834
+ observer.disconnect();
4835
+ };
4836
+ } catch {
4837
+ return () => {
4838
+ };
4839
+ }
4840
+ }
4841
+
4842
+ // src/debug/browser-capture.ts
4843
+ var BrowserEventCapture = class {
4844
+ constructor(config) {
4845
+ this.buffer = [];
4846
+ this.installed = false;
4847
+ this.cleanups = [];
4848
+ this.onEvent = null;
4849
+ this.config = config ?? {};
4850
+ this.maxEntries = config?.maxEntries ?? DEFAULT_CAPTURE_CONFIG.maxEntries;
4851
+ }
4852
+ setOnEvent(cb) {
4853
+ this.onEvent = cb;
4854
+ }
4855
+ /**
4856
+ * Install all enabled capture sub-modules.
4857
+ * Safe to call multiple times (no-ops if already installed).
4858
+ */
4859
+ install() {
4860
+ if (this.installed) return;
4861
+ const cfg = { ...DEFAULT_CAPTURE_CONFIG, ...this.config };
4862
+ const emit = (event) => {
4863
+ this.buffer.push(event);
4864
+ this.trim();
4865
+ this.onEvent?.(event);
4866
+ };
4867
+ if (cfg.console) {
4868
+ this.cleanups.push(installConsoleCapture(emit));
4869
+ }
4870
+ if (cfg.network) {
4871
+ this.cleanups.push(installNetworkCapture(emit, cfg.networkOptions));
4872
+ }
4873
+ if (cfg.navigation) {
4874
+ this.cleanups.push(installNavigationCapture(emit));
4875
+ }
4876
+ if (cfg.longTasks) {
4877
+ this.cleanups.push(installLongTaskCapture(emit));
4878
+ }
4879
+ if (cfg.longAnimationFrames) {
4880
+ this.cleanups.push(installLoafCapture(emit));
4881
+ }
4882
+ if (cfg.resourceErrors) {
4883
+ this.cleanups.push(installResourceErrorCapture(emit));
4884
+ }
4885
+ if (cfg.webVitals) {
4886
+ this.cleanups.push(installWebVitalsCapture(emit));
4887
+ }
4888
+ if (cfg.memory) {
4889
+ this.cleanups.push(installMemoryCapture(emit, cfg.memoryIntervalMs));
4890
+ }
4891
+ if (cfg.hmr) {
4892
+ this.cleanups.push(installHmrCapture(emit));
4893
+ }
4894
+ this.installed = true;
4895
+ }
4896
+ /**
4897
+ * Uninstall all capture sub-modules.
4898
+ */
4899
+ uninstall() {
4900
+ if (!this.installed) return;
4901
+ for (const cleanup of this.cleanups) {
4902
+ cleanup();
4903
+ }
4904
+ this.cleanups = [];
4905
+ this.installed = false;
4906
+ }
4907
+ // -------------------------------------------------------------------------
4908
+ // Manual event reporting (for events that can't be auto-captured)
4909
+ // -------------------------------------------------------------------------
4910
+ reportReactError(error, errorInfo) {
4911
+ const event = {
4912
+ type: "react-error",
4913
+ timestamp: Date.now(),
4914
+ url: typeof window !== "undefined" ? window.location.href : "",
4915
+ message: error.message,
4916
+ stack: error.stack,
4917
+ componentStack: errorInfo.componentStack
4918
+ };
4919
+ this.buffer.push(event);
4920
+ this.trim();
4921
+ this.onEvent?.(event);
4922
+ }
4923
+ reportWsStateChange(prev, next, reconnectAttempt) {
4924
+ if (next === "disconnected" || next === "error") {
4925
+ const event = {
4926
+ type: "ws-disconnection",
4927
+ timestamp: Date.now(),
4928
+ url: typeof window !== "undefined" ? window.location.href : "",
4929
+ previousState: prev,
4930
+ newState: next,
4931
+ reconnectAttempt
4932
+ };
4933
+ this.buffer.push(event);
4934
+ this.trim();
4935
+ this.onEvent?.(event);
4936
+ }
4937
+ }
4938
+ // -------------------------------------------------------------------------
4939
+ // Query methods
4940
+ // -------------------------------------------------------------------------
4941
+ getSince(ts) {
4942
+ return this.buffer.filter((e) => e.timestamp >= ts);
4943
+ }
4944
+ getRecent(n = 50) {
4945
+ return this.buffer.slice(-n);
4946
+ }
4947
+ getByType(type) {
4948
+ return this.buffer.filter((e) => e.type === type);
4949
+ }
4950
+ /**
4951
+ * Get console errors since a timestamp (backward-compat for ActionExecutor).
4952
+ */
4953
+ getConsoleSince(ts) {
4954
+ return this.buffer.filter((e) => (e.type === "console" || e.type === "hmr") && e.timestamp >= ts).map((e) => ({
4955
+ timestamp: e.timestamp,
4956
+ level: e.type === "hmr" ? e.level === "warning" ? "warn" : e.level : e.level,
4957
+ message: e.message,
4958
+ stack: e.stack
4959
+ }));
4960
+ }
4961
+ /**
4962
+ * Get recent console errors (backward-compat for ActionExecutor).
4963
+ */
4964
+ getConsoleRecent(n = 50) {
4965
+ return this.buffer.filter((e) => e.type === "console" || e.type === "hmr").slice(-n).map((e) => ({
4966
+ timestamp: e.timestamp,
4967
+ level: e.type === "hmr" ? e.level === "warning" ? "warn" : e.level : e.level,
4968
+ message: e.message,
4969
+ stack: e.stack
4970
+ }));
4971
+ }
4972
+ clear() {
4973
+ this.buffer = [];
4974
+ }
4975
+ trim() {
4976
+ if (this.buffer.length > this.maxEntries) {
4977
+ this.buffer = this.buffer.slice(-this.maxEntries);
4978
+ }
4979
+ }
4980
+ };
4981
+ var UIBridgeContext = react.createContext(null);
4982
+ function UIBridgeProvider({
4983
+ children,
4984
+ features = {},
4985
+ config = {},
4986
+ onEvent,
4987
+ onBrowserEvent,
4988
+ browserCaptureConfig
4989
+ }) {
4990
+ const registryRef = react.useRef(null);
4991
+ const renderLogRef = react.useRef(null);
4992
+ const metricsRef = react.useRef(null);
4993
+ const browserCaptureRef = react.useRef(null);
4994
+ const wsClientRef = react.useRef(null);
4995
+ const [wsConnectionState, setWsConnectionState] = react.useState("disconnected");
4996
+ const prevWsStateRef = react.useRef("disconnected");
4997
+ if (!registryRef.current) {
4998
+ registryRef.current = new UIBridgeRegistry({
4999
+ verbose: config.verbose,
5000
+ onEvent
5001
+ });
5002
+ setGlobalRegistry(registryRef.current);
5003
+ if (features.renderLog) {
5004
+ renderLogRef.current = createRenderLogManager({
5005
+ maxEntries: config.maxLogEntries
5006
+ });
5007
+ }
5008
+ if (features.debug) {
5009
+ metricsRef.current = createMetricsCollector();
5010
+ }
5011
+ browserCaptureRef.current = new BrowserEventCapture(browserCaptureConfig);
5012
+ browserCaptureRef.current.install();
5013
+ if (config.websocket) {
5014
+ const wsPort = config.websocketPort || config.serverPort || 9876;
5015
+ const wsUrl = `ws://localhost:${wsPort}`;
5016
+ wsClientRef.current = createWSClient({
3605
5017
  url: wsUrl,
3606
5018
  autoReconnect: true,
3607
5019
  reconnectDelay: 1e3,
@@ -3609,12 +5021,27 @@ function UIBridgeProvider({
3609
5021
  pingInterval: 3e4
3610
5022
  });
3611
5023
  }
5024
+ if (typeof window !== "undefined") {
5025
+ const w = window;
5026
+ if (!w.__UI_BRIDGE__) {
5027
+ w.__UI_BRIDGE__ = {};
5028
+ }
5029
+ w.__UI_BRIDGE__.specs = {
5030
+ getGlobalSpecStore
5031
+ };
5032
+ w.__UI_BRIDGE__.browserCapture = browserCaptureRef.current;
5033
+ w.__UI_BRIDGE__.consoleCapture = browserCaptureRef.current;
5034
+ }
3612
5035
  }
3613
5036
  const registry = registryRef.current;
3614
5037
  const renderLog = renderLogRef.current || void 0;
3615
5038
  const metrics = metricsRef.current || void 0;
3616
5039
  const wsClient = wsClientRef.current || void 0;
3617
- const executor = react.useMemo(() => createActionExecutor(registry), [registry]);
5040
+ const browserCapture = browserCaptureRef.current || void 0;
5041
+ const executor = react.useMemo(
5042
+ () => createActionExecutor(registry, browserCapture),
5043
+ [registry, browserCapture]
5044
+ );
3618
5045
  const workflowEngine = react.useMemo(
3619
5046
  () => createWorkflowEngine(registry, executor),
3620
5047
  [registry, executor]
@@ -3641,13 +5068,21 @@ function UIBridgeProvider({
3641
5068
  react.useEffect(() => {
3642
5069
  if (!wsClient) return;
3643
5070
  const unsubscribe = wsClient.onConnectionChange((state) => {
5071
+ const prev = prevWsStateRef.current;
5072
+ prevWsStateRef.current = state;
3644
5073
  setWsConnectionState(state);
5074
+ browserCaptureRef.current?.reportWsStateChange(prev, state);
3645
5075
  });
3646
5076
  return unsubscribe;
3647
5077
  }, [wsClient]);
5078
+ react.useEffect(() => {
5079
+ browserCaptureRef.current?.setOnEvent(onBrowserEvent ?? null);
5080
+ }, [onBrowserEvent]);
3648
5081
  react.useEffect(() => {
3649
5082
  return () => {
3650
5083
  renderLog?.stop();
5084
+ browserCaptureRef.current?.setOnEvent(null);
5085
+ browserCaptureRef.current?.uninstall();
3651
5086
  wsClient?.disconnect();
3652
5087
  resetGlobalRegistry();
3653
5088
  };
@@ -3682,6 +5117,10 @@ function UIBridgeProvider({
3682
5117
  const getElements = react.useCallback(() => registry.getAllElements(), [registry]);
3683
5118
  const getComponents = react.useCallback(() => registry.getAllComponents(), [registry]);
3684
5119
  const createSnapshot = react.useCallback(() => registry.createSnapshot(), [registry]);
5120
+ const createSnapshotAsync = react.useCallback(
5121
+ (batchSize) => registry.createSnapshotAsync(batchSize),
5122
+ [registry]
5123
+ );
3685
5124
  const on = react.useCallback(
3686
5125
  (type, listener) => registry.on(type, listener),
3687
5126
  [registry]
@@ -3704,6 +5143,7 @@ function UIBridgeProvider({
3704
5143
  getElements,
3705
5144
  getComponents,
3706
5145
  createSnapshot,
5146
+ createSnapshotAsync,
3707
5147
  on,
3708
5148
  off,
3709
5149
  initialized: true,
@@ -3725,6 +5165,7 @@ function UIBridgeProvider({
3725
5165
  getElements,
3726
5166
  getComponents,
3727
5167
  createSnapshot,
5168
+ createSnapshotAsync,
3728
5169
  on,
3729
5170
  off,
3730
5171
  wsConnect,
@@ -3838,11 +5279,31 @@ function useUIComponent(options) {
3838
5279
  const registeredRef = react.useRef(false);
3839
5280
  const actionsRef = react.useRef(options.actions || []);
3840
5281
  const elementIdsRef = react.useRef(options.elementIds || []);
5282
+ const stateRef = react.useRef(options.state);
5283
+ const computedRef = react.useRef(options.computed);
3841
5284
  const { id, name, description, autoRegister = true } = options;
3842
5285
  react.useEffect(() => {
3843
5286
  actionsRef.current = options.actions || [];
3844
5287
  elementIdsRef.current = options.elementIds || [];
3845
- }, [options.actions, options.elementIds]);
5288
+ stateRef.current = options.state;
5289
+ computedRef.current = options.computed;
5290
+ }, [options.actions, options.elementIds, options.state, options.computed]);
5291
+ const createGetComputed = react.useCallback(() => {
5292
+ return () => {
5293
+ const computed = computedRef.current;
5294
+ if (!computed) return {};
5295
+ const result = {};
5296
+ for (const [key, def] of Object.entries(computed)) {
5297
+ try {
5298
+ const getter = typeof def === "function" ? def : def.getter;
5299
+ result[key] = getter();
5300
+ } catch {
5301
+ result[key] = void 0;
5302
+ }
5303
+ }
5304
+ return result;
5305
+ };
5306
+ }, []);
3846
5307
  const register = react.useCallback(() => {
3847
5308
  if (!bridge || registeredRef.current) return;
3848
5309
  bridge.registry.registerComponent(id, {
@@ -3854,10 +5315,12 @@ function useUIComponent(options) {
3854
5315
  description: a.description,
3855
5316
  handler: a.handler
3856
5317
  })),
3857
- elementIds: elementIdsRef.current
5318
+ elementIds: elementIdsRef.current,
5319
+ getState: stateRef.current,
5320
+ getComputed: createGetComputed()
3858
5321
  });
3859
5322
  registeredRef.current = true;
3860
- }, [bridge, id, name, description]);
5323
+ }, [bridge, id, name, description, createGetComputed]);
3861
5324
  const unregister = react.useCallback(() => {
3862
5325
  if (!bridge || !registeredRef.current) return;
3863
5326
  bridge.registry.unregisterComponent(id);
@@ -3942,6 +5405,20 @@ function useUIBridge() {
3942
5405
  }
3943
5406
  return context.createSnapshot();
3944
5407
  }, [context]);
5408
+ const createSnapshotAsync = react.useCallback(
5409
+ async (batchSize) => {
5410
+ if (!context) {
5411
+ return {
5412
+ timestamp: Date.now(),
5413
+ elements: [],
5414
+ components: [],
5415
+ workflows: []
5416
+ };
5417
+ }
5418
+ return context.createSnapshotAsync(batchSize);
5419
+ },
5420
+ [context]
5421
+ );
3945
5422
  const executeAction = react.useCallback(
3946
5423
  async (elementId, request) => {
3947
5424
  if (!context) {
@@ -4063,6 +5540,7 @@ function useUIBridge() {
4063
5540
  components,
4064
5541
  workflows,
4065
5542
  createSnapshot,
5543
+ createSnapshotAsync,
4066
5544
  executeAction,
4067
5545
  executeComponentAction,
4068
5546
  find,
@@ -4478,6 +5956,305 @@ function useNavigationPath(targetStates) {
4478
5956
  return bridge.registry.findPath(targetStates);
4479
5957
  }, [bridge, targetStates]);
4480
5958
  }
5959
+
5960
+ // src/react/content-discovery.ts
5961
+ var CONTENT_SELECTORS = [
5962
+ "h1",
5963
+ "h2",
5964
+ "h3",
5965
+ "h4",
5966
+ "h5",
5967
+ "h6",
5968
+ "p",
5969
+ "li",
5970
+ "td",
5971
+ "th",
5972
+ "label:not([for])",
5973
+ "figcaption",
5974
+ "caption",
5975
+ "blockquote",
5976
+ "pre",
5977
+ "code",
5978
+ "dd",
5979
+ "dt",
5980
+ '[role="heading"]',
5981
+ '[role="status"]',
5982
+ '[role="alert"]',
5983
+ "[aria-live]",
5984
+ "legend",
5985
+ "summary",
5986
+ "[data-content-role]"
5987
+ ];
5988
+ var CONTENT_EXCLUDE_SELECTORS = [
5989
+ "script",
5990
+ "style",
5991
+ "noscript",
5992
+ "template",
5993
+ '[aria-hidden="true"]',
5994
+ "[data-no-register]",
5995
+ ".sr-only",
5996
+ ".visually-hidden"
5997
+ ];
5998
+ function getDirectTextContent(element) {
5999
+ let text = "";
6000
+ for (const node of element.childNodes) {
6001
+ if (node.nodeType === Node.TEXT_NODE) {
6002
+ text += node.textContent || "";
6003
+ }
6004
+ }
6005
+ return text.trim();
6006
+ }
6007
+ var SEMANTIC_CONTENT_TAGS = /* @__PURE__ */ new Set([
6008
+ "h1",
6009
+ "h2",
6010
+ "h3",
6011
+ "h4",
6012
+ "h5",
6013
+ "h6",
6014
+ "p",
6015
+ "li",
6016
+ "td",
6017
+ "th",
6018
+ "label",
6019
+ "figcaption",
6020
+ "caption",
6021
+ "blockquote",
6022
+ "pre",
6023
+ "code",
6024
+ "dd",
6025
+ "dt",
6026
+ "legend",
6027
+ "summary"
6028
+ ]);
6029
+ function isNoise(element) {
6030
+ const text = getDirectTextContent(element);
6031
+ const tag = element.tagName.toLowerCase();
6032
+ if (!text && element.children.length > 0 && !SEMANTIC_CONTENT_TAGS.has(tag) && !element.hasAttribute("data-content-role")) {
6033
+ return true;
6034
+ }
6035
+ const fullText = element.textContent?.trim() || "";
6036
+ if (fullText.length === 1 && !/\w/.test(fullText)) {
6037
+ return true;
6038
+ }
6039
+ return false;
6040
+ }
6041
+ function isContentVisible(element) {
6042
+ const style = window.getComputedStyle(element);
6043
+ if (style.display === "none" || style.visibility === "hidden") {
6044
+ return false;
6045
+ }
6046
+ if (parseFloat(style.opacity) === 0) {
6047
+ return false;
6048
+ }
6049
+ const rect = element.getBoundingClientRect();
6050
+ return rect.width > 0 && rect.height > 0;
6051
+ }
6052
+ function shouldRegisterContent(element, options = {}, registeredIds) {
6053
+ const minTextLength = options.minTextLength ?? 1;
6054
+ const excludeSelectors = [
6055
+ ...CONTENT_EXCLUDE_SELECTORS,
6056
+ ...options.excludeContentSelectors || []
6057
+ ];
6058
+ for (const sel of excludeSelectors) {
6059
+ if (element.matches(sel)) {
6060
+ return false;
6061
+ }
6062
+ }
6063
+ if (!isContentVisible(element)) {
6064
+ return false;
6065
+ }
6066
+ const text = element.textContent?.trim() || "";
6067
+ if (text.length < minTextLength) {
6068
+ return false;
6069
+ }
6070
+ if (isNoise(element)) {
6071
+ return false;
6072
+ }
6073
+ if (isInteractiveElement(element)) {
6074
+ return false;
6075
+ }
6076
+ const id = generateContentId(element);
6077
+ if (registeredIds.has(id)) {
6078
+ return false;
6079
+ }
6080
+ if (options.contentRoles && options.contentRoles.length > 0) {
6081
+ const metadata = inferContentMetadata(element);
6082
+ if (!options.contentRoles.includes(metadata.contentRole)) {
6083
+ return false;
6084
+ }
6085
+ }
6086
+ return true;
6087
+ }
6088
+ function isInteractiveElement(element) {
6089
+ const tag = element.tagName.toLowerCase();
6090
+ const interactiveTags = ["button", "input", "select", "textarea", "a"];
6091
+ if (interactiveTags.includes(tag)) return true;
6092
+ const role = element.getAttribute("role");
6093
+ const interactiveRoles = [
6094
+ "button",
6095
+ "link",
6096
+ "checkbox",
6097
+ "radio",
6098
+ "menuitem",
6099
+ "tab",
6100
+ "switch",
6101
+ "slider",
6102
+ "spinbutton",
6103
+ "combobox",
6104
+ "listbox",
6105
+ "option",
6106
+ "textbox"
6107
+ ];
6108
+ if (role && interactiveRoles.includes(role)) return true;
6109
+ if (element.getAttribute("contenteditable") === "true") return true;
6110
+ if (element.hasAttribute("data-ui-element")) return true;
6111
+ return false;
6112
+ }
6113
+ function inferContentType(element) {
6114
+ const explicitRole = element.getAttribute("data-content-role");
6115
+ if (explicitRole) return roleToContentType(explicitRole);
6116
+ const tag = element.tagName.toLowerCase();
6117
+ const role = element.getAttribute("role");
6118
+ if (role === "heading") return "heading";
6119
+ if (role === "status") return "status-message";
6120
+ if (role === "alert") return "status-message";
6121
+ if (/^h[1-6]$/.test(tag)) return "heading";
6122
+ if (tag === "p") return "paragraph";
6123
+ if (tag === "li") return "list-item";
6124
+ if (tag === "td") return "table-cell";
6125
+ if (tag === "th") return "table-header";
6126
+ if (tag === "label") return "label";
6127
+ if (tag === "figcaption" || tag === "caption") return "caption";
6128
+ if (tag === "blockquote") return "blockquote";
6129
+ if (tag === "pre" || tag === "code") return "code-block";
6130
+ if (tag === "dd") return "description-text";
6131
+ if (tag === "dt") return "label";
6132
+ if (tag === "legend") return "label";
6133
+ if (tag === "summary") return "label";
6134
+ if (element.hasAttribute("aria-live")) return "status-message";
6135
+ const classList = element.className?.toLowerCase() || "";
6136
+ if (classList.includes("badge")) return "badge";
6137
+ if (classList.includes("status")) return "status-message";
6138
+ if (classList.includes("metric") || classList.includes("stat")) return "metric-value";
6139
+ return "content-generic";
6140
+ }
6141
+ function roleToContentType(role) {
6142
+ const map = {
6143
+ heading: "heading",
6144
+ "body-text": "paragraph",
6145
+ "list-item": "list-item",
6146
+ "table-cell": "table-cell",
6147
+ "table-header": "table-header",
6148
+ label: "label",
6149
+ caption: "caption",
6150
+ quote: "blockquote",
6151
+ code: "code-block",
6152
+ badge: "badge",
6153
+ status: "status-message",
6154
+ metric: "metric-value",
6155
+ description: "description-text",
6156
+ navigation: "nav-text",
6157
+ generic: "content-generic"
6158
+ };
6159
+ return map[role] ?? "content-generic";
6160
+ }
6161
+ function contentTypeToRole(contentType) {
6162
+ const map = {
6163
+ heading: "heading",
6164
+ paragraph: "body-text",
6165
+ "list-item": "list-item",
6166
+ "table-cell": "table-cell",
6167
+ "table-header": "table-header",
6168
+ label: "label",
6169
+ caption: "caption",
6170
+ blockquote: "quote",
6171
+ "code-block": "code",
6172
+ badge: "badge",
6173
+ "status-message": "status",
6174
+ "metric-value": "metric",
6175
+ "description-text": "description",
6176
+ "nav-text": "navigation",
6177
+ "content-generic": "generic"
6178
+ };
6179
+ return map[contentType];
6180
+ }
6181
+ function inferContentMetadata(element) {
6182
+ const contentType = inferContentType(element);
6183
+ const explicitRole = element.getAttribute("data-content-role");
6184
+ const contentRole = explicitRole ? explicitRole : contentTypeToRole(contentType);
6185
+ const metadata = {
6186
+ contentRole
6187
+ };
6188
+ const explicitLevel = element.getAttribute("data-content-level");
6189
+ const tag = element.tagName.toLowerCase();
6190
+ const role = element.getAttribute("role");
6191
+ if (explicitLevel) {
6192
+ metadata.headingLevel = parseInt(explicitLevel, 10);
6193
+ } else if (/^h([1-6])$/.test(tag)) {
6194
+ metadata.headingLevel = parseInt(tag[1], 10);
6195
+ } else if (role === "heading") {
6196
+ const ariaLevel = element.getAttribute("aria-level");
6197
+ metadata.headingLevel = ariaLevel ? parseInt(ariaLevel, 10) : 2;
6198
+ }
6199
+ if (element.hasAttribute("aria-live") || role === "status" || role === "alert") {
6200
+ metadata.dynamic = true;
6201
+ }
6202
+ if (tag === "td" || tag === "th") {
6203
+ const row = element.closest("tr");
6204
+ const table = element.closest("table");
6205
+ if (row && table) {
6206
+ const rows = Array.from(table.querySelectorAll("tr"));
6207
+ const rowIndex = rows.indexOf(row);
6208
+ const cells = Array.from(row.children);
6209
+ const colIndex = cells.indexOf(element);
6210
+ metadata.structuralContext = `table > row ${rowIndex} > col ${colIndex}`;
6211
+ }
6212
+ }
6213
+ if (metadata.dynamic) {
6214
+ const text = element.textContent?.trim() || "";
6215
+ if (text.length > 10) {
6216
+ metadata.stableTextPrefix = text.substring(0, 10);
6217
+ }
6218
+ }
6219
+ return metadata;
6220
+ }
6221
+ function slugify(text, maxLength = 30) {
6222
+ return text.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").slice(0, maxLength).replace(/-+$/, "");
6223
+ }
6224
+ function generateContentId(element) {
6225
+ const explicitId = element.getAttribute("data-content-id");
6226
+ if (explicitId) return explicitId;
6227
+ const contentType = inferContentType(element);
6228
+ const tag = element.tagName.toLowerCase();
6229
+ const contentLabel = element.getAttribute("data-content-label");
6230
+ if (contentType === "heading") {
6231
+ const level = element.getAttribute("data-content-level") || (/^h([1-6])$/.test(tag) ? tag[1] : element.getAttribute("aria-level") || "2");
6232
+ const text = contentLabel || element.textContent?.trim() || "";
6233
+ const slug = slugify(text);
6234
+ return `heading-${level}-${slug || "untitled"}`;
6235
+ }
6236
+ if (contentType === "table-cell" || contentType === "table-header") {
6237
+ const row = element.closest("tr");
6238
+ const table = element.closest("table");
6239
+ if (row && table) {
6240
+ const rows = Array.from(table.querySelectorAll("tr"));
6241
+ const rowIndex = rows.indexOf(row);
6242
+ const cells = Array.from(row.children);
6243
+ const colIndex = cells.indexOf(element);
6244
+ const tableAnchor = table.getAttribute("data-ui-id") || table.getAttribute("data-testid") || table.id || "table";
6245
+ return `cell-r${rowIndex}-c${colIndex}-${tableAnchor}`;
6246
+ }
6247
+ }
6248
+ const parent = element.parentElement;
6249
+ const anchorId = parent?.getAttribute("data-ui-id") || parent?.getAttribute("data-testid") || parent?.id || "";
6250
+ const siblings = parent ? Array.from(parent.querySelectorAll(`:scope > ${tag}`)) : [];
6251
+ const siblingIndex = siblings.indexOf(element);
6252
+ const textSlug = slugify(contentLabel || element.textContent?.trim() || "", 20);
6253
+ const anchor = anchorId ? slugify(anchorId, 15) : textSlug;
6254
+ return `content-${contentType}-${anchor || "unknown"}-${siblingIndex >= 0 ? siblingIndex : 0}`;
6255
+ }
6256
+
6257
+ // src/react/useAutoRegister.ts
4481
6258
  var INTERACTIVE_SELECTORS2 = [
4482
6259
  "a[href]",
4483
6260
  "button",
@@ -4672,12 +6449,17 @@ function useAutoRegister(options = {}) {
4672
6449
  excludeSelectors = [],
4673
6450
  generateId: customGenerateId,
4674
6451
  onRegister,
4675
- onUnregister
6452
+ onUnregister,
6453
+ contentDiscovery
4676
6454
  } = options;
6455
+ const contentEnabled = contentDiscovery?.enabled !== false;
4677
6456
  const bridge = useUIBridgeOptional();
4678
6457
  const registeredElementsRef = react.useRef(/* @__PURE__ */ new Map());
6458
+ const registeredContentElementsRef = react.useRef(/* @__PURE__ */ new Map());
4679
6459
  const pendingRegistrationsRef = react.useRef(/* @__PURE__ */ new Set());
6460
+ const pendingContentRegistrationsRef = react.useRef(/* @__PURE__ */ new Set());
4680
6461
  const debounceTimeoutRef = react.useRef(null);
6462
+ const contentDebounceTimeoutRef = react.useRef(null);
4681
6463
  const shouldRegister = react.useCallback(
4682
6464
  (element) => {
4683
6465
  if (!includeHidden && !isElementVisible2(element)) {
@@ -4745,6 +6527,41 @@ function useAutoRegister(options = {}) {
4745
6527
  },
4746
6528
  [bridge, onUnregister]
4747
6529
  );
6530
+ const registerContentElement = react.useCallback(
6531
+ (element) => {
6532
+ if (!bridge?.registry || registeredContentElementsRef.current.has(element)) {
6533
+ return;
6534
+ }
6535
+ const maxElements = contentDiscovery?.maxContentElements ?? 500;
6536
+ if (registeredContentElementsRef.current.size >= maxElements) {
6537
+ return;
6538
+ }
6539
+ const id = generateContentId(element);
6540
+ const existing = bridge.registry.getElement(id);
6541
+ if (existing) {
6542
+ return;
6543
+ }
6544
+ const contentType = inferContentType(element);
6545
+ const metadata = inferContentMetadata(element);
6546
+ const label = element.getAttribute("data-content-label") || element.textContent?.trim().substring(0, 50) || void 0;
6547
+ bridge.registry.registerContentElement(id, element, {
6548
+ contentType,
6549
+ contentMetadata: metadata,
6550
+ label
6551
+ });
6552
+ registeredContentElementsRef.current.set(element, id);
6553
+ },
6554
+ [bridge, contentDiscovery?.maxContentElements]
6555
+ );
6556
+ const unregisterContentElement = react.useCallback(
6557
+ (element) => {
6558
+ const id = registeredContentElementsRef.current.get(element);
6559
+ if (!id || !bridge?.registry) return;
6560
+ bridge.registry.unregisterElement(id);
6561
+ registeredContentElementsRef.current.delete(element);
6562
+ },
6563
+ [bridge]
6564
+ );
4748
6565
  const processPendingRegistrations = react.useCallback(() => {
4749
6566
  pendingRegistrationsRef.current.forEach((element) => {
4750
6567
  if (shouldRegister(element)) {
@@ -4753,6 +6570,15 @@ function useAutoRegister(options = {}) {
4753
6570
  });
4754
6571
  pendingRegistrationsRef.current.clear();
4755
6572
  }, [shouldRegister, registerElement]);
6573
+ const processPendingContentRegistrations = react.useCallback(() => {
6574
+ const registeredIds = new Set(registeredContentElementsRef.current.values());
6575
+ pendingContentRegistrationsRef.current.forEach((element) => {
6576
+ if (shouldRegisterContent(element, contentDiscovery, registeredIds)) {
6577
+ registerContentElement(element);
6578
+ }
6579
+ });
6580
+ pendingContentRegistrationsRef.current.clear();
6581
+ }, [contentDiscovery, registerContentElement]);
4756
6582
  const queueRegistration = react.useCallback(
4757
6583
  (element) => {
4758
6584
  pendingRegistrationsRef.current.add(element);
@@ -4763,6 +6589,20 @@ function useAutoRegister(options = {}) {
4763
6589
  },
4764
6590
  [debounceMs, processPendingRegistrations]
4765
6591
  );
6592
+ const queueContentRegistration = react.useCallback(
6593
+ (element) => {
6594
+ pendingContentRegistrationsRef.current.add(element);
6595
+ if (contentDebounceTimeoutRef.current) {
6596
+ clearTimeout(contentDebounceTimeoutRef.current);
6597
+ }
6598
+ const contentDebounceMs = contentDiscovery?.contentDebounceMs ?? 250;
6599
+ contentDebounceTimeoutRef.current = setTimeout(
6600
+ processPendingContentRegistrations,
6601
+ contentDebounceMs
6602
+ );
6603
+ },
6604
+ [contentDiscovery?.contentDebounceMs, processPendingContentRegistrations]
6605
+ );
4766
6606
  const scanAndRegister = react.useCallback(
4767
6607
  (rootElement) => {
4768
6608
  const allSelectors = [...INTERACTIVE_SELECTORS2, ...includeSelectors].join(", ");
@@ -4772,8 +6612,28 @@ function useAutoRegister(options = {}) {
4772
6612
  queueRegistration(element);
4773
6613
  }
4774
6614
  });
6615
+ if (contentEnabled) {
6616
+ const contentSelectors = [
6617
+ ...CONTENT_SELECTORS,
6618
+ ...contentDiscovery?.includeContentSelectors || []
6619
+ ].join(", ");
6620
+ const contentElements = rootElement.querySelectorAll(contentSelectors);
6621
+ const registeredIds = new Set(registeredContentElementsRef.current.values());
6622
+ contentElements.forEach((element) => {
6623
+ if (shouldRegisterContent(element, contentDiscovery, registeredIds)) {
6624
+ queueContentRegistration(element);
6625
+ }
6626
+ });
6627
+ }
4775
6628
  },
4776
- [includeSelectors, shouldRegister, queueRegistration]
6629
+ [
6630
+ includeSelectors,
6631
+ shouldRegister,
6632
+ queueRegistration,
6633
+ contentEnabled,
6634
+ contentDiscovery,
6635
+ queueContentRegistration
6636
+ ]
4777
6637
  );
4778
6638
  const handleMutations = react.useCallback(
4779
6639
  (mutations) => {
@@ -4791,6 +6651,22 @@ function useAutoRegister(options = {}) {
4791
6651
  queueRegistration(descendant);
4792
6652
  }
4793
6653
  });
6654
+ if (contentEnabled) {
6655
+ const contentSelectors = [
6656
+ ...CONTENT_SELECTORS,
6657
+ ...contentDiscovery?.includeContentSelectors || []
6658
+ ].join(", ");
6659
+ const registeredIds = new Set(registeredContentElementsRef.current.values());
6660
+ if (shouldRegisterContent(element, contentDiscovery, registeredIds)) {
6661
+ queueContentRegistration(element);
6662
+ }
6663
+ const contentDescendants = element.querySelectorAll(contentSelectors);
6664
+ contentDescendants.forEach((descendant) => {
6665
+ if (shouldRegisterContent(descendant, contentDiscovery, registeredIds)) {
6666
+ queueContentRegistration(descendant);
6667
+ }
6668
+ });
6669
+ }
4794
6670
  }
4795
6671
  });
4796
6672
  mutation.removedNodes.forEach((node) => {
@@ -4799,17 +6675,32 @@ function useAutoRegister(options = {}) {
4799
6675
  if (registeredElementsRef.current.has(element)) {
4800
6676
  unregisterElement(element);
4801
6677
  }
6678
+ if (registeredContentElementsRef.current.has(element)) {
6679
+ unregisterContentElement(element);
6680
+ }
4802
6681
  const descendants = element.querySelectorAll("*");
4803
6682
  descendants.forEach((descendant) => {
4804
6683
  if (registeredElementsRef.current.has(descendant)) {
4805
6684
  unregisterElement(descendant);
4806
6685
  }
6686
+ if (registeredContentElementsRef.current.has(descendant)) {
6687
+ unregisterContentElement(descendant);
6688
+ }
4807
6689
  });
4808
6690
  }
4809
6691
  });
4810
6692
  });
4811
6693
  },
4812
- [shouldRegister, queueRegistration, unregisterElement, includeSelectors]
6694
+ [
6695
+ shouldRegister,
6696
+ queueRegistration,
6697
+ unregisterElement,
6698
+ includeSelectors,
6699
+ contentEnabled,
6700
+ contentDiscovery,
6701
+ queueContentRegistration,
6702
+ unregisterContentElement
6703
+ ]
4813
6704
  );
4814
6705
  react.useEffect(() => {
4815
6706
  if (!enabled || !bridge?.registry) return;
@@ -4825,10 +6716,17 @@ function useAutoRegister(options = {}) {
4825
6716
  if (debounceTimeoutRef.current) {
4826
6717
  clearTimeout(debounceTimeoutRef.current);
4827
6718
  }
6719
+ if (contentDebounceTimeoutRef.current) {
6720
+ clearTimeout(contentDebounceTimeoutRef.current);
6721
+ }
4828
6722
  registeredElementsRef.current.forEach((id, _element) => {
4829
6723
  bridge.registry.unregisterElement(id);
4830
6724
  });
4831
6725
  registeredElementsRef.current.clear();
6726
+ registeredContentElementsRef.current.forEach((id, _element) => {
6727
+ bridge.registry.unregisterElement(id);
6728
+ });
6729
+ registeredContentElementsRef.current.clear();
4832
6730
  };
4833
6731
  }, [enabled, bridge, root, scanAndRegister, handleMutations]);
4834
6732
  }
@@ -4843,7 +6741,8 @@ function AutoRegisterProvider({
4843
6741
  excludeSelectors = [],
4844
6742
  generateId: generateId4,
4845
6743
  onRegister,
4846
- onUnregister
6744
+ onUnregister,
6745
+ contentDiscovery
4847
6746
  }) {
4848
6747
  const containerRef = react.useRef(null);
4849
6748
  useAutoRegister({
@@ -4856,7 +6755,8 @@ function AutoRegisterProvider({
4856
6755
  excludeSelectors,
4857
6756
  generateId: generateId4,
4858
6757
  onRegister,
4859
- onUnregister
6758
+ onUnregister,
6759
+ contentDiscovery
4860
6760
  });
4861
6761
  if (scopeToChildren) {
4862
6762
  return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, style: { display: "contents" }, children });
@@ -4864,6 +6764,253 @@ function AutoRegisterProvider({
4864
6764
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
4865
6765
  }
4866
6766
 
6767
+ // src/annotations/types.ts
6768
+ var ANNOTATION_CONFIG_VERSION = "1.0.0";
6769
+
6770
+ // src/annotations/store.ts
6771
+ var AnnotationStore = class {
6772
+ constructor() {
6773
+ this.store = /* @__PURE__ */ new Map();
6774
+ this.listeners = /* @__PURE__ */ new Set();
6775
+ }
6776
+ /**
6777
+ * Get an annotation by element ID.
6778
+ */
6779
+ get(elementId) {
6780
+ return this.store.get(elementId);
6781
+ }
6782
+ /**
6783
+ * Get all annotations as a record.
6784
+ */
6785
+ getAll() {
6786
+ const result = {};
6787
+ for (const [id, annotation] of this.store) {
6788
+ result[id] = annotation;
6789
+ }
6790
+ return result;
6791
+ }
6792
+ /**
6793
+ * Set an annotation for an element. Auto-sets `updatedAt`.
6794
+ */
6795
+ set(elementId, annotation) {
6796
+ const updated = {
6797
+ ...annotation,
6798
+ updatedAt: Date.now()
6799
+ };
6800
+ this.store.set(elementId, updated);
6801
+ this.emit({
6802
+ type: "annotation:set",
6803
+ elementId,
6804
+ annotation: updated,
6805
+ timestamp: Date.now()
6806
+ });
6807
+ }
6808
+ /**
6809
+ * Delete an annotation by element ID.
6810
+ *
6811
+ * @returns true if the annotation existed and was deleted
6812
+ */
6813
+ delete(elementId) {
6814
+ const existed = this.store.delete(elementId);
6815
+ if (existed) {
6816
+ this.emit({
6817
+ type: "annotation:deleted",
6818
+ elementId,
6819
+ timestamp: Date.now()
6820
+ });
6821
+ }
6822
+ return existed;
6823
+ }
6824
+ /**
6825
+ * Check if an annotation exists for an element.
6826
+ */
6827
+ has(elementId) {
6828
+ return this.store.has(elementId);
6829
+ }
6830
+ /**
6831
+ * Get the number of stored annotations.
6832
+ */
6833
+ get count() {
6834
+ return this.store.size;
6835
+ }
6836
+ /**
6837
+ * Clear all annotations.
6838
+ */
6839
+ clear() {
6840
+ this.store.clear();
6841
+ this.emit({
6842
+ type: "annotation:cleared",
6843
+ timestamp: Date.now()
6844
+ });
6845
+ }
6846
+ /**
6847
+ * Import annotations from a config object.
6848
+ *
6849
+ * Merges with existing annotations (new values overwrite per element ID).
6850
+ *
6851
+ * @returns Number of annotations imported
6852
+ *
6853
+ * @example
6854
+ * ```ts
6855
+ * const config: AnnotationConfig = {
6856
+ * version: '1.0.0',
6857
+ * annotations: {
6858
+ * 'btn-1': { description: 'Submit button', tags: ['form'] },
6859
+ * 'input-1': { description: 'Name field' },
6860
+ * },
6861
+ * };
6862
+ * const count = store.importConfig(config); // 2
6863
+ * ```
6864
+ */
6865
+ importConfig(config) {
6866
+ let count = 0;
6867
+ for (const [id, annotation] of Object.entries(config.annotations)) {
6868
+ this.store.set(id, {
6869
+ ...annotation,
6870
+ updatedAt: annotation.updatedAt ?? Date.now()
6871
+ });
6872
+ count++;
6873
+ }
6874
+ this.emit({
6875
+ type: "annotation:imported",
6876
+ count,
6877
+ timestamp: Date.now()
6878
+ });
6879
+ return count;
6880
+ }
6881
+ /**
6882
+ * Export all annotations as a config object.
6883
+ *
6884
+ * The returned object can be serialized to JSON and saved to a file,
6885
+ * then later re-imported with {@link importConfig}.
6886
+ *
6887
+ * @param metadata - Optional metadata to include (appName, description, etc.)
6888
+ * @returns AnnotationConfig with all current annotations
6889
+ *
6890
+ * @example
6891
+ * ```ts
6892
+ * const config = store.exportConfig({ appName: 'MyApp' });
6893
+ * // config.version === '1.0.0'
6894
+ * // config.annotations === { 'btn-1': { ... }, 'input-1': { ... } }
6895
+ * // config.metadata === { appName: 'MyApp', exportedAt: 1706900000000 }
6896
+ *
6897
+ * // Save to file
6898
+ * fs.writeFileSync('annotations.json', JSON.stringify(config, null, 2));
6899
+ * ```
6900
+ */
6901
+ exportConfig(metadata) {
6902
+ return {
6903
+ version: ANNOTATION_CONFIG_VERSION,
6904
+ annotations: this.getAll(),
6905
+ metadata: {
6906
+ ...metadata,
6907
+ exportedAt: Date.now()
6908
+ }
6909
+ };
6910
+ }
6911
+ /**
6912
+ * Compute annotation coverage against a set of known element IDs.
6913
+ *
6914
+ * Compares the store's annotations against the provided list of element IDs
6915
+ * to determine what percentage of elements have been annotated.
6916
+ *
6917
+ * @param allElementIds - Array of all known element IDs in the UI
6918
+ * @returns Coverage statistics including percentages and lists of annotated/unannotated IDs
6919
+ *
6920
+ * @example
6921
+ * ```ts
6922
+ * store.set('btn-1', { description: 'Submit' });
6923
+ * store.set('input-1', { description: 'Name' });
6924
+ *
6925
+ * const coverage = store.getCoverage(['btn-1', 'input-1', 'input-2', 'link-1']);
6926
+ * // coverage.totalElements === 4
6927
+ * // coverage.annotatedElements === 2
6928
+ * // coverage.coveragePercent === 50
6929
+ * // coverage.annotatedIds === ['btn-1', 'input-1']
6930
+ * // coverage.unannotatedIds === ['input-2', 'link-1']
6931
+ * ```
6932
+ */
6933
+ getCoverage(allElementIds) {
6934
+ const annotatedIds = [];
6935
+ const unannotatedIds = [];
6936
+ for (const id of allElementIds) {
6937
+ if (this.store.has(id)) {
6938
+ annotatedIds.push(id);
6939
+ } else {
6940
+ unannotatedIds.push(id);
6941
+ }
6942
+ }
6943
+ const total = allElementIds.length;
6944
+ return {
6945
+ totalElements: total,
6946
+ annotatedElements: annotatedIds.length,
6947
+ coveragePercent: total > 0 ? annotatedIds.length / total * 100 : 0,
6948
+ annotatedIds,
6949
+ unannotatedIds,
6950
+ timestamp: Date.now()
6951
+ };
6952
+ }
6953
+ /**
6954
+ * Subscribe to annotation events.
6955
+ *
6956
+ * The listener is called whenever annotations are set, deleted, imported,
6957
+ * or cleared. Returns an unsubscribe function to stop listening.
6958
+ *
6959
+ * @param listener - Callback function receiving {@link AnnotationEvent} objects
6960
+ * @returns Unsubscribe function - call it to remove the listener
6961
+ *
6962
+ * @example
6963
+ * ```ts
6964
+ * const unsubscribe = store.on((event) => {
6965
+ * if (event.type === 'annotation:set') {
6966
+ * console.log(`Element ${event.elementId} annotated:`, event.annotation);
6967
+ * }
6968
+ * });
6969
+ *
6970
+ * store.set('btn-1', { description: 'Submit' });
6971
+ * // Logs: "Element btn-1 annotated: { description: 'Submit', updatedAt: ... }"
6972
+ *
6973
+ * unsubscribe(); // Stop listening
6974
+ * ```
6975
+ */
6976
+ on(listener) {
6977
+ this.listeners.add(listener);
6978
+ return () => {
6979
+ this.listeners.delete(listener);
6980
+ };
6981
+ }
6982
+ /**
6983
+ * Emit an event to all listeners.
6984
+ */
6985
+ emit(event) {
6986
+ for (const listener of this.listeners) {
6987
+ try {
6988
+ listener(event);
6989
+ } catch {
6990
+ }
6991
+ }
6992
+ }
6993
+ };
6994
+ var globalStore2 = null;
6995
+ function getGlobalAnnotationStore() {
6996
+ if (!globalStore2) {
6997
+ globalStore2 = new AnnotationStore();
6998
+ }
6999
+ return globalStore2;
7000
+ }
7001
+
7002
+ // src/react/useUIAnnotation.ts
7003
+ function useUIAnnotation(elementId, annotation) {
7004
+ const serializedRef = react.useRef("");
7005
+ react.useEffect(() => {
7006
+ const serialized = JSON.stringify(annotation);
7007
+ if (serialized !== serializedRef.current) {
7008
+ serializedRef.current = serialized;
7009
+ getGlobalAnnotationStore().set(elementId, annotation);
7010
+ }
7011
+ }, [elementId, annotation]);
7012
+ }
7013
+
4867
7014
  exports.AutoRegisterProvider = AutoRegisterProvider;
4868
7015
  exports.UIBridgeProvider = UIBridgeProvider;
4869
7016
  exports.useActiveStates = useActiveStates;
@@ -4873,6 +7020,7 @@ exports.useCanNavigateTo = useCanNavigateTo;
4873
7020
  exports.useNavigationPath = useNavigationPath;
4874
7021
  exports.useStateSnapshot = useStateSnapshot;
4875
7022
  exports.useTransitions = useTransitions;
7023
+ exports.useUIAnnotation = useUIAnnotation;
4876
7024
  exports.useUIBridge = useUIBridge;
4877
7025
  exports.useUIBridgeContext = useUIBridgeContext;
4878
7026
  exports.useUIBridgeOptional = useUIBridgeOptional;