@qontinui/ui-bridge 0.3.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 (127) 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 +23 -34
  16. package/dist/babel-plugin/index.js.map +1 -1
  17. package/dist/babel-plugin/index.mjs +23 -34
  18. package/dist/babel-plugin/index.mjs.map +1 -1
  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 +2 -2
  28. package/dist/core/index.d.ts +2 -2
  29. package/dist/core/index.js.map +1 -1
  30. package/dist/core/index.mjs.map +1 -1
  31. package/dist/debug/index.d.mts +5 -3
  32. package/dist/debug/index.d.ts +5 -3
  33. package/dist/debug/index.js +925 -1
  34. package/dist/debug/index.js.map +1 -1
  35. package/dist/debug/index.mjs +924 -2
  36. package/dist/debug/index.mjs.map +1 -1
  37. package/dist/index.d.mts +12 -7
  38. package/dist/index.d.ts +12 -7
  39. package/dist/index.js +4720 -173
  40. package/dist/index.js.map +1 -1
  41. package/dist/index.mjs +4656 -174
  42. package/dist/index.mjs.map +1 -1
  43. package/dist/{metrics-DTA2bwG7.d.mts → metrics-DuA2qIIz.d.mts} +2 -2
  44. package/dist/{metrics-BfiT_rhZ.d.ts → metrics-KFAAKNEB.d.ts} +2 -2
  45. package/dist/native/control/index.js +2 -7
  46. package/dist/native/control/index.js.map +1 -1
  47. package/dist/native/control/index.mjs +2 -7
  48. package/dist/native/control/index.mjs.map +1 -1
  49. package/dist/native/core/index.js.map +1 -1
  50. package/dist/native/core/index.mjs.map +1 -1
  51. package/dist/native/debug/index.js +23 -66
  52. package/dist/native/debug/index.js.map +1 -1
  53. package/dist/native/debug/index.mjs +23 -66
  54. package/dist/native/debug/index.mjs.map +1 -1
  55. package/dist/native/index.js +89 -131
  56. package/dist/native/index.js.map +1 -1
  57. package/dist/native/index.mjs +89 -131
  58. package/dist/native/index.mjs.map +1 -1
  59. package/dist/native/react/index.js +28 -52
  60. package/dist/native/react/index.js.map +1 -1
  61. package/dist/native/react/index.mjs +28 -52
  62. package/dist/native/react/index.mjs.map +1 -1
  63. package/dist/native/server/index.js +38 -13
  64. package/dist/native/server/index.js.map +1 -1
  65. package/dist/native/server/index.mjs +38 -13
  66. package/dist/native/server/index.mjs.map +1 -1
  67. package/dist/react/index.d.mts +107 -8
  68. package/dist/react/index.d.ts +107 -8
  69. package/dist/react/index.js +2194 -84
  70. package/dist/react/index.js.map +1 -1
  71. package/dist/react/index.mjs +2194 -85
  72. package/dist/react/index.mjs.map +1 -1
  73. package/dist/{registry-BKLEm-yk.d.ts → registry-C6dDtn1v.d.ts} +27 -2
  74. package/dist/{registry-BmZgyCz8.d.mts → registry-POtcxnal.d.mts} +27 -2
  75. package/dist/render-log/index.d.mts +1 -1
  76. package/dist/render-log/index.d.ts +1 -1
  77. package/dist/server/express.d.mts +5 -4
  78. package/dist/server/express.d.ts +5 -4
  79. package/dist/server/express.js +104 -2
  80. package/dist/server/express.js.map +1 -1
  81. package/dist/server/express.mjs +104 -2
  82. package/dist/server/express.mjs.map +1 -1
  83. package/dist/server/handlers.d.mts +36 -5
  84. package/dist/server/handlers.d.ts +36 -5
  85. package/dist/server/handlers.js +3129 -224
  86. package/dist/server/handlers.js.map +1 -1
  87. package/dist/server/handlers.mjs +3129 -224
  88. package/dist/server/handlers.mjs.map +1 -1
  89. package/dist/server/index.d.mts +7 -5
  90. package/dist/server/index.d.ts +7 -5
  91. package/dist/server/index.js +3215 -183
  92. package/dist/server/index.js.map +1 -1
  93. package/dist/server/index.mjs +3215 -183
  94. package/dist/server/index.mjs.map +1 -1
  95. package/dist/server/nextjs.d.mts +6 -4
  96. package/dist/server/nextjs.d.ts +6 -4
  97. package/dist/server/nextjs.js +106 -3
  98. package/dist/server/nextjs.js.map +1 -1
  99. package/dist/server/nextjs.mjs +106 -3
  100. package/dist/server/nextjs.mjs.map +1 -1
  101. package/dist/server/standalone.d.mts +6 -5
  102. package/dist/server/standalone.d.ts +6 -5
  103. package/dist/server/standalone.js +131 -5
  104. package/dist/server/standalone.js.map +1 -1
  105. package/dist/server/standalone.mjs +131 -5
  106. package/dist/server/standalone.mjs.map +1 -1
  107. package/dist/specs/index.d.mts +365 -0
  108. package/dist/specs/index.d.ts +365 -0
  109. package/dist/specs/index.js +2809 -0
  110. package/dist/specs/index.js.map +1 -0
  111. package/dist/specs/index.mjs +2786 -0
  112. package/dist/specs/index.mjs.map +1 -0
  113. package/dist/{standalone-BURj8J3G.d.ts → standalone-B6GLIEmR.d.ts} +6 -2
  114. package/dist/{standalone-Dwmel29d.d.mts → standalone-CjdYqj3P.d.mts} +6 -2
  115. package/dist/{types-CHnlwiTK.d.ts → types-B2EfvEaq.d.ts} +83 -3
  116. package/dist/{types-B7J7noLK.d.mts → types-C7gVYRnF.d.ts} +72 -2
  117. package/dist/{types-BkNRILUa.d.ts → types-CJGrBEhC.d.mts} +72 -2
  118. package/dist/types-CebMQj76.d.ts +1275 -0
  119. package/dist/types-D_ypYl3T.d.mts +1275 -0
  120. package/dist/types-UBtp7R0u.d.mts +132 -0
  121. package/dist/types-UBtp7R0u.d.ts +132 -0
  122. package/dist/{types-CEQLnFMv.d.mts → types-gO696T_t.d.mts} +83 -3
  123. package/dist/{types-jKVgTI6_.d.mts → types-suaYwWWg.d.mts} +173 -2
  124. package/dist/{types-jKVgTI6_.d.ts → types-suaYwWWg.d.ts} +173 -2
  125. package/package.json +18 -2
  126. package/dist/types-B5Q0GVo0.d.mts +0 -646
  127. package/dist/types-DfPqwU-i.d.ts +0 -646
