explorbot 0.3.5 → 0.4.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/bin/explorbot-cli.ts +18 -13
- package/boat/doc-collector/src/cli.ts +3 -0
- package/boat/doc-collector/src/docbot.ts +3 -1
- package/boat/prima/src/cli.ts +21 -8
- package/boat/prima/src/envelope.ts +35 -9
- package/boat/prima/src/prima.ts +23 -10
- package/dist/bin/explorbot-cli.js +19 -13
- package/dist/boat/doc-collector/src/cli.js +3 -0
- package/dist/boat/doc-collector/src/docbot.js +3 -1
- package/dist/boat/prima/src/cli.js +19 -8
- package/dist/boat/prima/src/envelope.js +24 -6
- package/dist/boat/prima/src/prima.js +23 -11
- package/dist/package.json +2 -2
- package/dist/src/action-result.d.ts +9 -1
- package/dist/src/action-result.js +57 -18
- package/dist/src/action.d.ts +1 -1
- package/dist/src/action.js +87 -12
- package/dist/src/ai/driller.d.ts +0 -1
- package/dist/src/ai/driller.js +8 -20
- package/dist/src/ai/fisherman-tools.d.ts +9 -0
- package/dist/src/ai/fisherman-tools.js +52 -6
- package/dist/src/ai/fisherman.d.ts +4 -2
- package/dist/src/ai/fisherman.js +48 -27
- package/dist/src/ai/historian/codeceptjs.js +1 -1
- package/dist/src/ai/historian/playwright.js +1 -1
- package/dist/src/ai/pilot.d.ts +1 -0
- package/dist/src/ai/pilot.js +13 -1
- package/dist/src/ai/provider.js +20 -3
- package/dist/src/ai/researcher/deep-analysis.js +1 -3
- package/dist/src/ai/researcher.js +3 -3
- package/dist/src/ai/tester.d.ts +3 -0
- package/dist/src/ai/tester.js +40 -3
- package/dist/src/ai/tools.d.ts +1 -0
- package/dist/src/ai/tools.js +13 -6
- package/dist/src/api/request-result.d.ts +2 -0
- package/dist/src/api/request-result.js +8 -2
- package/dist/src/api/request-store.d.ts +3 -2
- package/dist/src/api/request-store.js +66 -14
- package/dist/src/commands/explore-command.d.ts +6 -0
- package/dist/src/commands/explore-command.js +27 -2
- package/dist/src/commands/freesail-command.js +10 -1
- package/dist/src/commands/plans-command.js +6 -6
- package/dist/src/config.js +1 -0
- package/dist/src/experience-tracker.js +5 -0
- package/dist/src/explorbot.d.ts +0 -1
- package/dist/src/explorbot.js +23 -36
- package/dist/src/state-manager.d.ts +5 -1
- package/dist/src/state-manager.js +10 -7
- package/dist/src/test-plan.d.ts +3 -0
- package/dist/src/test-plan.js +27 -0
- package/dist/src/utils/aria.d.ts +1 -1
- package/dist/src/utils/aria.js +6 -42
- package/dist/src/utils/html-diff.d.ts +4 -0
- package/dist/src/utils/html-diff.js +62 -7
- package/dist/src/utils/html.d.ts +5 -15
- package/dist/src/utils/html.js +14 -85
- package/dist/src/utils/overlay.d.ts +56 -11
- package/dist/src/utils/overlay.js +191 -21
- package/dist/src/utils/request-map.d.ts +7 -0
- package/dist/src/utils/request-map.js +16 -0
- package/dist/src/utils/url-matcher.js +4 -2
- package/docs/reference/commands.md +8 -1
- package/docs/reference/websocket.md +1 -0
- package/docs/superpowers/plans/2026-08-29-fisherman-reliability.md +953 -0
- package/docs/superpowers/plans/2026-08-29-region-states.md +1292 -0
- package/docs/superpowers/plans/2026-08-30-fisherman-live-session-auth.md +457 -0
- package/docs/superpowers/specs/2026-08-29-fisherman-reliability-design.md +45 -0
- package/docs/superpowers/specs/2026-08-29-region-states-design.md +262 -0
- package/docs/superpowers/specs/2026-08-29-region-states-fixes-design.md +269 -0
- package/docs/superpowers/specs/2026-08-30-fisherman-live-session-auth-design.md +37 -0
- package/docs/workflow/agentic-usage.md +1 -0
- package/docs/workflow/ci.md +1 -0
- package/package.json +2 -2
- package/src/action-result.ts +61 -22
- package/src/action.ts +87 -14
- package/src/ai/driller.ts +7 -39
- package/src/ai/fisherman-tools.ts +56 -7
- package/src/ai/fisherman.ts +48 -28
- package/src/ai/historian/codeceptjs.ts +1 -1
- package/src/ai/historian/playwright.ts +1 -1
- package/src/ai/pilot.ts +9 -1
- package/src/ai/provider.ts +21 -3
- package/src/ai/researcher/deep-analysis.ts +1 -2
- package/src/ai/researcher.ts +3 -3
- package/src/ai/tester.ts +40 -3
- package/src/ai/tools.ts +17 -9
- package/src/api/request-result.ts +10 -2
- package/src/api/request-store.ts +60 -13
- package/src/commands/explore-command.ts +25 -2
- package/src/commands/freesail-command.ts +7 -1
- package/src/commands/plans-command.ts +6 -6
- package/src/config.ts +1 -0
- package/src/experience-tracker.ts +5 -1
- package/src/explorbot.ts +20 -36
- package/src/state-manager.ts +13 -7
- package/src/test-plan.ts +29 -0
- package/src/utils/aria.ts +7 -44
- package/src/utils/html-diff.ts +62 -7
- package/src/utils/html.ts +14 -91
- package/src/utils/overlay.ts +226 -23
- package/src/utils/request-map.ts +19 -0
- package/src/utils/url-matcher.ts +3 -2
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
export const STATUS_FILE = 'status.json';
|
|
4
|
+
const ARTIFACT_FILES = { aria: 'aria.yml', html: 'page.html', screenshot: 'page.png', network: 'network.jsonl' };
|
|
3
5
|
const EXPECTATION_LABELS = {
|
|
4
6
|
passed: 'PASSED ',
|
|
5
7
|
failed: 'FAILED ',
|
|
@@ -13,21 +15,35 @@ export function renderEnvelope(data) {
|
|
|
13
15
|
export function writeArtifacts(dir, snapshot) {
|
|
14
16
|
mkdirSync(dir, { recursive: true });
|
|
15
17
|
const paths = {
|
|
16
|
-
aria: path.resolve(dir,
|
|
17
|
-
html: path.resolve(dir,
|
|
18
|
+
aria: path.resolve(dir, ARTIFACT_FILES.aria),
|
|
19
|
+
html: path.resolve(dir, ARTIFACT_FILES.html),
|
|
18
20
|
};
|
|
19
21
|
writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
|
|
20
22
|
writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
|
|
21
23
|
if (snapshot.screenshot) {
|
|
22
|
-
paths.screenshot = path.resolve(dir,
|
|
24
|
+
paths.screenshot = path.resolve(dir, ARTIFACT_FILES.screenshot);
|
|
23
25
|
writeFileSync(paths.screenshot, snapshot.screenshot);
|
|
24
26
|
}
|
|
25
27
|
if (!snapshot.requests.length)
|
|
26
28
|
return paths;
|
|
27
|
-
paths.network = path.resolve(dir,
|
|
29
|
+
paths.network = path.resolve(dir, ARTIFACT_FILES.network);
|
|
28
30
|
writeFileSync(paths.network, snapshot.requests.map((request) => `${JSON.stringify(request)}\n`).join(''), 'utf-8');
|
|
29
31
|
return paths;
|
|
30
32
|
}
|
|
33
|
+
export function readArtifacts(dir) {
|
|
34
|
+
const paths = { aria: path.resolve(dir, ARTIFACT_FILES.aria), html: path.resolve(dir, ARTIFACT_FILES.html) };
|
|
35
|
+
if (existsSync(path.resolve(dir, ARTIFACT_FILES.screenshot)))
|
|
36
|
+
paths.screenshot = path.resolve(dir, ARTIFACT_FILES.screenshot);
|
|
37
|
+
if (existsSync(path.resolve(dir, ARTIFACT_FILES.network)))
|
|
38
|
+
paths.network = path.resolve(dir, ARTIFACT_FILES.network);
|
|
39
|
+
const recorded = [...Object.values(ARTIFACT_FILES), STATUS_FILE];
|
|
40
|
+
const files = readdirSync(dir)
|
|
41
|
+
.filter((entry) => !recorded.includes(entry))
|
|
42
|
+
.sort();
|
|
43
|
+
if (files.length)
|
|
44
|
+
paths.files = files;
|
|
45
|
+
return paths;
|
|
46
|
+
}
|
|
31
47
|
function renderResult(data) {
|
|
32
48
|
const lines = [`ok: ${data.ok}`, `command: ${data.command}`];
|
|
33
49
|
if (data.used?.length)
|
|
@@ -70,7 +86,7 @@ function renderSteps(data) {
|
|
|
70
86
|
lines.push(` ${line}`);
|
|
71
87
|
});
|
|
72
88
|
if (data.stepFiles)
|
|
73
|
-
lines.push('', `page after each step: ${data.stepFiles}`);
|
|
89
|
+
lines.push('', `page after each step: ${data.stepFiles}/<n>-<step>.{aria.yaml,html,diff.yaml}`);
|
|
74
90
|
return section('Steps', lines.join('\n'));
|
|
75
91
|
}
|
|
76
92
|
function renderExpectations(data) {
|
|
@@ -154,6 +170,8 @@ export function renderArtifacts(data) {
|
|
|
154
170
|
lines.push(`screenshot: ${data.artifacts.screenshot}`);
|
|
155
171
|
if (data.artifacts.network)
|
|
156
172
|
lines.push(`network: ${data.artifacts.network}`);
|
|
173
|
+
if (data.artifacts.files?.length)
|
|
174
|
+
lines.push(`also here: ${data.artifacts.files.join(', ')}`);
|
|
157
175
|
return section('Artifacts', lines.join('\n'));
|
|
158
176
|
}
|
|
159
177
|
function align(label, marker, width) {
|
|
@@ -23,7 +23,7 @@ import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
|
|
|
23
23
|
import { pluralize } from "../../../src/utils/logger.js";
|
|
24
24
|
import { mdq } from "../../../src/utils/markdown-query.js";
|
|
25
25
|
import { safeFilename } from "../../../src/utils/strings.js";
|
|
26
|
-
import { writeArtifacts } from "./envelope.js";
|
|
26
|
+
import { STATUS_FILE, readArtifacts, writeArtifacts } from "./envelope.js";
|
|
27
27
|
import { isFunctionExpression, takePwValue, toCodeceptWrapper } from "./pw-parser.js";
|
|
28
28
|
import { readDescriptors, selectDescriptor } from "./pw-registry.js";
|
|
29
29
|
import { latestSessionFile, readSession, recordCommand, sessionFile, sessionsDir } from "./session-log.js";
|
|
@@ -341,8 +341,6 @@ export class Prima {
|
|
|
341
341
|
const problems = [...unreached.map((expectation) => `not reached: ${expectation.text}`), ...contradicted.map((expectation) => `the picture and the run disagree about: ${expectation.text}`)];
|
|
342
342
|
if (problems.length)
|
|
343
343
|
envelope.failure = { error: problems.join('\n') };
|
|
344
|
-
if (contradicted.length)
|
|
345
|
-
envelope.artifacts = this.artifacts;
|
|
346
344
|
if (!test.hasFinished || test.isSkipped) {
|
|
347
345
|
envelope.ok = false;
|
|
348
346
|
envelope.failure = { error: `the run did not complete, so it established nothing about the app: ${notes.at(-1)?.message || 'no steps were recorded'}` };
|
|
@@ -924,14 +922,17 @@ export class Prima {
|
|
|
924
922
|
return response?.text || '';
|
|
925
923
|
}
|
|
926
924
|
async successEnvelope(command, used, result, previousState) {
|
|
925
|
+
const changes = await this.pageChanges(result, previousState, used[0]);
|
|
926
|
+
const status = await this.saveStatus(result);
|
|
927
927
|
return {
|
|
928
928
|
ok: true,
|
|
929
929
|
command,
|
|
930
930
|
used,
|
|
931
931
|
page: this.pageBlock(result, previousState),
|
|
932
|
-
changes
|
|
932
|
+
changes,
|
|
933
933
|
instance: await this.instanceInfo(),
|
|
934
|
-
status
|
|
934
|
+
status,
|
|
935
|
+
artifacts: this.artifacts,
|
|
935
936
|
};
|
|
936
937
|
}
|
|
937
938
|
async failureEnvelope(command, error, previousState) {
|
|
@@ -939,23 +940,27 @@ export class Prima {
|
|
|
939
940
|
const failure = { error: browserErrorMessage(error) };
|
|
940
941
|
if (result.ariaSnapshot)
|
|
941
942
|
failure.compactAria = compactAriaSnapshot(result.ariaSnapshot, true);
|
|
943
|
+
const status = await this.saveStatus(result);
|
|
942
944
|
return {
|
|
943
945
|
ok: false,
|
|
944
946
|
command,
|
|
945
947
|
page: this.pageBlock(result, previousState),
|
|
946
948
|
failure,
|
|
947
949
|
instance: await this.instanceInfo(),
|
|
948
|
-
status
|
|
950
|
+
status,
|
|
951
|
+
artifacts: this.artifacts,
|
|
949
952
|
};
|
|
950
953
|
}
|
|
951
954
|
async reportEnvelope(command, result, previousState, outcome) {
|
|
955
|
+
const status = await this.saveStatus(result);
|
|
952
956
|
return {
|
|
953
957
|
ok: true,
|
|
954
958
|
command,
|
|
955
959
|
page: this.pageBlock(result, previousState),
|
|
956
960
|
...outcome,
|
|
957
961
|
instance: await this.instanceInfo(),
|
|
958
|
-
status
|
|
962
|
+
status,
|
|
963
|
+
artifacts: this.artifacts,
|
|
959
964
|
};
|
|
960
965
|
}
|
|
961
966
|
async capturedResult(previousState, opts = {}) {
|
|
@@ -1000,23 +1005,30 @@ export class Prima {
|
|
|
1000
1005
|
return lines.join('\n');
|
|
1001
1006
|
}
|
|
1002
1007
|
async status(hash) {
|
|
1008
|
+
if (!this.artifactsDir) {
|
|
1009
|
+
const sites = listSites();
|
|
1010
|
+
const site = sites.find((candidate) => existsSync(path.join(candidate.dir, 'output', 'prima', hash))) || sites[0];
|
|
1011
|
+
if (site && !this.configBaseUrl())
|
|
1012
|
+
this.sessionUrl = site.url;
|
|
1013
|
+
await this.loadConfig();
|
|
1014
|
+
}
|
|
1003
1015
|
const dir = this.statusDir(hash);
|
|
1004
|
-
const statusFile = path.join(dir,
|
|
1016
|
+
const statusFile = path.join(dir, STATUS_FILE);
|
|
1005
1017
|
if (!existsSync(statusFile))
|
|
1006
|
-
return this.toolFailureEnvelope(`status ${hash}`, `No command was recorded under ${
|
|
1018
|
+
return this.toolFailureEnvelope(`status ${hash}`, `No command was recorded under ${dir}. Every envelope prints its own hash on the Instance line.`);
|
|
1007
1019
|
const saved = JSON.parse(readFileSync(statusFile, 'utf-8'));
|
|
1008
1020
|
return {
|
|
1009
1021
|
ok: true,
|
|
1010
1022
|
command: `status ${hash}`,
|
|
1011
1023
|
page: saved.page,
|
|
1012
1024
|
instance: await this.instanceInfo(),
|
|
1013
|
-
artifacts:
|
|
1025
|
+
artifacts: readArtifacts(dir),
|
|
1014
1026
|
};
|
|
1015
1027
|
}
|
|
1016
1028
|
async saveStatus(result) {
|
|
1017
1029
|
const hash = this.statusHash();
|
|
1018
1030
|
await this.writeSnapshot(result);
|
|
1019
|
-
writeFileSync(path.join(this.statusDir(hash),
|
|
1031
|
+
writeFileSync(path.join(this.statusDir(hash), STATUS_FILE), JSON.stringify({ page: this.pageBlock(result, null) }), 'utf-8');
|
|
1020
1032
|
return hash;
|
|
1021
1033
|
}
|
|
1022
1034
|
async writeStepFiles(index, label, diff) {
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"dependencies": {
|
|
78
78
|
"@ai-sdk/anthropic": "^4.0",
|
|
79
79
|
"@ai-sdk/google": "^4.0.18",
|
|
80
|
-
"@ai-sdk/groq": "^4.0",
|
|
80
|
+
"@ai-sdk/groq": "^4.0.34",
|
|
81
81
|
"@ai-sdk/mistral": "^4.0.13",
|
|
82
82
|
"@ai-sdk/openai": "^4.0",
|
|
83
83
|
"@ai-sdk/otel": "^1.0.2",
|
|
@@ -29,7 +29,6 @@ interface ActionResultData extends WebPageState {
|
|
|
29
29
|
focusedElement?: FocusedElement | null;
|
|
30
30
|
iframeURL?: string;
|
|
31
31
|
links?: Link[];
|
|
32
|
-
overlayHtml?: string;
|
|
33
32
|
}
|
|
34
33
|
export interface PageDiff {
|
|
35
34
|
urlChanged: boolean;
|
|
@@ -42,6 +41,7 @@ export interface PageDiff {
|
|
|
42
41
|
consoleErrors?: string[];
|
|
43
42
|
htmlParts?: HtmlDiffPart[];
|
|
44
43
|
iframes?: string;
|
|
44
|
+
areaOfInterest?: string;
|
|
45
45
|
}
|
|
46
46
|
export interface ToolResultMetadata {
|
|
47
47
|
url: string;
|
|
@@ -84,6 +84,10 @@ export declare class ActionResult implements ActionResultData {
|
|
|
84
84
|
links: Link[];
|
|
85
85
|
verifications?: Record<string, boolean>;
|
|
86
86
|
overlay: Overlay;
|
|
87
|
+
_diffCache: {
|
|
88
|
+
previousId: number | undefined;
|
|
89
|
+
diff: Diff;
|
|
90
|
+
} | null;
|
|
87
91
|
constructor(data: ActionResultData);
|
|
88
92
|
get hash(): string;
|
|
89
93
|
get html(): string;
|
|
@@ -117,8 +121,10 @@ export declare class ActionResult implements ActionResultData {
|
|
|
117
121
|
get relativeUrl(): string | null;
|
|
118
122
|
get isInsideIframe(): boolean;
|
|
119
123
|
getStateHash(): string;
|
|
124
|
+
get baseHash(): string;
|
|
120
125
|
diff(previousState: ActionResult | null): Promise<Diff>;
|
|
121
126
|
toToolResult(previousState: ActionResult | null, locator: string): Promise<ToolResultMetadata>;
|
|
127
|
+
computeStateHash(includeRegion: boolean): string;
|
|
122
128
|
consoleErrors(): string[];
|
|
123
129
|
}
|
|
124
130
|
export declare class Diff {
|
|
@@ -140,6 +146,8 @@ export declare class Diff {
|
|
|
140
146
|
get ariaChangeCount(): number;
|
|
141
147
|
get htmlDiff(): HtmlDiffResult | null;
|
|
142
148
|
get messages(): string[];
|
|
149
|
+
get similarity(): number;
|
|
150
|
+
get pageSize(): number;
|
|
143
151
|
calculate(): Promise<void>;
|
|
144
152
|
}
|
|
145
153
|
export interface NetworkCall {
|
|
@@ -39,6 +39,7 @@ export class ActionResult {
|
|
|
39
39
|
links = [];
|
|
40
40
|
verifications;
|
|
41
41
|
overlay = new Overlay();
|
|
42
|
+
_diffCache = null;
|
|
42
43
|
constructor(data) {
|
|
43
44
|
this.id = data.id;
|
|
44
45
|
this.timestamp = data.timestamp ?? new Date();
|
|
@@ -108,6 +109,7 @@ export class ActionResult {
|
|
|
108
109
|
set html(value) {
|
|
109
110
|
this._html = value;
|
|
110
111
|
this.snapshotCache.clear();
|
|
112
|
+
this._diffCache = null;
|
|
111
113
|
}
|
|
112
114
|
get screenshot() {
|
|
113
115
|
if (this._screenshot) {
|
|
@@ -193,6 +195,14 @@ export class ActionResult {
|
|
|
193
195
|
isRelevantExperienceRecord(record, options) {
|
|
194
196
|
if (!record.url || !this.url)
|
|
195
197
|
return false;
|
|
198
|
+
if (record.region && this.overlay.name !== record.region)
|
|
199
|
+
return false;
|
|
200
|
+
if (record.root) {
|
|
201
|
+
if (!this.overlay.present)
|
|
202
|
+
return false;
|
|
203
|
+
if (this.overlay.root && this.overlay.root !== record.root)
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
196
206
|
if (this.isMatchedBy(record))
|
|
197
207
|
return true;
|
|
198
208
|
if (!options?.includeDescendantExperience)
|
|
@@ -386,24 +396,17 @@ export class ActionResult {
|
|
|
386
396
|
return !!this.iframeURL;
|
|
387
397
|
}
|
|
388
398
|
getStateHash() {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
parts.push(`h1_${this.h1}`);
|
|
394
|
-
if (this.h2)
|
|
395
|
-
parts.push(`h2_${this.h2}`);
|
|
396
|
-
let stateString = slugify(parts.map((part) => part.substring(0, 100)).join('_'));
|
|
397
|
-
if (stateString.length > 200) {
|
|
398
|
-
stateString = stateString.substring(0, 200);
|
|
399
|
-
if (stateString.endsWith('_')) {
|
|
400
|
-
stateString = stateString.slice(0, -1);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
return stateString;
|
|
399
|
+
return this.computeStateHash(true);
|
|
400
|
+
}
|
|
401
|
+
get baseHash() {
|
|
402
|
+
return this.computeStateHash(false);
|
|
404
403
|
}
|
|
405
404
|
async diff(previousState) {
|
|
406
|
-
|
|
405
|
+
if (this._diffCache && this._diffCache.previousId === previousState?.id)
|
|
406
|
+
return this._diffCache.diff;
|
|
407
|
+
const diff = await Diff.create(this, previousState);
|
|
408
|
+
this._diffCache = { previousId: previousState?.id, diff };
|
|
409
|
+
return diff;
|
|
407
410
|
}
|
|
408
411
|
async toToolResult(previousState, locator) {
|
|
409
412
|
const result = {
|
|
@@ -443,7 +446,18 @@ export class ActionResult {
|
|
|
443
446
|
pageDiff.ariaChanges = diff.ariaChanged;
|
|
444
447
|
pageDiff.ariaChangeCount = diff.ariaChangeCount;
|
|
445
448
|
}
|
|
446
|
-
if (
|
|
449
|
+
if (this.overlay.present && (!previousState.overlay.present || previousState.overlay.name !== this.overlay.name)) {
|
|
450
|
+
pageDiff.areaOfInterest = this.overlay.describe();
|
|
451
|
+
}
|
|
452
|
+
if (pageDiff.areaOfInterest && this.overlay.html && this.overlay.root) {
|
|
453
|
+
const htmlConfig = ConfigParser.getInstance().getConfig().html;
|
|
454
|
+
let subtree = await minifyHtml(htmlCombinedSnapshot(this.overlay.html, htmlConfig?.combined));
|
|
455
|
+
if (subtree.length > HTML_PART_SUBTREE_BUDGET) {
|
|
456
|
+
subtree = `${subtree.slice(0, HTML_PART_SUBTREE_BUDGET)}...<!-- truncated -->`;
|
|
457
|
+
}
|
|
458
|
+
pageDiff.htmlParts = [{ container: this.overlay.root, subtree, rawSize: subtree.length, added: [], removed: [] }];
|
|
459
|
+
}
|
|
460
|
+
else if (diff.isSameUrl() && diff.htmlParts.length > 0) {
|
|
447
461
|
const collapsed = collapseHtmlParts(await diff.cleanedHtmlParts());
|
|
448
462
|
if (collapsed.length > 0) {
|
|
449
463
|
pageDiff.htmlParts = collapsed;
|
|
@@ -456,6 +470,25 @@ export class ActionResult {
|
|
|
456
470
|
}
|
|
457
471
|
return result;
|
|
458
472
|
}
|
|
473
|
+
computeStateHash(includeRegion) {
|
|
474
|
+
const parts = [];
|
|
475
|
+
parts.push(this.relativeUrl || this.url || '/');
|
|
476
|
+
this.extractHeadings(this.html);
|
|
477
|
+
if (this.h1)
|
|
478
|
+
parts.push(`h1_${this.h1}`);
|
|
479
|
+
if (this.h2)
|
|
480
|
+
parts.push(`h2_${this.h2}`);
|
|
481
|
+
if (includeRegion && this.overlay.present && this.overlay.name)
|
|
482
|
+
parts.push(`region_${this.overlay.name}`);
|
|
483
|
+
let stateString = slugify(parts.map((part) => part.substring(0, 100)).join('_'));
|
|
484
|
+
if (stateString.length > 200) {
|
|
485
|
+
stateString = stateString.substring(0, 200);
|
|
486
|
+
if (stateString.endsWith('_')) {
|
|
487
|
+
stateString = stateString.slice(0, -1);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return stateString;
|
|
491
|
+
}
|
|
459
492
|
consoleErrors() {
|
|
460
493
|
const errors = [];
|
|
461
494
|
for (const log of this.browserLogs) {
|
|
@@ -560,14 +593,20 @@ export class Diff {
|
|
|
560
593
|
get messages() {
|
|
561
594
|
return this._messages;
|
|
562
595
|
}
|
|
596
|
+
get similarity() {
|
|
597
|
+
return this._htmlDiffResult?.similarity ?? 0;
|
|
598
|
+
}
|
|
599
|
+
get pageSize() {
|
|
600
|
+
return this._htmlDiffResult?.pageSize ?? 0;
|
|
601
|
+
}
|
|
563
602
|
async calculate() {
|
|
564
603
|
if (!this.previous)
|
|
565
604
|
return;
|
|
605
|
+
this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html, ConfigParser.getInstance().getConfig().html);
|
|
566
606
|
if (!this._isSameUrl) {
|
|
567
607
|
this._messages = liveRegionMessages(this.previous.html, this.current.html);
|
|
568
608
|
return;
|
|
569
609
|
}
|
|
570
|
-
this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html, ConfigParser.getInstance().getConfig().html);
|
|
571
610
|
this._messages = this._htmlDiffResult.messages;
|
|
572
611
|
const ariaDiff = diffAriaSnapshots(this.previous.ariaSnapshot, this.current.ariaSnapshot);
|
|
573
612
|
this._ariaDiffResult = ariaDiff.text;
|
package/dist/src/action.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ declare class Action {
|
|
|
33
33
|
includeScreenshot?: boolean;
|
|
34
34
|
codeBlock?: string;
|
|
35
35
|
}): Promise<ActionResult>;
|
|
36
|
-
|
|
36
|
+
detectRegionOfInterest(result: ActionResult): Promise<void>;
|
|
37
37
|
captureMainDocumentStatus(): Promise<number | undefined>;
|
|
38
38
|
captureResponses(): () => void;
|
|
39
39
|
recordNetworkCall(request: any, status: number): void;
|
package/dist/src/action.js
CHANGED
|
@@ -8,9 +8,9 @@ import { clearActivity, setActivity } from "./activity.js";
|
|
|
8
8
|
import { ConfigParser, outputPath } from './config.js';
|
|
9
9
|
import { Observability } from "./observability.js";
|
|
10
10
|
import { browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
|
|
11
|
-
import { captureHtmlForSnapshot,
|
|
11
|
+
import { captureHtmlForSnapshot, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
12
12
|
import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
|
|
13
|
-
import { Overlay } from './utils/overlay.js';
|
|
13
|
+
import { Overlay, OverlayPage } from './utils/overlay.js';
|
|
14
14
|
import { sleep, waitForPageReadiness } from "./utils/page-readiness.js";
|
|
15
15
|
import { safeFilename } from "./utils/strings.js";
|
|
16
16
|
import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from "./utils/web-sandbox.js";
|
|
@@ -133,13 +133,10 @@ class Action {
|
|
|
133
133
|
let ariaSnapshot = null;
|
|
134
134
|
let ariaSnapshotFile = undefined;
|
|
135
135
|
let focusedElement = null;
|
|
136
|
-
let overlayHtml = '';
|
|
137
136
|
try {
|
|
138
137
|
const page = this.playwrightHelper.page;
|
|
139
138
|
ariaSnapshot = await page.locator('body').ariaSnapshot();
|
|
140
139
|
focusedElement = await page.evaluate(readFocusedElement);
|
|
141
|
-
if (!frame)
|
|
142
|
-
overlayHtml = await this.captureOverlayHtml();
|
|
143
140
|
}
|
|
144
141
|
catch (err) {
|
|
145
142
|
debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
|
|
@@ -166,9 +163,10 @@ class Action {
|
|
|
166
163
|
ariaSnapshot,
|
|
167
164
|
ariaSnapshotFile,
|
|
168
165
|
focusedElement,
|
|
169
|
-
overlayHtml: overlayHtml || undefined,
|
|
170
166
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
171
167
|
});
|
|
168
|
+
if (!frame)
|
|
169
|
+
await this.detectRegionOfInterest(result).catch((err) => debugLog('Region detection failed:', err.message));
|
|
172
170
|
this.stateManager.updateState(result, codeBlock);
|
|
173
171
|
return result;
|
|
174
172
|
}
|
|
@@ -181,11 +179,64 @@ class Action {
|
|
|
181
179
|
return new ActionResult({ url, error: msg });
|
|
182
180
|
}
|
|
183
181
|
}
|
|
184
|
-
async
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return
|
|
188
|
-
|
|
182
|
+
async detectRegionOfInterest(result) {
|
|
183
|
+
const previousState = this.stateManager.getCurrentState();
|
|
184
|
+
if (!previousState)
|
|
185
|
+
return;
|
|
186
|
+
const previous = ActionResult.fromState(previousState);
|
|
187
|
+
const previousOverlay = previous.overlay;
|
|
188
|
+
const sameUrl = !!previous.url && result.isSameUrl({ url: previous.url });
|
|
189
|
+
const overlayPage = new OverlayPage(this.playwrightHelper.page);
|
|
190
|
+
if (result.overlay.detected && previousOverlay.detected && previousOverlay.root && previousOverlay.type === result.overlay.type && previousOverlay.name === result.overlay.name) {
|
|
191
|
+
result.overlay = previousOverlay;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (!previous.html)
|
|
195
|
+
return;
|
|
196
|
+
if (previous.html === result.html) {
|
|
197
|
+
if (sameUrl && previousOverlay.present && previousOverlay.xpath && !result.overlay.detected)
|
|
198
|
+
result.overlay = previousOverlay;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
let carried = null;
|
|
202
|
+
if (sameUrl && previousOverlay.present && previousOverlay.xpath) {
|
|
203
|
+
if (await overlayPage.isStillOpen(previousOverlay)) {
|
|
204
|
+
carried = previousOverlay;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
debugLog(`Region closed: ${previousOverlay.name || previousOverlay.type}`);
|
|
208
|
+
if (!result.overlay.detected) {
|
|
209
|
+
const parent = previousOverlay.parent;
|
|
210
|
+
if (parent?.xpath) {
|
|
211
|
+
const restored = new Overlay(parent);
|
|
212
|
+
if (await overlayPage.isStillOpen(restored))
|
|
213
|
+
result.overlay = restored;
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const diff = await result.diff(previous);
|
|
220
|
+
const detected = await overlayPage.detectRegion({
|
|
221
|
+
parts: diff.htmlParts,
|
|
222
|
+
pageSize: diff.pageSize,
|
|
223
|
+
similarity: diff.similarity,
|
|
224
|
+
sameUrl,
|
|
225
|
+
previousHtml: previous.html,
|
|
226
|
+
});
|
|
227
|
+
if (result.overlay.detected) {
|
|
228
|
+
if (detected)
|
|
229
|
+
result.overlay = result.overlay.withGeometry(detected);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (detected) {
|
|
233
|
+
result.overlay = detected;
|
|
234
|
+
if (carried)
|
|
235
|
+
result.overlay = detected.withParent(carried);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (carried)
|
|
239
|
+
result.overlay = carried;
|
|
189
240
|
}
|
|
190
241
|
async captureMainDocumentStatus() {
|
|
191
242
|
if (this.mainDocumentStatus)
|
|
@@ -349,6 +400,14 @@ class Action {
|
|
|
349
400
|
await recorder.reset();
|
|
350
401
|
await recorder.start();
|
|
351
402
|
}
|
|
403
|
+
if (executedSteps.length > 0) {
|
|
404
|
+
codeString = executedSteps.map((step) => step.command).join('\n');
|
|
405
|
+
}
|
|
406
|
+
if (!isFatalBrowserError(err)) {
|
|
407
|
+
const captured = await this.captureOnce({ codeBlock: codeString }).catch(() => null);
|
|
408
|
+
if (captured && !captured.error)
|
|
409
|
+
this.actionResult = captured;
|
|
410
|
+
}
|
|
352
411
|
this.assertionSteps = [];
|
|
353
412
|
throw err;
|
|
354
413
|
}
|
|
@@ -448,14 +507,30 @@ async function captureTitle(page, actor) {
|
|
|
448
507
|
}
|
|
449
508
|
const ASSERTION_STEP_NAMES = new Set(['see', 'dontSee', 'seeElement', 'dontSeeElement', 'seeInField', 'dontSeeInField', 'seeInCurrentUrl', 'dontSeeInCurrentUrl']);
|
|
450
509
|
export const attachStepLogger = (target, assertionsTarget) => {
|
|
510
|
+
const recorded = new WeakMap();
|
|
511
|
+
let batchFailed = false;
|
|
451
512
|
const listener = (step, error) => {
|
|
452
513
|
if (!step?.toCode)
|
|
453
514
|
return;
|
|
454
515
|
if (step.name?.startsWith('grab'))
|
|
455
516
|
return;
|
|
517
|
+
const existing = recorded.get(step);
|
|
518
|
+
if (existing) {
|
|
519
|
+
if (!error && !existing.success) {
|
|
520
|
+
existing.success = true;
|
|
521
|
+
existing.error = undefined;
|
|
522
|
+
batchFailed = target.some((entry) => !entry.success);
|
|
523
|
+
}
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (batchFailed)
|
|
527
|
+
return;
|
|
456
528
|
const executed = { command: step.toCode(), success: !error };
|
|
457
|
-
if (error)
|
|
529
|
+
if (error) {
|
|
458
530
|
executed.error = errorToString(error);
|
|
531
|
+
batchFailed = true;
|
|
532
|
+
}
|
|
533
|
+
recorded.set(step, executed);
|
|
459
534
|
target.push(executed);
|
|
460
535
|
if (assertionsTarget && ASSERTION_STEP_NAMES.has(step.name)) {
|
|
461
536
|
assertionsTarget.push({ name: step.name, args: step.args || [] });
|
package/dist/src/ai/driller.d.ts
CHANGED
|
@@ -135,7 +135,6 @@ export declare class Driller extends TaskAgent implements Agent {
|
|
|
135
135
|
createVerifiedActionTools(baseTools: Record<string, any>, component: ComponentInfo): Record<string, any>;
|
|
136
136
|
hasVerifiedAction(componentId: string): boolean;
|
|
137
137
|
detectNestedOverlayContext(component: ComponentInfo, result: any): Promise<string | null>;
|
|
138
|
-
getVisibleOverlayHtml(): Promise<string>;
|
|
139
138
|
getComponentScopeHtml(component: ComponentInfo, originalState: ActionResult): Promise<string>;
|
|
140
139
|
saveToKnowledge(knowledgePath: string, state: ActionResult, results: InteractionResult[]): Promise<void>;
|
|
141
140
|
generateKnowledgeContent(state: ActionResult, interactions: InteractionResult[]): string;
|
package/dist/src/ai/driller.js
CHANGED
|
@@ -6,10 +6,9 @@ import { setActivity } from "../activity.js";
|
|
|
6
6
|
import { Observability } from "../observability.js";
|
|
7
7
|
import { Plan, Test, TestResult } from "../test-plan.js";
|
|
8
8
|
import { collectInteractiveNodes } from "../utils/aria.js";
|
|
9
|
-
import { EXPLORBOT_ATTRS, HTML_COMPOSITE_AREA_HINTS, HTML_COMPOSITE_TARGET_ROLES, HTML_EXTRACTION_LIMITS, HTML_FORM_CONTROL_ROLES, HTML_FORM_CONTROL_TAGS, HTML_INTERACTIVE_ROLES, HTML_SELECTORS,
|
|
9
|
+
import { EXPLORBOT_ATTRS, HTML_COMPOSITE_AREA_HINTS, HTML_COMPOSITE_TARGET_ROLES, HTML_EXTRACTION_LIMITS, HTML_FORM_CONTROL_ROLES, HTML_FORM_CONTROL_TAGS, HTML_INTERACTIVE_ROLES, HTML_SELECTORS, getComponentScopeHtmlExtractorSource, inferHtmlRole } from "../utils/html.js";
|
|
10
10
|
import { createDebug, tag } from "../utils/logger.js";
|
|
11
11
|
import { loop, pause } from "../utils/loop.js";
|
|
12
|
-
import { OVERLAY_SELECTORS } from "../utils/overlay.js";
|
|
13
12
|
import { annotatePageElements } from "../utils/web-annotate.js";
|
|
14
13
|
import { eidxInContainer } from "../utils/web-eidx.js";
|
|
15
14
|
import { WebElement } from "../utils/web-element.js";
|
|
@@ -547,8 +546,12 @@ export class Driller extends TaskAgent {
|
|
|
547
546
|
async detectNestedOverlayContext(component, result) {
|
|
548
547
|
if (!result?.pageDiff?.ariaChanges || result.pageDiff.urlChanged)
|
|
549
548
|
return null;
|
|
550
|
-
const
|
|
551
|
-
|
|
549
|
+
const parts = result.pageDiff.htmlParts ?? [];
|
|
550
|
+
let appeared = parts.filter((part) => part.added?.length > 0);
|
|
551
|
+
if (result.pageDiff.areaOfInterest)
|
|
552
|
+
appeared = parts;
|
|
553
|
+
const appearedHtml = appeared.map((part) => part.subtree).join('\n');
|
|
554
|
+
if (!appearedHtml)
|
|
552
555
|
return null;
|
|
553
556
|
const state = this.stateManager.getCurrentState();
|
|
554
557
|
if (!state)
|
|
@@ -561,7 +564,7 @@ export class Driller extends TaskAgent {
|
|
|
561
564
|
Keep the recorded code reusable and include the parent-opening action when the nested element requires the overlay to be open.
|
|
562
565
|
|
|
563
566
|
<overlay_html>
|
|
564
|
-
${
|
|
567
|
+
${appearedHtml}
|
|
565
568
|
</overlay_html>
|
|
566
569
|
|
|
567
570
|
<current_page_aria>
|
|
@@ -570,21 +573,6 @@ export class Driller extends TaskAgent {
|
|
|
570
573
|
</nested_overlay>
|
|
571
574
|
`;
|
|
572
575
|
}
|
|
573
|
-
async getVisibleOverlayHtml() {
|
|
574
|
-
return this.explorer.withPage((page) => page.evaluate(({ extractorSource, config }) => {
|
|
575
|
-
const extract = new Function(`return ${extractorSource}`)();
|
|
576
|
-
return extract(config);
|
|
577
|
-
}, {
|
|
578
|
-
extractorSource: getVisibleOverlayHtmlExtractorSource(),
|
|
579
|
-
config: {
|
|
580
|
-
interactiveContentSelector: HTML_SELECTORS.interactiveContent,
|
|
581
|
-
limits: HTML_EXTRACTION_LIMITS,
|
|
582
|
-
overlaySelectors: OVERLAY_SELECTORS.semanticOverlays,
|
|
583
|
-
overlaySemanticSelector: OVERLAY_SELECTORS.overlaySemanticSelector,
|
|
584
|
-
visibilityLimits: HTML_VISIBILITY_LIMITS,
|
|
585
|
-
},
|
|
586
|
-
}));
|
|
587
|
-
}
|
|
588
576
|
async getComponentScopeHtml(component, originalState) {
|
|
589
577
|
const scopedHtml = await this.explorer.withPage((page) => page.evaluate(({ eidx, extractorSource, config }) => {
|
|
590
578
|
const extract = new Function(`return ${extractorSource}`)();
|
|
@@ -74,6 +74,7 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
|
|
|
74
74
|
statusText?: undefined;
|
|
75
75
|
category?: undefined;
|
|
76
76
|
errorPreview?: undefined;
|
|
77
|
+
extracted?: undefined;
|
|
77
78
|
} | {
|
|
78
79
|
success: boolean;
|
|
79
80
|
status: number;
|
|
@@ -81,9 +82,11 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
|
|
|
81
82
|
category: ResponseCategory;
|
|
82
83
|
errorPreview: string;
|
|
83
84
|
error?: undefined;
|
|
85
|
+
extracted?: undefined;
|
|
84
86
|
} | {
|
|
85
87
|
success: boolean;
|
|
86
88
|
status: number;
|
|
89
|
+
extracted: Record<string, any>;
|
|
87
90
|
error?: undefined;
|
|
88
91
|
statusText?: undefined;
|
|
89
92
|
category?: undefined;
|
|
@@ -102,6 +105,10 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
|
|
|
102
105
|
}[];
|
|
103
106
|
}, {
|
|
104
107
|
finished: boolean;
|
|
108
|
+
error: string;
|
|
109
|
+
} | {
|
|
110
|
+
finished: boolean;
|
|
111
|
+
error?: undefined;
|
|
105
112
|
}, import("@ai-sdk/provider-utils").Context>>;
|
|
106
113
|
stop: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
|
|
107
114
|
reason: any;
|
|
@@ -111,6 +118,7 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
|
|
|
111
118
|
};
|
|
112
119
|
getResult: () => FishermanResult;
|
|
113
120
|
isFinished: () => boolean;
|
|
121
|
+
finishFromText: (text?: string) => void;
|
|
114
122
|
};
|
|
115
123
|
export interface FishermanResult {
|
|
116
124
|
success: boolean;
|
|
@@ -119,6 +127,7 @@ export interface FishermanResult {
|
|
|
119
127
|
type: string;
|
|
120
128
|
id?: string | number;
|
|
121
129
|
title?: string;
|
|
130
|
+
request?: string;
|
|
122
131
|
}>;
|
|
123
132
|
failed: Array<{
|
|
124
133
|
type: string;
|