@xbrowser/cli 0.16.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +17 -26
  2. package/dist/{browser-R7B255ML.js → browser-GITRHHFO.js} +4 -1
  3. package/dist/{browser-GWBH6OJK.js → browser-R56O3CW6.js} +3 -1
  4. package/dist/{browser-I2HJZ7IP.js → browser-ZJOZB5CR.js} +4 -2
  5. package/dist/cdp-driver-BE3FOMRN.js +2803 -0
  6. package/dist/cdp-driver-TOPYJIFL.js +47 -0
  7. package/dist/chunk-2SVQTI2O.js +2794 -0
  8. package/dist/{chunk-KDYXFLAC.js → chunk-ACFE6PKF.js} +1015 -121
  9. package/dist/chunk-BBMRDUYQ.js +260 -0
  10. package/dist/chunk-CAFNSGYM.js +4834 -0
  11. package/dist/{chunk-DTJRVA76.js → chunk-ETCO4SNK.js} +2 -2
  12. package/dist/{chunk-RS6YYWTK.js → chunk-JPA2ZT2R.js} +140 -72
  13. package/dist/chunk-JPHCY4TC.js +260 -0
  14. package/dist/chunk-KFQGP6VL.js +33 -0
  15. package/dist/{chunk-ITKPSIP7.js → chunk-MDAPTB7C.js} +6 -25
  16. package/dist/chunk-OZKD3W4X.js +417 -0
  17. package/dist/chunk-PPG4D2EW.js +2796 -0
  18. package/dist/{chunk-ATFTAKMN.js → chunk-Q4IGYTKR.js} +39 -7
  19. package/dist/{chunk-F3ZWFCJJ.js → chunk-QIK2I3VQ.js} +141 -72
  20. package/dist/chunk-WJRE55TN.js +83 -0
  21. package/dist/cli.js +2358 -1086
  22. package/dist/{convert-4DUWZIKH.js → convert-LB3GJTLR.js} +4 -2
  23. package/dist/{convert-EKQVHKB4.js → convert-R3XXYKC6.js} +2 -2
  24. package/dist/{daemon-client-GX2UYIW4.js → daemon-client-DRCUMNHK.js} +45 -72
  25. package/dist/{daemon-client-XWSSQBEA.js → daemon-client-UZZEHHIV.js} +8 -1
  26. package/dist/daemon-main.js +3067 -1688
  27. package/dist/{extract-JUOQQX4V.js → extract-2ZFW2MX7.js} +1 -1
  28. package/dist/{extract-EGRXZSSK.js → extract-BSYBM4MR.js} +2 -0
  29. package/dist/{filter-OLAE26HN.js → filter-KCFO4RSV.js} +2 -0
  30. package/dist/{filter-VID2GGZ7.js → filter-T7DSZ2X7.js} +1 -1
  31. package/dist/{human-interaction-W753RVJB.js → human-interaction-UKAS5ZXV.js} +2 -2
  32. package/dist/index.d.ts +745 -148
  33. package/dist/index.js +3488 -1719
  34. package/dist/launcher-QUJ4M2VS.js +19 -0
  35. package/dist/launcher-YARP45UY.js +19 -0
  36. package/dist/{network-store-YAF5OIBH.js → network-store-XGZ25FFC.js} +1 -0
  37. package/dist/{network-store-BN6QEZ7R.js → network-store-YVDNUREI.js} +1 -1
  38. package/dist/{parse-action-dsl-T3DYC33D.js → parse-action-dsl-UM333TL2.js} +1 -1
  39. package/dist/{proxy-WKGUCH2C.js → proxy-LV4BJ5RC.js} +1 -1
  40. package/dist/session-recorder-RTDGURIJ.js +8 -0
  41. package/dist/session-recorder-YI7YYM36.js +7 -0
  42. package/dist/session-replayer-GLTUICSD.js +276 -0
  43. package/dist/site-knowledge-SYC6VCDB.js +23 -0
  44. package/package.json +6 -6
  45. package/dist/chunk-2ONMTDLK.js +0 -2050
  46. package/dist/daemon-client-3IJD6X4B.js +0 -59
  47. package/dist/network-store-2S5HATEV.js +0 -194
  48. package/dist/parse-action-dsl-DRSPBALP.js +0 -72
  49. package/dist/screenshot-CWAWMXVA.js +0 -28
  50. package/dist/screenshot-MB6R7RSS.js +0 -26
  51. package/dist/session-recorder-ILSSV2UC.js +0 -6
  52. package/dist/session-recorder-XET3DNML.js +0 -7
@@ -1,14 +1,231 @@
1
+ import {
2
+ init_site_knowledge,
3
+ site_knowledge_exports,
4
+ updateSiteKnowledge
5
+ } from "./chunk-OZKD3W4X.js";
6
+ import {
7
+ __require,
8
+ __toCommonJS
9
+ } from "./chunk-KFQGP6VL.js";
10
+
1
11
  // src/recorder/session-recorder.ts
2
12
  import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "fs";
3
13
  import { join } from "path";
4
14
  import { homedir } from "os";
