@xbrowser/cli 1.0.0 → 1.0.3

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 (48) hide show
  1. package/README.md +17 -26
  2. package/dist/{browser-DSVV4GHS.js → browser-5CTOA2WS.js} +4 -3
  3. package/dist/{browser-53KUFEEM.js → browser-ITLZZDHJ.js} +5 -5
  4. package/dist/{browser-GURRY444.js → browser-IUJXXNBT.js} +6 -3
  5. package/dist/{cdp-driver-MNPR3HZH.js → cdp-driver-4X3DK6PS.js} +339 -59
  6. package/dist/{cdp-driver-SSXUGXP6.js → cdp-driver-D6WMSMWX.js} +4 -3
  7. package/dist/chunk-2SVQTI2O.js +2794 -0
  8. package/dist/{chunk-IDVD44ED.js → chunk-6WOSXSCQ.js} +23 -7
  9. package/dist/{chunk-ZZ2TFWIV.js → chunk-ABXMBNQ6.js} +1 -1
  10. package/dist/{chunk-2MFXKN32.js → chunk-ACFE6PKF.js} +1013 -119
  11. package/dist/chunk-AMI64BSD.js +268 -0
  12. package/dist/{chunk-E4O5ZU3H.js → chunk-DKWR54XQ.js} +412 -98
  13. package/dist/{chunk-DTJRVA76.js → chunk-ETCO4SNK.js} +2 -2
  14. package/dist/chunk-GDKLH7ZY.js +8 -0
  15. package/dist/chunk-KFQGP6VL.js +33 -0
  16. package/dist/{chunk-2BQZIT3S.js → chunk-LRBSUKUZ.js} +85 -2497
  17. package/dist/{chunk-ITKPSIP7.js → chunk-MDAPTB7C.js} +6 -25
  18. package/dist/{chunk-42RPMJ76.js → chunk-N2JFPWMI.js} +342 -60
  19. package/dist/chunk-OZKD3W4X.js +417 -0
  20. package/dist/{chunk-T4J4C2NZ.js → chunk-TNEN6VQ2.js} +17 -4
  21. package/dist/{chunk-YKOHDEFV.js → chunk-TWWOIJM7.js} +74 -38
  22. package/dist/chunk-WJRE55TN.js +83 -0
  23. package/dist/cli.js +1558 -1122
  24. package/dist/{convert-EGFYNICZ.js → convert-LB3GJTLR.js} +3 -3
  25. package/dist/{convert-EKQVHKB4.js → convert-R3XXYKC6.js} +2 -2
  26. package/dist/{daemon-client-YAVQ343A.js → daemon-client-3JOKX2L2.js} +3 -2
  27. package/dist/{daemon-client-3VM7VU7O.js → daemon-client-DIEHGP5B.js} +28 -74
  28. package/dist/daemon-main.js +2296 -1722
  29. package/dist/{extract-JUOQQX4V.js → extract-2ZFW2MX7.js} +1 -1
  30. package/dist/{extract-L2IW3IUB.js → extract-BSYBM4MR.js} +1 -1
  31. package/dist/{filter-HC4RA7JY.js → filter-KCFO4RSV.js} +1 -1
  32. package/dist/{filter-VID2GGZ7.js → filter-T7DSZ2X7.js} +1 -1
  33. package/dist/{human-interaction-W753RVJB.js → human-interaction-UKAS5ZXV.js} +2 -2
  34. package/dist/index.d.ts +166 -109
  35. package/dist/index.js +2668 -1742
  36. package/dist/launcher-L2JNDB2H.js +20 -0
  37. package/dist/{launcher-KA7J32K5.js → launcher-OZXJQPNG.js} +1 -1
  38. package/dist/{network-store-66A2RATI.js → network-store-XGZ25FFC.js} +1 -1
  39. package/dist/{network-store-BN6QEZ7R.js → network-store-YVDNUREI.js} +1 -1
  40. package/dist/{parse-action-dsl-T3DYC33D.js → parse-action-dsl-UM333TL2.js} +1 -1
  41. package/dist/{proxy-WKGUCH2C.js → proxy-C6CK3UH5.js} +2 -2
  42. package/dist/session-recorder-RTDGURIJ.js +8 -0
  43. package/dist/session-recorder-YI7YYM36.js +7 -0
  44. package/dist/session-replayer-MY27H4DX.js +276 -0
  45. package/dist/site-knowledge-SYC6VCDB.js +23 -0
  46. package/package.json +5 -4
  47. package/dist/screenshot-CWAWMXVA.js +0 -28
  48. package/dist/session-recorder-MA75PKTQ.js +0 -7
