explorbot 0.1.25 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/package.json +1 -1
- package/dist/src/ai/historian/codeceptjs.js +1 -1
- package/dist/src/ai/historian/experience.js +1 -1
- package/dist/src/ai/historian/playwright.js +1 -1
- package/dist/src/ai/historian/screencast.js +3 -3
- package/dist/src/ai/historian.js +1 -1
- package/dist/src/ai/navigator.js +4 -4
- package/dist/src/ai/pilot.js +8 -5
- package/dist/src/ai/planner.js +2 -4
- package/dist/src/ai/provider.js +5 -3
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher.js +4 -4
- package/dist/src/ai/session-analyst.js +4 -1
- package/dist/src/ai/task-agent.js +2 -2
- package/dist/src/commands/context-aria-command.js +1 -1
- package/dist/src/commands/explore-command.js +67 -25
- package/dist/src/commands/test-command.js +2 -1
- package/dist/src/components/App.js +1 -33
- package/dist/src/components/LogPane.js +9 -3
- package/dist/src/experience-tracker.js +2 -2
- package/dist/src/explorbot.js +1 -1
- package/dist/src/reporter.js +24 -5
- package/dist/src/utils/log-filters.js +27 -0
- package/dist/src/utils/logger.js +28 -1
- package/dist/src/utils/next-steps.js +1 -7
- package/package.json +1 -1
- package/src/ai/historian/codeceptjs.ts +1 -1
- package/src/ai/historian/experience.ts +1 -1
- package/src/ai/historian/playwright.ts +1 -1
- package/src/ai/historian/screencast.ts +3 -3
- package/src/ai/historian.ts +1 -1
- package/src/ai/navigator.ts +4 -4
- package/src/ai/pilot.ts +8 -5
- package/src/ai/planner.ts +1 -3
- package/src/ai/provider.ts +5 -3
- package/src/ai/researcher/locators.ts +1 -1
- package/src/ai/researcher.ts +4 -4
- package/src/ai/session-analyst.ts +4 -1
- package/src/ai/task-agent.ts +2 -2
- package/src/commands/context-aria-command.ts +1 -1
- package/src/commands/explore-command.ts +68 -23
- package/src/commands/test-command.ts +2 -1
- package/src/components/App.tsx +0 -33
- package/src/components/LogPane.tsx +18 -3
- package/src/experience-tracker.ts +2 -2
- package/src/explorbot.ts +1 -1
- package/src/reporter.ts +24 -6
- package/src/utils/log-filters.ts +26 -0
- package/src/utils/logger.ts +24 -2
- package/src/utils/next-steps.ts +1 -6
package/dist/package.json
CHANGED
|
@@ -82,7 +82,7 @@ export function WithCodeceptJS(Base) {
|
|
|
82
82
|
const filePath = join(testsDir, safeFilename(plan.title, '.js'));
|
|
83
83
|
writeFileSync(filePath, lines.join('\n'));
|
|
84
84
|
this.savedFiles.add(filePath);
|
|
85
|
-
tag('
|
|
85
|
+
tag('operation').log(`Saved plan tests to: ${relativeToCwd(filePath)}`);
|
|
86
86
|
return filePath;
|
|
87
87
|
}
|
|
88
88
|
getKnowledgeLines(url, indent = ' ') {
|
|
@@ -30,7 +30,7 @@ export function WithExperience(Base) {
|
|
|
30
30
|
await this.reportSession(task, steps);
|
|
31
31
|
}
|
|
32
32
|
await this.stopScreencast();
|
|
33
|
-
tag('
|
|
33
|
+
tag('operation').log(`Historian saved session for: ${task.description}`);
|
|
34
34
|
}
|
|
35
35
|
async reportSession(test, steps) {
|
|
36
36
|
if (!this.reporter)
|
|
@@ -119,7 +119,7 @@ export function WithPlaywright(Base) {
|
|
|
119
119
|
const filePath = join(testsDir, safeFilename(plan.title, '.spec.ts'));
|
|
120
120
|
writeFileSync(filePath, lines.join('\n'));
|
|
121
121
|
this.savedFiles.add(filePath);
|
|
122
|
-
tag('
|
|
122
|
+
tag('operation').log(`Saved plan tests to: ${relativeToCwd(filePath)}`);
|
|
123
123
|
return filePath;
|
|
124
124
|
}
|
|
125
125
|
getPlaywrightKnowledgeLines(url, indent = ' ') {
|
|
@@ -76,7 +76,7 @@ export function WithScreencast(Base) {
|
|
|
76
76
|
this.screencastLastChapter = null;
|
|
77
77
|
}
|
|
78
78
|
catch (err) {
|
|
79
|
-
tag('
|
|
79
|
+
tag('operation').log(`Screencast start failed: ${err.message}`);
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
async emitChapter(_step) {
|
|
@@ -104,7 +104,7 @@ export function WithScreencast(Base) {
|
|
|
104
104
|
await this.screencastPage.screencast.stop();
|
|
105
105
|
}
|
|
106
106
|
catch (err) {
|
|
107
|
-
tag('
|
|
107
|
+
tag('operation').log(`Screencast stop failed: ${err.message}`);
|
|
108
108
|
}
|
|
109
109
|
this.screencastActive = false;
|
|
110
110
|
this.screencastPage = null;
|
|
@@ -114,7 +114,7 @@ export function WithScreencast(Base) {
|
|
|
114
114
|
if (path) {
|
|
115
115
|
this.savedFiles.add(path);
|
|
116
116
|
task?.addArtifact?.(path);
|
|
117
|
-
tag('
|
|
117
|
+
tag('operation').log(`Saved screencast: ${relativeToCwd(path)}`);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
};
|
package/dist/src/ai/historian.js
CHANGED
|
@@ -38,6 +38,6 @@ export class Historian extends HistorianBase {
|
|
|
38
38
|
}
|
|
39
39
|
writeFileSync(filePath, content);
|
|
40
40
|
this.savedFiles.add(filePath);
|
|
41
|
-
tag('
|
|
41
|
+
tag('operation').log(`Updated test file with healed steps: ${relativeToCwd(filePath)}`);
|
|
42
42
|
}
|
|
43
43
|
}
|
package/dist/src/ai/navigator.js
CHANGED
|
@@ -182,7 +182,7 @@ class Navigator {
|
|
|
182
182
|
if (!actionResult.isInsideIframe) {
|
|
183
183
|
const successful = this.experienceTracker.getSuccessfulExperience(actionResult);
|
|
184
184
|
if (successful.length > 0) {
|
|
185
|
-
tag('
|
|
185
|
+
tag('operation').log(`Found ${successful.length} experience ${pluralize(successful.length, 'file')} for: ${actionResult.url}`);
|
|
186
186
|
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>`;
|
|
187
187
|
}
|
|
188
188
|
}
|
|
@@ -276,7 +276,7 @@ class Navigator {
|
|
|
276
276
|
stop();
|
|
277
277
|
return;
|
|
278
278
|
}
|
|
279
|
-
tag('
|
|
279
|
+
tag('operation').log('Feeding failures back to AI for a new batch...');
|
|
280
280
|
let contextMsg = 'Previous solutions did not work. Analyze the failures and try DIFFERENT strategies (not syntactic variants of the same locator).\n\n';
|
|
281
281
|
if (batchFailures.length > 0) {
|
|
282
282
|
const lines = batchFailures
|
|
@@ -573,7 +573,7 @@ class Navigator {
|
|
|
573
573
|
debugLog('Verification message:', message);
|
|
574
574
|
const cachedVerification = actionResult.getVerification(message);
|
|
575
575
|
if (cachedVerification !== null) {
|
|
576
|
-
tag('
|
|
576
|
+
tag('operation').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
|
|
577
577
|
return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
578
578
|
}
|
|
579
579
|
let knowledge = '';
|
|
@@ -591,7 +591,7 @@ class Navigator {
|
|
|
591
591
|
const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
|
|
592
592
|
if (toc.length > 0) {
|
|
593
593
|
const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
|
|
594
|
-
tag('
|
|
594
|
+
tag('operation').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections) for: ${actionResult.url}`);
|
|
595
595
|
experience = renderExperienceToc(toc);
|
|
596
596
|
}
|
|
597
597
|
}
|
package/dist/src/ai/pilot.js
CHANGED
|
@@ -84,7 +84,7 @@ export class Pilot {
|
|
|
84
84
|
}
|
|
85
85
|
const schema = z.object({
|
|
86
86
|
decision: z.enum(['pass', 'fail', 'continue', 'skipped']).describe('pass = test succeeded, fail = test failed, continue = tester should keep going, skipped = scenario is irrelevant OR systematic execution failures prevented testing'),
|
|
87
|
-
reason: z.string().describe('
|
|
87
|
+
reason: z.string().describe('Concise user-facing reason, maximum 1 short sentence and 120 characters. Do NOT repeat the decision status; explain only the evidence. For continue: explain why rejected and suggest alternatives.'),
|
|
88
88
|
guidance: z.string().nullable().describe('Required for "continue": specific actionable instruction for the tester — what exactly to verify, retry differently, or complete next. Be concrete.'),
|
|
89
89
|
requestVerification: z
|
|
90
90
|
.string()
|
|
@@ -149,7 +149,7 @@ export class Pilot {
|
|
|
149
149
|
this.explorer.getPlaywrightRecorder().recordVerification(verifyResult.assertionSteps);
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
-
tag('info').log(`Pilot: ${result.decision}
|
|
152
|
+
tag('info').log(`Pilot: ${result.decision} - ${result.reason}`);
|
|
153
153
|
task.summary = result.reason;
|
|
154
154
|
const verdictState = screenshotState || currentState;
|
|
155
155
|
if (result.decision === 'pass') {
|
|
@@ -187,7 +187,7 @@ export class Pilot {
|
|
|
187
187
|
const notes = task.notesToString() || 'No notes recorded.';
|
|
188
188
|
const schema = z.object({
|
|
189
189
|
decision: z.enum(['allow', 'fail', 'continue', 'skipped']).describe('allow = reset proceeds, fail = test failed (stop looping), continue = veto reset, tester should act on current page instead, skipped = scenario is irrelevant or cannot be executed'),
|
|
190
|
-
reason: z.string().describe('
|
|
190
|
+
reason: z.string().describe('Concise evidence-only reason, maximum 1 short sentence and 120 characters. Do not restate the decision.'),
|
|
191
191
|
guidance: z.string().nullable().describe('Required for "continue": concrete instruction for what the tester should do instead of resetting (e.g. which tool to call, what to verify).'),
|
|
192
192
|
});
|
|
193
193
|
const userContent = dedent `
|
|
@@ -343,8 +343,9 @@ export class Pilot {
|
|
|
343
343
|
- "continue": tester hasn't completed the goal; provide concrete guidance (which tool, what to check).
|
|
344
344
|
If a verify() asserted a state that was ALREADY TRUE before the test, it proves nothing — reject.
|
|
345
345
|
|
|
346
|
-
reason field:
|
|
347
|
-
what was verified, what failed,
|
|
346
|
+
reason field: one short sentence, maximum 120 characters. Do NOT restate the decision
|
|
347
|
+
("scenario goal achieved/not achieved"). State what happened: what was verified, what failed,
|
|
348
|
+
or what evidence was found.
|
|
348
349
|
`;
|
|
349
350
|
}
|
|
350
351
|
async planTest(task, currentState) {
|
|
@@ -919,6 +920,8 @@ export class Pilot {
|
|
|
919
920
|
Response format:
|
|
920
921
|
PROGRESS: <1 sentence assessment>
|
|
921
922
|
NEXT: <specific actionable instruction for Tester>
|
|
923
|
+
|
|
924
|
+
Keep user-facing reasons concise: one short sentence, maximum 120 characters, evidence only, no repeated verdict wording.
|
|
922
925
|
`;
|
|
923
926
|
}
|
|
924
927
|
}
|
package/dist/src/ai/planner.js
CHANGED
|
@@ -14,7 +14,7 @@ import { mdq } from '../utils/markdown-query.js';
|
|
|
14
14
|
import { planToCompactAiContext } from "../utils/test-plan-markdown.js";
|
|
15
15
|
import { Conversation } from "./conversation.js";
|
|
16
16
|
import { WithSessionDedup } from "./planner/session-dedup.js";
|
|
17
|
-
import { getActiveStyle
|
|
17
|
+
import { getActiveStyle } from "./planner/styles.js";
|
|
18
18
|
import { WithSubPages, getPlannedByStateHash, getRegisteredPlan, registerPlan } from "./planner/subpages.js";
|
|
19
19
|
import { POSSIBLE_SECTIONS, Researcher } from "./researcher.js";
|
|
20
20
|
import { findSimilarStateHash } from "./researcher/cache.js";
|
|
@@ -28,7 +28,7 @@ const TasksSchema = z.object({
|
|
|
28
28
|
scenario: z.string().describe('A single sentence describing what to test'),
|
|
29
29
|
priority: z.enum(['critical', 'important', 'high', 'normal', 'low']).describe('Priority of the task based on business importance'),
|
|
30
30
|
startUrl: z.string().nullable().describe('Start URL for the test if different from plan URL (only for tests on visited subpages)'),
|
|
31
|
-
steps: z.array(z.string()).describe('List of steps to perform for this scenario. Each step should be a specific action (e.g., "
|
|
31
|
+
steps: z.array(z.string()).describe('List of steps to perform for this scenario. Each step should be a specific action (e.g., "Open the form", "Enter required data", "Submit the form"). Keep steps atomic and actionable.'),
|
|
32
32
|
expectedOutcomes: z
|
|
33
33
|
.array(z.string())
|
|
34
34
|
.describe('List of expected outcomes that can be verified. Each outcome should be simple, specific, and easy to check (e.g., "Success message appears", "URL changes to /dashboard", "Form field shows error"). Keep outcomes atomic - do not combine multiple checks into one.'),
|
|
@@ -199,9 +199,7 @@ export class Planner extends PlannerBase {
|
|
|
199
199
|
tag('multiline').log(summary);
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
|
-
const availableStyles = Object.keys(getStyles()).join(', ');
|
|
203
202
|
tag('success').log(`Planning complete! ${this.currentPlan.tests.length} tests in plan: ${this.currentPlan.title}`);
|
|
204
|
-
tag('info').log(`Planning style: ${this.lastStyleName} (available: ${availableStyles})`);
|
|
205
203
|
if (state.url)
|
|
206
204
|
registerPlan(state.url, this.currentPlan, feature, state.hash);
|
|
207
205
|
this.registerPlanInSession(this.currentPlan);
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -259,8 +259,11 @@ export class Provider {
|
|
|
259
259
|
}
|
|
260
260
|
throw new ContextLengthError(error.message || error.toString());
|
|
261
261
|
}
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
const message = error.message || error.toString();
|
|
263
|
+
if (message !== 'No response text from AI') {
|
|
264
|
+
tag('error').log(message);
|
|
265
|
+
}
|
|
266
|
+
throw new AiError(message);
|
|
264
267
|
}
|
|
265
268
|
}
|
|
266
269
|
async generateWithTools(messages, model, tools, options = {}) {
|
|
@@ -333,7 +336,6 @@ export class Provider {
|
|
|
333
336
|
catch (error) {
|
|
334
337
|
clearActivity();
|
|
335
338
|
if (error?.message?.includes('Tool choice is required')) {
|
|
336
|
-
tag('warning').log('Model completed without calling a tool, returning empty result');
|
|
337
339
|
return { text: '', toolCalls: [], toolResults: [], response: { messages: [] }, usage: null };
|
|
338
340
|
}
|
|
339
341
|
if (error?.name === 'AbortError')
|
|
@@ -70,7 +70,7 @@ export function WithLocators(Base) {
|
|
|
70
70
|
broken++;
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
tag('
|
|
73
|
+
tag('operation').log(`Validated ${locators.length} locators: ${locators.length - broken} valid, ${broken} broken`);
|
|
74
74
|
}
|
|
75
75
|
async fixBrokenSections(result, conversation) {
|
|
76
76
|
const broken = result.locators.filter((l) => l.valid === false);
|
|
@@ -114,7 +114,7 @@ export class Researcher extends ResearcherBase {
|
|
|
114
114
|
if (!deep && !force) {
|
|
115
115
|
const similar = await findSimilarResearch(combinedHtml);
|
|
116
116
|
if (similar) {
|
|
117
|
-
tag('
|
|
117
|
+
tag('operation').log('Similar research found, reusing cached result');
|
|
118
118
|
if (stateHash)
|
|
119
119
|
saveResearch(stateHash, similar, combinedHtml);
|
|
120
120
|
tag('multiline').log(formatResearchSummary(similar));
|
|
@@ -264,10 +264,10 @@ export class Researcher extends ResearcherBase {
|
|
|
264
264
|
tag('multiline').log(formatResearchSummary(result.text, { visionUsed: this.hasScreenshotToAnalyze }));
|
|
265
265
|
tag('success').log('Research complete');
|
|
266
266
|
if (researchFile)
|
|
267
|
-
tag('
|
|
267
|
+
tag('operation').log(`Research file saved to: ${researchFile}`);
|
|
268
268
|
if (this.actionResult?.screenshotFile) {
|
|
269
269
|
const screenshotPath = outputPath('states', this.actionResult.screenshotFile);
|
|
270
|
-
tag('
|
|
270
|
+
tag('operation').log(`UI screenshot: file://${screenshotPath}`);
|
|
271
271
|
}
|
|
272
272
|
await this.hooksRunner.runAfterHook('researcher', state.url);
|
|
273
273
|
return result.text;
|
|
@@ -402,7 +402,7 @@ export class Researcher extends ResearcherBase {
|
|
|
402
402
|
.map((k) => k.content)
|
|
403
403
|
.filter((k) => !!k)
|
|
404
404
|
.join('\n\n');
|
|
405
|
-
tag('
|
|
405
|
+
tag('operation').log(`Found ${knowledgeFiles.length} relevant knowledge ${pluralize(knowledgeFiles.length, 'file')} for: ${this.actionResult.url}`);
|
|
406
406
|
knowledge = `
|
|
407
407
|
<hint>
|
|
408
408
|
Here is relevant knowledge for this page:
|
|
@@ -34,6 +34,9 @@ export class SessionAnalyst {
|
|
|
34
34
|
|
|
35
35
|
Crucial distinction: "the app misbehaved" vs "the automation could not interact with the app". ONLY the first is a Defect. If the automation gives up before the app responds — timeout, retries exhausted, dead loop / loop detected, could not click or find an element — that is an Execution issue regardless of what the log calls it. Failure inside the automation ≠ failure inside the product.
|
|
36
36
|
|
|
37
|
+
The action log is more authoritative than the scenario title. If the actual submitted data, page state, or action sequence does not match the scenario title, classify it as Execution issue and do not list that scenario under What works. Do NOT infer a product Defect or UX issue from behavior caused by incorrect test data or an automation mismatch.
|
|
38
|
+
Negative test data is valid when it matches a negative scenario. Do not call intentionally invalid input wrong data when the scenario expects rejection or validation feedback.
|
|
39
|
+
|
|
37
40
|
A solitary failure where adjacent tests on the same feature passed → Execution, not Defect.
|
|
38
41
|
|
|
39
42
|
## Severity (defects only)
|
|
@@ -69,7 +72,7 @@ export class SessionAnalyst {
|
|
|
69
72
|
|
|
70
73
|
## Brevity rules
|
|
71
74
|
|
|
72
|
-
- Headline: 2 sentences MAX. About the FEATURE, not the run. No counts, no "N tests", no "this session".
|
|
75
|
+
- Headline: 2 sentences MAX. About the FEATURE, not the run. No counts, no "N tests", no "this session". Never use these words: "exercised", "comprehensive", "notably", "this session", "module", "targeted", "covered creation".
|
|
73
76
|
- What works: feature name + test refs. NO parentheticals, NO caveats. If there's a caveat, the entry doesn't belong here.
|
|
74
77
|
- Defect title is the BUG ("Search returns non-matching results"), never the scenario name.
|
|
75
78
|
- Reproduce steps are imperative one-liners drawn from the log.
|
|
@@ -24,7 +24,7 @@ export class TaskAgent {
|
|
|
24
24
|
.map((k) => k.content)
|
|
25
25
|
.filter((k) => !!k)
|
|
26
26
|
.join('\n\n');
|
|
27
|
-
tag('
|
|
27
|
+
tag('operation').log(`Found ${knowledgeFiles.length} relevant knowledge ${pluralize(knowledgeFiles.length, 'file')}`);
|
|
28
28
|
return dedent `
|
|
29
29
|
<knowledge>
|
|
30
30
|
Here is relevant knowledge for this page:
|
|
@@ -40,7 +40,7 @@ export class TaskAgent {
|
|
|
40
40
|
return '';
|
|
41
41
|
const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
|
|
42
42
|
debugLog(`injecting experience TOC (${toc.length} files, ${totalSections} sections)`);
|
|
43
|
-
tag('
|
|
43
|
+
tag('operation').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections)`);
|
|
44
44
|
return renderExperienceToc(toc);
|
|
45
45
|
}
|
|
46
46
|
setHistorian(historian) {
|
|
@@ -12,6 +12,6 @@ export class ContextAriaCommand extends BaseCommand {
|
|
|
12
12
|
if (!ariaSnapshot) {
|
|
13
13
|
throw new Error('No ARIA snapshot available for current page');
|
|
14
14
|
}
|
|
15
|
-
tag('multiline').log(`ARIA Snapshot:\n\n${ariaSnapshot}
|
|
15
|
+
tag('multiline').log(`ARIA Snapshot:\n\n${ariaSnapshot}`, { maxLines: 10 });
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -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.
|
|
409
|
+
const hasSubPages = new Set(this.completedPlans.map((plan) => plan.title)).size > 1;
|
|
398
410
|
const hasOrigin = this.oldTestRefs.size > 0;
|
|
399
|
-
const
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
416
|
-
|
|
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
|
-
|
|
419
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
|
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 =
|
|
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('
|
|
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('
|
|
193
|
+
tag('operation').log(`Added FLOW to: ${stateHash}.md`);
|
|
194
194
|
}
|
|
195
195
|
getAllExperience() {
|
|
196
196
|
const allFiles = [];
|
package/dist/src/explorbot.js
CHANGED
|
@@ -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();
|