libretto 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/cli/commands/init.js +4 -21
- package/dist/cli/core/ai-config.js +12 -2
- package/dist/cli/core/browser.js +75 -8
- package/dist/cli/core/session-telemetry.js +429 -172
- package/dist/cli/core/telemetry.js +10 -2
- package/dist/shared/condense-dom/condense-dom.js +11 -56
- package/dist/shared/dom-semantics.d.ts +8 -0
- package/dist/shared/dom-semantics.js +69 -0
- package/dist/shared/run/browser.js +40 -1
- package/dist/shared/visualization/ghost-cursor.js +17 -4
- package/package.json +1 -1
- package/skills/libretto/SKILL.md +52 -38
- package/skills/libretto/references/action-logs.md +101 -0
- package/skills/libretto/references/auth-profiles.md +1 -2
- package/skills/libretto/references/code-generation-rules.md +4 -2
- package/skills/libretto/references/pages-and-page-targeting.md +1 -1
- package/src/cli/commands/init.ts +5 -24
- package/src/cli/core/ai-config.ts +12 -1
- package/src/cli/core/browser.ts +82 -8
- package/src/cli/core/session-telemetry.ts +431 -190
- package/src/cli/core/telemetry.ts +23 -1
- package/src/shared/condense-dom/condense-dom.ts +12 -64
- package/src/shared/dom-semantics.ts +68 -0
- package/src/shared/run/browser.ts +53 -0
- package/src/shared/visualization/ghost-cursor.ts +22 -4
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { BrowserContext, Page } from "playwright";
|
|
2
|
+
import {
|
|
3
|
+
filterSemanticClasses,
|
|
4
|
+
INTERACTIVE_ROLE_NAMES,
|
|
5
|
+
INTERACTIVE_TAG_NAMES,
|
|
6
|
+
isObfuscatedClass,
|
|
7
|
+
TEST_ATTRIBUTE_NAMES,
|
|
8
|
+
TRUSTED_ATTRIBUTE_NAMES,
|
|
9
|
+
} from "../../shared/dom-semantics.js";
|
|
2
10
|
|
|
3
11
|
type TelemetryEntry = Record<string, unknown>;
|
|
4
12
|
|
|
@@ -186,6 +194,428 @@ export async function installSessionTelemetry(
|
|
|
186
194
|
return locator;
|
|
187
195
|
};
|
|
188
196
|
|
|
197
|
+
const createUserDomTrackingInitScript = (): string => {
|
|
198
|
+
const selectorAttributeNames = [
|
|
199
|
+
"id",
|
|
200
|
+
...TEST_ATTRIBUTE_NAMES,
|
|
201
|
+
"name",
|
|
202
|
+
"for",
|
|
203
|
+
"role",
|
|
204
|
+
"aria-label",
|
|
205
|
+
"title",
|
|
206
|
+
"placeholder",
|
|
207
|
+
"alt",
|
|
208
|
+
"href",
|
|
209
|
+
"type",
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
return `
|
|
213
|
+
(() => {
|
|
214
|
+
if (window.__btDomListenersInstalled) return;
|
|
215
|
+
window.__btDomListenersInstalled = true;
|
|
216
|
+
|
|
217
|
+
const TEST_ATTRS = new Set(${JSON.stringify([...TEST_ATTRIBUTE_NAMES])});
|
|
218
|
+
const TRUSTED_ATTRS = new Set(${JSON.stringify([...TRUSTED_ATTRIBUTE_NAMES])});
|
|
219
|
+
const INTERACTIVE_TAGS = new Set(${JSON.stringify([...INTERACTIVE_TAG_NAMES])});
|
|
220
|
+
const INTERACTIVE_ROLES = new Set(${JSON.stringify([...INTERACTIVE_ROLE_NAMES])});
|
|
221
|
+
const SELECTOR_ATTRS = ${JSON.stringify(selectorAttributeNames)};
|
|
222
|
+
|
|
223
|
+
${filterSemanticClasses.toString()}
|
|
224
|
+
${isObfuscatedClass.toString()}
|
|
225
|
+
|
|
226
|
+
const normalizeWhitespace = (value) =>
|
|
227
|
+
String(value || "").replace(/\\s+/g, " ").trim();
|
|
228
|
+
|
|
229
|
+
const clipText = (value, limit = 120) => {
|
|
230
|
+
const normalized = normalizeWhitespace(value);
|
|
231
|
+
if (!normalized) return "";
|
|
232
|
+
return normalized.length > limit
|
|
233
|
+
? \`\${normalized.slice(0, limit - 1)}…\`
|
|
234
|
+
: normalized;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const cssEscape = (value) => {
|
|
238
|
+
if (globalThis.CSS && typeof globalThis.CSS.escape === "function") {
|
|
239
|
+
return globalThis.CSS.escape(String(value));
|
|
240
|
+
}
|
|
241
|
+
return String(value).replace(/[^a-zA-Z0-9_-]/g, (char) => {
|
|
242
|
+
const hex = char.charCodeAt(0).toString(16);
|
|
243
|
+
return \`\\\\\${hex} \`;
|
|
244
|
+
});
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const quoteAttrValue = (value) =>
|
|
248
|
+
String(value).replace(/\\\\/g, "\\\\\\\\").replace(/"/g, '\\\\"');
|
|
249
|
+
|
|
250
|
+
const isElementNode = (value) => value instanceof Element;
|
|
251
|
+
|
|
252
|
+
const isInteractiveElement = (el) => {
|
|
253
|
+
if (!el || !el.tagName) return false;
|
|
254
|
+
const tagName = el.tagName.toLowerCase();
|
|
255
|
+
if (INTERACTIVE_TAGS.has(tagName)) return true;
|
|
256
|
+
if (el.hasAttribute("tabindex") || el.hasAttribute("contenteditable")) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
const role = normalizeWhitespace(el.getAttribute("role")).toLowerCase();
|
|
260
|
+
return Boolean(role) && INTERACTIVE_ROLES.has(role);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const getFilteredClassSelector = (el) => {
|
|
264
|
+
const className =
|
|
265
|
+
typeof el.className === "string"
|
|
266
|
+
? filterSemanticClasses(el.className)
|
|
267
|
+
: "";
|
|
268
|
+
const classes = className
|
|
269
|
+
.split(/\\s+/)
|
|
270
|
+
.filter(Boolean)
|
|
271
|
+
.slice(0, 2);
|
|
272
|
+
if (classes.length === 0) return "";
|
|
273
|
+
return classes.map((cls) => \`.\${cssEscape(cls)}\`).join("");
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const getMeaningfulAttrValue = (el, attrName) => {
|
|
277
|
+
if (!el.hasAttribute(attrName)) return "";
|
|
278
|
+
const value = clipText(el.getAttribute(attrName) || "", 80);
|
|
279
|
+
if (!value) return "";
|
|
280
|
+
if (attrName === "href" && value.startsWith("javascript:")) return "";
|
|
281
|
+
return value;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const buildSelectorForElement = (el) => {
|
|
285
|
+
if (!isElementNode(el)) return "";
|
|
286
|
+
const tagName = el.tagName.toLowerCase();
|
|
287
|
+
const id = clipText(el.id || "", 80);
|
|
288
|
+
if (id) {
|
|
289
|
+
return \`\${tagName}#\${cssEscape(id)}\`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
for (const attrName of SELECTOR_ATTRS) {
|
|
293
|
+
if (attrName === "id") continue;
|
|
294
|
+
const value = getMeaningfulAttrValue(el, attrName);
|
|
295
|
+
if (!value) continue;
|
|
296
|
+
if (attrName.startsWith("data-")) {
|
|
297
|
+
return \`\${tagName}[\${attrName}="\${quoteAttrValue(value)}"]\`;
|
|
298
|
+
}
|
|
299
|
+
return \`\${tagName}[\${attrName}="\${quoteAttrValue(value)}"]\`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const classSelector = getFilteredClassSelector(el);
|
|
303
|
+
if (classSelector) {
|
|
304
|
+
return \`\${tagName}\${classSelector}\`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return tagName;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const getElementSummary = (el) => {
|
|
311
|
+
if (!isElementNode(el)) return "";
|
|
312
|
+
const selector = buildSelectorForElement(el);
|
|
313
|
+
const role = clipText(el.getAttribute("role") || "", 40);
|
|
314
|
+
const text = getElementText(el);
|
|
315
|
+
const suffix = [];
|
|
316
|
+
if (role) suffix.push(\`role=\${role}\`);
|
|
317
|
+
if (text) suffix.push(\`text="\${text}"\`);
|
|
318
|
+
return suffix.length > 0 ? \`\${selector} [\${suffix.join(", ")}]\` : selector;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const getElementText = (el) => {
|
|
322
|
+
if (!isElementNode(el)) return "";
|
|
323
|
+
const tagName = el.tagName.toLowerCase();
|
|
324
|
+
if (tagName === "input") {
|
|
325
|
+
const inputType = normalizeWhitespace(el.getAttribute("type")).toLowerCase();
|
|
326
|
+
if (inputType === "password") return "";
|
|
327
|
+
const value = clipText(el.value || el.getAttribute("value") || "", 80);
|
|
328
|
+
if (value) return value;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const attrCandidates = [
|
|
332
|
+
el.getAttribute("aria-label"),
|
|
333
|
+
el.getAttribute("title"),
|
|
334
|
+
el.getAttribute("placeholder"),
|
|
335
|
+
el.getAttribute("alt"),
|
|
336
|
+
el.textContent,
|
|
337
|
+
];
|
|
338
|
+
|
|
339
|
+
for (const candidate of attrCandidates) {
|
|
340
|
+
const text = clipText(candidate || "", 120);
|
|
341
|
+
if (text.length >= 2) return text;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return "";
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const isMeaningfulAncestor = (el) => {
|
|
348
|
+
if (!isElementNode(el)) return false;
|
|
349
|
+
const tagName = el.tagName.toLowerCase();
|
|
350
|
+
if (isInteractiveElement(el)) return true;
|
|
351
|
+
if (
|
|
352
|
+
[
|
|
353
|
+
"tr",
|
|
354
|
+
"li",
|
|
355
|
+
"td",
|
|
356
|
+
"th",
|
|
357
|
+
"label",
|
|
358
|
+
"section",
|
|
359
|
+
"article",
|
|
360
|
+
"dialog",
|
|
361
|
+
"fieldset",
|
|
362
|
+
"summary",
|
|
363
|
+
].includes(tagName)
|
|
364
|
+
) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
if (el.id) return true;
|
|
368
|
+
for (const attrName of TEST_ATTRS) {
|
|
369
|
+
if (el.hasAttribute(attrName)) return true;
|
|
370
|
+
}
|
|
371
|
+
const role = normalizeWhitespace(el.getAttribute("role")).toLowerCase();
|
|
372
|
+
return Boolean(role);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const hasStrongSelectorSignal = (el, selector) => {
|
|
376
|
+
if (!isElementNode(el)) return false;
|
|
377
|
+
if (!selector) return false;
|
|
378
|
+
const tagName = el.tagName.toLowerCase();
|
|
379
|
+
if (el.id) return true;
|
|
380
|
+
if (isInteractiveElement(el)) return true;
|
|
381
|
+
if (getFilteredClassSelector(el)) return true;
|
|
382
|
+
for (const attrName of SELECTOR_ATTRS) {
|
|
383
|
+
if (attrName === "id") continue;
|
|
384
|
+
if (getMeaningfulAttrValue(el, attrName)) return true;
|
|
385
|
+
}
|
|
386
|
+
return selector !== tagName;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const getAncestorSelectors = (target) => {
|
|
390
|
+
const selectors = [];
|
|
391
|
+
let current = isElementNode(target) ? target : null;
|
|
392
|
+
let depth = 0;
|
|
393
|
+
while (current && depth < 7) {
|
|
394
|
+
const selector = buildSelectorForElement(current);
|
|
395
|
+
if (
|
|
396
|
+
selector &&
|
|
397
|
+
!selectors.includes(selector) &&
|
|
398
|
+
(
|
|
399
|
+
(depth === 0 && hasStrongSelectorSignal(current, selector)) ||
|
|
400
|
+
isMeaningfulAncestor(current)
|
|
401
|
+
)
|
|
402
|
+
) {
|
|
403
|
+
selectors.push(selector);
|
|
404
|
+
}
|
|
405
|
+
current = current.parentElement;
|
|
406
|
+
depth += 1;
|
|
407
|
+
}
|
|
408
|
+
if (selectors.length === 0 && isElementNode(target)) {
|
|
409
|
+
selectors.push(buildSelectorForElement(target));
|
|
410
|
+
}
|
|
411
|
+
return selectors;
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const getNearbyText = (target) => {
|
|
415
|
+
let current = isElementNode(target) ? target : null;
|
|
416
|
+
let depth = 0;
|
|
417
|
+
while (current && depth < 6) {
|
|
418
|
+
const text = getElementText(current);
|
|
419
|
+
if (text.length >= 2) return text;
|
|
420
|
+
current = current.parentElement;
|
|
421
|
+
depth += 1;
|
|
422
|
+
}
|
|
423
|
+
return "";
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const getComposedPathSummary = (event) => {
|
|
427
|
+
const path = typeof event.composedPath === "function" ? event.composedPath() : [];
|
|
428
|
+
return path
|
|
429
|
+
.filter((entry) => isElementNode(entry))
|
|
430
|
+
.slice(0, 6)
|
|
431
|
+
.map((entry) => getElementSummary(entry));
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const getCoordinates = (event) => {
|
|
435
|
+
if (!(event instanceof MouseEvent)) return undefined;
|
|
436
|
+
return {
|
|
437
|
+
x: Math.round(event.clientX),
|
|
438
|
+
y: Math.round(event.clientY),
|
|
439
|
+
};
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const buildActionPayload = (event, action, extra = {}) => {
|
|
443
|
+
const target = isElementNode(event.target) ? event.target : null;
|
|
444
|
+
const ancestorSelectors = getAncestorSelectors(target);
|
|
445
|
+
const bestSemanticSelector = ancestorSelectors[0] || "";
|
|
446
|
+
const targetSelector = target ? buildSelectorForElement(target) : "";
|
|
447
|
+
return {
|
|
448
|
+
action,
|
|
449
|
+
bestSemanticSelector: bestSemanticSelector || undefined,
|
|
450
|
+
targetSelector: targetSelector || undefined,
|
|
451
|
+
ancestorSelectors: ancestorSelectors.length > 0 ? ancestorSelectors : undefined,
|
|
452
|
+
nearbyText: getNearbyText(target) || undefined,
|
|
453
|
+
composedPath: getComposedPathSummary(event),
|
|
454
|
+
coordinates: getCoordinates(event),
|
|
455
|
+
success: true,
|
|
456
|
+
...extra,
|
|
457
|
+
};
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
let clickTimer = null;
|
|
461
|
+
let pendingClick = null;
|
|
462
|
+
|
|
463
|
+
document.addEventListener(
|
|
464
|
+
"click",
|
|
465
|
+
(event) => {
|
|
466
|
+
if (window.__btApiActionInProgress) return;
|
|
467
|
+
const target = isElementNode(event.target) ? event.target : null;
|
|
468
|
+
if (target?.type === "checkbox") {
|
|
469
|
+
window.__btActionLog(
|
|
470
|
+
JSON.stringify(
|
|
471
|
+
buildActionPayload(event, target.checked ? "check" : "uncheck"),
|
|
472
|
+
),
|
|
473
|
+
);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
pendingClick = buildActionPayload(event, "click");
|
|
478
|
+
if (clickTimer) clearTimeout(clickTimer);
|
|
479
|
+
clickTimer = setTimeout(() => {
|
|
480
|
+
if (pendingClick) {
|
|
481
|
+
window.__btActionLog(JSON.stringify(pendingClick));
|
|
482
|
+
}
|
|
483
|
+
pendingClick = null;
|
|
484
|
+
clickTimer = null;
|
|
485
|
+
}, 200);
|
|
486
|
+
},
|
|
487
|
+
true,
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
document.addEventListener(
|
|
491
|
+
"dblclick",
|
|
492
|
+
(event) => {
|
|
493
|
+
if (window.__btApiActionInProgress) return;
|
|
494
|
+
if (clickTimer) {
|
|
495
|
+
clearTimeout(clickTimer);
|
|
496
|
+
clickTimer = null;
|
|
497
|
+
pendingClick = null;
|
|
498
|
+
}
|
|
499
|
+
window.__btActionLog(
|
|
500
|
+
JSON.stringify(buildActionPayload(event, "dblclick")),
|
|
501
|
+
);
|
|
502
|
+
},
|
|
503
|
+
true,
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
const inputTimers = new WeakMap();
|
|
507
|
+
document.addEventListener(
|
|
508
|
+
"input",
|
|
509
|
+
(event) => {
|
|
510
|
+
if (window.__btApiActionInProgress) return;
|
|
511
|
+
const target = isElementNode(event.target) ? event.target : null;
|
|
512
|
+
if (!target) return;
|
|
513
|
+
|
|
514
|
+
if (target.tagName === "SELECT") {
|
|
515
|
+
window.__btActionLog(
|
|
516
|
+
JSON.stringify(
|
|
517
|
+
buildActionPayload(event, "selectOption", {
|
|
518
|
+
value: target.value,
|
|
519
|
+
}),
|
|
520
|
+
),
|
|
521
|
+
);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const existing = inputTimers.get(target);
|
|
526
|
+
if (existing) clearTimeout(existing);
|
|
527
|
+
inputTimers.set(
|
|
528
|
+
target,
|
|
529
|
+
setTimeout(() => {
|
|
530
|
+
inputTimers.delete(target);
|
|
531
|
+
window.__btActionLog(
|
|
532
|
+
JSON.stringify(
|
|
533
|
+
buildActionPayload(event, "fill", {
|
|
534
|
+
value: String(target.value || "").slice(0, 100),
|
|
535
|
+
}),
|
|
536
|
+
),
|
|
537
|
+
);
|
|
538
|
+
}, 500),
|
|
539
|
+
);
|
|
540
|
+
},
|
|
541
|
+
true,
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
const specialKeys = new Set([
|
|
545
|
+
"Enter",
|
|
546
|
+
"Escape",
|
|
547
|
+
"Tab",
|
|
548
|
+
"Backspace",
|
|
549
|
+
"Delete",
|
|
550
|
+
"ArrowUp",
|
|
551
|
+
"ArrowDown",
|
|
552
|
+
"ArrowLeft",
|
|
553
|
+
"ArrowRight",
|
|
554
|
+
"Home",
|
|
555
|
+
"End",
|
|
556
|
+
"PageUp",
|
|
557
|
+
"PageDown",
|
|
558
|
+
"F1",
|
|
559
|
+
"F2",
|
|
560
|
+
"F3",
|
|
561
|
+
"F4",
|
|
562
|
+
"F5",
|
|
563
|
+
"F6",
|
|
564
|
+
"F7",
|
|
565
|
+
"F8",
|
|
566
|
+
"F9",
|
|
567
|
+
"F10",
|
|
568
|
+
"F11",
|
|
569
|
+
"F12",
|
|
570
|
+
]);
|
|
571
|
+
|
|
572
|
+
document.addEventListener(
|
|
573
|
+
"keydown",
|
|
574
|
+
(event) => {
|
|
575
|
+
if (window.__btApiActionInProgress) return;
|
|
576
|
+
const isShortcut = event.ctrlKey || event.metaKey || event.altKey;
|
|
577
|
+
if (!isShortcut && !specialKeys.has(event.key)) return;
|
|
578
|
+
const keyDesc =
|
|
579
|
+
(event.ctrlKey ? "Ctrl+" : "") +
|
|
580
|
+
(event.metaKey ? "Meta+" : "") +
|
|
581
|
+
(event.altKey ? "Alt+" : "") +
|
|
582
|
+
(event.shiftKey ? "Shift+" : "") +
|
|
583
|
+
event.key;
|
|
584
|
+
window.__btActionLog(
|
|
585
|
+
JSON.stringify(
|
|
586
|
+
buildActionPayload(event, "press", {
|
|
587
|
+
value: keyDesc,
|
|
588
|
+
}),
|
|
589
|
+
),
|
|
590
|
+
);
|
|
591
|
+
},
|
|
592
|
+
true,
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
let scrollTimer = null;
|
|
596
|
+
document.addEventListener(
|
|
597
|
+
"scroll",
|
|
598
|
+
() => {
|
|
599
|
+
if (window.__btApiActionInProgress) return;
|
|
600
|
+
if (scrollTimer) clearTimeout(scrollTimer);
|
|
601
|
+
scrollTimer = setTimeout(() => {
|
|
602
|
+
scrollTimer = null;
|
|
603
|
+
window.__btActionLog(
|
|
604
|
+
JSON.stringify({
|
|
605
|
+
action: "scroll",
|
|
606
|
+
bestSemanticSelector: "document",
|
|
607
|
+
success: true,
|
|
608
|
+
value: \`y=\${window.scrollY}\`,
|
|
609
|
+
}),
|
|
610
|
+
);
|
|
611
|
+
}, 300);
|
|
612
|
+
},
|
|
613
|
+
true,
|
|
614
|
+
);
|
|
615
|
+
})();
|
|
616
|
+
`;
|
|
617
|
+
};
|
|
618
|
+
|
|
189
619
|
const installUserDomTracking = async (
|
|
190
620
|
page: Page,
|
|
191
621
|
pageId: string,
|
|
@@ -202,196 +632,7 @@ export async function installSessionTelemetry(
|
|
|
202
632
|
});
|
|
203
633
|
});
|
|
204
634
|
|
|
205
|
-
await page.addInitScript(()
|
|
206
|
-
if ((window as any).__btDomListenersInstalled) return;
|
|
207
|
-
(window as any).__btDomListenersInstalled = true;
|
|
208
|
-
|
|
209
|
-
const identify = (el: any): string => {
|
|
210
|
-
if (!el || !el.tagName) return "";
|
|
211
|
-
const testId = el.getAttribute("data-testid");
|
|
212
|
-
if (testId) return `[data-testid="${testId}"]`;
|
|
213
|
-
const role = el.getAttribute("role") || "";
|
|
214
|
-
const id = el.id;
|
|
215
|
-
if (role && id) return `${role}#${id}`;
|
|
216
|
-
const label =
|
|
217
|
-
el.getAttribute("aria-label") ||
|
|
218
|
-
(el.textContent || "").trim().slice(0, 30) ||
|
|
219
|
-
"";
|
|
220
|
-
if (role && label) return `${role} "${label}"`;
|
|
221
|
-
const tag = el.tagName.toLowerCase();
|
|
222
|
-
const cls =
|
|
223
|
-
el.className && typeof el.className === "string"
|
|
224
|
-
? "." + el.className.trim().split(/\s+/).slice(0, 2).join(".")
|
|
225
|
-
: "";
|
|
226
|
-
return `${tag}${cls}`;
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
let clickTimer: ReturnType<typeof setTimeout> | null = null;
|
|
230
|
-
let pendingClick: { selector: string } | null = null;
|
|
231
|
-
|
|
232
|
-
document.addEventListener(
|
|
233
|
-
"click",
|
|
234
|
-
(event) => {
|
|
235
|
-
if ((window as any).__btApiActionInProgress) return;
|
|
236
|
-
const target = event.target as any;
|
|
237
|
-
const selector = identify(target);
|
|
238
|
-
if (target?.type === "checkbox") {
|
|
239
|
-
(window as any).__btActionLog(
|
|
240
|
-
JSON.stringify({
|
|
241
|
-
action: target.checked ? "check" : "uncheck",
|
|
242
|
-
selector,
|
|
243
|
-
success: true,
|
|
244
|
-
}),
|
|
245
|
-
);
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
pendingClick = { selector };
|
|
249
|
-
if (clickTimer) clearTimeout(clickTimer);
|
|
250
|
-
clickTimer = setTimeout(() => {
|
|
251
|
-
if (pendingClick) {
|
|
252
|
-
(window as any).__btActionLog(
|
|
253
|
-
JSON.stringify({
|
|
254
|
-
action: "click",
|
|
255
|
-
selector: pendingClick.selector,
|
|
256
|
-
success: true,
|
|
257
|
-
}),
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
pendingClick = null;
|
|
261
|
-
clickTimer = null;
|
|
262
|
-
}, 200);
|
|
263
|
-
},
|
|
264
|
-
true,
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
document.addEventListener(
|
|
268
|
-
"dblclick",
|
|
269
|
-
(event) => {
|
|
270
|
-
if ((window as any).__btApiActionInProgress) return;
|
|
271
|
-
if (clickTimer) {
|
|
272
|
-
clearTimeout(clickTimer);
|
|
273
|
-
clickTimer = null;
|
|
274
|
-
pendingClick = null;
|
|
275
|
-
}
|
|
276
|
-
const selector = identify(event.target);
|
|
277
|
-
(window as any).__btActionLog(
|
|
278
|
-
JSON.stringify({ action: "dblclick", selector, success: true }),
|
|
279
|
-
);
|
|
280
|
-
},
|
|
281
|
-
true,
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
const inputTimers = new WeakMap<any, ReturnType<typeof setTimeout>>();
|
|
285
|
-
document.addEventListener(
|
|
286
|
-
"input",
|
|
287
|
-
(event) => {
|
|
288
|
-
if ((window as any).__btApiActionInProgress) return;
|
|
289
|
-
const target = event.target as any;
|
|
290
|
-
const selector = identify(target);
|
|
291
|
-
if (target.tagName === "SELECT") {
|
|
292
|
-
(window as any).__btActionLog(
|
|
293
|
-
JSON.stringify({
|
|
294
|
-
action: "selectOption",
|
|
295
|
-
selector,
|
|
296
|
-
value: target.value,
|
|
297
|
-
success: true,
|
|
298
|
-
}),
|
|
299
|
-
);
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
const existing = inputTimers.get(target);
|
|
303
|
-
if (existing) clearTimeout(existing);
|
|
304
|
-
inputTimers.set(
|
|
305
|
-
target,
|
|
306
|
-
setTimeout(() => {
|
|
307
|
-
inputTimers.delete(target);
|
|
308
|
-
(window as any).__btActionLog(
|
|
309
|
-
JSON.stringify({
|
|
310
|
-
action: "fill",
|
|
311
|
-
selector,
|
|
312
|
-
value: (target.value || "").slice(0, 100),
|
|
313
|
-
success: true,
|
|
314
|
-
}),
|
|
315
|
-
);
|
|
316
|
-
}, 500),
|
|
317
|
-
);
|
|
318
|
-
},
|
|
319
|
-
true,
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
const specialKeys = new Set([
|
|
323
|
-
"Enter",
|
|
324
|
-
"Escape",
|
|
325
|
-
"Tab",
|
|
326
|
-
"Backspace",
|
|
327
|
-
"Delete",
|
|
328
|
-
"ArrowUp",
|
|
329
|
-
"ArrowDown",
|
|
330
|
-
"ArrowLeft",
|
|
331
|
-
"ArrowRight",
|
|
332
|
-
"Home",
|
|
333
|
-
"End",
|
|
334
|
-
"PageUp",
|
|
335
|
-
"PageDown",
|
|
336
|
-
"F1",
|
|
337
|
-
"F2",
|
|
338
|
-
"F3",
|
|
339
|
-
"F4",
|
|
340
|
-
"F5",
|
|
341
|
-
"F6",
|
|
342
|
-
"F7",
|
|
343
|
-
"F8",
|
|
344
|
-
"F9",
|
|
345
|
-
"F10",
|
|
346
|
-
"F11",
|
|
347
|
-
"F12",
|
|
348
|
-
]);
|
|
349
|
-
document.addEventListener(
|
|
350
|
-
"keydown",
|
|
351
|
-
(event) => {
|
|
352
|
-
if ((window as any).__btApiActionInProgress) return;
|
|
353
|
-
const isShortcut = event.ctrlKey || event.metaKey || event.altKey;
|
|
354
|
-
if (!isShortcut && !specialKeys.has(event.key)) return;
|
|
355
|
-
const selector = identify(event.target);
|
|
356
|
-
const keyDesc =
|
|
357
|
-
(event.ctrlKey ? "Ctrl+" : "") +
|
|
358
|
-
(event.metaKey ? "Meta+" : "") +
|
|
359
|
-
(event.altKey ? "Alt+" : "") +
|
|
360
|
-
(event.shiftKey ? "Shift+" : "") +
|
|
361
|
-
event.key;
|
|
362
|
-
(window as any).__btActionLog(
|
|
363
|
-
JSON.stringify({
|
|
364
|
-
action: "press",
|
|
365
|
-
selector,
|
|
366
|
-
value: keyDesc,
|
|
367
|
-
success: true,
|
|
368
|
-
}),
|
|
369
|
-
);
|
|
370
|
-
},
|
|
371
|
-
true,
|
|
372
|
-
);
|
|
373
|
-
|
|
374
|
-
let scrollTimer: ReturnType<typeof setTimeout> | null = null;
|
|
375
|
-
document.addEventListener(
|
|
376
|
-
"scroll",
|
|
377
|
-
() => {
|
|
378
|
-
if ((window as any).__btApiActionInProgress) return;
|
|
379
|
-
if (scrollTimer) clearTimeout(scrollTimer);
|
|
380
|
-
scrollTimer = setTimeout(() => {
|
|
381
|
-
scrollTimer = null;
|
|
382
|
-
(window as any).__btActionLog(
|
|
383
|
-
JSON.stringify({
|
|
384
|
-
action: "scroll",
|
|
385
|
-
selector: "document",
|
|
386
|
-
value: `y=${window.scrollY}`,
|
|
387
|
-
success: true,
|
|
388
|
-
}),
|
|
389
|
-
);
|
|
390
|
-
}, 300);
|
|
391
|
-
},
|
|
392
|
-
true,
|
|
393
|
-
);
|
|
394
|
-
});
|
|
635
|
+
await page.addInitScript({ content: createUserDomTrackingInitScript() });
|
|
395
636
|
};
|
|
396
637
|
|
|
397
638
|
const wrapPageActions = (page: Page, pageId: string): void => {
|
|
@@ -96,6 +96,15 @@ export type ActionLogEntry = {
|
|
|
96
96
|
action: string;
|
|
97
97
|
source: "user" | "agent";
|
|
98
98
|
selector?: string;
|
|
99
|
+
bestSemanticSelector?: string;
|
|
100
|
+
targetSelector?: string;
|
|
101
|
+
ancestorSelectors?: string[];
|
|
102
|
+
nearbyText?: string;
|
|
103
|
+
composedPath?: string[];
|
|
104
|
+
coordinates?: {
|
|
105
|
+
x: number;
|
|
106
|
+
y: number;
|
|
107
|
+
};
|
|
99
108
|
value?: string;
|
|
100
109
|
url?: string;
|
|
101
110
|
duration?: number;
|
|
@@ -152,6 +161,11 @@ export function readActionLog(
|
|
|
152
161
|
(e) =>
|
|
153
162
|
re.test(e.action) ||
|
|
154
163
|
re.test(e.selector || "") ||
|
|
164
|
+
re.test(e.bestSemanticSelector || "") ||
|
|
165
|
+
re.test(e.targetSelector || "") ||
|
|
166
|
+
re.test((e.ancestorSelectors || []).join(" ")) ||
|
|
167
|
+
re.test(e.nearbyText || "") ||
|
|
168
|
+
re.test((e.composedPath || []).join(" ")) ||
|
|
155
169
|
re.test(e.value || "") ||
|
|
156
170
|
re.test(e.url || ""),
|
|
157
171
|
);
|
|
@@ -171,8 +185,16 @@ export function readActionLog(
|
|
|
171
185
|
export function formatActionEntry(e: ActionLogEntry): string {
|
|
172
186
|
const time = e.ts.replace(/.*T/, "").replace(/\.\d+Z$/, "");
|
|
173
187
|
const src = e.source.toUpperCase().padEnd(5);
|
|
188
|
+
const displaySelector = e.bestSemanticSelector || e.selector;
|
|
174
189
|
const parts = [`[${time}]`, `[${src}]`, e.action];
|
|
175
|
-
if (
|
|
190
|
+
if (displaySelector) parts.push(displaySelector);
|
|
191
|
+
if (e.targetSelector && e.targetSelector !== displaySelector) {
|
|
192
|
+
parts.push(`target=${e.targetSelector}`);
|
|
193
|
+
}
|
|
194
|
+
if (e.nearbyText) parts.push(`text="${e.nearbyText}"`);
|
|
195
|
+
if (e.coordinates) {
|
|
196
|
+
parts.push(`@(${e.coordinates.x},${e.coordinates.y})`);
|
|
197
|
+
}
|
|
176
198
|
if (e.value) parts.push(`"${e.value}"`);
|
|
177
199
|
if (e.url) parts.push(e.url);
|
|
178
200
|
if (e.duration != null) parts.push(`${e.duration}ms`);
|