explorbot 0.1.25 → 0.1.27

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 (54) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/ai/historian/codeceptjs.js +1 -1
  3. package/dist/src/ai/historian/experience.js +1 -1
  4. package/dist/src/ai/historian/playwright.js +1 -1
  5. package/dist/src/ai/historian/screencast.js +3 -3
  6. package/dist/src/ai/historian.js +1 -1
  7. package/dist/src/ai/navigator.js +4 -4
  8. package/dist/src/ai/pilot.js +8 -5
  9. package/dist/src/ai/planner.js +2 -4
  10. package/dist/src/ai/provider.js +5 -3
  11. package/dist/src/ai/researcher/cache.js +8 -0
  12. package/dist/src/ai/researcher/deep-analysis.js +146 -61
  13. package/dist/src/ai/researcher/locators.js +1 -1
  14. package/dist/src/ai/researcher.js +4 -4
  15. package/dist/src/ai/session-analyst.js +4 -1
  16. package/dist/src/ai/task-agent.js +2 -2
  17. package/dist/src/commands/context-aria-command.js +1 -1
  18. package/dist/src/commands/explore-command.js +67 -25
  19. package/dist/src/commands/test-command.js +2 -1
  20. package/dist/src/components/App.js +1 -33
  21. package/dist/src/components/LogPane.js +9 -3
  22. package/dist/src/experience-tracker.js +2 -2
  23. package/dist/src/explorbot.js +1 -1
  24. package/dist/src/reporter.js +24 -5
  25. package/dist/src/utils/log-filters.js +27 -0
  26. package/dist/src/utils/logger.js +28 -1
  27. package/dist/src/utils/next-steps.js +1 -7
  28. package/package.json +1 -1
  29. package/src/ai/historian/codeceptjs.ts +1 -1
  30. package/src/ai/historian/experience.ts +1 -1
  31. package/src/ai/historian/playwright.ts +1 -1
  32. package/src/ai/historian/screencast.ts +3 -3
  33. package/src/ai/historian.ts +1 -1
  34. package/src/ai/navigator.ts +4 -4
  35. package/src/ai/pilot.ts +8 -5
  36. package/src/ai/planner.ts +1 -3
  37. package/src/ai/provider.ts +5 -3
  38. package/src/ai/researcher/cache.ts +7 -0
  39. package/src/ai/researcher/deep-analysis.ts +166 -68
  40. package/src/ai/researcher/locators.ts +1 -1
  41. package/src/ai/researcher.ts +4 -4
  42. package/src/ai/session-analyst.ts +4 -1
  43. package/src/ai/task-agent.ts +2 -2
  44. package/src/commands/context-aria-command.ts +1 -1
  45. package/src/commands/explore-command.ts +68 -23
  46. package/src/commands/test-command.ts +2 -1
  47. package/src/components/App.tsx +0 -33
  48. package/src/components/LogPane.tsx +18 -3
  49. package/src/experience-tracker.ts +2 -2
  50. package/src/explorbot.ts +1 -1
  51. package/src/reporter.ts +24 -6
  52. package/src/utils/log-filters.ts +26 -0
  53. package/src/utils/logger.ts +24 -2
  54. package/src/utils/next-steps.ts +1 -6
@@ -7,7 +7,6 @@ import { TestResult } from '../test-plan.js';
7
7
  import { getCliName } from "../utils/cli-name.js";
8
8
  import { ErrorPageError } from "../utils/error-page.js";
9
9
  import { tag } from '../utils/logger.js';
10
- import { jsonToTable } from '../utils/markdown-parser.js';
11
10
  import { printNextSteps, relativeToCwd } from "../utils/next-steps.js";
12
11
  import { safeFilename } from "../utils/strings.js";
13
12
  import { BaseCommand } from './base-command.js';
@@ -85,14 +84,14 @@ export class ExploreCommand extends BaseCommand {
85
84
  const t = tests[i];
86
85
  lines.push(` ${String(i + 1).padStart(2)}. [${this.originLabel(t)}] [${t.priority.padEnd(9)}] ${t.scenario}`);
87
86
  }
88
- tag('multiline').log(lines.join('\n'));
87
+ tag('multiline').log(lines.join('\n'), { maxLines: 24 });
89
88
  }
