explorbot 0.3.5 → 0.4.0

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 (102) hide show
  1. package/bin/explorbot-cli.ts +18 -13
  2. package/boat/doc-collector/src/cli.ts +3 -0
  3. package/boat/doc-collector/src/docbot.ts +3 -1
  4. package/boat/prima/src/cli.ts +21 -8
  5. package/boat/prima/src/envelope.ts +35 -9
  6. package/boat/prima/src/prima.ts +23 -10
  7. package/dist/bin/explorbot-cli.js +19 -13
  8. package/dist/boat/doc-collector/src/cli.js +3 -0
  9. package/dist/boat/doc-collector/src/docbot.js +3 -1
  10. package/dist/boat/prima/src/cli.js +19 -8
  11. package/dist/boat/prima/src/envelope.js +24 -6
  12. package/dist/boat/prima/src/prima.js +23 -11
  13. package/dist/package.json +2 -2
  14. package/dist/src/action-result.d.ts +9 -1
  15. package/dist/src/action-result.js +57 -18
  16. package/dist/src/action.d.ts +1 -1
  17. package/dist/src/action.js +87 -12
  18. package/dist/src/ai/driller.d.ts +0 -1
  19. package/dist/src/ai/driller.js +8 -20
  20. package/dist/src/ai/fisherman-tools.d.ts +9 -0
  21. package/dist/src/ai/fisherman-tools.js +52 -6
  22. package/dist/src/ai/fisherman.d.ts +4 -2
  23. package/dist/src/ai/fisherman.js +48 -27
  24. package/dist/src/ai/historian/codeceptjs.js +1 -1
  25. package/dist/src/ai/historian/playwright.js +1 -1
  26. package/dist/src/ai/pilot.d.ts +1 -0
  27. package/dist/src/ai/pilot.js +13 -1
  28. package/dist/src/ai/provider.js +20 -3
  29. package/dist/src/ai/researcher/deep-analysis.js +1 -3
  30. package/dist/src/ai/researcher.js +3 -3
  31. package/dist/src/ai/tester.d.ts +3 -0
  32. package/dist/src/ai/tester.js +40 -3
  33. package/dist/src/ai/tools.d.ts +1 -0
  34. package/dist/src/ai/tools.js +13 -6
  35. package/dist/src/api/request-result.d.ts +2 -0
  36. package/dist/src/api/request-result.js +8 -2
  37. package/dist/src/api/request-store.d.ts +3 -2
  38. package/dist/src/api/request-store.js +66 -14
  39. package/dist/src/commands/explore-command.d.ts +6 -0
  40. package/dist/src/commands/explore-command.js +27 -2
  41. package/dist/src/commands/freesail-command.js +10 -1
  42. package/dist/src/commands/plans-command.js +6 -6
  43. package/dist/src/config.js +1 -0
  44. package/dist/src/experience-tracker.js +5 -0
  45. package/dist/src/explorbot.d.ts +0 -1
  46. package/dist/src/explorbot.js +23 -36
  47. package/dist/src/state-manager.d.ts +5 -1
  48. package/dist/src/state-manager.js +10 -7
  49. package/dist/src/test-plan.d.ts +3 -0
  50. package/dist/src/test-plan.js +27 -0
  51. package/dist/src/utils/aria.d.ts +1 -1
  52. package/dist/src/utils/aria.js +6 -42
  53. package/dist/src/utils/html-diff.d.ts +4 -0
  54. package/dist/src/utils/html-diff.js +62 -7
  55. package/dist/src/utils/html.d.ts +5 -15
  56. package/dist/src/utils/html.js +14 -85
  57. package/dist/src/utils/overlay.d.ts +56 -11
  58. package/dist/src/utils/overlay.js +191 -21
  59. package/dist/src/utils/request-map.d.ts +7 -0
  60. package/dist/src/utils/request-map.js +16 -0
  61. package/dist/src/utils/url-matcher.js +4 -2
  62. package/docs/reference/commands.md +8 -1
  63. package/docs/reference/websocket.md +1 -0
  64. package/docs/superpowers/plans/2026-08-29-fisherman-reliability.md +953 -0
  65. package/docs/superpowers/plans/2026-08-29-region-states.md +1292 -0
  66. package/docs/superpowers/plans/2026-08-30-fisherman-live-session-auth.md +457 -0
  67. package/docs/superpowers/specs/2026-08-29-fisherman-reliability-design.md +45 -0
  68. package/docs/superpowers/specs/2026-08-29-region-states-design.md +262 -0
  69. package/docs/superpowers/specs/2026-08-29-region-states-fixes-design.md +269 -0
  70. package/docs/superpowers/specs/2026-08-30-fisherman-live-session-auth-design.md +37 -0
  71. package/docs/workflow/agentic-usage.md +1 -0
  72. package/docs/workflow/ci.md +1 -0
  73. package/package.json +2 -2
  74. package/src/action-result.ts +61 -22
  75. package/src/action.ts +87 -14
  76. package/src/ai/driller.ts +7 -39
  77. package/src/ai/fisherman-tools.ts +56 -7
  78. package/src/ai/fisherman.ts +48 -28
  79. package/src/ai/historian/codeceptjs.ts +1 -1
  80. package/src/ai/historian/playwright.ts +1 -1
  81. package/src/ai/pilot.ts +9 -1
  82. package/src/ai/provider.ts +21 -3
  83. package/src/ai/researcher/deep-analysis.ts +1 -2
  84. package/src/ai/researcher.ts +3 -3
  85. package/src/ai/tester.ts +40 -3
  86. package/src/ai/tools.ts +17 -9
  87. package/src/api/request-result.ts +10 -2
  88. package/src/api/request-store.ts +60 -13
  89. package/src/commands/explore-command.ts +25 -2
  90. package/src/commands/freesail-command.ts +7 -1
  91. package/src/commands/plans-command.ts +6 -6
  92. package/src/config.ts +1 -0
  93. package/src/experience-tracker.ts +5 -1
  94. package/src/explorbot.ts +20 -36
  95. package/src/state-manager.ts +13 -7
  96. package/src/test-plan.ts +29 -0
  97. package/src/utils/aria.ts +7 -44
  98. package/src/utils/html-diff.ts +62 -7
  99. package/src/utils/html.ts +14 -91
  100. package/src/utils/overlay.ts +226 -23
  101. package/src/utils/request-map.ts +19 -0
  102. package/src/utils/url-matcher.ts +3 -2