15
+
16
+ // src/recorder/selector-utils.ts
17
+ function getSelectorGeneratorScript() {
18
+ return `
19
+ (function() {
20
+ if (window.__xb_generateSelector) return;
21
+
22
+ var IGNORE_CLASS = [
23
+ /^css-[a-zA-Z0-9]+$/,
24
+ /^sc-[a-zA-Z0-9]+$/,
25
+ /^emotion-\\d+$/,
26
+ /^_[a-f0-9]{4,}$/,
27
+ /^[a-f0-9]{6,}$/,
28
+ /^styled-[a-zA-Z0-9]+/,
29
+ /^__[a-zA-Z0-9]{4,}$/,
30
+ /^makeStyles-/,
31
+ /^MuiPrivate/,
32
+ /^jss\\d+$/,
33
+ /^ant-[a-z]+$/,
34
+ /^semi-[a-z]{4,}/
35
+ ];
36
+ var STABLE_ATTRS = ['data-testid', 'data-test-id', 'data-cy', 'data-qa', 'data-el'];
37
+ var MAX_DEPTH = 5;
38
+
39
+ function isStableClass(cls) {
40
+ if (!cls || cls.length <= 1) return false;
41
+ for (var i = 0; i < IGNORE_CLASS.length; i++) {
42
+ if (IGNORE_CLASS[i].test(cls)) return false;
43
+ }
44
+ return true;
45
+ }
46
+
47
+ function isUnique(root, sel) {
48
+ try { return root.querySelectorAll(sel).length === 1; } catch(e) { return false; }
49
+ }
50
+
51
+ function esc(s) { return CSS.escape(s); }
52
+
53
+ window.__xb_generateSelector = function(el, root) {
54
+ root = root || document;
55
+ if (!el || !el.tagName) return null;
56
+ if (el === root || el === document.documentElement) return { selector: 'html', strategy: 'root', confidence: 'high' };
57
+
58
+ var tag = el.tagName.toLowerCase();
59
+
60
+ // #id
61
+ if (el.id) {
62
+ var s = '#' + esc(el.id);
63
+ if (isUnique(root, s)) return { selector: s, strategy: 'id', confidence: 'high' };
64
+ s = tag + s;
65
+ if (isUnique(root, s)) return { selector: s, strategy: 'id+tag', confidence: 'high' };
66
+ }
67
+
68
+ // [data-testid]
69
+ for (var si = 0; si < STABLE_ATTRS.length; si++) {
70
+ var v = el.getAttribute(STABLE_ATTRS[si]);
71
+ if (v) {
72
+ var s = '[' + STABLE_ATTRS[si] + '="' + esc(v) + '"]';
73
+ if (isUnique(root, s)) return { selector: s, strategy: 'testid', confidence: 'high' };
74
+ s = tag + s;
75
+ if (isUnique(root, s)) return { selector: s, strategy: 'testid+tag', confidence: 'high' };
76
+ }
77
+ }
78
+
79
+ // [name]
80
+ var name = el.getAttribute('name');
81
+ if (name) {
82
+ var s = tag + '[name="' + esc(name) + '"]';
83
+ if (isUnique(root, s)) return { selector: s, strategy: 'name', confidence: 'high' };
84
+ }
85
+
86
+ // [aria-label]
87
+ var aria = el.getAttribute('aria-label');
88
+ if (aria) {
89
+ var s = '[aria-label="' + esc(aria.substring(0, 60)) + '"]';
90
+ if (isUnique(root, s)) return { selector: s, strategy: 'aria-label', confidence: 'high' };
91
+ s = tag + s;
92
+ if (isUnique(root, s)) return { selector: s, strategy: 'aria-label+tag', confidence: 'high' };
93
+ }
94
+
95
+ // [placeholder]
96
+ var ph = el.getAttribute('placeholder');
97
+ if (ph) {
98
+ var s = tag + '[placeholder="' + esc(ph.substring(0, 60)) + '"]';
99
+ if (isUnique(root, s)) return { selector: s, strategy: 'placeholder', confidence: 'high' };
100
+ }
101
+
102
+ // [alt]
103
+ var alt = el.getAttribute('alt');
104
+ if (alt && tag === 'img') {
105
+ var s = 'img[alt="' + esc(alt.substring(0, 60)) + '"]';
106
+ if (isUnique(root, s)) return { selector: s, strategy: 'alt', confidence: 'high' };
107
+ }
108
+
109
+ // [title]
110
+ var title = el.getAttribute('title');
111
+ if (title) {
112
+ var s = tag + '[title="' + esc(title.substring(0, 60)) + '"]';
113
+ if (isUnique(root, s)) return { selector: s, strategy: 'title', confidence: 'high' };
114
+ }
115
+
116
+ // Unique attribute (skip URL-like, long)
117
+ var skipAttr = {class:1,style:1,id:1,name:1,'aria-label':1,placeholder:1,alt:1,title:1,role:1,src:1,href:1,action:1,'data-src':1,'data-href':1};
118
+ for (var ai = 0; ai < el.attributes.length; ai++) {
119
+ var a = el.attributes[ai];
120
+ if (skipAttr[a.name] || a.name.startsWith('data-') || a.name.startsWith('aria-')) continue;
121
+ if (a.value && a.value.length > 2 && a.value.length <= 60) {
122
+ var s = tag + '[' + a.name + '="' + esc(a.value) + '"]';
123
+ if (isUnique(root, s)) return { selector: s, strategy: 'attribute', confidence: 'medium' };
124
+ }
125
+ }
126
+
127
+ // Class combos
128
+ var rawCls = (typeof el.className === 'string' ? el.className : '').trim().split(/\\s+/);
129
+ var cls = rawCls.filter(isStableClass);
130
+ if (cls.length > 0) {
131
+ cls.sort(function(a, b) {
132
+ return root.querySelectorAll('.' + esc(a)).length - root.querySelectorAll('.' + esc(b)).length;
133
+ });
134
+ // single class
135
+ for (var i = 0; i < cls.length; i++) {
136
+ var s = '.' + esc(cls[i]);
137
+ if (isUnique(root, s)) return { selector: s, strategy: 'class', confidence: 'medium' };
138
+ }
139
+ // tag + class
140
+ for (var i = 0; i < cls.length; i++) {
141
+ var s = tag + '.' + esc(cls[i]);
142
+ if (isUnique(root, s)) return { selector: s, strategy: 'tag+class', confidence: 'medium' };
143
+ }
144
+ // 2-class combo
145
+ for (var i = 0; i < cls.length && i < 5; i++) {
146
+ for (var j = i+1; j < cls.length && j < 5; j++) {
147
+ var s = '.' + esc(cls[i]) + '.' + esc(cls[j]);
148
+ if (isUnique(root, s)) return { selector: s, strategy: 'class-combo', confidence: 'medium' };
149
+ }
150
+ }
151
+ // tag + 2-class combo
152
+ for (var i = 0; i < cls.length && i < 4; i++) {
153
+ for (var j = i+1; j < cls.length && j < 4; j++) {
154
+ var s = tag + '.' + esc(cls[i]) + '.' + esc(cls[j]);
155
+ if (isUnique(root, s)) return { selector: s, strategy: 'tag+class-combo', confidence: 'medium' };
156
+ }
157
+ }
158
+ }
159
+
160
+ // Parent scope
161
+ var parent = el.parentElement;
162
+ if (parent && parent !== root) {
163
+ if (parent.id) {
164
+ var s = '#' + esc(parent.id) + ' > ' + tag;
165
+ if (isUnique(root, s)) return { selector: s, strategy: 'parent-scope', confidence: 'medium' };
166
+ }
167
+ var pCls = (typeof parent.className === 'string' ? parent.className : '').trim().split(/\\s+/).filter(isStableClass);
168
+ for (var i = 0; i < pCls.length; i++) {
169
+ var s = '.' + esc(pCls[i]) + ' > ' + tag;
170
+ if (isUnique(root, s)) return { selector: s, strategy: 'parent-class-scope', confidence: 'medium' };
171
+ }
172
+ if (cls.length > 0) {
173
+ for (var i = 0; i < Math.min(cls.length, 3); i++) {
174
+ var s = parent.tagName.toLowerCase() + ' > ' + tag + '.' + esc(cls[i]);
175
+ if (isUnique(root, s)) return { selector: s, strategy: 'parent>tag.class', confidence: 'medium' };
176
+ }
177
+ }
178
+ }
179
+
180
+ // :nth-of-type chain \u2014 walk all the way up, then find shortest unique suffix
181
+ var path = [];
182
+ var cur = el;
183
+ while (cur && cur !== root && cur !== document.documentElement) {
184
+ var p = cur.parentElement;
185
+ if (!p || p === root) break;
186
+ var t = cur.tagName.toLowerCase();
187
+ var same = [];
188
+ for (var k = 0; k < p.children.length; k++) {
189
+ if (p.children[k].tagName === cur.tagName) same.push(p.children[k]);
190
+ }
191
+ var nth = same.length === 1 ? null : (same.indexOf(cur) + 1);
192
+ path.unshift({tag: t, nth: nth});
193
+ if (cur !== el && cur.id) {
194
+ path.unshift({tag: '#' + esc(cur.id), nth: null});
195
+ break;
196
+ }
197
+ cur = p;
198
+ }
199
+ // Try suffixes from shortest to longest
200
+ for (var d = 1; d <= path.length; d++) {
201
+ var parts = path.slice(-d).map(function(p) {
202
+ return p.nth !== null ? p.tag + ':nth-of-type(' + p.nth + ')' : p.tag;
203
+ });
204
+ var sel = parts.join(' > ');
205
+ if (isUnique(root, sel)) return { selector: sel, strategy: 'nth-of-type', confidence: 'low' };
206
+ }
207
+ // Fallback
208
+ if (path.length > 0) {
209
+ var parts = path.map(function(p) {
210
+ return p.nth !== null ? p.tag + ':nth-of-type(' + p.nth + ')' : p.tag;
211
+ });
212
+ return { selector: parts.join(' > '), strategy: 'nth-of-type', confidence: 'low' };
213
+ }
214
+ return { selector: tag, strategy: 'tag-only', confidence: 'low' };
215
+ };
216
+ })();
217
+ `;
218
+ }
219
+
220
+ // src/recorder/session-recorder.ts
221
+ init_site_knowledge();
5
222
  var ACTION_SIGNAL_SCRIPT = `
6
223
  (function() {
7
224
  if (window.__xb_action_signal) return;
8
225
  window.__xb_action_signal = true;
9
226
  window.__xb_pending_actions = [];
10
227
 
11
- // --- Unique short selector generator ---
228
+ // --- Unique short selector generator (delegates to 13-strategy selector-utils) ---
12
229
  function uniqueSelector(el) {
13
230
  if (!el || !el.tagName) return null;
14
231
  var doc = el.ownerDocument || document;
@@ -17,6 +234,14 @@ var ACTION_SIGNAL_SCRIPT = `
17
234
  try { return doc.querySelectorAll(sel).length === 1; } catch(e) { return false; }
18
235
  }
19
236
 
237
+ // Prefer window.__xb_generateSelector from selector-utils (13 strategies)
238
+ if (typeof window.__xb_generateSelector === 'function') {
239
+ try {
240
+ var result = window.__xb_generateSelector(el, doc);
241
+ if (result && result.selector) return result.selector;
242
+ } catch(e) { /* fallback to local logic */ }
243
+ }
244
+
20
245
  // 1. #id (shortest, globally unique)
