@qontinui/ui-bridge 0.1.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.
- package/dist/control/index.d.mts +134 -0
- package/dist/control/index.d.ts +134 -0
- package/dist/control/index.js +924 -0
- package/dist/control/index.js.map +1 -0
- package/dist/control/index.mjs +919 -0
- package/dist/control/index.mjs.map +1 -0
- package/dist/core/index.d.mts +52 -0
- package/dist/core/index.d.ts +52 -0
- package/dist/core/index.js +1424 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/index.mjs +1409 -0
- package/dist/core/index.mjs.map +1 -0
- package/dist/debug/index.d.mts +93 -0
- package/dist/debug/index.d.ts +93 -0
- package/dist/debug/index.js +673 -0
- package/dist/debug/index.js.map +1 -0
- package/dist/debug/index.mjs +664 -0
- package/dist/debug/index.mjs.map +1 -0
- package/dist/index.d.mts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +4719 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4665 -0
- package/dist/index.mjs.map +1 -0
- package/dist/metrics-BCG7z7Aq.d.mts +147 -0
- package/dist/metrics-QCnK0EFw.d.ts +147 -0
- package/dist/react/index.d.mts +786 -0
- package/dist/react/index.d.ts +786 -0
- package/dist/react/index.js +4312 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +4290 -0
- package/dist/react/index.mjs.map +1 -0
- package/dist/registry-CT6BVVKr.d.mts +253 -0
- package/dist/registry-D4mQ01B3.d.ts +253 -0
- package/dist/render-log/index.d.mts +340 -0
- package/dist/render-log/index.d.ts +340 -0
- package/dist/render-log/index.js +702 -0
- package/dist/render-log/index.js.map +1 -0
- package/dist/render-log/index.mjs +695 -0
- package/dist/render-log/index.mjs.map +1 -0
- package/dist/types-BDkXy5si.d.ts +354 -0
- package/dist/types-BpvpStn3.d.mts +802 -0
- package/dist/types-BpvpStn3.d.ts +802 -0
- package/dist/types-DdJD9yw5.d.mts +354 -0
- package/dist/websocket-client-B2LC9CYc.d.mts +124 -0
- package/dist/websocket-client-DupH0X7B.d.ts +124 -0
- package/package.json +83 -0
|
@@ -0,0 +1,4290 @@
|
|
|
1
|
+
import { createContext, useRef, useState, useMemo, useEffect, useCallback, useContext } from 'react';
|
|
2
|
+
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/react/UIBridgeProvider.tsx
|
|
5
|
+
|
|
6
|
+
// src/core/element-identifier.ts
|
|
7
|
+
function generateXPath(element) {
|
|
8
|
+
if (element.id) {
|
|
9
|
+
return `//*[@id="${element.id}"]`;
|
|
10
|
+
}
|
|
11
|
+
const parts = [];
|
|
12
|
+
let current = element;
|
|
13
|
+
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
14
|
+
let selector = current.nodeName.toLowerCase();
|
|
15
|
+
const uiId = current.getAttribute("data-ui-id");
|
|
16
|
+
if (uiId) {
|
|
17
|
+
selector += `[@data-ui-id="${uiId}"]`;
|
|
18
|
+
parts.unshift(selector);
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
const testId = current.getAttribute("data-testid");
|
|
22
|
+
if (testId) {
|
|
23
|
+
selector += `[@data-testid="${testId}"]`;
|
|
24
|
+
parts.unshift(selector);
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
const id = current.id;
|
|
28
|
+
if (id) {
|
|
29
|
+
selector += `[@id="${id}"]`;
|
|
30
|
+
parts.unshift(selector);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
const parentEl = current.parentElement;
|
|
34
|
+
if (parentEl) {
|
|
35
|
+
const currentEl = current;
|
|
36
|
+
const siblings = Array.from(parentEl.children).filter(
|
|
37
|
+
(child) => child.nodeName === currentEl.nodeName
|
|
38
|
+
);
|
|
39
|
+
if (siblings.length > 1) {
|
|
40
|
+
const index = siblings.indexOf(currentEl) + 1;
|
|
41
|
+
selector += `[${index}]`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
parts.unshift(selector);
|
|
45
|
+
current = parentEl;
|
|
46
|
+
}
|
|
47
|
+
return "/" + parts.join("/");
|
|
48
|
+
}
|
|
49
|
+
function generateCSSSelector(element) {
|
|
50
|
+
const uiId = element.getAttribute("data-ui-id");
|
|
51
|
+
if (uiId) {
|
|
52
|
+
return `[data-ui-id="${uiId}"]`;
|
|
53
|
+
}
|
|
54
|
+
const testId = element.getAttribute("data-testid");
|
|
55
|
+
if (testId) {
|
|
56
|
+
return `[data-testid="${testId}"]`;
|
|
57
|
+
}
|
|
58
|
+
const awasId = element.getAttribute("data-awas-element");
|
|
59
|
+
if (awasId) {
|
|
60
|
+
return `[data-awas-element="${awasId}"]`;
|
|
61
|
+
}
|
|
62
|
+
if (element.id) {
|
|
63
|
+
return `#${CSS.escape(element.id)}`;
|
|
64
|
+
}
|
|
65
|
+
const path = [];
|
|
66
|
+
let current = element;
|
|
67
|
+
while (current && current.nodeType === Node.ELEMENT_NODE) {
|
|
68
|
+
let selector = current.nodeName.toLowerCase();
|
|
69
|
+
const parentUiId = current.getAttribute("data-ui-id");
|
|
70
|
+
if (parentUiId && current !== element) {
|
|
71
|
+
path.unshift(`[data-ui-id="${parentUiId}"]`);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
const parentTestId = current.getAttribute("data-testid");
|
|
75
|
+
if (parentTestId && current !== element) {
|
|
76
|
+
path.unshift(`[data-testid="${parentTestId}"]`);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
if (current.id) {
|
|
80
|
+
path.unshift(`#${CSS.escape(current.id)}`);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
const parentEl = current.parentElement;
|
|
84
|
+
if (parentEl) {
|
|
85
|
+
const currentEl = current;
|
|
86
|
+
const siblings = Array.from(parentEl.children);
|
|
87
|
+
const sameTagSiblings = siblings.filter(
|
|
88
|
+
(s) => s.nodeName === currentEl.nodeName
|
|
89
|
+
);
|
|
90
|
+
if (sameTagSiblings.length > 1) {
|
|
91
|
+
const index = siblings.indexOf(currentEl) + 1;
|
|
92
|
+
selector += `:nth-child(${index})`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
path.unshift(selector);
|
|
96
|
+
current = current.parentElement;
|
|
97
|
+
}
|
|
98
|
+
return path.join(" > ");
|
|
99
|
+
}
|
|
100
|
+
function getBestIdentifier(element) {
|
|
101
|
+
const uiId = element.getAttribute("data-ui-id");
|
|
102
|
+
if (uiId) return uiId;
|
|
103
|
+
const testId = element.getAttribute("data-testid");
|
|
104
|
+
if (testId) return testId;
|
|
105
|
+
const awasId = element.getAttribute("data-awas-element");
|
|
106
|
+
if (awasId) return awasId;
|
|
107
|
+
if (element.id) return element.id;
|
|
108
|
+
return generateCSSSelector(element);
|
|
109
|
+
}
|
|
110
|
+
function createElementIdentifier(element) {
|
|
111
|
+
return {
|
|
112
|
+
uiId: element.getAttribute("data-ui-id") || void 0,
|
|
113
|
+
testId: element.getAttribute("data-testid") || void 0,
|
|
114
|
+
awasId: element.getAttribute("data-awas-element") || void 0,
|
|
115
|
+
htmlId: element.id || void 0,
|
|
116
|
+
xpath: generateXPath(element),
|
|
117
|
+
selector: generateCSSSelector(element)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function findElementByIdentifier(identifier, root = document) {
|
|
121
|
+
if (typeof identifier === "string") {
|
|
122
|
+
const byUiId = root.querySelector(`[data-ui-id="${identifier}"]`);
|
|
123
|
+
if (byUiId) return byUiId;
|
|
124
|
+
const byTestId = root.querySelector(`[data-testid="${identifier}"]`);
|
|
125
|
+
if (byTestId) return byTestId;
|
|
126
|
+
const byAwasId = root.querySelector(`[data-awas-element="${identifier}"]`);
|
|
127
|
+
if (byAwasId) return byAwasId;
|
|
128
|
+
const byId = root.querySelector(`#${CSS.escape(identifier)}`);
|
|
129
|
+
if (byId) return byId;
|
|
130
|
+
try {
|
|
131
|
+
const bySelector = root.querySelector(identifier);
|
|
132
|
+
if (bySelector) return bySelector;
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const result = document.evaluate(
|
|
137
|
+
identifier,
|
|
138
|
+
root,
|
|
139
|
+
null,
|
|
140
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
141
|
+
null
|
|
142
|
+
);
|
|
143
|
+
if (result.singleNodeValue instanceof HTMLElement) {
|
|
144
|
+
return result.singleNodeValue;
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (identifier.uiId) {
|
|
151
|
+
const el = root.querySelector(`[data-ui-id="${identifier.uiId}"]`);
|
|
152
|
+
if (el) return el;
|
|
153
|
+
}
|
|
154
|
+
if (identifier.testId) {
|
|
155
|
+
const el = root.querySelector(`[data-testid="${identifier.testId}"]`);
|
|
156
|
+
if (el) return el;
|
|
157
|
+
}
|
|
158
|
+
if (identifier.awasId) {
|
|
159
|
+
const el = root.querySelector(`[data-awas-element="${identifier.awasId}"]`);
|
|
160
|
+
if (el) return el;
|
|
161
|
+
}
|
|
162
|
+
if (identifier.htmlId) {
|
|
163
|
+
const el = root.querySelector(`#${CSS.escape(identifier.htmlId)}`);
|
|
164
|
+
if (el) return el;
|
|
165
|
+
}
|
|
166
|
+
if (identifier.selector) {
|
|
167
|
+
try {
|
|
168
|
+
const el = root.querySelector(identifier.selector);
|
|
169
|
+
if (el) return el;
|
|
170
|
+
} catch {
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (identifier.xpath) {
|
|
174
|
+
try {
|
|
175
|
+
const result = document.evaluate(
|
|
176
|
+
identifier.xpath,
|
|
177
|
+
root,
|
|
178
|
+
null,
|
|
179
|
+
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
|
180
|
+
null
|
|
181
|
+
);
|
|
182
|
+
if (result.singleNodeValue instanceof HTMLElement) {
|
|
183
|
+
return result.singleNodeValue;
|
|
184
|
+
}
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/core/registry.ts
|
|
192
|
+
function getElementState(element) {
|
|
193
|
+
const rect = element.getBoundingClientRect();
|
|
194
|
+
const computedStyle = window.getComputedStyle(element);
|
|
195
|
+
const state = {
|
|
196
|
+
visible: isElementVisible(element, rect, computedStyle),
|
|
197
|
+
enabled: !isElementDisabled(element),
|
|
198
|
+
focused: document.activeElement === element,
|
|
199
|
+
rect: {
|
|
200
|
+
x: rect.x,
|
|
201
|
+
y: rect.y,
|
|
202
|
+
width: rect.width,
|
|
203
|
+
height: rect.height,
|
|
204
|
+
top: rect.top,
|
|
205
|
+
right: rect.right,
|
|
206
|
+
bottom: rect.bottom,
|
|
207
|
+
left: rect.left
|
|
208
|
+
},
|
|
209
|
+
textContent: element.textContent?.trim() || void 0,
|
|
210
|
+
computedStyles: {
|
|
211
|
+
display: computedStyle.display,
|
|
212
|
+
visibility: computedStyle.visibility,
|
|
213
|
+
opacity: computedStyle.opacity,
|
|
214
|
+
pointerEvents: computedStyle.pointerEvents
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
if (element instanceof HTMLInputElement) {
|
|
218
|
+
state.value = element.value;
|
|
219
|
+
if (element.type === "checkbox" || element.type === "radio") {
|
|
220
|
+
state.checked = element.checked;
|
|
221
|
+
}
|
|
222
|
+
} else if (element instanceof HTMLTextAreaElement) {
|
|
223
|
+
state.value = element.value;
|
|
224
|
+
} else if (element instanceof HTMLSelectElement) {
|
|
225
|
+
state.value = element.value;
|
|
226
|
+
state.selectedOptions = Array.from(element.selectedOptions).map((opt) => opt.value);
|
|
227
|
+
}
|
|
228
|
+
return state;
|
|
229
|
+
}
|
|
230
|
+
function isElementVisible(element, rect, style) {
|
|
231
|
+
if (rect.width === 0 || rect.height === 0) return false;
|
|
232
|
+
if (style.display === "none") return false;
|
|
233
|
+
if (style.visibility === "hidden") return false;
|
|
234
|
+
if (parseFloat(style.opacity) === 0) return false;
|
|
235
|
+
const inViewport = rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
|
|
236
|
+
return inViewport;
|
|
237
|
+
}
|
|
238
|
+
function isElementDisabled(element) {
|
|
239
|
+
if ("disabled" in element && element.disabled) {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
if (element.getAttribute("aria-disabled") === "true") {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
function inferActions(type) {
|
|
248
|
+
const baseActions = ["focus", "blur", "hover"];
|
|
249
|
+
switch (type) {
|
|
250
|
+
case "button":
|
|
251
|
+
return [...baseActions, "click", "doubleClick", "rightClick"];
|
|
252
|
+
case "input":
|
|
253
|
+
return [...baseActions, "click", "type", "clear"];
|
|
254
|
+
case "textarea":
|
|
255
|
+
return [...baseActions, "click", "type", "clear"];
|
|
256
|
+
case "select":
|
|
257
|
+
return [...baseActions, "click", "select"];
|
|
258
|
+
case "checkbox":
|
|
259
|
+
return [...baseActions, "click", "check", "uncheck", "toggle"];
|
|
260
|
+
case "radio":
|
|
261
|
+
return [...baseActions, "click", "check"];
|
|
262
|
+
case "link":
|
|
263
|
+
return [...baseActions, "click"];
|
|
264
|
+
case "form":
|
|
265
|
+
return ["focus", "blur"];
|
|
266
|
+
case "menu":
|
|
267
|
+
case "menuitem":
|
|
268
|
+
return [...baseActions, "click"];
|
|
269
|
+
case "tab":
|
|
270
|
+
return [...baseActions, "click"];
|
|
271
|
+
case "dialog":
|
|
272
|
+
return ["focus", "blur"];
|
|
273
|
+
case "custom":
|
|
274
|
+
default:
|
|
275
|
+
return [...baseActions, "click"];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function inferElementType(element) {
|
|
279
|
+
const tagName = element.tagName.toLowerCase();
|
|
280
|
+
const role = element.getAttribute("role");
|
|
281
|
+
if (role) {
|
|
282
|
+
switch (role) {
|
|
283
|
+
case "button":
|
|
284
|
+
return "button";
|
|
285
|
+
case "textbox":
|
|
286
|
+
return "input";
|
|
287
|
+
case "checkbox":
|
|
288
|
+
return "checkbox";
|
|
289
|
+
case "radio":
|
|
290
|
+
return "radio";
|
|
291
|
+
case "link":
|
|
292
|
+
return "link";
|
|
293
|
+
case "listbox":
|
|
294
|
+
case "combobox":
|
|
295
|
+
return "select";
|
|
296
|
+
case "menu":
|
|
297
|
+
return "menu";
|
|
298
|
+
case "menuitem":
|
|
299
|
+
return "menuitem";
|
|
300
|
+
case "tab":
|
|
301
|
+
return "tab";
|
|
302
|
+
case "dialog":
|
|
303
|
+
return "dialog";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
switch (tagName) {
|
|
307
|
+
case "button":
|
|
308
|
+
return "button";
|
|
309
|
+
case "input": {
|
|
310
|
+
const inputType = element.type;
|
|
311
|
+
if (inputType === "checkbox") return "checkbox";
|
|
312
|
+
if (inputType === "radio") return "radio";
|
|
313
|
+
if (inputType === "submit" || inputType === "button") return "button";
|
|
314
|
+
return "input";
|
|
315
|
+
}
|
|
316
|
+
case "textarea":
|
|
317
|
+
return "textarea";
|
|
318
|
+
case "select":
|
|
319
|
+
return "select";
|
|
320
|
+
case "a":
|
|
321
|
+
return "link";
|
|
322
|
+
case "form":
|
|
323
|
+
return "form";
|
|
324
|
+
default:
|
|
325
|
+
return "custom";
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
var UIBridgeRegistry = class {
|
|
329
|
+
constructor(options = {}) {
|
|
330
|
+
this.elements = /* @__PURE__ */ new Map();
|
|
331
|
+
this.components = /* @__PURE__ */ new Map();
|
|
332
|
+
this.workflows = /* @__PURE__ */ new Map();
|
|
333
|
+
this.eventListeners = /* @__PURE__ */ new Map();
|
|
334
|
+
// State management
|
|
335
|
+
this.states = /* @__PURE__ */ new Map();
|
|
336
|
+
this.stateGroups = /* @__PURE__ */ new Map();
|
|
337
|
+
this.transitions = /* @__PURE__ */ new Map();
|
|
338
|
+
this.activeStates = /* @__PURE__ */ new Set();
|
|
339
|
+
this.options = options;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Emit an event
|
|
343
|
+
*/
|
|
344
|
+
emit(type, data) {
|
|
345
|
+
const event = {
|
|
346
|
+
type,
|
|
347
|
+
timestamp: Date.now(),
|
|
348
|
+
data
|
|
349
|
+
};
|
|
350
|
+
this.options.onEvent?.(event);
|
|
351
|
+
const listeners = this.eventListeners.get(type);
|
|
352
|
+
if (listeners) {
|
|
353
|
+
for (const listener of listeners) {
|
|
354
|
+
try {
|
|
355
|
+
listener(event);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error(`Error in event listener for ${type}:`, error);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (this.options.verbose) {
|
|
362
|
+
console.log("[UIBridge]", type, data);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Register an event listener
|
|
367
|
+
*/
|
|
368
|
+
on(type, listener) {
|
|
369
|
+
if (!this.eventListeners.has(type)) {
|
|
370
|
+
this.eventListeners.set(type, /* @__PURE__ */ new Set());
|
|
371
|
+
}
|
|
372
|
+
this.eventListeners.get(type).add(listener);
|
|
373
|
+
return () => {
|
|
374
|
+
this.eventListeners.get(type)?.delete(listener);
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Remove an event listener
|
|
379
|
+
*/
|
|
380
|
+
off(type, listener) {
|
|
381
|
+
this.eventListeners.get(type)?.delete(listener);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Register an element
|
|
385
|
+
*/
|
|
386
|
+
registerElement(id, element, options = {}) {
|
|
387
|
+
const type = options.type ?? inferElementType(element);
|
|
388
|
+
const actions = options.actions ?? inferActions(type);
|
|
389
|
+
element.setAttribute("data-ui-id", id);
|
|
390
|
+
const registered = {
|
|
391
|
+
id,
|
|
392
|
+
element,
|
|
393
|
+
type,
|
|
394
|
+
label: options.label,
|
|
395
|
+
actions,
|
|
396
|
+
customActions: options.customActions,
|
|
397
|
+
getState: () => getElementState(element),
|
|
398
|
+
getIdentifier: () => createElementIdentifier(element),
|
|
399
|
+
registeredAt: Date.now(),
|
|
400
|
+
mounted: true
|
|
401
|
+
};
|
|
402
|
+
this.elements.set(id, registered);
|
|
403
|
+
this.emit("element:registered", { id, type, label: options.label });
|
|
404
|
+
return registered;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Unregister an element
|
|
408
|
+
*/
|
|
409
|
+
unregisterElement(id) {
|
|
410
|
+
const registered = this.elements.get(id);
|
|
411
|
+
if (registered) {
|
|
412
|
+
registered.mounted = false;
|
|
413
|
+
registered.element.removeAttribute("data-ui-id");
|
|
414
|
+
this.elements.delete(id);
|
|
415
|
+
this.emit("element:unregistered", { id });
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Get a registered element
|
|
422
|
+
*/
|
|
423
|
+
getElement(id) {
|
|
424
|
+
return this.elements.get(id);
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Get all registered elements
|
|
428
|
+
*/
|
|
429
|
+
getAllElements() {
|
|
430
|
+
return Array.from(this.elements.values());
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Find element by DOM element reference
|
|
434
|
+
*/
|
|
435
|
+
findByDOMElement(element) {
|
|
436
|
+
for (const registered of this.elements.values()) {
|
|
437
|
+
if (registered.element === element) {
|
|
438
|
+
return registered;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return void 0;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Register a component
|
|
445
|
+
*/
|
|
446
|
+
registerComponent(id, options) {
|
|
447
|
+
const registered = {
|
|
448
|
+
id,
|
|
449
|
+
name: options.name,
|
|
450
|
+
description: options.description,
|
|
451
|
+
actions: options.actions?.map((a) => ({
|
|
452
|
+
id: a.id,
|
|
453
|
+
label: a.label,
|
|
454
|
+
description: a.description,
|
|
455
|
+
handler: a.handler
|
|
456
|
+
})) ?? [],
|
|
457
|
+
elementIds: options.elementIds,
|
|
458
|
+
registeredAt: Date.now(),
|
|
459
|
+
mounted: true
|
|
460
|
+
};
|
|
461
|
+
this.components.set(id, registered);
|
|
462
|
+
this.emit("component:registered", { id, name: options.name });
|
|
463
|
+
return registered;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Unregister a component
|
|
467
|
+
*/
|
|
468
|
+
unregisterComponent(id) {
|
|
469
|
+
const component = this.components.get(id);
|
|
470
|
+
if (component) {
|
|
471
|
+
component.mounted = false;
|
|
472
|
+
this.components.delete(id);
|
|
473
|
+
this.emit("component:unregistered", { id });
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Get a registered component
|
|
480
|
+
*/
|
|
481
|
+
getComponent(id) {
|
|
482
|
+
return this.components.get(id);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Get all registered components
|
|
486
|
+
*/
|
|
487
|
+
getAllComponents() {
|
|
488
|
+
return Array.from(this.components.values());
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Register a workflow
|
|
492
|
+
*/
|
|
493
|
+
registerWorkflow(workflow) {
|
|
494
|
+
this.workflows.set(workflow.id, workflow);
|
|
495
|
+
return workflow;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Unregister a workflow
|
|
499
|
+
*/
|
|
500
|
+
unregisterWorkflow(id) {
|
|
501
|
+
return this.workflows.delete(id);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Get a workflow
|
|
505
|
+
*/
|
|
506
|
+
getWorkflow(id) {
|
|
507
|
+
return this.workflows.get(id);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Get all workflows
|
|
511
|
+
*/
|
|
512
|
+
getAllWorkflows() {
|
|
513
|
+
return Array.from(this.workflows.values());
|
|
514
|
+
}
|
|
515
|
+
// ==========================================================================
|
|
516
|
+
// State Management
|
|
517
|
+
// ==========================================================================
|
|
518
|
+
/**
|
|
519
|
+
* Register a state
|
|
520
|
+
*/
|
|
521
|
+
registerState(state) {
|
|
522
|
+
this.states.set(state.id, state);
|
|
523
|
+
this.emit("element:registered", { id: state.id, type: "state", name: state.name });
|
|
524
|
+
return state;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Unregister a state
|
|
528
|
+
*/
|
|
529
|
+
unregisterState(id) {
|
|
530
|
+
const state = this.states.get(id);
|
|
531
|
+
if (state) {
|
|
532
|
+
this.activeStates.delete(id);
|
|
533
|
+
this.states.delete(id);
|
|
534
|
+
this.emit("element:unregistered", { id, type: "state" });
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Get a registered state
|
|
541
|
+
*/
|
|
542
|
+
getState(id) {
|
|
543
|
+
return this.states.get(id);
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Get all registered states
|
|
547
|
+
*/
|
|
548
|
+
getAllStates() {
|
|
549
|
+
return Array.from(this.states.values());
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Register a state group
|
|
553
|
+
*/
|
|
554
|
+
registerStateGroup(group) {
|
|
555
|
+
this.stateGroups.set(group.id, group);
|
|
556
|
+
return group;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Unregister a state group
|
|
560
|
+
*/
|
|
561
|
+
unregisterStateGroup(id) {
|
|
562
|
+
return this.stateGroups.delete(id);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Get a state group
|
|
566
|
+
*/
|
|
567
|
+
getStateGroup(id) {
|
|
568
|
+
return this.stateGroups.get(id);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Get all state groups
|
|
572
|
+
*/
|
|
573
|
+
getAllStateGroups() {
|
|
574
|
+
return Array.from(this.stateGroups.values());
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Register a transition
|
|
578
|
+
*/
|
|
579
|
+
registerTransition(transition) {
|
|
580
|
+
this.transitions.set(transition.id, transition);
|
|
581
|
+
return transition;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Unregister a transition
|
|
585
|
+
*/
|
|
586
|
+
unregisterTransition(id) {
|
|
587
|
+
return this.transitions.delete(id);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Get a transition
|
|
591
|
+
*/
|
|
592
|
+
getTransition(id) {
|
|
593
|
+
return this.transitions.get(id);
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Get all transitions
|
|
597
|
+
*/
|
|
598
|
+
getAllTransitions() {
|
|
599
|
+
return Array.from(this.transitions.values());
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Get currently active states
|
|
603
|
+
*/
|
|
604
|
+
getActiveStates() {
|
|
605
|
+
return Array.from(this.activeStates);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Check if a state is active
|
|
609
|
+
*/
|
|
610
|
+
isStateActive(id) {
|
|
611
|
+
return this.activeStates.has(id);
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Activate a state
|
|
615
|
+
*/
|
|
616
|
+
activateState(id) {
|
|
617
|
+
const state = this.states.get(id);
|
|
618
|
+
if (!state) {
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
for (const activeId of this.activeStates) {
|
|
622
|
+
const activeState = this.states.get(activeId);
|
|
623
|
+
if (activeState?.blocking && activeState.id !== id) {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
if (activeState?.blocks?.includes(id)) {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
const wasActive = this.activeStates.has(id);
|
|
631
|
+
this.activeStates.add(id);
|
|
632
|
+
if (!wasActive) {
|
|
633
|
+
this.emit("element:stateChanged", {
|
|
634
|
+
stateId: id,
|
|
635
|
+
active: true,
|
|
636
|
+
activeStates: this.getActiveStates()
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Deactivate a state
|
|
643
|
+
*/
|
|
644
|
+
deactivateState(id) {
|
|
645
|
+
const wasActive = this.activeStates.has(id);
|
|
646
|
+
this.activeStates.delete(id);
|
|
647
|
+
if (wasActive) {
|
|
648
|
+
this.emit("element:stateChanged", {
|
|
649
|
+
stateId: id,
|
|
650
|
+
active: false,
|
|
651
|
+
activeStates: this.getActiveStates()
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
return wasActive;
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* Activate multiple states
|
|
658
|
+
*/
|
|
659
|
+
activateStates(ids) {
|
|
660
|
+
const activated = [];
|
|
661
|
+
for (const id of ids) {
|
|
662
|
+
if (this.activateState(id)) {
|
|
663
|
+
activated.push(id);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return activated;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Deactivate multiple states
|
|
670
|
+
*/
|
|
671
|
+
deactivateStates(ids) {
|
|
672
|
+
const deactivated = [];
|
|
673
|
+
for (const id of ids) {
|
|
674
|
+
if (this.deactivateState(id)) {
|
|
675
|
+
deactivated.push(id);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return deactivated;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Activate a state group (all states in the group)
|
|
682
|
+
*/
|
|
683
|
+
activateStateGroup(groupId) {
|
|
684
|
+
const group = this.stateGroups.get(groupId);
|
|
685
|
+
if (!group) return [];
|
|
686
|
+
return this.activateStates(group.states);
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Deactivate a state group (all states in the group)
|
|
690
|
+
*/
|
|
691
|
+
deactivateStateGroup(groupId) {
|
|
692
|
+
const group = this.stateGroups.get(groupId);
|
|
693
|
+
if (!group) return [];
|
|
694
|
+
return this.deactivateStates(group.states);
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Check if a transition can be executed from current state
|
|
698
|
+
*/
|
|
699
|
+
canExecuteTransition(transitionId) {
|
|
700
|
+
const transition = this.transitions.get(transitionId);
|
|
701
|
+
if (!transition) return false;
|
|
702
|
+
return transition.fromStates.some((stateId) => this.activeStates.has(stateId));
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Execute a transition
|
|
706
|
+
*/
|
|
707
|
+
async executeTransition(transitionId) {
|
|
708
|
+
const startTime = performance.now();
|
|
709
|
+
const transition = this.transitions.get(transitionId);
|
|
710
|
+
if (!transition) {
|
|
711
|
+
return {
|
|
712
|
+
success: false,
|
|
713
|
+
activatedStates: [],
|
|
714
|
+
deactivatedStates: [],
|
|
715
|
+
error: `Transition not found: ${transitionId}`,
|
|
716
|
+
durationMs: performance.now() - startTime
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
if (!this.canExecuteTransition(transitionId)) {
|
|
720
|
+
return {
|
|
721
|
+
success: false,
|
|
722
|
+
activatedStates: [],
|
|
723
|
+
deactivatedStates: [],
|
|
724
|
+
error: "Precondition not met: none of the fromStates are active",
|
|
725
|
+
failedPhase: "precondition",
|
|
726
|
+
durationMs: performance.now() - startTime
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
const deactivated = this.deactivateStates(transition.exitStates);
|
|
731
|
+
if (transition.exitGroups) {
|
|
732
|
+
for (const groupId of transition.exitGroups) {
|
|
733
|
+
deactivated.push(...this.deactivateStateGroup(groupId));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const activated = this.activateStates(transition.activateStates);
|
|
737
|
+
if (transition.activateGroups) {
|
|
738
|
+
for (const groupId of transition.activateGroups) {
|
|
739
|
+
activated.push(...this.activateStateGroup(groupId));
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
success: true,
|
|
744
|
+
activatedStates: activated,
|
|
745
|
+
deactivatedStates: deactivated,
|
|
746
|
+
durationMs: performance.now() - startTime
|
|
747
|
+
};
|
|
748
|
+
} catch (error) {
|
|
749
|
+
return {
|
|
750
|
+
success: false,
|
|
751
|
+
activatedStates: [],
|
|
752
|
+
deactivatedStates: [],
|
|
753
|
+
error: error instanceof Error ? error.message : String(error),
|
|
754
|
+
failedPhase: "execution",
|
|
755
|
+
durationMs: performance.now() - startTime
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Find a path from current state to target states
|
|
761
|
+
*
|
|
762
|
+
* Uses a simple BFS algorithm for pathfinding.
|
|
763
|
+
* For more advanced pathfinding (Dijkstra, A*), use the Python state manager service.
|
|
764
|
+
*/
|
|
765
|
+
findPath(targetStates) {
|
|
766
|
+
if (targetStates.every((t) => this.activeStates.has(t))) {
|
|
767
|
+
return {
|
|
768
|
+
found: true,
|
|
769
|
+
transitions: [],
|
|
770
|
+
totalCost: 0,
|
|
771
|
+
targetStates,
|
|
772
|
+
estimatedSteps: 0
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
const queue = [
|
|
776
|
+
{ activeStates: new Set(this.activeStates), path: [], cost: 0 }
|
|
777
|
+
];
|
|
778
|
+
const visited = /* @__PURE__ */ new Set();
|
|
779
|
+
while (queue.length > 0) {
|
|
780
|
+
const current = queue.shift();
|
|
781
|
+
const stateKey = Array.from(current.activeStates).sort().join(",");
|
|
782
|
+
if (visited.has(stateKey)) continue;
|
|
783
|
+
visited.add(stateKey);
|
|
784
|
+
if (targetStates.every((t) => current.activeStates.has(t))) {
|
|
785
|
+
return {
|
|
786
|
+
found: true,
|
|
787
|
+
transitions: current.path,
|
|
788
|
+
totalCost: current.cost,
|
|
789
|
+
targetStates,
|
|
790
|
+
estimatedSteps: current.path.length
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
for (const transition of this.transitions.values()) {
|
|
794
|
+
const canExecute = transition.fromStates.some((s) => current.activeStates.has(s));
|
|
795
|
+
if (!canExecute) continue;
|
|
796
|
+
const newActive = new Set(current.activeStates);
|
|
797
|
+
for (const s of transition.exitStates) newActive.delete(s);
|
|
798
|
+
for (const s of transition.activateStates) newActive.add(s);
|
|
799
|
+
const newCost = current.cost + (transition.pathCost ?? 1);
|
|
800
|
+
queue.push({
|
|
801
|
+
activeStates: newActive,
|
|
802
|
+
path: [...current.path, transition.id],
|
|
803
|
+
cost: newCost
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
found: false,
|
|
809
|
+
transitions: [],
|
|
810
|
+
totalCost: 0,
|
|
811
|
+
targetStates,
|
|
812
|
+
estimatedSteps: 0
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Navigate to target states using pathfinding
|
|
817
|
+
*/
|
|
818
|
+
async navigateTo(targetStates) {
|
|
819
|
+
const startTime = performance.now();
|
|
820
|
+
const path = this.findPath(targetStates);
|
|
821
|
+
if (!path.found) {
|
|
822
|
+
return {
|
|
823
|
+
success: false,
|
|
824
|
+
path,
|
|
825
|
+
executedTransitions: [],
|
|
826
|
+
finalActiveStates: this.getActiveStates(),
|
|
827
|
+
error: `No path found to target states: ${targetStates.join(", ")}`,
|
|
828
|
+
durationMs: performance.now() - startTime
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
const executedTransitions = [];
|
|
832
|
+
for (const transitionId of path.transitions) {
|
|
833
|
+
const result = await this.executeTransition(transitionId);
|
|
834
|
+
if (!result.success) {
|
|
835
|
+
return {
|
|
836
|
+
success: false,
|
|
837
|
+
path,
|
|
838
|
+
executedTransitions,
|
|
839
|
+
finalActiveStates: this.getActiveStates(),
|
|
840
|
+
error: result.error,
|
|
841
|
+
durationMs: performance.now() - startTime
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
executedTransitions.push(transitionId);
|
|
845
|
+
}
|
|
846
|
+
return {
|
|
847
|
+
success: true,
|
|
848
|
+
path,
|
|
849
|
+
executedTransitions,
|
|
850
|
+
finalActiveStates: this.getActiveStates(),
|
|
851
|
+
durationMs: performance.now() - startTime
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Create a state snapshot
|
|
856
|
+
*/
|
|
857
|
+
createStateSnapshot() {
|
|
858
|
+
return {
|
|
859
|
+
timestamp: Date.now(),
|
|
860
|
+
activeStates: this.getActiveStates(),
|
|
861
|
+
states: this.getAllStates(),
|
|
862
|
+
groups: this.getAllStateGroups(),
|
|
863
|
+
transitions: this.getAllTransitions()
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Create a snapshot of the current state
|
|
868
|
+
*/
|
|
869
|
+
createSnapshot() {
|
|
870
|
+
return {
|
|
871
|
+
timestamp: Date.now(),
|
|
872
|
+
elements: this.getAllElements().map((el) => ({
|
|
873
|
+
id: el.id,
|
|
874
|
+
type: el.type,
|
|
875
|
+
label: el.label,
|
|
876
|
+
identifier: el.getIdentifier(),
|
|
877
|
+
state: el.getState(),
|
|
878
|
+
actions: el.actions,
|
|
879
|
+
customActions: el.customActions ? Object.keys(el.customActions) : void 0
|
|
880
|
+
})),
|
|
881
|
+
components: this.getAllComponents().map((comp) => ({
|
|
882
|
+
id: comp.id,
|
|
883
|
+
name: comp.name,
|
|
884
|
+
description: comp.description,
|
|
885
|
+
actions: comp.actions.map((a) => a.id),
|
|
886
|
+
elementIds: comp.elementIds
|
|
887
|
+
})),
|
|
888
|
+
workflows: this.getAllWorkflows().map((wf) => ({
|
|
889
|
+
id: wf.id,
|
|
890
|
+
name: wf.name,
|
|
891
|
+
description: wf.description,
|
|
892
|
+
stepCount: wf.steps.length
|
|
893
|
+
}))
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Clear all registrations
|
|
898
|
+
*/
|
|
899
|
+
clear() {
|
|
900
|
+
this.elements.clear();
|
|
901
|
+
this.components.clear();
|
|
902
|
+
this.workflows.clear();
|
|
903
|
+
this.eventListeners.clear();
|
|
904
|
+
this.states.clear();
|
|
905
|
+
this.stateGroups.clear();
|
|
906
|
+
this.transitions.clear();
|
|
907
|
+
this.activeStates.clear();
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Get registry statistics
|
|
911
|
+
*/
|
|
912
|
+
getStats() {
|
|
913
|
+
const elements = this.getAllElements();
|
|
914
|
+
const components = this.getAllComponents();
|
|
915
|
+
return {
|
|
916
|
+
elementCount: elements.length,
|
|
917
|
+
componentCount: components.length,
|
|
918
|
+
workflowCount: this.workflows.size,
|
|
919
|
+
mountedElementCount: elements.filter((e) => e.mounted).length,
|
|
920
|
+
mountedComponentCount: components.filter((c) => c.mounted).length,
|
|
921
|
+
stateCount: this.states.size,
|
|
922
|
+
stateGroupCount: this.stateGroups.size,
|
|
923
|
+
transitionCount: this.transitions.size,
|
|
924
|
+
activeStateCount: this.activeStates.size
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
var globalRegistry = null;
|
|
929
|
+
function setGlobalRegistry(registry) {
|
|
930
|
+
globalRegistry = registry;
|
|
931
|
+
}
|
|
932
|
+
function resetGlobalRegistry() {
|
|
933
|
+
globalRegistry?.clear();
|
|
934
|
+
globalRegistry = null;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/core/websocket-client.ts
|
|
938
|
+
function generateId() {
|
|
939
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
940
|
+
}
|
|
941
|
+
var UIBridgeWSClient = class {
|
|
942
|
+
constructor(config) {
|
|
943
|
+
this.ws = null;
|
|
944
|
+
this.state = "disconnected";
|
|
945
|
+
this.clientId = null;
|
|
946
|
+
this.reconnectAttempts = 0;
|
|
947
|
+
this.reconnectTimer = null;
|
|
948
|
+
this.pingTimer = null;
|
|
949
|
+
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
950
|
+
// Event listeners
|
|
951
|
+
this.connectionListeners = /* @__PURE__ */ new Set();
|
|
952
|
+
this.eventListeners = /* @__PURE__ */ new Map();
|
|
953
|
+
this.errorListeners = /* @__PURE__ */ new Set();
|
|
954
|
+
// Current subscriptions
|
|
955
|
+
this.subscriptions = {};
|
|
956
|
+
this.config = {
|
|
957
|
+
url: config.url,
|
|
958
|
+
autoReconnect: config.autoReconnect ?? true,
|
|
959
|
+
reconnectDelay: config.reconnectDelay ?? 1e3,
|
|
960
|
+
maxReconnectAttempts: config.maxReconnectAttempts ?? 10,
|
|
961
|
+
pingInterval: config.pingInterval ?? 3e4,
|
|
962
|
+
connectionTimeout: config.connectionTimeout ?? 1e4
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Get current connection state
|
|
967
|
+
*/
|
|
968
|
+
get connectionState() {
|
|
969
|
+
return this.state;
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Get assigned client ID
|
|
973
|
+
*/
|
|
974
|
+
get id() {
|
|
975
|
+
return this.clientId;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Connect to the WebSocket server
|
|
979
|
+
*/
|
|
980
|
+
connect() {
|
|
981
|
+
return new Promise((resolve, reject) => {
|
|
982
|
+
if (this.ws && this.state === "connected") {
|
|
983
|
+
resolve();
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
this.setState("connecting");
|
|
987
|
+
try {
|
|
988
|
+
this.ws = new WebSocket(this.config.url);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
this.setState("disconnected");
|
|
991
|
+
reject(error);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const connectionTimeout = setTimeout(() => {
|
|
995
|
+
if (this.state === "connecting") {
|
|
996
|
+
this.ws?.close();
|
|
997
|
+
this.setState("disconnected");
|
|
998
|
+
reject(new Error("Connection timeout"));
|
|
999
|
+
}
|
|
1000
|
+
}, this.config.connectionTimeout);
|
|
1001
|
+
this.ws.onopen = () => {
|
|
1002
|
+
clearTimeout(connectionTimeout);
|
|
1003
|
+
};
|
|
1004
|
+
this.ws.onmessage = (event) => {
|
|
1005
|
+
try {
|
|
1006
|
+
const message = JSON.parse(event.data);
|
|
1007
|
+
this.handleMessage(message);
|
|
1008
|
+
if (message.type === "welcome") {
|
|
1009
|
+
clearTimeout(connectionTimeout);
|
|
1010
|
+
this.reconnectAttempts = 0;
|
|
1011
|
+
this.setState("connected");
|
|
1012
|
+
this.startPingInterval();
|
|
1013
|
+
if (this.subscriptions.events?.length || this.subscriptions.elementIds?.length || this.subscriptions.componentIds?.length) {
|
|
1014
|
+
this.subscribe(this.subscriptions);
|
|
1015
|
+
}
|
|
1016
|
+
resolve();
|
|
1017
|
+
}
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
console.error("Failed to parse WebSocket message:", error);
|
|
1020
|
+
}
|
|
1021
|
+
};
|
|
1022
|
+
this.ws.onerror = (_event) => {
|
|
1023
|
+
clearTimeout(connectionTimeout);
|
|
1024
|
+
const error = new Error("WebSocket error");
|
|
1025
|
+
this.notifyError(error);
|
|
1026
|
+
if (this.state === "connecting") {
|
|
1027
|
+
reject(error);
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
this.ws.onclose = () => {
|
|
1031
|
+
clearTimeout(connectionTimeout);
|
|
1032
|
+
this.stopPingInterval();
|
|
1033
|
+
this.clientId = null;
|
|
1034
|
+
const wasConnected = this.state === "connected";
|
|
1035
|
+
this.setState("disconnected");
|
|
1036
|
+
for (const [_id, pending] of this.pendingRequests) {
|
|
1037
|
+
clearTimeout(pending.timeout);
|
|
1038
|
+
pending.reject(new Error("Connection closed"));
|
|
1039
|
+
}
|
|
1040
|
+
this.pendingRequests.clear();
|
|
1041
|
+
if (wasConnected && this.config.autoReconnect && (this.config.maxReconnectAttempts === 0 || this.reconnectAttempts < this.config.maxReconnectAttempts)) {
|
|
1042
|
+
this.scheduleReconnect();
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Disconnect from the server
|
|
1049
|
+
*/
|
|
1050
|
+
disconnect() {
|
|
1051
|
+
if (this.reconnectTimer) {
|
|
1052
|
+
clearTimeout(this.reconnectTimer);
|
|
1053
|
+
this.reconnectTimer = null;
|
|
1054
|
+
}
|
|
1055
|
+
this.stopPingInterval();
|
|
1056
|
+
if (this.ws) {
|
|
1057
|
+
this.ws.close();
|
|
1058
|
+
this.ws = null;
|
|
1059
|
+
}
|
|
1060
|
+
this.setState("disconnected");
|
|
1061
|
+
}
|
|
1062
|
+
/**
|
|
1063
|
+
* Subscribe to events
|
|
1064
|
+
*/
|
|
1065
|
+
async subscribe(options) {
|
|
1066
|
+
this.subscriptions = { ...this.subscriptions, ...options };
|
|
1067
|
+
const response = await this.sendRequest({
|
|
1068
|
+
id: generateId(),
|
|
1069
|
+
type: "subscribe",
|
|
1070
|
+
timestamp: Date.now(),
|
|
1071
|
+
payload: options
|
|
1072
|
+
});
|
|
1073
|
+
return response.events;
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Unsubscribe from events
|
|
1077
|
+
*/
|
|
1078
|
+
async unsubscribe(events) {
|
|
1079
|
+
if (events) {
|
|
1080
|
+
this.subscriptions.events = this.subscriptions.events?.filter((e) => !events.includes(e));
|
|
1081
|
+
} else {
|
|
1082
|
+
this.subscriptions = {};
|
|
1083
|
+
}
|
|
1084
|
+
const response = await this.sendRequest({
|
|
1085
|
+
id: generateId(),
|
|
1086
|
+
type: "unsubscribe",
|
|
1087
|
+
timestamp: Date.now(),
|
|
1088
|
+
payload: { events }
|
|
1089
|
+
});
|
|
1090
|
+
return response.events;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Find elements
|
|
1094
|
+
*/
|
|
1095
|
+
async find(options) {
|
|
1096
|
+
const response = await this.sendRequest({
|
|
1097
|
+
id: generateId(),
|
|
1098
|
+
type: "find",
|
|
1099
|
+
timestamp: Date.now(),
|
|
1100
|
+
payload: options
|
|
1101
|
+
});
|
|
1102
|
+
return response.elements;
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Discover elements
|
|
1106
|
+
* @deprecated Use find() instead
|
|
1107
|
+
*/
|
|
1108
|
+
async discover(options) {
|
|
1109
|
+
return this.find(options);
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Get element details
|
|
1113
|
+
*/
|
|
1114
|
+
async getElement(elementId, includeState = true) {
|
|
1115
|
+
const response = await this.sendRequest({
|
|
1116
|
+
id: generateId(),
|
|
1117
|
+
type: "getElement",
|
|
1118
|
+
timestamp: Date.now(),
|
|
1119
|
+
payload: { elementId, includeState }
|
|
1120
|
+
});
|
|
1121
|
+
return response.element;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Get full snapshot
|
|
1125
|
+
*/
|
|
1126
|
+
async getSnapshot() {
|
|
1127
|
+
const response = await this.sendRequest({
|
|
1128
|
+
id: generateId(),
|
|
1129
|
+
type: "getSnapshot",
|
|
1130
|
+
timestamp: Date.now()
|
|
1131
|
+
});
|
|
1132
|
+
return response;
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Execute action on an element
|
|
1136
|
+
*/
|
|
1137
|
+
async executeAction(elementId, action) {
|
|
1138
|
+
const response = await this.sendRequest({
|
|
1139
|
+
id: generateId(),
|
|
1140
|
+
type: "executeAction",
|
|
1141
|
+
timestamp: Date.now(),
|
|
1142
|
+
payload: { elementId, action }
|
|
1143
|
+
});
|
|
1144
|
+
return response;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Execute component action
|
|
1148
|
+
*/
|
|
1149
|
+
async executeComponentAction(componentId, action, params) {
|
|
1150
|
+
const response = await this.sendRequest({
|
|
1151
|
+
id: generateId(),
|
|
1152
|
+
type: "executeComponentAction",
|
|
1153
|
+
timestamp: Date.now(),
|
|
1154
|
+
payload: { componentId, action, params }
|
|
1155
|
+
});
|
|
1156
|
+
return response;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Execute workflow with optional progress streaming
|
|
1160
|
+
*/
|
|
1161
|
+
async executeWorkflow(workflowId, params, onProgress) {
|
|
1162
|
+
const id = generateId();
|
|
1163
|
+
const progressHandler = onProgress ? (message) => {
|
|
1164
|
+
if (message.type === "workflowProgress" && message.requestId === id) {
|
|
1165
|
+
onProgress({
|
|
1166
|
+
currentStep: message.payload.currentStep,
|
|
1167
|
+
totalSteps: message.payload.totalSteps,
|
|
1168
|
+
step: {
|
|
1169
|
+
id: message.payload.step.id,
|
|
1170
|
+
status: message.payload.step.status
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
} : void 0;
|
|
1175
|
+
const response = await this.sendRequest(
|
|
1176
|
+
{
|
|
1177
|
+
id,
|
|
1178
|
+
type: "executeWorkflow",
|
|
1179
|
+
timestamp: Date.now(),
|
|
1180
|
+
payload: { workflowId, params, streamProgress: !!onProgress }
|
|
1181
|
+
},
|
|
1182
|
+
progressHandler
|
|
1183
|
+
);
|
|
1184
|
+
return response;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Add connection state listener
|
|
1188
|
+
*/
|
|
1189
|
+
onConnectionChange(listener) {
|
|
1190
|
+
this.connectionListeners.add(listener);
|
|
1191
|
+
return () => this.connectionListeners.delete(listener);
|
|
1192
|
+
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Add event listener
|
|
1195
|
+
*/
|
|
1196
|
+
onEvent(eventType, listener) {
|
|
1197
|
+
if (!this.eventListeners.has(eventType)) {
|
|
1198
|
+
this.eventListeners.set(eventType, /* @__PURE__ */ new Set());
|
|
1199
|
+
}
|
|
1200
|
+
this.eventListeners.get(eventType).add(listener);
|
|
1201
|
+
return () => this.eventListeners.get(eventType)?.delete(listener);
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Add error listener
|
|
1205
|
+
*/
|
|
1206
|
+
onError(listener) {
|
|
1207
|
+
this.errorListeners.add(listener);
|
|
1208
|
+
return () => this.errorListeners.delete(listener);
|
|
1209
|
+
}
|
|
1210
|
+
// Private methods
|
|
1211
|
+
setState(state) {
|
|
1212
|
+
this.state = state;
|
|
1213
|
+
for (const listener of this.connectionListeners) {
|
|
1214
|
+
try {
|
|
1215
|
+
listener(state);
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
console.error("Connection listener error:", error);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
handleMessage(message) {
|
|
1222
|
+
switch (message.type) {
|
|
1223
|
+
case "welcome":
|
|
1224
|
+
this.clientId = message.payload.clientId;
|
|
1225
|
+
break;
|
|
1226
|
+
case "pong":
|
|
1227
|
+
break;
|
|
1228
|
+
case "subscribed":
|
|
1229
|
+
case "unsubscribed":
|
|
1230
|
+
break;
|
|
1231
|
+
case "event":
|
|
1232
|
+
this.notifyEvent(message.payload);
|
|
1233
|
+
break;
|
|
1234
|
+
case "response":
|
|
1235
|
+
this.handleResponse(message);
|
|
1236
|
+
break;
|
|
1237
|
+
case "error":
|
|
1238
|
+
if (message.requestId) {
|
|
1239
|
+
this.handleResponse({
|
|
1240
|
+
...message,
|
|
1241
|
+
type: "response",
|
|
1242
|
+
requestId: message.requestId,
|
|
1243
|
+
payload: {
|
|
1244
|
+
success: false,
|
|
1245
|
+
error: message.payload.message
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
} else {
|
|
1249
|
+
this.notifyError(new Error(message.payload.message));
|
|
1250
|
+
}
|
|
1251
|
+
break;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
handleResponse(message) {
|
|
1255
|
+
const pending = this.pendingRequests.get(message.requestId);
|
|
1256
|
+
if (!pending) return;
|
|
1257
|
+
clearTimeout(pending.timeout);
|
|
1258
|
+
this.pendingRequests.delete(message.requestId);
|
|
1259
|
+
if (message.type === "response") {
|
|
1260
|
+
if (message.payload.success) {
|
|
1261
|
+
pending.resolve(message.payload.data);
|
|
1262
|
+
} else {
|
|
1263
|
+
pending.reject(new Error(message.payload.error || "Request failed"));
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
notifyEvent(event) {
|
|
1268
|
+
const typeListeners = this.eventListeners.get(event.type);
|
|
1269
|
+
if (typeListeners) {
|
|
1270
|
+
for (const listener of typeListeners) {
|
|
1271
|
+
try {
|
|
1272
|
+
listener(event);
|
|
1273
|
+
} catch (error) {
|
|
1274
|
+
console.error("Event listener error:", error);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
const wildcardListeners = this.eventListeners.get("*");
|
|
1279
|
+
if (wildcardListeners) {
|
|
1280
|
+
for (const listener of wildcardListeners) {
|
|
1281
|
+
try {
|
|
1282
|
+
listener(event);
|
|
1283
|
+
} catch (error) {
|
|
1284
|
+
console.error("Event listener error:", error);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
notifyError(error) {
|
|
1290
|
+
for (const listener of this.errorListeners) {
|
|
1291
|
+
try {
|
|
1292
|
+
listener(error);
|
|
1293
|
+
} catch (e) {
|
|
1294
|
+
console.error("Error listener error:", e);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
sendRequest(message, progressHandler) {
|
|
1299
|
+
return new Promise((resolve, reject) => {
|
|
1300
|
+
if (!this.ws || this.state !== "connected") {
|
|
1301
|
+
reject(new Error("Not connected"));
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const timeout = setTimeout(() => {
|
|
1305
|
+
this.pendingRequests.delete(message.id);
|
|
1306
|
+
reject(new Error("Request timeout"));
|
|
1307
|
+
}, 3e4);
|
|
1308
|
+
this.pendingRequests.set(message.id, {
|
|
1309
|
+
resolve,
|
|
1310
|
+
reject,
|
|
1311
|
+
timeout
|
|
1312
|
+
});
|
|
1313
|
+
if (progressHandler && this.ws) {
|
|
1314
|
+
const originalHandler = this.ws.onmessage;
|
|
1315
|
+
const wsRef = this.ws;
|
|
1316
|
+
const wrappedHandler = (event) => {
|
|
1317
|
+
try {
|
|
1318
|
+
const msg = JSON.parse(event.data);
|
|
1319
|
+
if (msg.type === "workflowProgress") {
|
|
1320
|
+
progressHandler(msg);
|
|
1321
|
+
}
|
|
1322
|
+
} catch {
|
|
1323
|
+
}
|
|
1324
|
+
if (originalHandler) {
|
|
1325
|
+
originalHandler.call(wsRef, event);
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
this.ws.onmessage = wrappedHandler;
|
|
1329
|
+
}
|
|
1330
|
+
this.ws.send(JSON.stringify(message));
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
scheduleReconnect() {
|
|
1334
|
+
if (this.reconnectTimer) return;
|
|
1335
|
+
this.setState("reconnecting");
|
|
1336
|
+
this.reconnectAttempts++;
|
|
1337
|
+
const delay = Math.min(
|
|
1338
|
+
this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
|
|
1339
|
+
3e4
|
|
1340
|
+
);
|
|
1341
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1342
|
+
this.reconnectTimer = null;
|
|
1343
|
+
this.connect().catch(() => {
|
|
1344
|
+
});
|
|
1345
|
+
}, delay);
|
|
1346
|
+
}
|
|
1347
|
+
startPingInterval() {
|
|
1348
|
+
if (this.config.pingInterval <= 0) return;
|
|
1349
|
+
this.pingTimer = setInterval(() => {
|
|
1350
|
+
if (this.ws && this.state === "connected") {
|
|
1351
|
+
this.ws.send(
|
|
1352
|
+
JSON.stringify({
|
|
1353
|
+
id: generateId(),
|
|
1354
|
+
type: "ping",
|
|
1355
|
+
timestamp: Date.now()
|
|
1356
|
+
})
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
}, this.config.pingInterval);
|
|
1360
|
+
}
|
|
1361
|
+
stopPingInterval() {
|
|
1362
|
+
if (this.pingTimer) {
|
|
1363
|
+
clearInterval(this.pingTimer);
|
|
1364
|
+
this.pingTimer = null;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
function createWSClient(config) {
|
|
1369
|
+
return new UIBridgeWSClient(config);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// src/control/action-executor.ts
|
|
1373
|
+
var DEFAULT_WAIT_OPTIONS = {
|
|
1374
|
+
visible: true,
|
|
1375
|
+
enabled: true,
|
|
1376
|
+
focused: false,
|
|
1377
|
+
state: {},
|
|
1378
|
+
timeout: 1e4,
|
|
1379
|
+
interval: 100
|
|
1380
|
+
};
|
|
1381
|
+
function getElementState2(element) {
|
|
1382
|
+
const rect = element.getBoundingClientRect();
|
|
1383
|
+
const style = window.getComputedStyle(element);
|
|
1384
|
+
const state = {
|
|
1385
|
+
visible: isVisible(element, rect, style),
|
|
1386
|
+
enabled: !isDisabled(element),
|
|
1387
|
+
focused: document.activeElement === element,
|
|
1388
|
+
rect: {
|
|
1389
|
+
x: rect.x,
|
|
1390
|
+
y: rect.y,
|
|
1391
|
+
width: rect.width,
|
|
1392
|
+
height: rect.height,
|
|
1393
|
+
top: rect.top,
|
|
1394
|
+
right: rect.right,
|
|
1395
|
+
bottom: rect.bottom,
|
|
1396
|
+
left: rect.left
|
|
1397
|
+
},
|
|
1398
|
+
computedStyles: {
|
|
1399
|
+
display: style.display,
|
|
1400
|
+
visibility: style.visibility,
|
|
1401
|
+
opacity: style.opacity,
|
|
1402
|
+
pointerEvents: style.pointerEvents
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
if (element instanceof HTMLInputElement) {
|
|
1406
|
+
state.value = element.value;
|
|
1407
|
+
if (element.type === "checkbox" || element.type === "radio") {
|
|
1408
|
+
state.checked = element.checked;
|
|
1409
|
+
}
|
|
1410
|
+
} else if (element instanceof HTMLTextAreaElement) {
|
|
1411
|
+
state.value = element.value;
|
|
1412
|
+
} else if (element instanceof HTMLSelectElement) {
|
|
1413
|
+
state.value = element.value;
|
|
1414
|
+
state.selectedOptions = Array.from(element.selectedOptions).map((opt) => opt.value);
|
|
1415
|
+
}
|
|
1416
|
+
return state;
|
|
1417
|
+
}
|
|
1418
|
+
function isVisible(element, rect, style) {
|
|
1419
|
+
if (rect.width === 0 || rect.height === 0) return false;
|
|
1420
|
+
if (style.display === "none") return false;
|
|
1421
|
+
if (style.visibility === "hidden") return false;
|
|
1422
|
+
if (parseFloat(style.opacity) === 0) return false;
|
|
1423
|
+
return rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
|
|
1424
|
+
}
|
|
1425
|
+
function isDisabled(element) {
|
|
1426
|
+
if ("disabled" in element && element.disabled) return true;
|
|
1427
|
+
if (element.getAttribute("aria-disabled") === "true") return true;
|
|
1428
|
+
return false;
|
|
1429
|
+
}
|
|
1430
|
+
function sleep(ms) {
|
|
1431
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1432
|
+
}
|
|
1433
|
+
function createMouseEvent(type, element, options) {
|
|
1434
|
+
const rect = element.getBoundingClientRect();
|
|
1435
|
+
const x = options?.position?.x ?? rect.width / 2;
|
|
1436
|
+
const y = options?.position?.y ?? rect.height / 2;
|
|
1437
|
+
return new MouseEvent(type, {
|
|
1438
|
+
bubbles: true,
|
|
1439
|
+
cancelable: true,
|
|
1440
|
+
view: window,
|
|
1441
|
+
button: options?.button === "right" ? 2 : options?.button === "middle" ? 1 : 0,
|
|
1442
|
+
clientX: rect.left + x,
|
|
1443
|
+
clientY: rect.top + y
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
var DefaultActionExecutor = class {
|
|
1447
|
+
constructor(registry) {
|
|
1448
|
+
this.registry = registry;
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Execute an action on an element
|
|
1452
|
+
*/
|
|
1453
|
+
async executeAction(elementId, request) {
|
|
1454
|
+
const startTime = performance.now();
|
|
1455
|
+
let waitDurationMs = 0;
|
|
1456
|
+
try {
|
|
1457
|
+
const registered = this.registry.getElement(elementId);
|
|
1458
|
+
let element = registered?.element ?? null;
|
|
1459
|
+
if (!element) {
|
|
1460
|
+
element = findElementByIdentifier(elementId);
|
|
1461
|
+
}
|
|
1462
|
+
if (!element) {
|
|
1463
|
+
return {
|
|
1464
|
+
success: false,
|
|
1465
|
+
error: `Element not found: ${elementId}`,
|
|
1466
|
+
durationMs: performance.now() - startTime,
|
|
1467
|
+
timestamp: Date.now(),
|
|
1468
|
+
requestId: request.requestId
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
if (request.waitOptions) {
|
|
1472
|
+
const waitResult = await this.waitForElement(element, request.waitOptions);
|
|
1473
|
+
waitDurationMs = waitResult.waitedMs;
|
|
1474
|
+
if (!waitResult.met) {
|
|
1475
|
+
return {
|
|
1476
|
+
success: false,
|
|
1477
|
+
error: waitResult.error || "Wait condition not met",
|
|
1478
|
+
durationMs: performance.now() - startTime,
|
|
1479
|
+
timestamp: Date.now(),
|
|
1480
|
+
requestId: request.requestId,
|
|
1481
|
+
waitDurationMs
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
const result = await this.performAction(element, request.action, request.params);
|
|
1486
|
+
return {
|
|
1487
|
+
success: true,
|
|
1488
|
+
elementState: getElementState2(element),
|
|
1489
|
+
result,
|
|
1490
|
+
durationMs: performance.now() - startTime,
|
|
1491
|
+
timestamp: Date.now(),
|
|
1492
|
+
requestId: request.requestId,
|
|
1493
|
+
waitDurationMs
|
|
1494
|
+
};
|
|
1495
|
+
} catch (error) {
|
|
1496
|
+
return {
|
|
1497
|
+
success: false,
|
|
1498
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1499
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
1500
|
+
durationMs: performance.now() - startTime,
|
|
1501
|
+
timestamp: Date.now(),
|
|
1502
|
+
requestId: request.requestId,
|
|
1503
|
+
waitDurationMs
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
/**
|
|
1508
|
+
* Execute an action on a component
|
|
1509
|
+
*/
|
|
1510
|
+
async executeComponentAction(componentId, request) {
|
|
1511
|
+
const startTime = performance.now();
|
|
1512
|
+
try {
|
|
1513
|
+
const component = this.registry.getComponent(componentId);
|
|
1514
|
+
if (!component) {
|
|
1515
|
+
return {
|
|
1516
|
+
success: false,
|
|
1517
|
+
error: `Component not found: ${componentId}`,
|
|
1518
|
+
durationMs: performance.now() - startTime,
|
|
1519
|
+
timestamp: Date.now(),
|
|
1520
|
+
requestId: request.requestId
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
const action = component.actions.find((a) => a.id === request.action);
|
|
1524
|
+
if (!action) {
|
|
1525
|
+
return {
|
|
1526
|
+
success: false,
|
|
1527
|
+
error: `Action not found: ${request.action}`,
|
|
1528
|
+
durationMs: performance.now() - startTime,
|
|
1529
|
+
timestamp: Date.now(),
|
|
1530
|
+
requestId: request.requestId
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
const result = await action.handler(request.params);
|
|
1534
|
+
return {
|
|
1535
|
+
success: true,
|
|
1536
|
+
result,
|
|
1537
|
+
durationMs: performance.now() - startTime,
|
|
1538
|
+
timestamp: Date.now(),
|
|
1539
|
+
requestId: request.requestId
|
|
1540
|
+
};
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
return {
|
|
1543
|
+
success: false,
|
|
1544
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1545
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
1546
|
+
durationMs: performance.now() - startTime,
|
|
1547
|
+
timestamp: Date.now(),
|
|
1548
|
+
requestId: request.requestId
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
/**
|
|
1553
|
+
* Wait for a condition on an element
|
|
1554
|
+
*/
|
|
1555
|
+
async waitFor(elementId, options) {
|
|
1556
|
+
const registered = this.registry.getElement(elementId);
|
|
1557
|
+
let element = registered?.element ?? null;
|
|
1558
|
+
if (!element) {
|
|
1559
|
+
element = findElementByIdentifier(elementId);
|
|
1560
|
+
}
|
|
1561
|
+
if (!element) {
|
|
1562
|
+
return {
|
|
1563
|
+
met: false,
|
|
1564
|
+
waitedMs: 0,
|
|
1565
|
+
error: `Element not found: ${elementId}`
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
return this.waitForElement(element, options);
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Find controllable elements
|
|
1572
|
+
*/
|
|
1573
|
+
async find(options) {
|
|
1574
|
+
const startTime = performance.now();
|
|
1575
|
+
const elements = [];
|
|
1576
|
+
let root = document.body;
|
|
1577
|
+
if (options?.root) {
|
|
1578
|
+
const rootEl = document.querySelector(options.root);
|
|
1579
|
+
if (rootEl) root = rootEl;
|
|
1580
|
+
}
|
|
1581
|
+
const interactiveSelectors = [
|
|
1582
|
+
"a[href]",
|
|
1583
|
+
"button",
|
|
1584
|
+
"input",
|
|
1585
|
+
"select",
|
|
1586
|
+
"textarea",
|
|
1587
|
+
"[onclick]",
|
|
1588
|
+
'[role="button"]',
|
|
1589
|
+
'[role="link"]',
|
|
1590
|
+
'[role="checkbox"]',
|
|
1591
|
+
'[role="radio"]',
|
|
1592
|
+
'[role="menuitem"]',
|
|
1593
|
+
'[role="tab"]',
|
|
1594
|
+
'[role="switch"]',
|
|
1595
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
1596
|
+
'[contenteditable="true"]',
|
|
1597
|
+
"[data-ui-id]",
|
|
1598
|
+
"[data-testid]"
|
|
1599
|
+
];
|
|
1600
|
+
const selector = options?.selector || interactiveSelectors.join(", ");
|
|
1601
|
+
const foundElements = root.querySelectorAll(selector);
|
|
1602
|
+
for (const el of foundElements) {
|
|
1603
|
+
if (options?.limit && elements.length >= options.limit) break;
|
|
1604
|
+
const state = getElementState2(el);
|
|
1605
|
+
if (!options?.includeHidden && !state.visible) continue;
|
|
1606
|
+
if (options?.types) {
|
|
1607
|
+
const type = this.inferElementType(el);
|
|
1608
|
+
if (!options.types.includes(type)) continue;
|
|
1609
|
+
}
|
|
1610
|
+
const registered = this.registry.findByDOMElement(el);
|
|
1611
|
+
elements.push({
|
|
1612
|
+
id: registered?.id || this.getElementId(el),
|
|
1613
|
+
type: registered?.type || this.inferElementType(el),
|
|
1614
|
+
label: registered?.label || this.getElementLabel(el),
|
|
1615
|
+
tagName: el.tagName.toLowerCase(),
|
|
1616
|
+
role: el.getAttribute("role") || void 0,
|
|
1617
|
+
accessibleName: this.getAccessibleName(el),
|
|
1618
|
+
actions: registered?.actions || this.inferActions(el),
|
|
1619
|
+
state,
|
|
1620
|
+
registered: !!registered
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
return {
|
|
1624
|
+
elements,
|
|
1625
|
+
total: elements.length,
|
|
1626
|
+
durationMs: performance.now() - startTime,
|
|
1627
|
+
timestamp: Date.now()
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Discover controllable elements
|
|
1632
|
+
* @deprecated Use find() instead
|
|
1633
|
+
*/
|
|
1634
|
+
async discover(options) {
|
|
1635
|
+
return this.find(options);
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* Get control snapshot
|
|
1639
|
+
*/
|
|
1640
|
+
async getSnapshot() {
|
|
1641
|
+
const elements = this.registry.getAllElements();
|
|
1642
|
+
const components = this.registry.getAllComponents();
|
|
1643
|
+
const workflows = this.registry.getAllWorkflows();
|
|
1644
|
+
return {
|
|
1645
|
+
timestamp: Date.now(),
|
|
1646
|
+
elements: elements.map((el) => ({
|
|
1647
|
+
id: el.id,
|
|
1648
|
+
type: el.type,
|
|
1649
|
+
label: el.label,
|
|
1650
|
+
actions: [...el.actions, ...el.customActions ? Object.keys(el.customActions) : []],
|
|
1651
|
+
state: el.getState()
|
|
1652
|
+
})),
|
|
1653
|
+
components: components.map((comp) => ({
|
|
1654
|
+
id: comp.id,
|
|
1655
|
+
name: comp.name,
|
|
1656
|
+
actions: comp.actions.map((a) => a.id)
|
|
1657
|
+
})),
|
|
1658
|
+
workflows: workflows.map((wf) => ({
|
|
1659
|
+
id: wf.id,
|
|
1660
|
+
name: wf.name,
|
|
1661
|
+
stepCount: wf.steps.length
|
|
1662
|
+
})),
|
|
1663
|
+
activeRuns: []
|
|
1664
|
+
// Workflow engine manages this
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Wait for element conditions
|
|
1669
|
+
*/
|
|
1670
|
+
async waitForElement(element, options) {
|
|
1671
|
+
const opts = { ...DEFAULT_WAIT_OPTIONS, ...options };
|
|
1672
|
+
const startTime = performance.now();
|
|
1673
|
+
const deadline = startTime + opts.timeout;
|
|
1674
|
+
while (Date.now() < deadline) {
|
|
1675
|
+
const state = getElementState2(element);
|
|
1676
|
+
let allMet = true;
|
|
1677
|
+
if (opts.visible && !state.visible) allMet = false;
|
|
1678
|
+
if (opts.enabled && !state.enabled) allMet = false;
|
|
1679
|
+
if (opts.focused && !state.focused) allMet = false;
|
|
1680
|
+
if (opts.state) {
|
|
1681
|
+
for (const [key, value] of Object.entries(opts.state)) {
|
|
1682
|
+
if (state[key] !== value) {
|
|
1683
|
+
allMet = false;
|
|
1684
|
+
break;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
if (allMet) {
|
|
1689
|
+
return {
|
|
1690
|
+
met: true,
|
|
1691
|
+
waitedMs: performance.now() - startTime,
|
|
1692
|
+
state
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
await sleep(opts.interval);
|
|
1696
|
+
}
|
|
1697
|
+
return {
|
|
1698
|
+
met: false,
|
|
1699
|
+
waitedMs: performance.now() - startTime,
|
|
1700
|
+
state: getElementState2(element),
|
|
1701
|
+
error: `Timeout waiting for conditions after ${opts.timeout}ms`
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Perform an action on an element
|
|
1706
|
+
*/
|
|
1707
|
+
async performAction(element, action, params) {
|
|
1708
|
+
switch (action) {
|
|
1709
|
+
case "click":
|
|
1710
|
+
return this.performClick(element, params);
|
|
1711
|
+
case "doubleClick":
|
|
1712
|
+
return this.performDoubleClick(element, params);
|
|
1713
|
+
case "rightClick":
|
|
1714
|
+
return this.performRightClick(element, params);
|
|
1715
|
+
case "type":
|
|
1716
|
+
return this.performType(element, params);
|
|
1717
|
+
case "clear":
|
|
1718
|
+
return this.performClear(element);
|
|
1719
|
+
case "select":
|
|
1720
|
+
return this.performSelect(element, params);
|
|
1721
|
+
case "focus":
|
|
1722
|
+
return this.performFocus(element);
|
|
1723
|
+
case "blur":
|
|
1724
|
+
return this.performBlur(element);
|
|
1725
|
+
case "hover":
|
|
1726
|
+
return this.performHover(element);
|
|
1727
|
+
case "scroll":
|
|
1728
|
+
return this.performScroll(element, params);
|
|
1729
|
+
case "check":
|
|
1730
|
+
return this.performCheck(element, true);
|
|
1731
|
+
case "uncheck":
|
|
1732
|
+
return this.performCheck(element, false);
|
|
1733
|
+
case "toggle":
|
|
1734
|
+
return this.performToggle(element);
|
|
1735
|
+
default: {
|
|
1736
|
+
const registered = this.registry.findByDOMElement(element);
|
|
1737
|
+
if (registered?.customActions?.[action]) {
|
|
1738
|
+
return registered.customActions[action].handler(params);
|
|
1739
|
+
}
|
|
1740
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
performClick(element, options) {
|
|
1745
|
+
element.dispatchEvent(createMouseEvent("mousedown", element, options));
|
|
1746
|
+
element.dispatchEvent(createMouseEvent("mouseup", element, options));
|
|
1747
|
+
element.dispatchEvent(createMouseEvent("click", element, options));
|
|
1748
|
+
}
|
|
1749
|
+
performDoubleClick(element, options) {
|
|
1750
|
+
this.performClick(element, options);
|
|
1751
|
+
this.performClick(element, options);
|
|
1752
|
+
element.dispatchEvent(createMouseEvent("dblclick", element, options));
|
|
1753
|
+
}
|
|
1754
|
+
performRightClick(element, options) {
|
|
1755
|
+
const opts = { ...options, button: "right" };
|
|
1756
|
+
element.dispatchEvent(createMouseEvent("mousedown", element, opts));
|
|
1757
|
+
element.dispatchEvent(createMouseEvent("mouseup", element, opts));
|
|
1758
|
+
element.dispatchEvent(createMouseEvent("contextmenu", element, opts));
|
|
1759
|
+
}
|
|
1760
|
+
async performType(element, options) {
|
|
1761
|
+
if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) {
|
|
1762
|
+
throw new Error("Type action requires an input or textarea element");
|
|
1763
|
+
}
|
|
1764
|
+
element.focus();
|
|
1765
|
+
if (options?.clear) {
|
|
1766
|
+
element.value = "";
|
|
1767
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1768
|
+
}
|
|
1769
|
+
const text = options?.text || "";
|
|
1770
|
+
const delay = options?.delay || 0;
|
|
1771
|
+
for (const char of text) {
|
|
1772
|
+
element.value += char;
|
|
1773
|
+
if (options?.triggerEvents !== false) {
|
|
1774
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1775
|
+
}
|
|
1776
|
+
if (delay > 0) {
|
|
1777
|
+
await sleep(delay);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
if (options?.triggerEvents !== false) {
|
|
1781
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
performClear(element) {
|
|
1785
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
1786
|
+
element.value = "";
|
|
1787
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1788
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
performSelect(element, options) {
|
|
1792
|
+
if (!(element instanceof HTMLSelectElement)) {
|
|
1793
|
+
throw new Error("Select action requires a select element");
|
|
1794
|
+
}
|
|
1795
|
+
const values = Array.isArray(options?.value) ? options.value : [options?.value];
|
|
1796
|
+
if (!options?.additive) {
|
|
1797
|
+
for (const option of element.options) {
|
|
1798
|
+
option.selected = false;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
for (const option of element.options) {
|
|
1802
|
+
const matchValue = options?.byLabel ? option.text : option.value;
|
|
1803
|
+
if (values.includes(matchValue)) {
|
|
1804
|
+
option.selected = true;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1808
|
+
}
|
|
1809
|
+
performFocus(element) {
|
|
1810
|
+
element.focus();
|
|
1811
|
+
element.dispatchEvent(new FocusEvent("focus", { bubbles: true }));
|
|
1812
|
+
}
|
|
1813
|
+
performBlur(element) {
|
|
1814
|
+
element.blur();
|
|
1815
|
+
element.dispatchEvent(new FocusEvent("blur", { bubbles: true }));
|
|
1816
|
+
}
|
|
1817
|
+
performHover(element) {
|
|
1818
|
+
element.dispatchEvent(createMouseEvent("mouseenter", element));
|
|
1819
|
+
element.dispatchEvent(createMouseEvent("mouseover", element));
|
|
1820
|
+
}
|
|
1821
|
+
performScroll(element, options) {
|
|
1822
|
+
if (options?.toElement) {
|
|
1823
|
+
const target = document.querySelector(options.toElement);
|
|
1824
|
+
if (target) {
|
|
1825
|
+
target.scrollIntoView({ behavior: options.smooth ? "smooth" : "auto" });
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
if (options?.position) {
|
|
1830
|
+
element.scrollTo({
|
|
1831
|
+
left: options.position.x,
|
|
1832
|
+
top: options.position.y,
|
|
1833
|
+
behavior: options.smooth ? "smooth" : "auto"
|
|
1834
|
+
});
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
const amount = options?.amount || 100;
|
|
1838
|
+
const direction = options?.direction || "down";
|
|
1839
|
+
switch (direction) {
|
|
1840
|
+
case "up":
|
|
1841
|
+
element.scrollBy({ top: -amount, behavior: options?.smooth ? "smooth" : "auto" });
|
|
1842
|
+
break;
|
|
1843
|
+
case "down":
|
|
1844
|
+
element.scrollBy({ top: amount, behavior: options?.smooth ? "smooth" : "auto" });
|
|
1845
|
+
break;
|
|
1846
|
+
case "left":
|
|
1847
|
+
element.scrollBy({ left: -amount, behavior: options?.smooth ? "smooth" : "auto" });
|
|
1848
|
+
break;
|
|
1849
|
+
case "right":
|
|
1850
|
+
element.scrollBy({ left: amount, behavior: options?.smooth ? "smooth" : "auto" });
|
|
1851
|
+
break;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
performCheck(element, checked) {
|
|
1855
|
+
if (element instanceof HTMLInputElement && (element.type === "checkbox" || element.type === "radio")) {
|
|
1856
|
+
if (element.checked !== checked) {
|
|
1857
|
+
element.checked = checked;
|
|
1858
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
performToggle(element) {
|
|
1863
|
+
if (element instanceof HTMLInputElement && element.type === "checkbox") {
|
|
1864
|
+
element.checked = !element.checked;
|
|
1865
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
getElementId(element) {
|
|
1869
|
+
return element.getAttribute("data-ui-id") || element.getAttribute("data-testid") || element.id || `${element.tagName.toLowerCase()}-${Math.random().toString(36).substr(2, 8)}`;
|
|
1870
|
+
}
|
|
1871
|
+
getElementLabel(element) {
|
|
1872
|
+
return element.getAttribute("aria-label") || element.getAttribute("title") || element.textContent?.trim().substring(0, 50) || void 0;
|
|
1873
|
+
}
|
|
1874
|
+
getAccessibleName(element) {
|
|
1875
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
1876
|
+
if (ariaLabel) return ariaLabel;
|
|
1877
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
1878
|
+
if (labelledBy) {
|
|
1879
|
+
const labels = labelledBy.split(" ").map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean);
|
|
1880
|
+
if (labels.length > 0) return labels.join(" ");
|
|
1881
|
+
}
|
|
1882
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
|
|
1883
|
+
if (element.id) {
|
|
1884
|
+
const label = document.querySelector(`label[for="${element.id}"]`);
|
|
1885
|
+
if (label) return label.textContent?.trim();
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return element.getAttribute("title") || element.textContent?.trim().substring(0, 50) || void 0;
|
|
1889
|
+
}
|
|
1890
|
+
inferElementType(element) {
|
|
1891
|
+
const tagName = element.tagName.toLowerCase();
|
|
1892
|
+
const role = element.getAttribute("role");
|
|
1893
|
+
if (role) {
|
|
1894
|
+
switch (role) {
|
|
1895
|
+
case "button":
|
|
1896
|
+
return "button";
|
|
1897
|
+
case "textbox":
|
|
1898
|
+
return "input";
|
|
1899
|
+
case "checkbox":
|
|
1900
|
+
return "checkbox";
|
|
1901
|
+
case "radio":
|
|
1902
|
+
return "radio";
|
|
1903
|
+
case "link":
|
|
1904
|
+
return "link";
|
|
1905
|
+
case "listbox":
|
|
1906
|
+
case "combobox":
|
|
1907
|
+
return "select";
|
|
1908
|
+
case "menu":
|
|
1909
|
+
return "menu";
|
|
1910
|
+
case "menuitem":
|
|
1911
|
+
return "menuitem";
|
|
1912
|
+
case "tab":
|
|
1913
|
+
return "tab";
|
|
1914
|
+
case "dialog":
|
|
1915
|
+
return "dialog";
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
switch (tagName) {
|
|
1919
|
+
case "button":
|
|
1920
|
+
return "button";
|
|
1921
|
+
case "input": {
|
|
1922
|
+
const type = element.type;
|
|
1923
|
+
if (type === "checkbox") return "checkbox";
|
|
1924
|
+
if (type === "radio") return "radio";
|
|
1925
|
+
if (type === "submit" || type === "button") return "button";
|
|
1926
|
+
return "input";
|
|
1927
|
+
}
|
|
1928
|
+
case "textarea":
|
|
1929
|
+
return "textarea";
|
|
1930
|
+
case "select":
|
|
1931
|
+
return "select";
|
|
1932
|
+
case "a":
|
|
1933
|
+
return "link";
|
|
1934
|
+
case "form":
|
|
1935
|
+
return "form";
|
|
1936
|
+
default:
|
|
1937
|
+
return "custom";
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
inferActions(element) {
|
|
1941
|
+
const type = this.inferElementType(element);
|
|
1942
|
+
const baseActions = ["focus", "blur", "hover"];
|
|
1943
|
+
switch (type) {
|
|
1944
|
+
case "button":
|
|
1945
|
+
return [...baseActions, "click", "doubleClick", "rightClick"];
|
|
1946
|
+
case "input":
|
|
1947
|
+
return [...baseActions, "click", "type", "clear"];
|
|
1948
|
+
case "textarea":
|
|
1949
|
+
return [...baseActions, "click", "type", "clear"];
|
|
1950
|
+
case "select":
|
|
1951
|
+
return [...baseActions, "click", "select"];
|
|
1952
|
+
case "checkbox":
|
|
1953
|
+
return [...baseActions, "click", "check", "uncheck", "toggle"];
|
|
1954
|
+
case "radio":
|
|
1955
|
+
return [...baseActions, "click", "check"];
|
|
1956
|
+
case "link":
|
|
1957
|
+
return [...baseActions, "click"];
|
|
1958
|
+
default:
|
|
1959
|
+
return [...baseActions, "click"];
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
function createActionExecutor(registry) {
|
|
1964
|
+
return new DefaultActionExecutor(registry);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// src/control/workflow-engine.ts
|
|
1968
|
+
function generateRunId() {
|
|
1969
|
+
return `run-${Date.now()}-${Math.random().toString(36).substr(2, 8)}`;
|
|
1970
|
+
}
|
|
1971
|
+
var DefaultWorkflowEngine = class {
|
|
1972
|
+
constructor(registry, executor) {
|
|
1973
|
+
this.registry = registry;
|
|
1974
|
+
this.executor = executor;
|
|
1975
|
+
this.activeRuns = /* @__PURE__ */ new Map();
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Run a workflow
|
|
1979
|
+
*/
|
|
1980
|
+
async run(workflowId, request) {
|
|
1981
|
+
const workflow = this.registry.getWorkflow(workflowId);
|
|
1982
|
+
if (!workflow) {
|
|
1983
|
+
return {
|
|
1984
|
+
workflowId,
|
|
1985
|
+
runId: generateRunId(),
|
|
1986
|
+
status: "failed",
|
|
1987
|
+
steps: [],
|
|
1988
|
+
totalSteps: 0,
|
|
1989
|
+
success: false,
|
|
1990
|
+
error: `Workflow not found: ${workflowId}`,
|
|
1991
|
+
startedAt: Date.now(),
|
|
1992
|
+
completedAt: Date.now(),
|
|
1993
|
+
durationMs: 0
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
1996
|
+
const runId = generateRunId();
|
|
1997
|
+
const state = {
|
|
1998
|
+
workflowId,
|
|
1999
|
+
runId,
|
|
2000
|
+
workflow,
|
|
2001
|
+
request,
|
|
2002
|
+
status: "running",
|
|
2003
|
+
steps: [],
|
|
2004
|
+
currentStep: 0,
|
|
2005
|
+
startedAt: Date.now()
|
|
2006
|
+
};
|
|
2007
|
+
this.activeRuns.set(runId, state);
|
|
2008
|
+
try {
|
|
2009
|
+
await this.executeWorkflow(state);
|
|
2010
|
+
} catch (error) {
|
|
2011
|
+
state.status = "failed";
|
|
2012
|
+
state.error = error instanceof Error ? error.message : String(error);
|
|
2013
|
+
}
|
|
2014
|
+
state.completedAt = Date.now();
|
|
2015
|
+
state.durationMs = state.completedAt - state.startedAt;
|
|
2016
|
+
state.success = state.status === "completed" && state.steps.every((s) => s.success);
|
|
2017
|
+
setTimeout(() => {
|
|
2018
|
+
this.activeRuns.delete(runId);
|
|
2019
|
+
}, 6e4);
|
|
2020
|
+
return this.buildResponse(state);
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Get workflow run status
|
|
2024
|
+
*/
|
|
2025
|
+
async getRunStatus(runId) {
|
|
2026
|
+
const state = this.activeRuns.get(runId);
|
|
2027
|
+
if (!state) return null;
|
|
2028
|
+
return this.buildResponse(state);
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* Cancel a running workflow
|
|
2032
|
+
*/
|
|
2033
|
+
async cancel(runId) {
|
|
2034
|
+
const state = this.activeRuns.get(runId);
|
|
2035
|
+
if (!state || state.status !== "running") return false;
|
|
2036
|
+
state.status = "cancelled";
|
|
2037
|
+
state.completedAt = Date.now();
|
|
2038
|
+
state.durationMs = state.completedAt - state.startedAt;
|
|
2039
|
+
state.error = "Workflow cancelled by user";
|
|
2040
|
+
return true;
|
|
2041
|
+
}
|
|
2042
|
+
/**
|
|
2043
|
+
* List active runs
|
|
2044
|
+
*/
|
|
2045
|
+
async listActiveRuns() {
|
|
2046
|
+
return Array.from(this.activeRuns.values()).filter((state) => state.status === "running").map((state) => this.buildResponse(state));
|
|
2047
|
+
}
|
|
2048
|
+
/**
|
|
2049
|
+
* Execute a workflow
|
|
2050
|
+
*/
|
|
2051
|
+
async executeWorkflow(state) {
|
|
2052
|
+
const { workflow, request } = state;
|
|
2053
|
+
const params = { ...workflow.defaultParams, ...request?.params };
|
|
2054
|
+
let startIndex = 0;
|
|
2055
|
+
if (request?.startStep) {
|
|
2056
|
+
const idx = workflow.steps.findIndex((s) => s.id === request.startStep);
|
|
2057
|
+
if (idx >= 0) startIndex = idx;
|
|
2058
|
+
}
|
|
2059
|
+
let stopIndex = workflow.steps.length;
|
|
2060
|
+
if (request?.stopStep) {
|
|
2061
|
+
const idx = workflow.steps.findIndex((s) => s.id === request.stopStep);
|
|
2062
|
+
if (idx >= 0) stopIndex = idx + 1;
|
|
2063
|
+
}
|
|
2064
|
+
for (let i = startIndex; i < stopIndex; i++) {
|
|
2065
|
+
if (state.status === "cancelled") break;
|
|
2066
|
+
state.currentStep = i;
|
|
2067
|
+
const step = workflow.steps[i];
|
|
2068
|
+
const stepResult = await this.executeStep(step, params, request?.stepTimeout);
|
|
2069
|
+
state.steps.push(stepResult);
|
|
2070
|
+
if (!stepResult.success) {
|
|
2071
|
+
state.status = "failed";
|
|
2072
|
+
state.error = stepResult.error;
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
state.status = "completed";
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Execute a single step
|
|
2080
|
+
*/
|
|
2081
|
+
async executeStep(step, params, timeout) {
|
|
2082
|
+
const startTime = performance.now();
|
|
2083
|
+
try {
|
|
2084
|
+
const timeoutPromise = timeout ? new Promise(
|
|
2085
|
+
(_, reject) => setTimeout(() => reject(new Error("Step timeout")), timeout)
|
|
2086
|
+
) : null;
|
|
2087
|
+
const executePromise = this.executeStepInternal(step, params);
|
|
2088
|
+
const result = timeoutPromise ? await Promise.race([executePromise, timeoutPromise]) : await executePromise;
|
|
2089
|
+
return {
|
|
2090
|
+
stepId: step.id,
|
|
2091
|
+
stepType: step.type,
|
|
2092
|
+
success: true,
|
|
2093
|
+
result,
|
|
2094
|
+
durationMs: performance.now() - startTime,
|
|
2095
|
+
timestamp: Date.now()
|
|
2096
|
+
};
|
|
2097
|
+
} catch (error) {
|
|
2098
|
+
return {
|
|
2099
|
+
stepId: step.id,
|
|
2100
|
+
stepType: step.type,
|
|
2101
|
+
success: false,
|
|
2102
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2103
|
+
durationMs: performance.now() - startTime,
|
|
2104
|
+
timestamp: Date.now()
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Execute step internal logic
|
|
2110
|
+
*/
|
|
2111
|
+
async executeStepInternal(step, params) {
|
|
2112
|
+
const resolvedParams = this.interpolateParams(step.params || {}, params);
|
|
2113
|
+
switch (step.type) {
|
|
2114
|
+
case "element-action":
|
|
2115
|
+
if (!step.target || !step.action) {
|
|
2116
|
+
throw new Error("Element action requires target and action");
|
|
2117
|
+
}
|
|
2118
|
+
return this.executor.executeAction(step.target, {
|
|
2119
|
+
action: step.action,
|
|
2120
|
+
params: resolvedParams,
|
|
2121
|
+
waitOptions: step.waitOptions
|
|
2122
|
+
});
|
|
2123
|
+
case "component-action":
|
|
2124
|
+
if (!step.target || !step.action) {
|
|
2125
|
+
throw new Error("Component action requires target and action");
|
|
2126
|
+
}
|
|
2127
|
+
return this.executor.executeComponentAction(step.target, {
|
|
2128
|
+
action: step.action,
|
|
2129
|
+
params: resolvedParams
|
|
2130
|
+
});
|
|
2131
|
+
case "wait": {
|
|
2132
|
+
if (!step.target) {
|
|
2133
|
+
throw new Error("Wait step requires target");
|
|
2134
|
+
}
|
|
2135
|
+
const waitResult = await this.executor.waitFor(step.target, step.waitOptions || {});
|
|
2136
|
+
if (!waitResult.met) {
|
|
2137
|
+
throw new Error(waitResult.error || "Wait condition not met");
|
|
2138
|
+
}
|
|
2139
|
+
return waitResult;
|
|
2140
|
+
}
|
|
2141
|
+
case "assert":
|
|
2142
|
+
if (!step.target || !step.expectedState) {
|
|
2143
|
+
throw new Error("Assert step requires target and expectedState");
|
|
2144
|
+
}
|
|
2145
|
+
return this.performAssertion(step.target, step.expectedState);
|
|
2146
|
+
case "custom":
|
|
2147
|
+
if (!step.handler) {
|
|
2148
|
+
throw new Error("Custom step requires handler");
|
|
2149
|
+
}
|
|
2150
|
+
return step.handler();
|
|
2151
|
+
default:
|
|
2152
|
+
throw new Error(`Unknown step type: ${step.type}`);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
/**
|
|
2156
|
+
* Perform state assertion
|
|
2157
|
+
*/
|
|
2158
|
+
async performAssertion(target, expectedState) {
|
|
2159
|
+
const snapshot = await this.executor.getSnapshot();
|
|
2160
|
+
const element = snapshot.elements.find((e) => e.id === target);
|
|
2161
|
+
if (!element) {
|
|
2162
|
+
throw new Error(`Element not found for assertion: ${target}`);
|
|
2163
|
+
}
|
|
2164
|
+
const differences = [];
|
|
2165
|
+
for (const [key, expected] of Object.entries(expectedState)) {
|
|
2166
|
+
const actual = element.state[key];
|
|
2167
|
+
if (actual !== expected) {
|
|
2168
|
+
differences.push(`${key}: expected ${expected}, got ${actual}`);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
if (differences.length > 0) {
|
|
2172
|
+
throw new Error(`Assertion failed:
|
|
2173
|
+
${differences.join("\n")}`);
|
|
2174
|
+
}
|
|
2175
|
+
return { passed: true, differences };
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Interpolate parameters with {{param}} syntax
|
|
2179
|
+
*/
|
|
2180
|
+
interpolateParams(stepParams, workflowParams) {
|
|
2181
|
+
const result = {};
|
|
2182
|
+
for (const [key, value] of Object.entries(stepParams)) {
|
|
2183
|
+
if (typeof value === "string") {
|
|
2184
|
+
result[key] = value.replace(/\{\{(\w+)\}\}/g, (_, name) => {
|
|
2185
|
+
return String(workflowParams[name] ?? "");
|
|
2186
|
+
});
|
|
2187
|
+
} else {
|
|
2188
|
+
result[key] = value;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
return result;
|
|
2192
|
+
}
|
|
2193
|
+
/**
|
|
2194
|
+
* Build response from state
|
|
2195
|
+
*/
|
|
2196
|
+
buildResponse(state) {
|
|
2197
|
+
return {
|
|
2198
|
+
workflowId: state.workflowId,
|
|
2199
|
+
runId: state.runId,
|
|
2200
|
+
status: state.status,
|
|
2201
|
+
steps: [...state.steps],
|
|
2202
|
+
currentStep: state.currentStep,
|
|
2203
|
+
totalSteps: state.workflow.steps.length,
|
|
2204
|
+
success: state.success,
|
|
2205
|
+
error: state.error,
|
|
2206
|
+
startedAt: state.startedAt,
|
|
2207
|
+
completedAt: state.completedAt,
|
|
2208
|
+
durationMs: state.durationMs
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2212
|
+
function createWorkflowEngine(registry, executor) {
|
|
2213
|
+
return new DefaultWorkflowEngine(registry, executor);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// src/render-log/dom-capture.ts
|
|
2217
|
+
var CAPTURE_ATTRIBUTES = [
|
|
2218
|
+
"data-ui-id",
|
|
2219
|
+
"data-testid",
|
|
2220
|
+
"data-awas-element",
|
|
2221
|
+
"id",
|
|
2222
|
+
"name",
|
|
2223
|
+
"type",
|
|
2224
|
+
"href",
|
|
2225
|
+
"src",
|
|
2226
|
+
"alt",
|
|
2227
|
+
"title",
|
|
2228
|
+
"placeholder",
|
|
2229
|
+
"value",
|
|
2230
|
+
"aria-label",
|
|
2231
|
+
"aria-labelledby",
|
|
2232
|
+
"aria-describedby",
|
|
2233
|
+
"aria-expanded",
|
|
2234
|
+
"aria-selected",
|
|
2235
|
+
"aria-checked",
|
|
2236
|
+
"aria-disabled",
|
|
2237
|
+
"aria-hidden",
|
|
2238
|
+
"role",
|
|
2239
|
+
"tabindex",
|
|
2240
|
+
"disabled",
|
|
2241
|
+
"readonly",
|
|
2242
|
+
"required",
|
|
2243
|
+
"checked"
|
|
2244
|
+
];
|
|
2245
|
+
var INTERACTIVE_SELECTORS = [
|
|
2246
|
+
"a[href]",
|
|
2247
|
+
"button",
|
|
2248
|
+
"input",
|
|
2249
|
+
"select",
|
|
2250
|
+
"textarea",
|
|
2251
|
+
"[onclick]",
|
|
2252
|
+
'[role="button"]',
|
|
2253
|
+
'[role="link"]',
|
|
2254
|
+
'[role="checkbox"]',
|
|
2255
|
+
'[role="radio"]',
|
|
2256
|
+
'[role="menuitem"]',
|
|
2257
|
+
'[role="tab"]',
|
|
2258
|
+
'[role="switch"]',
|
|
2259
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
2260
|
+
'[contenteditable="true"]'
|
|
2261
|
+
];
|
|
2262
|
+
function isInteractive(element) {
|
|
2263
|
+
return INTERACTIVE_SELECTORS.some((selector) => {
|
|
2264
|
+
try {
|
|
2265
|
+
return element.matches(selector);
|
|
2266
|
+
} catch {
|
|
2267
|
+
return false;
|
|
2268
|
+
}
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
function getAccessibleName(element) {
|
|
2272
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
2273
|
+
if (ariaLabel) return ariaLabel;
|
|
2274
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
2275
|
+
if (labelledBy) {
|
|
2276
|
+
const labels = labelledBy.split(" ").map((id) => document.getElementById(id)?.textContent?.trim()).filter(Boolean);
|
|
2277
|
+
if (labels.length > 0) return labels.join(" ");
|
|
2278
|
+
}
|
|
2279
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
|
|
2280
|
+
const id = element.id;
|
|
2281
|
+
if (id) {
|
|
2282
|
+
const label = document.querySelector(`label[for="${id}"]`);
|
|
2283
|
+
if (label) return label.textContent?.trim();
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
const title = element.getAttribute("title");
|
|
2287
|
+
if (title) return title;
|
|
2288
|
+
if (element instanceof HTMLImageElement) {
|
|
2289
|
+
return element.alt || void 0;
|
|
2290
|
+
}
|
|
2291
|
+
if (element.matches('button, a, [role="button"], [role="link"]')) {
|
|
2292
|
+
return element.textContent?.trim() || void 0;
|
|
2293
|
+
}
|
|
2294
|
+
return void 0;
|
|
2295
|
+
}
|
|
2296
|
+
function getElementState3(element) {
|
|
2297
|
+
const rect = element.getBoundingClientRect();
|
|
2298
|
+
const style = window.getComputedStyle(element);
|
|
2299
|
+
const state = {
|
|
2300
|
+
visible: isVisible2(element, rect, style),
|
|
2301
|
+
enabled: !isDisabled2(element),
|
|
2302
|
+
focused: document.activeElement === element,
|
|
2303
|
+
rect: {
|
|
2304
|
+
x: rect.x,
|
|
2305
|
+
y: rect.y,
|
|
2306
|
+
width: rect.width,
|
|
2307
|
+
height: rect.height,
|
|
2308
|
+
top: rect.top,
|
|
2309
|
+
right: rect.right,
|
|
2310
|
+
bottom: rect.bottom,
|
|
2311
|
+
left: rect.left
|
|
2312
|
+
},
|
|
2313
|
+
computedStyles: {
|
|
2314
|
+
display: style.display,
|
|
2315
|
+
visibility: style.visibility,
|
|
2316
|
+
opacity: style.opacity,
|
|
2317
|
+
pointerEvents: style.pointerEvents
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
if (element instanceof HTMLInputElement) {
|
|
2321
|
+
state.value = element.value;
|
|
2322
|
+
if (element.type === "checkbox" || element.type === "radio") {
|
|
2323
|
+
state.checked = element.checked;
|
|
2324
|
+
}
|
|
2325
|
+
} else if (element instanceof HTMLTextAreaElement) {
|
|
2326
|
+
state.value = element.value;
|
|
2327
|
+
} else if (element instanceof HTMLSelectElement) {
|
|
2328
|
+
state.value = element.value;
|
|
2329
|
+
state.selectedOptions = Array.from(element.selectedOptions).map((opt) => opt.value);
|
|
2330
|
+
}
|
|
2331
|
+
return state;
|
|
2332
|
+
}
|
|
2333
|
+
function isVisible2(element, rect, style) {
|
|
2334
|
+
if (rect.width === 0 || rect.height === 0) return false;
|
|
2335
|
+
if (style.display === "none") return false;
|
|
2336
|
+
if (style.visibility === "hidden") return false;
|
|
2337
|
+
if (parseFloat(style.opacity) === 0) return false;
|
|
2338
|
+
if (element.getAttribute("aria-hidden") === "true") return false;
|
|
2339
|
+
return rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0;
|
|
2340
|
+
}
|
|
2341
|
+
function isDisabled2(element) {
|
|
2342
|
+
if ("disabled" in element && element.disabled) return true;
|
|
2343
|
+
if (element.getAttribute("aria-disabled") === "true") return true;
|
|
2344
|
+
return false;
|
|
2345
|
+
}
|
|
2346
|
+
function captureAttributes(element) {
|
|
2347
|
+
const attrs = {};
|
|
2348
|
+
for (const attr of CAPTURE_ATTRIBUTES) {
|
|
2349
|
+
const value = element.getAttribute(attr);
|
|
2350
|
+
if (value !== null) {
|
|
2351
|
+
attrs[attr] = value;
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
return attrs;
|
|
2355
|
+
}
|
|
2356
|
+
function captureElement(element, depth, maxTextLength) {
|
|
2357
|
+
const identifier = createElementIdentifier(element);
|
|
2358
|
+
let textContent = element.textContent?.trim();
|
|
2359
|
+
if (textContent && textContent.length > maxTextLength) {
|
|
2360
|
+
textContent = textContent.substring(0, maxTextLength) + "...";
|
|
2361
|
+
}
|
|
2362
|
+
return {
|
|
2363
|
+
identifier,
|
|
2364
|
+
bestId: getBestIdentifier(element),
|
|
2365
|
+
tagName: element.tagName.toLowerCase(),
|
|
2366
|
+
role: element.getAttribute("role") || void 0,
|
|
2367
|
+
accessibleName: getAccessibleName(element),
|
|
2368
|
+
textContent,
|
|
2369
|
+
state: getElementState3(element),
|
|
2370
|
+
attributes: captureAttributes(element),
|
|
2371
|
+
childCount: element.children.length,
|
|
2372
|
+
depth
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
function captureDOMSnapshot(options = {}) {
|
|
2376
|
+
const startTime = performance.now();
|
|
2377
|
+
const {
|
|
2378
|
+
root = document.body,
|
|
2379
|
+
maxDepth = 50,
|
|
2380
|
+
maxElements = 5e3,
|
|
2381
|
+
interactiveOnly = false,
|
|
2382
|
+
includeHidden = false,
|
|
2383
|
+
includeSelectors,
|
|
2384
|
+
excludeSelectors,
|
|
2385
|
+
filter,
|
|
2386
|
+
maxTextLength = 200
|
|
2387
|
+
} = options;
|
|
2388
|
+
const elements = [];
|
|
2389
|
+
let totalNodeCount = 0;
|
|
2390
|
+
function shouldCapture(element) {
|
|
2391
|
+
if (filter && !filter(element)) return false;
|
|
2392
|
+
if (excludeSelectors) {
|
|
2393
|
+
for (const selector of excludeSelectors) {
|
|
2394
|
+
try {
|
|
2395
|
+
if (element.matches(selector)) return false;
|
|
2396
|
+
} catch {
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
if (includeSelectors && includeSelectors.length > 0) {
|
|
2401
|
+
let matches = false;
|
|
2402
|
+
for (const selector of includeSelectors) {
|
|
2403
|
+
try {
|
|
2404
|
+
if (element.matches(selector)) {
|
|
2405
|
+
matches = true;
|
|
2406
|
+
break;
|
|
2407
|
+
}
|
|
2408
|
+
} catch {
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
if (!matches) return false;
|
|
2412
|
+
}
|
|
2413
|
+
if (interactiveOnly && !isInteractive(element)) return false;
|
|
2414
|
+
if (!includeHidden) {
|
|
2415
|
+
const rect = element.getBoundingClientRect();
|
|
2416
|
+
const style = window.getComputedStyle(element);
|
|
2417
|
+
if (!isVisible2(element, rect, style)) return false;
|
|
2418
|
+
}
|
|
2419
|
+
return true;
|
|
2420
|
+
}
|
|
2421
|
+
function traverse(element, depth) {
|
|
2422
|
+
if (depth > maxDepth || elements.length >= maxElements) return;
|
|
2423
|
+
totalNodeCount++;
|
|
2424
|
+
if (shouldCapture(element)) {
|
|
2425
|
+
elements.push(captureElement(element, depth, maxTextLength));
|
|
2426
|
+
}
|
|
2427
|
+
for (const child of element.children) {
|
|
2428
|
+
if (child instanceof HTMLElement) {
|
|
2429
|
+
traverse(child, depth + 1);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
traverse(root, 0);
|
|
2434
|
+
const endTime = performance.now();
|
|
2435
|
+
return {
|
|
2436
|
+
timestamp: Date.now(),
|
|
2437
|
+
url: window.location.href,
|
|
2438
|
+
title: document.title,
|
|
2439
|
+
viewport: {
|
|
2440
|
+
width: window.innerWidth,
|
|
2441
|
+
height: window.innerHeight,
|
|
2442
|
+
scrollX: window.scrollX,
|
|
2443
|
+
scrollY: window.scrollY
|
|
2444
|
+
},
|
|
2445
|
+
elements,
|
|
2446
|
+
totalNodeCount,
|
|
2447
|
+
captureDurationMs: endTime - startTime
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
var DOMChangeObserver = class {
|
|
2451
|
+
constructor(options = {}) {
|
|
2452
|
+
this.observer = null;
|
|
2453
|
+
this.changes = [];
|
|
2454
|
+
this.maxChanges = options.maxChanges ?? 1e3;
|
|
2455
|
+
this.callback = options.callback;
|
|
2456
|
+
}
|
|
2457
|
+
start(root = document.body) {
|
|
2458
|
+
if (this.observer) return;
|
|
2459
|
+
this.observer = new MutationObserver((mutations) => {
|
|
2460
|
+
for (const mutation of mutations) {
|
|
2461
|
+
const change = this.processMutation(mutation);
|
|
2462
|
+
if (change) {
|
|
2463
|
+
this.addChange(change);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
});
|
|
2467
|
+
this.observer.observe(root, {
|
|
2468
|
+
childList: true,
|
|
2469
|
+
attributes: true,
|
|
2470
|
+
characterData: true,
|
|
2471
|
+
subtree: true,
|
|
2472
|
+
attributeOldValue: true
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
stop() {
|
|
2476
|
+
this.observer?.disconnect();
|
|
2477
|
+
this.observer = null;
|
|
2478
|
+
}
|
|
2479
|
+
processMutation(mutation) {
|
|
2480
|
+
const target = mutation.target;
|
|
2481
|
+
if (!(target instanceof HTMLElement)) return null;
|
|
2482
|
+
const elementId = getBestIdentifier(target);
|
|
2483
|
+
if (mutation.type === "attributes") {
|
|
2484
|
+
return {
|
|
2485
|
+
timestamp: Date.now(),
|
|
2486
|
+
type: "attribute",
|
|
2487
|
+
elementId,
|
|
2488
|
+
tagName: target.tagName.toLowerCase(),
|
|
2489
|
+
details: {
|
|
2490
|
+
attributeName: mutation.attributeName || void 0,
|
|
2491
|
+
oldValue: mutation.oldValue || void 0,
|
|
2492
|
+
newValue: mutation.attributeName ? target.getAttribute(mutation.attributeName) || void 0 : void 0
|
|
2493
|
+
}
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
if (mutation.type === "childList") {
|
|
2497
|
+
if (mutation.addedNodes.length > 0) {
|
|
2498
|
+
return {
|
|
2499
|
+
timestamp: Date.now(),
|
|
2500
|
+
type: "added",
|
|
2501
|
+
elementId,
|
|
2502
|
+
tagName: target.tagName.toLowerCase(),
|
|
2503
|
+
details: {
|
|
2504
|
+
addedNodes: mutation.addedNodes.length
|
|
2505
|
+
}
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
if (mutation.removedNodes.length > 0) {
|
|
2509
|
+
return {
|
|
2510
|
+
timestamp: Date.now(),
|
|
2511
|
+
type: "removed",
|
|
2512
|
+
elementId,
|
|
2513
|
+
tagName: target.tagName.toLowerCase(),
|
|
2514
|
+
details: {
|
|
2515
|
+
removedNodes: mutation.removedNodes.length
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
return null;
|
|
2521
|
+
}
|
|
2522
|
+
addChange(change) {
|
|
2523
|
+
this.changes.push(change);
|
|
2524
|
+
if (this.changes.length > this.maxChanges) {
|
|
2525
|
+
this.changes.shift();
|
|
2526
|
+
}
|
|
2527
|
+
this.callback?.(change);
|
|
2528
|
+
}
|
|
2529
|
+
getChanges() {
|
|
2530
|
+
return [...this.changes];
|
|
2531
|
+
}
|
|
2532
|
+
clearChanges() {
|
|
2533
|
+
this.changes = [];
|
|
2534
|
+
}
|
|
2535
|
+
};
|
|
2536
|
+
|
|
2537
|
+
// src/render-log/snapshot.ts
|
|
2538
|
+
var InMemoryRenderLogStorage = class {
|
|
2539
|
+
constructor(maxEntries = 1e3) {
|
|
2540
|
+
this.entries = [];
|
|
2541
|
+
this.maxEntries = maxEntries;
|
|
2542
|
+
}
|
|
2543
|
+
async append(entry) {
|
|
2544
|
+
this.entries.push(entry);
|
|
2545
|
+
while (this.entries.length > this.maxEntries) {
|
|
2546
|
+
this.entries.shift();
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
async getEntries(options) {
|
|
2550
|
+
let results = [...this.entries];
|
|
2551
|
+
if (options?.type) {
|
|
2552
|
+
results = results.filter((e) => e.type === options.type);
|
|
2553
|
+
}
|
|
2554
|
+
if (options?.since) {
|
|
2555
|
+
results = results.filter((e) => e.timestamp >= options.since);
|
|
2556
|
+
}
|
|
2557
|
+
if (options?.until) {
|
|
2558
|
+
results = results.filter((e) => e.timestamp <= options.until);
|
|
2559
|
+
}
|
|
2560
|
+
if (options?.limit) {
|
|
2561
|
+
results = results.slice(-options.limit);
|
|
2562
|
+
}
|
|
2563
|
+
return results;
|
|
2564
|
+
}
|
|
2565
|
+
async clear() {
|
|
2566
|
+
this.entries = [];
|
|
2567
|
+
}
|
|
2568
|
+
async count() {
|
|
2569
|
+
return this.entries.length;
|
|
2570
|
+
}
|
|
2571
|
+
/** Get entries synchronously (for in-memory only) */
|
|
2572
|
+
getEntriesSync() {
|
|
2573
|
+
return [...this.entries];
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2576
|
+
function generateId2() {
|
|
2577
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
2578
|
+
}
|
|
2579
|
+
var RenderLogManager = class {
|
|
2580
|
+
constructor(options = {}) {
|
|
2581
|
+
this.changeObserver = null;
|
|
2582
|
+
this.snapshotTimer = null;
|
|
2583
|
+
this.pendingChanges = [];
|
|
2584
|
+
this.started = false;
|
|
2585
|
+
this.options = options;
|
|
2586
|
+
this.storage = options.storage ?? new InMemoryRenderLogStorage(options.maxEntries);
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Start capturing
|
|
2590
|
+
*/
|
|
2591
|
+
start() {
|
|
2592
|
+
if (this.started) return;
|
|
2593
|
+
this.started = true;
|
|
2594
|
+
if (this.options.captureChanges !== false) {
|
|
2595
|
+
this.changeObserver = new DOMChangeObserver({
|
|
2596
|
+
callback: (change) => {
|
|
2597
|
+
this.pendingChanges.push(change);
|
|
2598
|
+
}
|
|
2599
|
+
});
|
|
2600
|
+
this.changeObserver.start();
|
|
2601
|
+
}
|
|
2602
|
+
if (this.options.captureOnNavigation !== false) {
|
|
2603
|
+
this.setupNavigationObserver();
|
|
2604
|
+
}
|
|
2605
|
+
if (this.options.snapshotInterval) {
|
|
2606
|
+
this.snapshotTimer = setInterval(() => {
|
|
2607
|
+
this.captureSnapshot();
|
|
2608
|
+
}, this.options.snapshotInterval);
|
|
2609
|
+
}
|
|
2610
|
+
this.captureSnapshot();
|
|
2611
|
+
}
|
|
2612
|
+
/**
|
|
2613
|
+
* Stop capturing
|
|
2614
|
+
*/
|
|
2615
|
+
stop() {
|
|
2616
|
+
if (!this.started) return;
|
|
2617
|
+
this.started = false;
|
|
2618
|
+
this.changeObserver?.stop();
|
|
2619
|
+
this.changeObserver = null;
|
|
2620
|
+
if (this.snapshotTimer) {
|
|
2621
|
+
clearInterval(this.snapshotTimer);
|
|
2622
|
+
this.snapshotTimer = null;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
/**
|
|
2626
|
+
* Capture a DOM snapshot
|
|
2627
|
+
*/
|
|
2628
|
+
async captureSnapshot(metadata) {
|
|
2629
|
+
if (this.pendingChanges.length > 0) {
|
|
2630
|
+
await this.flushChanges();
|
|
2631
|
+
}
|
|
2632
|
+
const snapshot = captureDOMSnapshot(this.options.captureOptions);
|
|
2633
|
+
const entry = {
|
|
2634
|
+
id: generateId2(),
|
|
2635
|
+
type: "snapshot",
|
|
2636
|
+
timestamp: snapshot.timestamp,
|
|
2637
|
+
data: snapshot,
|
|
2638
|
+
metadata
|
|
2639
|
+
};
|
|
2640
|
+
await this.addEntry(entry);
|
|
2641
|
+
return entry;
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Flush pending DOM changes
|
|
2645
|
+
*/
|
|
2646
|
+
async flushChanges() {
|
|
2647
|
+
if (this.pendingChanges.length === 0) return null;
|
|
2648
|
+
const changes = [...this.pendingChanges];
|
|
2649
|
+
this.pendingChanges = [];
|
|
2650
|
+
const entry = {
|
|
2651
|
+
id: generateId2(),
|
|
2652
|
+
type: "change",
|
|
2653
|
+
timestamp: Date.now(),
|
|
2654
|
+
data: changes
|
|
2655
|
+
};
|
|
2656
|
+
await this.addEntry(entry);
|
|
2657
|
+
return entry;
|
|
2658
|
+
}
|
|
2659
|
+
/**
|
|
2660
|
+
* Log an interaction
|
|
2661
|
+
*/
|
|
2662
|
+
async logInteraction(eventType, details) {
|
|
2663
|
+
const entry = {
|
|
2664
|
+
id: generateId2(),
|
|
2665
|
+
type: "interaction",
|
|
2666
|
+
timestamp: Date.now(),
|
|
2667
|
+
data: {
|
|
2668
|
+
eventType,
|
|
2669
|
+
...details
|
|
2670
|
+
}
|
|
2671
|
+
};
|
|
2672
|
+
await this.addEntry(entry);
|
|
2673
|
+
return entry;
|
|
2674
|
+
}
|
|
2675
|
+
/**
|
|
2676
|
+
* Log an error
|
|
2677
|
+
*/
|
|
2678
|
+
async logError(message, details) {
|
|
2679
|
+
const entry = {
|
|
2680
|
+
id: generateId2(),
|
|
2681
|
+
type: "error",
|
|
2682
|
+
timestamp: Date.now(),
|
|
2683
|
+
data: {
|
|
2684
|
+
message,
|
|
2685
|
+
...details
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2688
|
+
await this.addEntry(entry);
|
|
2689
|
+
return entry;
|
|
2690
|
+
}
|
|
2691
|
+
/**
|
|
2692
|
+
* Log a navigation
|
|
2693
|
+
*/
|
|
2694
|
+
async logNavigation(from, to, navigationType) {
|
|
2695
|
+
const entry = {
|
|
2696
|
+
id: generateId2(),
|
|
2697
|
+
type: "navigation",
|
|
2698
|
+
timestamp: Date.now(),
|
|
2699
|
+
data: {
|
|
2700
|
+
from,
|
|
2701
|
+
to,
|
|
2702
|
+
navigationType
|
|
2703
|
+
}
|
|
2704
|
+
};
|
|
2705
|
+
await this.addEntry(entry);
|
|
2706
|
+
return entry;
|
|
2707
|
+
}
|
|
2708
|
+
/**
|
|
2709
|
+
* Add a custom entry
|
|
2710
|
+
*/
|
|
2711
|
+
async logCustom(data, metadata) {
|
|
2712
|
+
const entry = {
|
|
2713
|
+
id: generateId2(),
|
|
2714
|
+
type: "custom",
|
|
2715
|
+
timestamp: Date.now(),
|
|
2716
|
+
data,
|
|
2717
|
+
metadata
|
|
2718
|
+
};
|
|
2719
|
+
await this.addEntry(entry);
|
|
2720
|
+
return entry;
|
|
2721
|
+
}
|
|
2722
|
+
/**
|
|
2723
|
+
* Get log entries
|
|
2724
|
+
*/
|
|
2725
|
+
async getEntries(options) {
|
|
2726
|
+
return this.storage.getEntries(options);
|
|
2727
|
+
}
|
|
2728
|
+
/**
|
|
2729
|
+
* Clear the log
|
|
2730
|
+
*/
|
|
2731
|
+
async clear() {
|
|
2732
|
+
this.pendingChanges = [];
|
|
2733
|
+
await this.storage.clear();
|
|
2734
|
+
}
|
|
2735
|
+
/**
|
|
2736
|
+
* Get entry count
|
|
2737
|
+
*/
|
|
2738
|
+
async count() {
|
|
2739
|
+
return this.storage.count();
|
|
2740
|
+
}
|
|
2741
|
+
/**
|
|
2742
|
+
* Get the latest snapshot
|
|
2743
|
+
*/
|
|
2744
|
+
async getLatestSnapshot() {
|
|
2745
|
+
const snapshots = await this.storage.getEntries({ type: "snapshot", limit: 1 });
|
|
2746
|
+
return snapshots[0] || null;
|
|
2747
|
+
}
|
|
2748
|
+
async addEntry(entry) {
|
|
2749
|
+
await this.storage.append(entry);
|
|
2750
|
+
this.options.onEntry?.(entry);
|
|
2751
|
+
}
|
|
2752
|
+
setupNavigationObserver() {
|
|
2753
|
+
let lastUrl = window.location.href;
|
|
2754
|
+
const originalPushState = history.pushState;
|
|
2755
|
+
const originalReplaceState = history.replaceState;
|
|
2756
|
+
history.pushState = (...args) => {
|
|
2757
|
+
const result = originalPushState.apply(history, args);
|
|
2758
|
+
const newUrl = window.location.href;
|
|
2759
|
+
if (newUrl !== lastUrl) {
|
|
2760
|
+
this.logNavigation(lastUrl, newUrl, "push");
|
|
2761
|
+
this.captureSnapshot({ trigger: "navigation" });
|
|
2762
|
+
lastUrl = newUrl;
|
|
2763
|
+
}
|
|
2764
|
+
return result;
|
|
2765
|
+
};
|
|
2766
|
+
history.replaceState = (...args) => {
|
|
2767
|
+
const result = originalReplaceState.apply(history, args);
|
|
2768
|
+
const newUrl = window.location.href;
|
|
2769
|
+
if (newUrl !== lastUrl) {
|
|
2770
|
+
this.logNavigation(lastUrl, newUrl, "replace");
|
|
2771
|
+
this.captureSnapshot({ trigger: "navigation" });
|
|
2772
|
+
lastUrl = newUrl;
|
|
2773
|
+
}
|
|
2774
|
+
return result;
|
|
2775
|
+
};
|
|
2776
|
+
window.addEventListener("popstate", () => {
|
|
2777
|
+
const newUrl = window.location.href;
|
|
2778
|
+
if (newUrl !== lastUrl) {
|
|
2779
|
+
this.logNavigation(lastUrl, newUrl, "pop");
|
|
2780
|
+
this.captureSnapshot({ trigger: "navigation" });
|
|
2781
|
+
lastUrl = newUrl;
|
|
2782
|
+
}
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
};
|
|
2786
|
+
function createRenderLogManager(options) {
|
|
2787
|
+
return new RenderLogManager(options);
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// src/debug/metrics.ts
|
|
2791
|
+
function generateId3() {
|
|
2792
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 8)}`;
|
|
2793
|
+
}
|
|
2794
|
+
var MetricsCollector = class {
|
|
2795
|
+
constructor(options = {}) {
|
|
2796
|
+
this.history = [];
|
|
2797
|
+
this.maxHistoryEntries = options.maxHistoryEntries ?? 1e3;
|
|
2798
|
+
this.rateWindow = options.rateWindow ?? 6e4;
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* Record an element action
|
|
2802
|
+
*/
|
|
2803
|
+
recordElementAction(target, action, response, params) {
|
|
2804
|
+
const entry = {
|
|
2805
|
+
id: generateId3(),
|
|
2806
|
+
timestamp: response.timestamp,
|
|
2807
|
+
type: "element",
|
|
2808
|
+
target,
|
|
2809
|
+
action,
|
|
2810
|
+
success: response.success,
|
|
2811
|
+
durationMs: response.durationMs,
|
|
2812
|
+
error: response.error,
|
|
2813
|
+
params,
|
|
2814
|
+
response: response.elementState
|
|
2815
|
+
};
|
|
2816
|
+
this.addEntry(entry);
|
|
2817
|
+
return entry;
|
|
2818
|
+
}
|
|
2819
|
+
/**
|
|
2820
|
+
* Record a component action
|
|
2821
|
+
*/
|
|
2822
|
+
recordComponentAction(target, action, response, params) {
|
|
2823
|
+
const entry = {
|
|
2824
|
+
id: generateId3(),
|
|
2825
|
+
timestamp: response.timestamp,
|
|
2826
|
+
type: "component",
|
|
2827
|
+
target,
|
|
2828
|
+
action,
|
|
2829
|
+
success: response.success,
|
|
2830
|
+
durationMs: response.durationMs,
|
|
2831
|
+
error: response.error,
|
|
2832
|
+
params,
|
|
2833
|
+
response: response.result
|
|
2834
|
+
};
|
|
2835
|
+
this.addEntry(entry);
|
|
2836
|
+
return entry;
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* Record a workflow step
|
|
2840
|
+
*/
|
|
2841
|
+
recordWorkflowStep(workflowId, result) {
|
|
2842
|
+
const entry = {
|
|
2843
|
+
id: generateId3(),
|
|
2844
|
+
timestamp: result.timestamp,
|
|
2845
|
+
type: "workflow-step",
|
|
2846
|
+
target: workflowId,
|
|
2847
|
+
action: result.stepId,
|
|
2848
|
+
success: result.success,
|
|
2849
|
+
durationMs: result.durationMs,
|
|
2850
|
+
error: result.error,
|
|
2851
|
+
response: result.result
|
|
2852
|
+
};
|
|
2853
|
+
this.addEntry(entry);
|
|
2854
|
+
return entry;
|
|
2855
|
+
}
|
|
2856
|
+
/**
|
|
2857
|
+
* Record from a bridge event
|
|
2858
|
+
*/
|
|
2859
|
+
recordEvent(event) {
|
|
2860
|
+
if (event.type === "action:completed" || event.type === "action:failed") {
|
|
2861
|
+
const data = event.data;
|
|
2862
|
+
if (data.elementId) {
|
|
2863
|
+
this.recordElementAction(
|
|
2864
|
+
data.elementId,
|
|
2865
|
+
data.action,
|
|
2866
|
+
data.response,
|
|
2867
|
+
data.params
|
|
2868
|
+
);
|
|
2869
|
+
} else if (data.componentId) {
|
|
2870
|
+
this.recordComponentAction(
|
|
2871
|
+
data.componentId,
|
|
2872
|
+
data.action,
|
|
2873
|
+
data.response,
|
|
2874
|
+
data.params
|
|
2875
|
+
);
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
/**
|
|
2880
|
+
* Get action history
|
|
2881
|
+
*/
|
|
2882
|
+
getHistory(options) {
|
|
2883
|
+
let results = [...this.history];
|
|
2884
|
+
if (options?.type) {
|
|
2885
|
+
results = results.filter((e) => e.type === options.type);
|
|
2886
|
+
}
|
|
2887
|
+
if (options?.target) {
|
|
2888
|
+
results = results.filter((e) => e.target === options.target);
|
|
2889
|
+
}
|
|
2890
|
+
if (options?.action) {
|
|
2891
|
+
results = results.filter((e) => e.action === options.action);
|
|
2892
|
+
}
|
|
2893
|
+
if (options?.success !== void 0) {
|
|
2894
|
+
results = results.filter((e) => e.success === options.success);
|
|
2895
|
+
}
|
|
2896
|
+
if (options?.since) {
|
|
2897
|
+
results = results.filter((e) => e.timestamp >= options.since);
|
|
2898
|
+
}
|
|
2899
|
+
if (options?.limit) {
|
|
2900
|
+
results = results.slice(-options.limit);
|
|
2901
|
+
}
|
|
2902
|
+
return results;
|
|
2903
|
+
}
|
|
2904
|
+
/**
|
|
2905
|
+
* Get performance metrics
|
|
2906
|
+
*/
|
|
2907
|
+
getMetrics(since) {
|
|
2908
|
+
const entries = since ? this.history.filter((e) => e.timestamp >= since) : this.history;
|
|
2909
|
+
if (entries.length === 0) {
|
|
2910
|
+
return {
|
|
2911
|
+
totalActions: 0,
|
|
2912
|
+
successfulActions: 0,
|
|
2913
|
+
failedActions: 0,
|
|
2914
|
+
successRate: 0,
|
|
2915
|
+
avgDurationMs: 0,
|
|
2916
|
+
minDurationMs: 0,
|
|
2917
|
+
maxDurationMs: 0,
|
|
2918
|
+
p95DurationMs: 0,
|
|
2919
|
+
actionsPerSecond: 0,
|
|
2920
|
+
errorsByType: {},
|
|
2921
|
+
actionsByType: {}
|
|
2922
|
+
};
|
|
2923
|
+
}
|
|
2924
|
+
const successful = entries.filter((e) => e.success);
|
|
2925
|
+
const failed = entries.filter((e) => !e.success);
|
|
2926
|
+
const durations = entries.map((e) => e.durationMs).sort((a, b) => a - b);
|
|
2927
|
+
const now = Date.now();
|
|
2928
|
+
const windowStart = now - this.rateWindow;
|
|
2929
|
+
const recentActions = this.history.filter((e) => e.timestamp >= windowStart);
|
|
2930
|
+
const windowSeconds = this.rateWindow / 1e3;
|
|
2931
|
+
const errorsByType = {};
|
|
2932
|
+
for (const entry of failed) {
|
|
2933
|
+
const errorType = entry.error?.split(":")[0] || "Unknown";
|
|
2934
|
+
errorsByType[errorType] = (errorsByType[errorType] || 0) + 1;
|
|
2935
|
+
}
|
|
2936
|
+
const actionsByType = {};
|
|
2937
|
+
for (const entry of entries) {
|
|
2938
|
+
const key = `${entry.type}:${entry.action}`;
|
|
2939
|
+
actionsByType[key] = (actionsByType[key] || 0) + 1;
|
|
2940
|
+
}
|
|
2941
|
+
return {
|
|
2942
|
+
totalActions: entries.length,
|
|
2943
|
+
successfulActions: successful.length,
|
|
2944
|
+
failedActions: failed.length,
|
|
2945
|
+
successRate: successful.length / entries.length,
|
|
2946
|
+
avgDurationMs: durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
2947
|
+
minDurationMs: durations[0],
|
|
2948
|
+
maxDurationMs: durations[durations.length - 1],
|
|
2949
|
+
p95DurationMs: durations[Math.floor(durations.length * 0.95)],
|
|
2950
|
+
actionsPerSecond: recentActions.length / windowSeconds,
|
|
2951
|
+
errorsByType,
|
|
2952
|
+
actionsByType
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
/**
|
|
2956
|
+
* Get recent errors
|
|
2957
|
+
*/
|
|
2958
|
+
getRecentErrors(limit = 10) {
|
|
2959
|
+
return this.history.filter((e) => !e.success).slice(-limit);
|
|
2960
|
+
}
|
|
2961
|
+
/**
|
|
2962
|
+
* Get slowest actions
|
|
2963
|
+
*/
|
|
2964
|
+
getSlowestActions(limit = 10) {
|
|
2965
|
+
return [...this.history].sort((a, b) => b.durationMs - a.durationMs).slice(0, limit);
|
|
2966
|
+
}
|
|
2967
|
+
/**
|
|
2968
|
+
* Clear history
|
|
2969
|
+
*/
|
|
2970
|
+
clearHistory() {
|
|
2971
|
+
this.history = [];
|
|
2972
|
+
}
|
|
2973
|
+
/**
|
|
2974
|
+
* Export history as JSON
|
|
2975
|
+
*/
|
|
2976
|
+
exportHistory() {
|
|
2977
|
+
return JSON.stringify(this.history, null, 2);
|
|
2978
|
+
}
|
|
2979
|
+
/**
|
|
2980
|
+
* Import history from JSON
|
|
2981
|
+
*/
|
|
2982
|
+
importHistory(json) {
|
|
2983
|
+
const entries = JSON.parse(json);
|
|
2984
|
+
this.history = entries.slice(-this.maxHistoryEntries);
|
|
2985
|
+
}
|
|
2986
|
+
addEntry(entry) {
|
|
2987
|
+
this.history.push(entry);
|
|
2988
|
+
while (this.history.length > this.maxHistoryEntries) {
|
|
2989
|
+
this.history.shift();
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
};
|
|
2993
|
+
function createMetricsCollector(options) {
|
|
2994
|
+
return new MetricsCollector(options);
|
|
2995
|
+
}
|
|
2996
|
+
var UIBridgeContext = createContext(null);
|
|
2997
|
+
function UIBridgeProvider({
|
|
2998
|
+
children,
|
|
2999
|
+
features = {},
|
|
3000
|
+
config = {},
|
|
3001
|
+
onEvent
|
|
3002
|
+
}) {
|
|
3003
|
+
const registryRef = useRef(null);
|
|
3004
|
+
const renderLogRef = useRef(null);
|
|
3005
|
+
const metricsRef = useRef(null);
|
|
3006
|
+
const wsClientRef = useRef(null);
|
|
3007
|
+
const [wsConnectionState, setWsConnectionState] = useState("disconnected");
|
|
3008
|
+
if (!registryRef.current) {
|
|
3009
|
+
registryRef.current = new UIBridgeRegistry({
|
|
3010
|
+
verbose: config.verbose,
|
|
3011
|
+
onEvent
|
|
3012
|
+
});
|
|
3013
|
+
setGlobalRegistry(registryRef.current);
|
|
3014
|
+
if (features.renderLog) {
|
|
3015
|
+
renderLogRef.current = createRenderLogManager({
|
|
3016
|
+
maxEntries: config.maxLogEntries
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
if (features.debug) {
|
|
3020
|
+
metricsRef.current = createMetricsCollector();
|
|
3021
|
+
}
|
|
3022
|
+
if (config.websocket) {
|
|
3023
|
+
const wsPort = config.websocketPort || config.serverPort || 9876;
|
|
3024
|
+
const wsUrl = `ws://localhost:${wsPort}`;
|
|
3025
|
+
wsClientRef.current = createWSClient({
|
|
3026
|
+
url: wsUrl,
|
|
3027
|
+
autoReconnect: true,
|
|
3028
|
+
reconnectDelay: 1e3,
|
|
3029
|
+
maxReconnectAttempts: 10,
|
|
3030
|
+
pingInterval: 3e4
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
const registry = registryRef.current;
|
|
3035
|
+
const renderLog = renderLogRef.current || void 0;
|
|
3036
|
+
const metrics = metricsRef.current || void 0;
|
|
3037
|
+
const wsClient = wsClientRef.current || void 0;
|
|
3038
|
+
const executor = useMemo(() => createActionExecutor(registry), [registry]);
|
|
3039
|
+
const workflowEngine = useMemo(
|
|
3040
|
+
() => createWorkflowEngine(registry, executor),
|
|
3041
|
+
[registry, executor]
|
|
3042
|
+
);
|
|
3043
|
+
useEffect(() => {
|
|
3044
|
+
if (features.renderLog && renderLog) {
|
|
3045
|
+
renderLog.start();
|
|
3046
|
+
return () => renderLog.stop();
|
|
3047
|
+
}
|
|
3048
|
+
}, [features.renderLog, renderLog]);
|
|
3049
|
+
useEffect(() => {
|
|
3050
|
+
if (!metrics) return;
|
|
3051
|
+
const unsubCompleted = registry.on("action:completed", (event) => {
|
|
3052
|
+
metrics.recordEvent(event);
|
|
3053
|
+
});
|
|
3054
|
+
const unsubFailed = registry.on("action:failed", (event) => {
|
|
3055
|
+
metrics.recordEvent(event);
|
|
3056
|
+
});
|
|
3057
|
+
return () => {
|
|
3058
|
+
unsubCompleted();
|
|
3059
|
+
unsubFailed();
|
|
3060
|
+
};
|
|
3061
|
+
}, [registry, metrics]);
|
|
3062
|
+
useEffect(() => {
|
|
3063
|
+
if (!wsClient) return;
|
|
3064
|
+
const unsubscribe = wsClient.onConnectionChange((state) => {
|
|
3065
|
+
setWsConnectionState(state);
|
|
3066
|
+
});
|
|
3067
|
+
return unsubscribe;
|
|
3068
|
+
}, [wsClient]);
|
|
3069
|
+
useEffect(() => {
|
|
3070
|
+
return () => {
|
|
3071
|
+
renderLog?.stop();
|
|
3072
|
+
wsClient?.disconnect();
|
|
3073
|
+
resetGlobalRegistry();
|
|
3074
|
+
};
|
|
3075
|
+
}, [renderLog, wsClient]);
|
|
3076
|
+
const wsConnect = useCallback(async () => {
|
|
3077
|
+
if (wsClient) {
|
|
3078
|
+
await wsClient.connect();
|
|
3079
|
+
}
|
|
3080
|
+
}, [wsClient]);
|
|
3081
|
+
const wsDisconnect = useCallback(() => {
|
|
3082
|
+
wsClient?.disconnect();
|
|
3083
|
+
}, [wsClient]);
|
|
3084
|
+
const wsSubscribe = useCallback(
|
|
3085
|
+
async (options) => {
|
|
3086
|
+
if (!wsClient) {
|
|
3087
|
+
return [];
|
|
3088
|
+
}
|
|
3089
|
+
return wsClient.subscribe(options);
|
|
3090
|
+
},
|
|
3091
|
+
[wsClient]
|
|
3092
|
+
);
|
|
3093
|
+
const onWsEvent = useCallback(
|
|
3094
|
+
(eventType, listener) => {
|
|
3095
|
+
if (!wsClient) {
|
|
3096
|
+
return () => {
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
return wsClient.onEvent(eventType, listener);
|
|
3100
|
+
},
|
|
3101
|
+
[wsClient]
|
|
3102
|
+
);
|
|
3103
|
+
const getElements = useCallback(() => registry.getAllElements(), [registry]);
|
|
3104
|
+
const getComponents = useCallback(() => registry.getAllComponents(), [registry]);
|
|
3105
|
+
const createSnapshot = useCallback(() => registry.createSnapshot(), [registry]);
|
|
3106
|
+
const on = useCallback(
|
|
3107
|
+
(type, listener) => registry.on(type, listener),
|
|
3108
|
+
[registry]
|
|
3109
|
+
);
|
|
3110
|
+
const off = useCallback(
|
|
3111
|
+
(type, listener) => registry.off(type, listener),
|
|
3112
|
+
[registry]
|
|
3113
|
+
);
|
|
3114
|
+
const contextValue = useMemo(
|
|
3115
|
+
() => ({
|
|
3116
|
+
features,
|
|
3117
|
+
config,
|
|
3118
|
+
registry,
|
|
3119
|
+
executor,
|
|
3120
|
+
workflowEngine,
|
|
3121
|
+
renderLog,
|
|
3122
|
+
metrics,
|
|
3123
|
+
wsClient,
|
|
3124
|
+
wsConnectionState,
|
|
3125
|
+
getElements,
|
|
3126
|
+
getComponents,
|
|
3127
|
+
createSnapshot,
|
|
3128
|
+
on,
|
|
3129
|
+
off,
|
|
3130
|
+
initialized: true,
|
|
3131
|
+
wsConnect,
|
|
3132
|
+
wsDisconnect,
|
|
3133
|
+
wsSubscribe,
|
|
3134
|
+
onWsEvent
|
|
3135
|
+
}),
|
|
3136
|
+
[
|
|
3137
|
+
features,
|
|
3138
|
+
config,
|
|
3139
|
+
registry,
|
|
3140
|
+
executor,
|
|
3141
|
+
workflowEngine,
|
|
3142
|
+
renderLog,
|
|
3143
|
+
metrics,
|
|
3144
|
+
wsClient,
|
|
3145
|
+
wsConnectionState,
|
|
3146
|
+
getElements,
|
|
3147
|
+
getComponents,
|
|
3148
|
+
createSnapshot,
|
|
3149
|
+
on,
|
|
3150
|
+
off,
|
|
3151
|
+
wsConnect,
|
|
3152
|
+
wsDisconnect,
|
|
3153
|
+
wsSubscribe,
|
|
3154
|
+
onWsEvent
|
|
3155
|
+
]
|
|
3156
|
+
);
|
|
3157
|
+
return /* @__PURE__ */ jsx(UIBridgeContext.Provider, { value: contextValue, children });
|
|
3158
|
+
}
|
|
3159
|
+
function useUIBridgeContext() {
|
|
3160
|
+
const context = useContext(UIBridgeContext);
|
|
3161
|
+
if (!context) {
|
|
3162
|
+
throw new Error("useUIBridgeContext must be used within a UIBridgeProvider");
|
|
3163
|
+
}
|
|
3164
|
+
return context;
|
|
3165
|
+
}
|
|
3166
|
+
function useUIBridgeOptional() {
|
|
3167
|
+
return useContext(UIBridgeContext);
|
|
3168
|
+
}
|
|
3169
|
+
function useUIElement(options) {
|
|
3170
|
+
const bridge = useUIBridgeOptional();
|
|
3171
|
+
const elementRef = useRef(null);
|
|
3172
|
+
const registeredRef = useRef(false);
|
|
3173
|
+
const { id, type, label, actions, customActions, autoRegister = true } = options;
|
|
3174
|
+
const register = useCallback(() => {
|
|
3175
|
+
if (!bridge || !elementRef.current || registeredRef.current) return;
|
|
3176
|
+
bridge.registry.registerElement(id, elementRef.current, {
|
|
3177
|
+
type,
|
|
3178
|
+
label,
|
|
3179
|
+
actions,
|
|
3180
|
+
customActions
|
|
3181
|
+
});
|
|
3182
|
+
registeredRef.current = true;
|
|
3183
|
+
}, [bridge, id, type, label, actions, customActions]);
|
|
3184
|
+
const unregister = useCallback(() => {
|
|
3185
|
+
if (!bridge || !registeredRef.current) return;
|
|
3186
|
+
bridge.registry.unregisterElement(id);
|
|
3187
|
+
registeredRef.current = false;
|
|
3188
|
+
}, [bridge, id]);
|
|
3189
|
+
const ref = useCallback(
|
|
3190
|
+
(node) => {
|
|
3191
|
+
if (elementRef.current && elementRef.current !== node) {
|
|
3192
|
+
unregister();
|
|
3193
|
+
}
|
|
3194
|
+
elementRef.current = node;
|
|
3195
|
+
if (node && autoRegister) {
|
|
3196
|
+
register();
|
|
3197
|
+
}
|
|
3198
|
+
},
|
|
3199
|
+
[autoRegister, register, unregister]
|
|
3200
|
+
);
|
|
3201
|
+
useEffect(() => {
|
|
3202
|
+
return () => {
|
|
3203
|
+
unregister();
|
|
3204
|
+
};
|
|
3205
|
+
}, [unregister]);
|
|
3206
|
+
const getState = useCallback(() => {
|
|
3207
|
+
if (!bridge) return null;
|
|
3208
|
+
const registered = bridge.registry.getElement(id);
|
|
3209
|
+
return registered?.getState() || null;
|
|
3210
|
+
}, [bridge, id]);
|
|
3211
|
+
const getIdentifier = useCallback(() => {
|
|
3212
|
+
if (!bridge) return null;
|
|
3213
|
+
const registered = bridge.registry.getElement(id);
|
|
3214
|
+
return registered?.getIdentifier() || null;
|
|
3215
|
+
}, [bridge, id]);
|
|
3216
|
+
const trigger = useCallback(
|
|
3217
|
+
async (action, params) => {
|
|
3218
|
+
if (!bridge) {
|
|
3219
|
+
throw new Error("UI Bridge not available");
|
|
3220
|
+
}
|
|
3221
|
+
const response = await bridge.executor.executeAction(id, {
|
|
3222
|
+
action,
|
|
3223
|
+
params
|
|
3224
|
+
});
|
|
3225
|
+
if (!response.success) {
|
|
3226
|
+
throw new Error(response.error || "Action failed");
|
|
3227
|
+
}
|
|
3228
|
+
},
|
|
3229
|
+
[bridge, id]
|
|
3230
|
+
);
|
|
3231
|
+
const registeredElement = useMemo(() => {
|
|
3232
|
+
if (!bridge) return null;
|
|
3233
|
+
return bridge.registry.getElement(id) || null;
|
|
3234
|
+
}, [bridge, id]);
|
|
3235
|
+
return {
|
|
3236
|
+
ref,
|
|
3237
|
+
element: elementRef.current,
|
|
3238
|
+
registered: registeredRef.current,
|
|
3239
|
+
getState,
|
|
3240
|
+
getIdentifier,
|
|
3241
|
+
trigger,
|
|
3242
|
+
register,
|
|
3243
|
+
unregister,
|
|
3244
|
+
registeredElement
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
function useUIElementRef(id) {
|
|
3248
|
+
return useCallback(
|
|
3249
|
+
(node) => {
|
|
3250
|
+
if (node) {
|
|
3251
|
+
node.setAttribute("data-ui-id", id);
|
|
3252
|
+
}
|
|
3253
|
+
},
|
|
3254
|
+
[id]
|
|
3255
|
+
);
|
|
3256
|
+
}
|
|
3257
|
+
function useUIComponent(options) {
|
|
3258
|
+
const bridge = useUIBridgeOptional();
|
|
3259
|
+
const registeredRef = useRef(false);
|
|
3260
|
+
const actionsRef = useRef(options.actions || []);
|
|
3261
|
+
const elementIdsRef = useRef(options.elementIds || []);
|
|
3262
|
+
const { id, name, description, autoRegister = true } = options;
|
|
3263
|
+
useEffect(() => {
|
|
3264
|
+
actionsRef.current = options.actions || [];
|
|
3265
|
+
elementIdsRef.current = options.elementIds || [];
|
|
3266
|
+
}, [options.actions, options.elementIds]);
|
|
3267
|
+
const register = useCallback(() => {
|
|
3268
|
+
if (!bridge || registeredRef.current) return;
|
|
3269
|
+
bridge.registry.registerComponent(id, {
|
|
3270
|
+
name,
|
|
3271
|
+
description,
|
|
3272
|
+
actions: actionsRef.current.map((a) => ({
|
|
3273
|
+
id: a.id,
|
|
3274
|
+
label: a.label,
|
|
3275
|
+
description: a.description,
|
|
3276
|
+
handler: a.handler
|
|
3277
|
+
})),
|
|
3278
|
+
elementIds: elementIdsRef.current
|
|
3279
|
+
});
|
|
3280
|
+
registeredRef.current = true;
|
|
3281
|
+
}, [bridge, id, name, description]);
|
|
3282
|
+
const unregister = useCallback(() => {
|
|
3283
|
+
if (!bridge || !registeredRef.current) return;
|
|
3284
|
+
bridge.registry.unregisterComponent(id);
|
|
3285
|
+
registeredRef.current = false;
|
|
3286
|
+
}, [bridge, id]);
|
|
3287
|
+
const executeAction = useCallback(
|
|
3288
|
+
async (actionId, params) => {
|
|
3289
|
+
if (!bridge) {
|
|
3290
|
+
throw new Error("UI Bridge not available");
|
|
3291
|
+
}
|
|
3292
|
+
const response = await bridge.executor.executeComponentAction(id, {
|
|
3293
|
+
action: actionId,
|
|
3294
|
+
params
|
|
3295
|
+
});
|
|
3296
|
+
if (!response.success) {
|
|
3297
|
+
throw new Error(response.error || "Action failed");
|
|
3298
|
+
}
|
|
3299
|
+
return response.result;
|
|
3300
|
+
},
|
|
3301
|
+
[bridge, id]
|
|
3302
|
+
);
|
|
3303
|
+
const updateActions = useCallback(
|
|
3304
|
+
(actions) => {
|
|
3305
|
+
actionsRef.current = actions;
|
|
3306
|
+
if (registeredRef.current && bridge) {
|
|
3307
|
+
bridge.registry.unregisterComponent(id);
|
|
3308
|
+
registeredRef.current = false;
|
|
3309
|
+
register();
|
|
3310
|
+
}
|
|
3311
|
+
},
|
|
3312
|
+
[bridge, id, register]
|
|
3313
|
+
);
|
|
3314
|
+
const addElement = useCallback((elementId) => {
|
|
3315
|
+
if (!elementIdsRef.current.includes(elementId)) {
|
|
3316
|
+
elementIdsRef.current = [...elementIdsRef.current, elementId];
|
|
3317
|
+
}
|
|
3318
|
+
}, []);
|
|
3319
|
+
const removeElement = useCallback((elementId) => {
|
|
3320
|
+
elementIdsRef.current = elementIdsRef.current.filter((id2) => id2 !== elementId);
|
|
3321
|
+
}, []);
|
|
3322
|
+
useEffect(() => {
|
|
3323
|
+
if (autoRegister) {
|
|
3324
|
+
register();
|
|
3325
|
+
}
|
|
3326
|
+
return () => {
|
|
3327
|
+
unregister();
|
|
3328
|
+
};
|
|
3329
|
+
}, [autoRegister, register, unregister]);
|
|
3330
|
+
const registeredComponent = useMemo(() => {
|
|
3331
|
+
if (!bridge) return null;
|
|
3332
|
+
return bridge.registry.getComponent(id) || null;
|
|
3333
|
+
}, [bridge, id]);
|
|
3334
|
+
return {
|
|
3335
|
+
registered: registeredRef.current,
|
|
3336
|
+
executeAction,
|
|
3337
|
+
register,
|
|
3338
|
+
unregister,
|
|
3339
|
+
updateActions,
|
|
3340
|
+
addElement,
|
|
3341
|
+
removeElement,
|
|
3342
|
+
registeredComponent
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3345
|
+
function useUIComponentAction(handler, deps) {
|
|
3346
|
+
return useCallback(handler, deps);
|
|
3347
|
+
}
|
|
3348
|
+
function useUIBridge() {
|
|
3349
|
+
const context = useUIBridgeOptional();
|
|
3350
|
+
const available = !!context;
|
|
3351
|
+
const initialized = context?.initialized ?? false;
|
|
3352
|
+
const elements = useMemo(() => context?.getElements() ?? [], [context]);
|
|
3353
|
+
const components = useMemo(() => context?.getComponents() ?? [], [context]);
|
|
3354
|
+
const workflows = useMemo(() => context?.registry.getAllWorkflows() ?? [], [context]);
|
|
3355
|
+
const createSnapshot = useCallback(() => {
|
|
3356
|
+
if (!context) {
|
|
3357
|
+
return {
|
|
3358
|
+
timestamp: Date.now(),
|
|
3359
|
+
elements: [],
|
|
3360
|
+
components: [],
|
|
3361
|
+
workflows: []
|
|
3362
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
return context.createSnapshot();
|
|
3365
|
+
}, [context]);
|
|
3366
|
+
const executeAction = useCallback(
|
|
3367
|
+
async (elementId, request) => {
|
|
3368
|
+
if (!context) {
|
|
3369
|
+
return {
|
|
3370
|
+
success: false,
|
|
3371
|
+
error: "UI Bridge not available",
|
|
3372
|
+
durationMs: 0,
|
|
3373
|
+
timestamp: Date.now()
|
|
3374
|
+
};
|
|
3375
|
+
}
|
|
3376
|
+
return context.executor.executeAction(elementId, request);
|
|
3377
|
+
},
|
|
3378
|
+
[context]
|
|
3379
|
+
);
|
|
3380
|
+
const executeComponentAction = useCallback(
|
|
3381
|
+
async (componentId, request) => {
|
|
3382
|
+
if (!context) {
|
|
3383
|
+
return {
|
|
3384
|
+
success: false,
|
|
3385
|
+
error: "UI Bridge not available",
|
|
3386
|
+
durationMs: 0,
|
|
3387
|
+
timestamp: Date.now()
|
|
3388
|
+
};
|
|
3389
|
+
}
|
|
3390
|
+
return context.executor.executeComponentAction(componentId, request);
|
|
3391
|
+
},
|
|
3392
|
+
[context]
|
|
3393
|
+
);
|
|
3394
|
+
const find = useCallback(
|
|
3395
|
+
async (options) => {
|
|
3396
|
+
if (!context) {
|
|
3397
|
+
return {
|
|
3398
|
+
elements: [],
|
|
3399
|
+
total: 0,
|
|
3400
|
+
durationMs: 0,
|
|
3401
|
+
timestamp: Date.now()
|
|
3402
|
+
};
|
|
3403
|
+
}
|
|
3404
|
+
return context.executor.find(options);
|
|
3405
|
+
},
|
|
3406
|
+
[context]
|
|
3407
|
+
);
|
|
3408
|
+
const discover = useCallback(
|
|
3409
|
+
async (options) => {
|
|
3410
|
+
return find(options);
|
|
3411
|
+
},
|
|
3412
|
+
[find]
|
|
3413
|
+
);
|
|
3414
|
+
const runWorkflow = useCallback(
|
|
3415
|
+
async (workflowId, request) => {
|
|
3416
|
+
if (!context) {
|
|
3417
|
+
return {
|
|
3418
|
+
workflowId,
|
|
3419
|
+
runId: "",
|
|
3420
|
+
status: "failed",
|
|
3421
|
+
steps: [],
|
|
3422
|
+
totalSteps: 0,
|
|
3423
|
+
success: false,
|
|
3424
|
+
error: "UI Bridge not available",
|
|
3425
|
+
startedAt: Date.now(),
|
|
3426
|
+
completedAt: Date.now(),
|
|
3427
|
+
durationMs: 0
|
|
3428
|
+
};
|
|
3429
|
+
}
|
|
3430
|
+
return context.workflowEngine.run(workflowId, request);
|
|
3431
|
+
},
|
|
3432
|
+
[context]
|
|
3433
|
+
);
|
|
3434
|
+
const getElement = useCallback(
|
|
3435
|
+
(id) => {
|
|
3436
|
+
return context?.registry.getElement(id);
|
|
3437
|
+
},
|
|
3438
|
+
[context]
|
|
3439
|
+
);
|
|
3440
|
+
const getComponent = useCallback(
|
|
3441
|
+
(id) => {
|
|
3442
|
+
return context?.registry.getComponent(id);
|
|
3443
|
+
},
|
|
3444
|
+
[context]
|
|
3445
|
+
);
|
|
3446
|
+
const getElementState4 = useCallback(
|
|
3447
|
+
(id) => {
|
|
3448
|
+
const element = context?.registry.getElement(id);
|
|
3449
|
+
return element?.getState();
|
|
3450
|
+
},
|
|
3451
|
+
[context]
|
|
3452
|
+
);
|
|
3453
|
+
const registerWorkflow = useCallback(
|
|
3454
|
+
(workflow) => {
|
|
3455
|
+
context?.registry.registerWorkflow(workflow);
|
|
3456
|
+
},
|
|
3457
|
+
[context]
|
|
3458
|
+
);
|
|
3459
|
+
const unregisterWorkflow = useCallback(
|
|
3460
|
+
(id) => {
|
|
3461
|
+
context?.registry.unregisterWorkflow(id);
|
|
3462
|
+
},
|
|
3463
|
+
[context]
|
|
3464
|
+
);
|
|
3465
|
+
const captureRenderLog = useCallback(async () => {
|
|
3466
|
+
await context?.renderLog?.captureSnapshot();
|
|
3467
|
+
}, [context]);
|
|
3468
|
+
const getRenderLogEntries = useCallback(async () => {
|
|
3469
|
+
return await context?.renderLog?.getEntries() ?? [];
|
|
3470
|
+
}, [context]);
|
|
3471
|
+
const clearRenderLog = useCallback(async () => {
|
|
3472
|
+
await context?.renderLog?.clear();
|
|
3473
|
+
}, [context]);
|
|
3474
|
+
const getMetrics = useCallback(() => {
|
|
3475
|
+
return context?.metrics?.getMetrics();
|
|
3476
|
+
}, [context]);
|
|
3477
|
+
const getActionHistory = useCallback(() => {
|
|
3478
|
+
return context?.metrics?.getHistory();
|
|
3479
|
+
}, [context]);
|
|
3480
|
+
return {
|
|
3481
|
+
available,
|
|
3482
|
+
initialized,
|
|
3483
|
+
elements,
|
|
3484
|
+
components,
|
|
3485
|
+
workflows,
|
|
3486
|
+
createSnapshot,
|
|
3487
|
+
executeAction,
|
|
3488
|
+
executeComponentAction,
|
|
3489
|
+
find,
|
|
3490
|
+
discover,
|
|
3491
|
+
// deprecated - use find
|
|
3492
|
+
runWorkflow,
|
|
3493
|
+
getElement,
|
|
3494
|
+
getComponent,
|
|
3495
|
+
getElementState: getElementState4,
|
|
3496
|
+
registerWorkflow,
|
|
3497
|
+
unregisterWorkflow,
|
|
3498
|
+
captureRenderLog,
|
|
3499
|
+
getRenderLogEntries,
|
|
3500
|
+
clearRenderLog,
|
|
3501
|
+
getMetrics,
|
|
3502
|
+
getActionHistory
|
|
3503
|
+
};
|
|
3504
|
+
}
|
|
3505
|
+
function useUIBridgeRequired() {
|
|
3506
|
+
useUIBridgeContext();
|
|
3507
|
+
return useUIBridge();
|
|
3508
|
+
}
|
|
3509
|
+
function useUIState(options) {
|
|
3510
|
+
const bridge = useUIBridgeOptional();
|
|
3511
|
+
const [registered, setRegistered] = useState(false);
|
|
3512
|
+
const [isActive, setIsActive] = useState(options.initialActive ?? false);
|
|
3513
|
+
const [activeStates, setActiveStates] = useState([]);
|
|
3514
|
+
const {
|
|
3515
|
+
id,
|
|
3516
|
+
name,
|
|
3517
|
+
elements = [],
|
|
3518
|
+
activeWhen,
|
|
3519
|
+
blocking,
|
|
3520
|
+
blocks,
|
|
3521
|
+
group,
|
|
3522
|
+
pathCost,
|
|
3523
|
+
metadata,
|
|
3524
|
+
autoRegister = true,
|
|
3525
|
+
initialActive = false
|
|
3526
|
+
} = options;
|
|
3527
|
+
const register = useCallback(() => {
|
|
3528
|
+
if (!bridge || registered) return;
|
|
3529
|
+
const state2 = {
|
|
3530
|
+
id,
|
|
3531
|
+
name,
|
|
3532
|
+
elements,
|
|
3533
|
+
activeWhen,
|
|
3534
|
+
blocking,
|
|
3535
|
+
blocks,
|
|
3536
|
+
group,
|
|
3537
|
+
pathCost,
|
|
3538
|
+
metadata
|
|
3539
|
+
};
|
|
3540
|
+
bridge.registry.registerState(state2);
|
|
3541
|
+
setRegistered(true);
|
|
3542
|
+
if (initialActive) {
|
|
3543
|
+
bridge.registry.activateState(id);
|
|
3544
|
+
setIsActive(true);
|
|
3545
|
+
}
|
|
3546
|
+
setActiveStates(bridge.registry.getActiveStates());
|
|
3547
|
+
}, [
|
|
3548
|
+
bridge,
|
|
3549
|
+
registered,
|
|
3550
|
+
id,
|
|
3551
|
+
name,
|
|
3552
|
+
elements,
|
|
3553
|
+
activeWhen,
|
|
3554
|
+
blocking,
|
|
3555
|
+
blocks,
|
|
3556
|
+
group,
|
|
3557
|
+
pathCost,
|
|
3558
|
+
metadata,
|
|
3559
|
+
initialActive
|
|
3560
|
+
]);
|
|
3561
|
+
const unregister = useCallback(() => {
|
|
3562
|
+
if (!bridge || !registered) return;
|
|
3563
|
+
bridge.registry.unregisterState(id);
|
|
3564
|
+
setRegistered(false);
|
|
3565
|
+
setIsActive(false);
|
|
3566
|
+
}, [bridge, registered, id]);
|
|
3567
|
+
useEffect(() => {
|
|
3568
|
+
if (autoRegister && bridge) {
|
|
3569
|
+
register();
|
|
3570
|
+
}
|
|
3571
|
+
return () => {
|
|
3572
|
+
if (registered) {
|
|
3573
|
+
unregister();
|
|
3574
|
+
}
|
|
3575
|
+
};
|
|
3576
|
+
}, [autoRegister, bridge, register, unregister, registered]);
|
|
3577
|
+
useEffect(() => {
|
|
3578
|
+
if (!bridge) return;
|
|
3579
|
+
const unsubscribe = bridge.registry.on("element:stateChanged", (event) => {
|
|
3580
|
+
const data = event.data;
|
|
3581
|
+
if (data.stateId === id) {
|
|
3582
|
+
setIsActive(data.active);
|
|
3583
|
+
}
|
|
3584
|
+
setActiveStates(data.activeStates);
|
|
3585
|
+
});
|
|
3586
|
+
return unsubscribe;
|
|
3587
|
+
}, [bridge, id]);
|
|
3588
|
+
const activate = useCallback(() => {
|
|
3589
|
+
if (!bridge) return false;
|
|
3590
|
+
const success = bridge.registry.activateState(id);
|
|
3591
|
+
if (success) {
|
|
3592
|
+
setIsActive(true);
|
|
3593
|
+
setActiveStates(bridge.registry.getActiveStates());
|
|
3594
|
+
}
|
|
3595
|
+
return success;
|
|
3596
|
+
}, [bridge, id]);
|
|
3597
|
+
const deactivate = useCallback(() => {
|
|
3598
|
+
if (!bridge) return false;
|
|
3599
|
+
const success = bridge.registry.deactivateState(id);
|
|
3600
|
+
if (success) {
|
|
3601
|
+
setIsActive(false);
|
|
3602
|
+
setActiveStates(bridge.registry.getActiveStates());
|
|
3603
|
+
}
|
|
3604
|
+
return success;
|
|
3605
|
+
}, [bridge, id]);
|
|
3606
|
+
const toggle = useCallback(() => {
|
|
3607
|
+
return isActive ? deactivate() : activate();
|
|
3608
|
+
}, [isActive, activate, deactivate]);
|
|
3609
|
+
const state = useMemo(() => {
|
|
3610
|
+
if (!bridge) return void 0;
|
|
3611
|
+
return bridge.registry.getState(id);
|
|
3612
|
+
}, [bridge, id, registered]);
|
|
3613
|
+
return {
|
|
3614
|
+
registered,
|
|
3615
|
+
isActive,
|
|
3616
|
+
activate,
|
|
3617
|
+
deactivate,
|
|
3618
|
+
toggle,
|
|
3619
|
+
activeStates,
|
|
3620
|
+
register,
|
|
3621
|
+
unregister,
|
|
3622
|
+
state
|
|
3623
|
+
};
|
|
3624
|
+
}
|
|
3625
|
+
function useUIStateGroup(options) {
|
|
3626
|
+
const bridge = useUIBridgeOptional();
|
|
3627
|
+
const [registered, setRegistered] = useState(false);
|
|
3628
|
+
const { id, name, states, autoRegister = true } = options;
|
|
3629
|
+
const register = useCallback(() => {
|
|
3630
|
+
if (!bridge || registered) return;
|
|
3631
|
+
const group2 = { id, name, states };
|
|
3632
|
+
bridge.registry.registerStateGroup(group2);
|
|
3633
|
+
setRegistered(true);
|
|
3634
|
+
}, [bridge, registered, id, name, states]);
|
|
3635
|
+
const unregister = useCallback(() => {
|
|
3636
|
+
if (!bridge || !registered) return;
|
|
3637
|
+
bridge.registry.unregisterStateGroup(id);
|
|
3638
|
+
setRegistered(false);
|
|
3639
|
+
}, [bridge, registered, id]);
|
|
3640
|
+
useEffect(() => {
|
|
3641
|
+
if (autoRegister && bridge) {
|
|
3642
|
+
register();
|
|
3643
|
+
}
|
|
3644
|
+
return () => {
|
|
3645
|
+
if (registered) {
|
|
3646
|
+
unregister();
|
|
3647
|
+
}
|
|
3648
|
+
};
|
|
3649
|
+
}, [autoRegister, bridge, register, unregister, registered]);
|
|
3650
|
+
const activate = useCallback(() => {
|
|
3651
|
+
if (!bridge) return [];
|
|
3652
|
+
return bridge.registry.activateStateGroup(id);
|
|
3653
|
+
}, [bridge, id]);
|
|
3654
|
+
const deactivate = useCallback(() => {
|
|
3655
|
+
if (!bridge) return [];
|
|
3656
|
+
return bridge.registry.deactivateStateGroup(id);
|
|
3657
|
+
}, [bridge, id]);
|
|
3658
|
+
const group = useMemo(() => {
|
|
3659
|
+
if (!bridge) return void 0;
|
|
3660
|
+
return bridge.registry.getStateGroup(id);
|
|
3661
|
+
}, [bridge, id, registered]);
|
|
3662
|
+
return {
|
|
3663
|
+
registered,
|
|
3664
|
+
activate,
|
|
3665
|
+
deactivate,
|
|
3666
|
+
register,
|
|
3667
|
+
unregister,
|
|
3668
|
+
group
|
|
3669
|
+
};
|
|
3670
|
+
}
|
|
3671
|
+
function useActiveStates() {
|
|
3672
|
+
const bridge = useUIBridgeOptional();
|
|
3673
|
+
const [activeStates, setActiveStates] = useState([]);
|
|
3674
|
+
useEffect(() => {
|
|
3675
|
+
if (!bridge) return;
|
|
3676
|
+
setActiveStates(bridge.registry.getActiveStates());
|
|
3677
|
+
const unsubscribe = bridge.registry.on("element:stateChanged", (event) => {
|
|
3678
|
+
const data = event.data;
|
|
3679
|
+
setActiveStates(data.activeStates);
|
|
3680
|
+
});
|
|
3681
|
+
return unsubscribe;
|
|
3682
|
+
}, [bridge]);
|
|
3683
|
+
return activeStates;
|
|
3684
|
+
}
|
|
3685
|
+
function useStateSnapshot() {
|
|
3686
|
+
const bridge = useUIBridgeOptional();
|
|
3687
|
+
return useMemo(() => {
|
|
3688
|
+
if (!bridge) return null;
|
|
3689
|
+
return bridge.registry.createStateSnapshot();
|
|
3690
|
+
}, [bridge]);
|
|
3691
|
+
}
|
|
3692
|
+
function useUITransition(options) {
|
|
3693
|
+
const bridge = useUIBridgeOptional();
|
|
3694
|
+
const [registered, setRegistered] = useState(false);
|
|
3695
|
+
const [canExecute, setCanExecute] = useState(false);
|
|
3696
|
+
const {
|
|
3697
|
+
id,
|
|
3698
|
+
name,
|
|
3699
|
+
fromStates,
|
|
3700
|
+
activateStates,
|
|
3701
|
+
exitStates,
|
|
3702
|
+
activateGroups,
|
|
3703
|
+
exitGroups,
|
|
3704
|
+
actions,
|
|
3705
|
+
pathCost,
|
|
3706
|
+
staysVisible,
|
|
3707
|
+
autoRegister = true
|
|
3708
|
+
} = options;
|
|
3709
|
+
const register = useCallback(() => {
|
|
3710
|
+
if (!bridge || registered) return;
|
|
3711
|
+
const transition2 = {
|
|
3712
|
+
id,
|
|
3713
|
+
name,
|
|
3714
|
+
fromStates,
|
|
3715
|
+
activateStates,
|
|
3716
|
+
exitStates,
|
|
3717
|
+
activateGroups,
|
|
3718
|
+
exitGroups,
|
|
3719
|
+
actions,
|
|
3720
|
+
pathCost,
|
|
3721
|
+
staysVisible
|
|
3722
|
+
};
|
|
3723
|
+
bridge.registry.registerTransition(transition2);
|
|
3724
|
+
setRegistered(true);
|
|
3725
|
+
setCanExecute(bridge.registry.canExecuteTransition(id));
|
|
3726
|
+
}, [
|
|
3727
|
+
bridge,
|
|
3728
|
+
registered,
|
|
3729
|
+
id,
|
|
3730
|
+
name,
|
|
3731
|
+
fromStates,
|
|
3732
|
+
activateStates,
|
|
3733
|
+
exitStates,
|
|
3734
|
+
activateGroups,
|
|
3735
|
+
exitGroups,
|
|
3736
|
+
actions,
|
|
3737
|
+
pathCost,
|
|
3738
|
+
staysVisible
|
|
3739
|
+
]);
|
|
3740
|
+
const unregister = useCallback(() => {
|
|
3741
|
+
if (!bridge || !registered) return;
|
|
3742
|
+
bridge.registry.unregisterTransition(id);
|
|
3743
|
+
setRegistered(false);
|
|
3744
|
+
setCanExecute(false);
|
|
3745
|
+
}, [bridge, registered, id]);
|
|
3746
|
+
useEffect(() => {
|
|
3747
|
+
if (autoRegister && bridge) {
|
|
3748
|
+
register();
|
|
3749
|
+
}
|
|
3750
|
+
return () => {
|
|
3751
|
+
if (registered) {
|
|
3752
|
+
unregister();
|
|
3753
|
+
}
|
|
3754
|
+
};
|
|
3755
|
+
}, [autoRegister, bridge, register, unregister, registered]);
|
|
3756
|
+
useEffect(() => {
|
|
3757
|
+
if (!bridge || !registered) return;
|
|
3758
|
+
const unsubscribe = bridge.registry.on("element:stateChanged", () => {
|
|
3759
|
+
setCanExecute(bridge.registry.canExecuteTransition(id));
|
|
3760
|
+
});
|
|
3761
|
+
return unsubscribe;
|
|
3762
|
+
}, [bridge, id, registered]);
|
|
3763
|
+
const execute = useCallback(async () => {
|
|
3764
|
+
if (!bridge) {
|
|
3765
|
+
return {
|
|
3766
|
+
success: false,
|
|
3767
|
+
activatedStates: [],
|
|
3768
|
+
deactivatedStates: [],
|
|
3769
|
+
error: "UI Bridge not available",
|
|
3770
|
+
durationMs: 0
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
const result = await bridge.registry.executeTransition(id);
|
|
3774
|
+
setCanExecute(bridge.registry.canExecuteTransition(id));
|
|
3775
|
+
return result;
|
|
3776
|
+
}, [bridge, id]);
|
|
3777
|
+
const transition = useMemo(() => {
|
|
3778
|
+
if (!bridge) return void 0;
|
|
3779
|
+
return bridge.registry.getTransition(id);
|
|
3780
|
+
}, [bridge, id, registered]);
|
|
3781
|
+
return {
|
|
3782
|
+
registered,
|
|
3783
|
+
canExecute,
|
|
3784
|
+
execute,
|
|
3785
|
+
register,
|
|
3786
|
+
unregister,
|
|
3787
|
+
transition
|
|
3788
|
+
};
|
|
3789
|
+
}
|
|
3790
|
+
function useTransitions() {
|
|
3791
|
+
const bridge = useUIBridgeOptional();
|
|
3792
|
+
return useMemo(() => {
|
|
3793
|
+
if (!bridge) return [];
|
|
3794
|
+
return bridge.registry.getAllTransitions();
|
|
3795
|
+
}, [bridge]);
|
|
3796
|
+
}
|
|
3797
|
+
function useAvailableTransitions() {
|
|
3798
|
+
const bridge = useUIBridgeOptional();
|
|
3799
|
+
const [available, setAvailable] = useState([]);
|
|
3800
|
+
useEffect(() => {
|
|
3801
|
+
if (!bridge) return;
|
|
3802
|
+
const updateAvailable = () => {
|
|
3803
|
+
const transitions = bridge.registry.getAllTransitions();
|
|
3804
|
+
const availableTransitions = transitions.filter(
|
|
3805
|
+
(t) => bridge.registry.canExecuteTransition(t.id)
|
|
3806
|
+
);
|
|
3807
|
+
setAvailable(availableTransitions);
|
|
3808
|
+
};
|
|
3809
|
+
updateAvailable();
|
|
3810
|
+
const unsubscribe = bridge.registry.on("element:stateChanged", updateAvailable);
|
|
3811
|
+
return unsubscribe;
|
|
3812
|
+
}, [bridge]);
|
|
3813
|
+
return available;
|
|
3814
|
+
}
|
|
3815
|
+
function useUINavigation() {
|
|
3816
|
+
const bridge = useUIBridgeOptional();
|
|
3817
|
+
const [isNavigating, setIsNavigating] = useState(false);
|
|
3818
|
+
const [lastResult, setLastResult] = useState(null);
|
|
3819
|
+
const available = !!bridge;
|
|
3820
|
+
const activeStates = useMemo(() => {
|
|
3821
|
+
if (!bridge) return [];
|
|
3822
|
+
return bridge.registry.getActiveStates();
|
|
3823
|
+
}, [bridge]);
|
|
3824
|
+
const findPath = useCallback(
|
|
3825
|
+
(targetStates) => {
|
|
3826
|
+
if (!bridge) {
|
|
3827
|
+
return {
|
|
3828
|
+
found: false,
|
|
3829
|
+
transitions: [],
|
|
3830
|
+
totalCost: 0,
|
|
3831
|
+
targetStates,
|
|
3832
|
+
estimatedSteps: 0
|
|
3833
|
+
};
|
|
3834
|
+
}
|
|
3835
|
+
return bridge.registry.findPath(targetStates);
|
|
3836
|
+
},
|
|
3837
|
+
[bridge]
|
|
3838
|
+
);
|
|
3839
|
+
const navigateTo = useCallback(
|
|
3840
|
+
async (targetStates) => {
|
|
3841
|
+
if (!bridge) {
|
|
3842
|
+
const result = {
|
|
3843
|
+
success: false,
|
|
3844
|
+
path: {
|
|
3845
|
+
found: false,
|
|
3846
|
+
transitions: [],
|
|
3847
|
+
totalCost: 0,
|
|
3848
|
+
targetStates,
|
|
3849
|
+
estimatedSteps: 0
|
|
3850
|
+
},
|
|
3851
|
+
executedTransitions: [],
|
|
3852
|
+
finalActiveStates: [],
|
|
3853
|
+
error: "UI Bridge not available",
|
|
3854
|
+
durationMs: 0
|
|
3855
|
+
};
|
|
3856
|
+
setLastResult(result);
|
|
3857
|
+
return result;
|
|
3858
|
+
}
|
|
3859
|
+
setIsNavigating(true);
|
|
3860
|
+
try {
|
|
3861
|
+
const result = await bridge.registry.navigateTo(targetStates);
|
|
3862
|
+
setLastResult(result);
|
|
3863
|
+
return result;
|
|
3864
|
+
} finally {
|
|
3865
|
+
setIsNavigating(false);
|
|
3866
|
+
}
|
|
3867
|
+
},
|
|
3868
|
+
[bridge]
|
|
3869
|
+
);
|
|
3870
|
+
return {
|
|
3871
|
+
available,
|
|
3872
|
+
isNavigating,
|
|
3873
|
+
lastResult,
|
|
3874
|
+
findPath,
|
|
3875
|
+
navigateTo,
|
|
3876
|
+
activeStates
|
|
3877
|
+
};
|
|
3878
|
+
}
|
|
3879
|
+
function useCanNavigateTo(targetStates) {
|
|
3880
|
+
const bridge = useUIBridgeOptional();
|
|
3881
|
+
return useMemo(() => {
|
|
3882
|
+
if (!bridge) return false;
|
|
3883
|
+
const path = bridge.registry.findPath(targetStates);
|
|
3884
|
+
return path.found;
|
|
3885
|
+
}, [bridge, targetStates]);
|
|
3886
|
+
}
|
|
3887
|
+
function useNavigationPath(targetStates) {
|
|
3888
|
+
const bridge = useUIBridgeOptional();
|
|
3889
|
+
return useMemo(() => {
|
|
3890
|
+
if (!bridge) {
|
|
3891
|
+
return {
|
|
3892
|
+
found: false,
|
|
3893
|
+
transitions: [],
|
|
3894
|
+
totalCost: 0,
|
|
3895
|
+
targetStates,
|
|
3896
|
+
estimatedSteps: 0
|
|
3897
|
+
};
|
|
3898
|
+
}
|
|
3899
|
+
return bridge.registry.findPath(targetStates);
|
|
3900
|
+
}, [bridge, targetStates]);
|
|
3901
|
+
}
|
|
3902
|
+
var INTERACTIVE_SELECTORS2 = [
|
|
3903
|
+
"a[href]",
|
|
3904
|
+
"button",
|
|
3905
|
+
"input",
|
|
3906
|
+
"select",
|
|
3907
|
+
"textarea",
|
|
3908
|
+
'[role="button"]',
|
|
3909
|
+
'[role="link"]',
|
|
3910
|
+
'[role="checkbox"]',
|
|
3911
|
+
'[role="radio"]',
|
|
3912
|
+
'[role="menuitem"]',
|
|
3913
|
+
'[role="tab"]',
|
|
3914
|
+
'[role="switch"]',
|
|
3915
|
+
'[role="slider"]',
|
|
3916
|
+
'[role="spinbutton"]',
|
|
3917
|
+
'[role="combobox"]',
|
|
3918
|
+
'[role="listbox"]',
|
|
3919
|
+
'[role="option"]',
|
|
3920
|
+
'[role="textbox"]',
|
|
3921
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
3922
|
+
'[contenteditable="true"]',
|
|
3923
|
+
"[data-ui-element]",
|
|
3924
|
+
// Explicitly marked for registration
|
|
3925
|
+
"[data-testid]"
|
|
3926
|
+
// Testing library convention
|
|
3927
|
+
];
|
|
3928
|
+
function inferElementType2(element) {
|
|
3929
|
+
const role = element.getAttribute("role");
|
|
3930
|
+
if (role) {
|
|
3931
|
+
const roleMap = {
|
|
3932
|
+
button: "button",
|
|
3933
|
+
link: "link",
|
|
3934
|
+
checkbox: "checkbox",
|
|
3935
|
+
radio: "radio",
|
|
3936
|
+
menuitem: "menuitem",
|
|
3937
|
+
tab: "tab",
|
|
3938
|
+
switch: "switch",
|
|
3939
|
+
slider: "slider",
|
|
3940
|
+
combobox: "combobox",
|
|
3941
|
+
listbox: "listbox",
|
|
3942
|
+
option: "option",
|
|
3943
|
+
textbox: "textbox"
|
|
3944
|
+
};
|
|
3945
|
+
if (role in roleMap) {
|
|
3946
|
+
return roleMap[role];
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
const tagName = element.tagName.toLowerCase();
|
|
3950
|
+
switch (tagName) {
|
|
3951
|
+
case "a":
|
|
3952
|
+
return "link";
|
|
3953
|
+
case "button":
|
|
3954
|
+
return "button";
|
|
3955
|
+
case "input": {
|
|
3956
|
+
const type = element.type?.toLowerCase() || "text";
|
|
3957
|
+
switch (type) {
|
|
3958
|
+
case "checkbox":
|
|
3959
|
+
return "checkbox";
|
|
3960
|
+
case "radio":
|
|
3961
|
+
return "radio";
|
|
3962
|
+
case "range":
|
|
3963
|
+
return "slider";
|
|
3964
|
+
case "submit":
|
|
3965
|
+
case "button":
|
|
3966
|
+
return "button";
|
|
3967
|
+
default:
|
|
3968
|
+
return "input";
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
case "select":
|
|
3972
|
+
return "select";
|
|
3973
|
+
case "textarea":
|
|
3974
|
+
return "textarea";
|
|
3975
|
+
case "option":
|
|
3976
|
+
return "option";
|
|
3977
|
+
default:
|
|
3978
|
+
return "generic";
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
function inferActions2(type) {
|
|
3982
|
+
const baseActions = ["focus", "blur"];
|
|
3983
|
+
const typeActions = {
|
|
3984
|
+
button: [...baseActions, "click", "hover"],
|
|
3985
|
+
link: [...baseActions, "click", "hover"],
|
|
3986
|
+
input: [...baseActions, "type", "clear", "click"],
|
|
3987
|
+
textarea: [...baseActions, "type", "clear", "click"],
|
|
3988
|
+
textbox: [...baseActions, "type", "clear", "click"],
|
|
3989
|
+
checkbox: [...baseActions, "check", "uncheck", "toggle", "click"],
|
|
3990
|
+
radio: [...baseActions, "click", "select"],
|
|
3991
|
+
select: [...baseActions, "select", "click"],
|
|
3992
|
+
combobox: [...baseActions, "select", "type", "click"],
|
|
3993
|
+
listbox: [...baseActions, "select", "click"],
|
|
3994
|
+
option: [...baseActions, "select", "click"],
|
|
3995
|
+
switch: [...baseActions, "toggle", "click"],
|
|
3996
|
+
slider: [...baseActions, "setValue", "click", "drag"],
|
|
3997
|
+
tab: [...baseActions, "click", "select"],
|
|
3998
|
+
menuitem: [...baseActions, "click"],
|
|
3999
|
+
dialog: [...baseActions],
|
|
4000
|
+
menu: [...baseActions],
|
|
4001
|
+
form: [...baseActions, "submit", "reset"],
|
|
4002
|
+
custom: [...baseActions, "click"],
|
|
4003
|
+
generic: [...baseActions, "click"]
|
|
4004
|
+
};
|
|
4005
|
+
return typeActions[type] || baseActions;
|
|
4006
|
+
}
|
|
4007
|
+
function getAccessibleLabel(element) {
|
|
4008
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
4009
|
+
if (ariaLabel) return ariaLabel;
|
|
4010
|
+
const labelledBy = element.getAttribute("aria-labelledby");
|
|
4011
|
+
if (labelledBy) {
|
|
4012
|
+
const labelEl = document.getElementById(labelledBy);
|
|
4013
|
+
if (labelEl) return labelEl.textContent?.trim();
|
|
4014
|
+
}
|
|
4015
|
+
if (element.id) {
|
|
4016
|
+
const label = document.querySelector(`label[for="${element.id}"]`);
|
|
4017
|
+
if (label) return label.textContent?.trim();
|
|
4018
|
+
}
|
|
4019
|
+
const title = element.getAttribute("title");
|
|
4020
|
+
if (title) return title;
|
|
4021
|
+
const text = element.textContent?.trim();
|
|
4022
|
+
if (text && text.length <= 50) return text;
|
|
4023
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
4024
|
+
const placeholder = element.placeholder;
|
|
4025
|
+
if (placeholder) return placeholder;
|
|
4026
|
+
}
|
|
4027
|
+
return void 0;
|
|
4028
|
+
}
|
|
4029
|
+
function isElementVisible2(element) {
|
|
4030
|
+
const style = window.getComputedStyle(element);
|
|
4031
|
+
if (style.display === "none" || style.visibility === "hidden") {
|
|
4032
|
+
return false;
|
|
4033
|
+
}
|
|
4034
|
+
const rect = element.getBoundingClientRect();
|
|
4035
|
+
return rect.width > 0 && rect.height > 0;
|
|
4036
|
+
}
|
|
4037
|
+
function generateSemanticId(element) {
|
|
4038
|
+
const type = inferElementType2(element);
|
|
4039
|
+
const label = getAccessibleLabel(element);
|
|
4040
|
+
if (label) {
|
|
4041
|
+
const sanitized = label.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").slice(0, 30);
|
|
4042
|
+
return `${type}-${sanitized}`;
|
|
4043
|
+
}
|
|
4044
|
+
const parent = element.parentElement;
|
|
4045
|
+
if (parent) {
|
|
4046
|
+
const siblings = Array.from(parent.querySelectorAll(element.tagName));
|
|
4047
|
+
const index = siblings.indexOf(element);
|
|
4048
|
+
return `${element.tagName.toLowerCase()}-${index}`;
|
|
4049
|
+
}
|
|
4050
|
+
return `${type}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
4051
|
+
}
|
|
4052
|
+
function generateAutoId(element) {
|
|
4053
|
+
const type = inferElementType2(element);
|
|
4054
|
+
return `${type}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4055
|
+
}
|
|
4056
|
+
function generateIdForElement(element, strategy, customGenerator) {
|
|
4057
|
+
if (customGenerator) {
|
|
4058
|
+
return customGenerator(element);
|
|
4059
|
+
}
|
|
4060
|
+
switch (strategy) {
|
|
4061
|
+
case "data-testid": {
|
|
4062
|
+
const testId = element.getAttribute("data-testid");
|
|
4063
|
+
return testId || generateAutoId(element);
|
|
4064
|
+
}
|
|
4065
|
+
case "data-ui-id": {
|
|
4066
|
+
const uiId = element.getAttribute("data-ui-id");
|
|
4067
|
+
return uiId || generateAutoId(element);
|
|
4068
|
+
}
|
|
4069
|
+
case "semantic":
|
|
4070
|
+
return generateSemanticId(element);
|
|
4071
|
+
case "auto":
|
|
4072
|
+
return generateAutoId(element);
|
|
4073
|
+
case "prefer-existing":
|
|
4074
|
+
default: {
|
|
4075
|
+
const uiId = element.getAttribute("data-ui-id");
|
|
4076
|
+
if (uiId) return uiId;
|
|
4077
|
+
const testId = element.getAttribute("data-testid");
|
|
4078
|
+
if (testId) return testId;
|
|
4079
|
+
const htmlId = element.id;
|
|
4080
|
+
if (htmlId) return htmlId;
|
|
4081
|
+
return generateSemanticId(element);
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
function useAutoRegister(options = {}) {
|
|
4086
|
+
const {
|
|
4087
|
+
enabled = process.env.NODE_ENV === "development",
|
|
4088
|
+
root = null,
|
|
4089
|
+
idStrategy = "prefer-existing",
|
|
4090
|
+
debounceMs = 100,
|
|
4091
|
+
includeHidden = false,
|
|
4092
|
+
includeSelectors = [],
|
|
4093
|
+
excludeSelectors = [],
|
|
4094
|
+
generateId: customGenerateId,
|
|
4095
|
+
onRegister,
|
|
4096
|
+
onUnregister
|
|
4097
|
+
} = options;
|
|
4098
|
+
const bridge = useUIBridgeOptional();
|
|
4099
|
+
const registeredElementsRef = useRef(/* @__PURE__ */ new Map());
|
|
4100
|
+
const pendingRegistrationsRef = useRef(/* @__PURE__ */ new Set());
|
|
4101
|
+
const debounceTimeoutRef = useRef(null);
|
|
4102
|
+
const shouldRegister = useCallback(
|
|
4103
|
+
(element) => {
|
|
4104
|
+
if (!includeHidden && !isElementVisible2(element)) {
|
|
4105
|
+
return false;
|
|
4106
|
+
}
|
|
4107
|
+
for (const selector of excludeSelectors) {
|
|
4108
|
+
if (element.matches(selector)) {
|
|
4109
|
+
return false;
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
if (registeredElementsRef.current.has(element)) {
|
|
4113
|
+
return false;
|
|
4114
|
+
}
|
|
4115
|
+
const allSelectors = [...INTERACTIVE_SELECTORS2, ...includeSelectors];
|
|
4116
|
+
for (const selector of allSelectors) {
|
|
4117
|
+
if (element.matches(selector)) {
|
|
4118
|
+
return true;
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
return false;
|
|
4122
|
+
},
|
|
4123
|
+
[includeHidden, includeSelectors, excludeSelectors]
|
|
4124
|
+
);
|
|
4125
|
+
const registerElement = useCallback(
|
|
4126
|
+
(element) => {
|
|
4127
|
+
if (!bridge?.registry || registeredElementsRef.current.has(element)) {
|
|
4128
|
+
return;
|
|
4129
|
+
}
|
|
4130
|
+
const id = generateIdForElement(element, idStrategy, customGenerateId);
|
|
4131
|
+
const existing = bridge.registry.getElement(id);
|
|
4132
|
+
if (existing) {
|
|
4133
|
+
const uniqueId = `${id}-${Date.now().toString(36)}`;
|
|
4134
|
+
const type = inferElementType2(element);
|
|
4135
|
+
const actions = inferActions2(type);
|
|
4136
|
+
const label = getAccessibleLabel(element);
|
|
4137
|
+
bridge.registry.registerElement(uniqueId, element, {
|
|
4138
|
+
type,
|
|
4139
|
+
actions,
|
|
4140
|
+
label
|
|
4141
|
+
});
|
|
4142
|
+
registeredElementsRef.current.set(element, uniqueId);
|
|
4143
|
+
onRegister?.(uniqueId, element);
|
|
4144
|
+
} else {
|
|
4145
|
+
const type = inferElementType2(element);
|
|
4146
|
+
const actions = inferActions2(type);
|
|
4147
|
+
const label = getAccessibleLabel(element);
|
|
4148
|
+
bridge.registry.registerElement(id, element, {
|
|
4149
|
+
type,
|
|
4150
|
+
actions,
|
|
4151
|
+
label
|
|
4152
|
+
});
|
|
4153
|
+
registeredElementsRef.current.set(element, id);
|
|
4154
|
+
onRegister?.(id, element);
|
|
4155
|
+
}
|
|
4156
|
+
},
|
|
4157
|
+
[bridge, idStrategy, customGenerateId, onRegister]
|
|
4158
|
+
);
|
|
4159
|
+
const unregisterElement = useCallback(
|
|
4160
|
+
(element) => {
|
|
4161
|
+
const id = registeredElementsRef.current.get(element);
|
|
4162
|
+
if (!id || !bridge?.registry) return;
|
|
4163
|
+
bridge.registry.unregisterElement(id);
|
|
4164
|
+
registeredElementsRef.current.delete(element);
|
|
4165
|
+
onUnregister?.(id);
|
|
4166
|
+
},
|
|
4167
|
+
[bridge, onUnregister]
|
|
4168
|
+
);
|
|
4169
|
+
const processPendingRegistrations = useCallback(() => {
|
|
4170
|
+
pendingRegistrationsRef.current.forEach((element) => {
|
|
4171
|
+
if (shouldRegister(element)) {
|
|
4172
|
+
registerElement(element);
|
|
4173
|
+
}
|
|
4174
|
+
});
|
|
4175
|
+
pendingRegistrationsRef.current.clear();
|
|
4176
|
+
}, [shouldRegister, registerElement]);
|
|
4177
|
+
const queueRegistration = useCallback(
|
|
4178
|
+
(element) => {
|
|
4179
|
+
pendingRegistrationsRef.current.add(element);
|
|
4180
|
+
if (debounceTimeoutRef.current) {
|
|
4181
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
4182
|
+
}
|
|
4183
|
+
debounceTimeoutRef.current = setTimeout(processPendingRegistrations, debounceMs);
|
|
4184
|
+
},
|
|
4185
|
+
[debounceMs, processPendingRegistrations]
|
|
4186
|
+
);
|
|
4187
|
+
const scanAndRegister = useCallback(
|
|
4188
|
+
(rootElement) => {
|
|
4189
|
+
const allSelectors = [...INTERACTIVE_SELECTORS2, ...includeSelectors].join(", ");
|
|
4190
|
+
const elements = rootElement.querySelectorAll(allSelectors);
|
|
4191
|
+
elements.forEach((element) => {
|
|
4192
|
+
if (shouldRegister(element)) {
|
|
4193
|
+
queueRegistration(element);
|
|
4194
|
+
}
|
|
4195
|
+
});
|
|
4196
|
+
},
|
|
4197
|
+
[includeSelectors, shouldRegister, queueRegistration]
|
|
4198
|
+
);
|
|
4199
|
+
const handleMutations = useCallback(
|
|
4200
|
+
(mutations) => {
|
|
4201
|
+
mutations.forEach((mutation) => {
|
|
4202
|
+
mutation.addedNodes.forEach((node) => {
|
|
4203
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
4204
|
+
const element = node;
|
|
4205
|
+
if (shouldRegister(element)) {
|
|
4206
|
+
queueRegistration(element);
|
|
4207
|
+
}
|
|
4208
|
+
const allSelectors = [...INTERACTIVE_SELECTORS2, ...includeSelectors].join(", ");
|
|
4209
|
+
const descendants = element.querySelectorAll(allSelectors);
|
|
4210
|
+
descendants.forEach((descendant) => {
|
|
4211
|
+
if (shouldRegister(descendant)) {
|
|
4212
|
+
queueRegistration(descendant);
|
|
4213
|
+
}
|
|
4214
|
+
});
|
|
4215
|
+
}
|
|
4216
|
+
});
|
|
4217
|
+
mutation.removedNodes.forEach((node) => {
|
|
4218
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
4219
|
+
const element = node;
|
|
4220
|
+
if (registeredElementsRef.current.has(element)) {
|
|
4221
|
+
unregisterElement(element);
|
|
4222
|
+
}
|
|
4223
|
+
const descendants = element.querySelectorAll("*");
|
|
4224
|
+
descendants.forEach((descendant) => {
|
|
4225
|
+
if (registeredElementsRef.current.has(descendant)) {
|
|
4226
|
+
unregisterElement(descendant);
|
|
4227
|
+
}
|
|
4228
|
+
});
|
|
4229
|
+
}
|
|
4230
|
+
});
|
|
4231
|
+
});
|
|
4232
|
+
},
|
|
4233
|
+
[shouldRegister, queueRegistration, unregisterElement, includeSelectors]
|
|
4234
|
+
);
|
|
4235
|
+
useEffect(() => {
|
|
4236
|
+
if (!enabled || !bridge?.registry) return;
|
|
4237
|
+
const rootElement = root || document.body;
|
|
4238
|
+
scanAndRegister(rootElement);
|
|
4239
|
+
const observer = new MutationObserver(handleMutations);
|
|
4240
|
+
observer.observe(rootElement, {
|
|
4241
|
+
childList: true,
|
|
4242
|
+
subtree: true
|
|
4243
|
+
});
|
|
4244
|
+
return () => {
|
|
4245
|
+
observer.disconnect();
|
|
4246
|
+
if (debounceTimeoutRef.current) {
|
|
4247
|
+
clearTimeout(debounceTimeoutRef.current);
|
|
4248
|
+
}
|
|
4249
|
+
registeredElementsRef.current.forEach((id, _element) => {
|
|
4250
|
+
bridge.registry.unregisterElement(id);
|
|
4251
|
+
});
|
|
4252
|
+
registeredElementsRef.current.clear();
|
|
4253
|
+
};
|
|
4254
|
+
}, [enabled, bridge, root, scanAndRegister, handleMutations]);
|
|
4255
|
+
}
|
|
4256
|
+
function AutoRegisterProvider({
|
|
4257
|
+
children,
|
|
4258
|
+
scopeToChildren = false,
|
|
4259
|
+
enabled = process.env.NODE_ENV === "development",
|
|
4260
|
+
idStrategy = "prefer-existing",
|
|
4261
|
+
debounceMs = 100,
|
|
4262
|
+
includeHidden = false,
|
|
4263
|
+
includeSelectors = [],
|
|
4264
|
+
excludeSelectors = [],
|
|
4265
|
+
generateId: generateId4,
|
|
4266
|
+
onRegister,
|
|
4267
|
+
onUnregister
|
|
4268
|
+
}) {
|
|
4269
|
+
const containerRef = useRef(null);
|
|
4270
|
+
useAutoRegister({
|
|
4271
|
+
enabled,
|
|
4272
|
+
root: scopeToChildren ? containerRef.current : null,
|
|
4273
|
+
idStrategy,
|
|
4274
|
+
debounceMs,
|
|
4275
|
+
includeHidden,
|
|
4276
|
+
includeSelectors,
|
|
4277
|
+
excludeSelectors,
|
|
4278
|
+
generateId: generateId4,
|
|
4279
|
+
onRegister,
|
|
4280
|
+
onUnregister
|
|
4281
|
+
});
|
|
4282
|
+
if (scopeToChildren) {
|
|
4283
|
+
return /* @__PURE__ */ jsx("div", { ref: containerRef, style: { display: "contents" }, children });
|
|
4284
|
+
}
|
|
4285
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
export { AutoRegisterProvider, UIBridgeProvider, useActiveStates, useAutoRegister, useAvailableTransitions, useCanNavigateTo, useNavigationPath, useStateSnapshot, useTransitions, useUIBridge, useUIBridgeContext, useUIBridgeOptional, useUIBridgeRequired, useUIComponent, useUIComponentAction, useUIElement, useUIElementRef, useUINavigation, useUIState, useUIStateGroup, useUITransition };
|
|
4289
|
+
//# sourceMappingURL=index.mjs.map
|
|
4290
|
+
//# sourceMappingURL=index.mjs.map
|