explorbot 0.3.4 → 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 (104) 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 +15 -1
  28. package/dist/src/ai/planner.js +1 -1
  29. package/dist/src/ai/provider.js +47 -4
  30. package/dist/src/ai/researcher/deep-analysis.js +1 -3
  31. package/dist/src/ai/researcher.js +3 -3
  32. package/dist/src/ai/tester.d.ts +3 -0
  33. package/dist/src/ai/tester.js +40 -3
  34. package/dist/src/ai/tools.d.ts +1 -0
  35. package/dist/src/ai/tools.js +13 -6
  36. package/dist/src/api/request-result.d.ts +2 -0
  37. package/dist/src/api/request-result.js +8 -2
  38. package/dist/src/api/request-store.d.ts +3 -2
  39. package/dist/src/api/request-store.js +66 -14
  40. package/dist/src/commands/explore-command.d.ts +6 -0
  41. package/dist/src/commands/explore-command.js +27 -2
  42. package/dist/src/commands/freesail-command.js +10 -1
  43. package/dist/src/commands/plans-command.js +6 -6
  44. package/dist/src/config.js +1 -0
  45. package/dist/src/experience-tracker.js +5 -0
  46. package/dist/src/explorbot.d.ts +0 -1
  47. package/dist/src/explorbot.js +23 -36
  48. package/dist/src/state-manager.d.ts +5 -1
  49. package/dist/src/state-manager.js +10 -7
  50. package/dist/src/test-plan.d.ts +3 -0
  51. package/dist/src/test-plan.js +27 -0
  52. package/dist/src/utils/aria.d.ts +1 -1
  53. package/dist/src/utils/aria.js +6 -42
  54. package/dist/src/utils/html-diff.d.ts +4 -0
  55. package/dist/src/utils/html-diff.js +62 -7
  56. package/dist/src/utils/html.d.ts +5 -15
  57. package/dist/src/utils/html.js +14 -85
  58. package/dist/src/utils/overlay.d.ts +56 -11
  59. package/dist/src/utils/overlay.js +191 -21
  60. package/dist/src/utils/request-map.d.ts +7 -0
  61. package/dist/src/utils/request-map.js +16 -0
  62. package/dist/src/utils/url-matcher.js +4 -2
  63. package/docs/reference/commands.md +8 -1
  64. package/docs/reference/websocket.md +1 -0
  65. package/docs/superpowers/plans/2026-08-29-fisherman-reliability.md +953 -0
  66. package/docs/superpowers/plans/2026-08-29-region-states.md +1292 -0
  67. package/docs/superpowers/plans/2026-08-30-fisherman-live-session-auth.md +457 -0
  68. package/docs/superpowers/specs/2026-08-29-fisherman-reliability-design.md +45 -0
  69. package/docs/superpowers/specs/2026-08-29-region-states-design.md +262 -0
  70. package/docs/superpowers/specs/2026-08-29-region-states-fixes-design.md +269 -0
  71. package/docs/superpowers/specs/2026-08-30-fisherman-live-session-auth-design.md +37 -0
  72. package/docs/workflow/agentic-usage.md +1 -0
  73. package/docs/workflow/ci.md +1 -0
  74. package/package.json +2 -2
  75. package/src/action-result.ts +61 -22
  76. package/src/action.ts +87 -14
  77. package/src/ai/driller.ts +7 -39
  78. package/src/ai/fisherman-tools.ts +56 -7
  79. package/src/ai/fisherman.ts +48 -28
  80. package/src/ai/historian/codeceptjs.ts +1 -1
  81. package/src/ai/historian/playwright.ts +1 -1
  82. package/src/ai/pilot.ts +11 -1
  83. package/src/ai/planner.ts +1 -1
  84. package/src/ai/provider.ts +48 -4
  85. package/src/ai/researcher/deep-analysis.ts +1 -2
  86. package/src/ai/researcher.ts +3 -3
  87. package/src/ai/tester.ts +40 -3
  88. package/src/ai/tools.ts +17 -9
  89. package/src/api/request-result.ts +10 -2
  90. package/src/api/request-store.ts +60 -13
  91. package/src/commands/explore-command.ts +25 -2
  92. package/src/commands/freesail-command.ts +7 -1
  93. package/src/commands/plans-command.ts +6 -6
  94. package/src/config.ts +1 -0
  95. package/src/experience-tracker.ts +5 -1
  96. package/src/explorbot.ts +20 -36
  97. package/src/state-manager.ts +13 -7
  98. package/src/test-plan.ts +29 -0
  99. package/src/utils/aria.ts +7 -44
  100. package/src/utils/html-diff.ts +62 -7
  101. package/src/utils/html.ts +14 -91
  102. package/src/utils/overlay.ts +226 -23
  103. package/src/utils/request-map.ts +19 -0
  104. package/src/utils/url-matcher.ts +3 -2