21
246
  if (el.id) {
22
247
  var idSel = '#' + CSS.escape(el.id);
@@ -103,10 +328,92 @@ var ACTION_SIGNAL_SCRIPT = `
103
328
  ? (el.value || el.getAttribute('placeholder') || '').trim().substring(0, 40)
104
329
  : (el.textContent || '').trim().substring(0, 40);
105
330
  if (tag === 'a' && el.getAttribute('href')) displayText = el.textContent.trim().substring(0, 40);
331
+
332
+ // Prefer window.__xb_generateSelector (13 strategies) \u2014 also extracts strategy + confidence
333
+ var selector, strategy, confidence;
334
+ if (typeof window.__xb_generateSelector === 'function') {
335
+ try {
336
+ var result = window.__xb_generateSelector(el, el.ownerDocument || document);
337
+ if (result && result.selector) {
338
+ selector = result.selector;
339
+ strategy = result.strategy;
340
+ confidence = result.confidence;
341
+ }
342
+ } catch(e) { /* fall through to local */ }
343
+ }
344
+ if (!selector) {
345
+ selector = uniqueSelector(el);
346
+ }
347
+
348
+ // For low-confidence selectors (nth-of-type), generate a text-based fallback
349
+ // when the element has short, unique text (e.g. menu items "\u5220\u9664", "\u786E\u8BA4")
350
+ var textFallback;
351
+ var popupContext;
352
+
353
+ // Check if element is inside a popup/menu first (needed for scoped text uniqueness)
354
+ var popupEl;
355
+ try {
356
+ popupEl = el.closest('[role="menu"], [role="listbox"], [role="dialog"], [role="tooltip"], [role="list"], [class*="popover"], [class*="popup"], [class*="dropdown"], [class*="menu"], [class*="modal"], [id*="menu"], [id*="dropdown"], [id*="popup"], [id*="modal"]');
357
+ } catch(e) {}
358
+
359
+ if (confidence === 'low') {
360
+ var rawText = (el.textContent || '').trim();
361
+ if (rawText && rawText.length >= 1 && rawText.length <= 30 && el.children.length === 0) {
362
+ try {
363
+ var doc = el.ownerDocument || document;
364
+ var escapedText = rawText.replace(/'/g, "\\'");
365
+
366
+ // If inside popup, check uniqueness WITHIN popup only
367
+ var count;
368
+ if (popupEl && popupEl !== el) {
369
+ var popupCount = doc.evaluate(
370
+ "count(.//*[normalize-space(text())='" + escapedText + "'])",
371
+ popupEl, null, XPathResult.NUMBER_TYPE, null
372
+ );
373
+ count = popupCount.numberValue;
374
+ } else {
375
+ // Check global uniqueness
376
+ var globalCount = doc.evaluate(
377
+ "count(//*[normalize-space(text())='" + escapedText + "'])",
378
+ doc, null, XPathResult.NUMBER_TYPE, null
379
+ );
380
+ count = globalCount.numberValue;
381
+ }
382
+
383
+ if (count === 1) {
384
+ textFallback = {
385
+ type: popupEl && popupEl !== el ? 'popup-text' : 'text',
386
+ value: rawText,
387
+ selector: popupEl && popupEl !== el ? 'popup-text=' + rawText : 'text=' + rawText,
388
+ };
389
+ }
390
+ } catch(e) { /* xpath not available or error */ }
391
+ }
392
+ }
393
+
394
+ // Generate popup context info
395
+ if (popupEl && popupEl !== el) {
396
+ try {
397
+ var popupResult = window.__xb_generateSelector
398
+ ? window.__xb_generateSelector(popupEl, el.ownerDocument || document)
399
+ : null;
400
+ if (popupResult && popupResult.selector) {
401
+ popupContext = {
402
+ containerSelector: popupResult.selector,
403
+ containerText: (popupEl.textContent || '').trim().substring(0, 50),
404
+ };
405
+ }
406
+ } catch(e) { /* skip */ }
407
+ }
408
+
106
409
  return {
107
410
  tag: tag,
108
- selector: uniqueSelector(el),
411
+ selector: selector,
109
412
  text: displayText,
413
+ strategy: strategy,
414
+ confidence: confidence,
415
+ textFallback: textFallback,
416
+ popup: popupContext,
110
417
  role: el.getAttribute('role') || undefined,
111
418
  type: el.getAttribute('type') || undefined,
112
419
  placeholder: el.getAttribute('placeholder') || undefined,
@@ -115,6 +422,107 @@ var ACTION_SIGNAL_SCRIPT = `
115
422
  };
116
423
  }
117
424
 
425
+ // \u2500\u2500 Mouse trajectory capture \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
426
+ // Continuously samples mousemove (every ~60ms).
427
+ // When a meaningful action fires, we snapshot the buffer,
428
+ // simplify it (Douglas-Peucker), and attach as trajectory.
429
+ var __xb_traj_buffer = []; // raw samples: {x, y, t}
430
+ var __xb_traj_last_action = null; // {x, y, t} of previous action
431
+
432
+ document.addEventListener('mousemove', function(e) {
433
+ var now = Date.now();
434
+ // Sample at ~60ms intervals
435
+ if (__xb_traj_buffer.length > 0) {
436
+ var last = __xb_traj_buffer[__xb_traj_buffer.length - 1];
437
+ if (now - last.t < 60) return;
438
+ }
439
+ __xb_traj_buffer.push({ x: e.clientX, y: e.clientY, t: now });
440
+ // Cap buffer at 200 points (~12 seconds at 60ms)
441
+ if (__xb_traj_buffer.length > 200) {
442
+ __xb_traj_buffer = __xb_traj_buffer.slice(-150);
443
+ }
444
+ }, true);
445
+
446
+ // Douglas-Peucker simplification: keep only points that define the path shape
447
+ function dpSimplify(pts, epsilon) {
448
+ if (pts.length <= 2) return pts;
449
+ var maxDist = 0, maxIdx = 0;
450
+ var first = pts[0], last = pts[pts.length - 1];
451
+ for (var i = 1; i < pts.length - 1; i++) {
452
+ var d = pointLineDistance(pts[i], first, last);
453
+ if (d > maxDist) { maxDist = d; maxIdx = i; }
454
+ }
455
+ if (maxDist > epsilon) {
456
+ var left = dpSimplify(pts.slice(0, maxIdx + 1), epsilon);
457
+ var right = dpSimplify(pts.slice(maxIdx), epsilon);
458
+ return left.slice(0, -1).concat(right);
459
+ }
460
+ return [first, last];
461
+ }
462
+
463
+ function pointLineDistance(p, a, b) {
464
+ var dx = b.x - a.x, dy = b.y - a.y;
465
+ var lenSq = dx * dx + dy * dy;
466
+ if (lenSq === 0) return Math.sqrt((p.x - a.x) * (p.x - a.x) + (p.y - a.y) * (p.y - a.y));
467
+ var t = Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / lenSq));
468
+ var projX = a.x + t * dx, projY = a.y + t * dy;
469
+ return Math.sqrt((p.x - projX) * (p.x - projX) + (p.y - projY) * (p.y - projY));
470
+ }
471
+
472
+ // Extract trajectory from buffer ending at (toX, toY, toT).
473
+ // Returns null if no meaningful path.
474
+ function extractTrajectory(toX, toY) {
475
+ var now = Date.now();
476
+ // Add current position as final point
477
+ var raw = __xb_traj_buffer.slice();
478
+ raw.push({ x: toX, y: toY, t: now });
479
+
480
+ // Trim to last 5 seconds
481
+ var cutoff = now - 5000;
482
+ while (raw.length > 0 && raw[0].t < cutoff) raw.shift();
483
+ if (raw.length < 2) { __xb_traj_buffer = []; return null; }
484
+
485
+ // If we know the previous action position, prepend it as start
486
+ if (__xb_traj_last_action) {
487
+ // Trim raw points before the last action
488
+ while (raw.length > 1 && raw[0].t < __xb_traj_last_action.t) raw.shift();
489
+ raw.unshift({ x: __xb_traj_last_action.x, y: __xb_traj_last_action.y, t: __xb_traj_last_action.t });
490
+ }
491
+
492
+ // Simplify (epsilon=3px keeps shape but removes jitter)
493
+ var simplified = dpSimplify(raw, 3);
494
+ if (simplified.length < 2) { __xb_traj_buffer = []; return null; }
495
+
496
+ // Build result with delta times
497
+ var totalDist = 0;
498
+ var points = [];
499
+ for (var i = 0; i < simplified.length; i++) {
500
+ var dt = i === 0 ? 0 : simplified[i].t - simplified[i - 1].t;
501
+ if (i > 0) {
502
+ var ddx = simplified[i].x - simplified[i - 1].x;
503
+ var ddy = simplified[i].y - simplified[i - 1].y;
504
+ totalDist += Math.sqrt(ddx * ddx + ddy * ddy);
505
+ }
506
+ points.push({ x: simplified[i].x, y: simplified[i].y, dt: dt });
507
+ }
508
+
509
+ var duration = simplified[simplified.length - 1].t - simplified[0].t;
510
+
511
+ // Only return if meaningful movement (>5px total distance)
512
+ if (totalDist < 5) { __xb_traj_buffer = []; return null; }
513
+
514
+ // Reset buffer
515
+ __xb_traj_buffer = [];
516
+ __xb_traj_last_action = { x: toX, y: toY, t: now };
517
+
518
+ return { points: points, distance: Math.round(totalDist), duration: duration };
519
+ }
520
+
521
+ // \u2500\u2500 End trajectory capture \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
522
+
523
+ // Expose describe() for CDP command element metadata extraction
524
+ window.__xb_describe = describe;
525
+
118
526
  function isMeaningful(el) {
119
527
  if (!el || !el.tagName) return false;
120
528
  var tag = el.tagName.toLowerCase();
@@ -153,6 +561,12 @@ var ACTION_SIGNAL_SCRIPT = `
153
561
  }
154
562
 
155
563
  function pushAction(type, detail) {
564
+ // Attach mouse trajectory for actions with coordinates
565
+ if (detail && detail.x != null && detail.y != null) {
566
+ var traj = extractTrajectory(detail.x, detail.y);
567
+ if (traj) detail.trajectory = traj;
568
+ }
569
+
156
570
  if (type === 'input') {
157
571
  if (__xb_input_timer) clearTimeout(__xb_input_timer);
158
572
  __xb_input_pending = {
@@ -331,12 +745,66 @@ var ACTION_SIGNAL_SCRIPT = `
331
745
  var tag = target.tagName && target.tagName.toLowerCase();
332
746
  if (tag === 'select') {
333
747
  pushAction('change', { element: describe(target), value: (target.value || '').substring(0, 100) });
748
+ } else if (tag === 'input' && target.type === 'file') {
749
+ var files = target.files;
750
+ var fileNames = [];
751
+ for (var i = 0; i < files.length; i++) {
752
+ fileNames.push(files[i].name);
753
+ }
754
+ // Read file contents asynchronously, then push action
755
+ var readers = [];
756
+ for (var i = 0; i < files.length; i++) {
757
+ readers.push(new Promise(function(resolve) {
758
+ var reader = new FileReader();
759
+ reader.onload = function() { resolve(reader.result); };
760
+ reader.onerror = function() { resolve(null); };
761
+ reader.readAsDataURL(files[i]);
762
+ }));
763
+ }
764
+ Promise.all(readers).then(function(contents) {
765
+ var fileData = [];
766
+ for (var i = 0; i < files.length; i++) {
767
+ fileData.push({
768
+ name: files[i].name,
769
+ type: files[i].type,
770
+ size: files[i].size,
771
+ dataUrl: contents[i],
772
+ });
773
+ }
774
+ pushAction('filechooser', {
775
+ element: describe(target),
776
+ value: fileNames.join(', '),
777
+ files: {
778
+ names: fileNames,
779
+ count: files.length,
780
+ isMultiple: target.multiple,
781
+ fileData: fileData,
782
+ },
783
+ });
784
+ });
334
785
  }
335
786
  }, true);
