explorbot 0.3.1 → 0.3.4

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 (65) hide show
  1. package/bin/explorbot-cli.ts +16 -5
  2. package/dist/bin/explorbot-cli.js +14 -4
  3. package/dist/models.json +2 -0
  4. package/dist/package.json +1 -1
  5. package/dist/src/action-result.d.ts +4 -0
  6. package/dist/src/action-result.js +20 -12
  7. package/dist/src/action.d.ts +11 -0
  8. package/dist/src/action.js +28 -11
  9. package/dist/src/ai/conversation.d.ts +2 -1
  10. package/dist/src/ai/conversation.js +9 -4
  11. package/dist/src/ai/driller.js +3 -1
  12. package/dist/src/ai/navigator.d.ts +3 -0
  13. package/dist/src/ai/navigator.js +20 -4
  14. package/dist/src/ai/pilot.js +9 -2
  15. package/dist/src/ai/provider.d.ts +2 -0
  16. package/dist/src/ai/provider.js +18 -1
  17. package/dist/src/ai/researcher/deep-analysis.js +7 -3
  18. package/dist/src/ai/researcher/sections.js +0 -1
  19. package/dist/src/ai/researcher.js +0 -1
  20. package/dist/src/ai/rules.js +0 -1
  21. package/dist/src/ai/tester.d.ts +1 -0
  22. package/dist/src/ai/tester.js +9 -11
  23. package/dist/src/ai/tools.d.ts +2 -0
  24. package/dist/src/ai/tools.js +21 -4
  25. package/dist/src/commands/exit-command.js +1 -1
  26. package/dist/src/commands/init-command.js +74 -24
  27. package/dist/src/components/InitWizard.d.ts +2 -1
  28. package/dist/src/components/InitWizard.js +8 -4
  29. package/dist/src/explorbot.js +1 -0
  30. package/dist/src/explorer.js +1 -0
  31. package/dist/src/knowledge-tracker.d.ts +3 -1
  32. package/dist/src/knowledge-tracker.js +4 -4
  33. package/dist/src/state-manager.d.ts +2 -0
  34. package/dist/src/state-manager.js +3 -3
  35. package/dist/src/utils/aria.js +1 -1
  36. package/dist/src/utils/html.d.ts +2 -1
  37. package/dist/src/utils/html.js +10 -4
  38. package/dist/src/utils/overlay.d.ts +24 -0
  39. package/dist/src/utils/overlay.js +43 -0
  40. package/docs/basics/providers.md +2 -4
  41. package/models.json +2 -0
  42. package/package.json +1 -1
  43. package/src/action-result.ts +25 -15
  44. package/src/action.ts +36 -13
  45. package/src/ai/conversation.ts +11 -5
  46. package/src/ai/driller.ts +3 -1
  47. package/src/ai/navigator.ts +22 -4
  48. package/src/ai/pilot.ts +9 -2
  49. package/src/ai/provider.ts +19 -1
  50. package/src/ai/researcher/deep-analysis.ts +8 -3
  51. package/src/ai/researcher/sections.ts +0 -1
  52. package/src/ai/researcher.ts +0 -1
  53. package/src/ai/rules.ts +0 -1
  54. package/src/ai/tester.ts +9 -9
  55. package/src/ai/tools.ts +20 -4
  56. package/src/commands/exit-command.ts +1 -1
  57. package/src/commands/init-command.ts +81 -22
  58. package/src/components/InitWizard.tsx +8 -4
  59. package/src/explorbot.ts +1 -0
  60. package/src/explorer.ts +1 -0
  61. package/src/knowledge-tracker.ts +4 -4
  62. package/src/state-manager.ts +4 -3
  63. package/src/utils/aria.ts +1 -1
  64. package/src/utils/html.ts +13 -4
  65. package/src/utils/overlay.ts +51 -0
@@ -7,6 +7,7 @@ import { Command } from 'commander';
7
7
  import figureSet from 'figures';
8
8
  import { render } from 'ink';
9
9
  import React from 'react';
10
+ import { flushTelemetry } from '../src/ai/provider.js';
10
11
  import { App } from '../src/components/App.js';
11
12
  import { StatusPane } from '../src/components/StatusPane.js';
12
13
  import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
@@ -15,7 +16,7 @@ import { remote } from '../src/remote.js';
15
16
  import { Stats } from '../src/stats.js';
16
17
  import { Plan } from '../src/test-plan.js';
17
18
  import { getCliName } from '../src/utils/cli-name.ts';
