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/src/ai/planner.ts
CHANGED
|
@@ -301,13 +301,14 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
301
301
|
.replaceEach((section) => {
|
|
302
302
|
const heading = section.query('heading').text().trim();
|
|
303
303
|
const withoutHeadings = mdq(section.text()).query('heading').replace('');
|
|
304
|
-
const body = mdq(withoutHeadings).query('hr').replace('').trim();
|
|
304
|
+
const body = mdq(withoutHeadings).query('hr').replace('').toString().trim();
|
|
305
305
|
if (body && !seenTitles.has(heading)) {
|
|
306
306
|
seenTitles.add(heading);
|
|
307
307
|
return section.text();
|
|
308
308
|
}
|
|
309
309
|
return '';
|
|
310
|
-
})
|
|
310
|
+
})
|
|
311
|
+
.toString();
|
|
311
312
|
}
|
|
312
313
|
|
|
313
314
|
const trimmedTitles = new Set<string>();
|
|
@@ -319,10 +320,11 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
319
320
|
if (trimmedTitles.has(heading)) return section.text();
|
|
320
321
|
const count = section.query('blockquote').count();
|
|
321
322
|
if (count <= 10) return section.text();
|
|
322
|
-
const kept = mdq(section.text()).query('blockquote[10:]').replace('');
|
|
323
|
+
const kept = mdq(section.text()).query('blockquote[10:]').replace('').toString();
|
|
323
324
|
trimmedTitles.add(heading);
|
|
324
325
|
return `${kept.trimEnd()}\n> ... and ${count - 10} more discoveries\n`;
|
|
325
|
-
})
|
|
326
|
+
})
|
|
327
|
+
.toString();
|
|
326
328
|
}
|
|
327
329
|
|
|
328
330
|
return result.trim() || null;
|
|
@@ -404,7 +406,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
404
406
|
if (this.scout && this.docsWeight > 0) {
|
|
405
407
|
docsPromise = this.scout.collectDocs({ url: state.url, title: state.title, feature, excludeUrls: this.knowledgeTracker.applicationSpecUrls(state) });
|
|
406
408
|
}
|
|
407
|
-
let plannerResearch = mdq(research).query('code').replace('');
|
|
409
|
+
let plannerResearch = mdq(research).query('code').replace('').toString();
|
|
408
410
|
plannerResearch = mdq(plannerResearch)
|
|
409
411
|
.query('table')
|
|
410
412
|
.replaceEach((table) => {
|
|
@@ -415,7 +417,8 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
415
417
|
Type: r.Type || '',
|
|
416
418
|
}));
|
|
417
419
|
return jsonToTable(elementWithType, ['Element', 'Type']);
|
|
418
|
-
})
|
|
420
|
+
})
|
|
421
|
+
.toString();
|
|
419
422
|
|
|
420
423
|
const hasFocusedOverlay = hasFocusedSection(plannerResearch);
|
|
421
424
|
const focusNote = hasFocusedOverlay ? "IMPORTANT: One section is marked as **Focused** — this is the user's current focus area. Concentrate testing on the Focused section FIRST — test all interactions inside it before planning tests for the rest of the page." : '';
|
package/src/ai/provider.ts
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 type { ModelMessage } from 'ai';
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import { clearActivity, setActivity } from '../activity.ts';
|
|
@@ -60,11 +60,11 @@ function extractCachedTokens(usage: any): number {
|
|
|
60
60
|
return usage?.inputTokenDetails?.cacheReadTokens ?? 0;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
function abortAfterIdle(ms: number, cancel: { cancelled: boolean }, controller: AbortController): Promise<never> {
|
|
63
|
+
function abortAfterIdle(ms: number, cancel: { cancelled: boolean }, controller: AbortController, busy: { tools: number }): Promise<never> {
|
|
64
64
|
return new Promise((_, reject) => {
|
|
65
65
|
const tick = () => {
|
|
66
66
|
if (cancel.cancelled) return;
|
|
67
|
-
if (executionController.isAwaitingInput()) {
|
|
67
|
+
if (executionController.isAwaitingInput() || busy.tools > 0) {
|
|
68
68
|
setTimeout(tick, ms);
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
@@ -87,7 +87,7 @@ export class Provider {
|
|
|
87
87
|
private otelSdk: NodeSDK | null = null;
|
|
88
88
|
private defaultRetryOptions: RetryOptions = {
|
|
89
89
|
maxAttempts: 3,
|
|
90
|
-
baseDelay:
|
|
90
|
+
baseDelay: 1000,
|
|
91
91
|
maxDelay: 10000,
|
|
92
92
|
retryCondition: (error: Error) => {
|
|
93
93
|
return (
|
|
@@ -251,12 +251,12 @@ export class Provider {
|
|
|
251
251
|
});
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
private async raceWithIdleTimeout<T>(fn: (signal: AbortSignal) => Promise<T>, timeoutMs: number): Promise<T> {
|
|
254
|
+
private async raceWithIdleTimeout<T>(fn: (signal: AbortSignal) => Promise<T>, timeoutMs: number, busy: { tools: number } = { tools: 0 }): Promise<T> {
|
|
255
255
|
const cancel = { cancelled: false };
|
|
256
256
|
const controller = new AbortController();
|
|
257
257
|
const combinedSignal = combinedAbortSignal(controller);
|
|
258
258
|
try {
|
|
259
|
-
return await Promise.race([fn(combinedSignal), abortAfterIdle(timeoutMs, cancel, controller)]);
|
|
259
|
+
return await Promise.race([fn(combinedSignal), abortAfterIdle(timeoutMs, cancel, controller, busy)]);
|
|
260
260
|
} finally {
|
|
261
261
|
cancel.cancelled = true;
|
|
262
262
|
}
|
|
@@ -269,6 +269,24 @@ export class Provider {
|
|
|
269
269
|
return retry(reduced.messages, { ...options, _contextRetryLevel: reduced.nextLevel });
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
private async recoverWithPlainJson(messages: ModelMessage[], schema: any, model: any, options: any): Promise<any> {
|
|
273
|
+
const target = asSchema(schema);
|
|
274
|
+
tag('warning').log(`${getModelName(model)} returned no structured output, asking for plain JSON instead`);
|
|
275
|
+
const instruction = dedent`
|
|
276
|
+
Respond with a single JSON object that matches this JSON Schema, and nothing else:
|
|
277
|
+
${JSON.stringify(await target.jsonSchema)}
|
|
278
|
+
`;
|
|
279
|
+
const response = await this.chat([...messages, { role: 'user', content: instruction }], wrapLanguageModel({ model, middleware: extractJsonMiddleware() }), options);
|
|
280
|
+
|
|
281
|
+
const parsed = await parsePartialJson(response.text);
|
|
282
|
+
if (parsed.state !== 'successful-parse') throw new AiError('No object generated: plain JSON fallback returned no parsable JSON');
|
|
283
|
+
const validated = await target.validate?.(parsed.value);
|
|
284
|
+
if (validated && !validated.success) throw new AiError(`No object generated: plain JSON fallback did not match the schema: ${validated.error.message}`);
|
|
285
|
+
|
|
286
|
+
responseLog(parsed.value);
|
|
287
|
+
return { ...response, object: validated?.value ?? parsed.value };
|
|
288
|
+
}
|
|
289
|
+
|
|
272
290
|
private initLangfuse() {
|
|
273
291
|
const { enabled, publicKey, secretKey, baseUrl } = this.config.langfuse || {};
|
|
274
292
|
|
|
@@ -411,6 +429,8 @@ export class Provider {
|
|
|
411
429
|
|
|
412
430
|
async generateWithTools(messages: ModelMessage[], model: any, tools: any, options: any = {}): Promise<any> {
|
|
413
431
|
const modelName = getModelName(model);
|
|
432
|
+
const busy = { tools: 0 };
|
|
433
|
+
tools = withIdleExemption(tools, busy);
|
|
414
434
|
setActivity(`🤖 Asking ${modelName} with dynamic tools`, 'ai');
|
|
415
435
|
promptLog(`Using model: ${modelName}`);
|
|
416
436
|
|
|
@@ -437,7 +457,7 @@ export class Provider {
|
|
|
437
457
|
const onStepEnd = (step: any) => {
|
|
438
458
|
stepMessages.push(...(step.response?.messages || []));
|
|
439
459
|
};
|
|
440
|
-
const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages: attemptMessages, ...config, abortSignal: signal, onStepEnd }), config.timeout || 30000).catch((error) => {
|
|
460
|
+
const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages: attemptMessages, ...config, abortSignal: signal, onStepEnd }), config.timeout || 30000, busy).catch((error) => {
|
|
441
461
|
if (stepMessages.length > 0) {
|
|
442
462
|
tag('warning').log(`Keeping ${stepMessages.length} messages from tool steps that already ran before the failure`);
|
|
443
463
|
executedStepMessages.push(...stepMessages);
|
|
@@ -527,6 +547,7 @@ export class Provider {
|
|
|
527
547
|
if (Provider.isContextLengthError(error)) {
|
|
528
548
|
return this.recoverFromContextLength(error, messages, options, (m, o) => this.generateObject(m, schema, model, o));
|
|
529
549
|
}
|
|
550
|
+
if (NoObjectGeneratedError.isInstance(error)) return this.recoverWithPlainJson(messages, schema, modelToUse, options);
|
|
530
551
|
throw new AiError(error.message || error.toString());
|
|
531
552
|
}
|
|
532
553
|
}
|
|
@@ -769,6 +790,29 @@ function repairHarmonyChannel({ toolCall, tools }: ToolCallRepairOptions): any |
|
|
|
769
790
|
return { ...toolCall, toolName: NARRATION_TOOL, input };
|
|
770
791
|
}
|
|
771
792
|
|
|
793
|
+
function withIdleExemption(tools: any, busy: { tools: number }): any {
|
|
794
|
+
if (!tools) return tools;
|
|
795
|
+
const wrapped: any = {};
|
|
796
|
+
for (const [name, definition] of Object.entries<any>(tools)) {
|
|
797
|
+
if (typeof definition?.execute !== 'function') {
|
|
798
|
+
wrapped[name] = definition;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
wrapped[name] = {
|
|
802
|
+
...definition,
|
|
803
|
+
execute: async (...args: any[]) => {
|
|
804
|
+
busy.tools++;
|
|
805
|
+
try {
|
|
806
|
+
return await definition.execute(...args);
|
|
807
|
+
} finally {
|
|
808
|
+
busy.tools--;
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
return wrapped;
|
|
814
|
+
}
|
|
815
|
+
|
|
772
816
|
export { AiError, Provider as AIProvider };
|
|
773
817
|
|
|
774
818
|
type ToolCallRepairOptions = { toolCall: any; tools: any };
|
package/src/ai/rerunner.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { formatHeadings } from '../utils/context-formatter.ts';
|
|
|
18
18
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
19
19
|
import { loop } from '../utils/loop.ts';
|
|
20
20
|
import { RulesLoader } from '../utils/rules-loader.ts';
|
|
21
|
+
import { isInternalStep } from '../utils/step-analyzer.ts';
|
|
21
22
|
import type { Agent, AgentDeps } from './agent.ts';
|
|
22
23
|
import { toolExecutionLabel } from './conversation.ts';
|
|
23
24
|
import type { Navigator } from './navigator.ts';
|
|
@@ -85,6 +86,7 @@ export class Rerunner extends TaskAgent implements Agent {
|
|
|
85
86
|
|
|
86
87
|
const onStepStarted = (step: any) => {
|
|
87
88
|
if (!step.toCode) return;
|
|
89
|
+
if (isInternalStep(step)) return;
|
|
88
90
|
const code = highlight(step.toCode(), { language: 'javascript' });
|
|
89
91
|
console.log(chalk.dim(` ${code}`));
|
|
90
92
|
};
|
|
@@ -92,12 +94,14 @@ export class Rerunner extends TaskAgent implements Agent {
|
|
|
92
94
|
const onStepPassed = (step: any) => {
|
|
93
95
|
const task = this.getCurrentTask(testMap);
|
|
94
96
|
if (!task || !step.toCode) return;
|
|
97
|
+
if (isInternalStep(step)) return;
|
|
95
98
|
task.addStep(step.toCode(), step.duration, 'passed');
|
|
96
99
|
};
|
|
97
100
|
|
|
98
101
|
const onStepFailed = (step: any, error: any) => {
|
|
99
102
|
const task = this.getCurrentTask(testMap);
|
|
100
103
|
if (!task || !step.toCode) return;
|
|
104
|
+
if (isInternalStep(step)) return;
|
|
101
105
|
task.addStep(step.toCode(), step.duration, 'failed', error?.message);
|
|
102
106
|
console.log(chalk.red(` ${figureSet.cross} ${step.toCode()} — ${error?.message || 'failed'}`));
|
|
103
107
|
};
|
|
@@ -126,7 +126,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
126
126
|
let updated: string;
|
|
127
127
|
if (extQuery.count() > 0) {
|
|
128
128
|
const existing = extQuery.text().trimEnd();
|
|
129
|
-
updated = extQuery.replace(`${existing}\n\n${sectionMarkdown}\n`);
|
|
129
|
+
updated = extQuery.replace(`${existing}\n\n${sectionMarkdown}\n`).toString();
|
|
130
130
|
} else {
|
|
131
131
|
updated = `${cached.trimEnd()}\n\n# Extended Research\n\n${sectionMarkdown}\n`;
|
|
132
132
|
}
|
|
@@ -539,7 +539,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
539
539
|
if (heading.count() === 0) heading = mdq(sectionMarkdown).query('h2[0]');
|
|
540
540
|
if (heading.count() === 0) return sectionMarkdown;
|
|
541
541
|
|
|
542
|
-
return heading.replace(`${heading.text().trimEnd()}\n\n> Container: '${containerCss}'\n\n`);
|
|
542
|
+
return heading.replace(`${heading.text().trimEnd()}\n\n> Container: '${containerCss}'\n\n`).toString();
|
|
543
543
|
}
|
|
544
544
|
|
|
545
545
|
private _deduplicateExpandedSections(sections: string[]): string[] {
|
|
@@ -304,9 +304,9 @@ export function WithLocators<T extends Constructor>(Base: T) {
|
|
|
304
304
|
if (sectionQuery.count() === 0) sectionQuery = mdq(result.text).query(`section3(~"${escaped}")`);
|
|
305
305
|
|
|
306
306
|
if (newCss) {
|
|
307
|
-
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Container', `'${newCss}'`);
|
|
307
|
+
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Container', `'${newCss}'`).toString();
|
|
308
308
|
} else {
|
|
309
|
-
result.text = sectionQuery.query('blockquote[0]').replace('');
|
|
309
|
+
result.text = sectionQuery.query('blockquote[0]').replace('').toString();
|
|
310
310
|
result.text = result.text.replace(`${FOCUSED_MARKER}\n`, '');
|
|
311
311
|
}
|
|
312
312
|
|
|
@@ -58,7 +58,7 @@ export function WithPagination<T extends Constructor>(Base: T) {
|
|
|
58
58
|
let sectionQuery = mdq(result.text).query(`section2(~"${escaped}")`);
|
|
59
59
|
if (sectionQuery.count() === 0) sectionQuery = mdq(result.text).query(`section3(~"${escaped}")`);
|
|
60
60
|
if (sectionQuery.count() === 0) return;
|
|
61
|
-
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Pagination', strategy);
|
|
61
|
+
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Pagination', strategy).toString();
|
|
62
62
|
}
|
|
63
63
|
};
|
|
64
64
|
}
|
|
@@ -52,9 +52,9 @@ export class ResearchResult {
|
|
|
52
52
|
const escaped = section.name.replace(/"/g, '\\"');
|
|
53
53
|
let sectionQuery = mdq(this.text).query(`section2(~"${escaped}")`);
|
|
54
54
|
if (sectionQuery.count() === 0) sectionQuery = mdq(this.text).query(`section3(~"${escaped}")`);
|
|
55
|
-
const updated = sectionQuery.query('table').replace(`${newTable.trimEnd()}\n`);
|
|
55
|
+
const updated = sectionQuery.query('table').replace(`${newTable.trimEnd()}\n`).toString();
|
|
56
56
|
if (updated === this.text) return;
|
|
57
|
-
section.rawMarkdown = mdq(section.rawMarkdown).query('table').replace(`${newTable.trimEnd()}\n`);
|
|
57
|
+
section.rawMarkdown = mdq(section.rawMarkdown).query('table').replace(`${newTable.trimEnd()}\n`).toString();
|
|
58
58
|
this.text = updated;
|
|
59
59
|
}
|
|
60
60
|
|
package/src/ai/researcher.ts
CHANGED
|
@@ -313,7 +313,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
313
313
|
researchFile = saveResearch(researchState, result.text, combinedHtml);
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
const summaryText = mdq(result.text).query('section2(/^summary/)').query('paragraph[0]').text().trim();
|
|
316
|
+
const summaryText = mdq(result.text).query('section2(/^summary/i)').query('paragraph[0]').text().trim();
|
|
317
317
|
const summaryLine = summaryText.split('\n')[0]?.trim().slice(0, 200);
|
|
318
318
|
if (summaryLine) this.experienceTracker.updateSummary(this.actionResult!, summaryLine);
|
|
319
319
|
|
package/src/ai/task-agent.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { StateManager } from '../state-manager.ts';
|
|
|
8
8
|
import { HooksRunner } from '../utils/hooks-runner.ts';
|
|
9
9
|
import type { AgentDeps, ToolDeps } from './agent.ts';
|
|
10
10
|
import { Historian } from './historian.js';
|
|
11
|
+
import type { Judge } from './judge.ts';
|
|
11
12
|
import type { Navigator } from './navigator.js';
|
|
12
13
|
import type { Provider } from './provider.js';
|
|
13
14
|
import { Quartermaster } from './quartermaster.js';
|
|
@@ -29,6 +30,7 @@ export abstract class TaskAgent {
|
|
|
29
30
|
config!: ExplorbotConfig;
|
|
30
31
|
stateManager!: StateManager;
|
|
31
32
|
knowledgeTracker!: KnowledgeTracker;
|
|
33
|
+
protected judge?: Judge;
|
|
32
34
|
protected hooksRunner!: HooksRunner;
|
|
33
35
|
protected consecutiveFailures = 0;
|
|
34
36
|
protected consecutiveEmptyResults = 0;
|
|
@@ -45,6 +47,7 @@ export abstract class TaskAgent {
|
|
|
45
47
|
this.config = deps.config;
|
|
46
48
|
this.stateManager = deps.stateManager;
|
|
47
49
|
this.knowledgeTracker = deps.knowledgeTracker;
|
|
50
|
+
this.judge = deps.judge;
|
|
48
51
|
this.hooksRunner = new HooksRunner(deps.explorer, deps.config);
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -59,7 +62,7 @@ export abstract class TaskAgent {
|
|
|
59
62
|
protected abstract getNavigator(): Navigator;
|
|
60
63
|
|
|
61
64
|
protected get toolDeps(): ToolDeps {
|
|
62
|
-
return { explorer: this.explorer, stateManager: this.stateManager, ai: this.provider };
|
|
65
|
+
return { explorer: this.explorer, stateManager: this.stateManager, ai: this.provider, judge: this.judge };
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
protected getExperienceTracker(): ExperienceTracker {
|
package/src/ai/tester.ts
CHANGED
|
@@ -136,8 +136,8 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
136
136
|
let initialState = ActionResult.fromState(state);
|
|
137
137
|
const currentUrl = state.fullUrl || state.url;
|
|
138
138
|
let startOnCurrentPage = opts.startOnCurrentPage;
|
|
139
|
-
if (
|
|
140
|
-
debugLog(`
|
|
139
|
+
if (!startOnCurrentPage && task.startUrl && normalizeUrl(currentUrl) !== normalizeUrl(task.startUrl)) {
|
|
140
|
+
debugLog(`Opening test start URL ${task.startUrl} before building context (was at ${currentUrl})`);
|
|
141
141
|
try {
|
|
142
142
|
await this.explorer.visit(task.startUrl);
|
|
143
143
|
state = this.stateManager.getCurrentState();
|
|
@@ -145,7 +145,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
145
145
|
initialState = ActionResult.fromState(state);
|
|
146
146
|
startOnCurrentPage = true;
|
|
147
147
|
} catch (error) {
|
|
148
|
-
debugLog(`Could not
|
|
148
|
+
debugLog(`Could not open test start URL: ${compactErrorMessage(error)}`);
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
if (isErrorPage(initialState)) {
|
|
@@ -159,14 +159,6 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
159
159
|
conversation.markLastMessageCacheable();
|
|
160
160
|
this.currentConversation = conversation;
|
|
161
161
|
|
|
162
|
-
const scenarioBlock = this.buildScenarioBlock(task, initialState);
|
|
163
|
-
conversation.addUserText(scenarioBlock);
|
|
164
|
-
conversation.markLastMessageCacheable();
|
|
165
|
-
conversation.protectPrefix(conversation.messages.length);
|
|
166
|
-
|
|
167
|
-
const pageContext = await this.reinjectContextIfNeeded(1, initialState);
|
|
168
|
-
if (pageContext) conversation.addUserText(pageContext);
|
|
169
|
-
|
|
170
162
|
return await Observability.run(
|
|
171
163
|
`test: ${task.scenario}`,
|
|
172
164
|
{
|
|
@@ -185,16 +177,15 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
185
177
|
private async runTestSession(task: Test, initialState: ActionResult, conversation: Conversation, handlers: TestSessionHandlers, opts: TestOptions): Promise<{ success: boolean }> {
|
|
186
178
|
const { offFailedRequest } = handlers;
|
|
187
179
|
|
|
180
|
+
let plan = '';
|
|
188
181
|
if (this.pilot) {
|
|
189
182
|
try {
|
|
190
|
-
|
|
183
|
+
await this.researcher.research(initialState).catch(this.skipResearch);
|
|
184
|
+
plan = await this.pilot.planTest(task, initialState);
|
|
191
185
|
if (task.hasFinished) {
|
|
192
186
|
offFailedRequest?.();
|
|
193
187
|
return { success: task.isSuccessful };
|
|
194
188
|
}
|
|
195
|
-
if (plan) {
|
|
196
|
-
conversation.addUserText(`Pilot's test plan:\n${plan}\n\nFollow this plan while executing the test.`);
|
|
197
|
-
}
|
|
198
189
|
} catch (err) {
|
|
199
190
|
const message = err instanceof Error ? err.message : String(err);
|
|
200
191
|
tag('error').log(`Pilot planning failed: ${message}`);
|
|
@@ -216,7 +207,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
216
207
|
|
|
217
208
|
if (opts.startOnCurrentPage) debugLog(`Starting on the page already open at ${task.startUrl}`);
|
|
218
209
|
|
|
219
|
-
if (!opts.startOnCurrentPage) {
|
|
210
|
+
if (!opts.startOnCurrentPage || task.preparedData.length > 0) {
|
|
220
211
|
debugLog(`Navigating to ${task.startUrl}`);
|
|
221
212
|
try {
|
|
222
213
|
await this.explorer.visit(task.startUrl!);
|
|
@@ -242,6 +233,15 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
242
233
|
const currentUrl = startState?.url || task.startUrl || '';
|
|
243
234
|
await this.hooksRunner.runBeforeHook('tester', currentUrl);
|
|
244
235
|
|
|
236
|
+
const testerState = this.getCurrentState();
|
|
237
|
+
conversation.addUserText(this.buildScenarioBlock(task, testerState));
|
|
238
|
+
conversation.markLastMessageCacheable();
|
|
239
|
+
conversation.protectPrefix(conversation.messages.length);
|
|
240
|
+
|
|
241
|
+
const pageContext = await this.reinjectContextIfNeeded(1, testerState);
|
|
242
|
+
if (pageContext) conversation.addUserText(pageContext);
|
|
243
|
+
if (plan) conversation.addUserText(`Pilot's test plan:\n${plan}\n\nFollow this plan while executing the test.`);
|
|
244
|
+
|
|
245
245
|
const offStateChange = this.stateManager.onStateChange((event: StateTransition) => {
|
|
246
246
|
if (task.hasFinished) return;
|
|
247
247
|
if (event.toState?.url === event.fromState?.url) return;
|
|
@@ -912,6 +912,9 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
912
912
|
}
|
|
913
913
|
|
|
914
914
|
private buildDeletionScope(task: Test): string {
|
|
915
|
+
if (task.preparedData.length > 0) {
|
|
916
|
+
return `When deleting items, ONLY delete the data prepared for this test: ${task.preparedData.join('; ')}.`;
|
|
917
|
+
}
|
|
915
918
|
const deletableItems = this.getDeletableSessionNames(task);
|
|
916
919
|
if (deletableItems.length > 0) {
|
|
917
920
|
return `When deleting items, ONLY delete items whose title contains one of these session names: ${deletableItems.join(', ')}. These were created by previous tests.`;
|
package/src/ai/tools.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { pause } from '../utils/loop.js';
|
|
|
15
15
|
import { compactErrorMessage, normalizeInlineText, truncate } from '../utils/strings.ts';
|
|
16
16
|
import { WebElement } from '../utils/web-element.ts';
|
|
17
17
|
import type { ToolDeps } from './agent.ts';
|
|
18
|
+
import { createJudgeTool } from './judge-tool.ts';
|
|
19
|
+
import { JUDGE_PAGE_CAP, type Judge, UNDECIDED } from './judge.ts';
|
|
18
20
|
import { Navigator } from './navigator.ts';
|
|
19
21
|
import { Researcher } from './researcher.ts';
|
|
20
22
|
import { sectionContextRule } from './rules.ts';
|
|
@@ -31,7 +33,7 @@ interface AgentToolDeps extends ToolDeps {
|
|
|
31
33
|
|
|
32
34
|
export const ASSERTION_TOOLS = ['verify'] as const;
|
|
33
35
|
|
|
34
|
-
export function createCodeceptJSTools({ explorer, stateManager }: ToolDeps, task: Task) {
|
|
36
|
+
export function createCodeceptJSTools({ explorer, stateManager, judge }: ToolDeps, task: Task) {
|
|
35
37
|
return {
|
|
36
38
|
click: tool({
|
|
37
39
|
description: dedent`
|
|
@@ -156,7 +158,9 @@ export function createCodeceptJSTools({ explorer, stateManager }: ToolDeps, task
|
|
|
156
158
|
attempts,
|
|
157
159
|
suggestion,
|
|
158
160
|
},
|
|
159
|
-
ambiguityError || action.lastError
|
|
161
|
+
ambiguityError || action.lastError,
|
|
162
|
+
judge,
|
|
163
|
+
explanation
|
|
160
164
|
);
|
|
161
165
|
},
|
|
162
166
|
}),
|
|
@@ -232,7 +236,9 @@ export function createCodeceptJSTools({ explorer, stateManager }: ToolDeps, task
|
|
|
232
236
|
attempts,
|
|
233
237
|
suggestion: 'Use xpathCheck() to locate the row/card/tree node, or visualClick() if the hover target is only visually identifiable.',
|
|
234
238
|
},
|
|
235
|
-
action.lastError
|
|
239
|
+
action.lastError,
|
|
240
|
+
judge,
|
|
241
|
+
explanation
|
|
236
242
|
);
|
|
237
243
|
},
|
|
238
244
|
}),
|
|
@@ -375,7 +381,7 @@ export function createCodeceptJSTools({ explorer, stateManager }: ToolDeps, task
|
|
|
375
381
|
- Performing multiple form actions in a single batch
|
|
376
382
|
- Complex interactions requiring sequential commands
|
|
377
383
|
- Reaching items further down a list (I.scrollTo)
|
|
378
|
-
- Reloading the page to prove a change outlived it (I.
|
|
384
|
+
- Reloading the page to prove a change outlived it (I.refreshPage)
|
|
379
385
|
|
|
380
386
|
Example - filling a form with context (PREFERRED):
|
|
381
387
|
I.fillField('Username', 'John', '.login-form')
|
|
@@ -437,7 +443,9 @@ export function createCodeceptJSTools({ explorer, stateManager }: ToolDeps, task
|
|
|
437
443
|
attempts: action.executedSteps,
|
|
438
444
|
suggestion: formSuggestion,
|
|
439
445
|
},
|
|
440
|
-
action.lastError
|
|
446
|
+
action.lastError,
|
|
447
|
+
judge,
|
|
448
|
+
explanation
|
|
441
449
|
);
|
|
442
450
|
}
|
|
443
451
|
|
|
@@ -587,7 +595,7 @@ export function createLearnExperienceTool({ getExperienceTracker, getState }: {
|
|
|
587
595
|
});
|
|
588
596
|
}
|
|
589
597
|
|
|
590
|
-
export function createAgentTools({ explorer, stateManager, ai, researcher, navigator, supervisor, withExperience }: AgentToolDeps): any {
|
|
598
|
+
export function createAgentTools({ explorer, stateManager, ai, judge, researcher, navigator, supervisor, withExperience }: AgentToolDeps): any {
|
|
591
599
|
const tools: Record<string, any> = {
|
|
592
600
|
see: tool({
|
|
593
601
|
description: dedent`
|
|
@@ -724,6 +732,13 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
724
732
|
}
|
|
725
733
|
|
|
726
734
|
if (result.inexpressible) {
|
|
735
|
+
if (result.judged?.approved) {
|
|
736
|
+
return failedToolResult('verify', `No assertion could express this claim, but the page appears to confirm it: ${assertion}`, {
|
|
737
|
+
inexpressible: true,
|
|
738
|
+
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.',
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
727
742
|
return failedToolResult('verify', `No assertion could express this claim: ${assertion}`, {
|
|
728
743
|
inexpressible: true,
|
|
729
744
|
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().',
|
|
@@ -1143,6 +1158,20 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
1143
1158
|
});
|
|
1144
1159
|
}
|
|
1145
1160
|
|
|
1161
|
+
const buildJudgeState = async () => {
|
|
1162
|
+
const activeTest = explorer.activeTest;
|
|
1163
|
+
const state = stateManager.getCurrentState();
|
|
1164
|
+
const result = state ? ActionResult.fromState(state) : null;
|
|
1165
|
+
return {
|
|
1166
|
+
task: activeTest?.scenario || '',
|
|
1167
|
+
page: cap(result?.getCompactARIA(), JUDGE_PAGE_CAP),
|
|
1168
|
+
recentActions: Object.values(activeTest?.steps || {})
|
|
1169
|
+
.slice(-JUDGE_RECENT_ACTIONS_LIMIT)
|
|
1170
|
+
.map((step) => step.text),
|
|
1171
|
+
};
|
|
1172
|
+
};
|
|
1173
|
+
Object.assign(tools, createJudgeTool({ explorer, stateManager, ai, judge }, buildJudgeState));
|
|
1174
|
+
|
|
1146
1175
|
withdrawVisionTools(tools);
|
|
1147
1176
|
|
|
1148
1177
|
return tools;
|
|
@@ -1158,6 +1187,7 @@ const NAVIGATED_SUGGESTION = 'The action left the page. Elements are never compa
|
|
|
1158
1187
|
const ARIA_OUTPUT_CAP = 4000;
|
|
1159
1188
|
const HTML_OUTPUT_CAP = 6000;
|
|
1160
1189
|
const ANALYSIS_OUTPUT_CAP = 2000;
|
|
1190
|
+
const JUDGE_RECENT_ACTIONS_LIMIT = 8;
|
|
1161
1191
|
|
|
1162
1192
|
function cap(text: string | undefined | null, max: number): string {
|
|
1163
1193
|
if (!text) return '';
|
|
@@ -1267,7 +1297,7 @@ function hasObservablePageChange(data?: Record<string, any>): boolean {
|
|
|
1267
1297
|
return Array.isArray(data.pageDiff.htmlParts) && data.pageDiff.htmlParts.length > 0;
|
|
1268
1298
|
}
|
|
1269
1299
|
|
|
1270
|
-
export async function failedToolResult(action: string, message: string, data?: Record<string, any>, error?: Error | null) {
|
|
1300
|
+
export async function failedToolResult(action: string, message: string, data?: Record<string, any>, error?: Error | null, judge?: Judge, intent?: string) {
|
|
1271
1301
|
const result: Record<string, any> = { success: false, action, message, ...data };
|
|
1272
1302
|
if (data?.pageDiff) {
|
|
1273
1303
|
result.suggestion = data.suggestion ? `${data.suggestion} ${PAGE_DIFF_SUGGESTION}` : PAGE_DIFF_SUGGESTION;
|
|
@@ -1279,6 +1309,11 @@ export async function failedToolResult(action: string, message: string, data?: R
|
|
|
1279
1309
|
result.suggestion = getMultipleElementsSuggestion();
|
|
1280
1310
|
result.multipleElementsDetected = true;
|
|
1281
1311
|
result.elements = formatElementList(matched);
|
|
1312
|
+
const labels = (matched || []).map((element) => `${element.text || 'no text'} — ${element.html}`);
|
|
1313
|
+
const pick = await judge?.decide('Which listed element does the intent name?', [...labels, UNDECIDED], { intent });
|
|
1314
|
+
if (!pick?.value) return result;
|
|
1315
|
+
const index = labels.indexOf(pick.value) + 1;
|
|
1316
|
+
result.suggestion = `Element ${index} is the one meant. Repeat the action with step.opts({ elementIndex: ${index} }) as the last argument.`;
|
|
1282
1317
|
return result;
|
|
1283
1318
|
}
|
|
1284
1319
|
|
|
@@ -199,7 +199,8 @@ export class RequestResult {
|
|
|
199
199
|
if (Array.isArray(data)) return {};
|
|
200
200
|
|
|
201
201
|
const id = data.id ?? data._id ?? data.uuid;
|
|
202
|
-
const
|
|
202
|
+
const attributes = data.attributes || {};
|
|
203
|
+
const title = data.name ?? data.title ?? data.label ?? attributes.name ?? attributes.title ?? attributes.label;
|
|
203
204
|
|
|
204
205
|
const result: { id?: string | number; title?: string } = {};
|
|
205
206
|
if (id !== undefined) result.id = id;
|
package/src/api/xhr-capture.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSameHostFamily } from '../utils/url-matcher.js';
|
|
1
2
|
import { RequestResult, generateRequestId } from './request-result.ts';
|
|
2
3
|
import type { RequestStore } from './request-store.ts';
|
|
3
4
|
|
|
@@ -39,7 +40,7 @@ export class XhrCapture {
|
|
|
39
40
|
|
|
40
41
|
const method = request.method();
|
|
41
42
|
const url = request.url();
|
|
42
|
-
if (!url
|
|
43
|
+
if (!isSameHostFamily(url, this.baseOrigin)) return;
|
|
43
44
|
|
|
44
45
|
const status = response.status();
|
|
45
46
|
|
package/src/command-handler.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface CommandAutocomplete {
|
|
|
31
31
|
replaceTo: number;
|
|
32
32
|
visible: boolean;
|
|
33
33
|
argumentHint?: string;
|
|
34
|
+
completesArgument?: boolean;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function parseCommand(input: string): ParsedCommand | null {
|
|
@@ -266,6 +267,31 @@ export class CommandHandler implements InputManager {
|
|
|
266
267
|
const parsed = parseCommand(query);
|
|
267
268
|
const exactCommand = parsed ? this.findCommand(parsed.name) : undefined;
|
|
268
269
|
const hasArguments = commandEnd !== -1 && input.slice(commandEnd + 1).trim().length > 0;
|
|
270
|
+
const argumentHint = !insideCommand && exactCommand && !hasArguments && exactCommand.options.length > 0 ? exactCommand.options.map((option) => option.flags).join(' ') : undefined;
|
|
271
|
+
|
|
272
|
+
if (!insideCommand && exactCommand) {
|
|
273
|
+
const argumentQuery = input.slice(commandEnd + 1).trim();
|
|
274
|
+
const argumentEntries = exactCommand
|
|
275
|
+
.completeArguments()
|
|
276
|
+
.filter((completion) => completion.value !== argumentQuery)
|
|
277
|
+
.map((completion) => ({
|
|
278
|
+
aliases: [],
|
|
279
|
+
canonical: completion.value,
|
|
280
|
+
description: '',
|
|
281
|
+
value: completion.display || completion.value,
|
|
282
|
+
}));
|
|
283
|
+
const argumentSuggestions = this.rankSuggestions(argumentQuery, argumentEntries);
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
suggestions: argumentSuggestions,
|
|
287
|
+
replaceFrom: commandEnd + 1,
|
|
288
|
+
replaceTo: input.length,
|
|
289
|
+
visible: argumentSuggestions.length > 0,
|
|
290
|
+
argumentHint,
|
|
291
|
+
completesArgument: true,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
269
295
|
const commandEntries = this.getSlashCommandEntries();
|
|
270
296
|
let suggestions: CommandAutocompleteSuggestion[] = [];
|
|
271
297
|
if (insideCommand) {
|
|
@@ -280,10 +306,9 @@ export class CommandHandler implements InputManager {
|
|
|
280
306
|
suggestions = this.rankSuggestions(query, commandEntries);
|
|
281
307
|
}
|
|
282
308
|
}
|
|
283
|
-
const argumentHint = !insideCommand && exactCommand && !hasArguments && exactCommand.options.length > 0 ? exactCommand.options.map((option) => option.flags).join(' ') : undefined;
|
|
284
309
|
|
|
285
310
|
return {
|
|
286
|
-
suggestions,
|
|
311
|
+
suggestions: suggestions.map((suggestion) => ({ ...suggestion, value: `${suggestion.value} ` })),
|
|
287
312
|
replaceFrom: 0,
|
|
288
313
|
replaceTo,
|
|
289
314
|
visible: insideCommand && suggestions.length > 0,
|
|
@@ -378,7 +403,7 @@ export class CommandHandler implements InputManager {
|
|
|
378
403
|
private rankSuggestions(query: string, entries: AutocompleteEntry[]): CommandAutocompleteSuggestion[] {
|
|
379
404
|
if (!query) {
|
|
380
405
|
return entries.slice(0, 20).map((entry) => ({
|
|
381
|
-
value: entry.
|
|
406
|
+
value: entry.canonical,
|
|
382
407
|
display: entry.value,
|
|
383
408
|
description: entry.description,
|
|
384
409
|
argumentHint: entry.argumentHint,
|
|
@@ -35,6 +35,10 @@ export abstract class BaseCommand<T = ExplorBot> {
|
|
|
35
35
|
return this.name === commandName || this.aliases.includes(commandName);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
completeArguments(): ArgumentCompletion[] {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
printSuggestions(): void {
|
|
39
43
|
if (this.suggestions.length === 0) return;
|
|
40
44
|
const prefix = isInteractive() ? '/' : `${getCliName()} `;
|
|
@@ -63,3 +67,8 @@ export abstract class BaseCommand<T = ExplorBot> {
|
|
|
63
67
|
return { opts: cmd.opts(), args: cmd.args };
|
|
64
68
|
}
|
|
65
69
|
}
|
|
70
|
+
|
|
71
|
+
export interface ArgumentCompletion {
|
|
72
|
+
value: string;
|
|
73
|
+
display?: string;
|
|
74
|
+
}
|