336
787
 
337
788
  document.addEventListener('keydown', function(e) {
789
+ // Special keys always recorded
338
790
  if (e.key === 'Enter' || e.key === 'Tab' || e.key === 'Escape' || e.key.startsWith('Arrow')) {
339
791
  pushAction('keydown', { key: e.key, element: describe(actualTarget(e)) });
792
+ return;
793
+ }
794
+ // Editing keys
795
+ if (e.key === 'Backspace' || e.key === 'Delete') {
796
+ pushAction('keydown', { key: e.key, element: describe(actualTarget(e)) });
797
+ return;
798
+ }
799
+ // Modifier combinations (Ctrl/Cmd/Alt + key)
800
+ if (e.ctrlKey || e.metaKey || e.altKey) {
801
+ var combo = '';
802
+ if (e.ctrlKey) combo += 'Ctrl+';
803
+ if (e.metaKey) combo += 'Meta+';
804
+ if (e.altKey) combo += 'Alt+';
805
+ if (e.shiftKey) combo += 'Shift+';
806
+ combo += e.key;
807
+ pushAction('keydown', { key: combo, element: describe(actualTarget(e)) });
340
808
  }
341
809
  }, true);
342
810
 
@@ -350,6 +818,155 @@ var ACTION_SIGNAL_SCRIPT = `
350
818
  pushAction('scroll', { scrollX: window.scrollX, scrollY: window.scrollY });
351
819
  }
352
820
  }, true);
821
+
822
+ // \u2500\u2500 Double click \u2500\u2500
823
+ document.addEventListener('dblclick', function(e) {
824
+ pushAction('dblclick', {
825
+ element: describe(resolveMeaningful(e)),
826
+ x: e.clientX,
827
+ y: e.clientY,
828
+ });
829
+ }, true);
830
+
831
+ // \u2500\u2500 Right click (context menu) \u2500\u2500
832
+ document.addEventListener('contextmenu', function(e) {
833
+ pushAction('contextmenu', {
834
+ element: describe(resolveMeaningful(e)),
835
+ x: e.clientX,
836
+ y: e.clientY,
837
+ });
838
+ }, true);
839
+
840
+ // \u2500\u2500 Hover (throttled to 800ms) \u2500\u2500
841
+ var __xb_last_hover = 0;
842
+ document.addEventListener('mouseover', function(e) {
843
+ if (Date.now() - __xb_last_hover < 800) return;
844
+ __xb_last_hover = Date.now();
845
+ var target = resolveMeaningful(e);
846
+ // Only record hover on interactive elements
847
+ var tag = target.tagName && target.tagName.toLowerCase();
848
+ var isInteractive = tag === 'a' || tag === 'button' || tag === 'input'
849
+ || tag === 'select' || tag === 'textarea' || tag === 'summary'
850
+ || target.getAttribute('role') === 'button'
851
+ || target.getAttribute('role') === 'link'
852
+ || target.getAttribute('role') === 'menuitem'
853
+ || target.getAttribute('role') === 'tab'
854
+ || target.getAttribute('role') === 'option'
855
+ || !!target.closest('[role="menu"], [role="menubar"], [role="tablist"], [role="listbox"], [role="tree"], nav, menu');
856
+ if (isInteractive) {
857
+ pushAction('hover', {
858
+ element: describe(target),
859
+ x: e.clientX,
860
+ y: e.clientY,
861
+ });
862
+ }
863
+ }, true);
864
+
865
+ // \u2500\u2500 Drag & drop \u2500\u2500
866
+ var __xb_drag_source = null;
867
+ var __xb_drag_start_pos = null;
868
+ document.addEventListener('dragstart', function(e) {
869
+ __xb_drag_source = e.target;
870
+ __xb_drag_start_pos = { x: e.clientX, y: e.clientY };
871
+ }, true);
872
+ document.addEventListener('drop', function(e) {
873
+ if (__xb_drag_source && __xb_drag_start_pos) {
874
+ pushAction('drag', {
875
+ x: e.clientX,
876
+ y: e.clientY,
877
+ drag: {
878
+ fromX: __xb_drag_start_pos.x,
879
+ fromY: __xb_drag_start_pos.y,
880
+ toX: e.clientX,
881
+ toY: e.clientY,
882
+ source: describe(__xb_drag_source),
883
+ target: describe(resolveMeaningful(e)),
884
+ },
885
+ });
886
+ }
887
+ __xb_drag_source = null;
888
+ __xb_drag_start_pos = null;
889
+ }, true);
890
+ document.addEventListener('dragend', function() {
891
+ __xb_drag_source = null;
892
+ __xb_drag_start_pos = null;
893
+ }, true);
894
+
895
+ // \u2500\u2500 Window resize \u2500\u2500
896
+ var __xb_last_resize = 0;
897
+ window.addEventListener('resize', function() {
898
+ if (Date.now() - __xb_last_resize < 1000) return;
899
+ __xb_last_resize = Date.now();
900
+ pushAction('resize', {
901
+ resize: { width: window.innerWidth, height: window.innerHeight },
902
+ });
903
+ }, true);
904
+
905
+ // \u2500\u2500 Clipboard (copy/paste/cut) \u2500\u2500
906
+ document.addEventListener('copy', function(e) {
907
+ pushAction('clipboard', { clipboard: { operation: 'copy' } });
908
+ }, true);
909
+ document.addEventListener('paste', function(e) {
910
+ var preview = '';
911
+ try {
912
+ preview = (e.clipboardData || window.clipboardData).getData('text').substring(0, 100);
913
+ } catch(ex) {}
914
+ pushAction('clipboard', { clipboard: { operation: 'paste', textPreview: preview } });
915
+ }, true);
916
+ document.addEventListener('cut', function(e) {
917
+ pushAction('clipboard', { clipboard: { operation: 'cut' } });
918
+ }, true);
919
+
920
+ // \u2500\u2500 Touch events \u2500\u2500
921
+ document.addEventListener('touchstart', function(e) {
922
+ var touches = [];
923
+ for (var i = 0; i < e.touches.length; i++) {
924
+ touches.push({ x: e.touches[i].clientX, y: e.touches[i].clientY });
925
+ }
926
+ pushAction('touch', {
927
+ element: describe(resolveMeaningful(e)),
928
+ touch: { touchType: 'start', touches: touches },
929
+ });
930
+ }, true);
931
+ document.addEventListener('touchend', function(e) {
932
+ var touches = [];
933
+ for (var i = 0; i < e.changedTouches.length; i++) {
934
+ touches.push({ x: e.changedTouches[i].clientX, y: e.changedTouches[i].clientY });
935
+ }
936
+ pushAction('touch', {
937
+ element: describe(resolveMeaningful(e)),
938
+ touch: { touchType: 'end', touches: touches },
939
+ });
940
+ }, true);
941
+
942
+ // \u2500\u2500 Focus / Blur \u2500\u2500
943
+ document.addEventListener('focusin', function(e) {
944
+ var target = actualTarget(e);
945
+ var tag = target.tagName && target.tagName.toLowerCase();
946
+ if (tag === 'input' || tag === 'textarea' || tag === 'select' || target.isContentEditable) {
947
+ pushAction('focus', {
948
+ element: describe(target),
949
+ focus: { focusType: 'focus' },
950
+ });
951
+ }
952
+ }, true);
953
+ document.addEventListener('focusout', function(e) {
954
+ var target = actualTarget(e);
955
+ var tag = target.tagName && target.tagName.toLowerCase();
956
+ if (tag === 'input' || tag === 'textarea' || tag === 'select' || target.isContentEditable) {
957
+ pushAction('focus', {
958
+ element: describe(target),
959
+ focus: { focusType: 'blur' },
960
+ });
961
+ }
962
+ }, true);
963
+
964
+ // \u2500\u2500 Visibility change (tab switch) \u2500\u2500
965
+ document.addEventListener('visibilitychange', function() {
966
+ pushAction('visibility', {
967
+ visibility: { state: document.hidden ? 'hidden' : 'visible' },
968
+ });
969
+ }, true);
353
970
  })();
354
971
  `;