18
- import { isVerboseMode, log, setPreserveConsoleLogs, setQuietMode } from '../src/utils/logger.js';
19
+ import { isVerboseMode, log, setPreserveConsoleLogs, setQuietMode, tag } from '../src/utils/logger.js';
19
20
  import { jsonToTable } from '../src/utils/markdown-parser.js';
20
21
  import { parseMarkdownToTerminal } from '../src/utils/markdown-terminal.js';
21
22
  import { type NextStepSection, printNextSteps, relativeToCwd } from '../src/utils/next-steps.ts';
@@ -29,6 +30,16 @@ const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version as stri
29
30
  program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
30
31
  remote.registerOption(program);
31
32
 
33
+ process.on('uncaughtException', async (error) => {
34
+ tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
35
+ await flushTelemetry();
36
+ process.exit(1);
37
+ });
38
+
39
+ process.on('unhandledRejection', (reason) => {
40
+ tag('error').log(`Unhandled rejection: ${reason instanceof Error ? `${reason.message}\n${reason.stack}` : String(reason)}`);
41
+ });
42
+
32
43
  if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima')) {
33
44
  console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
34
45
  }
@@ -103,9 +114,7 @@ async function startTUI(explorBot: ExplorBot): Promise<void> {
103
114
  async function showStatsAndExit(code: number): Promise<never> {
104
115
  if (remote.isAttached()) {
105
116
  await remote.close(code);
106
- process.exit(code);
107
- }
108
- if (Stats.hasActivity()) {
117
+ } else if (Stats.hasActivity()) {
109
118
  await new Promise<void>((resolve) => {
110
119
  const { unmount } = render(
111
120
  React.createElement(StatusPane, {
@@ -121,6 +130,7 @@ async function showStatsAndExit(code: number): Promise<never> {
121
130
  );
122
131
  });
123
132
  }
133
+ await flushTelemetry();
124
134
  process.exit(code);
125
135
  }
126
136
 
@@ -546,6 +556,7 @@ program
546
556
  .command('learn [url] [description]')
547
557
  .description('Add knowledge for URLs')
548
558
  .option('-p, --path <path>', 'Working directory path')
559
+ .option('--replace', 'Replace existing knowledge for this URL instead of appending')
549
560
  .action(async (url, description, options) => {
550
561
  try {
551
562
  await ConfigParser.getInstance().loadConfig({
@@ -556,7 +567,7 @@ program
556
567
  const tracker = new KnowledgeTracker();
557
568
 
558
569
  if (url && description) {
559
- const result = tracker.addKnowledge(url, description);
570
+ const result = tracker.addKnowledge(url, description, { replace: options.replace });
560
571
  const action = result.isNewFile ? 'Created' : 'Updated';
561
572
  console.log(`Knowledge ${action} in: ${result.filename}`);
562
573
  return;
@@ -7,6 +7,7 @@ import { Command } from 'commander';
7
7
  import figureSet from 'figures';
8
8
  import { render } from 'ink';
9
9
  import React from 'react';
10
+ import { flushTelemetry } from '../src/ai/provider.js';
10
11
  import { App } from '../src/components/App.js';
11
12
  import { StatusPane } from '../src/components/StatusPane.js';
12
13
  import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
@@ -15,7 +16,7 @@ import { remote } from '../src/remote.js';
15
16
  import { Stats } from '../src/stats.js';
16
17
  import { Plan } from '../src/test-plan.js';
17
18
  import { getCliName } from "../src/utils/cli-name.js";
18
- import { isVerboseMode, log, setPreserveConsoleLogs, setQuietMode } from '../src/utils/logger.js';
19
+ import { isVerboseMode, log, setPreserveConsoleLogs, setQuietMode, tag } from '../src/utils/logger.js';
19
20
  import { jsonToTable } from '../src/utils/markdown-parser.js';
20
21
  import { parseMarkdownToTerminal } from '../src/utils/markdown-terminal.js';
21
22
  import { printNextSteps, relativeToCwd } from "../src/utils/next-steps.js";
@@ -25,6 +26,14 @@ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../p
25
26
  const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
26
27
  program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
27
28
  remote.registerOption(program);
29
+ process.on('uncaughtException', async (error) => {
30
+ tag('error').log(`Uncaught exception: ${error instanceof Error ? `${error.message}\n${error.stack}` : String(error)}`);
31
+ await flushTelemetry();
32
+ process.exit(1);
33
+ });
34
+ process.on('unhandledRejection', (reason) => {
35
+ tag('error').log(`Unhandled rejection: ${reason instanceof Error ? `${reason.message}\n${reason.stack}` : String(reason)}`);
36
+ });
28
37
  if (!process.env.EXPLORBOT_NO_BANNER && !process.argv.includes('prima')) {
29
38
  console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
30
39
  }
@@ -78,9 +87,8 @@ async function startTUI(explorBot) {
78
87
  async function showStatsAndExit(code) {
79
88
  if (remote.isAttached()) {
80
89
  await remote.close(code);
81
- process.exit(code);
82
90
  }
83
- if (Stats.hasActivity()) {
91
+ else if (Stats.hasActivity()) {
84
92
  await new Promise((resolve) => {
85
93
  const { unmount } = render(React.createElement(StatusPane, {
86
94
  onComplete: () => {
@@ -93,6 +101,7 @@ async function showStatsAndExit(code) {
93
101
  });
94
102
  });
95
103
  }
104
+ await flushTelemetry();
96
105
  process.exit(code);
97
106
  }
98
107
  addCommonOptions(program.command('start [path]').description('Start web exploration')).action(async (startPath, options) => {
@@ -492,6 +501,7 @@ program
492
501
  .command('learn [url] [description]')
493
502
  .description('Add knowledge for URLs')
494
503
  .option('-p, --path <path>', 'Working directory path')
504
+ .option('--replace', 'Replace existing knowledge for this URL instead of appending')
495
505
  .action(async (url, description, options) => {
496
506
  try {
497
507
  await ConfigParser.getInstance().loadConfig({
@@ -500,7 +510,7 @@ program
500
510
  const { KnowledgeTracker } = await import('../src/knowledge-tracker.js');
501
511
  const tracker = new KnowledgeTracker();
502
512
  if (url && description) {
503
- const result = tracker.addKnowledge(url, description);
513
+ const result = tracker.addKnowledge(url, description, { replace: options.replace });
504
514
  const action = result.isNewFile ? 'Created' : 'Updated';
505
515
  console.log(`Knowledge ${action} in: ${result.filename}`);
506
516
  return;
package/dist/models.json CHANGED
@@ -18,6 +18,8 @@
18
18
  "agenticModel": "gpt-5.6-luna"
19
19
  },
20
20
  "anthropic": {
21
+ "model": "claude-haiku-4-5-20251001",
22
+ "visionModel": "claude-haiku-4-5-20251001",
21
23
  "agenticModel": "claude-haiku-4-5-20251001"
22
24
  },
23
25
  "mistral": {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.3.1",
3
+ "version": "0.3.4",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -2,6 +2,7 @@ import { type HtmlConfig } from './config.js';
2
2
  import type { Link, WebPageState } from './state-manager.js';
3
3
  import { TTLCache } from './utils/cache.js';
4
4
  import { type HtmlDiffPart, type HtmlDiffResult } from './utils/html-diff.js';
5
+ import { Overlay } from './utils/overlay.js';
5
6
  interface ActionResultData extends WebPageState {
6
7
  html?: string;
7
8
  fullUrl?: string | undefined;
@@ -28,6 +29,7 @@ interface ActionResultData extends WebPageState {
28
29
  focusedElement?: FocusedElement | null;
29
30
  iframeURL?: string;
30
31
  links?: Link[];
32
+ overlayHtml?: string;
31
33
  }
32
34
  export interface PageDiff {
33
35
  urlChanged: boolean;
@@ -81,6 +83,7 @@ export declare class ActionResult implements ActionResultData {
81
83
  notes: string[];
82
84
  links: Link[];
83
85
  verifications?: Record<string, boolean>;
86
+ overlay: Overlay;
84
87
  constructor(data: ActionResultData);
85
88
  get hash(): string;
86
89
  get html(): string;
@@ -132,6 +135,7 @@ export declare class Diff {
132
135
  isSameUrl(): boolean;
133
136
  urlHasChanged(): boolean;
134
137
  get htmlParts(): HtmlDiffPart[];
138
+ cleanedHtmlParts(): Promise<HtmlDiffPart[]>;
135
139
  get ariaChanged(): string | null;
136
140
  get ariaChangeCount(): number;
137
141
  get htmlDiff(): HtmlDiffResult | null;
@@ -5,6 +5,7 @@ import { TTLCache } from "./utils/cache.js";
5
5
  import { htmlDiff, liveRegionMessages } from "./utils/html-diff.js";
6
6
  import { extractHeadings, extractLinks, extractTargetedHtml, htmlCombinedSnapshot, htmlMinimalUISnapshot, htmlTextSnapshot, minifyHtml } from "./utils/html.js";
7
7
  import { createDebug } from "./utils/logger.js";
8
+ import { Overlay } from "./utils/overlay.js";
8
9
  import { slugify } from "./utils/strings.js";
9
10
  import { extractStatePath, matchesUrl } from "./utils/url-matcher.js";
10
11
  const debugLog = createDebug('explorbot:state');
@@ -37,6 +38,7 @@ export class ActionResult {
37
38
  notes = [];
38
39
  links = [];
39
40
  verifications;
41
+ overlay = new Overlay();
40
42
  constructor(data) {
41
43
  this.id = data.id;
42
44
  this.timestamp = data.timestamp ?? new Date();
@@ -77,6 +79,7 @@ export class ActionResult {
77
79
  if (data.ariaSnapshot !== undefined) {
78
80
  this._ariaSnapshot = data.ariaSnapshot;
79
81
  }
82
+ this.overlay = Overlay.resolve(data);
80
83
  if (!this.fullUrl && this.url) {
81
84
  this.fullUrl = this.url;
82
85
  }
@@ -441,17 +444,9 @@ export class ActionResult {
441
444
  pageDiff.ariaChangeCount = diff.ariaChangeCount;
442
445
  }
443
446
  if (diff.htmlParts.length > 0) {
444
- const htmlConfig = this.normalizeHtmlConfig();
445
- const processedParts = [];
446
- for (const part of diff.htmlParts) {
447
- const filteredHtml = htmlCombinedSnapshot(part.subtree, htmlConfig?.combined);
448
- const minified = await minifyHtml(filteredHtml);
449
- if (minified) {
450
- processedParts.push({ ...part, subtree: minified });
451
- }
452
- }
453
- if (processedParts.length > 0) {
454
- pageDiff.htmlParts = collapseHtmlParts(processedParts);
447
+ const collapsed = collapseHtmlParts(await diff.cleanedHtmlParts());
448
+ if (collapsed.length > 0) {
449
+ pageDiff.htmlParts = collapsed;
455
450
  }
456
451
  }
457
452
  if (pageDiff.ariaChanges && this.iframeSnapshots.length > 0) {
@@ -487,7 +482,9 @@ function collapseHtmlParts(parts) {
487
482
  const total = parts.reduce((sum, p) => sum + p.subtree.length, 0);
488
483
  const fullPageReRender = total > HTML_PARTS_TOTAL_BUDGET || parts.length > HTML_PARTS_COUNT_LIMIT;
489
484
  if (fullPageReRender) {
490
- return parts.map((part) => ({
485
+ return parts
486
+ .filter((part) => part.added.length > 0 || part.removed.length > 0)
487
+ .map((part) => ({
491
488
  ...part,
492
489
  subtree: `<html><head></head><body>...collapsed (${part.subtree.length} chars, ${part.added.length} added, ${part.removed.length} removed)...</body></html>`,
493
490
  }));
@@ -540,6 +537,17 @@ export class Diff {
540
537
  return [];
541
538
  return this._htmlDiffResult.parts;
542
539
  }
540
+ async cleanedHtmlParts() {
541
+ const htmlConfig = ConfigParser.getInstance().getConfig().html;
542
+ const cleaned = [];
543
+ for (const part of this.htmlParts) {
544
+ const minified = await minifyHtml(htmlCombinedSnapshot(part.subtree, htmlConfig?.combined));
545
+ if (!minified)
546
+ continue;
547
+ cleaned.push({ ...part, subtree: minified });
548
+ }
549
+ return cleaned;
550
+ }
543
551
  get ariaChanged() {
544
552
  return this._ariaDiffResult;
545
553
  }
@@ -15,6 +15,7 @@ declare class Action {
15
15
  name: string;
16
16
  args: any[];
17
17
  }>;
18
+ executedSteps: ExecutedStep[];
18
19
  lastValue: unknown;
19
20
  recorder?: PlaywrightRecorder;
20
21
  recovery: RecoveryRunner;
@@ -32,6 +33,7 @@ declare class Action {
32
33
  includeScreenshot?: boolean;
33
34
  codeBlock?: string;
34
35
  }): Promise<ActionResult>;
36
+ captureOverlayHtml(): Promise<string>;
35
37
  captureMainDocumentStatus(): Promise<number | undefined>;
36
38
  captureResponses(): () => void;
37
39
  recordNetworkCall(request: any, status: number): void;
@@ -57,3 +59,12 @@ export type RecoveryRunner = <T>(fn: () => Promise<T>) => Promise<T>;
57
59
  export interface ExecuteOptions {
58
60
  verbatim?: boolean;
59
61
  }
62
+ export declare const attachStepLogger: (target: ExecutedStep[], assertionsTarget?: Array<{
63
+ name: string;
64
+ args: any[];
65
+ }>) => (() => void);
66
+ export interface ExecutedStep {
67
+ command: string;
68
+ success: boolean;
69
+ error?: string;
70
+ }
@@ -8,8 +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, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
11
+ import { captureHtmlForSnapshot, getVisibleOverlayHtmlExtractorSource, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
12
12
  import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
13
+ import { Overlay } from './utils/overlay.js';
13
14
  import { sleep, waitForPageReadiness } from "./utils/page-readiness.js";
14
15
  import { safeFilename } from "./utils/strings.js";
15
16
  import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from "./utils/web-sandbox.js";
@@ -30,6 +31,7 @@ class Action {
30
31
  playwrightHelper;
31
32
  playwrightGroupId = null;
32
33
  assertionSteps = [];
34
+ executedSteps = [];
33
35
  lastValue;
34
36
  recorder;
35
37
  recovery;
@@ -131,10 +133,13 @@ class Action {
131
133
  let ariaSnapshot = null;
132
134
  let ariaSnapshotFile = undefined;
133
135
  let focusedElement = null;
136
+ let overlayHtml = '';
134
137
  try {
135
138
  const page = this.playwrightHelper.page;
136
139
  ariaSnapshot = await page.locator('body').ariaSnapshot();
137
140
  focusedElement = await page.evaluate(readFocusedElement);
141
+ if (!frame)
142
+ overlayHtml = await this.captureOverlayHtml();
138
143
  }
139
144
  catch (err) {
140
145
  debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
@@ -161,6 +166,7 @@ class Action {
161
166
  ariaSnapshot,
162
167
  ariaSnapshotFile,
163
168
  focusedElement,
169
+ overlayHtml: overlayHtml || undefined,
164
170
  iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
165
171
  });
166
172
  this.stateManager.updateState(result, codeBlock);
@@ -175,6 +181,12 @@ class Action {
175
181
  return new ActionResult({ url, error: msg });
176
182
  }
177
183
  }
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() });
189
+ }
178
190
  async captureMainDocumentStatus() {
179
191
  if (this.mainDocumentStatus)
180
192
  return this.mainDocumentStatus;
@@ -293,7 +305,7 @@ class Action {
293
305
  let codeString = code.replace(/^\(I\) => /, '').trim();
294
306
  const executedSteps = [];
295
307
  const assertionSteps = [];
296
- const stepListener = attachStepLogger(executedSteps, assertionSteps);
308
+ const detachSteps = attachStepLogger(executedSteps, assertionSteps);
297
309
  const groupId = this.recorder ? await this.recorder.beginAction(codeString) : null;
298
310
  this.playwrightGroupId = groupId;
299
311
  const detachResponses = this.captureResponses();
@@ -319,10 +331,12 @@ class Action {
319
331
  await recorder.add(() => sleep(this.config.action?.delay || 500));
320
332
  await recorder.promise();
321
333
  this.lastValue = await returned;
334
+ if (!recorder.isRunning())
335
+ throw new Error('CodeceptJS recorder is stopped, commands were skipped and never reached the browser');
322
336
  }
323
337
  this.restorePageTimeout();
324
338
  if (executedSteps.length > 0) {
325
- codeString = executedSteps.join('\n');
339
+ codeString = executedSteps.map((step) => step.command).join('\n');
326
340
  }
327
341
  const pageState = await this.captureOnce({ codeBlock: codeString });
328
342
  this.actionResult = pageState;
@@ -339,11 +353,12 @@ class Action {
339
353
  throw err;
340
354
  }
341
355
  finally {
356
+ this.executedSteps = executedSteps;
342
357
  this.restorePageTimeout();
343
358
  detachResponses();
344
359
  if (groupId)
345
360
  await this.recorder.endAction();
346
- detachStepLogger(stepListener);
361
+ detachSteps();
347
362
  if (stepSpan) {
348
363
  stepSpan.end();
349
364
  }
@@ -432,13 +447,16 @@ async function captureTitle(page, actor) {
432
447
  return '';
433
448
  }
434
449
  const ASSERTION_STEP_NAMES = new Set(['see', 'dontSee', 'seeElement', 'dontSeeElement', 'seeInField', 'dontSeeInField', 'seeInCurrentUrl', 'dontSeeInCurrentUrl']);
435
- const attachStepLogger = (target, assertionsTarget) => {
450
+ export const attachStepLogger = (target, assertionsTarget) => {
436
451
  const listener = (step, error) => {
437
452
  if (!step?.toCode)
438
453
  return;
439
454
  if (step.name?.startsWith('grab'))
440
455
  return;
441
- target.push(step.toCode());
456
+ const executed = { command: step.toCode(), success: !error };
457
+ if (error)
458
+ executed.error = errorToString(error);
459
+ target.push(executed);
442
460
  if (assertionsTarget && ASSERTION_STEP_NAMES.has(step.name)) {
443
461
  assertionsTarget.push({ name: step.name, args: step.args || [] });
444
462
  }
@@ -450,11 +468,10 @@ const attachStepLogger = (target, assertionsTarget) => {
450
468
  };
451
469
  codeceptjs.event.dispatcher.on(codeceptjs.event.step.passed, listener);
452
470
  codeceptjs.event.dispatcher.on(codeceptjs.event.step.failed, listener);
453
- return listener;
454
- };
455
- const detachStepLogger = (listener) => {
456
- codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, listener);
457
- codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
471
+ return () => {
472
+ codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, listener);
473
+ codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
474
+ };
458
475
  };
459
476
  const readFocusedElement = () => {
460
477
  const el = document.activeElement;
@@ -4,8 +4,9 @@ export interface ToolExecution {
4
4
  input: any;
5
5
  output: any;
6
6
  wasSuccessful: boolean;
7
+ reasoning?: string;
7
8
  }
8
- export declare function toToolExecution(toolName: string, input: any, rawOutput: any): ToolExecution;
9
+ export declare function toToolExecution(toolName: string, input: any, rawOutput: any, reasoning?: string): ToolExecution;
9
10
  export declare function toolExecutionLabel(input: Record<string, any> | undefined): string;
10
11
  export declare const NARRATION_TOOL = "commentary";
11
12
  export declare class Conversation {
@@ -1,8 +1,8 @@
1
- export function toToolExecution(toolName, input, rawOutput) {
1
+ export function toToolExecution(toolName, input, rawOutput, reasoning) {
2
2
  let output = rawOutput;
3
3
  if (rawOutput?.type === 'json' && rawOutput?.value)
4
4
  output = rawOutput.value;
5
- return { toolName, input, output, wasSuccessful: output?.success !== false };
5
+ return { toolName, input, output, wasSuccessful: output?.success !== false, reasoning };
6
6
  }
7
7
  export function toolExecutionLabel(input) {
8
8
  return input?.explanation || input?.assertion || input?.reason || input?.request || '';
@@ -189,10 +189,14 @@ export class Conversation {
189
189
  continue;
190
190
  if (!Array.isArray(message.content))
191
191
  continue;
192
+ const reasoning = message.content
193
+ .filter((part) => part.type === 'reasoning' && part.text?.trim())
194
+ .map((part) => part.text.trim())
195
+ .join('\n');
192
196
  for (const part of message.content) {
193
197
  if (part.type !== 'tool-call')
194
198
  continue;
195
- toolCalls.set(part.toolCallId, part.input);
199
+ toolCalls.set(part.toolCallId, { input: part.input, reasoning });
196
200
  }
197
201
  }
198
202
  const executions = [];
@@ -206,7 +210,8 @@ export class Conversation {
206
210
  continue;
207
211
  if (part.toolName === NARRATION_TOOL)
208
212
  continue;
209
- executions.push(toToolExecution(part.toolName, toolCalls.get(part.toolCallId) || {}, part.output));
213
+ const call = toolCalls.get(part.toolCallId);
214
+ executions.push(toToolExecution(part.toolName, call?.input || {}, part.output, call?.reasoning));
210
215
  }
211
216
  }
212
217
  return executions;
@@ -9,6 +9,7 @@ import { collectInteractiveNodes } from "../utils/aria.js";
9
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";
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";
12
13
  import { annotatePageElements } from "../utils/web-annotate.js";
13
14
  import { eidxInContainer } from "../utils/web-eidx.js";
14
15
  import { WebElement } from "../utils/web-element.js";
@@ -578,7 +579,8 @@ export class Driller extends TaskAgent {
578
579
  config: {
579
580
  interactiveContentSelector: HTML_SELECTORS.interactiveContent,
580
581
  limits: HTML_EXTRACTION_LIMITS,
581
- overlaySelectors: HTML_SELECTORS.semanticOverlays,
582
+ overlaySelectors: OVERLAY_SELECTORS.semanticOverlays,
583
+ overlaySemanticSelector: OVERLAY_SELECTORS.overlaySemanticSelector,
582
584
  visibilityLimits: HTML_VISIBILITY_LIMITS,
583
585
  },
584
586
  }));
@@ -1,5 +1,6 @@
1
1
  import { ActionResult } from '../action-result.js';
2
2
  import type Action from '../action.js';
3
+ import type { ExecutedStep } from '../action.js';
3
4
  import type { ExplorbotConfig } from '../config.js';
4
5
  import type { ExperienceTracker } from '../experience-tracker.js';
5
6
  import Explorer from '../explorer.js';
@@ -16,6 +17,7 @@ declare class Navigator implements Agent {
16
17
  hooksRunner: HooksRunner;
17
18
  MAX_ATTEMPTS: number;
18
19
  lastFailureReason: string | null;
20
+ executedSteps: ExecutedStep[];
19
21
  systemPrompt: string;
20
22
  freeSailSystemPrompt: string;
21
23
  explorer: Explorer;
@@ -55,6 +57,7 @@ declare class Navigator implements Agent {
55
57
  freshState: ActionResult;
56
58
  urlMatches: boolean;
57
59
  }>;
60
+ targetUrlReached(action: Action, expectedUrl: string, state: ActionResult): boolean;
58
61
  ariaDiff(freshState: ActionResult, previous: ActionResult): Promise<string | null>;
59
62
  saveFlow(message: string, expectedUrl: string | undefined, actionResult: ActionResult, progressBlocks: string[]): void;
60
63
  rescueDelayedRedirect(action: Action, expectedUrl: string): Promise<boolean>;
@@ -2,8 +2,8 @@ import { tool } from 'ai';
2
2
  import dedent from 'dedent';
3
3
  import { z } from 'zod';
4
4
  import { ActionResult } from '../action-result.js';
5
- import { normalizeUrl } from '../state-manager.js';
6
5
  import { renderAssertion } from "../playwright-recorder.js";
6
+ import { normalizeUrl } from '../state-manager.js';
7
7
  import { isFatalBrowserError } from "../utils/browser-errors.js";
8
8
  import { getCliName } from "../utils/cli-name.js";
9
9
  import { extractCodeBlocks } from '../utils/code-extractor.js';
@@ -26,6 +26,7 @@ class Navigator {
26
26
  hooksRunner;
27
27
  MAX_ATTEMPTS = Number.parseInt(process.env.MAX_ATTEMPTS || '5');
28
28
  lastFailureReason = null;
29
+ executedSteps = [];
29
30
  systemPrompt = dedent `
30
31
  <role>
31
32
  You are senior test automation engineer with master QA skills.
@@ -194,10 +195,15 @@ class Navigator {
194
195
  if (!this.provider)
195
196
  throw new Error('AI-assisted recovery is unavailable: no AI model is configured.');
196
197
  this.lastFailureReason = null;
198
+ this.executedSteps = [];
197
199
  tag('info').log('AI Navigator resolving state at', actionResult.url);
198
200
  debugLog('Resolution message:', message);
199
201
  const action = opts?.action ?? this.explorer.action();
200
202
  const expectedUrl = opts?.expectedUrl;
203
+ if (expectedUrl && this.targetUrlReached(action, expectedUrl, actionResult)) {
204
+ tag('success').log(`Already at ${expectedUrl} — navigation resolved`);
205
+ return true;
206
+ }
201
207
  const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
202
208
  const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
203
209
  conversation.addUserText(await this.buildResolutionPrompt(message, actionResult, opts?.experience));
@@ -278,14 +284,20 @@ class Navigator {
278
284
  const freshHash = check.freshState.getStateHash();
279
285
  resolved = check.urlMatches && freshHash !== actionResult.getStateHash();
280
286
  if (!resolved && attempt.ok) {
281
- lastFailure = `URL did not change (still ${check.freshState.url})`;
287
+ if (check.urlMatches) {
288
+ lastFailure = `Reached ${check.freshState.url} but the page state did not change`;
289
+ tag('warning').log(`Page state did not change at ${check.freshState.url}`);
290
+ }
291
+ else {
292
+ lastFailure = `Reached ${check.freshState.url}, expected ${expectedUrl}`;
293
+ tag('warning').log(`URL verification failed: expected ${expectedUrl}, got ${check.freshState.url}`);
294
+ }
282
295
  batchFailures.push({
283
296
  code: codeBlock,
284
297
  error: lastFailure,
285
298
  ariaChanges: await this.ariaDiff(check.freshState, prevActionResult),
286
299
  urlAfter: check.freshState.url,
287
300
  });
288
- tag('warning').log(`URL verification failed: expected ${expectedUrl}, got ${check.freshState.url}`);
289
301
  }
290
302
  if (freshHash !== prevHash && (attempt.ok || check.urlMatches)) {
291
303
  progressBlocks.push(codeBlock);
@@ -411,6 +423,7 @@ class Navigator {
411
423
  await action.exitIframe();
412
424
  debugLog(`Attempting resolution: ${codeBlock}`);
413
425
  const ok = await action.attempt(codeBlock, message);
426
+ this.executedSteps.push(...action.executedSteps);
414
427
  const page = action.playwrightHelper?.page;
415
428
  if (page) {
416
429
  try {
@@ -437,9 +450,12 @@ class Navigator {
437
450
  }
438
451
  }
439
452
  const freshState = await this.explorer.capture();
440
- const urlMatches = this.isSameExpectedOrigin(expectedUrl, action.stateManager) && matchesNavigationUrl(expectedUrl, this.comparableUrl(freshState, expectedUrl));
453
+ const urlMatches = this.targetUrlReached(action, expectedUrl, freshState);
441
454
  return { freshState, urlMatches };
442
455
  }
456
+ targetUrlReached(action, expectedUrl, state) {
457
+ return this.isSameExpectedOrigin(expectedUrl, action.stateManager) && matchesNavigationUrl(expectedUrl, this.comparableUrl(state, expectedUrl));
458
+ }
443
459
  async ariaDiff(freshState, previous) {
444
460
  if (freshState.getStateHash() === previous.getStateHash())
445
461
  return null;
@@ -5,7 +5,7 @@ import { ActionResult } from "../action-result.js";
5
5
  import { ConfigParser } from "../config.js";
6
6
  import { Stats } from "../stats.js";
7
7
  import { TestResult } from "../test-plan.js";
8
- import { collectInteractiveNodes, detectFocusArea } from "../utils/aria.js";
8
+ import { collectInteractiveNodes } from "../utils/aria.js";
9
9
  import { ErrorPageError } from "../utils/error-page.js";
10
10
  import { createDebug, tag } from "../utils/logger.js";
11
11
  const debugLog = createDebug('explorbot:pilot');
@@ -16,6 +16,7 @@ import { withdrawVisionTools } from "./tools.js";
16
16
  const CHECK_TOOLS = ['verify', 'see', 'research'];
17
17
  const EVIDENCE_TOOLS = ['verify', 'see'];
18
18
  const META_TOOLS = ['record', 'reset', 'stop', 'finish'];
19
+ const PILOT_REASONING_LIMIT = 500;
19
20
  const PILOT_MESSAGE_LIMIT = 2;
20
21
  const PILOT_MESSAGE_MAX_LENGTH = 160;
21
22
  export class Pilot {
@@ -738,7 +739,7 @@ export class Pilot {
738
739
  lines.push(`h2: ${state.h2 || ''}`);
739
740
  lines.push(`h3: ${state.h3 || ''}`);
740
741
  lines.push(`h4: ${state.h4 || ''}`);
741
- const focusArea = detectFocusArea(state.ariaSnapshot);
742
+ const focusArea = state.overlay;
742
743
  if (focusArea.detected) {
743
744
  lines.push(`modal: ${focusArea.name || focusArea.type}`);
744
745
  }
@@ -941,6 +942,12 @@ export class Pilot {
941
942
  line += `\n result: ${resultMessage}`;
942
943
  if (errorDetail && errorDetail !== resultMessage)
943
944
  line += `\n error: ${errorDetail}`;
945
+ if (!t.wasSuccessful && t.reasoning) {
946
+ let rationale = t.reasoning;
947
+ if (rationale.length > PILOT_REASONING_LIMIT)
948
+ rationale = `...${rationale.slice(-PILOT_REASONING_LIMIT)}`;
949
+ line += `\n tester reasoned: ${rationale.replace(/\n+/g, ' ')}`;
950
+ }
944
951
  const attempts = t.output?.attempts;
945
952
  if (attempts && attempts.length > 1 && t.wasSuccessful) {
946
953
  const failedBefore = attempts.filter((a) => !a.success);
@@ -7,6 +7,7 @@ declare class AiError extends Error {
7
7
  }
8
8
  export declare class ContextLengthError extends Error {
9
9
  }
10
+ export declare function flushTelemetry(): Promise<void>;
10
11
  export declare class Provider {
11
12
  config: AIConfig;
12
13
  telemetryEnabled: boolean;
@@ -17,6 +18,7 @@ export declare class Provider {
17
18
  modelCallWaiters: (() => void)[];
18
19
  constructor(config: AIConfig);
19
20
  validateConnection(): Promise<void>;
21
+ stop(): Promise<void>;
20
22
  getModelForAgent(agentName?: string): any;
21
23
  getAgenticModel(agentName?: string): any;
22
24
  getVisionModel(): any;