@@ -1,5 +1,7 @@
1
- import { mkdirSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ export const STATUS_FILE = 'status.json';
4
+ const ARTIFACT_FILES = { aria: 'aria.yml', html: 'page.html', screenshot: 'page.png', network: 'network.jsonl' };
3
5
  const EXPECTATION_LABELS = {
4
6
  passed: 'PASSED ',
5
7
  failed: 'FAILED ',
@@ -13,21 +15,35 @@ export function renderEnvelope(data) {
13
15
  export function writeArtifacts(dir, snapshot) {
14
16
  mkdirSync(dir, { recursive: true });
15
17
  const paths = {
16
- aria: path.resolve(dir, 'aria.yml'),
17
- html: path.resolve(dir, 'page.html'),
18
+ aria: path.resolve(dir, ARTIFACT_FILES.aria),
19
+ html: path.resolve(dir, ARTIFACT_FILES.html),
18
20
  };
19
21
  writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
20
22
  writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
21
23
  if (snapshot.screenshot) {
22
- paths.screenshot = path.resolve(dir, 'page.png');
24
+ paths.screenshot = path.resolve(dir, ARTIFACT_FILES.screenshot);
23
25
  writeFileSync(paths.screenshot, snapshot.screenshot);
24
26
  }
25
27
  if (!snapshot.requests.length)
26
28
  return paths;
27
- paths.network = path.resolve(dir, 'network.jsonl');
29
+ paths.network = path.resolve(dir, ARTIFACT_FILES.network);
28
30
  writeFileSync(paths.network, snapshot.requests.map((request) => `${JSON.stringify(request)}\n`).join(''), 'utf-8');
29
31
  return paths;
30
32
  }
33
+ export function readArtifacts(dir) {
34
+ const paths = { aria: path.resolve(dir, ARTIFACT_FILES.aria), html: path.resolve(dir, ARTIFACT_FILES.html) };
35
+ if (existsSync(path.resolve(dir, ARTIFACT_FILES.screenshot)))
36
+ paths.screenshot = path.resolve(dir, ARTIFACT_FILES.screenshot);
37
+ if (existsSync(path.resolve(dir, ARTIFACT_FILES.network)))
38
+ paths.network = path.resolve(dir, ARTIFACT_FILES.network);
39
+ const recorded = [...Object.values(ARTIFACT_FILES), STATUS_FILE];
40
+ const files = readdirSync(dir)
41
+ .filter((entry) => !recorded.includes(entry))
42
+ .sort();
43
+ if (files.length)
44
+ paths.files = files;
45
+ return paths;
46
+ }
31
47
  function renderResult(data) {
32
48
  const lines = [`ok: ${data.ok}`, `command: ${data.command}`];
33
49
  if (data.used?.length)
@@ -70,7 +86,7 @@ function renderSteps(data) {
70
86
  lines.push(` ${line}`);
71
87
  });
72
88
  if (data.stepFiles)
73
- lines.push('', `page after each step: ${data.stepFiles}`);
89
+ lines.push('', `page after each step: ${data.stepFiles}/<n>-<step>.{aria.yaml,html,diff.yaml}`);
74
90
  return section('Steps', lines.join('\n'));
75
91
  }
76
92
  function renderExpectations(data) {
@@ -154,6 +170,8 @@ export function renderArtifacts(data) {
154
170
  lines.push(`screenshot: ${data.artifacts.screenshot}`);
155
171
  if (data.artifacts.network)
156
172
  lines.push(`network: ${data.artifacts.network}`);
173
+ if (data.artifacts.files?.length)
174
+ lines.push(`also here: ${data.artifacts.files.join(', ')}`);
157
175
  return section('Artifacts', lines.join('\n'));
158
176
  }
159
177
  function align(label, marker, width) {
@@ -23,7 +23,7 @@ import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
23
23
  import { pluralize } from "../../../src/utils/logger.js";
24
24
  import { mdq } from "../../../src/utils/markdown-query.js";
25
25
  import { safeFilename } from "../../../src/utils/strings.js";
26
- import { writeArtifacts } from "./envelope.js";
26
+ import { STATUS_FILE, readArtifacts, writeArtifacts } from "./envelope.js";
27
27
  import { isFunctionExpression, takePwValue, toCodeceptWrapper } from "./pw-parser.js";
28
28
  import { readDescriptors, selectDescriptor } from "./pw-registry.js";
29
29
  import { latestSessionFile, readSession, recordCommand, sessionFile, sessionsDir } from "./session-log.js";
@@ -341,8 +341,6 @@ export class Prima {
341
341
  const problems = [...unreached.map((expectation) => `not reached: ${expectation.text}`), ...contradicted.map((expectation) => `the picture and the run disagree about: ${expectation.text}`)];
342
342
  if (problems.length)
343
343
  envelope.failure = { error: problems.join('\n') };
344
- if (contradicted.length)
345
- envelope.artifacts = this.artifacts;
346
344
  if (!test.hasFinished || test.isSkipped) {
347
345
  envelope.ok = false;
348
346
  envelope.failure = { error: `the run did not complete, so it established nothing about the app: ${notes.at(-1)?.message || 'no steps were recorded'}` };
@@ -924,14 +922,17 @@ export class Prima {
924
922
  return response?.text || '';
925
923
  }
926
924
  async successEnvelope(command, used, result, previousState) {
925
+ const changes = await this.pageChanges(result, previousState, used[0]);
926
+ const status = await this.saveStatus(result);
927
927
  return {
928
928
  ok: true,
929
929
  command,
930
930
  used,
931
931
  page: this.pageBlock(result, previousState),
932
- changes: await this.pageChanges(result, previousState, used[0]),
932
+ changes,
933
933
  instance: await this.instanceInfo(),
934
- status: await this.saveStatus(result),
934
+ status,
935
+ artifacts: this.artifacts,
935
936
  };
936
937
  }
937
938
  async failureEnvelope(command, error, previousState) {
@@ -939,23 +940,27 @@ export class Prima {
939
940
  const failure = { error: browserErrorMessage(error) };
940
941
  if (result.ariaSnapshot)
941
942
  failure.compactAria = compactAriaSnapshot(result.ariaSnapshot, true);
943
+ const status = await this.saveStatus(result);
942
944
  return {
943
945
  ok: false,
944
946
  command,
945
947
  page: this.pageBlock(result, previousState),
946
948
  failure,
947
949
  instance: await this.instanceInfo(),
948
- status: await this.saveStatus(result),
950
+ status,
951
+ artifacts: this.artifacts,
949
952
  };
950
953
  }
951
954
  async reportEnvelope(command, result, previousState, outcome) {
955
+ const status = await this.saveStatus(result);
952
956
  return {
953
957
  ok: true,
954
958
  command,
955
959
  page: this.pageBlock(result, previousState),
956
960
  ...outcome,
957
961
  instance: await this.instanceInfo(),
958
- status: await this.saveStatus(result),
962
+ status,
963
+ artifacts: this.artifacts,
959
964
  };
960
965
  }
961
966
  async capturedResult(previousState, opts = {}) {
@@ -1000,23 +1005,30 @@ export class Prima {
1000
1005
  return lines.join('\n');
1001
1006
  }
1002
1007
  async status(hash) {
1008
+ if (!this.artifactsDir) {
1009
+ const sites = listSites();
1010
+ const site = sites.find((candidate) => existsSync(path.join(candidate.dir, 'output', 'prima', hash))) || sites[0];
1011
+ if (site && !this.configBaseUrl())
1012
+ this.sessionUrl = site.url;
1013
+ await this.loadConfig();
1014
+ }
1003
1015
  const dir = this.statusDir(hash);
1004
- const statusFile = path.join(dir, 'status.json');
1016
+ const statusFile = path.join(dir, STATUS_FILE);
1005
1017
  if (!existsSync(statusFile))
1006
- return this.toolFailureEnvelope(`status ${hash}`, `No command was recorded under ${hash}. Every envelope prints its own hash on the Instance line.`);
1018
+ return this.toolFailureEnvelope(`status ${hash}`, `No command was recorded under ${dir}. Every envelope prints its own hash on the Instance line.`);
1007
1019
  const saved = JSON.parse(readFileSync(statusFile, 'utf-8'));
1008
1020
  return {
1009
1021
  ok: true,
1010
1022
  command: `status ${hash}`,
1011
1023
  page: saved.page,
1012
1024
  instance: await this.instanceInfo(),
1013
- artifacts: { aria: path.join(dir, 'aria.yml'), html: path.join(dir, 'page.html') },
1025
+ artifacts: readArtifacts(dir),
1014
1026
  };
1015
1027
  }
1016
1028
  async saveStatus(result) {
1017
1029
  const hash = this.statusHash();
1018
1030
  await this.writeSnapshot(result);
1019
- writeFileSync(path.join(this.statusDir(hash), 'status.json'), JSON.stringify({ page: this.pageBlock(result, null) }), 'utf-8');
1031
+ writeFileSync(path.join(this.statusDir(hash), STATUS_FILE), JSON.stringify({ page: this.pageBlock(result, null) }), 'utf-8');
1020
1032
  return hash;
1021
1033
  }
1022
1034
  async writeStepFiles(index, label, diff) {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -77,7 +77,7 @@
77
77
  "dependencies": {
78
78
  "@ai-sdk/anthropic": "^4.0",
79
79
  "@ai-sdk/google": "^4.0.18",
80
- "@ai-sdk/groq": "^4.0",
80
+ "@ai-sdk/groq": "^4.0.34",
81
81
  "@ai-sdk/mistral": "^4.0.13",
82
82
  "@ai-sdk/openai": "^4.0",
83
83
  "@ai-sdk/otel": "^1.0.2",
@@ -29,7 +29,6 @@ interface ActionResultData extends WebPageState {
29
29
  focusedElement?: FocusedElement | null;
30
30
  iframeURL?: string;
31
31
  links?: Link[];
32
- overlayHtml?: string;
33
32
  }
34
33
  export interface PageDiff {
35
34
  urlChanged: boolean;
@@ -42,6 +41,7 @@ export interface PageDiff {
42
41
  consoleErrors?: string[];
43
42
  htmlParts?: HtmlDiffPart[];
44
43
  iframes?: string;
44
+ areaOfInterest?: string;
45
45
  }
46
46
  export interface ToolResultMetadata {
47
47
  url: string;
@@ -84,6 +84,10 @@ export declare class ActionResult implements ActionResultData {
84
84
  links: Link[];
85
85
  verifications?: Record<string, boolean>;
86
86
  overlay: Overlay;
87
+ _diffCache: {
88
+ previousId: number | undefined;
89
+ diff: Diff;
90
+ } | null;
87
91
  constructor(data: ActionResultData);
88
92
  get hash(): string;
89
93
  get html(): string;
@@ -117,8 +121,10 @@ export declare class ActionResult implements ActionResultData {
117
121
  get relativeUrl(): string | null;
118
122
  get isInsideIframe(): boolean;
119
123
  getStateHash(): string;
124
+ get baseHash(): string;
120
125
  diff(previousState: ActionResult | null): Promise<Diff>;
121
126
  toToolResult(previousState: ActionResult | null, locator: string): Promise<ToolResultMetadata>;
127
+ computeStateHash(includeRegion: boolean): string;
122
128
  consoleErrors(): string[];
123
129
  }
124
130
  export declare class Diff {
@@ -140,6 +146,8 @@ export declare class Diff {
140
146
  get ariaChangeCount(): number;
141
147
  get htmlDiff(): HtmlDiffResult | null;
142
148
  get messages(): string[];
149
+ get similarity(): number;
150
+ get pageSize(): number;
143
151
  calculate(): Promise<void>;
144
152
  }
145
153
  export interface NetworkCall {
@@ -39,6 +39,7 @@ export class ActionResult {
39
39
  links = [];
40
40
  verifications;
41
41
  overlay = new Overlay();
42
+ _diffCache = null;
42
43
  constructor(data) {
43
44
  this.id = data.id;
44
45
  this.timestamp = data.timestamp ?? new Date();
@@ -108,6 +109,7 @@ export class ActionResult {
108
109
  set html(value) {
109
110
  this._html = value;
110
111
  this.snapshotCache.clear();
112
+ this._diffCache = null;
111
113
  }
112
114
  get screenshot() {
113
115
  if (this._screenshot) {
@@ -193,6 +195,14 @@ export class ActionResult {
193
195
  isRelevantExperienceRecord(record, options) {
194
196
  if (!record.url || !this.url)
195
197
  return false;
198
+ if (record.region && this.overlay.name !== record.region)
199
+ return false;
200
+ if (record.root) {
201
+ if (!this.overlay.present)
202
+ return false;
203
+ if (this.overlay.root && this.overlay.root !== record.root)
204
+ return false;
205
+ }
196
206
  if (this.isMatchedBy(record))
197
207
  return true;
198
208
  if (!options?.includeDescendantExperience)
@@ -386,24 +396,17 @@ export class ActionResult {
386
396
  return !!this.iframeURL;
387
397
  }
388
398
  getStateHash() {
389
- const parts = [];
390
- parts.push(this.relativeUrl || this.url || '/');
391
- this.extractHeadings(this.html);
392
- if (this.h1)
393
- parts.push(`h1_${this.h1}`);
394
- if (this.h2)
395
- parts.push(`h2_${this.h2}`);
396
- let stateString = slugify(parts.map((part) => part.substring(0, 100)).join('_'));
397
- if (stateString.length > 200) {
398
- stateString = stateString.substring(0, 200);
399
- if (stateString.endsWith('_')) {
400
- stateString = stateString.slice(0, -1);
401
- }
402
- }
403
- return stateString;
399
+ return this.computeStateHash(true);
400
+ }
401
+ get baseHash() {
402
+ return this.computeStateHash(false);
404
403
  }
405
404
  async diff(previousState) {
406
- return Diff.create(this, previousState);
405
+ if (this._diffCache && this._diffCache.previousId === previousState?.id)
406
+ return this._diffCache.diff;
407
+ const diff = await Diff.create(this, previousState);
408
+ this._diffCache = { previousId: previousState?.id, diff };
409
+ return diff;
407
410
  }
408
411
  async toToolResult(previousState, locator) {
409
412
  const result = {
@@ -443,7 +446,18 @@ export class ActionResult {
443
446
  pageDiff.ariaChanges = diff.ariaChanged;
444
447
  pageDiff.ariaChangeCount = diff.ariaChangeCount;
445
448
  }
446
- if (diff.htmlParts.length > 0) {
449
+ if (this.overlay.present && (!previousState.overlay.present || previousState.overlay.name !== this.overlay.name)) {
450
+ pageDiff.areaOfInterest = this.overlay.describe();
451
+ }
452
+ if (pageDiff.areaOfInterest && this.overlay.html && this.overlay.root) {
453
+ const htmlConfig = ConfigParser.getInstance().getConfig().html;
454
+ let subtree = await minifyHtml(htmlCombinedSnapshot(this.overlay.html, htmlConfig?.combined));
455
+ if (subtree.length > HTML_PART_SUBTREE_BUDGET) {
456
+ subtree = `${subtree.slice(0, HTML_PART_SUBTREE_BUDGET)}...<!-- truncated -->`;
457
+ }
458
+ pageDiff.htmlParts = [{ container: this.overlay.root, subtree, rawSize: subtree.length, added: [], removed: [] }];
459
+ }
460
+ else if (diff.isSameUrl() && diff.htmlParts.length > 0) {
447
461
  const collapsed = collapseHtmlParts(await diff.cleanedHtmlParts());
448
462
  if (collapsed.length > 0) {
449
463
  pageDiff.htmlParts = collapsed;
@@ -456,6 +470,25 @@ export class ActionResult {
456
470
  }
457
471
  return result;
458
472
  }
473
+ computeStateHash(includeRegion) {
474
+ const parts = [];
475
+ parts.push(this.relativeUrl || this.url || '/');
476
+ this.extractHeadings(this.html);
477
+ if (this.h1)
478
+ parts.push(`h1_${this.h1}`);
479
+ if (this.h2)
480
+ parts.push(`h2_${this.h2}`);
481
+ if (includeRegion && this.overlay.present && this.overlay.name)
482
+ parts.push(`region_${this.overlay.name}`);
483
+ let stateString = slugify(parts.map((part) => part.substring(0, 100)).join('_'));
484
+ if (stateString.length > 200) {
485
+ stateString = stateString.substring(0, 200);
486
+ if (stateString.endsWith('_')) {
487
+ stateString = stateString.slice(0, -1);
488
+ }
489
+ }
490
+ return stateString;
491
+ }
459
492
  consoleErrors() {
460
493
  const errors = [];
461
494
  for (const log of this.browserLogs) {
@@ -560,14 +593,20 @@ export class Diff {
560
593
  get messages() {
561
594
  return this._messages;
562
595
  }
596
+ get similarity() {
597
+ return this._htmlDiffResult?.similarity ?? 0;
598
+ }
599
+ get pageSize() {
600
+ return this._htmlDiffResult?.pageSize ?? 0;
601
+ }
563
602
  async calculate() {
564
603
  if (!this.previous)
565
604
  return;
605
+ this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html, ConfigParser.getInstance().getConfig().html);
566
606
  if (!this._isSameUrl) {
567
607
  this._messages = liveRegionMessages(this.previous.html, this.current.html);
568
608
  return;
569
609
  }
570
- this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html, ConfigParser.getInstance().getConfig().html);
571
610
  this._messages = this._htmlDiffResult.messages;
572
611
  const ariaDiff = diffAriaSnapshots(this.previous.ariaSnapshot, this.current.ariaSnapshot);
573
612
  this._ariaDiffResult = ariaDiff.text;
@@ -33,7 +33,7 @@ declare class Action {
33
33
  includeScreenshot?: boolean;
34
34
  codeBlock?: string;
35
35
  }): Promise<ActionResult>;
36
- captureOverlayHtml(): Promise<string>;
36
+ detectRegionOfInterest(result: ActionResult): Promise<void>;
37
37
  captureMainDocumentStatus(): Promise<number | undefined>;
38
38
  captureResponses(): () => void;
39
39
  recordNetworkCall(request: any, status: number): void;
@@ -8,9 +8,9 @@ import { clearActivity, setActivity } from "./activity.js";
8
8
  import { ConfigParser, outputPath } from './config.js';
9
9
  import { Observability } from "./observability.js";
10
10
  import { browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
11
- import { captureHtmlForSnapshot, getVisibleOverlayHtmlExtractorSource, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
11
+ import { captureHtmlForSnapshot, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
12
12
  import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
13
- import { Overlay } from './utils/overlay.js';
13
+ import { Overlay, OverlayPage } from './utils/overlay.js';
14
14
  import { sleep, waitForPageReadiness } from "./utils/page-readiness.js";
15
15
  import { safeFilename } from "./utils/strings.js";
16
16
  import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from "./utils/web-sandbox.js";
@@ -133,13 +133,10 @@ class Action {
133
133
  let ariaSnapshot = null;
134
134
  let ariaSnapshotFile = undefined;
135
135
  let focusedElement = null;
136
- let overlayHtml = '';
137
136
  try {
138
137
  const page = this.playwrightHelper.page;
139
138
  ariaSnapshot = await page.locator('body').ariaSnapshot();
140
139
  focusedElement = await page.evaluate(readFocusedElement);
141
- if (!frame)
142
- overlayHtml = await this.captureOverlayHtml();
143
140
  }
144
141
  catch (err) {
145
142
  debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
@@ -166,9 +163,10 @@ class Action {
166
163
  ariaSnapshot,
167
164
  ariaSnapshotFile,
168
165
  focusedElement,
169
- overlayHtml: overlayHtml || undefined,
170
166
  iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
171
167
  });
168
+ if (!frame)
169
+ await this.detectRegionOfInterest(result).catch((err) => debugLog('Region detection failed:', err.message));
172
170
  this.stateManager.updateState(result, codeBlock);
173
171
  return result;
174
172
  }
@@ -181,11 +179,64 @@ class Action {
181
179
  return new ActionResult({ url, error: msg });
182
180
  }
183
181
  }
184
- async captureOverlayHtml() {
185
- return this.playwrightHelper.page.evaluate(({ extractorSource, config }) => {
186
- const extract = new Function(`return ${extractorSource}`)();
187
- return extract(config);
188
- }, { extractorSource: getVisibleOverlayHtmlExtractorSource(), config: Overlay.captureConfig() });
182
+ async detectRegionOfInterest(result) {
183
+ const previousState = this.stateManager.getCurrentState();
184
+ if (!previousState)
185
+ return;
186
+ const previous = ActionResult.fromState(previousState);
187
+ const previousOverlay = previous.overlay;
188
+ const sameUrl = !!previous.url && result.isSameUrl({ url: previous.url });
189
+ const overlayPage = new OverlayPage(this.playwrightHelper.page);
190
+ if (result.overlay.detected && previousOverlay.detected && previousOverlay.root && previousOverlay.type === result.overlay.type && previousOverlay.name === result.overlay.name) {
191
+ result.overlay = previousOverlay;
192
+ return;
193
+ }
194
+ if (!previous.html)
195
+ return;
196
+ if (previous.html === result.html) {
197
+ if (sameUrl && previousOverlay.present && previousOverlay.xpath && !result.overlay.detected)
198
+ result.overlay = previousOverlay;
199
+ return;
200
+ }
201
+ let carried = null;
202
+ if (sameUrl && previousOverlay.present && previousOverlay.xpath) {
203
+ if (await overlayPage.isStillOpen(previousOverlay)) {
204
+ carried = previousOverlay;
205
+ }
206
+ else {
207
+ debugLog(`Region closed: ${previousOverlay.name || previousOverlay.type}`);
208
+ if (!result.overlay.detected) {
209
+ const parent = previousOverlay.parent;
210
+ if (parent?.xpath) {
211
+ const restored = new Overlay(parent);
212
+ if (await overlayPage.isStillOpen(restored))
213
+ result.overlay = restored;
214
+ }
215
+ return;
216
+ }
217
+ }
218
+ }
219
+ const diff = await result.diff(previous);
220
+ const detected = await overlayPage.detectRegion({
221
+ parts: diff.htmlParts,
222
+ pageSize: diff.pageSize,
223
+ similarity: diff.similarity,
224
+ sameUrl,
225
+ previousHtml: previous.html,
226
+ });
227
+ if (result.overlay.detected) {
228
+ if (detected)
229
+ result.overlay = result.overlay.withGeometry(detected);
230
+ return;
231
+ }
232
+ if (detected) {
233
+ result.overlay = detected;
234
+ if (carried)
235
+ result.overlay = detected.withParent(carried);
236
+ return;
237
+ }
238
+ if (carried)
239
+ result.overlay = carried;
189
240
  }
190
241
  async captureMainDocumentStatus() {
191
242
  if (this.mainDocumentStatus)
@@ -349,6 +400,14 @@ class Action {
349
400
  await recorder.reset();
350
401
  await recorder.start();
351
402
  }
403
+ if (executedSteps.length > 0) {
404
+ codeString = executedSteps.map((step) => step.command).join('\n');
405
+ }
406
+ if (!isFatalBrowserError(err)) {
407
+ const captured = await this.captureOnce({ codeBlock: codeString }).catch(() => null);
408
+ if (captured && !captured.error)
409
+ this.actionResult = captured;
410
+ }
352
411
  this.assertionSteps = [];
353
412
  throw err;
354
413
  }
@@ -448,14 +507,30 @@ async function captureTitle(page, actor) {
448
507
  }
449
508
  const ASSERTION_STEP_NAMES = new Set(['see', 'dontSee', 'seeElement', 'dontSeeElement', 'seeInField', 'dontSeeInField', 'seeInCurrentUrl', 'dontSeeInCurrentUrl']);
450
509
  export const attachStepLogger = (target, assertionsTarget) => {
510
+ const recorded = new WeakMap();
511
+ let batchFailed = false;
451
512
  const listener = (step, error) => {
452
513
  if (!step?.toCode)
453
514
  return;
454
515
  if (step.name?.startsWith('grab'))
455
516
  return;
517
+ const existing = recorded.get(step);
518
+ if (existing) {
519
+ if (!error && !existing.success) {
520
+ existing.success = true;
521
+ existing.error = undefined;
522
+ batchFailed = target.some((entry) => !entry.success);
523
+ }
524
+ return;
525
+ }
526
+ if (batchFailed)
527
+ return;
456
528
  const executed = { command: step.toCode(), success: !error };
457
- if (error)
529
+ if (error) {
458
530
  executed.error = errorToString(error);
531
+ batchFailed = true;
532
+ }
533
+ recorded.set(step, executed);
459
534
  target.push(executed);
460
535
  if (assertionsTarget && ASSERTION_STEP_NAMES.has(step.name)) {
461
536
  assertionsTarget.push({ name: step.name, args: step.args || [] });
@@ -135,7 +135,6 @@ export declare class Driller extends TaskAgent implements Agent {
135
135
  createVerifiedActionTools(baseTools: Record<string, any>, component: ComponentInfo): Record<string, any>;
136
136
  hasVerifiedAction(componentId: string): boolean;
137
137
  detectNestedOverlayContext(component: ComponentInfo, result: any): Promise<string | null>;
138
- getVisibleOverlayHtml(): Promise<string>;
139
138
  getComponentScopeHtml(component: ComponentInfo, originalState: ActionResult): Promise<string>;
140
139
  saveToKnowledge(knowledgePath: string, state: ActionResult, results: InteractionResult[]): Promise<void>;
141
140
  generateKnowledgeContent(state: ActionResult, interactions: InteractionResult[]): string;
@@ -6,10 +6,9 @@ import { setActivity } from "../activity.js";
6
6
  import { Observability } from "../observability.js";
7
7
  import { Plan, Test, TestResult } from "../test-plan.js";
8
8
  import { collectInteractiveNodes } from "../utils/aria.js";
9
- import { EXPLORBOT_ATTRS, HTML_COMPOSITE_AREA_HINTS, HTML_COMPOSITE_TARGET_ROLES, HTML_EXTRACTION_LIMITS, HTML_FORM_CONTROL_ROLES, HTML_FORM_CONTROL_TAGS, HTML_INTERACTIVE_ROLES, HTML_SELECTORS, HTML_VISIBILITY_LIMITS, getComponentScopeHtmlExtractorSource, getVisibleOverlayHtmlExtractorSource, inferHtmlRole, } from "../utils/html.js";
9
+ import { EXPLORBOT_ATTRS, HTML_COMPOSITE_AREA_HINTS, HTML_COMPOSITE_TARGET_ROLES, HTML_EXTRACTION_LIMITS, HTML_FORM_CONTROL_ROLES, HTML_FORM_CONTROL_TAGS, HTML_INTERACTIVE_ROLES, HTML_SELECTORS, getComponentScopeHtmlExtractorSource, inferHtmlRole } from "../utils/html.js";
10
10
  import { createDebug, tag } from "../utils/logger.js";
11
11
  import { loop, pause } from "../utils/loop.js";
12
- import { OVERLAY_SELECTORS } from "../utils/overlay.js";
13
12
  import { annotatePageElements } from "../utils/web-annotate.js";
14
13
  import { eidxInContainer } from "../utils/web-eidx.js";
15
14
  import { WebElement } from "../utils/web-element.js";
@@ -547,8 +546,12 @@ export class Driller extends TaskAgent {
547
546
  async detectNestedOverlayContext(component, result) {
548
547
  if (!result?.pageDiff?.ariaChanges || result.pageDiff.urlChanged)
549
548
  return null;
550
- const overlayHtml = await this.getVisibleOverlayHtml();
551
- if (!overlayHtml)
549
+ const parts = result.pageDiff.htmlParts ?? [];
550
+ let appeared = parts.filter((part) => part.added?.length > 0);
551
+ if (result.pageDiff.areaOfInterest)
552
+ appeared = parts;
553
+ const appearedHtml = appeared.map((part) => part.subtree).join('\n');
554
+ if (!appearedHtml)
552
555
  return null;
553
556
  const state = this.stateManager.getCurrentState();
554
557
  if (!state)
@@ -561,7 +564,7 @@ export class Driller extends TaskAgent {
561
564
  Keep the recorded code reusable and include the parent-opening action when the nested element requires the overlay to be open.
562
565
 
563
566
  <overlay_html>
564
- ${overlayHtml}
567
+ ${appearedHtml}
565
568
  </overlay_html>
566
569
 
567
570
  <current_page_aria>
@@ -570,21 +573,6 @@ export class Driller extends TaskAgent {
570
573
  </nested_overlay>
571
574
  `;
572
575
  }
573
- async getVisibleOverlayHtml() {
574
- return this.explorer.withPage((page) => page.evaluate(({ extractorSource, config }) => {
575
- const extract = new Function(`return ${extractorSource}`)();
576
- return extract(config);
577
- }, {
578
- extractorSource: getVisibleOverlayHtmlExtractorSource(),
579
- config: {
580
- interactiveContentSelector: HTML_SELECTORS.interactiveContent,
581
- limits: HTML_EXTRACTION_LIMITS,
582
- overlaySelectors: OVERLAY_SELECTORS.semanticOverlays,
583
- overlaySemanticSelector: OVERLAY_SELECTORS.overlaySemanticSelector,
584
- visibilityLimits: HTML_VISIBILITY_LIMITS,
585
- },
586
- }));
587
- }
588
576
  async getComponentScopeHtml(component, originalState) {
589
577
  const scopedHtml = await this.explorer.withPage((page) => page.evaluate(({ eidx, extractorSource, config }) => {
590
578
  const extract = new Function(`return ${extractorSource}`)();
@@ -74,6 +74,7 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
74
74
  statusText?: undefined;
75
75
  category?: undefined;
76
76
  errorPreview?: undefined;
77
+ extracted?: undefined;
77
78
  } | {
78
79
  success: boolean;
79
80
  status: number;
@@ -81,9 +82,11 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
81
82
  category: ResponseCategory;
82
83
  errorPreview: string;
83
84
  error?: undefined;
85
+ extracted?: undefined;
84
86
  } | {
85
87
  success: boolean;
86
88
  status: number;
89
+ extracted: Record<string, any>;
87
90
  error?: undefined;
88
91
  statusText?: undefined;
89
92
  category?: undefined;
@@ -102,6 +105,10 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
102
105
  }[];
103
106
  }, {
104
107
  finished: boolean;
108
+ error: string;
109
+ } | {
110
+ finished: boolean;
111
+ error?: undefined;
105
112
  }, import("@ai-sdk/provider-utils").Context>>;
106
113
  stop: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
107
114
  reason: any;
@@ -111,6 +118,7 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
111
118
  };
112
119
  getResult: () => FishermanResult;
113
120
  isFinished: () => boolean;
121
+ finishFromText: (text?: string) => void;
114
122
  };
115
123
  export interface FishermanResult {
116
124
  success: boolean;
@@ -119,6 +127,7 @@ export interface FishermanResult {
119
127
  type: string;
120
128
  id?: string | number;
121
129
  title?: string;
130
+ request?: string;
122
131
  }>;
123
132
  failed: Array<{
124
133
  type: string;