@@ -0,0 +1,2794 @@
1
+ import {
2
+ __esm,
3
+ __export,
4
+ __require,
5
+ __toCommonJS
6
+ } from "./chunk-KFQGP6VL.js";
7
+
8
+ // src/recorder/site-knowledge.ts
9
+ var site_knowledge_exports = {};
10
+ __export(site_knowledge_exports, {
11
+ addKnownIssue: () => addKnownIssue,
12
+ getKnowledgeDir: () => getKnowledgeDir,
13
+ getKnowledgePath: () => getKnowledgePath,
14
+ listSiteKnowledge: () => listSiteKnowledge,
15
+ readSiteKnowledge: () => readSiteKnowledge,
16
+ readSiteKnowledgeMarkdown: () => readSiteKnowledgeMarkdown,
17
+ toMarkdown: () => toMarkdown,
18
+ updateSiteKnowledge: () => updateSiteKnowledge
19
+ });
20
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
21
+ import { join } from "path";
22
+ import { homedir } from "os";
23
+ function getKnowledgeDir() {
24
+ return join(homedir(), ".xbrowser", "knowledge");
25
+ }
26
+ function getKnowledgePath(domain, ext) {
27
+ return join(getKnowledgeDir(), `${domain}.${ext}`);
28
+ }
29
+ function extractDomain(url) {
30
+ try {
31
+ const u = new URL(url);
32
+ return u.hostname.replace(/^www\./, "");
33
+ } catch {
34
+ return "unknown";
35
+ }
36
+ }
37
+ function normalizePath(url) {
38
+ try {
39
+ const u = new URL(url);
40
+ return u.pathname;
41
+ } catch {
42
+ return url;
43
+ }
44
+ }
45
+ function extractSelectors(actions, pageUrl) {
46
+ const seen = /* @__PURE__ */ new Map();
47
+ const now = (/* @__PURE__ */ new Date()).toISOString();
48
+ for (const action of actions) {
49
+ if (normalizePath(action.url) !== normalizePath(pageUrl)) continue;
50
+ const el = action.element;
51
+ if (!el || !el.selector) continue;
52
+ const key = el.selector;
53
+ const existing = seen.get(key);
54
+ if (existing) {
55
+ existing.timesSeen++;
56
+ existing.lastSeen = now;
57
+ } else {
58
+ const description = buildDescription(el, action);
59
+ seen.set(key, {
60
+ selector: key,
61
+ tag: el.tag || "unknown",
62
+ description,
63
+ actionType: action.type,
64
+ role: el.role,
65
+ text: el.text?.substring(0, 60),
66
+ confidence: el.confidence || "medium",
67
+ lastSeen: now,
68
+ timesSeen: 1,
69
+ status: "active"
70
+ });
71
+ }
72
+ }
73
+ return Array.from(seen.values()).sort((a, b) => b.timesSeen - a.timesSeen);
74
+ }
75
+ function buildDescription(el, action) {
76
+ const parts = [];
77
+ if (el.text) parts.push(`"${el.text}"`);
78
+ if (el.placeholder) parts.push(`placeholder="${el.placeholder}"`);
79
+ if (el.ariaLabel) parts.push(`aria-label="${el.ariaLabel}"`);
80
+ if (el.role) parts.push(`role=${el.role}`);
81
+ if (el.type) parts.push(`type=${el.type}`);
82
+ const actionDesc = {
83
+ click: "clicked",
84
+ input: `filled with "${action.value?.substring(0, 30)}"`,
85
+ change: "changed",
86
+ submit: "submitted form",
87
+ dblclick: "double-clicked",
88
+ contextmenu: "right-clicked",
89
+ hover: "hovered over",
90
+ focus: "focused"
91
+ };
92
+ const verb = actionDesc[action.type] || action.type;
93
+ const base = parts.length > 0 ? parts.join(", ") : el.tag || "element";
94
+ return `${base} \u2014 ${verb}`;
95
+ }
96
+ function extractForms(actions, pageUrl) {
97
+ const forms = /* @__PURE__ */ new Map();
98
+ for (const action of actions) {
99
+ if (normalizePath(action.url) !== normalizePath(pageUrl)) continue;
100
+ const el = action.element;
101
+ if (!el) continue;
102
+ if (el.tag === "input" || el.tag === "textarea" || el.tag === "select") {
103
+ const formKey = "main";
104
+ const form = forms.get(formKey) || {
105
+ name: "Main Form",
106
+ action: pageUrl,
107
+ fields: []
108
+ };
109
+ if (!form.fields.some((f) => f.selector === el.selector)) {
110
+ form.fields.push({
111
+ selector: el.selector || "",
112
+ tag: el.tag,
113
+ label: el.ariaLabel || el.placeholder || el.text || el.selector || "",
114
+ inputType: el.type || el.tag,
115
+ placeholder: el.placeholder
116
+ });
117
+ }
118
+ forms.set(formKey, form);
119
+ }
120
+ if (action.type === "submit" || action.type === "click" && el.tag === "button" && el.text) {
121
+ const form = forms.get("main");
122
+ if (form && !form.submitSelector) {
123
+ form.submitSelector = el.selector;
124
+ }
125
+ }
126
+ }
127
+ return Array.from(forms.values());
128
+ }
129
+ function extractNavLinks(actions) {
130
+ const links = [];
131
+ const seen = /* @__PURE__ */ new Set();
132
+ for (const action of actions) {
133
+ if (action.type !== "click" && action.type !== "navigation") continue;
134
+ const el = action.element;
135
+ if (!el || el.tag !== "a" || !el.text) continue;
136
+ const href = el.href || action.url;
137
+ if (!href || seen.has(href)) continue;
138
+ seen.add(href);
139
+ links.push({
140
+ text: el.text.substring(0, 40),
141
+ href,
142
+ selector: el.selector || ""
143
+ });
144
+ }
145
+ return links;
146
+ }
147
+ function extractApiEndpoints(network, existingEndpoints) {
148
+ const endpoints = {};
149
+ const now = (/* @__PURE__ */ new Date()).toISOString();
150
+ if (existingEndpoints) {
151
+ for (const [key, ep] of Object.entries(existingEndpoints)) {
152
+ endpoints[key] = ep;
153
+ }
154
+ }
155
+ for (const entry of network) {
156
+ if (!entry.url.includes("/api/") && !entry.url.includes("/v1/") && !entry.url.includes("/v2/") && entry.contentType && !entry.contentType.includes("json") && !entry.contentType.includes("text/")) continue;
157
+ if (["image", "stylesheet", "font", "manifest"].includes(entry.resourceType)) continue;
158
+ let path;
159
+ try {
160
+ path = new URL(entry.url).pathname;
161
+ } catch {
162
+ continue;
163
+ }
164
+ const method = entry.method;
165
+ const key = `${method} ${path}`;
166
+ if (endpoints[key]) {
167
+ endpoints[key].timesSeen++;
168
+ endpoints[key].lastSeen = now;
169
+ } else {
170
+ let params = [];
171
+ if (entry.requestBody && typeof entry.requestBody === "object") {
172
+ params = Object.keys(entry.requestBody).slice(0, 10);
173
+ }
174
+ let responseFields = [];
175
+ if (entry.responseBody && typeof entry.responseBody === "object") {
176
+ const resp = entry.responseBody;
177
+ responseFields = Object.keys(resp).slice(0, 10);
178
+ if (resp.data && typeof resp.data === "object") {
179
+ const dataKeys = Object.keys(resp.data).slice(0, 10);
180
+ responseFields = [...responseFields, ...dataKeys.map((k) => `data.${k}`)];
181
+ }
182
+ }
183
+ endpoints[key] = {
184
+ method,
185
+ url: entry.url.substring(0, 200),
186
+ path,
187
+ params,
188
+ responseFields,
189
+ lastSeen: now,
190
+ timesSeen: 1
191
+ };
192
+ }
193
+ }
194
+ return endpoints;
195
+ }
196
+ function mergePages(existing, newPages) {
197
+ const merged = { ...existing };
198
+ const now = (/* @__PURE__ */ new Date()).toISOString();
199
+ for (const [path, newPage] of Object.entries(newPages)) {
200
+ if (merged[path]) {
201
+ const old = merged[path];
202
+ const selectorMap = /* @__PURE__ */ new Map();
203
+ for (const sel of old.selectors) selectorMap.set(sel.selector, sel);
204
+ for (const sel of newPage.selectors) {
205
+ const existing2 = selectorMap.get(sel.selector);
206
+ if (existing2) {
207
+ existing2.timesSeen += sel.timesSeen;
208
+ existing2.lastSeen = now;
209
+ existing2.status = "active";
210
+ } else {
211
+ selectorMap.set(sel.selector, sel);
212
+ }
213
+ }
214
+ const newSelectorSet = new Set(newPage.selectors.map((s) => s.selector));
215
+ for (const sel of selectorMap.values()) {
216
+ if (!newSelectorSet.has(sel.selector) && sel.timesSeen > 0) {
217
+ }
218
+ }
219
+ merged[path] = {
220
+ ...newPage,
221
+ selectors: Array.from(selectorMap.values()).sort((a, b) => b.timesSeen - a.timesSeen),
222
+ forms: newPage.forms.length > 0 ? newPage.forms : old.forms,
223
+ navigationLinks: [.../* @__PURE__ */ new Set([...old.navigationLinks, ...newPage.navigationLinks])].slice(0, 50),
224
+ lastVisited: now
225
+ };
226
+ } else {
227
+ merged[path] = newPage;
228
+ }
229
+ }
230
+ return merged;
231
+ }
232
+ function updateSiteKnowledge(data) {
233
+ const domain = extractDomain(data.startUrl);
234
+ const jsonPath = getKnowledgePath(domain, "json");
235
+ let existing = null;
236
+ if (existsSync(jsonPath)) {
237
+ try {
238
+ existing = JSON.parse(readFileSync(jsonPath, "utf-8"));
239
+ } catch {
240
+ existing = null;
241
+ }
242
+ }
243
+ const pageMap = /* @__PURE__ */ new Map();
244
+ for (const action of data.actions) {
245
+ const path = normalizePath(action.url);
246
+ if (!pageMap.has(path)) pageMap.set(path, []);
247
+ pageMap.get(path).push(action);
248
+ }
249
+ const newPages = {};
250
+ for (const [path, actions] of pageMap) {
251
+ const fullUrl = actions[0]?.url || data.startUrl;
252
+ newPages[path] = {
253
+ url: fullUrl,
254
+ title: actions[0]?.pageTitle || "",
255
+ selectors: extractSelectors(data.actions, fullUrl),
256
+ forms: extractForms(data.actions, fullUrl),
257
+ navigationLinks: extractNavLinks(actions),
258
+ lastVisited: (/* @__PURE__ */ new Date()).toISOString()
259
+ };
260
+ }
261
+ const apiEndpoints = extractApiEndpoints(data.network, existing?.apiEndpoints);
262
+ const pages = existing ? mergePages(existing.pages, newPages) : newPages;
263
+ const knowledge = {
264
+ domain,
265
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
266
+ recordingCount: (existing?.recordingCount || 0) + 1,
267
+ pages,
268
+ apiEndpoints,
269
+ knownIssues: existing?.knownIssues || [],
270
+ generatedBy: "xbrowser-recorder"
271
+ };
272
+ mkdirSync(getKnowledgeDir(), { recursive: true });
273
+ writeFileSync(jsonPath, JSON.stringify(knowledge, null, 2), "utf-8");
274
+ writeFileSync(getKnowledgePath(domain, "md"), toMarkdown(knowledge), "utf-8");
275
+ return knowledge;
276
+ }
277
+ function toMarkdown(kb) {
278
+ const lines = [];
279
+ lines.push(`# Site Knowledge: ${kb.domain}`);
280
+ lines.push("");
281
+ lines.push("> **Auto-generated by xbrowser recorder. This document is for LLM consumption.**");
282
+ lines.push("> Use these selectors when writing automation scripts for this site.");
283
+ lines.push(`> Updated: ${kb.lastUpdated} | Recordings: ${kb.recordingCount}`);
284
+ lines.push("");
285
+ lines.push("## Pages");
286
+ lines.push("");
287
+ for (const page of Object.values(kb.pages)) {
288
+ lines.push(`### ${page.url}`);
289
+ lines.push(`- **Path**: ${normalizePath(page.url)}`);
290
+ if (page.title) lines.push(`- **Title**: ${page.title}`);
291
+ lines.push(`- **Last Visited**: ${page.lastVisited}`);
292
+ lines.push("");
293
+ if (page.selectors.length > 0) {
294
+ lines.push("#### Selectors");
295
+ lines.push("");
296
+ lines.push("| Selector | Tag | Action | Description | Confidence | Seen |");
297
+ lines.push("|----------|-----|--------|-------------|------------|------|");
298
+ for (const sel of page.selectors) {
299
+ const status = sel.status === "deprecated" ? " \u26A0\uFE0FDEPRECATED" : "";
300
+ lines.push(
301
+ `| \`${sel.selector}\` | ${sel.tag} | ${sel.actionType} | ${sel.description} | ${sel.confidence} | ${sel.timesSeen}x${status} |`
302
+ );
303
+ }
304
+ lines.push("");
305
+ }
306
+ if (page.forms.length > 0) {
307
+ lines.push("#### Forms");
308
+ lines.push("");
309
+ for (const form of page.forms) {
310
+ lines.push(`- **${form.name}** (${form.action})`);
311
+ for (const field of form.fields) {
312
+ const parts = [field.tag, field.inputType];
313
+ if (field.placeholder) parts.push(`placeholder="${field.placeholder}"`);
314
+ lines.push(` - \`${field.selector}\` \u2192 ${field.label} (${parts.join(", ")})`);
315
+ }
316
+ if (form.submitSelector) {
317
+ lines.push(` - Submit: \`${form.submitSelector}\``);
318
+ }
319
+ }
320
+ lines.push("");
321
+ }
322
+ if (page.navigationLinks.length > 0) {
323
+ lines.push("#### Navigation Links");
324
+ lines.push("");
325
+ for (const link of page.navigationLinks.slice(0, 20)) {
326
+ lines.push(`- [${link.text}](${link.href}) \u2192 \`${link.selector}\``);
327
+ }
328
+ lines.push("");
329
+ }
330
+ }
331
+ const endpoints = Object.values(kb.apiEndpoints);
332
+ if (endpoints.length > 0) {
333
+ lines.push("## API Endpoints");
334
+ lines.push("");
335
+ lines.push("| Method | Path | Params | Response Fields | Frequency |");
336
+ lines.push("|--------|------|--------|-----------------|-----------|");
337
+ for (const ep of endpoints.sort((a, b) => b.timesSeen - a.timesSeen)) {
338
+ const params = ep.params.length > 0 ? ep.params.join(", ") : "-";
339
+ const respFields = ep.responseFields.length > 0 ? ep.responseFields.slice(0, 5).join(", ") : "-";
340
+ lines.push(`| ${ep.method} | ${ep.path} | ${params} | ${respFields} | ${ep.timesSeen}x |`);
341
+ }
342
+ lines.push("");
343
+ }
344
+ if (kb.knownIssues.length > 0) {
345
+ lines.push("## Known Issues");
346
+ lines.push("");
347
+ for (const issue of kb.knownIssues) {
348
+ lines.push(`- ${issue}`);
349
+ }
350
+ lines.push("");
351
+ }
352
+ lines.push("---");
353
+ lines.push("");
354
+ lines.push("## How to Use This Document");
355
+ lines.push("");
356
+ lines.push("When writing automation scripts for this site:");
357
+ lines.push("1. Use the selectors from the **Selectors** tables above");
358
+ lines.push("2. Prefer selectors with **high confidence** and **higher Seen count**");
359
+ lines.push("3. For form filling, follow the **Forms** structure");
360
+ lines.push("4. For API interactions, reference the **API Endpoints** table");
361
+ lines.push("5. If a selector fails, it may be **deprecated** \u2014 check for alternative selectors");
362
+ lines.push("");
363
+ return lines.join("\n");
364
+ }
365
+ function readSiteKnowledge(domain) {
366
+ const path = getKnowledgePath(domain, "json");
367
+ if (!existsSync(path)) return null;
368
+ try {
369
+ return JSON.parse(readFileSync(path, "utf-8"));
370
+ } catch {
371
+ return null;
372
+ }
373
+ }
374
+ function readSiteKnowledgeMarkdown(domain) {
375
+ const path = getKnowledgePath(domain, "md");
376
+ if (!existsSync(path)) return null;
377
+ try {
378
+ return readFileSync(path, "utf-8");
379
+ } catch {
380
+ return null;
381
+ }
382
+ }
383
+ function listSiteKnowledge() {
384
+ const dir = getKnowledgeDir();
385
+ if (!existsSync(dir)) return [];
386
+ try {
387
+ const files = __require("fs").readdirSync(dir);
388
+ return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(".json", ""));
389
+ } catch {
390
+ return [];
391
+ }
392
+ }
393
+ function addKnownIssue(domain, issue) {
394
+ const kb = readSiteKnowledge(domain);
395
+ if (!kb) return;
396
+ const dated = `[${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}] ${issue}`;
397
+ kb.knownIssues.push(dated);
398
+ kb.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
399
+ writeFileSync(getKnowledgePath(domain, "json"), JSON.stringify(kb, null, 2), "utf-8");
400
+ writeFileSync(getKnowledgePath(domain, "md"), toMarkdown(kb), "utf-8");
401
+ }
402
+ var init_site_knowledge = __esm({
403
+ "src/recorder/site-knowledge.ts"() {
404
+ "use strict";
405
+ }
406
+ });
407
+
408
+ // src/recorder/session-recorder.ts
409
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync2, rmSync } from "fs";
410
+ import { join as join2 } from "path";
411
+ import { homedir as homedir2 } from "os";
412
+
413
+ // src/recorder/selector-utils.ts
414
+ function getSelectorGeneratorScript() {
415
+ return `
416
+ (function() {
417
+ if (window.__xb_generateSelector) return;
418
+
419
+ var IGNORE_CLASS = [
420
+ /^css-[a-zA-Z0-9]+$/,
421
+ /^sc-[a-zA-Z0-9]+$/,
422
+ /^emotion-\\d+$/,
423
+ /^_[a-f0-9]{4,}$/,
424
+ /^[a-f0-9]{6,}$/,
425
+ /^styled-[a-zA-Z0-9]+/,
426
+ /^__[a-zA-Z0-9]{4,}$/,
427
+ /^makeStyles-/,
428
+ /^MuiPrivate/,
429
+ /^jss\\d+$/,
430
+ /^ant-[a-z]+$/,
431
+ /^semi-[a-z]{4,}/
432
+ ];
433
+ var STABLE_ATTRS = ['data-testid', 'data-test-id', 'data-cy', 'data-qa', 'data-el'];
434
+ var MAX_DEPTH = 5;
435
+
436
+ function isStableClass(cls) {
437
+ if (!cls || cls.length <= 1) return false;
438
+ for (var i = 0; i < IGNORE_CLASS.length; i++) {
439
+ if (IGNORE_CLASS[i].test(cls)) return false;
440
+ }
441
+ return true;
442
+ }
443
+
444
+ function isUnique(root, sel) {
445
+ try { return root.querySelectorAll(sel).length === 1; } catch(e) { return false; }
446
+ }
447
+
448
+ function esc(s) { return CSS.escape(s); }
449
+
450
+ window.__xb_generateSelector = function(el, root) {
451
+ root = root || document;
452
+ if (!el || !el.tagName) return null;
453
+ if (el === root || el === document.documentElement) return { selector: 'html', strategy: 'root', confidence: 'high' };
454
+
455
+ var tag = el.tagName.toLowerCase();
456
+
457
+ // #id
458
+ if (el.id) {
459
+ var s = '#' + esc(el.id);
460
+ if (isUnique(root, s)) return { selector: s, strategy: 'id', confidence: 'high' };
461
+ s = tag + s;
462
+ if (isUnique(root, s)) return { selector: s, strategy: 'id+tag', confidence: 'high' };
463
+ }
464
+
465
+ // [data-testid]
466
+ for (var si = 0; si < STABLE_ATTRS.length; si++) {
467
+ var v = el.getAttribute(STABLE_ATTRS[si]);
468
+ if (v) {
469
+ var s = '[' + STABLE_ATTRS[si] + '="' + esc(v) + '"]';
470
+ if (isUnique(root, s)) return { selector: s, strategy: 'testid', confidence: 'high' };
471
+ s = tag + s;
472
+ if (isUnique(root, s)) return { selector: s, strategy: 'testid+tag', confidence: 'high' };
473
+ }
474
+ }
475
+
476
+ // [name]
477
+ var name = el.getAttribute('name');
478
+ if (name) {
479
+ var s = tag + '[name="' + esc(name) + '"]';
480
+ if (isUnique(root, s)) return { selector: s, strategy: 'name', confidence: 'high' };
481
+ }
482
+
483
+ // [aria-label]
484
+ var aria = el.getAttribute('aria-label');
485
+ if (aria) {
486
+ var s = '[aria-label="' + esc(aria.substring(0, 60)) + '"]';
487
+ if (isUnique(root, s)) return { selector: s, strategy: 'aria-label', confidence: 'high' };
488
+ s = tag + s;
489
+ if (isUnique(root, s)) return { selector: s, strategy: 'aria-label+tag', confidence: 'high' };
490
+ }
491
+
492
+ // [placeholder]
493
+ var ph = el.getAttribute('placeholder');
494
+ if (ph) {
495
+ var s = tag + '[placeholder="' + esc(ph.substring(0, 60)) + '"]';
496
+ if (isUnique(root, s)) return { selector: s, strategy: 'placeholder', confidence: 'high' };
497
+ }
498
+
499
+ // [alt]
500
+ var alt = el.getAttribute('alt');
501
+ if (alt && tag === 'img') {
502
+ var s = 'img[alt="' + esc(alt.substring(0, 60)) + '"]';
503
+ if (isUnique(root, s)) return { selector: s, strategy: 'alt', confidence: 'high' };
504
+ }
505
+
506
+ // [title]
507
+ var title = el.getAttribute('title');
508
+ if (title) {
509
+ var s = tag + '[title="' + esc(title.substring(0, 60)) + '"]';
510
+ if (isUnique(root, s)) return { selector: s, strategy: 'title', confidence: 'high' };
511
+ }
512
+
513
+ // Unique attribute (skip URL-like, long)
514
+ 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};
515
+ for (var ai = 0; ai < el.attributes.length; ai++) {
516
+ var a = el.attributes[ai];
517
+ if (skipAttr[a.name] || a.name.startsWith('data-') || a.name.startsWith('aria-')) continue;
518
+ if (a.value && a.value.length > 2 && a.value.length <= 60) {
519
+ var s = tag + '[' + a.name + '="' + esc(a.value) + '"]';
520
+ if (isUnique(root, s)) return { selector: s, strategy: 'attribute', confidence: 'medium' };
521
+ }
522
+ }
523
+
524
+ // Class combos
525
+ var rawCls = (typeof el.className === 'string' ? el.className : '').trim().split(/\\s+/);
526
+ var cls = rawCls.filter(isStableClass);
527
+ if (cls.length > 0) {
528
+ cls.sort(function(a, b) {
529
+ return root.querySelectorAll('.' + esc(a)).length - root.querySelectorAll('.' + esc(b)).length;
530
+ });
531
+ // single class
532
+ for (var i = 0; i < cls.length; i++) {
533
+ var s = '.' + esc(cls[i]);
534
+ if (isUnique(root, s)) return { selector: s, strategy: 'class', confidence: 'medium' };
535
+ }
536
+ // tag + class
537
+ for (var i = 0; i < cls.length; i++) {
538
+ var s = tag + '.' + esc(cls[i]);
539
+ if (isUnique(root, s)) return { selector: s, strategy: 'tag+class', confidence: 'medium' };
540
+ }
541
+ // 2-class combo
542
+ for (var i = 0; i < cls.length && i < 5; i++) {
543
+ for (var j = i+1; j < cls.length && j < 5; j++) {
544
+ var s = '.' + esc(cls[i]) + '.' + esc(cls[j]);
545
+ if (isUnique(root, s)) return { selector: s, strategy: 'class-combo', confidence: 'medium' };
546
+ }
547
+ }
548
+ // tag + 2-class combo
549
+ for (var i = 0; i < cls.length && i < 4; i++) {
550
+ for (var j = i+1; j < cls.length && j < 4; j++) {
551
+ var s = tag + '.' + esc(cls[i]) + '.' + esc(cls[j]);
552
+ if (isUnique(root, s)) return { selector: s, strategy: 'tag+class-combo', confidence: 'medium' };
553
+ }
554
+ }
555
+ }
556
+
557
+ // Parent scope
558
+ var parent = el.parentElement;
559
+ if (parent && parent !== root) {
560
+ if (parent.id) {
561
+ var s = '#' + esc(parent.id) + ' > ' + tag;
562
+ if (isUnique(root, s)) return { selector: s, strategy: 'parent-scope', confidence: 'medium' };
563
+ }
564
+ var pCls = (typeof parent.className === 'string' ? parent.className : '').trim().split(/\\s+/).filter(isStableClass);
565
+ for (var i = 0; i < pCls.length; i++) {
566
+ var s = '.' + esc(pCls[i]) + ' > ' + tag;
567
+ if (isUnique(root, s)) return { selector: s, strategy: 'parent-class-scope', confidence: 'medium' };
568
+ }
569
+ if (cls.length > 0) {
570
+ for (var i = 0; i < Math.min(cls.length, 3); i++) {
571
+ var s = parent.tagName.toLowerCase() + ' > ' + tag + '.' + esc(cls[i]);
572
+ if (isUnique(root, s)) return { selector: s, strategy: 'parent>tag.class', confidence: 'medium' };
573
+ }
574
+ }
575
+ }
576
+
577
+ // :nth-of-type chain \u2014 walk all the way up, then find shortest unique suffix
578
+ var path = [];
579
+ var cur = el;
580
+ while (cur && cur !== root && cur !== document.documentElement) {
581
+ var p = cur.parentElement;
582
+ if (!p || p === root) break;
583
+ var t = cur.tagName.toLowerCase();
584
+ var same = [];
585
+ for (var k = 0; k < p.children.length; k++) {
586
+ if (p.children[k].tagName === cur.tagName) same.push(p.children[k]);
587
+ }
588
+ var nth = same.length === 1 ? null : (same.indexOf(cur) + 1);
589
+ path.unshift({tag: t, nth: nth});
590
+ if (cur !== el && cur.id) {
591
+ path.unshift({tag: '#' + esc(cur.id), nth: null});
592
+ break;
593
+ }
594
+ cur = p;
595
+ }
596
+ // Try suffixes from shortest to longest
597
+ for (var d = 1; d <= path.length; d++) {
598
+ var parts = path.slice(-d).map(function(p) {
599
+ return p.nth !== null ? p.tag + ':nth-of-type(' + p.nth + ')' : p.tag;
600
+ });
601
+ var sel = parts.join(' > ');
602
+ if (isUnique(root, sel)) return { selector: sel, strategy: 'nth-of-type', confidence: 'low' };
603
+ }
604
+ // Fallback
605
+ if (path.length > 0) {
606
+ var parts = path.map(function(p) {
607
+ return p.nth !== null ? p.tag + ':nth-of-type(' + p.nth + ')' : p.tag;
608
+ });
609
+ return { selector: parts.join(' > '), strategy: 'nth-of-type', confidence: 'low' };
610
+ }
611
+ return { selector: tag, strategy: 'tag-only', confidence: 'low' };
612
+ };
613
+ })();
614
+ `;
615
+ }
616
+
617
+ // src/recorder/session-recorder.ts
618
+ init_site_knowledge();
619
+ var ACTION_SIGNAL_SCRIPT = `
620
+ (function() {
621
+ if (window.__xb_action_signal) return;
622
+ window.__xb_action_signal = true;
623
+ window.__xb_pending_actions = [];
624
+
625
+ // --- Unique short selector generator (delegates to 13-strategy selector-utils) ---
626
+ function uniqueSelector(el) {
627
+ if (!el || !el.tagName) return null;
628
+ var doc = el.ownerDocument || document;
629
+
630
+ function isUnique(sel) {
631
+ try { return doc.querySelectorAll(sel).length === 1; } catch(e) { return false; }
632
+ }
633
+
634
+ // Prefer window.__xb_generateSelector from selector-utils (13 strategies)
635
+ if (typeof window.__xb_generateSelector === 'function') {
636
+ try {
637
+ var result = window.__xb_generateSelector(el, doc);
638
+ if (result && result.selector) return result.selector;
639
+ } catch(e) { /* fallback to local logic */ }
640
+ }
641
+
642
+ // 1. #id (shortest, globally unique)
643
+ if (el.id) {
644
+ var idSel = '#' + CSS.escape(el.id);
645
+ if (isUnique(idSel)) return idSel;
646
+ }
647
+
648
+ // 2. [data-testid="..."]
649
+ var testId = el.getAttribute('data-testid') || el.getAttribute('data-test-id');
650
+ if (testId) {
651
+ var sel = '[data-testid="' + testId + '"]';
652
+ if (isUnique(sel)) return sel;
653
+ }
654
+
655
+ // 3. [name="..."]
656
+ var name = el.getAttribute('name');
657
+ if (name) {
658
+ var sel = el.tagName.toLowerCase() + '[name="' + name + '"]';
659
+ if (isUnique(sel)) return sel;
660
+ }
661
+
662
+ // 4. [aria-label="..."]
663
+ var aria = el.getAttribute('aria-label');
664
+ if (aria) {
665
+ var sel = '[aria-label="' + aria.substring(0, 50) + '"]';
666
+ if (isUnique(sel)) return sel;
667
+ }
668
+
669
+ // 5. [placeholder="..."]
670
+ var ph = el.getAttribute('placeholder');
671
+ if (ph) {
672
+ var sel = el.tagName.toLowerCase() + '[placeholder="' + ph.substring(0, 50) + '"]';
673
+ if (isUnique(sel)) return sel;
674
+ }
675
+
676
+ // 6. tag.class \u2014 pick shortest combo that's unique
677
+ var tag = el.tagName.toLowerCase();
678
+ if (typeof el.className === 'string' && el.className.trim()) {
679
+ var classes = el.className.trim().split(/\\s+/).filter(function(c) {
680
+ return c && !/^(ng-|_|css-|sc-|styled-|emotion-)/.test(c);
681
+ });
682
+ // Sort by rarity (less common class first)
683
+ classes.sort(function(a, b) {
684
+ return doc.querySelectorAll('.' + a).length - doc.querySelectorAll('.' + b).length;
685
+ });
686
+ // Try tag + single class
687
+ for (var i = 0; i < classes.length; i++) {
688
+ var sel = tag + '.' + CSS.escape(classes[i]);
689
+ if (isUnique(sel)) return sel;
690
+ }
691
+ // Try tag + two classes
692
+ if (classes.length >= 2) {
693
+ var sel = tag + '.' + CSS.escape(classes[0]) + '.' + CSS.escape(classes[1]);
694
+ if (isUnique(sel)) return sel;
695
+ }
696
+ }
697
+
698
+ // 7. parent > tag (one level up)
699
+ var parent = el.parentElement;
700
+ if (parent) {
701
+ var parentSel = parent.id ? '#' + CSS.escape(parent.id) : parent.tagName.toLowerCase();
702
+ var sel = parentSel + ' > ' + tag;
703
+ if (isUnique(sel)) return sel;
704
+ }
705
+
706
+ // 8. :nth-child fallback (tag:nth-child(n) under parent)
707
+ if (parent) {
708
+ var siblings = Array.from(parent.children);
709
+ var idx = siblings.indexOf(el) + 1;
710
+ var parentSel = parent.id ? '#' + CSS.escape(parent.id) : parent.tagName.toLowerCase();
711
+ var sel = parentSel + ' > ' + tag + ':nth-child(' + idx + ')';
712
+ if (isUnique(sel)) return sel;
713
+ }
714
+
715
+ // 9. Last resort: full tag
716
+ return tag;
717
+ }
718
+
719
+ // --- Element descriptor ---
720
+ function describe(el) {
721
+ if (!el || !el.tagName) return null;
722
+ var tag = el.tagName.toLowerCase();
723
+ var isInputLike = (tag === 'input' || tag === 'textarea' || tag === 'select');
724
+ var displayText = isInputLike
725
+ ? (el.value || el.getAttribute('placeholder') || '').trim().substring(0, 40)
726
+ : (el.textContent || '').trim().substring(0, 40);
727
+ if (tag === 'a' && el.getAttribute('href')) displayText = el.textContent.trim().substring(0, 40);
728
+
729
+ // Prefer window.__xb_generateSelector (13 strategies) \u2014 also extracts strategy + confidence
730
+ var selector, strategy, confidence;
731
+ if (typeof window.__xb_generateSelector === 'function') {
732
+ try {
733
+ var result = window.__xb_generateSelector(el, el.ownerDocument || document);
734
+ if (result && result.selector) {
735
+ selector = result.selector;
736
+ strategy = result.strategy;
737
+ confidence = result.confidence;
738
+ }
739
+ } catch(e) { /* fall through to local */ }
740
+ }
741
+ if (!selector) {
742
+ selector = uniqueSelector(el);
743
+ }
744
+
745
+ // For low-confidence selectors (nth-of-type), generate a text-based fallback
746
+ // when the element has short, unique text (e.g. menu items "\u5220\u9664", "\u786E\u8BA4")
747
+ var textFallback;
748
+ var popupContext;
749
+
750
+ // Check if element is inside a popup/menu first (needed for scoped text uniqueness)
751
+ var popupEl;
752
+ try {
753
+ 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"]');
754
+ } catch(e) {}
755
+
756
+ if (confidence === 'low') {
757
+ var rawText = (el.textContent || '').trim();
758
+ if (rawText && rawText.length >= 1 && rawText.length <= 30 && el.children.length === 0) {
759
+ try {
760
+ var doc = el.ownerDocument || document;
761
+ var escapedText = rawText.replace(/'/g, "\\'");
762
+
763
+ // If inside popup, check uniqueness WITHIN popup only
764
+ var count;
765
+ if (popupEl && popupEl !== el) {
766
+ var popupCount = doc.evaluate(
767
+ "count(.//*[normalize-space(text())='" + escapedText + "'])",
768
+ popupEl, null, XPathResult.NUMBER_TYPE, null
769
+ );
770
+ count = popupCount.numberValue;
771
+ } else {
772
+ // Check global uniqueness
773
+ var globalCount = doc.evaluate(
774
+ "count(//*[normalize-space(text())='" + escapedText + "'])",
775
+ doc, null, XPathResult.NUMBER_TYPE, null
776
+ );
777
+ count = globalCount.numberValue;
778
+ }
779
+
780
+ if (count === 1) {
781
+ textFallback = {
782
+ type: popupEl && popupEl !== el ? 'popup-text' : 'text',
783
+ value: rawText,
784
+ selector: popupEl && popupEl !== el ? 'popup-text=' + rawText : 'text=' + rawText,
785
+ };
786
+ }
787
+ } catch(e) { /* xpath not available or error */ }
788
+ }
789
+ }
790
+
791
+ // Generate popup context info
792
+ if (popupEl && popupEl !== el) {
793
+ try {
794
+ var popupResult = window.__xb_generateSelector
795
+ ? window.__xb_generateSelector(popupEl, el.ownerDocument || document)
796
+ : null;
797
+ if (popupResult && popupResult.selector) {
798
+ popupContext = {
799
+ containerSelector: popupResult.selector,
800
+ containerText: (popupEl.textContent || '').trim().substring(0, 50),
801
+ };
802
+ }
803
+ } catch(e) { /* skip */ }
804
+ }
805
+
806
+ return {
807
+ tag: tag,
808
+ selector: selector,
809
+ text: displayText,
810
+ strategy: strategy,
811
+ confidence: confidence,
812
+ textFallback: textFallback,
813
+ popup: popupContext,
814
+ role: el.getAttribute('role') || undefined,
815
+ type: el.getAttribute('type') || undefined,
816
+ placeholder: el.getAttribute('placeholder') || undefined,
817
+ ariaLabel: el.getAttribute('aria-label') || undefined,
818
+ href: el.getAttribute('href') ? el.getAttribute('href').substring(0, 80) : undefined,
819
+ };
820
+ }
821
+
822
+ // \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
823
+ // Continuously samples mousemove (every ~60ms).
824
+ // When a meaningful action fires, we snapshot the buffer,
825
+ // simplify it (Douglas-Peucker), and attach as trajectory.
826
+ var __xb_traj_buffer = []; // raw samples: {x, y, t}
827
+ var __xb_traj_last_action = null; // {x, y, t} of previous action
828
+
829
+ document.addEventListener('mousemove', function(e) {
830
+ var now = Date.now();
831
+ // Sample at ~60ms intervals
832
+ if (__xb_traj_buffer.length > 0) {
833
+ var last = __xb_traj_buffer[__xb_traj_buffer.length - 1];
834
+ if (now - last.t < 60) return;
835
+ }
836
+ __xb_traj_buffer.push({ x: e.clientX, y: e.clientY, t: now });
837
+ // Cap buffer at 200 points (~12 seconds at 60ms)
838
+ if (__xb_traj_buffer.length > 200) {
839
+ __xb_traj_buffer = __xb_traj_buffer.slice(-150);
840
+ }
841
+ }, true);
842
+
843
+ // Douglas-Peucker simplification: keep only points that define the path shape
844
+ function dpSimplify(pts, epsilon) {
845
+ if (pts.length <= 2) return pts;
846
+ var maxDist = 0, maxIdx = 0;
847
+ var first = pts[0], last = pts[pts.length - 1];
848
+ for (var i = 1; i < pts.length - 1; i++) {
849
+ var d = pointLineDistance(pts[i], first, last);
850
+ if (d > maxDist) { maxDist = d; maxIdx = i; }
851
+ }
852
+ if (maxDist > epsilon) {
853
+ var left = dpSimplify(pts.slice(0, maxIdx + 1), epsilon);
854
+ var right = dpSimplify(pts.slice(maxIdx), epsilon);
855
+ return left.slice(0, -1).concat(right);
856
+ }
857
+ return [first, last];
858
+ }
859
+
860
+ function pointLineDistance(p, a, b) {
861
+ var dx = b.x - a.x, dy = b.y - a.y;
862
+ var lenSq = dx * dx + dy * dy;
863
+ if (lenSq === 0) return Math.sqrt((p.x - a.x) * (p.x - a.x) + (p.y - a.y) * (p.y - a.y));
864
+ var t = Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / lenSq));
865
+ var projX = a.x + t * dx, projY = a.y + t * dy;
866
+ return Math.sqrt((p.x - projX) * (p.x - projX) + (p.y - projY) * (p.y - projY));
867
+ }
868
+
869
+ // Extract trajectory from buffer ending at (toX, toY, toT).
870
+ // Returns null if no meaningful path.
871
+ function extractTrajectory(toX, toY) {
872
+ var now = Date.now();
873
+ // Add current position as final point
874
+ var raw = __xb_traj_buffer.slice();
875
+ raw.push({ x: toX, y: toY, t: now });
876
+
877
+ // Trim to last 5 seconds
878
+ var cutoff = now - 5000;
879
+ while (raw.length > 0 && raw[0].t < cutoff) raw.shift();
880
+ if (raw.length < 2) { __xb_traj_buffer = []; return null; }
881
+
882
+ // If we know the previous action position, prepend it as start
883
+ if (__xb_traj_last_action) {
884
+ // Trim raw points before the last action
885
+ while (raw.length > 1 && raw[0].t < __xb_traj_last_action.t) raw.shift();
886
+ raw.unshift({ x: __xb_traj_last_action.x, y: __xb_traj_last_action.y, t: __xb_traj_last_action.t });
887
+ }
888
+
889
+ // Simplify (epsilon=3px keeps shape but removes jitter)
890
+ var simplified = dpSimplify(raw, 3);
891
+ if (simplified.length < 2) { __xb_traj_buffer = []; return null; }
892
+
893
+ // Build result with delta times
894
+ var totalDist = 0;
895
+ var points = [];
896
+ for (var i = 0; i < simplified.length; i++) {
897
+ var dt = i === 0 ? 0 : simplified[i].t - simplified[i - 1].t;
898
+ if (i > 0) {
899
+ var ddx = simplified[i].x - simplified[i - 1].x;
900
+ var ddy = simplified[i].y - simplified[i - 1].y;
901
+ totalDist += Math.sqrt(ddx * ddx + ddy * ddy);
902
+ }
903
+ points.push({ x: simplified[i].x, y: simplified[i].y, dt: dt });
904
+ }
905
+
906
+ var duration = simplified[simplified.length - 1].t - simplified[0].t;
907
+
908
+ // Only return if meaningful movement (>5px total distance)
909
+ if (totalDist < 5) { __xb_traj_buffer = []; return null; }
910
+
911
+ // Reset buffer
912
+ __xb_traj_buffer = [];
913
+ __xb_traj_last_action = { x: toX, y: toY, t: now };
914
+
915
+ return { points: points, distance: Math.round(totalDist), duration: duration };
916
+ }
917
+
918
+ // \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
919
+
920
+ // Expose describe() for CDP command element metadata extraction
921
+ window.__xb_describe = describe;
922
+
923
+ function isMeaningful(el) {
924
+ if (!el || !el.tagName) return false;
925
+ var tag = el.tagName.toLowerCase();
926
+ if (tag === 'a' || tag === 'button' || tag === 'input' || tag === 'textarea' || tag === 'select') return true;
927
+ if (el.getAttribute('role')) return true;
928
+ if (el.getAttribute('aria-label')) return true;
929
+ var text = (el.textContent || '').trim();
930
+ if (text.length > 0 && text.length <= 80) return true;
931
+ return false;
932
+ }
933
+
934
+ function resolveMeaningful(e) {
935
+ var path = e.composedPath ? e.composedPath() : [e.target];
936
+ for (var i = 0; i < Math.min(path.length, 8); i++) {
937
+ var el = path[i];
938
+ if (isMeaningful(el)) return el;
939
+ }
940
+ return path[0] || e.target;
941
+ }
942
+
943
+ function actualTarget(e) {
944
+ var path = e.composedPath && e.composedPath();
945
+ return (path && path.length > 0) ? path[0] : e.target;
946
+ }
947
+
948
+ // --- Input debounce: coalesce rapid keystrokes on same element ---
949
+ var __xb_input_timer = null;
950
+ var __xb_input_pending = null;
951
+
952
+ function flushInputAction() {
953
+ if (__xb_input_pending) {
954
+ window.__xb_pending_actions.push(__xb_input_pending);
955
+ __xb_input_pending = null;
956
+ }
957
+ __xb_input_timer = null;
958
+ }
959
+
960
+ function pushAction(type, detail) {
961
+ // Attach mouse trajectory for actions with coordinates
962
+ if (detail && detail.x != null && detail.y != null) {
963
+ var traj = extractTrajectory(detail.x, detail.y);
964
+ if (traj) detail.trajectory = traj;
965
+ }
966
+
967
+ if (type === 'input') {
968
+ if (__xb_input_timer) clearTimeout(__xb_input_timer);
969
+ __xb_input_pending = {
970
+ type: type,
971
+ ts: Date.now(),
972
+ url: location.href,
973
+ title: document.title,
974
+ ...detail,
975
+ };
976
+ __xb_input_timer = setTimeout(flushInputAction, 800);
977
+ return;
978
+ }
979
+ if (type === 'click' || type === 'submit' || type === 'keydown') {
980
+ if (__xb_input_timer) { clearTimeout(__xb_input_timer); flushInputAction(); }
981
+ }
982
+ window.__xb_pending_actions.push({
983
+ type: type,
984
+ ts: Date.now(),
985
+ url: location.href,
986
+ title: document.title,
987
+ ...detail,
988
+ });
989
+ }
990
+
991
+ // --- Click context: capture popover/dropdown/menu/state changes after click ---
992
+ var POPOVER_SELECTORS = [
993
+ '[role="menu"]','[role="listbox"]','[role="dialog"]','[role="tooltip"]','[role="popover"]',
994
+ '[role="combobox"]','[role="tree"]','[role="grid"]',
995
+ '.popover','.popup','.dropdown','.menu','.modal','.tooltip','.panel',
996
+ '[class*="popover"]','[class*="popup"]','[class*="dropdown"]','[class*="menu"]','[class*="tooltip"]',
997
+ '[class*="modal"]','[class*="panel"]','[class*="overlay"]','[class*="sheet"]',
998
+ '[data-popup]','[data-dropdown]','[data-menu]','[data-popover]',
999
+ '.semi-dropdown','.semi-popover','.semi-modal','.semi-select-option',
1000
+ '.ant-dropdown','.ant-popover','.ant-modal','.ant-select-dropdown',
1001
+ '.el-dropdown','.el-popover','.el-dialog','.el-select-dropdown',
1002
+ '.t-dropdown','.t-popup','.t-dialog'
1003
+ ];
1004
+
1005
+ function isNearClick(el, cx, cy, range) {
1006
+ try {
1007
+ var r = el.getBoundingClientRect();
1008
+ if (!r || r.width === 0 || r.height === 0) return false;
1009
+ // Element overlaps with or is near the click area
1010
+ var margin = range || 300;
1011
+ return !(r.left > cx + margin || r.right < cx - margin || r.top > cy + margin || r.bottom < cy - margin);
1012
+ } catch(e) { return false; }
1013
+ }
1014
+
1015
+ function captureVisibleContext(cx, cy) {
1016
+ var result = { appeared: [], disappeared: [], stateChanges: [] };
1017
+ try {
1018
+ // 1. Find popover/dropdown/menu elements near the click
1019
+ for (var i = 0; i < POPOVER_SELECTORS.length; i++) {
1020
+ try {
1021
+ var els = document.querySelectorAll(POPOVER_SELECTORS[i]);
1022
+ for (var j = 0; j < els.length; j++) {
1023
+ var el = els[j];
1024
+ if (!isNearClick(el, cx, cy, 500)) continue;
1025
+ var rect = el.getBoundingClientRect();
1026
+ if (rect.width === 0 || rect.height === 0) continue;
1027
+ var items = [];
1028
+ // Capture child items (up to 20)
1029
+ var children = el.querySelectorAll('a,button,[role="menuitem"],[role="option"],[role="treeitem"],li,div[class*="item"]');
1030
+ for (var k = 0; k < Math.min(children.length, 20); k++) {
1031
+ var child = children[k];
1032
+ var childText = (child.textContent || '').trim().substring(0, 60);
1033
+ if (!childText) continue;
1034
+ var childInfo = { text: childText };
1035
+ if (child.disabled) childInfo.disabled = true;
1036
+ if (child.getAttribute('aria-disabled') === 'true') childInfo.disabled = true;
1037
+ if (child.tagName) childInfo.tag = child.tagName.toLowerCase();
1038
+ if (child.href) childInfo.href = child.href.substring(0, 80);
1039
+ items.push(childInfo);
1040
+ }
1041
+ result.appeared.push({
1042
+ tag: el.tagName.toLowerCase(),
1043
+ selector: uniqueSelector(el),
1044
+ role: el.getAttribute('role'),
1045
+ text: (el.textContent || '').trim().substring(0, 100),
1046
+ rect: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
1047
+ items: items,
1048
+ });
1049
+ }
1050
+ } catch(e) {}
1051
+ }
1052
+
1053
+ // 2. Find elements that changed aria-expanded or disabled state near click
1054
+ var nearbyEls = document.elementsFromPoint ? document.elementsFromPoint(cx, cy) : [];
1055
+ // Also check elements in a wider area
1056
+ var area = document.querySelector('body');
1057
+ if (area) {
1058
+ var allInteractive = area.querySelectorAll('[aria-expanded],[disabled],[aria-disabled],[aria-selected],[data-state]');
1059
+ for (var i = 0; i < allInteractive.length; i++) {
1060
+ var el = allInteractive[i];
1061
+ if (!isNearClick(el, cx, cy, 400)) continue;
1062
+ var info = { tag: el.tagName.toLowerCase(), text: (el.textContent || '').trim().substring(0, 60) };
1063
+ if (el.id) info.id = el.id;
1064
+ if (el.getAttribute('aria-expanded')) info.ariaExpanded = el.getAttribute('aria-expanded');
1065
+ if (el.disabled) info.disabled = true;
1066
+ if (el.getAttribute('aria-disabled') === 'true') info.disabled = true;
1067
+ if (el.getAttribute('aria-selected')) info.ariaSelected = el.getAttribute('aria-selected');
1068
+ if (el.getAttribute('data-state')) info.dataState = el.getAttribute('data-state');
1069
+ result.stateChanges.push(info);
1070
+ }
1071
+ }
1072
+ } catch(e) {}
1073
+ // Deduplicate appeared by selector
1074
+ var seen = {};
1075
+ result.appeared = result.appeared.filter(function(item) {
1076
+ if (!item.selector) return true;
1077
+ if (seen[item.selector]) return false;
1078
+ seen[item.selector] = true;
1079
+ return true;
1080
+ });
1081
+ return result;
1082
+ }
1083
+
1084
+ document.addEventListener('click', function(e) {
1085
+ var cx = e.clientX, cy = e.clientY;
1086
+ // Snapshot before (for diff)
1087
+ var beforeExpanded = {};
1088
+ try {
1089
+ var expandedEls = document.querySelectorAll('[aria-expanded]');
1090
+ for (var i = 0; i < expandedEls.length; i++) {
1091
+ var el = expandedEls[i];
1092
+ if (isNearClick(el, cx, cy, 400)) {
1093
+ beforeExpanded[el.id || uniqueSelector(el)] = el.getAttribute('aria-expanded');
1094
+ }
1095
+ }
1096
+ } catch(e) {}
1097
+
1098
+ pushAction('click', { element: describe(resolveMeaningful(e)), x: cx, y: cy });
1099
+
1100
+ // After 200ms, capture what changed
1101
+ setTimeout(function() {
1102
+ try {
1103
+ var ctx = captureVisibleContext(cx, cy);
1104
+ // Check aria-expanded changes
1105
+ try {
1106
+ var expandedEls = document.querySelectorAll('[aria-expanded]');
1107
+ for (var i = 0; i < expandedEls.length; i++) {
1108
+ var el = expandedEls[i];
1109
+ var key = el.id || uniqueSelector(el);
1110
+ var now = el.getAttribute('aria-expanded');
1111
+ if (beforeExpanded[key] !== undefined && beforeExpanded[key] !== now) {
1112
+ ctx.stateChanges.push({
1113
+ tag: el.tagName.toLowerCase(),
1114
+ text: (el.textContent || '').trim().substring(0, 60),
1115
+ id: el.id || undefined,
1116
+ ariaExpanded: now,
1117
+ changed: true,
1118
+ });
1119
+ }
1120
+ }
1121
+ } catch(e) {}
1122
+ if (ctx.appeared.length > 0 || ctx.stateChanges.length > 0) {
1123
+ var lastAction = window.__xb_pending_actions[window.__xb_pending_actions.length - 1];
1124
+ if (lastAction && lastAction.type === 'click') {
1125
+ lastAction.clickContext = ctx;
1126
+ }
1127
+ }
1128
+ } catch(e) {}
1129
+ }, 200);
1130
+ }, true);
1131
+
1132
+ document.addEventListener('input', function(e) {
1133
+ var target = actualTarget(e);
1134
+ pushAction('input', {
1135
+ element: describe(target),
1136
+ value: (target.value || target.textContent || '').substring(0, 200),
1137
+ });
1138
+ }, true);
1139
+
1140
+ document.addEventListener('change', function(e) {
1141
+ var target = actualTarget(e);
1142
+ var tag = target.tagName && target.tagName.toLowerCase();
1143
+ if (tag === 'select') {
1144
+ pushAction('change', { element: describe(target), value: (target.value || '').substring(0, 100) });
1145
+ } else if (tag === 'input' && target.type === 'file') {
1146
+ var files = target.files;
1147
+ var fileNames = [];
1148
+ for (var i = 0; i < files.length; i++) {
1149
+ fileNames.push(files[i].name);
1150
+ }
1151
+ // Read file contents asynchronously, then push action
1152
+ var readers = [];
1153
+ for (var i = 0; i < files.length; i++) {
1154
+ readers.push(new Promise(function(resolve) {
1155
+ var reader = new FileReader();
1156
+ reader.onload = function() { resolve(reader.result); };
1157
+ reader.onerror = function() { resolve(null); };
1158
+ reader.readAsDataURL(files[i]);
1159
+ }));
1160
+ }
1161
+ Promise.all(readers).then(function(contents) {
1162
+ var fileData = [];
1163
+ for (var i = 0; i < files.length; i++) {
1164
+ fileData.push({
1165
+ name: files[i].name,
1166
+ type: files[i].type,
1167
+ size: files[i].size,
1168
+ dataUrl: contents[i],
1169
+ });
1170
+ }
1171
+ pushAction('filechooser', {
1172
+ element: describe(target),
1173
+ value: fileNames.join(', '),
1174
+ files: {
1175
+ names: fileNames,
1176
+ count: files.length,
1177
+ isMultiple: target.multiple,
1178
+ fileData: fileData,
1179
+ },
1180
+ });
1181
+ });
1182
+ }
1183
+ }, true);
1184
+
1185
+ document.addEventListener('keydown', function(e) {
1186
+ // Special keys always recorded
1187
+ if (e.key === 'Enter' || e.key === 'Tab' || e.key === 'Escape' || e.key.startsWith('Arrow')) {
1188
+ pushAction('keydown', { key: e.key, element: describe(actualTarget(e)) });
1189
+ return;
1190
+ }
1191
+ // Editing keys
1192
+ if (e.key === 'Backspace' || e.key === 'Delete') {
1193
+ pushAction('keydown', { key: e.key, element: describe(actualTarget(e)) });
1194
+ return;
1195
+ }
1196
+ // Modifier combinations (Ctrl/Cmd/Alt + key)
1197
+ if (e.ctrlKey || e.metaKey || e.altKey) {
1198
+ var combo = '';
1199
+ if (e.ctrlKey) combo += 'Ctrl+';
1200
+ if (e.metaKey) combo += 'Meta+';
1201
+ if (e.altKey) combo += 'Alt+';
1202
+ if (e.shiftKey) combo += 'Shift+';
1203
+ combo += e.key;
1204
+ pushAction('keydown', { key: combo, element: describe(actualTarget(e)) });
1205
+ }
1206
+ }, true);
1207
+
1208
+ document.addEventListener('submit', function(e) {
1209
+ pushAction('submit', { element: describe(actualTarget(e)) });
1210
+ }, true);
1211
+
1212
+ document.addEventListener('scroll', function() {
1213
+ if (!window.__xb_last_scroll || Date.now() - window.__xb_last_scroll > 500) {
1214
+ window.__xb_last_scroll = Date.now();
1215
+ pushAction('scroll', { scrollX: window.scrollX, scrollY: window.scrollY });
1216
+ }
1217
+ }, true);
1218
+
1219
+ // \u2500\u2500 Double click \u2500\u2500
1220
+ document.addEventListener('dblclick', function(e) {
1221
+ pushAction('dblclick', {
1222
+ element: describe(resolveMeaningful(e)),
1223
+ x: e.clientX,
1224
+ y: e.clientY,
1225
+ });
1226
+ }, true);
1227
+
1228
+ // \u2500\u2500 Right click (context menu) \u2500\u2500
1229
+ document.addEventListener('contextmenu', function(e) {
1230
+ pushAction('contextmenu', {
1231
+ element: describe(resolveMeaningful(e)),
1232
+ x: e.clientX,
1233
+ y: e.clientY,
1234
+ });
1235
+ }, true);
1236
+
1237
+ // \u2500\u2500 Hover (throttled to 800ms) \u2500\u2500
1238
+ var __xb_last_hover = 0;
1239
+ document.addEventListener('mouseover', function(e) {
1240
+ if (Date.now() - __xb_last_hover < 800) return;
1241
+ __xb_last_hover = Date.now();
1242
+ var target = resolveMeaningful(e);
1243
+ // Only record hover on interactive elements
1244
+ var tag = target.tagName && target.tagName.toLowerCase();
1245
+ var isInteractive = tag === 'a' || tag === 'button' || tag === 'input'
1246
+ || tag === 'select' || tag === 'textarea' || tag === 'summary'
1247
+ || target.getAttribute('role') === 'button'
1248
+ || target.getAttribute('role') === 'link'
1249
+ || target.getAttribute('role') === 'menuitem'
1250
+ || target.getAttribute('role') === 'tab'
1251
+ || target.getAttribute('role') === 'option'
1252
+ || !!target.closest('[role="menu"], [role="menubar"], [role="tablist"], [role="listbox"], [role="tree"], nav, menu');
1253
+ if (isInteractive) {
1254
+ pushAction('hover', {
1255
+ element: describe(target),
1256
+ x: e.clientX,
1257
+ y: e.clientY,
1258
+ });
1259
+ }
1260
+ }, true);
1261
+
1262
+ // \u2500\u2500 Drag & drop \u2500\u2500
1263
+ var __xb_drag_source = null;
1264
+ var __xb_drag_start_pos = null;
1265
+ document.addEventListener('dragstart', function(e) {
1266
+ __xb_drag_source = e.target;
1267
+ __xb_drag_start_pos = { x: e.clientX, y: e.clientY };
1268
+ }, true);
1269
+ document.addEventListener('drop', function(e) {
1270
+ if (__xb_drag_source && __xb_drag_start_pos) {
1271
+ pushAction('drag', {
1272
+ x: e.clientX,
1273
+ y: e.clientY,
1274
+ drag: {
1275
+ fromX: __xb_drag_start_pos.x,
1276
+ fromY: __xb_drag_start_pos.y,
1277
+ toX: e.clientX,
1278
+ toY: e.clientY,
1279
+ source: describe(__xb_drag_source),
1280
+ target: describe(resolveMeaningful(e)),
1281
+ },
1282
+ });
1283
+ }
1284
+ __xb_drag_source = null;
1285
+ __xb_drag_start_pos = null;
1286
+ }, true);
1287
+ document.addEventListener('dragend', function() {
1288
+ __xb_drag_source = null;
1289
+ __xb_drag_start_pos = null;
1290
+ }, true);
1291
+
1292
+ // \u2500\u2500 Window resize \u2500\u2500
1293
+ var __xb_last_resize = 0;
1294
+ window.addEventListener('resize', function() {
1295
+ if (Date.now() - __xb_last_resize < 1000) return;
1296
+ __xb_last_resize = Date.now();
1297
+ pushAction('resize', {
1298
+ resize: { width: window.innerWidth, height: window.innerHeight },
1299
+ });
1300
+ }, true);
1301
+
1302
+ // \u2500\u2500 Clipboard (copy/paste/cut) \u2500\u2500
1303
+ document.addEventListener('copy', function(e) {
1304
+ pushAction('clipboard', { clipboard: { operation: 'copy' } });
1305
+ }, true);
1306
+ document.addEventListener('paste', function(e) {
1307
+ var preview = '';
1308
+ try {
1309
+ preview = (e.clipboardData || window.clipboardData).getData('text').substring(0, 100);
1310
+ } catch(ex) {}
1311
+ pushAction('clipboard', { clipboard: { operation: 'paste', textPreview: preview } });
1312
+ }, true);
1313
+ document.addEventListener('cut', function(e) {
1314
+ pushAction('clipboard', { clipboard: { operation: 'cut' } });
1315
+ }, true);
1316
+
1317
+ // \u2500\u2500 Touch events \u2500\u2500
1318
+ document.addEventListener('touchstart', function(e) {
1319
+ var touches = [];
1320
+ for (var i = 0; i < e.touches.length; i++) {
1321
+ touches.push({ x: e.touches[i].clientX, y: e.touches[i].clientY });
1322
+ }
1323
+ pushAction('touch', {
1324
+ element: describe(resolveMeaningful(e)),
1325
+ touch: { touchType: 'start', touches: touches },
1326
+ });
1327
+ }, true);
1328
+ document.addEventListener('touchend', function(e) {
1329
+ var touches = [];
1330
+ for (var i = 0; i < e.changedTouches.length; i++) {
1331
+ touches.push({ x: e.changedTouches[i].clientX, y: e.changedTouches[i].clientY });
1332
+ }
1333
+ pushAction('touch', {
1334
+ element: describe(resolveMeaningful(e)),
1335
+ touch: { touchType: 'end', touches: touches },
1336
+ });
1337
+ }, true);
1338
+
1339
+ // \u2500\u2500 Focus / Blur \u2500\u2500
1340
+ document.addEventListener('focusin', function(e) {
1341
+ var target = actualTarget(e);
1342
+ var tag = target.tagName && target.tagName.toLowerCase();
1343
+ if (tag === 'input' || tag === 'textarea' || tag === 'select' || target.isContentEditable) {
1344
+ pushAction('focus', {
1345
+ element: describe(target),
1346
+ focus: { focusType: 'focus' },
1347
+ });
1348
+ }
1349
+ }, true);
1350
+ document.addEventListener('focusout', function(e) {
1351
+ var target = actualTarget(e);
1352
+ var tag = target.tagName && target.tagName.toLowerCase();
1353
+ if (tag === 'input' || tag === 'textarea' || tag === 'select' || target.isContentEditable) {
1354
+ pushAction('focus', {
1355
+ element: describe(target),
1356
+ focus: { focusType: 'blur' },
1357
+ });
1358
+ }
1359
+ }, true);
1360
+
1361
+ // \u2500\u2500 Visibility change (tab switch) \u2500\u2500
1362
+ document.addEventListener('visibilitychange', function() {
1363
+ pushAction('visibility', {
1364
+ visibility: { state: document.hidden ? 'hidden' : 'visible' },
1365
+ });
1366
+ }, true);
1367
+ })();
1368
+ `;
1369
+ var CHECKPOINT_OVERLAY_SCRIPT = `
1370
+ (function() {
1371
+ if (window.__xb_checkpoint_overlay) return;
1372
+ window.__xb_checkpoint_overlay = true;
1373
+
1374
+ var __xb_overlay = null; // main overlay container
1375
+ var __xb_highlight = null; // highlight box around hovered element
1376
+ var __xb_preview = null; // content preview panel
1377
+ var __xb_hint = null; // top hint bar
1378
+ var __xb_active = false; // is marking mode active
1379
+ var __xb_hovered_el = null; // currently hovered element
1380
+ var __xb_flash_timer = null;
1381
+
1382
+ // \u2500\u2500 helper: get element content for preview \u2500\u2500
1383
+ function getElementContent(el) {
1384
+ var texts = [];
1385
+ // Collect child items (li, option, button, menu items)
1386
+ var children = el.querySelectorAll('li, option, button, [role="option"], [role="menuitem"], [class*="item"], [class*="option"], a[href]');
1387
+ if (children.length > 0) {
1388
+ for (var i = 0; i < Math.min(children.length, 20); i++) {
1389
+ var t = children[i].textContent.trim();
1390
+ if (t && t.length < 100) texts.push(t);
1391
+ }
1392
+ }
1393
+ if (texts.length > 0) return texts;
1394
+ // Fallback: element's own text (truncated)
1395
+ var own = el.textContent.trim();
1396
+ if (own.length > 0) return [own.substring(0, 300)];
1397
+ return [];
1398
+ }
1399
+
1400
+ // \u2500\u2500 helper: get short selector for element \u2500\u2500
1401
+ function shortSelector(el) {
1402
+ if (!el || !el.tagName) return '';
1403
+ if (el.id) return '#' + el.id;
1404
+ if (el.getAttribute('data-testid')) return '[data-testid="' + el.getAttribute('data-testid') + '"]';
1405
+ if (el.getAttribute('aria-label')) return '[aria-label="' + el.getAttribute('aria-label').substring(0, 30) + '"]';
1406
+ if (el.className && typeof el.className === 'string') {
1407
+ var cls = el.className.trim().split(/\\s+/)[0];
1408
+ if (cls) return el.tagName.toLowerCase() + '.' + cls;
1409
+ }
1410
+ return el.tagName.toLowerCase();
1411
+ }
1412
+
1413
+ // \u2500\u2500 helper: tag label \u2500\u2500
1414
+ function tagLabel(el) {
1415
+ var tag = el.tagName.toLowerCase();
1416
+ var type = el.getAttribute('type');
1417
+ var role = el.getAttribute('role');
1418
+ var placeholder = el.getAttribute('placeholder');
1419
+ var text = (el.textContent || '').trim().substring(0, 40);
1420
+ var parts = [tag];
1421
+ if (type) parts.push('type=' + type);
1422
+ if (role) parts.push('role=' + role);
1423
+ if (placeholder) parts.push('"' + placeholder.substring(0, 20) + '"');
1424
+ if (text && parts.length < 3) parts.push('"' + text + '"');
1425
+ return parts.join(' ');
1426
+ }
1427
+
1428
+ // \u2500\u2500 create overlay elements \u2500\u2500
1429
+ function createOverlay() {
1430
+ __xb_overlay = document.createElement('div');
1431
+ __xb_overlay.id = '__xb_mark_overlay';
1432
+ __xb_overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:999999;';
1433
+
1434
+ // Highlight box
1435
+ __xb_highlight = document.createElement('div');
1436
+ __xb_highlight.style.cssText = 'position:fixed;border:3px solid #3b82f6;border-radius:4px;pointer-events:none;transition:all 0.1s ease;display:none;box-shadow:0 0 12px rgba(59,130,246,0.5);';
1437
+ __xb_overlay.appendChild(__xb_highlight);
1438
+
1439
+ // Content preview panel (shows on hover)
1440
+ __xb_preview = document.createElement('div');
1441
+ __xb_preview.style.cssText = 'position:fixed;right:16px;top:50px;width:340px;max-height:60vh;overflow-y:auto;background:rgba(15,15,15,0.95);color:#e5e5e5;font:12px/1.5 system-ui,sans-serif;padding:12px;border-radius:8px;pointer-events:none;box-shadow:0 4px 20px rgba(0,0,0,0.5);display:none;';
1442
+ __xb_overlay.appendChild(__xb_preview);
1443
+
1444
+ // Top hint bar
1445
+ __xb_hint = document.createElement('div');
1446
+ __xb_hint.style.cssText = 'position:fixed;top:12px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.88);color:white;font:13px/1.4 system-ui,sans-serif;padding:8px 16px;border-radius:8px;pointer-events:none;white-space:nowrap;box-shadow:0 4px 16px rgba(0,0,0,0.3);';
1447
+ __xb_hint.textContent = '\\u00b7 \\u00b7 \\u00b7';
1448
+ __xb_overlay.appendChild(__xb_hint);
1449
+
1450
+ document.body.appendChild(__xb_overlay);
1451
+ document.body.style.cursor = 'crosshair';
1452
+ }
1453
+
1454
+ // \u2500\u2500 destroy overlay \u2500\u2500
1455
+ function destroyOverlay() {
1456
+ if (__xb_overlay && __xb_overlay.parentNode) {
1457
+ __xb_overlay.parentNode.removeChild(__xb_overlay);
1458
+ }
1459
+ __xb_overlay = null;
1460
+ __xb_highlight = null;
1461
+ __xb_preview = null;
1462
+ __xb_hint = null;
1463
+ document.body.style.cursor = '';
1464
+ __xb_hovered_el = null;
1465
+ }
1466
+
1467
+ // \u2500\u2500 update highlight on mouse move \u2500\u2500
1468
+ function updateHighlight(e) {
1469
+ var el = document.elementFromPoint(e.clientX, e.clientY);
1470
+ // Skip overlay elements
1471
+ if (!el || (el.id && el.id.indexOf('__xb_') === 0) || (__xb_overlay && __xb_overlay.contains(el))) {
1472
+ __xb_highlight.style.display = 'none';
1473
+ __xb_preview.style.display = 'none';
1474
+ __xb_hovered_el = null;
1475
+ __xb_hint.textContent = '\\u5c06 \\u9f20\\u6807 \\u79fb\\u5230\\u60f3\\u6807\\u8bb0\\u7684\\u5143\\u7d20\\u4e0a';
1476
+ return;
1477
+ }
1478
+ __xb_hovered_el = el;
1479
+ var rect = el.getBoundingClientRect();
1480
+
1481
+ // Update highlight box
1482
+ __xb_highlight.style.display = 'block';
1483
+ __xb_highlight.style.left = (rect.left - 3) + 'px';
1484
+ __xb_highlight.style.top = (rect.top - 3) + 'px';
1485
+ __xb_highlight.style.width = (rect.width + 6) + 'px';
1486
+ __xb_highlight.style.height = (rect.height + 6) + 'px';
1487
+
1488
+ // Build content preview
1489
+ var content = getElementContent(el);
1490
+ var sel = shortSelector(el);
1491
+ var tag = tagLabel(el);
1492
+ var html = '<div style="color:#888;margin-bottom:6px;font-size:11px;">' + sel + '</div>';
1493
+ html += '<div style="color:#fff;font-weight:bold;margin-bottom:8px;">' + tag + '</div>';
1494
+ if (content.length > 0) {
1495
+ html += '<div style="border-top:1px solid #333;padding-top:8px;">';
1496
+ for (var i = 0; i < Math.min(content.length, 15); i++) {
1497
+ html += '<div style="padding:2px 0;color:#ccc;">\\u2022 ' + content[i].replace(/</g, '&lt;') + '</div>';
1498
+ }
1499
+ if (content.length > 15) html += '<div style="color:#666;">... +' + (content.length - 15) + ' more</div>';
1500
+ html += '</div>';
1501
+ }
1502
+
1503
+ __xb_preview.innerHTML = html;
1504
+ __xb_preview.style.display = 'block';
1505
+
1506
+ // Update hint
1507
+ __xb_hint.textContent = '\\u2461 \\u91c7\\u96c6\\u6b64\\u5143\\u7d20 \\u2462 \\u5361\\u70b9\\uff08\\u4eba\\u5de5\\u4ecb\\u5165\\uff09 \\u2502 \\u677e\\u5f00 Option \\u9000\\u51fa';
1508
+ }
1509
+
1510
+ // \u2500\u2500 flash feedback \u2500\u2500
1511
+ function flash(color) {
1512
+ if (!__xb_highlight) return;
1513
+ __xb_highlight.style.borderColor = color;
1514
+ __xb_highlight.style.boxShadow = '0 0 24px ' + color + '80';
1515
+ clearTimeout(__xb_flash_timer);
1516
+ __xb_flash_timer = setTimeout(function() {
1517
+ if (__xb_highlight) {
1518
+ __xb_highlight.style.borderColor = '#3b82f6';
1519
+ __xb_highlight.style.boxShadow = '0 0 12px rgba(59,130,246,0.5)';
1520
+ }
1521
+ }, 600);
1522
+ }
1523
+
1524
+ // \u2500\u2500 push checkpoint to pending actions \u2500\u2500
1525
+ function pushCheckpoint(mode) {
1526
+ if (!__xb_hovered_el) return;
1527
+ var el = __xb_hovered_el;
1528
+ var content = getElementContent(el);
1529
+ var sel = shortSelector(el);
1530
+ var rect = el.getBoundingClientRect();
1531
+
1532
+ window.__xb_pending_actions = window.__xb_pending_actions || [];
1533
+ window.__xb_pending_actions.push({
1534
+ type: 'checkpoint',
1535
+ ts: Date.now(),
1536
+ url: location.href,
1537
+ title: document.title,
1538
+ checkpointType: mode === 'collect' ? 'collect' : 'blocker',
1539
+ hint: mode === 'collect' ? '\\u91c7\\u96c6: ' + tagLabel(el) : '\\u5361\\u70b9: \\u9700\\u8981\\u4eba\\u5de5\\u4ecb\\u5165',
1540
+ selector: sel,
1541
+ source: 'manual',
1542
+ category: mode,
1543
+ content: content.join(' | '),
1544
+ elementTag: el.tagName.toLowerCase(),
1545
+ elementText: (el.textContent || '').trim().substring(0, 200),
1546
+ rect: { x: Math.round(rect.left), y: Math.round(rect.top), w: Math.round(rect.width), h: Math.round(rect.height) },
1547
+ });
1548
+ }
1549
+
1550
+ // \u2500\u2500 event listeners \u2500\u2500
1551
+ document.addEventListener('keydown', function(e) {
1552
+ // Option/Alt pressed \u2192 enter marking mode
1553
+ if (e.key === 'Alt' && !e.repeat && !__xb_active) {
1554
+ __xb_active = true;
1555
+ createOverlay();
1556
+ e.preventDefault();
1557
+ return;
1558
+ }
1559
+
1560
+ // Only process keys while marking mode is active
1561
+ if (!__xb_active) return;
1562
+
1563
+ // Press 1 \u2192 collect (capture element)
1564
+ if (e.code === 'Digit1' || e.key === '1') {
1565
+ pushCheckpoint('collect');
1566
+ flash('#22c55e'); // green
1567
+ e.preventDefault();
1568
+ e.stopPropagation();
1569
+ return;
1570
+ }
1571
+
1572
+ // Press 2 \u2192 blocker (human intervention needed)
1573
+ if (e.code === 'Digit2' || e.key === '2') {
1574
+ pushCheckpoint('blocker');
1575
+ flash('#ef4444'); // red
1576
+ e.preventDefault();
1577
+ e.stopPropagation();
1578
+ return;
1579
+ }
1580
+ }, true);
1581
+
1582
+ document.addEventListener('keyup', function(e) {
1583
+ if (e.key === 'Alt') {
1584
+ __xb_active = false;
1585
+ destroyOverlay();
1586
+ }
1587
+ }, true);
1588
+
1589
+ // Track mouse move (only when active)
1590
+ document.addEventListener('mousemove', function(e) {
1591
+ if (!__xb_active) return;
1592
+ updateHighlight(e);
1593
+ }, true);
1594
+
1595
+ // Prevent click while in marking mode
1596
+ document.addEventListener('click', function(e) {
1597
+ if (__xb_active) {
1598
+ e.preventDefault();
1599
+ e.stopPropagation();
1600
+ }
1601
+ }, true);
1602
+ })();
1603
+ `;
1604
+ var SessionRecorder = class _SessionRecorder {
1605
+ context;
1606
+ page;
1607
+ sessionName;
1608
+ startUrl = "";
1609
+ startedAt = 0;
1610
+ actions = [];
1611
+ network = [];
1612
+ contextChanges = [];
1613
+ checkpoints = [];
1614
+ actionCounter = 0;
1615
+ networkCounter = 0;
1616
+ contextCounter = 0;
1617
+ checkpointCounter = 0;
1618
+ pollTimer = null;
1619
+ flushTimer = null;
1620
+ lastActionTs = 0;
1621
+ activePages = /* @__PURE__ */ new Set();
1622
+ lastKnownUrl = "";
1623
+ // Track URL to detect real navigation changes
1624
+ /** Dedup window: after a CDP command action, ignore matching action signals within this window */
1625
+ cdpActionDedup = null;
1626
+ /** Network dedup: last request key for short-window dedup */
1627
+ _lastRequestKey = "";
1628
+ _lastRequestTs = 0;
1629
+ _isRecording = false;
1630
+ constructor(context, page, sessionName) {
1631
+ this.context = context;
1632
+ this.page = page;
1633
+ this.sessionName = sessionName;
1634
+ }
1635
+ get isRecording() {
1636
+ return this._isRecording;
1637
+ }
1638
+ get actionCount() {
1639
+ return this.actions.length;
1640
+ }
1641
+ /** Record an action triggered by a CDP command (e.g. xbrowser fill/click/goto) */
1642
+ recordCommandAction(action) {
1643
+ const normalizedType = action.type === "cdp-fill" ? "input" : action.type === "cdp-click" ? "click" : action.type;
1644
+ const recent = this.actions[this.actions.length - 1];
1645
+ if (recent && Date.now() - recent.timestamp < 1500) {
1646
+ const typeMatch = recent.type === action.type || recent.type === normalizedType;
1647
+ const valueMatch = !action.value || recent.value === action.value;
1648
+ const selectorMatch = !action.selector || recent.element?.selector && (recent.element.selector === action.selector || recent.element.selector.endsWith(" " + action.selector) || action.selector.endsWith(" " + recent.element.selector));
1649
+ if (typeMatch && valueMatch && selectorMatch) {
1650
+ return;
1651
+ }
1652
+ }
1653
+ this.actionCounter++;
1654
+ const ts = Date.now();
1655
+ const actionUrl = action.url && action.url !== "about:blank" ? action.url : this.lastKnownUrl || this.page.url();
1656
+ this.actions.push({
1657
+ id: this.actionCounter,
1658
+ type: action.type,
1659
+ timestamp: ts,
1660
+ url: actionUrl,
1661
+ pageTitle: "",
1662
+ element: action.element || (action.selector ? { tag: "", selector: action.selector, text: "" } : void 0),
1663
+ value: action.value
1664
+ });
1665
+ this.lastActionTs = ts;
1666
+ this.cdpActionDedup = {
1667
+ type: normalizedType,
1668
+ value: action.value,
1669
+ selector: action.selector,
1670
+ until: Date.now() + 1500
1671
+ };
1672
+ if (action.url && action.url !== "about:blank") {
1673
+ this.lastKnownUrl = action.url;
1674
+ } else if (action.type === "goto" && action.value && action.value !== "about:blank") {
1675
+ this.lastKnownUrl = action.value;
1676
+ }
1677
+ }
1678
+ get networkCount() {
1679
+ return this.network.length;
1680
+ }
1681
+ getLiveData() {
1682
+ return this.buildData();
1683
+ }
1684
+ addManualCheckpoint(type, hint, selector) {
1685
+ this.checkpointCounter++;
1686
+ const cp = {
1687
+ id: this.checkpointCounter,
1688
+ type,
1689
+ timestamp: Date.now(),
1690
+ url: this.page.url(),
1691
+ pageTitle: "",
1692
+ hint,
1693
+ selector,
1694
+ source: "manual"
1695
+ };
1696
+ this.checkpoints.push(cp);
1697
+ return cp;
1698
+ }
1699
+ /** Directory for this session's recordings. */
1700
+ get recordingsDir() {
1701
+ return _SessionRecorder.getRecordingsDir(this.sessionName);
1702
+ }
1703
+ static getRecordingsDir(sessionName) {
1704
+ return join2(homedir2(), ".xbrowser", "sessions", sessionName, "recordings");
1705
+ }
1706
+ /** Path to the control file (used by record stop to signal this process). */
1707
+ get controlFilePath() {
1708
+ return join2(this.recordingsDir, ".control.json");
1709
+ }
1710
+ /** Path to the stop signal file (written by `record stop`). */
1711
+ get stopSignalPath() {
1712
+ return join2(this.recordingsDir, ".stop");
1713
+ }
1714
+ // ─── Start ──────────────────────────────────────────────────────
1715
+ async start(url) {
1716
+ if (this._isRecording) throw new Error("Already recording");
1717
+ this._isRecording = true;
1718
+ this.startedAt = Date.now();
1719
+ this.actions = [];
1720
+ this.network = [];
1721
+ this.contextChanges = [];
1722
+ this.checkpoints = [];
1723
+ this.checkpointCounter = 0;
1724
+ this.lastKnownUrl = this.page.url();
1725
+ await this.page.addInitScript(getSelectorGeneratorScript());
1726
+ await this.page.addInitScript(ACTION_SIGNAL_SCRIPT);
1727
+ await this.page.addInitScript(CHECKPOINT_OVERLAY_SCRIPT);
1728
+ if (url) {
1729
+ await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
1730
+ this.startUrl = url;
1731
+ } else {
1732
+ this.startUrl = this.page.url();
1733
+ }
1734
+ mkdirSync2(this.recordingsDir, { recursive: true });
1735
+ const control = {
1736
+ pid: process.pid,
1737
+ startedAt: new Date(this.startedAt).toISOString(),
1738
+ startUrl: this.startUrl,
1739
+ sessionName: this.sessionName
1740
+ };
1741
+ writeFileSync2(this.controlFilePath, JSON.stringify(control, null, 2), "utf-8");
1742
+ await this.injectActionScript(this.page);
1743
+ this.context.on("request", this.handleRequest);
1744
+ this.context.on("response", this.handleResponse);
1745
+ this.context.on("page", this.handleNewPage);
1746
+ for (const p of this.context.pages()) {
1747
+ p.on("request", this.handleRequest);
1748
+ p.on("response", this.handleResponse);
1749
+ }
1750
+ this.context.on("page", this.handleNewPage);
1751
+ this.page.on("framenavigated", this.handleFrameNavigated);
1752
+ this.page.on("dialog", this.handleDialog);
1753
+ this.page.on("filechooser", this.handleFileChooser);
1754
+ this.pollTimer = setInterval(() => void this.pollActions(), 200);
1755
+ this.flushTimer = setInterval(() => this.flushToDisk(), 5e3);
1756
+ }
1757
+ // ─── Stop ───────────────────────────────────────────────────────
1758
+ async stop() {
1759
+ if (!this._isRecording) throw new Error("Not recording");
1760
+ this._isRecording = false;
1761
+ if (this.pollTimer) {
1762
+ clearInterval(this.pollTimer);
1763
+ this.pollTimer = null;
1764
+ }
1765
+ if (this.flushTimer) {
1766
+ clearInterval(this.flushTimer);
1767
+ this.flushTimer = null;
1768
+ }
1769
+ this.context.off("request", this.handleRequest);
1770
+ this.context.off("response", this.handleResponse);
1771
+ this.context.off("page", this.handleNewPage);
1772
+ this.page.off("framenavigated", this.handleFrameNavigated);
1773
+ this.page.off("dialog", this.handleDialog);
1774
+ for (const p of this.activePages) {
1775
+ try {
1776
+ p.off("framenavigated", this.handleFrameNavigated);
1777
+ } catch {
1778
+ }
1779
+ }
1780
+ await this.flushPendingActions(this.page);
1781
+ for (const p of this.activePages) {
1782
+ await this.flushPendingActions(p).catch(() => {
1783
+ });
1784
+ }
1785
+ const data = this.buildData();
1786
+ const summary = this.buildSummary(data);
1787
+ this.writeFinalOutput(data, summary);
1788
+ try {
1789
+ const knowledge = updateSiteKnowledge(data);
1790
+ const knowledgeDir = join2(this.recordingsDir, "site-knowledge.md");
1791
+ const knowledgeJson = join2(this.recordingsDir, "site-knowledge.json");
1792
+ const { readFileSync: rf } = __require("fs");
1793
+ const { getKnowledgePath: getKnowledgePath2 } = (init_site_knowledge(), __toCommonJS(site_knowledge_exports));
1794
+ const mdPath = getKnowledgePath2(knowledge.domain, "md");
1795
+ try {
1796
+ const md = rf(mdPath, "utf-8");
1797
+ writeFileSync2(knowledgeDir, md, "utf-8");
1798
+ } catch {
1799
+ }
1800
+ writeFileSync2(knowledgeJson, JSON.stringify(knowledge, null, 2), "utf-8");
1801
+ } catch {
1802
+ }
1803
+ try {
1804
+ rmSync(this.controlFilePath);
1805
+ } catch {
1806
+ }
1807
+ try {
1808
+ rmSync(this.stopSignalPath);
1809
+ } catch {
1810
+ }
1811
+ return { data, summary };
1812
+ }
1813
+ // ─── Cleanup (called on session close) ──────────────────────────
1814
+ static cleanup(sessionName) {
1815
+ const dir = _SessionRecorder.getRecordingsDir(sessionName);
1816
+ if (existsSync2(dir)) {
1817
+ rmSync(dir, { recursive: true, force: true });
1818
+ }
1819
+ }
1820
+ // ─── Wait for stop signal (blocks the process) ──────────────────
1821
+ waitForStopSignal() {
1822
+ return new Promise((resolve) => {
1823
+ const check = () => {
1824
+ if (existsSync2(this.stopSignalPath)) {
1825
+ resolve();
1826
+ } else {
1827
+ setTimeout(check, 300);
1828
+ }
1829
+ };
1830
+ check();
1831
+ });
1832
+ }
1833
+ // ─── Static: send stop signal to a running recorder ─────────────
1834
+ static async sendStopSignal(sessionName) {
1835
+ const dir = _SessionRecorder.getRecordingsDir(sessionName);
1836
+ const controlPath = join2(dir, ".control.json");
1837
+ const stopPath = join2(dir, ".stop");
1838
+ if (!existsSync2(controlPath)) return null;
1839
+ const control = JSON.parse(readFileSync2(controlPath, "utf-8"));
1840
+ let alive = false;
1841
+ try {
1842
+ process.kill(control.pid, 0);
1843
+ alive = true;
1844
+ } catch {
1845
+ alive = false;
1846
+ }
1847
+ if (!alive) {
1848
+ try {
1849
+ rmSync(controlPath);
1850
+ } catch {
1851
+ }
1852
+ return control;
1853
+ }
1854
+ mkdirSync2(dir, { recursive: true });
1855
+ writeFileSync2(stopPath, JSON.stringify({ stoppedAt: (/* @__PURE__ */ new Date()).toISOString() }), "utf-8");
1856
+ for (let i = 0; i < 50; i++) {
1857
+ await new Promise((r) => setTimeout(r, 200));
1858
+ if (!existsSync2(controlPath)) return control;
1859
+ try {
1860
+ process.kill(control.pid, 0);
1861
+ } catch {
1862
+ try {
1863
+ rmSync(controlPath);
1864
+ } catch {
1865
+ }
1866
+ return control;
1867
+ }
1868
+ }
1869
+ try {
1870
+ rmSync(controlPath);
1871
+ } catch {
1872
+ }
1873
+ return control;
1874
+ }
1875
+ // ─── Static: read recording from disk ───────────────────────────
1876
+ static readSummary(sessionName) {
1877
+ const path = join2(_SessionRecorder.getRecordingsDir(sessionName), "summary.json");
1878
+ try {
1879
+ return JSON.parse(readFileSync2(path, "utf-8"));
1880
+ } catch {
1881
+ return null;
1882
+ }
1883
+ }
1884
+ static readData(sessionName) {
1885
+ const path = join2(_SessionRecorder.getRecordingsDir(sessionName), "recording.json");
1886
+ try {
1887
+ return JSON.parse(readFileSync2(path, "utf-8"));
1888
+ } catch {
1889
+ return null;
1890
+ }
1891
+ }
1892
+ // ==================== Private ====================
1893
+ async injectActionScript(page) {
1894
+ try {
1895
+ await page.evaluate(getSelectorGeneratorScript());
1896
+ await page.evaluate(ACTION_SIGNAL_SCRIPT);
1897
+ await page.evaluate(`window.__xb_action_script_src = ${JSON.stringify(ACTION_SIGNAL_SCRIPT)};`);
1898
+ } catch {
1899
+ }
1900
+ try {
1901
+ await page.evaluate(CHECKPOINT_OVERLAY_SCRIPT);
1902
+ } catch {
1903
+ }
1904
+ try {
1905
+ await page.evaluate(`
1906
+ (function() {
1907
+ var _scriptSrc = ${JSON.stringify(ACTION_SIGNAL_SCRIPT)};
1908
+ function injectIframe(iframe) {
1909
+ try {
1910
+ var w = iframe.contentWindow;
1911
+ if (!w) return;
1912
+ // Force re-injection: clear old flag
1913
+ try { delete w.__xb_action_signal; } catch(e) {}
1914
+ w.eval(_scriptSrc);
1915
+ // Tag iframe so flushIframes knows it's injected
1916
+ iframe.__xb_injected = true;
1917
+ } catch(e) {}
1918
+ }
1919
+ function watchIframe(iframe) {
1920
+ if (iframe.__xb_watched) return;
1921
+ iframe.__xb_watched = true;
1922
+ injectIframe(iframe);
1923
+ iframe.addEventListener('load', function() { injectIframe(iframe); });
1924
+ }
1925
+ // Inject into existing iframes
1926
+ try {
1927
+ var iframes = document.querySelectorAll('iframe');
1928
+ for (var i = 0; i < iframes.length; i++) watchIframe(iframes[i]);
1929
+ } catch(e) {}
1930
+ // Watch for dynamically inserted iframes
1931
+ if (!window.__xb_iframe_observer) {
1932
+ window.__xb_iframe_observer = new MutationObserver(function(mutations) {
1933
+ for (var m = 0; m < mutations.length; m++) {
1934
+ for (var n = 0; n < mutations[m].addedNodes.length; n++) {
1935
+ var node = mutations[m].addedNodes[n];
1936
+ if (node.tagName === 'IFRAME') {
1937
+ watchIframe(node);
1938
+ } else if (node.querySelectorAll) {
1939
+ var sub = node.querySelectorAll('iframe');
1940
+ for (var k = 0; k < sub.length; k++) watchIframe(sub[k]);
1941
+ }
1942
+ }
1943
+ }
1944
+ });
1945
+ window.__xb_iframe_observer.observe(document.documentElement, { childList: true, subtree: true });
1946
+ }
1947
+ // Periodic re-injection for iframes that load late or navigate internally
1948
+ if (!window.__xb_iframe_timer) {
1949
+ window.__xb_iframe_timer = setInterval(function() {
1950
+ try {
1951
+ var iframes = document.querySelectorAll('iframe');
1952
+ for (var i = 0; i < iframes.length; i++) {
1953
+ if (!iframes[i].__xb_injected) {
1954
+ watchIframe(iframes[i]);
1955
+ }
1956
+ }
1957
+ } catch(e) {}
1958
+ }, 3000);
1959
+ }
1960
+ })();
1961
+ `);
1962
+ } catch {
1963
+ }
1964
+ }
1965
+ // ─── Network capture ────────────────────────────────────────────
1966
+ handleRequest = (request) => {
1967
+ const resourceType = request.resourceType();
1968
+ if (["image", "stylesheet", "font", "manifest", "other"].includes(resourceType)) return;
1969
+ const url = request.url();
1970
+ if (url.startsWith("data:") || url.startsWith("chrome-extension://") || url.startsWith("blob:")) return;
1971
+ const dedupKey = request.method() + " " + url;
1972
+ const now = Date.now();
1973
+ if (this._lastRequestKey === dedupKey && now - this._lastRequestTs < 100) return;
1974
+ this._lastRequestKey = dedupKey;
1975
+ this._lastRequestTs = now;
1976
+ this.networkCounter++;
1977
+ const entry = {
1978
+ id: this.networkCounter,
1979
+ timestamp: Date.now(),
1980
+ method: request.method(),
1981
+ url,
1982
+ path: new URL(url).pathname,
1983
+ status: 0,
1984
+ resourceType,
1985
+ contentType: "",
1986
+ responseSize: 0
1987
+ };
1988
+ if (["POST", "PATCH", "PUT"].includes(request.method())) {
1989
+ try {
1990
+ const postData = request.postData();
1991
+ if (postData) {
1992
+ try {
1993
+ entry.requestBody = JSON.parse(postData);
1994
+ } catch {
1995
+ entry.requestBody = postData.substring(0, 500);
1996
+ }
1997
+ }
1998
+ } catch {
1999
+ }
2000
+ }
2001
+ this.network.push(entry);
2002
+ };
2003
+ handleResponse = async (response) => {
2004
+ const url = response.url();
2005
+ if (url.startsWith("data:") || url.startsWith("chrome-extension://") || url.startsWith("blob:")) return;
2006
+ const entry = [...this.network].reverse().find((e) => e.url === url && e.status === 0);
2007
+ if (!entry) return;
2008
+ entry.status = response.status();
2009
+ entry.contentType = response.headers()["content-type"] || "";
2010
+ const resourceType = response.request().resourceType();
2011
+ const isApi = ["fetch", "xhr"].includes(resourceType) || entry.contentType.includes("json") || entry.contentType.includes("text/");
2012
+ if (isApi) {
2013
+ try {
2014
+ const text = await response.text();
2015
+ entry.responseSize = text.length;
2016
+ if (text.length <= 20480) {
2017
+ try {
2018
+ entry.responseBody = JSON.parse(text);
2019
+ } catch {
2020
+ entry.responseBody = text.substring(0, 500);
2021
+ }
2022
+ }
2023
+ } catch {
2024
+ }
2025
+ } else {
2026
+ try {
2027
+ entry.responseSize = parseInt(response.headers()["content-length"] || "0", 10);
2028
+ } catch {
2029
+ }
2030
+ }
2031
+ };
2032
+ // ─── Page tracking ──────────────────────────────────────────────
2033
+ handleNewPage = async (page) => {
2034
+ this.activePages.add(page);
2035
+ this.contextCounter++;
2036
+ this.contextChanges.push({
2037
+ id: this.contextCounter,
2038
+ timestamp: Date.now(),
2039
+ type: "new_tab",
2040
+ url: page.url(),
2041
+ detail: "New tab/popup opened"
2042
+ });
2043
+ await page.addInitScript(ACTION_SIGNAL_SCRIPT);
2044
+ await page.addInitScript(CHECKPOINT_OVERLAY_SCRIPT);
2045
+ await page.addInitScript(getSelectorGeneratorScript());
2046
+ const injectAndRetry = async () => {
2047
+ for (let attempt = 0; attempt < 5; attempt++) {
2048
+ try {
2049
+ const url = page.url();
2050
+ if (url && url !== "about:blank" && !url.startsWith("chrome")) {
2051
+ await this.injectActionScript(page);
2052
+ return;
2053
+ }
2054
+ } catch {
2055
+ }
2056
+ await new Promise((r) => setTimeout(r, 1e3));
2057
+ }
2058
+ };
2059
+ injectAndRetry();
2060
+ page.on("framenavigated", this.handleFrameNavigated);
2061
+ page.on("request", this.handleRequest);
2062
+ page.on("response", this.handleResponse);
2063
+ page.on("filechooser", this.handleFileChooser);
2064
+ page.on("dialog", this.handleDialog);
2065
+ page.on("close", () => {
2066
+ this.activePages.delete(page);
2067
+ });
2068
+ page.on("framenavigated", async (frame) => {
2069
+ if (frame !== frame.page().mainFrame()) return;
2070
+ const url = frame.url();
2071
+ if (url && url !== "about:blank" && !url.startsWith("chrome")) {
2072
+ try {
2073
+ await this.injectActionScript(page);
2074
+ } catch {
2075
+ }
2076
+ }
2077
+ });
2078
+ };
2079
+ handleFrameNavigated = (frame) => {
2080
+ if (frame !== frame.page().mainFrame()) return;
2081
+ const newUrl = frame.url();
2082
+ this.contextCounter++;
2083
+ this.contextChanges.push({
2084
+ id: this.contextCounter,
2085
+ timestamp: Date.now(),
2086
+ type: "navigate",
2087
+ url: newUrl
2088
+ });
2089
+ const lastAction = this.actions[this.actions.length - 1];
2090
+ const lastActionUrl = lastAction?.url;
2091
+ if (newUrl && newUrl !== "about:blank" && newUrl !== lastActionUrl) {
2092
+ this.actionCounter++;
2093
+ this.actions.push({
2094
+ id: this.actionCounter,
2095
+ type: "navigation",
2096
+ timestamp: Date.now(),
2097
+ url: newUrl,
2098
+ pageTitle: "",
2099
+ element: void 0
2100
+ });
2101
+ }
2102
+ const page = frame.page();
2103
+ if (newUrl && newUrl !== "about:blank") {
2104
+ this.injectActionScript(page).catch(() => {
2105
+ });
2106
+ }
2107
+ };
2108
+ handleDialog = async (dialog) => {
2109
+ this.checkpointCounter++;
2110
+ this.checkpoints.push({
2111
+ id: this.checkpointCounter,
2112
+ type: "dialog",
2113
+ timestamp: Date.now(),
2114
+ url: this.page.url(),
2115
+ pageTitle: await this.page.title().catch(() => ""),
2116
+ hint: `Dialog [${dialog.type}]: "${dialog.message()}"`,
2117
+ source: "auto",
2118
+ context: { dialogType: dialog.type, message: dialog.message() }
2119
+ });
2120
+ await dialog.dismiss().catch(() => {
2121
+ });
2122
+ };
2123
+ handleFileChooser = async (fileChooser) => {
2124
+ const url = this.page.url();
2125
+ const sel = fileChooser.selector || 'input[type="file"]';
2126
+ let element;
2127
+ try {
2128
+ element = await this.page.evaluate(new Function("selector", `
2129
+ const el = document.querySelector(selector);
2130
+ if (!el) return null;
2131
+ return window.__xb_describe(el);
2132
+ `), sel) ?? void 0;
2133
+ } catch {
2134
+ }
2135
+ let fileData = [];
2136
+ try {
2137
+ fileData = await this.page.evaluate(new Function("selector", `
2138
+ return new Promise(resolve => {
2139
+ const input = document.querySelector(selector);
2140
+ if (!input || !input.files || input.files.length === 0) { resolve([]); return; }
2141
+ const readers = [];
2142
+ for (let i = 0; i < input.files.length; i++) {
2143
+ readers.push(new Promise(r => {
2144
+ const reader = new FileReader();
2145
+ reader.onload = () => r({ name: input.files[i].name, type: input.files[i].type, size: input.files[i].size, dataUrl: reader.result });
2146
+ reader.onerror = () => r({ name: input.files[i].name, type: input.files[i].type, size: input.files[i].size, dataUrl: null });
2147
+ reader.readAsDataURL(input.files[i]);
2148
+ }));
2149
+ }
2150
+ Promise.all(readers).then(resolve);
2151
+ });
2152
+ `), sel);
2153
+ } catch {
2154
+ }
2155
+ const names = fileData.map((f) => f.name);
2156
+ this.actionCounter++;
2157
+ this.actions.push({
2158
+ id: this.actionCounter,
2159
+ type: "filechooser",
2160
+ timestamp: Date.now(),
2161
+ url,
2162
+ pageTitle: "",
2163
+ element: element || {
2164
+ selector: sel,
2165
+ strategy: "css",
2166
+ confidence: "high",
2167
+ tag: "input",
2168
+ text: ""
2169
+ },
2170
+ value: names.join(", ") || void 0,
2171
+ files: {
2172
+ names,
2173
+ count: fileData.length,
2174
+ isMultiple: fileChooser.isMultiple,
2175
+ fileData: fileData.length > 0 ? fileData : void 0
2176
+ }
2177
+ });
2178
+ };
2179
+ // ─── Action polling ─────────────────────────────────────────────
2180
+ async pollActions() {
2181
+ const pages = [this.page, ...this.activePages];
2182
+ for (const page of pages) {
2183
+ try {
2184
+ if (page.isClosed()) continue;
2185
+ await this.flushPendingActions(page);
2186
+ } catch {
2187
+ }
2188
+ }
2189
+ }
2190
+ async flushPendingActions(page) {
2191
+ let pending = [];
2192
+ try {
2193
+ pending = await page.evaluate(`(function() {
2194
+ var w = window;
2195
+ var actions = w.__xb_pending_actions || [];
2196
+ w.__xb_pending_actions = [];
2197
+
2198
+ // Recursively flush pending actions from same-origin iframes (including nested)
2199
+ function flushIframes(doc) {
2200
+ try {
2201
+ var iframes = doc.querySelectorAll('iframe');
2202
+ for (var i = 0; i < iframes.length; i++) {
2203
+ try {
2204
+ var iframeWin = iframes[i].contentWindow;
2205
+ if (!iframeWin) continue;
2206
+ // Ensure action script is injected into iframe
2207
+ if (!iframeWin.__xb_action_script_injected) {
2208
+ iframeWin.__xb_action_script_injected = true;
2209
+ try { delete iframeWin.__xb_action_signal; } catch(e) {}
2210
+ try { iframeWin.eval(w.__xb_action_script_src); } catch(e) {}
2211
+ }
2212
+ var iframeActions = iframeWin.__xb_pending_actions;
2213
+ if (Array.isArray(iframeActions) && iframeActions.length > 0) {
2214
+ // Get iframe position to offset coordinates
2215
+ var rect = iframes[i].getBoundingClientRect();
2216
+ for (var j = 0; j < iframeActions.length; j++) {
2217
+ var act = iframeActions[j];
2218
+ // Offset coordinates from iframe-relative to page-relative
2219
+ if (act.x != null) act.x = act.x + rect.left;
2220
+ if (act.y != null) act.y = act.y + rect.top;
2221
+ // Tag the action as originating from an iframe
2222
+ act.iframeSrc = iframes[i].src || '';
2223
+ actions.push(act);
2224
+ }
2225
+ iframeWin.__xb_pending_actions = [];
2226
+ }
2227
+ // Recurse into nested iframes
2228
+ try { flushIframes(iframeWin.document); } catch(e) {}
2229
+ } catch(e) {}
2230
+ }
2231
+ } catch(e) {}
2232
+ }
2233
+
2234
+ flushIframes(document);
2235
+ return actions;
2236
+ })()`);
2237
+ } catch {
2238
+ return;
2239
+ }
2240
+ try {
2241
+ const currentUrl = page.url();
2242
+ const normalize = (u) => u.replace(/\/+$/, "");
2243
+ if (currentUrl && currentUrl !== "about:blank" && normalize(currentUrl) !== normalize(this.lastKnownUrl)) {
2244
+ const hasNav = this.actions.slice(-3).some(
2245
+ (a) => (a.type === "navigation" || a.type === "goto") && normalize(a.url || "") === normalize(currentUrl)
2246
+ );
2247
+ if (!hasNav) {
2248
+ this.actionCounter++;
2249
+ this.actions.push({
2250
+ id: this.actionCounter,
2251
+ type: "navigation",
2252
+ timestamp: Date.now(),
2253
+ url: currentUrl,
2254
+ pageTitle: "",
2255
+ element: void 0
2256
+ });
2257
+ }
2258
+ this.lastKnownUrl = currentUrl;
2259
+ }
2260
+ } catch {
2261
+ }
2262
+ for (const raw of pending) {
2263
+ if (raw.ts <= this.lastActionTs) continue;
2264
+ if (this.cdpActionDedup && Date.now() < this.cdpActionDedup.until) {
2265
+ const dedup = this.cdpActionDedup;
2266
+ const typeMatch = raw.type === dedup.type;
2267
+ const valueMatch = !dedup.value || raw.value === dedup.value;
2268
+ const selectorMatch = !dedup.selector || raw.element?.selector && (raw.element.selector === dedup.selector || raw.element.selector.endsWith(" " + dedup.selector) || dedup.selector.endsWith(" " + raw.element.selector));
2269
+ if (typeMatch && valueMatch && selectorMatch) {
2270
+ continue;
2271
+ }
2272
+ }
2273
+ this.actionCounter++;
2274
+ let clickContext;
2275
+ if (raw.type === "click" && raw.x !== void 0 && raw.y !== void 0) {
2276
+ clickContext = await this.captureClickContext(page, raw.x, raw.y);
2277
+ }
2278
+ this.actions.push({
2279
+ id: this.actionCounter,
2280
+ type: raw.type,
2281
+ timestamp: raw.ts,
2282
+ url: raw.url || page.url(),
2283
+ pageTitle: raw.title || "",
2284
+ element: raw.element,
2285
+ value: raw.value,
2286
+ key: raw.key,
2287
+ x: raw.x,
2288
+ y: raw.y,
2289
+ scrollX: raw.scrollX,
2290
+ scrollY: raw.scrollY,
2291
+ clickContext,
2292
+ files: raw.files
2293
+ });
2294
+ this.lastActionTs = raw.ts;
2295
+ if (raw.type === "click" || raw.type === "navigate" || raw.type === "submit") {
2296
+ const detected = await this.detectCheckpoints(page);
2297
+ for (const cp of detected) {
2298
+ cp.relatedActionId = this.actionCounter;
2299
+ this.checkpoints.push(cp);
2300
+ }
2301
+ }
2302
+ }
2303
+ }
2304
+ /**
2305
+ * After a click, wait 300ms then scan for popover/dropdown/menu elements
2306
+ * near the click position. This runs server-side to avoid race conditions
2307
+ * with the client-side poll interval.
2308
+ */
2309
+ async captureClickContext(page, cx, cy) {
2310
+ await new Promise((r) => setTimeout(r, 300));
2311
+ try {
2312
+ const ctx = await page.evaluate(`
2313
+ (function() {
2314
+ var cx = ${cx}, cy = ${cy};
2315
+ var POPOVER_SELECTORS = [
2316
+ '[role="menu"]','[role="listbox"]','[role="dialog"]','[role="tooltip"]','[role="popover"]',
2317
+ '[role="combobox"]','[role="tree"]','[role="grid"]',
2318
+ '.popover','.popup','.dropdown','.menu','.modal','.tooltip','.panel',
2319
+ '[class*="popover"]','[class*="popup"]','[class*="dropdown"]','[class*="menu"]',
2320
+ '[class*="tooltip"]','[class*="modal"]','[class*="panel"]','[class*="overlay"]','[class*="sheet"]',
2321
+ '[data-popup]','[data-dropdown]','[data-menu]','[data-popover"]',
2322
+ '.semi-dropdown','.semi-popover','.semi-modal',
2323
+ '.ant-dropdown','.ant-popover','.ant-modal',
2324
+ '.el-dropdown','.el-popover','.el-dialog',
2325
+ '.t-dropdown','.t-popup','.t-dialog'
2326
+ ];
2327
+
2328
+ function isNear(el, x, y, range) {
2329
+ try {
2330
+ var r = el.getBoundingClientRect();
2331
+ if (!r || r.width === 0 || r.height === 0) return false;
2332
+ return !(r.left > x + range || r.right < x - range || r.top > y + range || r.bottom < y - range);
2333
+ } catch(e) { return false; }
2334
+ }
2335
+
2336
+ var result = { appeared: [], disappeared: [], stateChanges: [] };
2337
+ var seenElements = new Set();
2338
+
2339
+ for (var si = 0; si < POPOVER_SELECTORS.length; si++) {
2340
+ try {
2341
+ var els = document.querySelectorAll(POPOVER_SELECTORS[si]);
2342
+ for (var j = 0; j < els.length; j++) {
2343
+ var el = els[j];
2344
+ if (seenElements.has(el)) continue;
2345
+ if (!isNear(el, cx, cy, 300)) continue;
2346
+ var rect = el.getBoundingClientRect();
2347
+ if (rect.width === 0 || rect.height === 0) continue;
2348
+ seenElements.add(el);
2349
+
2350
+ var items = [];
2351
+ var children = el.querySelectorAll('a,button,[role="menuitem"],[role="option"],[role="treeitem"],li,div[class*="item"]');
2352
+ var seenItemTexts = new Set();
2353
+ for (var k = 0; k < Math.min(children.length, 30); k++) {
2354
+ var child = children[k];
2355
+ var childText = (child.textContent || '').trim().substring(0, 60);
2356
+ if (!childText || seenItemTexts.has(childText)) continue;
2357
+ seenItemTexts.add(childText);
2358
+ var ci = { text: childText };
2359
+ try { if (child.disabled || child.getAttribute('aria-disabled') === 'true') ci.disabled = true; } catch(e) {}
2360
+ try { if (child.tagName) ci.tag = child.tagName.toLowerCase(); } catch(e) {}
2361
+ try { if (child.href) ci.href = child.href.substring(0, 80); } catch(e) {}
2362
+ items.push(ci);
2363
+ }
2364
+
2365
+ result.appeared.push({
2366
+ tag: el.tagName.toLowerCase(),
2367
+ selector: el.id ? '#' + el.id : undefined,
2368
+ role: el.getAttribute('role'),
2369
+ text: (el.textContent || '').trim().substring(0, 100),
2370
+ rect: { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) },
2371
+ items: items
2372
+ });
2373
+ }
2374
+ } catch(e) { /* skip invalid selectors */ }
2375
+ }
2376
+
2377
+ var allInteractive = document.querySelectorAll('[aria-expanded],[disabled],[aria-disabled],[aria-selected],[data-state]');
2378
+ for (var i = 0; i < allInteractive.length; i++) {
2379
+ var el2 = allInteractive[i];
2380
+ if (!isNear(el2, cx, cy, 200)) continue;
2381
+ var info = { tag: el2.tagName.toLowerCase(), text: (el2.textContent || '').trim().substring(0, 60) };
2382
+ try { if (el2.id) info.id = el2.id; } catch(e) {}
2383
+ try { if (el2.getAttribute('aria-expanded')) info.ariaExpanded = el2.getAttribute('aria-expanded'); } catch(e) {}
2384
+ try { if (el2.disabled || el2.getAttribute('aria-disabled') === 'true') info.disabled = true; } catch(e) {}
2385
+ try { if (el2.getAttribute('aria-selected')) info.ariaSelected = el2.getAttribute('aria-selected'); } catch(e) {}
2386
+ try { if (el2.getAttribute('data-state')) info.dataState = el2.getAttribute('data-state'); } catch(e) {}
2387
+ result.stateChanges.push(info);
2388
+ }
2389
+
2390
+ return result;
2391
+ })()
2392
+ `);
2393
+ if (ctx.appeared.length > 0 || ctx.stateChanges.length > 0) {
2394
+ return ctx;
2395
+ }
2396
+ } catch {
2397
+ }
2398
+ return void 0;
2399
+ }
2400
+ async detectCheckpoints(page) {
2401
+ const CHECKPOINT_RULES = [
2402
+ { type: "captcha", selectors: ['img[src*="captcha"]', 'img[src*="verify"]', '[class*="captcha"]', '[id*="captcha"]', "#captcha", ".captcha"] },
2403
+ { type: "slider", selectors: ['[class*="slider"]', '[class*="drag-verify"]', '[class*="slide-verify"]'] },
2404
+ { type: "login", selectors: ['input[type="password"]'] },
2405
+ { type: "iframe", selectors: ['iframe[src*="captcha"]', 'iframe[src*="verify"]', 'iframe[src*="recaptcha"]', 'iframe[title*="captcha"]'] }
2406
+ ];
2407
+ try {
2408
+ const found = await page.evaluate((rules) => {
2409
+ const results = [];
2410
+ for (const rule of rules) {
2411
+ for (const sel of rule.selectors) {
2412
+ try {
2413
+ const el = document.querySelector(sel);
2414
+ if (el) {
2415
+ const rect = el.getBoundingClientRect();
2416
+ if (rect.width > 0 && rect.height > 0) {
2417
+ results.push({
2418
+ type: rule.type,
2419
+ selector: sel,
2420
+ text: (el.textContent || "").substring(0, 60)
2421
+ });
2422
+ }
2423
+ }
2424
+ } catch {
2425
+ }
2426
+ }
2427
+ }
2428
+ return results;
2429
+ }, CHECKPOINT_RULES);
2430
+ const entries = [];
2431
+ for (const item of found) {
2432
+ this.checkpointCounter++;
2433
+ const hints = {
2434
+ captcha: "Captcha verification detected",
2435
+ slider: "Slider verification detected",
2436
+ login: "Login form detected (password field)",
2437
+ iframe: "Verification iframe detected"
2438
+ };
2439
+ entries.push({
2440
+ id: this.checkpointCounter,
2441
+ type: item.type,
2442
+ timestamp: Date.now(),
2443
+ url: page.url(),
2444
+ pageTitle: await page.title().catch(() => ""),
2445
+ hint: hints[item.type] || item.type,
2446
+ selector: item.selector,
2447
+ source: "auto",
2448
+ context: { matchedSelector: item.selector, elementText: item.text }
2449
+ });
2450
+ }
2451
+ return entries;
2452
+ } catch {
2453
+ return [];
2454
+ }
2455
+ }
2456
+ // ─── Periodic disk flush ────────────────────────────────────────
2457
+ flushToDisk() {
2458
+ const data = this.buildData();
2459
+ try {
2460
+ writeFileSync2(
2461
+ join2(this.recordingsDir, "recording.json"),
2462
+ JSON.stringify(data, null, 2),
2463
+ "utf-8"
2464
+ );
2465
+ } catch {
2466
+ }
2467
+ }
2468
+ writeFinalOutput(data, summary) {
2469
+ mkdirSync2(this.recordingsDir, { recursive: true });
2470
+ writeFileSync2(join2(this.recordingsDir, "recording.json"), JSON.stringify(data, null, 2), "utf-8");
2471
+ writeFileSync2(join2(this.recordingsDir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
2472
+ writeFileSync2(join2(this.recordingsDir, "summary.md"), this.buildMarkdownSummary(data, summary), "utf-8");
2473
+ }
2474
+ buildData() {
2475
+ return {
2476
+ startUrl: this.startUrl,
2477
+ sessionName: this.sessionName,
2478
+ startedAt: new Date(this.startedAt).toISOString(),
2479
+ actions: [...this.actions],
2480
+ network: [...this.network],
2481
+ contextChanges: [...this.contextChanges],
2482
+ checkpoints: [...this.checkpoints]
2483
+ };
2484
+ }
2485
+ // ─── Summary builder with ref compression + input→network matching ──
2486
+ buildSummary(data) {
2487
+ const POST_WINDOW = 5e3;
2488
+ const MERGE_WINDOW = 2e3;
2489
+ const steps = [];
2490
+ const selectorToRef = /* @__PURE__ */ new Map();
2491
+ const elements = {};
2492
+ let refCounter = 0;
2493
+ function getRef(action) {
2494
+ const sel = action.element?.selector || action.element?.tag || "_none";
2495
+ if (selectorToRef.has(sel)) return selectorToRef.get(sel);
2496
+ refCounter++;
2497
+ const ref = "e" + refCounter;
2498
+ selectorToRef.set(sel, ref);
2499
+ if (action.element) {
2500
+ elements[ref] = {
2501
+ selector: action.element.selector || action.element.tag,
2502
+ tag: action.element.tag,
2503
+ text: action.element.text,
2504
+ role: action.element.role,
2505
+ type: action.element.type,
2506
+ placeholder: action.element.placeholder,
2507
+ ariaLabel: action.element.ariaLabel,
2508
+ href: action.element.href
2509
+ };
2510
+ } else {
2511
+ elements[ref] = { selector: "_none", tag: "_", text: "" };
2512
+ }
2513
+ return ref;
2514
+ }
2515
+ const isNoiseNetwork = (n) => {
2516
+ const url = n.url || "";
2517
+ const path = n.path || "";
2518
+ const rt = n.resourceType || "";
2519
+ if (["image", "stylesheet", "font", "manifest", "other"].includes(rt)) return true;
2520
+ if (n.status === 0) return true;
2521
+ if (/\/ztbox|\/mwb2\.gif|\/hmslog|\/log\.gif|\/tongji|hm\.baidu|clickstream|\/actionlog|\/collect\?|\/track|\/beacon/i.test(url)) return true;
2522
+ if (/\/favicon\.ico|\/robots\.txt/i.test(path)) return true;
2523
+ return false;
2524
+ };
2525
+ const meaningfulNetwork = data.network.filter((n) => !isNoiseNetwork(n));
2526
+ const filtered = data.actions.filter((a) => a.type !== "scroll");
2527
+ const groups = [];
2528
+ let current = null;
2529
+ for (const action of filtered) {
2530
+ const sameElement = current && current.primary.element?.selector && current.primary.element.selector === action.element?.selector && action.timestamp - current.primary.timestamp < MERGE_WINDOW;
2531
+ const isInputLike = action.type === "input" || action.type === "keydown" || action.type === "change";
2532
+ if (current && (sameElement || isInputLike && current.actions.some((a) => a.type === "input" || a.type === "click") && action.timestamp - current.primary.timestamp < MERGE_WINDOW)) {
2533
+ current.actions.push(action);
2534
+ if (action.type === "input") current.primary = action;
2535
+ } else {
2536
+ current = { actions: [action], primary: action };
2537
+ groups.push(current);
2538
+ }
2539
+ }
2540
+ for (const group of groups) {
2541
+ const primary = group.primary;
2542
+ const tsStart = Math.min(...group.actions.map((a) => a.timestamp));
2543
+ const tsEnd = Math.max(...group.actions.map((a) => a.timestamp));
2544
+ const inputAction = group.actions.find((a) => a.type === "input");
2545
+ const nearbyNetwork = meaningfulNetwork.filter(
2546
+ (n) => n.timestamp >= tsStart - 500 && n.timestamp <= tsEnd + POST_WINDOW
2547
+ );
2548
+ const nearbyContext = data.contextChanges.filter(
2549
+ (c) => c.timestamp >= tsStart - 500 && c.timestamp <= tsEnd + POST_WINDOW
2550
+ );
2551
+ const matchedInputs = inputAction ? this.matchActionToNetwork(inputAction, nearbyNetwork) : [];
2552
+ const clickMatches = primary.type === "click" && primary.element?.text ? this.matchActionToNetwork(primary, nearbyNetwork) : [];
2553
+ steps.push({
2554
+ step: steps.length + 1,
2555
+ ref: getRef(primary),
2556
+ action: primary,
2557
+ network: nearbyNetwork.map((n) => ({
2558
+ ...n,
2559
+ responseBody: n.responseBody && JSON.stringify(n.responseBody).length > 1e3 ? "[truncated, " + JSON.stringify(n.responseBody).length + " bytes]" : n.responseBody
2560
+ })),
2561
+ contextChanges: nearbyContext,
2562
+ matchedInputs: [...matchedInputs, ...clickMatches]
2563
+ });
2564
+ }
2565
+ return {
2566
+ startUrl: data.startUrl,
2567
+ recordedAt: new Date(this.startedAt).toISOString(),
2568
+ durationMs: Date.now() - this.startedAt,
2569
+ totalActions: data.actions.length,
2570
+ totalNetworkRequests: meaningfulNetwork.length,
2571
+ steps,
2572
+ elements,
2573
+ checkpoints: data.checkpoints
2574
+ };
2575
+ }
2576
+ matchActionToNetwork(action, nearbyNetwork) {
2577
+ const matches = [];
2578
+ const searchValue = (action.value || action.element?.text || "").trim();
2579
+ if (!searchValue || searchValue.length < 2) return matches;
2580
+ for (const netEntry of nearbyNetwork) {
2581
+ if (netEntry.url.includes(encodeURIComponent(searchValue)) || netEntry.url.includes(searchValue)) {
2582
+ matches.push({ inputValue: searchValue, networkId: netEntry.id, paramName: "url" });
2583
+ }
2584
+ if (netEntry.requestBody && typeof netEntry.requestBody === "object") {
2585
+ this.searchObjectForValue(
2586
+ netEntry.requestBody,
2587
+ searchValue,
2588
+ netEntry.id,
2589
+ "",
2590
+ matches
2591
+ );
2592
+ }
2593
+ }
2594
+ return matches;
2595
+ }
2596
+ searchObjectForValue(obj, targetValue, networkId, prefix, results) {
2597
+ for (const [key, value] of Object.entries(obj)) {
2598
+ const fullKey = prefix ? `${prefix}.${key}` : key;
2599
+ if (typeof value === "string" && value.includes(targetValue)) {
2600
+ results.push({ inputValue: targetValue, networkId, paramName: fullKey });
2601
+ } else if (typeof value === "object" && value !== null) {
2602
+ this.searchObjectForValue(value, targetValue, networkId, fullKey, results);
2603
+ }
2604
+ }
2605
+ }
2606
+ buildMarkdownSummary(_data, summary) {
2607
+ const lines = [];
2608
+ const durSec = Math.round(summary.durationMs / 1e3);
2609
+ lines.push("# Recording Summary");
2610
+ lines.push("");
2611
+ lines.push(`- **URL**: ${summary.startUrl}`);
2612
+ lines.push(`- **Recorded**: ${summary.recordedAt}`);
2613
+ lines.push(`- **Duration**: ${durSec}s`);
2614
+ lines.push(`- **Steps**: ${summary.totalActions} actions, ${summary.totalNetworkRequests} network requests`);
2615
+ if (summary.checkpoints.length > 0) {
2616
+ const cpTypes = summary.checkpoints.map((c) => c.type);
2617
+ lines.push(`- **Checkpoints**: ${summary.checkpoints.length} (${[...new Set(cpTypes)].join(", ")})`);
2618
+ } else {
2619
+ lines.push("- **Checkpoints**: 0");
2620
+ }
2621
+ const checkpointSteps = /* @__PURE__ */ new Map();
2622
+ for (const cp of summary.checkpoints) {
2623
+ if (cp.relatedActionId != null) {
2624
+ for (const step of summary.steps) {
2625
+ if (step.action.id === cp.relatedActionId) {
2626
+ checkpointSteps.set(step.step, cp);
2627
+ break;
2628
+ }
2629
+ }
2630
+ }
2631
+ }
2632
+ lines.push("");
2633
+ lines.push("## Steps");
2634
+ lines.push("");
2635
+ for (const step of summary.steps) {
2636
+ const a = step.action;
2637
+ const el = a.element;
2638
+ const cp = checkpointSteps.get(step.step);
2639
+ if (cp) {
2640
+ lines.push(`### Step ${step.step}: \u26A0\uFE0F CHECKPOINT \u2014 ${cp.hint}`);
2641
+ lines.push(`- **Type**: ${cp.type} (${cp.source})`);
2642
+ lines.push(`- **Hint**: ${cp.hint}`);
2643
+ if (cp.selector) lines.push(`- **Selector**: \`${cp.selector}\``);
2644
+ lines.push(`- **Action needed**: Human intervention required before continuing`);
2645
+ } else {
2646
+ const title = describeActionTitle(a);
2647
+ lines.push(`### Step ${step.step}: ${title}`);
2648
+ }
2649
+ if (el) {
2650
+ const parts = [`\`${el.selector || el.tag}\``];
2651
+ if (el.text) parts.push(`"${el.text.substring(0, 60)}"`);
2652
+ parts.push(`(${el.tag})`);
2653
+ if (el.type) parts.push(`type=${el.type}`);
2654
+ lines.push(`- **Element**: ${parts.join(" ")}`);
2655
+ }
2656
+ if (a.value != null && a.type === "input") {
2657
+ lines.push(`- **Value**: "${a.value.substring(0, 100)}"`);
2658
+ }
2659
+ if (step.network.length > 0) {
2660
+ const netDescs = step.network.map((n) => {
2661
+ let desc = `${n.method} ${n.path}`;
2662
+ if (n.status) desc += ` \u2192 ${n.status}`;
2663
+ if (n.responseSize > 0) desc += ` (${formatBytes(n.responseSize)})`;
2664
+ return desc;
2665
+ });
2666
+ lines.push(`- **Network**: ${netDescs.join(", ")}`);
2667
+ for (const n of step.network) {
2668
+ if (n.requestBody && typeof n.requestBody === "object") {
2669
+ const bodyStr = JSON.stringify(n.requestBody);
2670
+ if (bodyStr.length <= 300) {
2671
+ lines.push(` - \`${n.method} ${n.path}\` body: \`${bodyStr}\``);
2672
+ } else {
2673
+ lines.push(` - \`${n.method} ${n.path}\` body: \`${bodyStr.substring(0, 300)}...\` (${bodyStr.length} bytes)`);
2674
+ }
2675
+ }
2676
+ }
2677
+ } else {
2678
+ lines.push("- **Network**: none");
2679
+ }
2680
+ if (step.matchedInputs.length > 0) {
2681
+ for (const m of step.matchedInputs) {
2682
+ lines.push(`- **Input matched**: "${m.inputValue}" \u2192 ${m.paramName} (network #${m.networkId})`);
2683
+ }
2684
+ }
2685
+ for (const ctx of step.contextChanges) {
2686
+ if (ctx.type === "navigate") {
2687
+ lines.push(`- **Navigate**: \u2192 ${ctx.url}`);
2688
+ } else if (ctx.type === "new_tab") {
2689
+ lines.push(`- **New tab**: ${ctx.url}`);
2690
+ }
2691
+ }
2692
+ if (a.clickContext) {
2693
+ if (a.clickContext.appeared?.length > 0) {
2694
+ for (const popup of a.clickContext.appeared) {
2695
+ const roleStr = popup.role ? ` [${popup.role}]` : "";
2696
+ lines.push(`- **Popup**: <${popup.tag}${roleStr}> "${(popup.text || "").substring(0, 60)}"`);
2697
+ if (popup.items?.length > 0) {
2698
+ const itemStrs = popup.items.slice(0, 8).map((i) => {
2699
+ const dis = i.disabled ? " [disabled]" : "";
2700
+ return `"${i.text}"${dis}`;
2701
+ });
2702
+ let itemLine = ` - Items: ${itemStrs.join(", ")}`;
2703
+ if (popup.items.length > 8) itemLine += ` ... +${popup.items.length - 8} more`;
2704
+ lines.push(itemLine);
2705
+ }
2706
+ }
2707
+ }
2708
+ if (a.clickContext.stateChanges?.length > 0) {
2709
+ for (const sc of a.clickContext.stateChanges) {
2710
+ const parts = [];
2711
+ if (sc.ariaExpanded !== void 0) parts.push(`expanded=${sc.ariaExpanded}`);
2712
+ if (sc.disabled) parts.push("disabled");
2713
+ if (sc.ariaSelected !== void 0) parts.push(`selected=${sc.ariaSelected}`);
2714
+ if (sc.dataState) parts.push(`state=${sc.dataState}`);
2715
+ if (parts.length > 0) {
2716
+ lines.push(`- **State**: <${sc.tag}> "${(sc.text || "").substring(0, 30)}" ${parts.join(", ")}`);
2717
+ }
2718
+ }
2719
+ }
2720
+ }
2721
+ lines.push("");
2722
+ }
2723
+ const allNetwork = summary.steps.flatMap((s) => s.network);
2724
+ if (allNetwork.length > 0) {
2725
+ lines.push("## Network Timeline");
2726
+ lines.push("");
2727
+ allNetwork.forEach((n, i) => {
2728
+ let line = `${i + 1}. ${n.method} ${n.path}`;
2729
+ if (n.status) line += ` \u2192 ${n.status}`;
2730
+ if (n.requestBody && typeof n.requestBody === "object") {
2731
+ const bodyStr = JSON.stringify(n.requestBody);
2732
+ if (bodyStr.length <= 150) {
2733
+ line += ` ${bodyStr}`;
2734
+ }
2735
+ }
2736
+ if (n.responseSize > 0) line += ` (${formatBytes(n.responseSize)})`;
2737
+ lines.push(line);
2738
+ });
2739
+ lines.push("");
2740
+ }
2741
+ const orphanCheckpoints = summary.checkpoints.filter(
2742
+ (cp) => cp.relatedActionId == null || !checkpointSteps.has(
2743
+ summary.steps.find((s) => s.action.id === cp.relatedActionId)?.step ?? -1
2744
+ )
2745
+ );
2746
+ if (orphanCheckpoints.length > 0) {
2747
+ lines.push("## Unresolved Checkpoints");
2748
+ lines.push("");
2749
+ for (const cp of orphanCheckpoints) {
2750
+ const src = cp.source === "auto" ? "[auto]" : "[manual]";
2751
+ lines.push(`- ${src} **${cp.type}**: ${cp.hint}`);
2752
+ if (cp.selector) lines.push(` - Selector: \`${cp.selector}\``);
2753
+ }
2754
+ lines.push("");
2755
+ }
2756
+ return lines.join("\n");
2757
+ }
2758
+ static readMarkdownSummary(sessionName) {
2759
+ const path = join2(_SessionRecorder.getRecordingsDir(sessionName), "summary.md");
2760
+ try {
2761
+ return readFileSync2(path, "utf-8");
2762
+ } catch {
2763
+ return null;
2764
+ }
2765
+ }
2766
+ };
2767
+ function describeActionTitle(a) {
2768
+ const el = a.element;
2769
+ const elText = el?.text ? `"${el.text.substring(0, 40)}"` : "";
2770
+ const elTag = el?.tag ? `<${el.tag}>` : "";
2771
+ switch (a.type) {
2772
+ case "click":
2773
+ return `Click ${elText || elTag} button`.replace(/ +/g, " ").trim();
2774
+ case "input":
2775
+ return `Input "${(a.value || "").substring(0, 50)}" in ${elText || elTag || "field"}`.replace(/ +/g, " ").trim();
2776
+ case "change":
2777
+ return `Change ${elText || elTag} to "${(a.value || "").substring(0, 30)}"`;
2778
+ case "keydown":
2779
+ return `Press ${a.key || "key"} on ${elText || elTag || "element"}`;
2780
+ case "submit":
2781
+ return `Submit ${elText || elTag || "form"}`;
2782
+ default:
2783
+ return `${a.type} ${elText || elTag}`.trim() || a.type;
2784
+ }
2785
+ }
2786
+ function formatBytes(bytes) {
2787
+ if (bytes < 1024) return `${bytes}B`;
2788
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
2789
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
2790
+ }
2791
+
2792
+ export {
2793
+ SessionRecorder
2794
+ };