355
972
  var CHECKPOINT_OVERLAY_SCRIPT = `
@@ -605,6 +1222,13 @@ var SessionRecorder = class _SessionRecorder {
605
1222
  flushTimer = null;
606
1223
  lastActionTs = 0;
607
1224
  activePages = /* @__PURE__ */ new Set();
1225
+ lastKnownUrl = "";
1226
+ // Track URL to detect real navigation changes
1227
+ /** Dedup window: after a CDP command action, ignore matching action signals within this window */
1228
+ cdpActionDedup = null;
1229
+ /** Network dedup: last request key for short-window dedup */
1230
+ _lastRequestKey = "";
1231
+ _lastRequestTs = 0;
608
1232
  _isRecording = false;
609
1233
  constructor(context, page, sessionName) {
610
1234
  this.context = context;
@@ -617,6 +1241,43 @@ var SessionRecorder = class _SessionRecorder {
617
1241
  get actionCount() {
618
1242
  return this.actions.length;
619
1243
  }
1244
+ /** Record an action triggered by a CDP command (e.g. xbrowser fill/click/goto) */
1245
+ recordCommandAction(action) {
1246
+ const normalizedType = action.type === "cdp-fill" ? "input" : action.type === "cdp-click" ? "click" : action.type;
1247
+ const recent = this.actions[this.actions.length - 1];
1248
+ if (recent && Date.now() - recent.timestamp < 1500) {
1249
+ const typeMatch = recent.type === action.type || recent.type === normalizedType;
1250
+ const valueMatch = !action.value || recent.value === action.value;
1251
+ const selectorMatch = !action.selector || recent.element?.selector && (recent.element.selector === action.selector || recent.element.selector.endsWith(" " + action.selector) || action.selector.endsWith(" " + recent.element.selector));
1252
+ if (typeMatch && valueMatch && selectorMatch) {
1253
+ return;
1254
+ }
1255
+ }
1256
+ this.actionCounter++;
1257
+ const ts = Date.now();
1258
+ const actionUrl = action.url && action.url !== "about:blank" ? action.url : this.lastKnownUrl || this.page.url();
1259
+ this.actions.push({
1260
+ id: this.actionCounter,
1261
+ type: action.type,
1262
+ timestamp: ts,
1263
+ url: actionUrl,
1264
+ pageTitle: "",
1265
+ element: action.element || (action.selector ? { tag: "", selector: action.selector, text: "" } : void 0),
1266
+ value: action.value
1267
+ });
1268
+ this.lastActionTs = ts;
1269
+ this.cdpActionDedup = {
1270
+ type: normalizedType,
1271
+ value: action.value,
1272
+ selector: action.selector,
1273
+ until: Date.now() + 1500
1274
+ };
1275
+ if (action.url && action.url !== "about:blank") {
1276
+ this.lastKnownUrl = action.url;
1277
+ } else if (action.type === "goto" && action.value && action.value !== "about:blank") {
1278
+ this.lastKnownUrl = action.value;
1279
+ }
1280
+ }
620
1281
  get networkCount() {
621
1282
  return this.network.length;
622
1283
  }
@@ -663,6 +1324,10 @@ var SessionRecorder = class _SessionRecorder {
663
1324
  this.contextChanges = [];
664
1325
  this.checkpoints = [];
665
1326
  this.checkpointCounter = 0;
1327
+ this.lastKnownUrl = this.page.url();
1328
+ await this.page.addInitScript(getSelectorGeneratorScript());
1329
+ await this.page.addInitScript(ACTION_SIGNAL_SCRIPT);
1330
+ await this.page.addInitScript(CHECKPOINT_OVERLAY_SCRIPT);
666
1331
  if (url) {
667
1332
  await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
668
1333
  this.startUrl = url;
@@ -678,13 +1343,17 @@ var SessionRecorder = class _SessionRecorder {
678
1343
  };
679
1344
  writeFileSync(this.controlFilePath, JSON.stringify(control, null, 2), "utf-8");
680
1345
  await this.injectActionScript(this.page);
681
- await this.page.addInitScript(ACTION_SIGNAL_SCRIPT);
682
- await this.page.addInitScript(CHECKPOINT_OVERLAY_SCRIPT);
683
1346
  this.context.on("request", this.handleRequest);
684
1347
  this.context.on("response", this.handleResponse);
685
1348
  this.context.on("page", this.handleNewPage);
1349
+ for (const p of this.context.pages()) {
1350
+ p.on("request", this.handleRequest);
1351
+ p.on("response", this.handleResponse);
1352
+ }
1353
+ this.context.on("page", this.handleNewPage);
686
1354
  this.page.on("framenavigated", this.handleFrameNavigated);
687
1355
  this.page.on("dialog", this.handleDialog);
1356
+ this.page.on("filechooser", this.handleFileChooser);
688
1357
  this.pollTimer = setInterval(() => void this.pollActions(), 200);
689
1358
  this.flushTimer = setInterval(() => this.flushToDisk(), 5e3);
690
1359
  }
@@ -719,6 +1388,21 @@ var SessionRecorder = class _SessionRecorder {
719
1388
  const data = this.buildData();
720
1389
  const summary = this.buildSummary(data);
721
1390
  this.writeFinalOutput(data, summary);
1391
+ try {
1392
+ const knowledge = updateSiteKnowledge(data);
1393
+ const knowledgeDir = join(this.recordingsDir, "site-knowledge.md");
1394
+ const knowledgeJson = join(this.recordingsDir, "site-knowledge.json");
1395
+ const { readFileSync: rf } = __require("fs");
1396
+ const { getKnowledgePath } = (init_site_knowledge(), __toCommonJS(site_knowledge_exports));
1397
+ const mdPath = getKnowledgePath(knowledge.domain, "md");
1398
+ try {
1399
+ const md = rf(mdPath, "utf-8");
1400
+ writeFileSync(knowledgeDir, md, "utf-8");
1401
+ } catch {
1402
+ }
1403
+ writeFileSync(knowledgeJson, JSON.stringify(knowledge, null, 2), "utf-8");
1404
+ } catch {
1405
+ }
722
1406
  try {
723
1407
  rmSync(this.controlFilePath);
724
1408
  } catch {
@@ -811,13 +1495,75 @@ var SessionRecorder = class _SessionRecorder {
811
1495
  // ==================== Private ====================
812
1496
  async injectActionScript(page) {
813
1497
  try {
1498
+ await page.evaluate(getSelectorGeneratorScript());
814
1499
  await page.evaluate(ACTION_SIGNAL_SCRIPT);
1500
+ await page.evaluate(`window.__xb_action_script_src = ${JSON.stringify(ACTION_SIGNAL_SCRIPT)};`);
815
1501
  } catch {
816
1502
  }
817
1503
  try {
818
1504
  await page.evaluate(CHECKPOINT_OVERLAY_SCRIPT);
819
1505
  } catch {
820
1506
  }
1507
+ try {
1508
+ await page.evaluate(`
1509
+ (function() {
1510
+ var _scriptSrc = ${JSON.stringify(ACTION_SIGNAL_SCRIPT)};
1511
+ function injectIframe(iframe) {
1512
+ try {
1513
+ var w = iframe.contentWindow;
1514
+ if (!w) return;
1515
+ // Force re-injection: clear old flag
1516
+ try { delete w.__xb_action_signal; } catch(e) {}
1517
+ w.eval(_scriptSrc);
1518
+ // Tag iframe so flushIframes knows it's injected
1519
+ iframe.__xb_injected = true;
1520
+ } catch(e) {}
1521
+ }
1522
+ function watchIframe(iframe) {
1523
+ if (iframe.__xb_watched) return;
1524
+ iframe.__xb_watched = true;
1525
+ injectIframe(iframe);
1526
+ iframe.addEventListener('load', function() { injectIframe(iframe); });
1527
+ }
1528
+ // Inject into existing iframes
1529
+ try {
1530
+ var iframes = document.querySelectorAll('iframe');
1531
+ for (var i = 0; i < iframes.length; i++) watchIframe(iframes[i]);
1532
+ } catch(e) {}
1533
+ // Watch for dynamically inserted iframes
1534
+ if (!window.__xb_iframe_observer) {
1535
+ window.__xb_iframe_observer = new MutationObserver(function(mutations) {
1536
+ for (var m = 0; m < mutations.length; m++) {
1537
+ for (var n = 0; n < mutations[m].addedNodes.length; n++) {
1538
+ var node = mutations[m].addedNodes[n];
1539
+ if (node.tagName === 'IFRAME') {
1540
+ watchIframe(node);
1541
+ } else if (node.querySelectorAll) {
1542
+ var sub = node.querySelectorAll('iframe');
1543
+ for (var k = 0; k < sub.length; k++) watchIframe(sub[k]);
1544
+ }
1545
+ }
1546
+ }
1547
+ });
1548
+ window.__xb_iframe_observer.observe(document.documentElement, { childList: true, subtree: true });
1549
+ }
1550
+ // Periodic re-injection for iframes that load late or navigate internally
1551
+ if (!window.__xb_iframe_timer) {
1552
+ window.__xb_iframe_timer = setInterval(function() {
1553
+ try {
1554
+ var iframes = document.querySelectorAll('iframe');
1555
+ for (var i = 0; i < iframes.length; i++) {
1556
+ if (!iframes[i].__xb_injected) {
1557
+ watchIframe(iframes[i]);
1558
+ }
1559
+ }
1560
+ } catch(e) {}
1561
+ }, 3000);
1562
+ }
1563
+ })();
1564
+ `);
1565
+ } catch {
1566
+ }
821
1567
  }
822
1568
  // ─── Network capture ────────────────────────────────────────────
823
1569
  handleRequest = (request) => {
@@ -825,6 +1571,11 @@ var SessionRecorder = class _SessionRecorder {
825
1571
  if (["image", "stylesheet", "font", "manifest", "other"].includes(resourceType)) return;
826
1572
  const url = request.url();
827
1573
  if (url.startsWith("data:") || url.startsWith("chrome-extension://") || url.startsWith("blob:")) return;
1574
+ const dedupKey = request.method() + " " + url;
1575
+ const now = Date.now();
1576
+ if (this._lastRequestKey === dedupKey && now - this._lastRequestTs < 100) return;
1577
+ this._lastRequestKey = dedupKey;
1578
+ this._lastRequestTs = now;
828
1579
  this.networkCounter++;
829
1580
  const entry = {
830
1581
  id: this.networkCounter,
@@ -894,22 +1645,68 @@ var SessionRecorder = class _SessionRecorder {
894
1645
  });
895
1646
  await page.addInitScript(ACTION_SIGNAL_SCRIPT);
896
1647
  await page.addInitScript(CHECKPOINT_OVERLAY_SCRIPT);
897
- await this.injectActionScript(page).catch(() => {
898
- });
1648
+ await page.addInitScript(getSelectorGeneratorScript());
1649
+ const injectAndRetry = async () => {
1650
+ for (let attempt = 0; attempt < 5; attempt++) {
1651
+ try {
1652
+ const url = page.url();
1653
+ if (url && url !== "about:blank" && !url.startsWith("chrome")) {
1654
+ await this.injectActionScript(page);
1655
+ return;
1656
+ }
1657
+ } catch {
1658
+ }
1659
+ await new Promise((r) => setTimeout(r, 1e3));
1660
+ }
1661
+ };
1662
+ injectAndRetry();
899
1663
  page.on("framenavigated", this.handleFrameNavigated);
1664
+ page.on("request", this.handleRequest);
1665
+ page.on("response", this.handleResponse);
1666
+ page.on("filechooser", this.handleFileChooser);
1667
+ page.on("dialog", this.handleDialog);
900
1668
  page.on("close", () => {
901
1669
  this.activePages.delete(page);
902
1670
  });
1671
+ page.on("framenavigated", async (frame) => {
1672
+ if (frame !== frame.page().mainFrame()) return;
1673
+ const url = frame.url();
1674
+ if (url && url !== "about:blank" && !url.startsWith("chrome")) {
1675
+ try {
1676
+ await this.injectActionScript(page);
1677
+ } catch {
1678
+ }
1679
+ }
1680
+ });
903
1681
  };
904
1682
  handleFrameNavigated = (frame) => {
905
1683
  if (frame !== frame.page().mainFrame()) return;
1684
+ const newUrl = frame.url();
906
1685
  this.contextCounter++;
907
1686
  this.contextChanges.push({
908
1687
  id: this.contextCounter,
909
1688
  timestamp: Date.now(),
910
1689
  type: "navigate",
911
- url: frame.url()
1690
+ url: newUrl
912
1691
  });
1692
+ const lastAction = this.actions[this.actions.length - 1];
1693
+ const lastActionUrl = lastAction?.url;
1694
+ if (newUrl && newUrl !== "about:blank" && newUrl !== lastActionUrl) {
1695
+ this.actionCounter++;
1696
+ this.actions.push({
1697
+ id: this.actionCounter,
1698
+ type: "navigation",
1699
+ timestamp: Date.now(),
1700
+ url: newUrl,
1701
+ pageTitle: "",
1702
+ element: void 0
1703
+ });
1704
+ }
1705
+ const page = frame.page();
1706
+ if (newUrl && newUrl !== "about:blank") {
1707
+ this.injectActionScript(page).catch(() => {
1708
+ });
1709
+ }
913
1710
  };
914
1711
  handleDialog = async (dialog) => {
915
1712
  this.checkpointCounter++;
@@ -919,13 +1716,69 @@ var SessionRecorder = class _SessionRecorder {
919
1716
  timestamp: Date.now(),
920
1717
  url: this.page.url(),
921
1718
  pageTitle: await this.page.title().catch(() => ""),
922
- hint: `Dialog [${dialog.type()}]: "${dialog.message()}"`,
1719
+ hint: `Dialog [${dialog.type}]: "${dialog.message()}"`,
923
1720
  source: "auto",
924
- context: { dialogType: dialog.type(), message: dialog.message() }
1721
+ context: { dialogType: dialog.type, message: dialog.message() }
925
1722
  });
926
1723
  await dialog.dismiss().catch(() => {
927
1724
  });
928
1725
  };
1726
+ handleFileChooser = async (fileChooser) => {
1727
+ const url = this.page.url();
1728
+ const sel = fileChooser.selector || 'input[type="file"]';
1729
+ let element;
1730
+ try {
1731
+ element = await this.page.evaluate(new Function("selector", `
1732
+ const el = document.querySelector(selector);
1733
+ if (!el) return null;
1734
+ return window.__xb_describe(el);
1735
+ `), sel) ?? void 0;
1736
+ } catch {
1737
+ }
1738
+ let fileData = [];
1739
+ try {
1740
+ fileData = await this.page.evaluate(new Function("selector", `
1741
+ return new Promise(resolve => {
1742
+ const input = document.querySelector(selector);
1743
+ if (!input || !input.files || input.files.length === 0) { resolve([]); return; }
1744
+ const readers = [];
1745
+ for (let i = 0; i < input.files.length; i++) {
1746
+ readers.push(new Promise(r => {
1747
+ const reader = new FileReader();
1748
+ reader.onload = () => r({ name: input.files[i].name, type: input.files[i].type, size: input.files[i].size, dataUrl: reader.result });
1749
+ reader.onerror = () => r({ name: input.files[i].name, type: input.files[i].type, size: input.files[i].size, dataUrl: null });
1750
+ reader.readAsDataURL(input.files[i]);
1751
+ }));
1752
+ }
1753
+ Promise.all(readers).then(resolve);
1754
+ });
1755
+ `), sel);
1756
+ } catch {
1757
+ }
1758
+ const names = fileData.map((f) => f.name);
1759
+ this.actionCounter++;
1760
+ this.actions.push({
1761
+ id: this.actionCounter,
1762
+ type: "filechooser",
1763
+ timestamp: Date.now(),
1764
+ url,
1765
+ pageTitle: "",
1766
+ element: element || {
1767
+ selector: sel,
1768
+ strategy: "css",
1769
+ confidence: "high",
1770
+ tag: "input",
1771
+ text: ""
1772
+ },
1773
+ value: names.join(", ") || void 0,
1774
+ files: {
1775
+ names,
1776
+ count: fileData.length,
1777
+ isMultiple: fileChooser.isMultiple,
1778
+ fileData: fileData.length > 0 ? fileData : void 0
1779
+ }
1780
+ });
1781
+ };
929
1782
  // ─── Action polling ─────────────────────────────────────────────
930
1783
  async pollActions() {
931
1784
  const pages = [this.page, ...this.activePages];
@@ -940,17 +1793,86 @@ var SessionRecorder = class _SessionRecorder {
940
1793
  async flushPendingActions(page) {
941
1794
  let pending = [];
942
1795
  try {
943
- pending = await page.evaluate(() => {
944
- const w = window;
945
- const actions = w.__xb_pending_actions || [];
1796
+ pending = await page.evaluate(`(function() {
1797
+ var w = window;
1798
+ var actions = w.__xb_pending_actions || [];
946
1799
  w.__xb_pending_actions = [];
1800
+
1801
+ // Recursively flush pending actions from same-origin iframes (including nested)
1802
+ function flushIframes(doc) {
1803
+ try {
1804
+ var iframes = doc.querySelectorAll('iframe');
1805
+ for (var i = 0; i < iframes.length; i++) {
1806
+ try {
1807
+ var iframeWin = iframes[i].contentWindow;
1808
+ if (!iframeWin) continue;
1809
+ // Ensure action script is injected into iframe
1810
+ if (!iframeWin.__xb_action_script_injected) {
1811
+ iframeWin.__xb_action_script_injected = true;
1812
+ try { delete iframeWin.__xb_action_signal; } catch(e) {}
1813
+ try { iframeWin.eval(w.__xb_action_script_src); } catch(e) {}
1814
+ }
1815
+ var iframeActions = iframeWin.__xb_pending_actions;
1816
+ if (Array.isArray(iframeActions) && iframeActions.length > 0) {
1817
+ // Get iframe position to offset coordinates
1818
+ var rect = iframes[i].getBoundingClientRect();
1819
+ for (var j = 0; j < iframeActions.length; j++) {
1820
+ var act = iframeActions[j];
1821
+ // Offset coordinates from iframe-relative to page-relative
1822
+ if (act.x != null) act.x = act.x + rect.left;
1823
+ if (act.y != null) act.y = act.y + rect.top;
1824
+ // Tag the action as originating from an iframe
1825
+ act.iframeSrc = iframes[i].src || '';
1826
+ actions.push(act);
1827
+ }
1828
+ iframeWin.__xb_pending_actions = [];
1829
+ }
1830
+ // Recurse into nested iframes
1831
+ try { flushIframes(iframeWin.document); } catch(e) {}
1832
+ } catch(e) {}
1833
+ }
1834
+ } catch(e) {}
1835
+ }
1836
+
1837
+ flushIframes(document);
947
1838
  return actions;
948
- });
1839
+ })()`);
949
1840
  } catch {
950
1841
  return;
951
1842
  }
1843
+ try {
1844
+ const currentUrl = page.url();
1845
+ const normalize = (u) => u.replace(/\/+$/, "");
1846
+ if (currentUrl && currentUrl !== "about:blank" && normalize(currentUrl) !== normalize(this.lastKnownUrl)) {
1847
+ const hasNav = this.actions.slice(-3).some(
1848
+ (a) => (a.type === "navigation" || a.type === "goto") && normalize(a.url || "") === normalize(currentUrl)
1849
+ );
1850
+ if (!hasNav) {
1851
+ this.actionCounter++;
1852
+ this.actions.push({
1853
+ id: this.actionCounter,
1854
+ type: "navigation",
1855
+ timestamp: Date.now(),
1856
+ url: currentUrl,
1857
+ pageTitle: "",
1858
+ element: void 0
1859
+ });
1860
+ }
1861
+ this.lastKnownUrl = currentUrl;
1862
+ }
1863
+ } catch {
1864
+ }
952
1865
  for (const raw of pending) {
953
1866
  if (raw.ts <= this.lastActionTs) continue;
1867
+ if (this.cdpActionDedup && Date.now() < this.cdpActionDedup.until) {
1868
+ const dedup = this.cdpActionDedup;
1869
+ const typeMatch = raw.type === dedup.type;
1870
+ const valueMatch = !dedup.value || raw.value === dedup.value;
1871
+ const selectorMatch = !dedup.selector || raw.element?.selector && (raw.element.selector === dedup.selector || raw.element.selector.endsWith(" " + dedup.selector) || dedup.selector.endsWith(" " + raw.element.selector));
1872
+ if (typeMatch && valueMatch && selectorMatch) {
1873
+ continue;
1874
+ }
1875
+ }
954
1876
  this.actionCounter++;
955
1877
  let clickContext;
956
1878
  if (raw.type === "click" && raw.x !== void 0 && raw.y !== void 0) {
@@ -969,7 +1891,8 @@ var SessionRecorder = class _SessionRecorder {
969
1891
  y: raw.y,
970
1892
  scrollX: raw.scrollX,
971
1893
  scrollY: raw.scrollY,
972
- clickContext
1894
+ clickContext,
1895
+ files: raw.files
973
1896
  });
974
1897
  this.lastActionTs = raw.ts;
975
1898
  if (raw.type === "click" || raw.type === "navigate" || raw.type === "submit") {
@@ -989,116 +1912,87 @@ var SessionRecorder = class _SessionRecorder {
989
1912
  async captureClickContext(page, cx, cy) {
990
1913
  await new Promise((r) => setTimeout(r, 300));
991
1914
  try {
992
- const ctx = await page.evaluate(([cx2, cy2]) => {
993
- const POPOVER_SELECTORS = [
994
- '[role="menu"]',
995
- '[role="listbox"]',
996
- '[role="dialog"]',
997
- '[role="tooltip"]',
998
- '[role="popover"]',
999
- '[role="combobox"]',
1000
- '[role="tree"]',
1001
- '[role="grid"]',
1002
- ".popover",
1003
- ".popup",
1004
- ".dropdown",
1005
- ".menu",
1006
- ".modal",
1007
- ".tooltip",
1008
- ".panel",
1009
- '[class*="popover"]',
1010
- '[class*="popup"]',
1011
- '[class*="dropdown"]',
1012
- '[class*="menu"]',
1013
- '[class*="tooltip"]',
1014
- '[class*="modal"]',
1015
- '[class*="panel"]',
1016
- '[class*="overlay"]',
1017
- '[class*="sheet"]',
1018
- "[data-popup]",
1019
- "[data-dropdown]",
1020
- "[data-menu]",
1021
- '[data-popover"]',
1022
- ".semi-dropdown",
1023
- ".semi-popover",
1024
- ".semi-modal",
1025
- ".ant-dropdown",
1026
- ".ant-popover",
1027
- ".ant-modal",
1028
- ".el-dropdown",
1029
- ".el-popover",
1030
- ".el-dialog",
1031
- ".t-dropdown",
1032
- ".t-popup",
1033
- ".t-dialog"
1034
- ];
1035
- function isNear(el, x, y, range) {
1036
- try {
1037
- const r = el.getBoundingClientRect();
1038
- if (!r || r.width === 0 || r.height === 0) return false;
1039
- return !(r.left > x + range || r.right < x - range || r.top > y + range || r.bottom < y - range);
1040
- } catch {
1041
- return false;
1915
+ const ctx = await page.evaluate(`
1916
+ (function() {
1917
+ var cx = ${cx}, cy = ${cy};
1918
+ var POPOVER_SELECTORS = [
1919
+ '[role="menu"]','[role="listbox"]','[role="dialog"]','[role="tooltip"]','[role="popover"]',
1920
+ '[role="combobox"]','[role="tree"]','[role="grid"]',
1921
+ '.popover','.popup','.dropdown','.menu','.modal','.tooltip','.panel',
1922
+ '[class*="popover"]','[class*="popup"]','[class*="dropdown"]','[class*="menu"]',
1923
+ '[class*="tooltip"]','[class*="modal"]','[class*="panel"]','[class*="overlay"]','[class*="sheet"]',
1924
+ '[data-popup]','[data-dropdown]','[data-menu]','[data-popover"]',
1925
+ '.semi-dropdown','.semi-popover','.semi-modal',
1926
+ '.ant-dropdown','.ant-popover','.ant-modal',
1927
+ '.el-dropdown','.el-popover','.el-dialog',
1928
+ '.t-dropdown','.t-popup','.t-dialog'
1929
+ ];
1930
+
1931
+ function isNear(el, x, y, range) {
1932
+ try {
1933
+ var r = el.getBoundingClientRect();
1934
+ if (!r || r.width === 0 || r.height === 0) return false;
1935
+ return !(r.left > x + range || r.right < x - range || r.top > y + range || r.bottom < y - range);
1936
+ } catch(e) { return false; }
1042
1937
  }
1043
- }
1044
- const result = {
1045
- appeared: [],
1046
- disappeared: [],
1047
- stateChanges: []
1048
- };
1049
- const seenSelectors = /* @__PURE__ */ new Set();
1050
- for (const sel of POPOVER_SELECTORS) {
1051
- try {
1052
- const els = document.querySelectorAll(sel);
1053
- for (let j = 0; j < els.length; j++) {
1054
- const el = els[j];
1055
- if (!isNear(el, cx2, cy2, 500)) continue;
1056
- const rect = el.getBoundingClientRect();
1057
- if (rect.width === 0 || rect.height === 0) continue;
1058
- const elSel = el.id ? "#" + el.id : sel;
1059
- if (seenSelectors.has(elSel + rect.x + rect.y)) continue;
1060
- seenSelectors.add(elSel + rect.x + rect.y);
1061
- const items = [];
1062
- const children = el.querySelectorAll('a,button,[role="menuitem"],[role="option"],[role="treeitem"],li,div[class*="item"]');
1063
- for (let k = 0; k < Math.min(children.length, 20); k++) {
1064
- const child = children[k];
1065
- const childText = (child.textContent || "").trim().substring(0, 60);
1066
- if (!childText) continue;
1067
- const ci = { text: childText };
1068
- if (child.disabled || child.getAttribute("aria-disabled") === "true") ci.disabled = true;
1069
- if (child.tagName) ci.tag = child.tagName.toLowerCase();
1070
- if (child.href) ci.href = child.href.substring(0, 80);
1071
- items.push(ci);
1938
+
1939
+ var result = { appeared: [], disappeared: [], stateChanges: [] };
1940
+ var seenElements = new Set();
1941
+
1942
+ for (var si = 0; si < POPOVER_SELECTORS.length; si++) {
1943
+ try {
1944
+ var els = document.querySelectorAll(POPOVER_SELECTORS[si]);
1945
+ for (var j = 0; j < els.length; j++) {
1946
+ var el = els[j];
1947
+ if (seenElements.has(el)) continue;
1948
+ if (!isNear(el, cx, cy, 300)) continue;
1949
+ var rect = el.getBoundingClientRect();
1950
+ if (rect.width === 0 || rect.height === 0) continue;
1951
+ seenElements.add(el);
1952
+
1953
+ var items = [];
1954
+ var children = el.querySelectorAll('a,button,[role="menuitem"],[role="option"],[role="treeitem"],li,div[class*="item"]');
1955
+ var seenItemTexts = new Set();
1956
+ for (var k = 0; k < Math.min(children.length, 30); k++) {
1957
+ var child = children[k];
1958
+ var childText = (child.textContent || '').trim().substring(0, 60);
1959
+ if (!childText || seenItemTexts.has(childText)) continue;
1960
+ seenItemTexts.add(childText);
1961
+ var ci = { text: childText };
1962
+ try { if (child.disabled || child.getAttribute('aria-disabled') === 'true') ci.disabled = true; } catch(e) {}
1963
+ try { if (child.tagName) ci.tag = child.tagName.toLowerCase(); } catch(e) {}
1964
+ try { if (child.href) ci.href = child.href.substring(0, 80); } catch(e) {}
1965
+ items.push(ci);
1966
+ }
1967
+
1968
+ result.appeared.push({
1969
+ tag: el.tagName.toLowerCase(),
1970
+ selector: el.id ? '#' + el.id : undefined,
1971
+ role: el.getAttribute('role'),
1972
+ text: (el.textContent || '').trim().substring(0, 100),
1973
+ rect: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
1974
+ items: items
1975
+ });
1072
1976
  }
1073
- result.appeared.push({
1074
- tag: el.tagName.toLowerCase(),
1075
- selector: el.id ? "#" + el.id : void 0,
1076
- role: el.getAttribute("role"),
1077
- text: (el.textContent || "").trim().substring(0, 100),
1078
- rect: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
1079
- items
1080
- });
1081
- }
1082
- } catch {
1977
+ } catch(e) { /* skip invalid selectors */ }
1083
1978
  }
1084
- }
1085
- const allInteractive = document.querySelectorAll("[aria-expanded],[disabled],[aria-disabled],[aria-selected],[data-state]");
1086
- for (let i = 0; i < allInteractive.length; i++) {
1087
- const el = allInteractive[i];
1088
- if (!isNear(el, cx2, cy2, 400)) continue;
1089
- const info = {
1090
- tag: el.tagName.toLowerCase(),
1091
- text: (el.textContent || "").trim().substring(0, 60)
1092
- };
1093
- if (el.id) info.id = el.id;
1094
- if (el.getAttribute("aria-expanded")) info.ariaExpanded = el.getAttribute("aria-expanded");
1095
- if (el.disabled || el.getAttribute("aria-disabled") === "true") info.disabled = true;
1096
- if (el.getAttribute("aria-selected")) info.ariaSelected = el.getAttribute("aria-selected");
1097
- if (el.getAttribute("data-state")) info.dataState = el.getAttribute("data-state");
1098
- result.stateChanges.push(info);
1099
- }
1100
- return result;
1101
- }, [cx, cy]);
1979
+
1980
+ var allInteractive = document.querySelectorAll('[aria-expanded],[disabled],[aria-disabled],[aria-selected],[data-state]');
1981
+ for (var i = 0; i < allInteractive.length; i++) {
1982
+ var el2 = allInteractive[i];
1983
+ if (!isNear(el2, cx, cy, 200)) continue;
1984
+ var info = { tag: el2.tagName.toLowerCase(), text: (el2.textContent || '').trim().substring(0, 60) };
1985
+ try { if (el2.id) info.id = el2.id; } catch(e) {}
1986
+ try { if (el2.getAttribute('aria-expanded')) info.ariaExpanded = el2.getAttribute('aria-expanded'); } catch(e) {}
1987
+ try { if (el2.disabled || el2.getAttribute('aria-disabled') === 'true') info.disabled = true; } catch(e) {}
1988
+ try { if (el2.getAttribute('aria-selected')) info.ariaSelected = el2.getAttribute('aria-selected'); } catch(e) {}
1989
+ try { if (el2.getAttribute('data-state')) info.dataState = el2.getAttribute('data-state'); } catch(e) {}
1990
+ result.stateChanges.push(info);
1991
+ }
1992
+
1993
+ return result;
1994
+ })()
1995
+ `);
1102
1996
  if (ctx.appeared.length > 0 || ctx.stateChanges.length > 0) {
1103
1997
  return ctx;
1104
1998
  }