@@ -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
  }
@@ -1471,8 +1501,56 @@ var UIBridgeRegistry = class {
1471
1501
  identifier: el.getIdentifier(),
1472
1502
  state: el.getState(),
1473
1503
  actions: el.actions,
1474
- 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
1475
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,
1476
1554
  components: this.getAllComponents().map((comp) => ({
1477
1555
  id: comp.id,
1478
1556
  name: comp.name,
@@ -1997,6 +2075,10 @@ function getElementState2(element) {
1997
2075
  pointerEvents: style.pointerEvents
1998
2076
  }
1999
2077
  };
2078
+ const rawText = element.textContent?.trim();
2079
+ if (rawText) {
2080
+ state.textContent = rawText.replace(/\s+/g, " ").slice(0, 500);
2081
+ }
2000
2082
  if (element instanceof HTMLInputElement) {
2001
2083
  state.value = element.value;
2002
2084
  if (element.type === "checkbox" || element.type === "radio") {
@@ -2038,9 +2120,25 @@ function createMouseEvent(type, element, options) {
2038
2120
  clientY: rect.top + y
2039
2121
  });
2040
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
+ }
2041
2138
  var DefaultActionExecutor = class {
2042
- constructor(registry) {
2139
+ constructor(registry, consoleCapture) {
2043
2140
  this.registry = registry;
2141
+ this.consoleCapture = consoleCapture;
2044
2142
  }
2045
2143
  /**
2046
2144
  * Execute an action on an element
@@ -2077,11 +2175,19 @@ var DefaultActionExecutor = class {
2077
2175
  };
2078
2176
  }
2079
2177
  }
2178
+ const actionStartTime = Date.now();
2080
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
+ }
2081
2186
  return {
2082
2187
  success: true,
2083
2188
  elementState: getElementState2(element),
2084
2189
  result,
2190
+ consoleErrors,
2085
2191
  durationMs: performance.now() - startTime,
2086
2192
  timestamp: Date.now(),
2087
2193
  requestId: request.requestId,
@@ -2173,47 +2279,76 @@ var DefaultActionExecutor = class {
2173
2279
  const rootEl = document.querySelector(options.root);
2174
2280
  if (rootEl) root = rootEl;
2175
2281
  }
2176
- const interactiveSelectors = [
2177
- "a[href]",
2178
- "button",
2179
- "input",
2180
- "select",
2181
- "textarea",
2182
- "[onclick]",
2183
- '[role="button"]',
2184
- '[role="link"]',
2185
- '[role="checkbox"]',
2186
- '[role="radio"]',
2187
- '[role="menuitem"]',
2188
- '[role="tab"]',
2189
- '[role="switch"]',
2190
- '[tabindex]:not([tabindex="-1"])',
2191
- '[contenteditable="true"]',
2192
- "[data-ui-id]",
2193
- "[data-testid]"
2194
- ];
2195
- const selector = options?.selector || interactiveSelectors.join(", ");
2196
- const foundElements = root.querySelectorAll(selector);
2197
- for (const el of foundElements) {
2198
- if (options?.limit && elements.length >= options.limit) break;
2199
- const state = getElementState2(el);
2200
- if (!options?.includeHidden && !state.visible) continue;
2201
- if (options?.types) {
2202
- const type = this.inferElementType(el);
2203
- if (!options.types.includes(type)) continue;
2204
- }
2205
- const registered = this.registry.findByDOMElement(el);
2206
- elements.push({
2207
- id: registered?.id || this.getElementId(el),
2208
- type: registered?.type || this.inferElementType(el),
2209
- label: registered?.label || this.getElementLabel(el),
2210
- tagName: el.tagName.toLowerCase(),
2211
- role: el.getAttribute("role") || void 0,
2212
- accessibleName: this.getAccessibleName(el),
2213
- actions: registered?.actions || this.inferActions(el),
2214
- state,
2215
- registered: !!registered
2216
- });
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
+ }
2217
2352
  }
2218
2353
  return {
2219
2354
  elements,
@@ -2243,7 +2378,9 @@ var DefaultActionExecutor = class {
2243
2378
  type: el.type,
2244
2379
  label: el.label,
2245
2380
  actions: [...el.actions, ...el.customActions ? Object.keys(el.customActions) : []],
2246
- state: el.getState()
2381
+ state: el.getState(),
2382
+ category: el.category,
2383
+ contentMetadata: el.contentMetadata
2247
2384
  })),
2248
2385
  components: components.map((comp) => ({
2249
2386
  id: comp.id,
@@ -2327,6 +2464,14 @@ var DefaultActionExecutor = class {
2327
2464
  return this.performCheck(element, false);
2328
2465
  case "toggle":
2329
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);
2330
2475
  default: {
2331
2476
  const registered = this.registry.findByDOMElement(element);
2332
2477
  if (registered?.customActions?.[action]) {
@@ -2356,15 +2501,26 @@ var DefaultActionExecutor = class {
2356
2501
  if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
2357
2502
  throw new Error("Type action requires an input or textarea element");
2358
2503
  }
2504
+ const proto = element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
2505
+ const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
2359
2506
  element.focus();
2360
2507
  if (options?.clear) {
2361
- element.value = "";
2508
+ if (nativeSetter) {
2509
+ nativeSetter.call(element, "");
2510
+ } else {
2511
+ element.value = "";
2512
+ }
2362
2513
  element.dispatchEvent(new Event("input", { bubbles: true }));
2363
2514
  }
2364
2515
  const text = options?.text || "";
2365
2516
  const delay = options?.delay || 0;
2366
2517
  for (const char of text) {
2367
- 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
+ }
2368
2524
  if (options?.triggerEvents !== false) {
2369
2525
  element.dispatchEvent(new Event("input", { bubbles: true }));
2370
2526
  }
@@ -2378,7 +2534,13 @@ var DefaultActionExecutor = class {
2378
2534
  }
2379
2535
  performClear(element) {
2380
2536
  if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
2381
- 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
+ }
2382
2544
  element.dispatchEvent(new Event("input", { bubbles: true }));
2383
2545
  element.dispatchEvent(new Event("change", { bubbles: true }));
2384
2546
  }
@@ -2460,6 +2622,150 @@ var DefaultActionExecutor = class {
2460
2622
  element.dispatchEvent(new Event("change", { bubbles: true }));
2461
2623
  }
2462
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
+ }
2463
2769
  getElementId(element) {
2464
2770
  return element.getAttribute("data-ui-id") || element.getAttribute("data-testid") || element.id || `${element.tagName.toLowerCase()}-${Math.random().toString(36).substr(2, 8)}`;
2465
2771
  }
@@ -2555,8 +2861,442 @@ var DefaultActionExecutor = class {
2555
2861
  }
2556
2862
  }
2557
2863
  };
2558
- function createActionExecutor(registry) {
2559
- 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;
2560
3300
  }
2561
3301
 
2562
3302
  // src/control/workflow-engine.ts
@@ -3588,34 +4328,690 @@ var MetricsCollector = class {
3588
4328
  function createMetricsCollector(options) {
3589
4329
  return new MetricsCollector(options);
3590
4330
  }
3591
- var UIBridgeContext = react.createContext(null);
3592
- function UIBridgeProvider({
3593
- children,
3594
- features = {},
3595
- config = {},
3596
- onEvent
3597
- }) {
3598
- const registryRef = react.useRef(null);
3599
- const renderLogRef = react.useRef(null);
3600
- const metricsRef = react.useRef(null);
3601
- const wsClientRef = react.useRef(null);
3602
- const [wsConnectionState, setWsConnectionState] = react.useState("disconnected");
3603
- if (!registryRef.current) {
3604
- registryRef.current = new UIBridgeRegistry({
3605
- verbose: config.verbose,
3606
- onEvent
3607
- });
3608
- setGlobalRegistry(registryRef.current);
3609
- if (features.renderLog) {
3610
- renderLogRef.current = createRenderLogManager({
3611
- maxEntries: config.maxLogEntries
3612
- });
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);
3613
4357
  }
3614
- if (features.debug) {
3615
- metricsRef.current = createMetricsCollector();
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);
3616
4399
  }
3617
- if (config.websocket) {
3618
- const wsPort = config.websocketPort || config.serverPort || 9876;
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;
3619
5015
  const wsUrl = `ws://localhost:${wsPort}`;
3620
5016
  wsClientRef.current = createWSClient({
3621
5017
  url: wsUrl,
@@ -3625,12 +5021,27 @@ function UIBridgeProvider({
3625
5021
  pingInterval: 3e4
3626
5022
  });
3627
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
+ }
3628
5035
  }
3629
5036
  const registry = registryRef.current;
3630
5037
  const renderLog = renderLogRef.current || void 0;
3631
5038
  const metrics = metricsRef.current || void 0;
3632
5039
  const wsClient = wsClientRef.current || void 0;
3633
- 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
+ );
3634
5045
  const workflowEngine = react.useMemo(
3635
5046
  () => createWorkflowEngine(registry, executor),
3636
5047
  [registry, executor]
@@ -3657,13 +5068,21 @@ function UIBridgeProvider({
3657
5068
  react.useEffect(() => {
3658
5069
  if (!wsClient) return;
3659
5070
  const unsubscribe = wsClient.onConnectionChange((state) => {
5071
+ const prev = prevWsStateRef.current;
5072
+ prevWsStateRef.current = state;
3660
5073
  setWsConnectionState(state);
5074
+ browserCaptureRef.current?.reportWsStateChange(prev, state);
3661
5075
  });
3662
5076
  return unsubscribe;
3663
5077
  }, [wsClient]);
5078
+ react.useEffect(() => {
5079
+ browserCaptureRef.current?.setOnEvent(onBrowserEvent ?? null);
5080
+ }, [onBrowserEvent]);
3664
5081
  react.useEffect(() => {
3665
5082
  return () => {
3666
5083
  renderLog?.stop();
5084
+ browserCaptureRef.current?.setOnEvent(null);
5085
+ browserCaptureRef.current?.uninstall();
3667
5086
  wsClient?.disconnect();
3668
5087
  resetGlobalRegistry();
3669
5088
  };
@@ -3698,6 +5117,10 @@ function UIBridgeProvider({
3698
5117
  const getElements = react.useCallback(() => registry.getAllElements(), [registry]);
3699
5118
  const getComponents = react.useCallback(() => registry.getAllComponents(), [registry]);
3700
5119
  const createSnapshot = react.useCallback(() => registry.createSnapshot(), [registry]);
5120
+ const createSnapshotAsync = react.useCallback(
5121
+ (batchSize) => registry.createSnapshotAsync(batchSize),
5122
+ [registry]
5123
+ );
3701
5124
  const on = react.useCallback(
3702
5125
  (type, listener) => registry.on(type, listener),
3703
5126
  [registry]
@@ -3720,6 +5143,7 @@ function UIBridgeProvider({
3720
5143
  getElements,
3721
5144
  getComponents,
3722
5145
  createSnapshot,
5146
+ createSnapshotAsync,
3723
5147
  on,
3724
5148
  off,
3725
5149
  initialized: true,
@@ -3741,6 +5165,7 @@ function UIBridgeProvider({
3741
5165
  getElements,
3742
5166
  getComponents,
3743
5167
  createSnapshot,
5168
+ createSnapshotAsync,
3744
5169
  on,
3745
5170
  off,
3746
5171
  wsConnect,
@@ -3980,6 +5405,20 @@ function useUIBridge() {
3980
5405
  }
3981
5406
  return context.createSnapshot();
3982
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
+ );
3983
5422
  const executeAction = react.useCallback(
3984
5423
  async (elementId, request) => {
3985
5424
  if (!context) {
@@ -4101,6 +5540,7 @@ function useUIBridge() {
4101
5540
  components,
4102
5541
  workflows,
4103
5542
  createSnapshot,
5543
+ createSnapshotAsync,
4104
5544
  executeAction,
4105
5545
  executeComponentAction,
4106
5546
  find,
@@ -4516,6 +5956,305 @@ function useNavigationPath(targetStates) {
4516
5956
  return bridge.registry.findPath(targetStates);
4517
5957
  }, [bridge, targetStates]);
4518
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
4519
6258
  var INTERACTIVE_SELECTORS2 = [
4520
6259
  "a[href]",
4521
6260
  "button",
@@ -4710,12 +6449,17 @@ function useAutoRegister(options = {}) {
4710
6449
  excludeSelectors = [],
4711
6450
  generateId: customGenerateId,
4712
6451
  onRegister,
4713
- onUnregister
6452
+ onUnregister,
6453
+ contentDiscovery
4714
6454
  } = options;
6455
+ const contentEnabled = contentDiscovery?.enabled !== false;
4715
6456
  const bridge = useUIBridgeOptional();
4716
6457
  const registeredElementsRef = react.useRef(/* @__PURE__ */ new Map());
6458
+ const registeredContentElementsRef = react.useRef(/* @__PURE__ */ new Map());
4717
6459
  const pendingRegistrationsRef = react.useRef(/* @__PURE__ */ new Set());
6460
+ const pendingContentRegistrationsRef = react.useRef(/* @__PURE__ */ new Set());
4718
6461
  const debounceTimeoutRef = react.useRef(null);
6462
+ const contentDebounceTimeoutRef = react.useRef(null);
4719
6463
  const shouldRegister = react.useCallback(
4720
6464
  (element) => {
4721
6465
  if (!includeHidden && !isElementVisible2(element)) {
@@ -4783,6 +6527,41 @@ function useAutoRegister(options = {}) {
4783
6527
  },
4784
6528
  [bridge, onUnregister]
4785
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
+ );
4786
6565
  const processPendingRegistrations = react.useCallback(() => {
4787
6566
  pendingRegistrationsRef.current.forEach((element) => {
4788
6567
  if (shouldRegister(element)) {
@@ -4791,6 +6570,15 @@ function useAutoRegister(options = {}) {
4791
6570
  });
4792
6571
  pendingRegistrationsRef.current.clear();
4793
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]);
4794
6582
  const queueRegistration = react.useCallback(
4795
6583
  (element) => {
4796
6584
  pendingRegistrationsRef.current.add(element);
@@ -4801,6 +6589,20 @@ function useAutoRegister(options = {}) {
4801
6589
  },
4802
6590
  [debounceMs, processPendingRegistrations]
4803
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
+ );
4804
6606
  const scanAndRegister = react.useCallback(
4805
6607
  (rootElement) => {
4806
6608
  const allSelectors = [...INTERACTIVE_SELECTORS2, ...includeSelectors].join(", ");
@@ -4810,8 +6612,28 @@ function useAutoRegister(options = {}) {
4810
6612
  queueRegistration(element);
4811
6613
  }
4812
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
+ }
4813
6628
  },
4814
- [includeSelectors, shouldRegister, queueRegistration]
6629
+ [
6630
+ includeSelectors,
6631
+ shouldRegister,
6632
+ queueRegistration,
6633
+ contentEnabled,
6634
+ contentDiscovery,
6635
+ queueContentRegistration
6636
+ ]
4815
6637
  );
4816
6638
  const handleMutations = react.useCallback(
4817
6639
  (mutations) => {
@@ -4829,6 +6651,22 @@ function useAutoRegister(options = {}) {
4829
6651
  queueRegistration(descendant);
4830
6652
  }
4831
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
+ }
4832
6670
  }
4833
6671
  });
4834
6672
  mutation.removedNodes.forEach((node) => {
@@ -4837,17 +6675,32 @@ function useAutoRegister(options = {}) {
4837
6675
  if (registeredElementsRef.current.has(element)) {
4838
6676
  unregisterElement(element);
4839
6677
  }
6678
+ if (registeredContentElementsRef.current.has(element)) {
6679
+ unregisterContentElement(element);
6680
+ }
4840
6681
  const descendants = element.querySelectorAll("*");
4841
6682
  descendants.forEach((descendant) => {
4842
6683
  if (registeredElementsRef.current.has(descendant)) {
4843
6684
  unregisterElement(descendant);
4844
6685
  }
6686
+ if (registeredContentElementsRef.current.has(descendant)) {
6687
+ unregisterContentElement(descendant);
6688
+ }
4845
6689
  });
4846
6690
  }
4847
6691
  });
4848
6692
  });
4849
6693
  },
4850
- [shouldRegister, queueRegistration, unregisterElement, includeSelectors]
6694
+ [
6695
+ shouldRegister,
6696
+ queueRegistration,
6697
+ unregisterElement,
6698
+ includeSelectors,
6699
+ contentEnabled,
6700
+ contentDiscovery,
6701
+ queueContentRegistration,
6702
+ unregisterContentElement
6703
+ ]
4851
6704
  );
4852
6705
  react.useEffect(() => {
4853
6706
  if (!enabled || !bridge?.registry) return;
@@ -4863,10 +6716,17 @@ function useAutoRegister(options = {}) {
4863
6716
  if (debounceTimeoutRef.current) {
4864
6717
  clearTimeout(debounceTimeoutRef.current);
4865
6718
  }
6719
+ if (contentDebounceTimeoutRef.current) {
6720
+ clearTimeout(contentDebounceTimeoutRef.current);
6721
+ }
4866
6722
  registeredElementsRef.current.forEach((id, _element) => {
4867
6723
  bridge.registry.unregisterElement(id);
4868
6724
  });
4869
6725
  registeredElementsRef.current.clear();
6726
+ registeredContentElementsRef.current.forEach((id, _element) => {
6727
+ bridge.registry.unregisterElement(id);
6728
+ });
6729
+ registeredContentElementsRef.current.clear();
4870
6730
  };
4871
6731
  }, [enabled, bridge, root, scanAndRegister, handleMutations]);
4872
6732
  }
@@ -4881,7 +6741,8 @@ function AutoRegisterProvider({
4881
6741
  excludeSelectors = [],
4882
6742
  generateId: generateId4,
4883
6743
  onRegister,
4884
- onUnregister
6744
+ onUnregister,
6745
+ contentDiscovery
4885
6746
  }) {
4886
6747
  const containerRef = react.useRef(null);
4887
6748
  useAutoRegister({
@@ -4894,7 +6755,8 @@ function AutoRegisterProvider({
4894
6755
  excludeSelectors,
4895
6756
  generateId: generateId4,
4896
6757
  onRegister,
4897
- onUnregister
6758
+ onUnregister,
6759
+ contentDiscovery
4898
6760
  });
4899
6761
  if (scopeToChildren) {
4900
6762
  return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, style: { display: "contents" }, children });
@@ -4902,6 +6764,253 @@ function AutoRegisterProvider({
4902
6764
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
4903
6765
  }
4904
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
+
4905
7014
  exports.AutoRegisterProvider = AutoRegisterProvider;
4906
7015
  exports.UIBridgeProvider = UIBridgeProvider;
4907
7016
  exports.useActiveStates = useActiveStates;
@@ -4911,6 +7020,7 @@ exports.useCanNavigateTo = useCanNavigateTo;
4911
7020
  exports.useNavigationPath = useNavigationPath;
4912
7021
  exports.useStateSnapshot = useStateSnapshot;
4913
7022
  exports.useTransitions = useTransitions;
7023
+ exports.useUIAnnotation = useUIAnnotation;
4914
7024
  exports.useUIBridge = useUIBridge;
4915
7025
  exports.useUIBridgeContext = useUIBridgeContext;
4916
7026
  exports.useUIBridgeOptional = useUIBridgeOptional;