explorbot 0.4.5 → 0.4.7
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/boat/api-tester/src/ai/curler.ts +70 -66
- package/boat/api-tester/src/apibot.ts +19 -2
- package/boat/api-tester/src/cli.ts +87 -274
- package/boat/api-tester/src/commands/api-command.ts +10 -0
- package/boat/api-tester/src/commands/explore-command.ts +52 -0
- package/boat/api-tester/src/commands/init-command.ts +119 -0
- package/boat/api-tester/src/commands/know-command.ts +44 -0
- package/boat/api-tester/src/commands/plan-command.ts +42 -0
- package/boat/api-tester/src/commands/test-command.ts +54 -0
- package/boat/api-tester/src/config.ts +18 -1
- package/dist/boat/api-tester/src/ai/curler.js +55 -56
- package/dist/boat/api-tester/src/apibot.js +15 -1
- package/dist/boat/api-tester/src/cli.js +89 -243
- package/dist/boat/api-tester/src/commands/api-command.js +7 -0
- package/dist/boat/api-tester/src/commands/explore-command.js +41 -0
- package/dist/boat/api-tester/src/commands/init-command.js +88 -0
- package/dist/boat/api-tester/src/commands/know-command.js +39 -0
- package/dist/boat/api-tester/src/commands/plan-command.js +37 -0
- package/dist/boat/api-tester/src/commands/test-command.js +45 -0
- package/dist/boat/api-tester/src/config.js +3 -1
- package/dist/package.json +4 -4
- package/dist/rules/researcher/pagination.md +6 -0
- package/dist/src/action-result.d.ts +6 -0
- package/dist/src/action-result.js +12 -0
- package/dist/src/ai/planner.js +4 -0
- package/dist/src/ai/researcher/deep-analysis.d.ts +1 -1
- package/dist/src/ai/researcher/deep-analysis.js +14 -6
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/pagination.d.ts +16 -0
- package/dist/src/ai/researcher/pagination.js +62 -0
- package/dist/src/ai/researcher/parser.d.ts +3 -0
- package/dist/src/ai/researcher/parser.js +22 -6
- package/dist/src/ai/researcher/sections.js +1 -1
- package/dist/src/ai/researcher.js +7 -2
- package/dist/src/ai/rules.js +16 -0
- package/dist/src/ai/scout.js +8 -2
- package/dist/src/ai/tools.d.ts +1 -1
- package/dist/src/ai/tools.js +25 -13
- package/dist/src/api/spec-reader.d.ts +1 -0
- package/dist/src/api/spec-reader.js +93 -1
- package/dist/src/commands/base-command.d.ts +3 -3
- package/dist/src/commands/init-command.d.ts +3 -0
- package/dist/src/commands/init-command.js +6 -3
- package/dist/src/commands/options/ws-option.d.ts +7 -0
- package/dist/src/commands/options/ws-option.js +14 -0
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +14 -11
- package/dist/src/explorer.d.ts +1 -1
- package/dist/src/explorer.js +1 -1
- package/dist/src/remote.d.ts +2 -0
- package/dist/src/remote.js +23 -16
- package/dist/src/utils/aria.d.ts +2 -0
- package/dist/src/utils/aria.js +6 -1
- package/dist/src/utils/html-diff.js +4 -1
- package/dist/src/utils/markdown-query.d.ts +2 -0
- package/dist/src/utils/markdown-query.js +39 -0
- package/dist/src/utils/pagination.d.ts +16 -0
- package/dist/src/utils/pagination.js +20 -0
- package/docs/api-testing/basics.md +26 -2
- package/docs/superpowers/plans/2026-09-10-pagination.md +1420 -0
- package/docs/superpowers/specs/2026-09-09-pagination-rule-design.md +345 -0
- package/package.json +4 -4
- package/rules/researcher/pagination.md +6 -0
- package/src/action-result.ts +16 -0
- package/src/ai/planner.ts +4 -0
- package/src/ai/researcher/deep-analysis.ts +13 -6
- package/src/ai/researcher/locators.ts +1 -1
- package/src/ai/researcher/pagination.ts +68 -0
- package/src/ai/researcher/parser.ts +23 -5
- package/src/ai/researcher/sections.ts +1 -1
- package/src/ai/researcher.ts +9 -3
- package/src/ai/rules.ts +16 -0
- package/src/ai/scout.ts +9 -2
- package/src/ai/tools.ts +22 -14
- package/src/api/spec-reader.ts +106 -1
- package/src/commands/base-command.ts +3 -3
- package/src/commands/init-command.ts +6 -3
- package/src/commands/options/ws-option.ts +14 -0
- package/src/config.ts +15 -11
- package/src/explorer.ts +1 -1
- package/src/remote.ts +22 -15
- package/src/utils/aria.ts +8 -1
- package/src/utils/html-diff.ts +3 -1
- package/src/utils/markdown-query.ts +39 -0
- package/src/utils/pagination.ts +36 -0
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"bash-tool": "^1.3.15",
|
|
102
102
|
"chalk": "^5.6.2",
|
|
103
103
|
"cli-highlight": "^2.1.11",
|
|
104
|
-
"codeceptjs": "4.
|
|
104
|
+
"codeceptjs": "^4.2.0-beta.3",
|
|
105
105
|
"commander": "^14.0.1",
|
|
106
106
|
"debug": "^4.4.3",
|
|
107
107
|
"dedent": "^1.6.0",
|
|
@@ -123,8 +123,8 @@
|
|
|
123
123
|
"ora-classic": "^5.4.2",
|
|
124
124
|
"parse5": "^8.0.0",
|
|
125
125
|
"pixelmatch": "^7.2.0",
|
|
126
|
-
"playwright": "^1.
|
|
127
|
-
"playwright-core": "^1.
|
|
126
|
+
"playwright": "^1.63",
|
|
127
|
+
"playwright-core": "^1.63",
|
|
128
128
|
"pngjs": "^7.0.0",
|
|
129
129
|
"react": "^19.1.1",
|
|
130
130
|
"sambanova-ai-provider": "^1.2.2",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<pagination>
|
|
2
|
+
When a section is a list that continues beyond what is shown, add one line under its `> Container:` line:
|
|
3
|
+
`> Pagination: controls` — it has page numbers (1, 2, 3), prev/next arrows, or a "load more" button.
|
|
4
|
+
`> Pagination: infinite` — it has none of those and loads more as it is scrolled.
|
|
5
|
+
Sorting, filtering and switching tabs are not pagination — omit the line then.
|
|
6
|
+
</pagination>
|
|
@@ -37,6 +37,8 @@ export interface PageDiff {
|
|
|
37
37
|
currentUrl: string;
|
|
38
38
|
ariaChanges?: string | null;
|
|
39
39
|
ariaChangeCount?: number;
|
|
40
|
+
ariaAdded?: number;
|
|
41
|
+
ariaRemoved?: number;
|
|
40
42
|
messages?: string[];
|
|
41
43
|
requests?: NetworkCall[];
|
|
42
44
|
consoleErrors?: string[];
|
|
@@ -137,6 +139,8 @@ export declare class Diff {
|
|
|
137
139
|
_messages: string[];
|
|
138
140
|
_ariaDiffResult: string | null;
|
|
139
141
|
_ariaChangeCount: number;
|
|
142
|
+
_ariaAdded: number;
|
|
143
|
+
_ariaRemoved: number;
|
|
140
144
|
_isSameUrl: boolean;
|
|
141
145
|
constructor(current: ActionResult, previous: ActionResult | null);
|
|
142
146
|
static create(current: ActionResult, previous: ActionResult | null): Promise<Diff>;
|
|
@@ -147,6 +151,8 @@ export declare class Diff {
|
|
|
147
151
|
cleanedHtmlParts(): Promise<HtmlDiffPart[]>;
|
|
148
152
|
get ariaChanged(): string | null;
|
|
149
153
|
get ariaChangeCount(): number;
|
|
154
|
+
get ariaAdded(): number;
|
|
155
|
+
get ariaRemoved(): number;
|
|
150
156
|
get htmlDiff(): HtmlDiffResult | null;
|
|
151
157
|
get messages(): string[];
|
|
152
158
|
get similarity(): number;
|
|
@@ -451,6 +451,8 @@ export class ActionResult {
|
|
|
451
451
|
if (diff.ariaChanged) {
|
|
452
452
|
pageDiff.ariaChanges = diff.ariaChanged;
|
|
453
453
|
pageDiff.ariaChangeCount = diff.ariaChangeCount;
|
|
454
|
+
pageDiff.ariaAdded = diff.ariaAdded;
|
|
455
|
+
pageDiff.ariaRemoved = diff.ariaRemoved;
|
|
454
456
|
}
|
|
455
457
|
if (this.overlay.isOpen && (!previousState.overlay.isOpen || previousState.overlay.name !== this.overlay.name)) {
|
|
456
458
|
pageDiff.areaOfInterest = this.overlay.describe();
|
|
@@ -542,6 +544,8 @@ export class Diff {
|
|
|
542
544
|
_messages = [];
|
|
543
545
|
_ariaDiffResult = null;
|
|
544
546
|
_ariaChangeCount = 0;
|
|
547
|
+
_ariaAdded = 0;
|
|
548
|
+
_ariaRemoved = 0;
|
|
545
549
|
_isSameUrl;
|
|
546
550
|
constructor(current, previous) {
|
|
547
551
|
this.current = current;
|
|
@@ -590,6 +594,12 @@ export class Diff {
|
|
|
590
594
|
get ariaChangeCount() {
|
|
591
595
|
return this._ariaChangeCount;
|
|
592
596
|
}
|
|
597
|
+
get ariaAdded() {
|
|
598
|
+
return this._ariaAdded;
|
|
599
|
+
}
|
|
600
|
+
get ariaRemoved() {
|
|
601
|
+
return this._ariaRemoved;
|
|
602
|
+
}
|
|
593
603
|
get htmlDiff() {
|
|
594
604
|
return this._htmlDiffResult;
|
|
595
605
|
}
|
|
@@ -614,5 +624,7 @@ export class Diff {
|
|
|
614
624
|
const ariaDiff = diffAriaSnapshots(this.previous.ariaSnapshot, this.current.ariaSnapshot);
|
|
615
625
|
this._ariaDiffResult = ariaDiff.text;
|
|
616
626
|
this._ariaChangeCount = ariaDiff.count;
|
|
627
|
+
this._ariaAdded = ariaDiff.added;
|
|
628
|
+
this._ariaRemoved = ariaDiff.removed;
|
|
617
629
|
}
|
|
618
630
|
}
|
package/dist/src/ai/planner.js
CHANGED
|
@@ -404,12 +404,16 @@ export class Planner extends PlannerBase {
|
|
|
404
404
|
`);
|
|
405
405
|
}
|
|
406
406
|
}
|
|
407
|
+
let activeRegion = '';
|
|
408
|
+
if (state.overlay.isOpen)
|
|
409
|
+
activeRegion = `Active region: ${state.overlay.describe()} — the user's current focus area. Plan tests for the controls inside it first.`;
|
|
407
410
|
conversation.addUserText(dedent `
|
|
408
411
|
${this.buildApproach(style)}
|
|
409
412
|
|
|
410
413
|
<context>
|
|
411
414
|
URL: ${state.url || 'Unknown'}
|
|
412
415
|
Title: ${state.title || 'Unknown'}
|
|
416
|
+
${activeRegion}
|
|
413
417
|
</context>
|
|
414
418
|
`);
|
|
415
419
|
if (this.fisherman) {
|
|
@@ -38,7 +38,7 @@ export declare function WithDeepAnalysis<T extends Constructor>(Base: T): {
|
|
|
38
38
|
}>): Promise<void>;
|
|
39
39
|
_executeAndAnalyze(commands: string[], description: string, state: WebPageState, originalAria: string, alreadyExpanded: string[]): Promise<ExpansionOutcome>;
|
|
40
40
|
_restorePageState(url: string, originalAria: string): Promise<void>;
|
|
41
|
-
_analyzeExpandedAction(code: string, description: string, diff: Diff, alreadyExpanded: string[]): Promise<string | null>;
|
|
41
|
+
_analyzeExpandedAction(code: string, description: string, diff: Diff, alreadyExpanded: string[], containerCss?: string | null): Promise<string | null>;
|
|
42
42
|
_deduplicateExpandedSections(sections: string[]): string[];
|
|
43
43
|
_summarizeExpanded(expandedSections: string[]): string[];
|
|
44
44
|
_sectionFingerprint(sectionMarkdown: string): string | null;
|
|
@@ -85,7 +85,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
85
85
|
.filter((s) => s.elements.length > 0)
|
|
86
86
|
.map((s) => s.rawMarkdown));
|
|
87
87
|
tag('substep').log(`Researching overlay: ${region.name}`);
|
|
88
|
-
const sectionMarkdown = await this._analyzeExpandedAction('', region.name, diff, alreadyExpanded);
|
|
88
|
+
const sectionMarkdown = await this._analyzeExpandedAction('', region.name, diff, alreadyExpanded, region.root);
|
|
89
89
|
if (!sectionMarkdown) {
|
|
90
90
|
debugLog(`Overlay "${region.name}" produced no meaningful expansion`);
|
|
91
91
|
return null;
|
|
@@ -367,9 +367,9 @@ export function WithDeepAnalysis(Base) {
|
|
|
367
367
|
}
|
|
368
368
|
await new Promise((r) => setTimeout(r, 500));
|
|
369
369
|
let diff;
|
|
370
|
+
let currAR;
|
|
370
371
|
try {
|
|
371
|
-
await this.explorer.capture();
|
|
372
|
-
const currAR = ActionResult.fromState(this.stateManager.getCurrentState());
|
|
372
|
+
currAR = await this.explorer.capture();
|
|
373
373
|
diff = await currAR.diff(previousState);
|
|
374
374
|
}
|
|
375
375
|
catch (err) {
|
|
@@ -388,7 +388,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
388
388
|
debugLog(`No changes from: ${description.slice(0, 80)}`);
|
|
389
389
|
return { status: 'none', code: clickCode };
|
|
390
390
|
}
|
|
391
|
-
const sectionMarkdown = await this._analyzeExpandedAction(clickCode, description, diff, alreadyExpanded);
|
|
391
|
+
const sectionMarkdown = await this._analyzeExpandedAction(clickCode, description, diff, alreadyExpanded, currAR.overlay.root);
|
|
392
392
|
await this._restorePageState(state.url, originalAria);
|
|
393
393
|
if (!sectionMarkdown)
|
|
394
394
|
return { status: 'none', code: clickCode };
|
|
@@ -413,7 +413,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
413
413
|
tag('warning').log(`navigateTo failed during restore: ${err instanceof Error ? err.message : err}`);
|
|
414
414
|
}
|
|
415
415
|
}
|
|
416
|
-
async _analyzeExpandedAction(code, description, diff, alreadyExpanded) {
|
|
416
|
+
async _analyzeExpandedAction(code, description, diff, alreadyExpanded, containerCss = null) {
|
|
417
417
|
const alreadyHint = alreadyExpanded.length > 0 ? `\nAlready expanded sections:\n${alreadyExpanded.join('\n')}` : '';
|
|
418
418
|
let intro;
|
|
419
419
|
if (code) {
|
|
@@ -473,7 +473,15 @@ export function WithDeepAnalysis(Base) {
|
|
|
473
473
|
const sections = parseResearchSections(text);
|
|
474
474
|
if (sections.length === 0)
|
|
475
475
|
return null;
|
|
476
|
-
|
|
476
|
+
const sectionMarkdown = sections[0].rawMarkdown;
|
|
477
|
+
if (!containerCss)
|
|
478
|
+
return sectionMarkdown;
|
|
479
|
+
let heading = mdq(sectionMarkdown).query('h3[0]');
|
|
480
|
+
if (heading.count() === 0)
|
|
481
|
+
heading = mdq(sectionMarkdown).query('h2[0]');
|
|
482
|
+
if (heading.count() === 0)
|
|
483
|
+
return sectionMarkdown;
|
|
484
|
+
return heading.replace(`${heading.text().trimEnd()}\n\n> Container: '${containerCss}'\n\n`);
|
|
477
485
|
}
|
|
478
486
|
_deduplicateExpandedSections(sections) {
|
|
479
487
|
const seen = new Set();
|
|
@@ -281,7 +281,7 @@ 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]').
|
|
284
|
+
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Container', `'${newCss}'`);
|
|
285
285
|
}
|
|
286
286
|
else {
|
|
287
287
|
result.text = sectionQuery.query('blockquote[0]').replace('');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type Explorer from '../../explorer.js';
|
|
2
|
+
import { type ListMeasure, type PaginationStrategy } from '../../utils/pagination.js';
|
|
3
|
+
import { type Constructor } from './mixin.js';
|
|
4
|
+
import type { ResearchResult } from './research-result.js';
|
|
5
|
+
export declare function WithPagination<T extends Constructor>(Base: T): {
|
|
6
|
+
new (...args: any[]): {
|
|
7
|
+
explorer: Explorer;
|
|
8
|
+
detectPagination(result: ResearchResult): Promise<void>;
|
|
9
|
+
probeSection(css: string): Promise<PaginationStrategy | null>;
|
|
10
|
+
measure(css: string): Promise<ListMeasure | null>;
|
|
11
|
+
recordPagination(result: ResearchResult, name: string, strategy: PaginationStrategy): void;
|
|
12
|
+
};
|
|
13
|
+
} & T;
|
|
14
|
+
export interface PaginationMethods {
|
|
15
|
+
detectPagination(result: ResearchResult): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { mdq } from "../../utils/markdown-query.js";
|
|
2
|
+
import { inspectList, restoreScroll } from "../../utils/pagination.js";
|
|
3
|
+
import { debugLog } from "./mixin.js";
|
|
4
|
+
import { extractPaginationFromBlockquote, parseDataSections, parseResearchSections } from "./parser.js";
|
|
5
|
+
export function WithPagination(Base) {
|
|
6
|
+
return class extends Base {
|
|
7
|
+
async detectPagination(result) {
|
|
8
|
+
const sections = [...parseResearchSections(result.text), ...parseDataSections(result.text)];
|
|
9
|
+
for (const section of sections) {
|
|
10
|
+
const css = section.containerCss;
|
|
11
|
+
if (!css)
|
|
12
|
+
continue;
|
|
13
|
+
if (extractPaginationFromBlockquote(section.rawMarkdown))
|
|
14
|
+
continue;
|
|
15
|
+
const strategy = await this.probeSection(css);
|
|
16
|
+
if (!strategy)
|
|
17
|
+
continue;
|
|
18
|
+
this.recordPagination(result, section.name, strategy);
|
|
19
|
+
debugLog(`Pagination in "${section.name}": ${strategy}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async probeSection(css) {
|
|
23
|
+
const before = await this.measure(css);
|
|
24
|
+
if (!before)
|
|
25
|
+
return null;
|
|
26
|
+
if (before.hasPagingControls)
|
|
27
|
+
return 'controls';
|
|
28
|
+
if (before.isFeed)
|
|
29
|
+
return 'infinite';
|
|
30
|
+
if (!before.scrolls)
|
|
31
|
+
return null;
|
|
32
|
+
const action = this.explorer.action();
|
|
33
|
+
const scrolled = await action.attempt(`I.scrollTo('${css} > *:last-child')`).catch(() => false);
|
|
34
|
+
if (!scrolled)
|
|
35
|
+
return null;
|
|
36
|
+
const after = await this.measure(css);
|
|
37
|
+
await this.explorer.withPage((page) => page.evaluate(restoreScroll, { css, scrollTop: before.scrollTop, pageScrollY: before.pageScrollY })).catch(() => { });
|
|
38
|
+
if (!after)
|
|
39
|
+
return null;
|
|
40
|
+
if (after.items > before.items)
|
|
41
|
+
return 'infinite';
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
measure(css) {
|
|
45
|
+
return this.explorer
|
|
46
|
+
.withPage((page) => page.evaluate(inspectList, css))
|
|
47
|
+
.catch((err) => {
|
|
48
|
+
debugLog(`List measurement failed for '${css}': ${err.message}`);
|
|
49
|
+
return null;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
recordPagination(result, name, strategy) {
|
|
53
|
+
const escaped = name.replace(/"/g, '\\"');
|
|
54
|
+
let sectionQuery = mdq(result.text).query(`section2(~"${escaped}")`);
|
|
55
|
+
if (sectionQuery.count() === 0)
|
|
56
|
+
sectionQuery = mdq(result.text).query(`section3(~"${escaped}")`);
|
|
57
|
+
if (sectionQuery.count() === 0)
|
|
58
|
+
return;
|
|
59
|
+
result.text = sectionQuery.query('blockquote[0]').setKeyValue('Pagination', strategy);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PaginationStrategy } from '../../utils/pagination.js';
|
|
1
2
|
export interface ResearchElement {
|
|
2
3
|
name: string;
|
|
3
4
|
type: string | null;
|
|
@@ -23,6 +24,8 @@ export declare const RESEARCH_COLUMN_ORDER: string[];
|
|
|
23
24
|
export declare function mapRowToElement(row: Record<string, string>): ResearchElement | null;
|
|
24
25
|
export declare function extractContainerFromBlockquote(sectionMarkdown: string): string | null;
|
|
25
26
|
export declare function parseResearchSections(markdown: string): ResearchSection[];
|
|
27
|
+
export declare function parseDataSections(markdown: string): ResearchSection[];
|
|
28
|
+
export declare function extractPaginationFromBlockquote(sectionMarkdown: string): PaginationStrategy | null;
|
|
26
29
|
export declare function extractValidContainers(researchText: string, opts?: {
|
|
27
30
|
exclude?: string[];
|
|
28
31
|
}): Array<{
|
|
@@ -57,13 +57,10 @@ export function mapRowToElement(row) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
export function extractContainerFromBlockquote(sectionMarkdown) {
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
60
|
+
const entry = mdq(sectionMarkdown).query('blockquote[0]').keyValue().container;
|
|
61
|
+
if (!entry)
|
|
62
62
|
return null;
|
|
63
|
-
const
|
|
64
|
-
if (!match)
|
|
65
|
-
return null;
|
|
66
|
-
const css = normalizeLocatorValue(match[1]);
|
|
63
|
+
const css = normalizeLocatorValue(entry);
|
|
67
64
|
if (!css || !/^[.#\[\w]/.test(css))
|
|
68
65
|
return null;
|
|
69
66
|
return css;
|
|
@@ -80,6 +77,25 @@ export function parseResearchSections(markdown) {
|
|
|
80
77
|
return { name: section.name, containerCss, elements, rawMarkdown: section.rawMarkdown, isExtended };
|
|
81
78
|
});
|
|
82
79
|
}
|
|
80
|
+
export function parseDataSections(markdown) {
|
|
81
|
+
return parseSections(markdown)
|
|
82
|
+
.filter((s) => s.name.toLowerCase().startsWith('data:'))
|
|
83
|
+
.map((section) => ({
|
|
84
|
+
name: section.name,
|
|
85
|
+
containerCss: extractContainerFromBlockquote(section.rawMarkdown),
|
|
86
|
+
elements: [],
|
|
87
|
+
rawMarkdown: section.rawMarkdown,
|
|
88
|
+
isExtended: false,
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
export function extractPaginationFromBlockquote(sectionMarkdown) {
|
|
92
|
+
const value = mdq(sectionMarkdown).query('blockquote[0]').keyValue().pagination?.toLowerCase();
|
|
93
|
+
if (value === 'controls')
|
|
94
|
+
return 'controls';
|
|
95
|
+
if (value === 'infinite')
|
|
96
|
+
return 'infinite';
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
83
99
|
export function extractValidContainers(researchText, opts) {
|
|
84
100
|
const exclude = opts?.exclude || [];
|
|
85
101
|
return parseResearchSections(researchText)
|
|
@@ -63,7 +63,7 @@ export function WithSections(Base) {
|
|
|
63
63
|
}
|
|
64
64
|
async _researchSingleSection(name, description, ariaSnapshot, focusCss) {
|
|
65
65
|
const currentUrl = this.stateManager.getCurrentState()?.url || '';
|
|
66
|
-
const rules = RulesLoader.loadRules('researcher', ['ui-map-table', 'list-element', 'container-rules'], currentUrl);
|
|
66
|
+
const rules = RulesLoader.loadRules('researcher', ['ui-map-table', 'list-element', 'container-rules', 'pagination'], currentUrl);
|
|
67
67
|
const url = this.actionResult?.url || 'Unknown';
|
|
68
68
|
const title = this.actionResult?.title || 'Unknown';
|
|
69
69
|
let focusHint = '';
|
|
@@ -18,6 +18,7 @@ import { WithCoordinates } from "./researcher/coordinates.js";
|
|
|
18
18
|
import { WithDeepAnalysis } from "./researcher/deep-analysis.js";
|
|
19
19
|
import { detectFocusedSection, hasFocusedSection, markSectionAsFocused, pickDefaultFocusedSection } from "./researcher/focus.js";
|
|
20
20
|
import { WithLocators } from "./researcher/locators.js";
|
|
21
|
+
import { WithPagination } from "./researcher/pagination.js";
|
|
21
22
|
import { extractValidContainers, formatResearchSummary, parseResearchSections } from "./researcher/parser.js";
|
|
22
23
|
import { ResearchResult } from "./researcher/research-result.js";
|
|
23
24
|
import { WithSections } from "./researcher/sections.js";
|
|
@@ -33,7 +34,7 @@ export const POSSIBLE_SECTIONS = {
|
|
|
33
34
|
menu: 'page menu (toolbar, context actions, filters, dropdowns)',
|
|
34
35
|
navigation: 'main navigation (top bar, sidebar, breadcrumbs)',
|
|
35
36
|
};
|
|
36
|
-
const ResearcherBase = WithSections(WithDeepAnalysis(WithCoordinates(WithLocators(TaskAgent))));
|
|
37
|
+
const ResearcherBase = WithSections(WithPagination(WithDeepAnalysis(WithCoordinates(WithLocators(TaskAgent)))));
|
|
37
38
|
export class Researcher extends ResearcherBase {
|
|
38
39
|
ACTION_TOOLS = ['click'];
|
|
39
40
|
emoji = '🔍';
|
|
@@ -230,6 +231,9 @@ export class Researcher extends ResearcherBase {
|
|
|
230
231
|
await this.backfillCoordinates(result);
|
|
231
232
|
await this.backfillBrokenLocators(result);
|
|
232
233
|
}
|
|
234
|
+
if (!interrupted()) {
|
|
235
|
+
await this.detectPagination(result);
|
|
236
|
+
}
|
|
233
237
|
// Focused section: final fallback (vision-only — without a screenshot we don't infer focus)
|
|
234
238
|
if (this.hasScreenshotToAnalyze && !hasFocusedSection(result.text)) {
|
|
235
239
|
const sections = parseResearchSections(result.text);
|
|
@@ -366,7 +370,7 @@ export class Researcher extends ResearcherBase {
|
|
|
366
370
|
|
|
367
371
|
${generalLocatorRuleText}
|
|
368
372
|
|
|
369
|
-
${RulesLoader.loadRules('researcher', ['ui-map-table', 'list-element', 'container-rules'], currentUrl)}
|
|
373
|
+
${RulesLoader.loadRules('researcher', ['ui-map-table', 'list-element', 'container-rules', 'pagination'], currentUrl)}
|
|
370
374
|
|
|
371
375
|
<section_identification>
|
|
372
376
|
Identify page sections in this priority order:
|
|
@@ -439,6 +443,7 @@ export class Researcher extends ResearcherBase {
|
|
|
439
443
|
- When a section contains a list of similar data items (records, entities, rows — content that varies by data, not by app UI), output it as a Data section with NO table.
|
|
440
444
|
- Data section heading MUST be a level-2 heading (##) that starts exactly with "Data:" — for example: "## Data: Suites List". Do NOT use ### or add section numbers.
|
|
441
445
|
- Data sections must NOT include a UI map table. Only include the container and a brief summary line.
|
|
446
|
+
- When the data list has controls that move between pages of the collection, add "> Pagination: controls" under its container.
|
|
442
447
|
- Example data section:
|
|
443
448
|
|
|
444
449
|
## Data: Suites List
|
package/dist/src/ai/rules.js
CHANGED
|
@@ -332,6 +332,22 @@ export const actionRule = dedent `
|
|
|
332
332
|
For checkboxes, prefer I.checkOption/I.uncheckOption over I.click.
|
|
333
333
|
|
|
334
334
|
|
|
335
|
+
### I.scrollTo
|
|
336
|
+
|
|
337
|
+
scrolls until the element is in view
|
|
338
|
+
|
|
339
|
+
I.scrollTo(<locator>)
|
|
340
|
+
|
|
341
|
+
Scrolls every scrollable ancestor of the target, so it reaches an element inside a container
|
|
342
|
+
that has its own scrollbar. I.scrollPageToBottom() moves only the page itself.
|
|
343
|
+
|
|
344
|
+
<example>
|
|
345
|
+
I.scrollTo('.rows > *:last-child');
|
|
346
|
+
I.scrollTo({ role: 'listitem', text: 'Last entry' });
|
|
347
|
+
I.scrollPageToBottom();
|
|
348
|
+
</example>
|
|
349
|
+
|
|
350
|
+
|
|
335
351
|
### I.fillField
|
|
336
352
|
|
|
337
353
|
fills the field with the given value
|
package/dist/src/ai/scout.js
CHANGED
|
@@ -47,12 +47,18 @@ export class Scout {
|
|
|
47
47
|
maxToolRoundtrips: MAX_TOOL_ROUNDTRIPS,
|
|
48
48
|
agentName: 'scout',
|
|
49
49
|
});
|
|
50
|
+
const responseText = invokeResult?.response?.text;
|
|
51
|
+
if (responseText?.trim()) {
|
|
52
|
+
finishFromText(responseText);
|
|
53
|
+
stop();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
50
56
|
if (!invokeResult?.toolExecutions?.length) {
|
|
51
|
-
finishFromText(invokeResult?.response?.text);
|
|
52
57
|
stop();
|
|
53
58
|
return;
|
|
54
59
|
}
|
|
55
|
-
if (iteration >= MAX_ITERATIONS) {
|
|
60
|
+
if (iteration >= MAX_ITERATIONS - 1) {
|
|
61
|
+
conversation.addUserText('Exploration time is over. Report your findings now as your final message.');
|
|
56
62
|
const final = await this.provider.invokeConversation(conversation, undefined, { agentName: 'scout' });
|
|
57
63
|
finishFromText(final?.response?.text);
|
|
58
64
|
stop();
|
package/dist/src/ai/tools.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExecutedStep } from '../action.js';
|
|
2
1
|
import { ActionResult, type PageDiff } from '../action-result.js';
|
|
2
|
+
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';
|
package/dist/src/ai/tools.js
CHANGED
|
@@ -5,13 +5,13 @@ import { ActionResult } from "../action-result.js";
|
|
|
5
5
|
import { renderExperienceRecipes } from "../experience-tracker.js";
|
|
6
6
|
import { Stats } from "../stats.js";
|
|
7
7
|
import { TestResult } from '../test-plan.js';
|
|
8
|
+
import { ariaRefSelector, describeRef, refIsGone } from "../utils/aria-ref.js";
|
|
8
9
|
import { LARGE_ARIA_CHANGE_THRESHOLD } from "../utils/aria.js";
|
|
9
10
|
import { isFatalBrowserError } from "../utils/browser-errors.js";
|
|
10
11
|
import { cleanHtmlSnippet } from "../utils/html.js";
|
|
11
12
|
import { createDebug, tag } from '../utils/logger.js';
|
|
12
|
-
import { compactErrorMessage, normalizeInlineText, truncate } from "../utils/strings.js";
|
|
13
13
|
import { pause } from '../utils/loop.js';
|
|
14
|
-
import {
|
|
14
|
+
import { compactErrorMessage, normalizeInlineText, truncate } from "../utils/strings.js";
|
|
15
15
|
import { WebElement } from "../utils/web-element.js";
|
|
16
16
|
import { sectionContextRule } from "./rules.js";
|
|
17
17
|
import { isInteractive } from "./task-agent.js";
|
|
@@ -109,7 +109,13 @@ export function createCodeceptJSTools({ explorer, stateManager }, task) {
|
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
await commitNote(activeNote, TestResult.PASSED, toolResult, action);
|
|
112
|
-
|
|
112
|
+
const data = { ...toolResult, attempts, code: command };
|
|
113
|
+
const notExecuted = commands.slice(i + 1);
|
|
114
|
+
if (notExecuted.length) {
|
|
115
|
+
data.notExecuted = notExecuted;
|
|
116
|
+
data.suggestion = `SKIPPED: ${notExecuted.join('; ')}`;
|
|
117
|
+
}
|
|
118
|
+
return successToolResult('click', data, action);
|
|
113
119
|
}
|
|
114
120
|
}
|
|
115
121
|
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, commands[0]);
|
|
@@ -305,6 +311,7 @@ export function createCodeceptJSTools({ explorer, stateManager }, task) {
|
|
|
305
311
|
- Working with iframes (switch context with I.switchTo)
|
|
306
312
|
- Performing multiple form actions in a single batch
|
|
307
313
|
- Complex interactions requiring sequential commands
|
|
314
|
+
- Reaching items further down a list (I.scrollTo)
|
|
308
315
|
|
|
309
316
|
Example - filling a form with context (PREFERRED):
|
|
310
317
|
I.fillField('Username', 'John', '.login-form')
|
|
@@ -359,10 +366,10 @@ export function createCodeceptJSTools({ explorer, stateManager }, task) {
|
|
|
359
366
|
}
|
|
360
367
|
if (!hasObservablePageChange(toolResult)) {
|
|
361
368
|
activeNote.commit(TestResult.FAILED);
|
|
362
|
-
return failedToolResult('form', '
|
|
369
|
+
return failedToolResult('form', 'Command executed, but nothing on the page changed: no navigation, no ARIA change, no HTML change and no request.', {
|
|
363
370
|
...toolResult,
|
|
364
371
|
code: codeBlock,
|
|
365
|
-
suggestion: '
|
|
372
|
+
suggestion: 'The command ran without reaching anything. Re-locate the target, check whether another UI layer is active, then retry. If the goal was to load more of a list, no change means the collection has ended.',
|
|
366
373
|
});
|
|
367
374
|
}
|
|
368
375
|
await commitNote(activeNote, TestResult.PASSED, toolResult, action);
|
|
@@ -1074,7 +1081,13 @@ export function successToolResult(action, data, source) {
|
|
|
1074
1081
|
return result;
|
|
1075
1082
|
}
|
|
1076
1083
|
export function isMajorPageChange(pageDiff) {
|
|
1077
|
-
|
|
1084
|
+
if (pageDiff.urlChanged === true)
|
|
1085
|
+
return false;
|
|
1086
|
+
if ((pageDiff.ariaChangeCount ?? 0) < LARGE_ARIA_CHANGE_THRESHOLD)
|
|
1087
|
+
return false;
|
|
1088
|
+
if (pageDiff.ariaRemoved === 0 && (pageDiff.ariaAdded ?? 0) > 0)
|
|
1089
|
+
return false;
|
|
1090
|
+
return true;
|
|
1078
1091
|
}
|
|
1079
1092
|
export function hasFailedRequest(pageDiff) {
|
|
1080
1093
|
return (pageDiff.requests ?? []).some((request) => request.status >= 400);
|
|
@@ -1109,7 +1122,7 @@ export async function failedToolResult(action, message, data, error) {
|
|
|
1109
1122
|
const errorTexts = [message, ...(data?.attempts?.map((a) => a.error || '') || [])];
|
|
1110
1123
|
if (errorTexts.some((t) => t.toLowerCase().includes(MULTIPLE_ELEMENTS_PATTERN))) {
|
|
1111
1124
|
const matched = await extractWebElements(error);
|
|
1112
|
-
result.suggestion = getMultipleElementsSuggestion(
|
|
1125
|
+
result.suggestion = getMultipleElementsSuggestion();
|
|
1113
1126
|
result.multipleElementsDetected = true;
|
|
1114
1127
|
result.elements = formatElementList(matched);
|
|
1115
1128
|
return result;
|
|
@@ -1121,16 +1134,12 @@ export async function failedToolResult(action, message, data, error) {
|
|
|
1121
1134
|
}
|
|
1122
1135
|
return result;
|
|
1123
1136
|
}
|
|
1124
|
-
function getMultipleElementsSuggestion(
|
|
1125
|
-
const visible = (matched || []).filter((element) => element.visible !== false);
|
|
1126
|
-
let onlyVisible = '';
|
|
1127
|
-
if (matched && visible.length === 1)
|
|
1128
|
-
onlyVisible = `\nOnly element ${matched.indexOf(visible[0]) + 1} is on screen, so that is the one to act on.`;
|
|
1137
|
+
function getMultipleElementsSuggestion() {
|
|
1129
1138
|
return dedent `
|
|
1130
1139
|
Multiple elements matched your locator, so that command did nothing — it selected no element and acted on none.
|
|
1131
1140
|
Read the numbered elements list and act on the one you meant by its number:
|
|
1132
1141
|
reuse the same locator with step.opts({ elementIndex: N }) as the last argument.
|
|
1133
|
-
A match reported as not visible can never be acted on — pick one that is
|
|
1142
|
+
A match reported as not visible can never be acted on — pick one that is.
|
|
1134
1143
|
If none of them is the element you want, narrow the locator with a container or its full unique text.
|
|
1135
1144
|
If the list is missing, call xpathCheck() to see what the locator matches.
|
|
1136
1145
|
`;
|
|
@@ -1197,6 +1206,9 @@ function formatElementList(matched) {
|
|
|
1197
1206
|
const lines = [`Element ${i + 1}:`, `Text: "${el.text}"`];
|
|
1198
1207
|
if (el.visible !== undefined)
|
|
1199
1208
|
lines.push(`Visible: ${el.visible}`);
|
|
1209
|
+
const wrapped = matched.map((_, j) => j).filter((j) => j !== i && matched[j].xpath.startsWith(`${el.xpath}/`));
|
|
1210
|
+
if (wrapped.length)
|
|
1211
|
+
lines.push(`Wraps: element ${wrapped.map((j) => j + 1).join(', ')}`);
|
|
1200
1212
|
lines.push(`XPath: ${el.xpath}`, `HTML: ${el.html}`);
|
|
1201
1213
|
return lines.join('\n');
|
|
1202
1214
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare function validateSpecs(specs?: string[]): void;
|
|
2
2
|
export declare function loadSpec(specPaths: string[], outputDir: string): Promise<any>;
|
|
3
3
|
export declare function extractEndpointDefinition(schema: any, endpoint: string, baseEndpoint?: string): string;
|
|
4
|
+
export declare function resolveEndpoints(schema: any, pattern: string, baseEndpoint?: string): string[];
|
|
4
5
|
export declare function searchEndpoints(schema: any, query: string, baseEndpoint?: string): string;
|
|
5
6
|
export declare function listAllEndpoints(schema: any, baseEndpoint?: string): string;
|