90
89
  async runFreshMode(mainUrl, feature, styles) {
91
90
  await this.runAllStyles(mainUrl, feature, undefined, undefined, styles);
91
+ this.rememberCurrentPlan();
92
92
  const mainPlan = this.explorBot.getCurrentPlan();
93
93
  if (!mainPlan)
94
94
  return;
95
- this.completedPlans.push(mainPlan);
96
95
  if (feature || this.isLimitReached())
97
96
  return;
98
97
  await this.discoverNewSubPages(mainPlan, mainUrl, styles, new Set());
@@ -264,6 +263,8 @@ export class ExploreCommand extends BaseCommand {
264
263
  const styleList = styles ?? Object.keys(getStyles());
265
264
  let fresh = true;
266
265
  for (const style of styleList) {
266
+ if (this.isLimitReached())
267
+ break;
267
268
  if (!fresh && pageUrl && !this.dryRun) {
268
269
  await this.explorBot.visit(pageUrl);
269
270
  }
@@ -274,9 +275,20 @@ export class ExploreCommand extends BaseCommand {
274
275
  opts.noSave = true;
275
276
  await this.planWithRetry(feature, opts, pageUrl);
276
277
  await this.runPendingTests();
278
+ this.rememberCurrentPlan();
277
279
  fresh = false;
278
280
  }
279
281
  }
282
+ rememberCurrentPlan() {
283
+ const plan = this.explorBot.getCurrentPlan();
284
+ if (!plan)
285
+ return;
286
+ if (this.completedPlans.includes(plan))
287
+ return;
288
+ if (plan.tests.every((test) => test.startTime == null))
289
+ return;
290
+ this.completedPlans.push(plan);
291
+ }
280
292
  async planWithRetry(feature, opts, pageUrl) {
281
293
  const before = new Set(this.explorBot.getCurrentPlan()?.tests ?? []);
282
294
  await this.explorBot.plan(feature, opts);
@@ -394,9 +406,9 @@ export class ExploreCommand extends BaseCommand {
394
406
  const allTests = this.completedPlans.flatMap((plan) => plan.tests.filter((t) => t.startTime != null).map((test) => ({ test, planTitle: plan.title }))).sort((a, b) => (a.test.startTime ?? 0) - (b.test.startTime ?? 0));
395
407
  if (allTests.length === 0)
396
408
  return;
397
- const hasSubPages = this.completedPlans.length > 1;
409
+ const hasSubPages = new Set(this.completedPlans.map((plan) => plan.title)).size > 1;
398
410
  const hasOrigin = this.oldTestRefs.size > 0;
399
- const rows = allTests.map(({ test, planTitle }, index) => {
411
+ const completed = allTests.map(({ test, planTitle }, index) => {
400
412
  const durationMs = test.getDurationMs();
401
413
  const duration = durationMs != null ? `${(durationMs / 1000).toFixed(1)}s` : '-';
402
414
  let status = 'failed';
@@ -404,28 +416,51 @@ export class ExploreCommand extends BaseCommand {
404
416
  status = 'passed';
405
417
  else if (test.isSkipped)
406
418
  status = 'skipped';
407
- const row = {
408
- '#': String(index + 1),
409
- Status: status,
410
- Title: test.scenario.replace(/\|/g, '-'),
411
- Priority: test.priority,
412
- Time: duration,
413
- Steps: String(Object.keys(test.notes).length),
419
+ return {
420
+ index: index + 1,
421
+ status,
422
+ title: test.scenario.replace(/\s+/g, ' ').trim(),
423
+ priority: test.priority,
424
+ duration,
425
+ durationMs: durationMs ?? 0,
426
+ steps: Object.keys(test.notes).length,
427
+ origin: hasOrigin ? this.originLabel(test) : '',
428
+ planTitle: hasSubPages ? planTitle : '',
414
429
  };
415
- if (hasOrigin) {
416
- row.Origin = this.originLabel(test);
430
+ });
431
+ const passed = completed.filter((t) => t.status === 'passed').length;
432
+ const failed = completed.filter((t) => t.status === 'failed').length;
433
+ const skipped = completed.filter((t) => t.status === 'skipped').length;
434
+ const totalSeconds = completed.reduce((sum, t) => sum + t.durationMs, 0) / 1000;
435
+ const lines = [`Results: ${passed} passed, ${failed} failed, ${skipped} skipped - ${formatDuration(totalSeconds)}`];
436
+ const failedTests = completed.filter((t) => t.status === 'failed');
437
+ if (failedTests.length > 0) {
438
+ lines.push('', 'Failed tests:');
439
+ for (const test of failedTests) {
440
+ lines.push(` #${test.index} [${test.priority}] ${test.title} (${test.duration}, ${test.steps} steps)`);
417
441
  }
418
- if (hasSubPages) {
419
- row.Plan = planTitle;
442
+ }
443
+ const slowTests = completed
444
+ .filter((t) => t.durationMs >= 1000)
445
+ .sort((a, b) => b.durationMs - a.durationMs)
446
+ .slice(0, 3);
447
+ if (slowTests.length > 0) {
448
+ lines.push('', 'Slowest tests:');
449
+ for (const test of slowTests) {
450
+ lines.push(` #${test.index} ${test.duration} - ${test.title}`);
420
451
  }
421
- return row;
422
- });
423
- const columns = ['#', 'Status', 'Title', 'Priority', 'Time', 'Steps'];
424
- if (hasOrigin)
425
- columns.push('Origin');
426
- if (hasSubPages)
427
- columns.push('Plan');
428
- tag('multiline').log(jsonToTable(rows, columns));
452
+ }
453
+ const detailLines = completed
454
+ .map((test) => {
455
+ const details = [test.origin, test.planTitle].filter(Boolean).join(' - ');
456
+ return details ? ` #${test.index} ${details}` : '';
457
+ })
458
+ .filter(Boolean);
459
+ if (detailLines.length > 0) {
460
+ lines.push('', 'Details:');
461
+ lines.push(...detailLines);
462
+ }
463
+ tag('multiline').log(lines.join('\n'));
429
464
  tag('info').log(`${figureSet.tick} ${allTests.length} tests completed`);
430
465
  }
431
466
  printNextSteps(savedPlanPath) {
@@ -455,8 +490,8 @@ export class ExploreCommand extends BaseCommand {
455
490
  });
456
491
  }
457
492
  if (screencasts.length > 0) {
458
- const commands = screencasts.map((f) => ({ label: '', command: relativeToCwd(f) }));
459
493
  const screencastDir = relativeToCwd(outputPath('screencasts'));
494
+ const commands = [{ label: 'Folder', command: screencastDir }];
460
495
  const planSlugs = [...new Set(this.completedPlans.map((p) => safeFilename(p.title)).filter(Boolean))];
461
496
  for (const slug of planSlugs) {
462
497
  commands.push({ label: 'Browse plan', command: `ls ${screencastDir}/${slug}-*` });
@@ -513,3 +548,10 @@ function parseRatio(s) {
513
548
  return null;
514
549
  return n;
515
550
  }
551
+ function formatDuration(seconds) {
552
+ if (seconds < 60)
553
+ return `${seconds.toFixed(1)}s`;
554
+ const minutes = Math.floor(seconds / 60);
555
+ const remainingSeconds = Math.round(seconds % 60);
556
+ return `${minutes}m ${remainingSeconds}s`;
557
+ }
@@ -68,7 +68,8 @@ export class TestCommand extends BaseCommand {
68
68
  }
69
69
  tag('info').log(`Launching ${toExecute.length} test scenario(s).`);
70
70
  const tester = this.explorBot.agentTester();
71
- for (const test of toExecute) {
71
+ for (const [index, test] of toExecute.entries()) {
72
+ tag('info').log(`Starting test ${index + 1}/${toExecute.length}: ${test.scenario}`);
72
73
  await tester.test(test);
73
74
  }
74
75
  tag('success').log('Test execution finished');
@@ -8,7 +8,6 @@ import Autocomplete from './Autocomplete.js';
8
8
  import InputReadline from './InputReadline.js';
9
9
  import LogPane from './LogPane.js';
10
10
  import PlanEditor from './PlanEditor.js';
11
- import PlanPane from './PlanPane.js';
12
11
  import SessionTimer from './SessionTimer.js';
13
12
  import StateTransitionPane from './StateTransitionPane.js';
14
13
  import TaskPane, { WINDOW_SIZE } from './TaskPane.js';
@@ -124,39 +123,18 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
124
123
  }, [explorBot, inputCallbackReady]);
125
124
  const planRef = useRef(undefined);
126
125
  const unsubscribeRef = useRef(undefined);
127
- const [completedPlans, setCompletedPlans] = useState([]);
128
- const [activePlanInfo, setActivePlanInfo] = useState(null);
129
126
  useEffect(() => {
130
- const makeSummary = (plan) => {
131
- const enabled = plan.tests.filter((t) => t.enabled);
132
- return {
133
- title: plan.title,
134
- testCount: enabled.length,
135
- passed: enabled.filter((t) => t.isSuccessful).length,
136
- failed: enabled.filter((t) => t.hasFailed).length,
137
- };
138
- };
139
127
  const subscribeToPlan = (plan) => {
140
128
  if (unsubscribeRef.current)
141
129
  unsubscribeRef.current();
142
- if (planRef.current && planRef.current !== plan && planRef.current.tests.length > 0) {
143
- const summary = makeSummary(planRef.current);
144
- setCompletedPlans((prev) => {
145
- if (prev.some((p) => p.title === summary.title))
146
- return prev;
147
- return [...prev, summary];
148
- });
149
- }
150
130
  planRef.current = plan;
151
131
  tasksRef.current = [...plan.tests];
152
132
  setTasks(tasksRef.current);
153
133
  setTaskScrollOffset(0);
154
- setActivePlanInfo(makeSummary(plan));
155
134
  let lastInProgressIdx = -1;
156
135
  unsubscribeRef.current = plan.onTestsChange((updatedTests) => {
157
136
  tasksRef.current = [...updatedTests];
158
137
  setTasks(tasksRef.current);
159
- setActivePlanInfo(makeSummary(plan));
160
138
  const inProgressIdx = updatedTests.findIndex((t) => t.status === 'in_progress' && t.enabled);
161
139
  if (inProgressIdx >= 0 && inProgressIdx !== lastInProgressIdx) {
162
140
  lastInProgressIdx = inProgressIdx;
@@ -175,18 +153,9 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
175
153
  else if (!currentPlan && planRef.current) {
176
154
  if (unsubscribeRef.current)
177
155
  unsubscribeRef.current();
178
- if (planRef.current.tests.length > 0) {
179
- const summary = makeSummary(planRef.current);
180
- setCompletedPlans((prev) => {
181
- if (prev.some((p) => p.title === summary.title))
182
- return prev;
183
- return [...prev, summary];
184
- });
185
- }
186
156
  planRef.current = undefined;
187
157
  tasksRef.current = [];
188
158
  setTasks([]);
189
- setActivePlanInfo(null);
190
159
  }
191
160
  }, 2000);
192
161
  return () => {
@@ -325,6 +294,5 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
325
294
  React.createElement(StateTransitionPane, { currentState: currentState }))),
326
295
  tasks.length > 0 && (React.createElement(Box, { width: currentState ? '50%' : '100%' },
327
296
  React.createElement(TaskPane, { tasks: tasks, scrollOffset: taskScrollOffset }))),
328
- React.createElement(Autocomplete, null)),
329
- React.createElement(PlanPane, { completedPlans: completedPlans, activePlan: activePlanInfo })));
297
+ React.createElement(Autocomplete, null))));
330
298
  }
@@ -10,7 +10,6 @@ const LogPane = React.memo(({ verboseMode }) => {
10
10
  const [logs, setLogs] = useState([]);
11
11
  const pendingLogsRef = React.useRef([]);
12
12
  const flushTimeoutRef = React.useRef(null);
13
- const MAX_MULTILINE_LINES = 16;
14
13
  const MAX_STEP_LINES = 8;
15
14
  const MAX_SUBSTEP_LINES = 6;
16
15
  const formatCollapsedContent = useCallback((lines, collapsedCount, label) => {
@@ -88,6 +87,8 @@ const LogPane = React.memo(({ verboseMode }) => {
88
87
  return { color: 'yellow' };
89
88
  case 'debug':
90
89
  return { color: 'gray', dimColor: true };
90
+ case 'operation':
91
+ return { color: 'gray', dimColor: true };
91
92
  case 'substep':
92
93
  return { color: 'gray', dimColor: true };
93
94
  case 'step':
@@ -116,7 +117,8 @@ const LogPane = React.memo(({ verboseMode }) => {
116
117
  const cleaned = stripAnsi(dedent(log.content));
117
118
  const parsed = parseMarkdownToTerminal(cleaned);
118
119
  const lines = parsed.split('\n');
119
- const truncated = lines.length > MAX_MULTILINE_LINES ? `${lines.slice(0, MAX_MULTILINE_LINES).join('\n')}\n... (${lines.length - MAX_MULTILINE_LINES} more lines)` : parsed;
120
+ const maxLines = log.maxLines || 16;
121
+ const truncated = lines.length > maxLines ? `${lines.slice(0, maxLines).join('\n')}\n... (${lines.length - maxLines} more lines)` : parsed;
120
122
  return (React.createElement(Box, { key: index, borderStyle: "classic", borderLeft: false, borderRight: false, marginY: 1, padding: 1, borderColor: "dim", overflow: "hidden" },
121
123
  React.createElement(Text, { color: "gray", dimColor: true }, truncated)));
122
124
  }
@@ -127,6 +129,7 @@ const LogPane = React.memo(({ verboseMode }) => {
127
129
  type: 'multiline',
128
130
  content: `HTML Content:\n\n${markdown}`,
129
131
  timestamp: log.timestamp,
132
+ maxLines: 10,
130
133
  };
131
134
  return renderLogEntry(multilineLog, index);
132
135
  }
@@ -134,6 +137,9 @@ const LogPane = React.memo(({ verboseMode }) => {
134
137
  if (log.type === 'substep') {
135
138
  return (React.createElement(Box, { key: index, marginLeft: 2, flexDirection: "column" }, lines.map((line, lineIndex) => (React.createElement(Text, { key: `${index}-${lineIndex}`, ...styles }, lineIndex === 0 ? `> ${line}` : ` ${line}`)))));
136
139
  }
140
+ if (log.type === 'operation') {
141
+ return (React.createElement(Box, { key: index, marginLeft: 2, flexDirection: "column" }, lines.map((line, lineIndex) => (React.createElement(Text, { key: `${index}-${lineIndex}`, ...styles }, lineIndex === 0 ? `· ${line}` : ` ${line}`)))));
142
+ }
137
143
  if (log.type === 'step') {
138
144
  return (React.createElement(Box, { key: index, flexDirection: "column", paddingLeft: 2 }, lines.map((line, lineIndex) => (React.createElement(Text, { key: `${index}-${lineIndex}`, ...styles }, line)))));
139
145
  }
@@ -145,7 +151,7 @@ const LogPane = React.memo(({ verboseMode }) => {
145
151
  icon && React.createElement(Text, { ...styles }, icon),
146
152
  React.createElement(Box, { flexDirection: "column" }, lines.map((line, lineIndex) => (React.createElement(Text, { key: `${index}-${lineIndex}`, ...styles }, line))))));
147
153
  };
148
- const maxLogs = 100;
154
+ const maxLogs = 80;
149
155
  const visibleLogs = logs.length > maxLogs ? logs.slice(-maxLogs) : logs;
150
156
  return React.createElement(Box, { flexDirection: "column" }, visibleLogs.map((log, index) => renderLogEntry(log, index)).filter(Boolean));
151
157
  });
@@ -164,7 +164,7 @@ export class ExperienceTracker {
164
164
  const newEntry = generateActionContent(title, filteredCode, action.explanation);
165
165
  const updatedContent = `${newEntry}\n\n${content}`;
166
166
  this.writeExperienceFile(stateHash, updatedContent, data);
167
- tag('substep').log(` Added ACTION to: ${stateHash}.md`);
167
+ tag('operation').log(`Added ACTION to: ${stateHash}.md`);
168
168
  }
169
169
  writeFlow(state, body, relatedUrls) {
170
170
  if (this.disabled || this.isWritingDisabled(state))
@@ -190,7 +190,7 @@ export class ExperienceTracker {
190
190
  }
191
191
  const updatedContent = `${body}\n${content}`;
192
192
  this.writeExperienceFile(stateHash, updatedContent, data);
193
- tag('substep').log(`Added FLOW to: ${stateHash}.md`);
193
+ tag('operation').log(`Added FLOW to: ${stateHash}.md`);
194
194
  }
195
195
  getAllExperience() {
196
196
  const allFiles = [];
@@ -422,7 +422,7 @@ export class ExplorBot {
422
422
  this.lastReportedTestCount = tests.length;
423
423
  return;
424
424
  }
425
- tag('multiline').log(markdown);
425
+ tag('multiline').log(markdown, { maxLines: 22 });
426
426
  const filePath = this.agentSessionAnalyst().writeReport(markdown);
427
427
  tag('info').log(`Session report saved: ${relativeToCwd(filePath)}`);
428
428
  const reporter = this.explorer?.getReporter();
@@ -82,10 +82,12 @@ export class Reporter {
82
82
  return;
83
83
  }
84
84
  try {
85
- this.client = new Client({ apiKey: process.env.TESTOMATIO || '', title: this.buildTitle() });
86
- const timeoutMs = Number(process.env.TESTOMATIO_TIMEOUT_MS || '15000');
87
- const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
88
- const result = await Promise.race([this.client.createRun({ configuration: { exploratory: true } }).then(() => 'success'), timeoutPromise]);
85
+ const result = await withQuietReporterLogs(async () => {
86
+ this.client = new Client({ apiKey: process.env.TESTOMATIO || '', title: this.buildTitle() });
87
+ const timeoutMs = Number(process.env.TESTOMATIO_TIMEOUT_MS || '15000');
88
+ const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
89
+ return await Promise.race([this.client.createRun({ configuration: { exploratory: true } }).then(() => 'success'), timeoutPromise]);
90
+ });
89
91
  if (result === 'timeout') {
90
92
  debugLog('Reporter run creation timed out');
91
93
  return;
@@ -252,7 +254,9 @@ export class Reporter {
252
254
  return;
253
255
  }
254
256
  try {
255
- await this.client.updateRunStatus('finished');
257
+ await withQuietReporterLogs(async () => {
258
+ await this.client.updateRunStatus('finished');
259
+ });
256
260
  this.isRunStarted = false;
257
261
  debugLog('Testomat.io run finished');
258
262
  }
@@ -299,3 +303,18 @@ export class Reporter {
299
303
  return;
300
304
  }
301
305
  }
306
+ async function withQuietReporterLogs(fn) {
307
+ const previousLevel = process.env.TESTOMATIO_LOG_LEVEL;
308
+ process.env.TESTOMATIO_LOG_LEVEL = 'ERROR';
309
+ try {
310
+ return await fn();
311
+ }
312
+ finally {
313
+ if (previousLevel === undefined) {
314
+ process.env.TESTOMATIO_LOG_LEVEL = undefined;
315
+ }
316
+ else {
317
+ process.env.TESTOMATIO_LOG_LEVEL = previousLevel;
318
+ }
319
+ }
320
+ }
@@ -0,0 +1,27 @@
1
+ export class RecentStepFilter {
2
+ ttlMs;
3
+ recentStepKeys = new Map();
4
+ constructor(ttlMs = 15000) {
5
+ this.ttlMs = ttlMs;
6
+ }
7
+ shouldSuppress(content, now = Date.now()) {
8
+ const key = normalizeStepCommand(content);
9
+ if (!key)
10
+ return false;
11
+ for (const [existingKey, timestamp] of this.recentStepKeys) {
12
+ if (now - timestamp > this.ttlMs) {
13
+ this.recentStepKeys.delete(existingKey);
14
+ }
15
+ }
16
+ if (this.recentStepKeys.has(key))
17
+ return true;
18
+ this.recentStepKeys.set(key, now);
19
+ return false;
20
+ }
21
+ }
22
+ function normalizeStepCommand(content) {
23
+ const normalized = content.replace(/\s+/g, ' ').trim();
24
+ if (!normalized.startsWith('I.'))
25
+ return null;
26
+ return normalized.toLowerCase();
27
+ }
@@ -7,6 +7,7 @@ import dedent from 'dedent';
7
7
  import stripAnsi from 'strip-ansi';
8
8
  import { ConfigParser } from '../config.js';
9
9
  import { Observability } from "../observability.js";
10
+ import { RecentStepFilter } from "./log-filters.js";
10
11
  import { parseMarkdownToTerminal } from "./markdown-terminal.js";
11
12
  class DebugFilter {
12
13
  patterns = [];
@@ -51,6 +52,7 @@ const debugFilter = new DebugFilter();
51
52
  class ConsoleDestination {
52
53
  verboseMode = false;
53
54
  forceEnabled = false;
55
+ recentSteps = new RecentStepFilter();
54
56
  isEnabled() {
55
57
  return this.forceEnabled || !process.env.INK_RUNNING;
56
58
  }
@@ -65,6 +67,10 @@ class ConsoleDestination {
65
67
  return;
66
68
  if (entry.type === 'html')
67
69
  return;
70
+ if (entry.type === 'operation' && !this.verboseMode)
71
+ return;
72
+ if (entry.type === 'step' && !this.verboseMode && this.recentSteps.shouldSuppress(entry.content))
73
+ return;
68
74
  let content = entry.content;
69
75
  if (entry.type === 'multiline') {
70
76
  const cleaned = stripAnsi(dedent(entry.content));
@@ -84,6 +90,9 @@ class ConsoleDestination {
84
90
  else if (entry.type === 'step') {
85
91
  content = chalk.gray(` ${content}`);
86
92
  }
93
+ else if (entry.type === 'operation') {
94
+ content = chalk.gray(` · ${content}`);
95
+ }
87
96
  else if (entry.type === 'substep') {
88
97
  content = chalk.gray(` > ${content}`);
89
98
  }
@@ -213,6 +222,8 @@ class ReactDestination {
213
222
  return this.debugMode;
214
223
  }
215
224
  shouldWrite(entry) {
225
+ if (entry.type === 'operation' && !this.debugMode)
226
+ return false;
216
227
  if (entry.type !== 'debug')
217
228
  return true;
218
229
  if (this.debugMode)
@@ -269,7 +280,7 @@ class CaptainDestination {
269
280
  }
270
281
  stopCapture() {
271
282
  this.capturing = false;
272
- const logs = this.entries.filter((e) => e.type !== 'debug' && e.type !== 'html' && e.type !== 'multiline').map((e) => `[${e.type}] ${e.content}`);
283
+ const logs = this.entries.filter((e) => e.type !== 'debug' && e.type !== 'html' && e.type !== 'multiline' && e.type !== 'operation').map((e) => `[${e.type}] ${e.content}`);
273
284
  this.entries = [];
274
285
  return logs;
275
286
  }
@@ -377,6 +388,7 @@ class Logger {
377
388
  }
378
389
  return;
379
390
  }
391
+ const options = this.extractLogOptions(type, args);
380
392
  let content = this.processArgs(args);
381
393
  if (type === 'step' && args[0]?.toCode) {
382
394
  content = args[0].toCode();
@@ -386,6 +398,7 @@ class Logger {
386
398
  content,
387
399
  timestamp: new Date(),
388
400
  originalArgs: args,
401
+ maxLines: options?.maxLines,
389
402
  };
390
403
  if (this.file.isEnabled())
391
404
  this.file.write(entry);
@@ -421,6 +434,20 @@ class Logger {
421
434
  multiline(...args) {
422
435
  this.log('multiline', ...args);
423
436
  }
437
+ extractLogOptions(type, args) {
438
+ if (type !== 'multiline')
439
+ return null;
440
+ const last = args[args.length - 1];
441
+ if (!last || typeof last !== 'object' || Array.isArray(last))
442
+ return null;
443
+ if (!('maxLines' in last))
444
+ return null;
445
+ args.pop();
446
+ const maxLines = Number(last.maxLines);
447
+ if (!Number.isFinite(maxLines) || maxLines <= 0)
448
+ return null;
449
+ return { maxLines };
450
+ }
424
451
  }
425
452
  const logger = Logger.getInstance();
426
453
  let stepSpanParent = null;
@@ -27,11 +27,5 @@ export function printNextSteps(sections) {
27
27
  }
28
28
  blocks.push(lines.join('\n'));
29
29
  }
30
- for (let i = 0; i < blocks.length; i++) {
31
- if (i > 0)
32
- tag('info').log('');
33
- for (const line of blocks[i].split('\n')) {
34
- tag('info').log(line);
35
- }
36
- }
30
+ tag('multiline').log(blocks.join('\n\n'), { maxLines: 18 });
37
31
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -102,7 +102,7 @@ export function WithCodeceptJS<T extends Constructor>(Base: T) {
102
102
  writeFileSync(filePath, lines.join('\n'));
103
103
  this.savedFiles.add(filePath);
104
104
 
105
- tag('substep').log(`Saved plan tests to: ${relativeToCwd(filePath)}`);
105
+ tag('operation').log(`Saved plan tests to: ${relativeToCwd(filePath)}`);
106
106
  return filePath;
107
107
  }
108
108
 
@@ -56,7 +56,7 @@ export function WithExperience<T extends Constructor>(Base: T) {
56
56
 
57
57
  await this.stopScreencast();
58
58
 
59
- tag('substep').log(`Historian saved session for: ${task.description}`);
59
+ tag('operation').log(`Historian saved session for: ${task.description}`);
60
60
  }
61
61
 
62
62
  private async reportSession(test: Test, steps: SessionStep[]): Promise<void> {
@@ -140,7 +140,7 @@ export function WithPlaywright<T extends Constructor>(Base: T) {
140
140
  writeFileSync(filePath, lines.join('\n'));
141
141
  this.savedFiles.add(filePath);
142
142
 
143
- tag('substep').log(`Saved plan tests to: ${relativeToCwd(filePath)}`);
143
+ tag('operation').log(`Saved plan tests to: ${relativeToCwd(filePath)}`);
144
144
  return filePath;
145
145
  }
146
146
 
@@ -92,7 +92,7 @@ export function WithScreencast<T extends Constructor>(Base: T) {
92
92
  this.screencastTask = test?._explorbotTest || null;
93
93
  this.screencastLastChapter = null;
94
94
  } catch (err) {
95
- tag('substep').log(`Screencast start failed: ${(err as Error).message}`);
95
+ tag('operation').log(`Screencast start failed: ${(err as Error).message}`);
96
96
  }
97
97
  }
98
98
 
@@ -116,7 +116,7 @@ export function WithScreencast<T extends Constructor>(Base: T) {
116
116
  try {
117
117
  await this.screencastPage.screencast.stop();
118
118
  } catch (err) {
119
- tag('substep').log(`Screencast stop failed: ${(err as Error).message}`);
119
+ tag('operation').log(`Screencast stop failed: ${(err as Error).message}`);
120
120
  }
121
121
  this.screencastActive = false;
122
122
  this.screencastPage = null;
@@ -126,7 +126,7 @@ export function WithScreencast<T extends Constructor>(Base: T) {
126
126
  if (path) {
127
127
  this.savedFiles.add(path);
128
128
  task?.addArtifact?.(path);
129
- tag('substep').log(`Saved screencast: ${relativeToCwd(path)}`);
129
+ tag('operation').log(`Saved screencast: ${relativeToCwd(path)}`);
130
130
  }
131
131
  }
132
132
  };
@@ -62,6 +62,6 @@ export class Historian extends HistorianBase {
62
62
 
63
63
  writeFileSync(filePath, content);
64
64
  this.savedFiles.add(filePath);
65
- tag('substep').log(`Updated test file with healed steps: ${relativeToCwd(filePath)}`);
65
+ tag('operation').log(`Updated test file with healed steps: ${relativeToCwd(filePath)}`);
66
66
  }
67
67
  }
@@ -206,7 +206,7 @@ class Navigator implements Agent {
206
206
  if (!actionResult.isInsideIframe) {
207
207
  const successful = this.experienceTracker.getSuccessfulExperience(actionResult);
208
208
  if (successful.length > 0) {
209
- tag('substep').log(`Found ${successful.length} experience ${pluralize(successful.length, 'file')} for: ${actionResult.url}`);
209
+ tag('operation').log(`Found ${successful.length} experience ${pluralize(successful.length, 'file')} for: ${actionResult.url}`);
210
210
  experience = `<experience>\nPast successful recipes recorded from prior runs for this page. Prefer these solutions first if they match the goal.\n\n${successful.join('\n\n')}\n</experience>`;
211
211
  }
212
212
  }
@@ -307,7 +307,7 @@ class Navigator implements Agent {
307
307
  stop();
308
308
  return;
309
309
  }
310
- tag('substep').log('Feeding failures back to AI for a new batch...');
310
+ tag('operation').log('Feeding failures back to AI for a new batch...');
311
311
  let contextMsg = 'Previous solutions did not work. Analyze the failures and try DIFFERENT strategies (not syntactic variants of the same locator).\n\n';
312
312
  if (batchFailures.length > 0) {
313
313
  const lines = batchFailures
@@ -633,7 +633,7 @@ class Navigator implements Agent {
633
633
 
634
634
  const cachedVerification = actionResult.getVerification(message);
635
635
  if (cachedVerification !== null) {
636
- tag('substep').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
636
+ tag('operation').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
637
637
  return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
638
638
  }
639
639
 
@@ -654,7 +654,7 @@ class Navigator implements Agent {
654
654
  const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
655
655
  if (toc.length > 0) {
656
656
  const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
657
- tag('substep').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections) for: ${actionResult.url}`);
657
+ tag('operation').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections) for: ${actionResult.url}`);
658
658
  experience = renderExperienceToc(toc);
659
659
  }
660
660
  }