@@ -469,47 +469,17 @@ const formatDiff = (added, removed, toggled, typed = []) => {
469
469
  sections.push(...formatDiffSection('added', added), ...formatDiffSection('removed', removed));
470
470
  return sections.join('\n');
471
471
  };
472
- const CLOSE_OVERLAY_BUTTON_RE = /^close\s+(modal|dialog|popup|drawer|panel|sheet)\b/i;
473
- const findOverlayByCloseButton = (nodeList) => {
474
- const closeIdx = nodeList.findIndex((n) => n.role === 'button' && CLOSE_OVERLAY_BUTTON_RE.test(n.name || ''));
475
- if (closeIdx !== -1) {
476
- let heading;
477
- for (let i = closeIdx - 1; i >= 0; i--) {
478
- if (nodeList[i].role === 'heading' && nodeList[i].name) {
479
- heading = nodeList[i];
480
- break;
481
- }
482
- }
483
- if (!heading) {
484
- for (let i = closeIdx + 1; i < nodeList.length; i++) {
485
- if (nodeList[i].role === 'heading' && nodeList[i].name) {
486
- heading = nodeList[i];
487
- break;
488
- }
489
- }
490
- }
491
- return { detected: true, type: 'dialog', name: heading?.name || null };
492
- }
493
- for (const node of nodeList) {
494
- const inner = findOverlayByCloseButton(node.children);
495
- if (inner)
496
- return inner;
497
- }
498
- return null;
499
- };
500
472
  const findDialogOrModal = (nodes) => {
473
+ let topmost = null;
501
474
  for (const node of nodes) {
502
- if (node.role === 'dialog' || node.role === 'alertdialog') {
503
- return { detected: true, type: 'dialog', name: node.name || null };
504
- }
505
- if (node.attributes.modal === true || node.attributes.modal === 'true') {
506
- return { detected: true, type: 'modal', name: node.name || null };
475
+ if (node.role === 'dialog' || node.role === 'alertdialog' || node.attributes.modal === true || node.attributes.modal === 'true') {
476
+ topmost = { detected: true, type: 'modal', name: node.name || null };
507
477
  }
508
478
  const child = findDialogOrModal(node.children);
509
479
  if (child)
510
- return child;
480
+ topmost = child;
511
481
  }
512
- return null;
482
+ return topmost;
513
483
  };
514
484
  export const compactAriaSnapshot = (snapshot, keepNamed = false, offload) => {
515
485
  if (!snapshot)
@@ -547,13 +517,7 @@ export const detectFocusArea = (snapshot) => {
547
517
  let tree = parseSnapshot(snapshot);
548
518
  tree = unwrapIgnored(tree);
549
519
  tree = dropEmpty(tree, { keepNamed: true });
550
- const direct = findDialogOrModal(tree);
551
- if (direct)
552
- return direct;
553
- const fallback = findOverlayByCloseButton(tree);
554
- if (fallback?.name)
555
- return fallback;
556
- return { detected: false, type: null, name: null };
520
+ return findDialogOrModal(tree) ?? { detected: false, type: null, name: null };
557
521
  };
558
522
  export const collectInteractiveNodes = (snapshot) => {
559
523
  let tree = parseSnapshot(snapshot);
@@ -2,14 +2,17 @@ import type { HtmlConfig } from '../config.js';
2
2
  export interface HtmlDiffPart {
3
3
  container: string;
4
4
  subtree: string;
5
+ rawSize: number;
5
6
  added: string[];
6
7
  removed: string[];
8
+ appearedSelector?: string;
7
9
  }
8
10
  export interface HtmlDiffResult {
9
11
  parts: HtmlDiffPart[];
10
12
  added: string[];
11
13
  removed: string[];
12
14
  similarity: number;
15
+ pageSize: number;
13
16
  summary: string;
14
17
  messages: string[];
15
18
  }
@@ -22,3 +25,4 @@ export declare function htmlDiff(originalHtml: string, modifiedHtml: string, htm
22
25
  * Text the app announced across a navigation. Only live regions: everything else on a new page is its content, not a message.
23
26
  */
24
27
  export declare function liveRegionMessages(originalHtml: string, modifiedHtml: string): string[];
28
+ export declare function pathToXPath(treePath: string): string;
@@ -2,6 +2,8 @@ import { parse, serialize } from 'parse5';
2
2
  import { TAILWIND_CLASS_PATTERNS, TRASH_HTML_CLASSES, minifyHtml } from "./html.js";
3
3
  import { isDynamicId, isGenericClass } from "./xpath.js";
4
4
  const IGNORED_PATHS = new Set(['html[1]', 'html[1]/head[1]', 'html[1]/body[1]']);
5
+ const SHELL_RATIO = 0.8;
6
+ const ROOT_CONTENT_RATIO = 0.8;
5
7
  const LIVE_REGION_ROLES = new Set(['alert', 'alertdialog', 'status', 'log']);
6
8
  const TEXT_LINE_PREFIX = 'TEXT:';
7
9
  const MESSAGE_MAX_LENGTH = 200;
@@ -147,7 +149,11 @@ export async function htmlDiff(originalHtml, modifiedHtml, htmlConfig) {
147
149
  const { added, removed } = findDifferences(originalLines, modifiedLines);
148
150
  const originalMap = collectElementMap(originalDocument);
149
151
  const modifiedMap = collectElementMap(modifiedDocument);
150
- const parts = await buildDiffParts(originalMap, modifiedMap);
152
+ const modifiedBody = findBodyElement(modifiedDocument);
153
+ let pageSize = modifiedHtml.length;
154
+ if (modifiedBody)
155
+ pageSize = serializeNode(modifiedBody).length;
156
+ const parts = await buildDiffParts(originalMap, modifiedMap, pageSize);
151
157
  const structuralAdditions = parts.flatMap((p) => p.added.filter((a) => a.startsWith('ELEMENT:')));
152
158
  const allAdded = [...added, ...structuralAdditions];
153
159
  const totalChanges = allAdded.length + removed.length;
@@ -157,10 +163,14 @@ export async function htmlDiff(originalHtml, modifiedHtml, htmlConfig) {
157
163
  added: allAdded,
158
164
  removed,
159
165
  similarity,
166
+ pageSize,
160
167
  summary,
161
168
  messages: collectMessages(originalMap, modifiedMap, allAdded),
162
169
  };
163
170
  }
171
+ function serializeNode(node) {
172
+ return serialize({ childNodes: [node], nodeName: '#document-fragment' });
173
+ }
164
174
  /**
165
175
  * Text the app announced while the page stayed the same: live region content first, then any other text that appeared.
166
176
  */
@@ -392,14 +402,14 @@ function buildContainerSelector(element, allElements) {
392
402
  }
393
403
  return matchCount === 1 ? selector : null;
394
404
  }
395
- function pathToXPath(treePath) {
405
+ export function pathToXPath(treePath) {
396
406
  const parts = treePath.split('/');
397
407
  const bodyIdx = parts.findIndex((p) => p.startsWith('body'));
398
408
  if (bodyIdx === -1)
399
409
  return `//${parts.join('/')}`;
400
410
  return `//body/${parts.slice(bodyIdx + 1).join('/')}`;
401
411
  }
402
- function findStableContainer(topLevelPath, originalMap, modifiedMap) {
412
+ function findStableContainer(topLevelPath, originalMap, modifiedMap, pageSize) {
403
413
  const segments = topLevelPath.split('/');
404
414
  for (let i = segments.length - 2; i >= 1; i--) {
405
415
  const candidatePath = segments.slice(0, i + 1).join('/');
@@ -408,6 +418,8 @@ function findStableContainer(topLevelPath, originalMap, modifiedMap) {
408
418
  if (!originalMap.has(candidatePath) || !modifiedMap.has(candidatePath))
409
419
  continue;
410
420
  const element = modifiedMap.get(candidatePath);
421
+ if (pageSize > 0 && serializeNode(element).length >= SHELL_RATIO * pageSize)
422
+ break;
411
423
  const css = buildContainerSelector(element, modifiedMap);
412
424
  if (css)
413
425
  return { path: candidatePath, selector: css };
@@ -415,7 +427,32 @@ function findStableContainer(topLevelPath, originalMap, modifiedMap) {
415
427
  }
416
428
  return { path: 'html[1]/body[1]', selector: 'body' };
417
429
  }
418
- async function buildDiffParts(originalMap, modifiedMap) {
430
+ function dominantChild(element) {
431
+ const children = (element.childNodes ?? []).filter((child) => 'tagName' in child && !!child.tagName);
432
+ if (children.length === 0)
433
+ return null;
434
+ if (children.length === 1)
435
+ return children[0];
436
+ const parentSize = serializeNode(element).length;
437
+ if (!parentSize)
438
+ return null;
439
+ for (const child of children) {
440
+ if (serializeNode(child).length >= ROOT_CONTENT_RATIO * parentSize)
441
+ return child;
442
+ }
443
+ return null;
444
+ }
445
+ function semanticSelectorFor(element, allElements) {
446
+ let current = element;
447
+ while (current) {
448
+ const selector = buildContainerSelector(current, allElements);
449
+ if (selector)
450
+ return selector;
451
+ current = dominantChild(current);
452
+ }
453
+ return undefined;
454
+ }
455
+ async function buildDiffParts(originalMap, modifiedMap, pageSize) {
419
456
  const addedPaths = [];
420
457
  const changedPaths = [];
421
458
  for (const [path, element] of modifiedMap.entries()) {
@@ -438,7 +475,7 @@ async function buildDiffParts(originalMap, modifiedMap) {
438
475
  const allTopLevel = [...addedTopLevel, ...changedTopLevel];
439
476
  const grouped = new Map();
440
477
  for (const path of allTopLevel) {
441
- const { path: containerPath, selector } = findStableContainer(path, originalMap, modifiedMap);
478
+ const { path: containerPath, selector } = findStableContainer(path, originalMap, modifiedMap, pageSize);
442
479
  const existing = grouped.get(containerPath);
443
480
  if (existing) {
444
481
  existing.paths.push(path);
@@ -482,9 +519,27 @@ async function buildDiffParts(originalMap, modifiedMap) {
482
519
  const subtree = serialized ? await minifyHtml(serialized) : '';
483
520
  if (!subtree)
484
521
  continue;
485
- const addedLines = paths.filter((p) => addedTopLevel.includes(p)).map((p) => `ELEMENT:${p}`);
522
+ const appearedPaths = paths.filter((p) => addedTopLevel.includes(p));
523
+ const sizeOf = (p) => {
524
+ const node = modifiedMap.get(p);
525
+ if (!node)
526
+ return 0;
527
+ return serializeNode(node).length;
528
+ };
529
+ appearedPaths.sort((a, b) => sizeOf(b) - sizeOf(a));
530
+ const addedLines = appearedPaths.map((p) => `ELEMENT:${p}`);
486
531
  const removedLines = [];
487
- parts.push({ container: selector, subtree, added: addedLines, removed: removedLines });
532
+ const part = { container: selector, subtree, rawSize: serialized.length, added: addedLines, removed: removedLines };
533
+ const appearedPath = appearedPaths[0];
534
+ if (appearedPath) {
535
+ const appearedElement = modifiedMap.get(appearedPath);
536
+ if (appearedElement) {
537
+ const appearedSelector = semanticSelectorFor(appearedElement, modifiedMap);
538
+ if (appearedSelector)
539
+ part.appearedSelector = appearedSelector;
540
+ }
541
+ }
542
+ parts.push(part);
488
543
  }
489
544
  return parts;
490
545
  }
@@ -16,16 +16,11 @@ export declare const HTML_SELECTORS: {
16
16
  readonly semanticContextContainer: "section, article, form, fieldset, li, tr, td, th, [role=\"group\"], [role=\"tabpanel\"], [role=\"region\"], [class*=\"card\"], [class*=\"panel\"], [class*=\"item\"], [class*=\"usage\"], [class*=\"group\"]";
17
17
  };
18
18
  export declare const HTML_VISIBILITY_LIMITS: {
19
- readonly maxViewportOverlayRatio: 0.95;
20
19
  readonly minOpacity: 0.1;
21
- readonly minOverlayHeight: 40;
22
- readonly minOverlayWidth: 80;
23
20
  };
24
21
  export declare const HTML_EXTRACTION_LIMITS: {
25
22
  readonly componentScopeHtmlLength: 8000;
26
- readonly maxOverlayCount: 3;
27
23
  readonly maxScopeInteractiveCount: 16;
28
- readonly overlayHtmlLength: 6000;
29
24
  };
30
25
  export declare const CODE_EDITOR_MARKERS: readonly ["monaco", "codemirror", "ace", "ace_editor", "code"];
31
26
  export declare const HTML_INTERACTIVE_ROLES: Set<string>;
@@ -62,14 +57,6 @@ export declare const ELEMENT_EXTRACTION_CONFIG: {
62
57
  };
63
58
  export type ElementExtractionConfig = typeof ELEMENT_EXTRACTION_CONFIG;
64
59
  export type RawElementData = NonNullable<ReturnType<typeof extractElementData>>;
65
- export type VisibleOverlayExtractionConfig = {
66
- interactiveContentSelector: string;
67
- limits: typeof HTML_EXTRACTION_LIMITS;
68
- overlaySelectors: readonly string[];
69
- overlaySemanticSelector: string;
70
- visibilityLimits: typeof HTML_VISIBILITY_LIMITS;
71
- geometryFallback?: boolean;
72
- };
73
60
  export type ComponentScopeExtractionConfig = {
74
61
  eidxAttr: string;
75
62
  interactiveControlSelector: string;
@@ -85,9 +72,7 @@ export declare function extractElementData(el: Element, config?: ElementExtracti
85
72
  y: number;
86
73
  };
87
74
  export declare function getElementDataExtractorSource(): string;
88
- export declare function extractVisibleOverlayHtml(config: VisibleOverlayExtractionConfig): string;
89
75
  export declare function extractComponentScopeHtml(eidx: string, config: ComponentScopeExtractionConfig): string;
90
- export declare function getVisibleOverlayHtmlExtractorSource(): string;
91
76
  export declare function getComponentScopeHtmlExtractorSource(): string;
92
77
  export declare const TRASH_HTML_CLASSES: RegExp;
93
78
  export declare const TAILWIND_CLASS_PATTERNS: RegExp[];
@@ -106,6 +91,11 @@ export declare function minifyHtml(html: string): Promise<string>;
106
91
  export declare function htmlCombinedSnapshot(html: string, htmlConfig?: HtmlConfig['combined'], opts?: {
107
92
  keepPositions?: boolean;
108
93
  }): string;
94
+ /**
95
+ * Cleans a small HTML snippet for AI consumption: keeps structure and text,
96
+ * drops noisy attributes and generated class names
97
+ */
98
+ export declare function cleanHtmlSnippet(html: string): string;
109
99
  /**
110
100
  * Creates text-only snapshot with markdown formatting
111
101
  */
@@ -81,16 +81,11 @@ export const HTML_SELECTORS = {
81
81
  semanticContextContainer: 'section, article, form, fieldset, li, tr, td, th, [role="group"], [role="tabpanel"], [role="region"], [class*="card"], [class*="panel"], [class*="item"], [class*="usage"], [class*="group"]',
82
82
  };
83
83
  export const HTML_VISIBILITY_LIMITS = {
84
- maxViewportOverlayRatio: 0.95,
85
84
  minOpacity: 0.1,
86
- minOverlayHeight: 40,
87
- minOverlayWidth: 80,
88
85
  };
89
86
  export const HTML_EXTRACTION_LIMITS = {
90
87
  componentScopeHtmlLength: 8000,
91
- maxOverlayCount: 3,
92
88
  maxScopeInteractiveCount: 16,
93
- overlayHtmlLength: 6000,
94
89
  };
95
90
  export const CODE_EDITOR_MARKERS = ['monaco', 'codemirror', 'ace', 'ace_editor', 'code'];
96
91
  export const HTML_INTERACTIVE_ROLES = new Set(['button', 'link', 'checkbox', 'radio', 'switch', 'tab', 'combobox', 'iframe', 'code-editor', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'slider', 'spinbutton', 'textbox', 'searchbox', 'treeitem']);
@@ -411,83 +406,6 @@ export function extractElementData(el, config) {
411
406
  export function getElementDataExtractorSource() {
412
407
  return extractElementData.toString();
413
408
  }
414
- export function extractVisibleOverlayHtml(config) {
415
- function isVisible(element) {
416
- const html = element;
417
- const style = window.getComputedStyle(html);
418
- const rect = html.getBoundingClientRect();
419
- if (rect.width === 0 && rect.height === 0)
420
- return false;
421
- if (style.display === 'none' || style.visibility === 'hidden')
422
- return false;
423
- if (Number.parseFloat(style.opacity || '1') < config.visibilityLimits.minOpacity)
424
- return false;
425
- return true;
426
- }
427
- function getUsefulContent(element) {
428
- const text = (element.textContent || '').replace(/\s+/g, ' ').trim();
429
- const interactiveCount = element.querySelectorAll(config.interactiveContentSelector).length;
430
- return { interactiveCount, text };
431
- }
432
- function isLikelyFloatingOverlay(element) {
433
- const html = element;
434
- const style = window.getComputedStyle(html);
435
- const rect = html.getBoundingClientRect();
436
- const zIndex = Number.parseInt(style.zIndex || '0', 10);
437
- const isFloating = style.position === 'fixed' || style.position === 'absolute' || style.position === 'sticky' || zIndex > 0;
438
- if (!isFloating)
439
- return false;
440
- if (rect.width < config.visibilityLimits.minOverlayWidth || rect.height < config.visibilityLimits.minOverlayHeight)
441
- return false;
442
- if (rect.bottom < 0 || rect.right < 0 || rect.top > window.innerHeight || rect.left > window.innerWidth)
443
- return false;
444
- if (rect.width >= window.innerWidth * config.visibilityLimits.maxViewportOverlayRatio && rect.height >= window.innerHeight * config.visibilityLimits.maxViewportOverlayRatio)
445
- return false;
446
- const { interactiveCount, text } = getUsefulContent(element);
447
- return interactiveCount > 0 || text.length > 0;
448
- }
449
- function isFloatingOverlay(element) {
450
- const style = window.getComputedStyle(element);
451
- return style.position === 'fixed' || style.position === 'absolute' || Number.parseInt(style.zIndex || '0', 10) > 0;
452
- }
453
- const seen = new Set();
454
- const collected = [];
455
- for (const selector of config.overlaySelectors) {
456
- for (const element of Array.from(document.querySelectorAll(selector))) {
457
- if (seen.has(element))
458
- continue;
459
- seen.add(element);
460
- if (!isVisible(element))
461
- continue;
462
- if (!element.matches(config.overlaySemanticSelector) && !isFloatingOverlay(element))
463
- continue;
464
- const { interactiveCount, text } = getUsefulContent(element);
465
- if (interactiveCount === 0 && text.length === 0)
466
- continue;
467
- collected.push(element);
468
- }
469
- }
470
- const overlays = collected.filter((element) => !collected.some((other) => other !== element && element.contains(other))).map((element) => element.outerHTML.slice(0, config.limits.overlayHtmlLength));
471
- if (overlays.length === 0 && config.geometryFallback !== false) {
472
- const floatingCandidates = Array.from(document.body.querySelectorAll('*'))
473
- .filter((element) => !seen.has(element) && isVisible(element) && isLikelyFloatingOverlay(element))
474
- .sort((left, right) => {
475
- const leftStyle = window.getComputedStyle(left);
476
- const rightStyle = window.getComputedStyle(right);
477
- const leftZ = Number.parseInt(leftStyle.zIndex || '0', 10) || 0;
478
- const rightZ = Number.parseInt(rightStyle.zIndex || '0', 10) || 0;
479
- if (leftZ !== rightZ)
480
- return rightZ - leftZ;
481
- const leftRect = left.getBoundingClientRect();
482
- const rightRect = right.getBoundingClientRect();
483
- return leftRect.width * leftRect.height - rightRect.width * rightRect.height;
484
- });
485
- for (const element of floatingCandidates.slice(0, config.limits.maxOverlayCount)) {
486
- overlays.push(element.outerHTML.slice(0, config.limits.overlayHtmlLength));
487
- }
488
- }
489
- return overlays.slice(0, config.limits.maxOverlayCount).join('\n\n--- overlay ---\n\n');
490
- }
491
409
  export function extractComponentScopeHtml(eidx, config) {
492
410
  const element = document.querySelector(`[${config.eidxAttr}="${eidx}"]`);
493
411
  if (!element)
@@ -507,9 +425,6 @@ export function extractComponentScopeHtml(eidx, config) {
507
425
  return element.outerHTML.slice(0, config.limits.componentScopeHtmlLength);
508
426
  return '';
509
427
  }
510
- export function getVisibleOverlayHtmlExtractorSource() {
511
- return extractVisibleOverlayHtml.toString();
512
- }
513
428
  export function getComponentScopeHtmlExtractorSource() {
514
429
  return extractComponentScopeHtml.toString();
515
430
  }
@@ -927,6 +842,19 @@ export function htmlCombinedSnapshot(html, htmlConfig, opts) {
927
842
  cleanAllElements(body);
928
843
  return serialize(document);
929
844
  }
845
+ /**
846
+ * Cleans a small HTML snippet for AI consumption: keeps structure and text,
847
+ * drops noisy attributes and generated class names
848
+ */
849
+ export function cleanHtmlSnippet(html) {
850
+ const fragment = parseFragment(html);
851
+ for (const node of fragment.childNodes) {
852
+ if (!('tagName' in node))
853
+ continue;
854
+ cleanAllElements(node);
855
+ }
856
+ return serialize(fragment);
857
+ }
930
858
  /**
931
859
  * Creates text-only snapshot with markdown formatting
932
860
  */
@@ -1381,6 +1309,7 @@ function cleanElement(element) {
1381
1309
  if (attr.name === 'class') {
1382
1310
  attr.value = attr.value
1383
1311
  .split(/\s+/)
1312
+ .filter(Boolean)
1384
1313
  .filter((className) => !/\d/.test(className))
1385
1314
  .filter((className) => !className.includes(':'))
1386
1315
  .filter((className) => !TAILWIND_CLASS_PATTERNS.some((pattern) => pattern.test(className)))
@@ -1,24 +1,69 @@
1
- import { type VisibleOverlayExtractionConfig } from './html.js';
2
- export declare const OVERLAY_SELECTORS: {
3
- readonly semanticOverlays: readonly ["[role=\"dialog\"]", "[role=\"listbox\"]", "[role=\"menu\"]", "[role=\"tooltip\"]:not([style*=\"display: none\"]):not([style*=\"visibility: hidden\"])", "[class*=\"modal\"]", "[class*=\"dialog\"]", "[class*=\"overlay\"]", "[class*=\"popup\"]", "[class*=\"drawer\"]", "[class*=\"lightbox\"]"];
4
- readonly modalOverlays: readonly ["[role=\"dialog\"]", "[role=\"alertdialog\"]", "[aria-modal=\"true\"]", "[class*=\"modal\"]", "[class*=\"dialog\"]", "[class*=\"overlay\"]", "[class*=\"popup\"]", "[class*=\"drawer\"]", "[class*=\"lightbox\"]"];
5
- readonly overlaySemanticSelector: "[role=\"dialog\"], [role=\"alertdialog\"], [aria-modal=\"true\"], [role=\"listbox\"], [role=\"menu\"], [role=\"tooltip\"]";
6
- };
1
+ import type { HtmlDiffPart } from './html-diff.js';
2
+ export type OverlayType = 'modal' | 'region';
7
3
  export type OverlayData = {
8
- type?: 'dialog' | 'modal' | null;
4
+ type?: OverlayType | null;
9
5
  name?: string | null;
6
+ root?: string | null;
7
+ html?: string | null;
8
+ xpath?: string | null;
9
+ parent?: OverlayData | null;
10
10
  };
11
11
  export declare class Overlay {
12
- readonly type: 'dialog' | 'modal' | null;
12
+ readonly type: OverlayType | null;
13
13
  readonly name: string | null;
14
+ readonly root: string | null;
15
+ readonly html: string | null;
16
+ readonly xpath: string | null;
17
+ readonly parent: OverlayData | null;
14
18
  constructor(data?: OverlayData);
15
19
  get detected(): boolean;
16
- static fromHtml(html: string): Overlay;
20
+ get present(): boolean;
21
+ describe(): string;
22
+ withGeometry(geometry: Overlay): Overlay;
23
+ withParent(parent: Overlay): Overlay;
17
24
  static fromAria(snapshot: string | null): Overlay;
18
25
  static resolve(data: {
19
- overlayHtml?: string;
20
26
  overlay?: OverlayData | null;
21
27
  ariaSnapshot?: string | null;
22
28
  }): Overlay;
23
- static captureConfig(): VisibleOverlayExtractionConfig;
24
29
  }
30
+ export declare class OverlayPage {
31
+ page: {
32
+ evaluate(fn: any, arg: any): Promise<any>;
33
+ } | null;
34
+ constructor(page: {
35
+ evaluate(fn: any, arg: any): Promise<any>;
36
+ } | null);
37
+ detectRegion(diff: RegionDiff): Promise<Overlay | null>;
38
+ isStillOpen(overlay: Overlay): Promise<boolean>;
39
+ appearedSubRoots(diff: RegionDiff): AppearedSubRoot[];
40
+ probe(xpath: string): Promise<RegionProbe | null>;
41
+ toOverlay(subRoot: AppearedSubRoot, probe: RegionProbe | null, previousHtml: string): Overlay;
42
+ nameFrom(html: string, previousHtml: string): string | null;
43
+ freshHeading(html: string, previousHtml: string): string | null;
44
+ headingsOf(html: string): string[];
45
+ }
46
+ export interface RegionDiff {
47
+ parts: HtmlDiffPart[];
48
+ pageSize: number;
49
+ similarity: number;
50
+ sameUrl: boolean;
51
+ previousHtml: string;
52
+ }
53
+ interface AppearedSubRoot {
54
+ container: string;
55
+ elementXPath: string;
56
+ subtree: string;
57
+ size: number;
58
+ rawSize: number;
59
+ appearedSelector?: string;
60
+ fresh: boolean;
61
+ }
62
+ interface RegionProbe {
63
+ found: boolean;
64
+ onScreen: boolean;
65
+ floating: boolean;
66
+ centerBelongs: boolean;
67
+ coveredByFloating: boolean;
68
+ }
69
+ export {};