explorbot 0.4.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/bin/mdq.ts +18 -0
- package/boat/api-tester/src/ai/chief.ts +72 -0
- package/boat/api-tester/src/api-client.ts +37 -0
- package/boat/prima/src/prima.ts +41 -2
- package/dist/bin/mdq.js +19 -0
- package/dist/boat/api-tester/src/ai/chief.js +69 -0
- package/dist/boat/api-tester/src/api-client.js +26 -0
- package/dist/boat/prima/src/prima.js +42 -2
- package/dist/package.json +3 -2
- package/dist/src/action.js +4 -2
- package/dist/src/ai/agent.d.ts +3 -1
- package/dist/src/ai/judge-provider.d.ts +17 -0
- package/dist/src/ai/judge-provider.js +56 -0
- package/dist/src/ai/judge-tool.d.ts +2 -0
- package/dist/src/ai/judge-tool.js +33 -0
- package/dist/src/ai/judge.d.ts +28 -0
- package/dist/src/ai/judge.js +71 -0
- package/dist/src/ai/navigator.d.ts +3 -1
- package/dist/src/ai/navigator.js +24 -28
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +67 -9
- package/dist/src/ai/planner.js +9 -6
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +52 -7
- package/dist/src/ai/rerunner.js +7 -0
- package/dist/src/ai/researcher/deep-analysis.js +2 -2
- package/dist/src/ai/researcher/locators.js +2 -2
- package/dist/src/ai/researcher/pagination.js +1 -1
- package/dist/src/ai/researcher/research-result.js +2 -2
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/task-agent.d.ts +2 -0
- package/dist/src/ai/task-agent.js +3 -1
- package/dist/src/ai/tester.js +19 -15
- package/dist/src/ai/tools.d.ts +4 -3
- package/dist/src/ai/tools.js +35 -7
- package/dist/src/api/request-result.js +2 -1
- package/dist/src/api/xhr-capture.js +2 -1
- package/dist/src/command-handler.d.ts +1 -0
- package/dist/src/command-handler.js +24 -3
- package/dist/src/commands/base-command.d.ts +5 -0
- package/dist/src/commands/base-command.js +3 -0
- package/dist/src/commands/explore-command.d.ts +2 -1
- package/dist/src/commands/explore-command.js +12 -1
- package/dist/src/commands/freesail-command.js +8 -2
- package/dist/src/commands/init-command.js +1 -1
- package/dist/src/commands/navigate-command.d.ts +2 -1
- package/dist/src/commands/navigate-command.js +6 -0
- package/dist/src/commands/plan-load-command.d.ts +2 -1
- package/dist/src/commands/plan-load-command.js +4 -0
- package/dist/src/commands/plans-command.d.ts +3 -9
- package/dist/src/commands/plans-command.js +11 -21
- package/dist/src/commands/rerun-command.d.ts +2 -1
- package/dist/src/commands/rerun-command.js +5 -1
- package/dist/src/commands/research-command.d.ts +2 -1
- package/dist/src/commands/research-command.js +6 -0
- package/dist/src/commands/test-command.d.ts +2 -1
- package/dist/src/commands/test-command.js +4 -1
- package/dist/src/components/Autocomplete.js +26 -12
- package/dist/src/components/InputReadline.js +10 -1
- package/dist/src/config.d.ts +6 -0
- package/dist/src/experience-tracker.js +4 -3
- package/dist/src/explorbot.d.ts +3 -0
- package/dist/src/explorbot.js +8 -0
- package/dist/src/explorer.js +2 -3
- package/dist/src/knowledge-tracker.js +1 -1
- package/dist/src/reporter.js +8 -4
- package/dist/src/state-manager.d.ts +2 -0
- package/dist/src/state-manager.js +16 -0
- package/dist/src/test-plan.d.ts +11 -0
- package/dist/src/test-plan.js +54 -2
- package/dist/src/utils/aria-ref.js +1 -1
- package/dist/src/utils/logger.js +9 -3
- package/dist/src/utils/markdown-query.d.ts +1 -48
- package/dist/src/utils/markdown-query.js +1 -444
- package/dist/src/utils/mdq/cli.d.ts +6 -0
- package/dist/src/utils/mdq/cli.js +122 -0
- package/dist/src/utils/mdq/edit.d.ts +24 -0
- package/dist/src/utils/mdq/edit.js +147 -0
- package/dist/src/utils/mdq/query.d.ts +118 -0
- package/dist/src/utils/mdq/query.js +451 -0
- package/dist/src/utils/step-analyzer.d.ts +3 -0
- package/dist/src/utils/step-analyzer.js +7 -0
- package/dist/src/utils/strings.d.ts +1 -0
- package/dist/src/utils/strings.js +7 -0
- package/dist/src/utils/test-files.d.ts +1 -0
- package/dist/src/utils/test-files.js +5 -2
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +7 -0
- package/docs/api-testing/planning.md +1 -1
- package/docs/superpowers/plans/2026-09-15-mdq-package.md +130 -94
- package/docs/superpowers/specs/2026-09-18-judge-decision-model-design.md +79 -0
- package/package.json +3 -2
- package/src/action.ts +4 -2
- package/src/ai/agent.ts +3 -1
- package/src/ai/judge-provider.ts +62 -0
- package/src/ai/judge-tool.ts +35 -0
- package/src/ai/judge.ts +75 -0
- package/src/ai/navigator.ts +24 -27
- package/src/ai/pilot.ts +68 -9
- package/src/ai/planner.ts +9 -6
- package/src/ai/provider.ts +51 -7
- package/src/ai/rerunner.ts +4 -0
- package/src/ai/researcher/deep-analysis.ts +2 -2
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/pagination.ts +1 -1
- package/src/ai/researcher/research-result.ts +2 -2
- package/src/ai/researcher.ts +1 -1
- package/src/ai/task-agent.ts +4 -1
- package/src/ai/tester.ts +19 -16
- package/src/ai/tools.ts +42 -7
- package/src/api/request-result.ts +2 -1
- package/src/api/xhr-capture.ts +2 -1
- package/src/command-handler.ts +28 -3
- package/src/commands/base-command.ts +9 -0
- package/src/commands/explore-command.ts +15 -2
- package/src/commands/freesail-command.ts +9 -2
- package/src/commands/init-command.ts +1 -1
- package/src/commands/navigate-command.ts +8 -1
- package/src/commands/plan-load-command.ts +6 -1
- package/src/commands/plans-command.ts +13 -29
- package/src/commands/rerun-command.ts +7 -2
- package/src/commands/research-command.ts +8 -1
- package/src/commands/test-command.ts +6 -2
- package/src/components/Autocomplete.tsx +39 -10
- package/src/components/InputReadline.tsx +10 -1
- package/src/config.ts +1 -0
- package/src/experience-tracker.ts +4 -3
- package/src/explorbot.ts +8 -0
- package/src/explorer.ts +2 -2
- package/src/knowledge-tracker.ts +1 -1
- package/src/reporter.ts +8 -4
- package/src/state-manager.ts +16 -0
- package/src/test-plan.ts +67 -2
- package/src/utils/aria-ref.ts +1 -1
- package/src/utils/logger.ts +7 -2
- package/src/utils/markdown-query.ts +1 -519
- package/src/utils/mdq/cli.ts +118 -0
- package/src/utils/mdq/edit.ts +158 -0
- package/src/utils/mdq/query.ts +556 -0
- package/src/utils/step-analyzer.ts +8 -0
- package/src/utils/strings.ts +7 -0
- package/src/utils/test-files.ts +5 -2
- package/src/utils/url-matcher.ts +7 -0
package/dist/src/ai/provider.js
CHANGED
|
@@ -3,7 +3,7 @@ import { LangfuseSpanProcessor } from '@langfuse/otel';
|
|
|
3
3
|
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
4
4
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
5
5
|
import dedent from 'dedent';
|
|
6
|
-
import { APICallError, generateObject, generateText, isStepCount, registerTelemetry, tool } from 'ai';
|
|
6
|
+
import { APICallError, NoObjectGeneratedError, asSchema, extractJsonMiddleware, generateObject, generateText, isStepCount, parsePartialJson, registerTelemetry, tool, wrapLanguageModel } from 'ai';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { clearActivity, setActivity } from "../activity.js";
|
|
9
9
|
import { configuredModels, modelName as getModelName } from '../config.js';
|
|
@@ -51,12 +51,12 @@ const CONTEXT_LENGTH_PATTERNS = ['reduce the length', 'context length', 'maximum
|
|
|
51
51
|
function extractCachedTokens(usage) {
|
|
52
52
|
return usage?.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
53
53
|
}
|
|
54
|
-
function abortAfterIdle(ms, cancel, controller) {
|
|
54
|
+
function abortAfterIdle(ms, cancel, controller, busy) {
|
|
55
55
|
return new Promise((_, reject) => {
|
|
56
56
|
const tick = () => {
|
|
57
57
|
if (cancel.cancelled)
|
|
58
58
|
return;
|
|
59
|
-
if (executionController.isAwaitingInput()) {
|
|
59
|
+
if (executionController.isAwaitingInput() || busy.tools > 0) {
|
|
60
60
|
setTimeout(tick, ms);
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
@@ -78,7 +78,7 @@ export class Provider {
|
|
|
78
78
|
otelSdk = null;
|
|
79
79
|
defaultRetryOptions = {
|
|
80
80
|
maxAttempts: 3,
|
|
81
|
-
baseDelay:
|
|
81
|
+
baseDelay: 1000,
|
|
82
82
|
maxDelay: 10000,
|
|
83
83
|
retryCondition: (error) => {
|
|
84
84
|
return ((error.name === 'AI_APICallError' ||
|
|
@@ -231,12 +231,12 @@ export class Provider {
|
|
|
231
231
|
cached: extractCachedTokens(usage),
|
|
232
232
|
});
|
|
233
233
|
}
|
|
234
|
-
async raceWithIdleTimeout(fn, timeoutMs) {
|
|
234
|
+
async raceWithIdleTimeout(fn, timeoutMs, busy = { tools: 0 }) {
|
|
235
235
|
const cancel = { cancelled: false };
|
|
236
236
|
const controller = new AbortController();
|
|
237
237
|
const combinedSignal = combinedAbortSignal(controller);
|
|
238
238
|
try {
|
|
239
|
-
return await Promise.race([fn(combinedSignal), abortAfterIdle(timeoutMs, cancel, controller)]);
|
|
239
|
+
return await Promise.race([fn(combinedSignal), abortAfterIdle(timeoutMs, cancel, controller, busy)]);
|
|
240
240
|
}
|
|
241
241
|
finally {
|
|
242
242
|
cancel.cancelled = true;
|
|
@@ -249,6 +249,23 @@ export class Provider {
|
|
|
249
249
|
tag('warning').log('Context length exceeded, retrying with reduced messages...');
|
|
250
250
|
return retry(reduced.messages, { ...options, _contextRetryLevel: reduced.nextLevel });
|
|
251
251
|
}
|
|
252
|
+
async recoverWithPlainJson(messages, schema, model, options) {
|
|
253
|
+
const target = asSchema(schema);
|
|
254
|
+
tag('warning').log(`${getModelName(model)} returned no structured output, asking for plain JSON instead`);
|
|
255
|
+
const instruction = dedent `
|
|
256
|
+
Respond with a single JSON object that matches this JSON Schema, and nothing else:
|
|
257
|
+
${JSON.stringify(await target.jsonSchema)}
|
|
258
|
+
`;
|
|
259
|
+
const response = await this.chat([...messages, { role: 'user', content: instruction }], wrapLanguageModel({ model, middleware: extractJsonMiddleware() }), options);
|
|
260
|
+
const parsed = await parsePartialJson(response.text);
|
|
261
|
+
if (parsed.state !== 'successful-parse')
|
|
262
|
+
throw new AiError('No object generated: plain JSON fallback returned no parsable JSON');
|
|
263
|
+
const validated = await target.validate?.(parsed.value);
|
|
264
|
+
if (validated && !validated.success)
|
|
265
|
+
throw new AiError(`No object generated: plain JSON fallback did not match the schema: ${validated.error.message}`);
|
|
266
|
+
responseLog(parsed.value);
|
|
267
|
+
return { ...response, object: validated?.value ?? parsed.value };
|
|
268
|
+
}
|
|
252
269
|
initLangfuse() {
|
|
253
270
|
const { enabled, publicKey, secretKey, baseUrl } = this.config.langfuse || {};
|
|
254
271
|
if (!enabled || !publicKey || !secretKey) {
|
|
@@ -369,6 +386,8 @@ export class Provider {
|
|
|
369
386
|
}
|
|
370
387
|
async generateWithTools(messages, model, tools, options = {}) {
|
|
371
388
|
const modelName = getModelName(model);
|
|
389
|
+
const busy = { tools: 0 };
|
|
390
|
+
tools = withIdleExemption(tools, busy);
|
|
372
391
|
setActivity(`🤖 Asking ${modelName} with dynamic tools`, 'ai');
|
|
373
392
|
promptLog(`Using model: ${modelName}`);
|
|
374
393
|
let toolsWithCommentary = tools;
|
|
@@ -394,7 +413,7 @@ export class Provider {
|
|
|
394
413
|
const onStepEnd = (step) => {
|
|
395
414
|
stepMessages.push(...(step.response?.messages || []));
|
|
396
415
|
};
|
|
397
|
-
const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages: attemptMessages, ...config, abortSignal: signal, onStepEnd }), config.timeout || 30000).catch((error) => {
|
|
416
|
+
const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages: attemptMessages, ...config, abortSignal: signal, onStepEnd }), config.timeout || 30000, busy).catch((error) => {
|
|
398
417
|
if (stepMessages.length > 0) {
|
|
399
418
|
tag('warning').log(`Keeping ${stepMessages.length} messages from tool steps that already ran before the failure`);
|
|
400
419
|
executedStepMessages.push(...stepMessages);
|
|
@@ -476,6 +495,8 @@ export class Provider {
|
|
|
476
495
|
if (Provider.isContextLengthError(error)) {
|
|
477
496
|
return this.recoverFromContextLength(error, messages, options, (m, o) => this.generateObject(m, schema, model, o));
|
|
478
497
|
}
|
|
498
|
+
if (NoObjectGeneratedError.isInstance(error))
|
|
499
|
+
return this.recoverWithPlainJson(messages, schema, modelToUse, options);
|
|
479
500
|
throw new AiError(error.message || error.toString());
|
|
480
501
|
}
|
|
481
502
|
}
|
|
@@ -695,4 +716,28 @@ function repairHarmonyChannel({ toolCall, tools }) {
|
|
|
695
716
|
tag('warning').log(`Repaired tool name '${toolCall.toolName}' → 'commentary'`);
|
|
696
717
|
return { ...toolCall, toolName: NARRATION_TOOL, input };
|
|
697
718
|
}
|
|
719
|
+
function withIdleExemption(tools, busy) {
|
|
720
|
+
if (!tools)
|
|
721
|
+
return tools;
|
|
722
|
+
const wrapped = {};
|
|
723
|
+
for (const [name, definition] of Object.entries(tools)) {
|
|
724
|
+
if (typeof definition?.execute !== 'function') {
|
|
725
|
+
wrapped[name] = definition;
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
wrapped[name] = {
|
|
729
|
+
...definition,
|
|
730
|
+
execute: async (...args) => {
|
|
731
|
+
busy.tools++;
|
|
732
|
+
try {
|
|
733
|
+
return await definition.execute(...args);
|
|
734
|
+
}
|
|
735
|
+
finally {
|
|
736
|
+
busy.tools--;
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
return wrapped;
|
|
742
|
+
}
|
|
698
743
|
export { AiError, Provider as AIProvider };
|
package/dist/src/ai/rerunner.js
CHANGED
|
@@ -18,6 +18,7 @@ import { formatHeadings } from "../utils/context-formatter.js";
|
|
|
18
18
|
import { createDebug, tag } from "../utils/logger.js";
|
|
19
19
|
import { loop } from "../utils/loop.js";
|
|
20
20
|
import { RulesLoader } from "../utils/rules-loader.js";
|
|
21
|
+
import { isInternalStep } from "../utils/step-analyzer.js";
|
|
21
22
|
import { toolExecutionLabel } from "./conversation.js";
|
|
22
23
|
import { actionRule, locatorRule, sectionContextRule } from "./rules.js";
|
|
23
24
|
import { TaskAgent } from "./task-agent.js";
|
|
@@ -70,6 +71,8 @@ export class Rerunner extends TaskAgent {
|
|
|
70
71
|
const onStepStarted = (step) => {
|
|
71
72
|
if (!step.toCode)
|
|
72
73
|
return;
|
|
74
|
+
if (isInternalStep(step))
|
|
75
|
+
return;
|
|
73
76
|
const code = highlight(step.toCode(), { language: 'javascript' });
|
|
74
77
|
console.log(chalk.dim(` ${code}`));
|
|
75
78
|
};
|
|
@@ -77,12 +80,16 @@ export class Rerunner extends TaskAgent {
|
|
|
77
80
|
const task = this.getCurrentTask(testMap);
|
|
78
81
|
if (!task || !step.toCode)
|
|
79
82
|
return;
|
|
83
|
+
if (isInternalStep(step))
|
|
84
|
+
return;
|
|
80
85
|
task.addStep(step.toCode(), step.duration, 'passed');
|
|
81
86
|
};
|
|
82
87
|
const onStepFailed = (step, error) => {
|
|
83
88
|
const task = this.getCurrentTask(testMap);
|
|
84
89
|
if (!task || !step.toCode)
|
|
85
90
|
return;
|
|
91
|
+
if (isInternalStep(step))
|
|
92
|
+
return;
|
|
86
93
|
task.addStep(step.toCode(), step.duration, 'failed', error?.message);
|
|
87
94
|
console.log(chalk.red(` ${figureSet.cross} ${step.toCode()} — ${error?.message || 'failed'}`));
|
|
88
95
|
};
|
|
@@ -94,7 +94,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
94
94
|
let updated;
|
|
95
95
|
if (extQuery.count() > 0) {
|
|
96
96
|
const existing = extQuery.text().trimEnd();
|
|
97
|
-
updated = extQuery.replace(`${existing}\n\n${sectionMarkdown}\n`);
|
|
97
|
+
updated = extQuery.replace(`${existing}\n\n${sectionMarkdown}\n`).toString();
|
|
98
98
|
}
|
|
99
99
|
else {
|
|
100
100
|
updated = `${cached.trimEnd()}\n\n# Extended Research\n\n${sectionMarkdown}\n`;
|
|
@@ -481,7 +481,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
481
481
|
heading = mdq(sectionMarkdown).query('h2[0]');
|
|
482
482
|
if (heading.count() === 0)
|
|
483
483
|
return sectionMarkdown;
|
|
484
|
-
return heading.replace(`${heading.text().trimEnd()}\n\n> Container: '${containerCss}'\n\n`);
|
|
484
|
+
return heading.replace(`${heading.text().trimEnd()}\n\n> Container: '${containerCss}'\n\n`).toString();
|
|
485
485
|
}
|
|
486
486
|
_deduplicateExpandedSections(sections) {
|
|
487
487
|
const seen = new Set();
|
|
@@ -281,10 +281,10 @@ export function WithLocators(Base) {
|
|
|
281
281
|
if (sectionQuery.count() === 0)
|
|
282
282
|
sectionQuery = mdq(result.text).query(`section3(~"${escaped}")`);
|
|
283
283
|
if (newCss) {
|
|
284
|
-
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Container', `'${newCss}'`);
|
|
284
|
+
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Container', `'${newCss}'`).toString();
|
|
285
285
|
}
|
|
286
286
|
else {
|
|
287
|
-
result.text = sectionQuery.query('blockquote[0]').replace('');
|
|
287
|
+
result.text = sectionQuery.query('blockquote[0]').replace('').toString();
|
|
288
288
|
result.text = result.text.replace(`${FOCUSED_MARKER}\n`, '');
|
|
289
289
|
}
|
|
290
290
|
for (const loc of result.locators) {
|
|
@@ -56,7 +56,7 @@ export function WithPagination(Base) {
|
|
|
56
56
|
sectionQuery = mdq(result.text).query(`section3(~"${escaped}")`);
|
|
57
57
|
if (sectionQuery.count() === 0)
|
|
58
58
|
return;
|
|
59
|
-
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Pagination', strategy);
|
|
59
|
+
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Pagination', strategy).toString();
|
|
60
60
|
}
|
|
61
61
|
};
|
|
62
62
|
}
|
|
@@ -50,10 +50,10 @@ export class ResearchResult {
|
|
|
50
50
|
let sectionQuery = mdq(this.text).query(`section2(~"${escaped}")`);
|
|
51
51
|
if (sectionQuery.count() === 0)
|
|
52
52
|
sectionQuery = mdq(this.text).query(`section3(~"${escaped}")`);
|
|
53
|
-
const updated = sectionQuery.query('table').replace(`${newTable.trimEnd()}\n`);
|
|
53
|
+
const updated = sectionQuery.query('table').replace(`${newTable.trimEnd()}\n`).toString();
|
|
54
54
|
if (updated === this.text)
|
|
55
55
|
return;
|
|
56
|
-
section.rawMarkdown = mdq(section.rawMarkdown).query('table').replace(`${newTable.trimEnd()}\n`);
|
|
56
|
+
section.rawMarkdown = mdq(section.rawMarkdown).query('table').replace(`${newTable.trimEnd()}\n`).toString();
|
|
57
57
|
this.text = updated;
|
|
58
58
|
}
|
|
59
59
|
cleanup() {
|
|
@@ -261,7 +261,7 @@ export class Researcher extends ResearcherBase {
|
|
|
261
261
|
if (stateHash) {
|
|
262
262
|
researchFile = saveResearch(researchState, result.text, combinedHtml);
|
|
263
263
|
}
|
|
264
|
-
const summaryText = mdq(result.text).query('section2(/^summary/)').query('paragraph[0]').text().trim();
|
|
264
|
+
const summaryText = mdq(result.text).query('section2(/^summary/i)').query('paragraph[0]').text().trim();
|
|
265
265
|
const summaryLine = summaryText.split('\n')[0]?.trim().slice(0, 200);
|
|
266
266
|
if (summaryLine)
|
|
267
267
|
this.experienceTracker.updateSummary(this.actionResult, summaryLine);
|
|
@@ -7,6 +7,7 @@ import type { StateManager } from '../state-manager.js';
|
|
|
7
7
|
import { HooksRunner } from '../utils/hooks-runner.js';
|
|
8
8
|
import type { AgentDeps, ToolDeps } from './agent.js';
|
|
9
9
|
import { Historian } from './historian.js';
|
|
10
|
+
import type { Judge } from './judge.js';
|
|
10
11
|
import type { Navigator } from './navigator.js';
|
|
11
12
|
import type { Provider } from './provider.js';
|
|
12
13
|
import { Quartermaster } from './quartermaster.js';
|
|
@@ -17,6 +18,7 @@ export declare abstract class TaskAgent {
|
|
|
17
18
|
config: ExplorbotConfig;
|
|
18
19
|
stateManager: StateManager;
|
|
19
20
|
knowledgeTracker: KnowledgeTracker;
|
|
21
|
+
judge?: Judge;
|
|
20
22
|
hooksRunner: HooksRunner;
|
|
21
23
|
consecutiveFailures: number;
|
|
22
24
|
consecutiveEmptyResults: number;
|
|
@@ -16,6 +16,7 @@ export class TaskAgent {
|
|
|
16
16
|
config;
|
|
17
17
|
stateManager;
|
|
18
18
|
knowledgeTracker;
|
|
19
|
+
judge;
|
|
19
20
|
hooksRunner;
|
|
20
21
|
consecutiveFailures = 0;
|
|
21
22
|
consecutiveEmptyResults = 0;
|
|
@@ -31,6 +32,7 @@ export class TaskAgent {
|
|
|
31
32
|
this.config = deps.config;
|
|
32
33
|
this.stateManager = deps.stateManager;
|
|
33
34
|
this.knowledgeTracker = deps.knowledgeTracker;
|
|
35
|
+
this.judge = deps.judge;
|
|
34
36
|
this.hooksRunner = new HooksRunner(deps.explorer, deps.config);
|
|
35
37
|
}
|
|
36
38
|
setHistorian(historian) {
|
|
@@ -40,7 +42,7 @@ export class TaskAgent {
|
|
|
40
42
|
this._quartermaster = quartermaster;
|
|
41
43
|
}
|
|
42
44
|
get toolDeps() {
|
|
43
|
-
return { explorer: this.explorer, stateManager: this.stateManager, ai: this.provider };
|
|
45
|
+
return { explorer: this.explorer, stateManager: this.stateManager, ai: this.provider, judge: this.judge };
|
|
44
46
|
}
|
|
45
47
|
getExperienceTracker() {
|
|
46
48
|
return this.stateManager.getExperienceTracker();
|
package/dist/src/ai/tester.js
CHANGED
|
@@ -112,8 +112,8 @@ export class Tester extends TaskAgent {
|
|
|
112
112
|
let initialState = ActionResult.fromState(state);
|
|
113
113
|
const currentUrl = state.fullUrl || state.url;
|
|
114
114
|
let startOnCurrentPage = opts.startOnCurrentPage;
|
|
115
|
-
if (
|
|
116
|
-
debugLog(`
|
|
115
|
+
if (!startOnCurrentPage && task.startUrl && normalizeUrl(currentUrl) !== normalizeUrl(task.startUrl)) {
|
|
116
|
+
debugLog(`Opening test start URL ${task.startUrl} before building context (was at ${currentUrl})`);
|
|
117
117
|
try {
|
|
118
118
|
await this.explorer.visit(task.startUrl);
|
|
119
119
|
state = this.stateManager.getCurrentState();
|
|
@@ -123,7 +123,7 @@ export class Tester extends TaskAgent {
|
|
|
123
123
|
startOnCurrentPage = true;
|
|
124
124
|
}
|
|
125
125
|
catch (error) {
|
|
126
|
-
debugLog(`Could not
|
|
126
|
+
debugLog(`Could not open test start URL: ${compactErrorMessage(error)}`);
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
if (isErrorPage(initialState)) {
|
|
@@ -135,13 +135,6 @@ export class Tester extends TaskAgent {
|
|
|
135
135
|
const conversation = this.provider.startConversation(this.getSystemMessage(), 'tester');
|
|
136
136
|
conversation.markLastMessageCacheable();
|
|
137
137
|
this.currentConversation = conversation;
|
|
138
|
-
const scenarioBlock = this.buildScenarioBlock(task, initialState);
|
|
139
|
-
conversation.addUserText(scenarioBlock);
|
|
140
|
-
conversation.markLastMessageCacheable();
|
|
141
|
-
conversation.protectPrefix(conversation.messages.length);
|
|
142
|
-
const pageContext = await this.reinjectContextIfNeeded(1, initialState);
|
|
143
|
-
if (pageContext)
|
|
144
|
-
conversation.addUserText(pageContext);
|
|
145
138
|
return await Observability.run(`test: ${task.scenario}`, {
|
|
146
139
|
sessionId: task.sessionName,
|
|
147
140
|
tags: ['tester'],
|
|
@@ -154,16 +147,15 @@ export class Tester extends TaskAgent {
|
|
|
154
147
|
}
|
|
155
148
|
async runTestSession(task, initialState, conversation, handlers, opts) {
|
|
156
149
|
const { offFailedRequest } = handlers;
|
|
150
|
+
let plan = '';
|
|
157
151
|
if (this.pilot) {
|
|
158
152
|
try {
|
|
159
|
-
|
|
153
|
+
await this.researcher.research(initialState).catch(this.skipResearch);
|
|
154
|
+
plan = await this.pilot.planTest(task, initialState);
|
|
160
155
|
if (task.hasFinished) {
|
|
161
156
|
offFailedRequest?.();
|
|
162
157
|
return { success: task.isSuccessful };
|
|
163
158
|
}
|
|
164
|
-
if (plan) {
|
|
165
|
-
conversation.addUserText(`Pilot's test plan:\n${plan}\n\nFollow this plan while executing the test.`);
|
|
166
|
-
}
|
|
167
159
|
}
|
|
168
160
|
catch (err) {
|
|
169
161
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -183,7 +175,7 @@ export class Tester extends TaskAgent {
|
|
|
183
175
|
}
|
|
184
176
|
if (opts.startOnCurrentPage)
|
|
185
177
|
debugLog(`Starting on the page already open at ${task.startUrl}`);
|
|
186
|
-
if (!opts.startOnCurrentPage) {
|
|
178
|
+
if (!opts.startOnCurrentPage || task.preparedData.length > 0) {
|
|
187
179
|
debugLog(`Navigating to ${task.startUrl}`);
|
|
188
180
|
try {
|
|
189
181
|
await this.explorer.visit(task.startUrl);
|
|
@@ -208,6 +200,15 @@ export class Tester extends TaskAgent {
|
|
|
208
200
|
}
|
|
209
201
|
const currentUrl = startState?.url || task.startUrl || '';
|
|
210
202
|
await this.hooksRunner.runBeforeHook('tester', currentUrl);
|
|
203
|
+
const testerState = this.getCurrentState();
|
|
204
|
+
conversation.addUserText(this.buildScenarioBlock(task, testerState));
|
|
205
|
+
conversation.markLastMessageCacheable();
|
|
206
|
+
conversation.protectPrefix(conversation.messages.length);
|
|
207
|
+
const pageContext = await this.reinjectContextIfNeeded(1, testerState);
|
|
208
|
+
if (pageContext)
|
|
209
|
+
conversation.addUserText(pageContext);
|
|
210
|
+
if (plan)
|
|
211
|
+
conversation.addUserText(`Pilot's test plan:\n${plan}\n\nFollow this plan while executing the test.`);
|
|
211
212
|
const offStateChange = this.stateManager.onStateChange((event) => {
|
|
212
213
|
if (task.hasFinished)
|
|
213
214
|
return;
|
|
@@ -836,6 +837,9 @@ export class Tester extends TaskAgent {
|
|
|
836
837
|
`;
|
|
837
838
|
}
|
|
838
839
|
buildDeletionScope(task) {
|
|
840
|
+
if (task.preparedData.length > 0) {
|
|
841
|
+
return `When deleting items, ONLY delete the data prepared for this test: ${task.preparedData.join('; ')}.`;
|
|
842
|
+
}
|
|
839
843
|
const deletableItems = this.getDeletableSessionNames(task);
|
|
840
844
|
if (deletableItems.length > 0) {
|
|
841
845
|
return `When deleting items, ONLY delete items whose title contains one of these session names: ${deletableItems.join(', ')}. These were created by previous tests.`;
|
package/dist/src/ai/tools.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ExecutedStep } from '../action.js';
|
|
|
3
3
|
import { type ExperienceTracker } from '../experience-tracker.js';
|
|
4
4
|
import { type Task } from '../test-plan.js';
|
|
5
5
|
import type { ToolDeps } from './agent.js';
|
|
6
|
+
import { type Judge } from './judge.js';
|
|
6
7
|
import { Navigator } from './navigator.js';
|
|
7
8
|
import { Researcher } from './researcher.js';
|
|
8
9
|
interface AgentToolDeps extends ToolDeps {
|
|
@@ -12,7 +13,7 @@ interface AgentToolDeps extends ToolDeps {
|
|
|
12
13
|
withExperience?: boolean;
|
|
13
14
|
}
|
|
14
15
|
export declare const ASSERTION_TOOLS: readonly ["verify"];
|
|
15
|
-
export declare function createCodeceptJSTools({ explorer, stateManager }: ToolDeps, task: Task): {
|
|
16
|
+
export declare function createCodeceptJSTools({ explorer, stateManager, judge }: ToolDeps, task: Task): {
|
|
16
17
|
click: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
|
|
17
18
|
commands: any;
|
|
18
19
|
explanation: any;
|
|
@@ -55,7 +56,7 @@ export declare function createLearnExperienceTool({ getExperienceTracker, getSta
|
|
|
55
56
|
} | {
|
|
56
57
|
error: string;
|
|
57
58
|
}, import("@ai-sdk/provider-utils").Context>>;
|
|
58
|
-
export declare function createAgentTools({ explorer, stateManager, ai, researcher, navigator, supervisor, withExperience }: AgentToolDeps): any;
|
|
59
|
+
export declare function createAgentTools({ explorer, stateManager, ai, judge, researcher, navigator, supervisor, withExperience }: AgentToolDeps): any;
|
|
59
60
|
export declare function commitNote(activeNote: any, result: TestResult, toolResult: any, action: any): Promise<void>;
|
|
60
61
|
export declare function successToolResult(action: string, data?: Record<string, any>, source?: {
|
|
61
62
|
playwrightGroupId?: string | null;
|
|
@@ -64,7 +65,7 @@ export declare function successToolResult(action: string, data?: Record<string,
|
|
|
64
65
|
export declare function isMajorPageChange(pageDiff: PageDiff): boolean;
|
|
65
66
|
export declare function hasFailedRequest(pageDiff: PageDiff): boolean;
|
|
66
67
|
export declare function formatExecutedSteps(steps: ExecutedStep[], requestedCount?: number): string;
|
|
67
|
-
export declare function failedToolResult(action: string, message: string, data?: Record<string, any>, error?: Error | null): Promise<Record<string, any>>;
|
|
68
|
+
export declare function failedToolResult(action: string, message: string, data?: Record<string, any>, error?: Error | null, judge?: Judge, intent?: string): Promise<Record<string, any>>;
|
|
68
69
|
export declare function withdrawVisionTools(tools: Record<string, any>): void;
|
|
69
70
|
export declare function clickFailureSuggestion(attempts: Array<{
|
|
70
71
|
error?: string;
|
package/dist/src/ai/tools.js
CHANGED
|
@@ -13,11 +13,13 @@ import { createDebug, tag } from '../utils/logger.js';
|
|
|
13
13
|
import { pause } from '../utils/loop.js';
|
|
14
14
|
import { compactErrorMessage, normalizeInlineText, truncate } from "../utils/strings.js";
|
|
15
15
|
import { WebElement } from "../utils/web-element.js";
|
|
16
|
+
import { createJudgeTool } from "./judge-tool.js";
|
|
17
|
+
import { JUDGE_PAGE_CAP, UNDECIDED } from "./judge.js";
|
|
16
18
|
import { sectionContextRule } from "./rules.js";
|
|
17
19
|
import { isInteractive } from "./task-agent.js";
|
|
18
20
|
const debugLog = createDebug('explorbot:tools');
|
|
19
21
|
export const ASSERTION_TOOLS = ['verify'];
|
|
20
|
-
export function createCodeceptJSTools({ explorer, stateManager }, task) {
|
|
22
|
+
export function createCodeceptJSTools({ explorer, stateManager, judge }, task) {
|
|
21
23
|
return {
|
|
22
24
|
click: tool({
|
|
23
25
|
description: dedent `
|
|
@@ -125,7 +127,7 @@ export function createCodeceptJSTools({ explorer, stateManager }, task) {
|
|
|
125
127
|
...toolResult,
|
|
126
128
|
attempts,
|
|
127
129
|
suggestion,
|
|
128
|
-
}, ambiguityError || action.lastError);
|
|
130
|
+
}, ambiguityError || action.lastError, judge, explanation);
|
|
129
131
|
},
|
|
130
132
|
}),
|
|
131
133
|
hover: tool({
|
|
@@ -189,7 +191,7 @@ export function createCodeceptJSTools({ explorer, stateManager }, task) {
|
|
|
189
191
|
...toolResult,
|
|
190
192
|
attempts,
|
|
191
193
|
suggestion: 'Use xpathCheck() to locate the row/card/tree node, or visualClick() if the hover target is only visually identifiable.',
|
|
192
|
-
}, action.lastError);
|
|
194
|
+
}, action.lastError, judge, explanation);
|
|
193
195
|
},
|
|
194
196
|
}),
|
|
195
197
|
pressKey: tool({
|
|
@@ -312,7 +314,7 @@ export function createCodeceptJSTools({ explorer, stateManager }, task) {
|
|
|
312
314
|
- Performing multiple form actions in a single batch
|
|
313
315
|
- Complex interactions requiring sequential commands
|
|
314
316
|
- Reaching items further down a list (I.scrollTo)
|
|
315
|
-
- Reloading the page to prove a change outlived it (I.
|
|
317
|
+
- Reloading the page to prove a change outlived it (I.refreshPage)
|
|
316
318
|
|
|
317
319
|
Example - filling a form with context (PREFERRED):
|
|
318
320
|
I.fillField('Username', 'John', '.login-form')
|
|
@@ -363,7 +365,7 @@ export function createCodeceptJSTools({ explorer, stateManager }, task) {
|
|
|
363
365
|
code: codeBlock,
|
|
364
366
|
attempts: action.executedSteps,
|
|
365
367
|
suggestion: formSuggestion,
|
|
366
|
-
}, action.lastError);
|
|
368
|
+
}, action.lastError, judge, explanation);
|
|
367
369
|
}
|
|
368
370
|
if (!hasObservablePageChange(toolResult)) {
|
|
369
371
|
activeNote.commit(TestResult.FAILED);
|
|
@@ -498,7 +500,7 @@ export function createLearnExperienceTool({ getExperienceTracker, getState }) {
|
|
|
498
500
|
},
|
|
499
501
|
});
|
|
500
502
|
}
|
|
501
|
-
export function createAgentTools({ explorer, stateManager, ai, researcher, navigator, supervisor, withExperience }) {
|
|
503
|
+
export function createAgentTools({ explorer, stateManager, ai, judge, researcher, navigator, supervisor, withExperience }) {
|
|
502
504
|
const tools = {
|
|
503
505
|
see: tool({
|
|
504
506
|
description: dedent `
|
|
@@ -619,6 +621,12 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
619
621
|
}, { assertionSteps: result.assertionSteps });
|
|
620
622
|
}
|
|
621
623
|
if (result.inexpressible) {
|
|
624
|
+
if (result.judged?.approved) {
|
|
625
|
+
return failedToolResult('verify', `No assertion could express this claim, but the page appears to confirm it: ${assertion}`, {
|
|
626
|
+
inexpressible: true,
|
|
627
|
+
suggestion: 'This is a judgement about the page, not an assertion that ran in the browser — a hint, not proof. Restate the claim in terms of what is visible or of a control state to get a real assertion.',
|
|
628
|
+
});
|
|
629
|
+
}
|
|
622
630
|
return failedToolResult('verify', `No assertion could express this claim: ${assertion}`, {
|
|
623
631
|
inexpressible: true,
|
|
624
632
|
suggestion: 'This is not evidence the page is wrong — the claim could not be turned into an assertion. Restate it in terms of what is visible or of a control state, or check it with see().',
|
|
@@ -994,6 +1002,19 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
994
1002
|
},
|
|
995
1003
|
});
|
|
996
1004
|
}
|
|
1005
|
+
const buildJudgeState = async () => {
|
|
1006
|
+
const activeTest = explorer.activeTest;
|
|
1007
|
+
const state = stateManager.getCurrentState();
|
|
1008
|
+
const result = state ? ActionResult.fromState(state) : null;
|
|
1009
|
+
return {
|
|
1010
|
+
task: activeTest?.scenario || '',
|
|
1011
|
+
page: cap(result?.getCompactARIA(), JUDGE_PAGE_CAP),
|
|
1012
|
+
recentActions: Object.values(activeTest?.steps || {})
|
|
1013
|
+
.slice(-JUDGE_RECENT_ACTIONS_LIMIT)
|
|
1014
|
+
.map((step) => step.text),
|
|
1015
|
+
};
|
|
1016
|
+
};
|
|
1017
|
+
Object.assign(tools, createJudgeTool({ explorer, stateManager, ai, judge }, buildJudgeState));
|
|
997
1018
|
withdrawVisionTools(tools);
|
|
998
1019
|
return tools;
|
|
999
1020
|
}
|
|
@@ -1003,6 +1024,7 @@ const NAVIGATED_SUGGESTION = 'The action left the page. Elements are never compa
|
|
|
1003
1024
|
const ARIA_OUTPUT_CAP = 4000;
|
|
1004
1025
|
const HTML_OUTPUT_CAP = 6000;
|
|
1005
1026
|
const ANALYSIS_OUTPUT_CAP = 2000;
|
|
1027
|
+
const JUDGE_RECENT_ACTIONS_LIMIT = 8;
|
|
1006
1028
|
function cap(text, max) {
|
|
1007
1029
|
if (!text)
|
|
1008
1030
|
return '';
|
|
@@ -1115,7 +1137,7 @@ function hasObservablePageChange(data) {
|
|
|
1115
1137
|
return true;
|
|
1116
1138
|
return Array.isArray(data.pageDiff.htmlParts) && data.pageDiff.htmlParts.length > 0;
|
|
1117
1139
|
}
|
|
1118
|
-
export async function failedToolResult(action, message, data, error) {
|
|
1140
|
+
export async function failedToolResult(action, message, data, error, judge, intent) {
|
|
1119
1141
|
const result = { success: false, action, message, ...data };
|
|
1120
1142
|
if (data?.pageDiff) {
|
|
1121
1143
|
result.suggestion = data.suggestion ? `${data.suggestion} ${PAGE_DIFF_SUGGESTION}` : PAGE_DIFF_SUGGESTION;
|
|
@@ -1126,6 +1148,12 @@ export async function failedToolResult(action, message, data, error) {
|
|
|
1126
1148
|
result.suggestion = getMultipleElementsSuggestion();
|
|
1127
1149
|
result.multipleElementsDetected = true;
|
|
1128
1150
|
result.elements = formatElementList(matched);
|
|
1151
|
+
const labels = (matched || []).map((element) => `${element.text || 'no text'} — ${element.html}`);
|
|
1152
|
+
const pick = await judge?.decide('Which listed element does the intent name?', [...labels, UNDECIDED], { intent });
|
|
1153
|
+
if (!pick?.value)
|
|
1154
|
+
return result;
|
|
1155
|
+
const index = labels.indexOf(pick.value) + 1;
|
|
1156
|
+
result.suggestion = `Element ${index} is the one meant. Repeat the action with step.opts({ elementIndex: ${index} }) as the last argument.`;
|
|
1129
1157
|
return result;
|
|
1130
1158
|
}
|
|
1131
1159
|
const notFoundSuggestion = getNotFoundSuggestion(message);
|
|
@@ -162,7 +162,8 @@ export class RequestResult {
|
|
|
162
162
|
if (Array.isArray(data))
|
|
163
163
|
return {};
|
|
164
164
|
const id = data.id ?? data._id ?? data.uuid;
|
|
165
|
-
const
|
|
165
|
+
const attributes = data.attributes || {};
|
|
166
|
+
const title = data.name ?? data.title ?? data.label ?? attributes.name ?? attributes.title ?? attributes.label;
|
|
166
167
|
const result = {};
|
|
167
168
|
if (id !== undefined)
|
|
168
169
|
result.id = id;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSameHostFamily } from '../utils/url-matcher.js';
|
|
1
2
|
import { RequestResult, generateRequestId } from "./request-result.js";
|
|
2
3
|
const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
|
3
4
|
const JSON_CONTENT_TYPES = /application\/json|application\/.*\+json/i;
|
|
@@ -33,7 +34,7 @@ export class XhrCapture {
|
|
|
33
34
|
return;
|
|
34
35
|
const method = request.method();
|
|
35
36
|
const url = request.url();
|
|
36
|
-
if (!url
|
|
37
|
+
if (!isSameHostFamily(url, this.baseOrigin))
|
|
37
38
|
return;
|
|
38
39
|
const status = response.status();
|
|
39
40
|
if (status >= 400) {
|
|
@@ -207,6 +207,28 @@ export class CommandHandler {
|
|
|
207
207
|
const parsed = parseCommand(query);
|
|
208
208
|
const exactCommand = parsed ? this.findCommand(parsed.name) : undefined;
|
|
209
209
|
const hasArguments = commandEnd !== -1 && input.slice(commandEnd + 1).trim().length > 0;
|
|
210
|
+
const argumentHint = !insideCommand && exactCommand && !hasArguments && exactCommand.options.length > 0 ? exactCommand.options.map((option) => option.flags).join(' ') : undefined;
|
|
211
|
+
if (!insideCommand && exactCommand) {
|
|
212
|
+
const argumentQuery = input.slice(commandEnd + 1).trim();
|
|
213
|
+
const argumentEntries = exactCommand
|
|
214
|
+
.completeArguments()
|
|
215
|
+
.filter((completion) => completion.value !== argumentQuery)
|
|
216
|
+
.map((completion) => ({
|
|
217
|
+
aliases: [],
|
|
218
|
+
canonical: completion.value,
|
|
219
|
+
description: '',
|
|
220
|
+
value: completion.display || completion.value,
|
|
221
|
+
}));
|
|
222
|
+
const argumentSuggestions = this.rankSuggestions(argumentQuery, argumentEntries);
|
|
223
|
+
return {
|
|
224
|
+
suggestions: argumentSuggestions,
|
|
225
|
+
replaceFrom: commandEnd + 1,
|
|
226
|
+
replaceTo: input.length,
|
|
227
|
+
visible: argumentSuggestions.length > 0,
|
|
228
|
+
argumentHint,
|
|
229
|
+
completesArgument: true,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
210
232
|
const commandEntries = this.getSlashCommandEntries();
|
|
211
233
|
let suggestions = [];
|
|
212
234
|
if (insideCommand) {
|
|
@@ -222,9 +244,8 @@ export class CommandHandler {
|
|
|
222
244
|
suggestions = this.rankSuggestions(query, commandEntries);
|
|
223
245
|
}
|
|
224
246
|
}
|
|
225
|
-
const argumentHint = !insideCommand && exactCommand && !hasArguments && exactCommand.options.length > 0 ? exactCommand.options.map((option) => option.flags).join(' ') : undefined;
|
|
226
247
|
return {
|
|
227
|
-
suggestions,
|
|
248
|
+
suggestions: suggestions.map((suggestion) => ({ ...suggestion, value: `${suggestion.value} ` })),
|
|
228
249
|
replaceFrom: 0,
|
|
229
250
|
replaceTo,
|
|
230
251
|
visible: insideCommand && suggestions.length > 0,
|
|
@@ -305,7 +326,7 @@ export class CommandHandler {
|
|
|
305
326
|
rankSuggestions(query, entries) {
|
|
306
327
|
if (!query) {
|
|
307
328
|
return entries.slice(0, 20).map((entry) => ({
|
|
308
|
-
value: entry.
|
|
329
|
+
value: entry.canonical,
|
|
309
330
|
display: entry.value,
|
|
310
331
|
description: entry.description,
|
|
311
332
|
argumentHint: entry.argumentHint,
|
|
@@ -18,9 +18,14 @@ export declare abstract class BaseCommand<T = ExplorBot> {
|
|
|
18
18
|
constructor(explorBot: T);
|
|
19
19
|
abstract execute(args: string): Promise<void>;
|
|
20
20
|
matches(commandName: string): boolean;
|
|
21
|
+
completeArguments(): ArgumentCompletion[];
|
|
21
22
|
printSuggestions(): void;
|
|
22
23
|
parseArgs(args: string): {
|
|
23
24
|
opts: Record<string, string | boolean>;
|
|
24
25
|
args: string[];
|
|
25
26
|
};
|
|
26
27
|
}
|
|
28
|
+
export interface ArgumentCompletion {
|
|
29
|
+
value: string;
|
|
30
|
+
display?: string;
|
|
31
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Plan, type Test } from '../test-plan.js';
|
|
2
|
-
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
2
|
+
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
3
3
|
export declare const DEADLINE_RESERVE_MS: number;
|
|
4
4
|
export declare const DEADLINE_TEST_ALLOWANCE_MS: number;
|
|
5
5
|
export declare class ExploreCommand extends BaseCommand {
|
|
@@ -22,6 +22,7 @@ export declare class ExploreCommand extends BaseCommand {
|
|
|
22
22
|
priorityFilter?: Set<string>;
|
|
23
23
|
getCurrentPageUrl(): string | undefined;
|
|
24
24
|
execute(args: string): Promise<void>;
|
|
25
|
+
completeArguments(): ArgumentCompletion[];
|
|
25
26
|
originLabel(test: Test): string;
|
|
26
27
|
printPreview(label: string, tests: Test[]): void;
|
|
27
28
|
runFreshMode(mainUrl: string | undefined, feature: string | undefined, styles?: string[]): Promise<void>;
|