explorbot 0.1.26 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/package.json
CHANGED
|
@@ -46,6 +46,14 @@ export function getCachedResearch(hash) {
|
|
|
46
46
|
memoryCacheTimestamps[hash] = now;
|
|
47
47
|
return cached;
|
|
48
48
|
}
|
|
49
|
+
export function getPreviousResearch(hash) {
|
|
50
|
+
if (!hash)
|
|
51
|
+
return '';
|
|
52
|
+
const researchFile = outputPath('research', `${hash}.md`);
|
|
53
|
+
if (!existsSync(researchFile))
|
|
54
|
+
return '';
|
|
55
|
+
return readFileSync(researchFile, 'utf8');
|
|
56
|
+
}
|
|
49
57
|
export function saveResearch(hash, text, combinedHtml) {
|
|
50
58
|
const researchDir = outputPath('research');
|
|
51
59
|
const researchFile = join(researchDir, `${hash}.md`);
|
|
@@ -2,9 +2,10 @@ import dedent from 'dedent';
|
|
|
2
2
|
import { ActionResult } from '../../action-result.js';
|
|
3
3
|
import { executionController } from "../../execution-controller.js";
|
|
4
4
|
import { detectFocusArea, diffAriaSnapshots } from "../../utils/aria.js";
|
|
5
|
+
import { extractCodeBlocks } from "../../utils/code-extractor.js";
|
|
5
6
|
import { tag } from '../../utils/logger.js';
|
|
6
7
|
import { mdq } from "../../utils/markdown-query.js";
|
|
7
|
-
import { getCachedResearch, saveResearch } from "./cache.js";
|
|
8
|
+
import { getCachedResearch, getPreviousResearch, saveResearch } from "./cache.js";
|
|
8
9
|
import { debugLog } from "./mixin.js";
|
|
9
10
|
import { parseResearchSections } from "./parser.js";
|
|
10
11
|
const DEFAULT_MAX_EXPANDABLE_CLICKS = 10;
|
|
@@ -13,13 +14,26 @@ export function WithDeepAnalysis(Base) {
|
|
|
13
14
|
async performDeepAnalysis(state, result) {
|
|
14
15
|
tag('info').log('Starting deep analysis of expandable elements');
|
|
15
16
|
await this.navigateTo(state.fullUrl || state.url);
|
|
16
|
-
let expandables = await this._discoverExpandables(result.text);
|
|
17
|
-
if (expandables.length === 0) {
|
|
18
|
-
tag('info').log('No expandable elements identified by AI');
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
tag('substep').log(`Identified ${expandables.length} expandable elements`);
|
|
22
17
|
const maxClicks = this.explorer.getConfig().ai?.agents?.researcher?.maxExpandableClicks ?? DEFAULT_MAX_EXPANDABLE_CLICKS;
|
|
18
|
+
const expandedSections = [];
|
|
19
|
+
const navigationLinks = [];
|
|
20
|
+
let verifiedCodes = [];
|
|
21
|
+
let missing = [];
|
|
22
|
+
const previousSections = this._loadPreviousExtendedSections(state.hash || '');
|
|
23
|
+
if (previousSections.length > 0) {
|
|
24
|
+
tag('substep').log(`Replaying ${previousSections.length} previously discovered sections`);
|
|
25
|
+
const replay = await this._replayPreviousSections(state, previousSections, maxClicks);
|
|
26
|
+
expandedSections.push(...replay.verified);
|
|
27
|
+
verifiedCodes = replay.verifiedCodes;
|
|
28
|
+
missing = replay.missing;
|
|
29
|
+
tag('info').log(`Reused ${replay.verified.length}/${previousSections.length} previous sections, ${missing.length} to re-discover`);
|
|
30
|
+
if (missing.length === 0 && replay.verified.length >= maxClicks) {
|
|
31
|
+
tag('info').log('Page appears unchanged, reusing previous sections and skipping discovery');
|
|
32
|
+
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
let expandables = await this._discoverExpandables(result.text, missing, verifiedCodes);
|
|
23
37
|
if (expandables.length > maxClicks) {
|
|
24
38
|
expandables = await this._selectExpandables(expandables, state.fullUrl || state.url, maxClicks);
|
|
25
39
|
tag('substep').log(`Selected ${expandables.length} expandables to click (max: ${maxClicks})`);
|
|
@@ -29,9 +43,11 @@ export function WithDeepAnalysis(Base) {
|
|
|
29
43
|
commands: this._buildClickCommands(el),
|
|
30
44
|
description: el.name,
|
|
31
45
|
}))
|
|
32
|
-
.filter((el) => el.commands.length > 0)
|
|
46
|
+
.filter((el) => el.commands.length > 0)
|
|
47
|
+
.filter((el) => !el.commands.some((cmd) => verifiedCodes.includes(cmd)));
|
|
33
48
|
if (elements.length === 0) {
|
|
34
|
-
tag('info').log('No
|
|
49
|
+
tag('info').log('No new expandable elements to click');
|
|
50
|
+
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
35
51
|
return;
|
|
36
52
|
}
|
|
37
53
|
const expandableRows = elements.map((el) => `| ${el.description} | \`${el.commands[0]}\` |`).join('\n');
|
|
@@ -39,21 +55,9 @@ export function WithDeepAnalysis(Base) {
|
|
|
39
55
|
for (const el of elements)
|
|
40
56
|
debugLog(`Expandable: ${el.description} → ${el.commands[0]}`);
|
|
41
57
|
tag('substep').log(`Clicking ${elements.length} expandable elements`);
|
|
42
|
-
const expandedSections = [];
|
|
43
|
-
const navigationLinks = [];
|
|
44
58
|
await this._clickExpandableElements(elements, state, expandedSections, navigationLinks);
|
|
45
59
|
tag('info').log(`Deep analysis complete. Sections: ${expandedSections.length}, navigation links: ${navigationLinks.length}`);
|
|
46
|
-
|
|
47
|
-
if (dedupedSections.length !== expandedSections.length) {
|
|
48
|
-
tag('substep').log(`Deduplicated ${expandedSections.length} → ${dedupedSections.length} extended sections`);
|
|
49
|
-
}
|
|
50
|
-
if (dedupedSections.length > 0) {
|
|
51
|
-
result.text += `\n\n# Extended Research\n\n${dedupedSections.join('\n\n---\n\n')}`;
|
|
52
|
-
}
|
|
53
|
-
if (navigationLinks.length > 0) {
|
|
54
|
-
const links = navigationLinks.map((l) => `- \`${l.code}\` opens ${l.url}`).join('\n');
|
|
55
|
-
result.text += `\n\n## Navigation Links\n\n${links}`;
|
|
56
|
-
}
|
|
60
|
+
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
57
61
|
}
|
|
58
62
|
async researchOverlay(current, previous, pageStateHash) {
|
|
59
63
|
const focusArea = detectFocusArea(current.ariaSnapshot);
|
|
@@ -97,7 +101,66 @@ export function WithDeepAnalysis(Base) {
|
|
|
97
101
|
tag('substep').log(`Overlay research appended: ${focusArea.name}`);
|
|
98
102
|
return sectionMarkdown;
|
|
99
103
|
}
|
|
100
|
-
|
|
104
|
+
_loadPreviousExtendedSections(hash) {
|
|
105
|
+
if (!hash)
|
|
106
|
+
return [];
|
|
107
|
+
const previous = getPreviousResearch(hash);
|
|
108
|
+
if (!previous)
|
|
109
|
+
return [];
|
|
110
|
+
const sections = [];
|
|
111
|
+
for (const section of parseResearchSections(previous)) {
|
|
112
|
+
if (!section.isExtended)
|
|
113
|
+
continue;
|
|
114
|
+
const code = extractCodeBlocks(section.rawMarkdown)[0];
|
|
115
|
+
if (!code)
|
|
116
|
+
continue;
|
|
117
|
+
sections.push({ name: section.name, code });
|
|
118
|
+
}
|
|
119
|
+
return sections;
|
|
120
|
+
}
|
|
121
|
+
async _replayPreviousSections(state, prevSections, maxClicks) {
|
|
122
|
+
const originalAria = state.ariaSnapshot || '';
|
|
123
|
+
const verified = [];
|
|
124
|
+
const verifiedCodes = [];
|
|
125
|
+
const missing = [];
|
|
126
|
+
for (const section of prevSections.slice(0, maxClicks)) {
|
|
127
|
+
if (executionController.isInterrupted())
|
|
128
|
+
break;
|
|
129
|
+
let outcome;
|
|
130
|
+
try {
|
|
131
|
+
outcome = await this._executeAndAnalyze([section.code], section.name, state, originalAria, this._summarizeExpanded(verified));
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
tag('warning').log(`Replay failed for "${section.name}": ${err instanceof Error ? err.message : err}`);
|
|
135
|
+
await this._restorePageState(state.url, originalAria).catch(() => { });
|
|
136
|
+
missing.push(section);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (outcome.status === 'revealed') {
|
|
140
|
+
verified.push(outcome.sectionMarkdown);
|
|
141
|
+
verifiedCodes.push(section.code);
|
|
142
|
+
debugLog(`Replayed and verified section: ${section.name}`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
debugLog(`Could not replay previous section: ${section.name}`);
|
|
146
|
+
missing.push(section);
|
|
147
|
+
}
|
|
148
|
+
return { verified, verifiedCodes, missing };
|
|
149
|
+
}
|
|
150
|
+
_appendExtendedResearch(result, expandedSections, navigationLinks) {
|
|
151
|
+
const dedupedSections = this._deduplicateExpandedSections(expandedSections);
|
|
152
|
+
if (dedupedSections.length !== expandedSections.length) {
|
|
153
|
+
tag('substep').log(`Deduplicated ${expandedSections.length} → ${dedupedSections.length} extended sections`);
|
|
154
|
+
}
|
|
155
|
+
if (dedupedSections.length > 0) {
|
|
156
|
+
result.text += `\n\n# Extended Research\n\n${dedupedSections.join('\n\n---\n\n')}`;
|
|
157
|
+
}
|
|
158
|
+
if (navigationLinks.length > 0) {
|
|
159
|
+
const links = navigationLinks.map((l) => `- \`${l.code}\` opens ${l.url}`).join('\n');
|
|
160
|
+
result.text += `\n\n## Navigation Links\n\n${links}`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async _discoverExpandables(researchText, missing = [], verifiedCodes = []) {
|
|
101
164
|
const allElements = new Map();
|
|
102
165
|
for (const section of parseResearchSections(researchText)) {
|
|
103
166
|
for (const el of section.elements) {
|
|
@@ -108,6 +171,15 @@ export function WithDeepAnalysis(Base) {
|
|
|
108
171
|
if (allElements.size === 0)
|
|
109
172
|
return [];
|
|
110
173
|
const eidxList = [...allElements.keys()].join(', ');
|
|
174
|
+
let missingHint = '';
|
|
175
|
+
if (missing.length > 0) {
|
|
176
|
+
const list = missing.map((s) => `- "${s.name}" (previously revealed via ${s.code})`).join('\n');
|
|
177
|
+
missingHint = dedent `
|
|
178
|
+
|
|
179
|
+
These sections were present on a previous visit but their trigger could not be replayed now — the element may have moved or been renamed. Prioritize finding the element that now reveals each:
|
|
180
|
+
${list}
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
111
183
|
const textPrompt = dedent `
|
|
112
184
|
From this UI research, identify elements that could reveal hidden UI when clicked
|
|
113
185
|
(dropdown menus, popups, expandable panels, accordion sections, overflow menus, tab switches).
|
|
@@ -115,6 +187,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
115
187
|
Available eidx refs: ${eidxList}
|
|
116
188
|
|
|
117
189
|
${researchText}
|
|
190
|
+
${missingHint}
|
|
118
191
|
|
|
119
192
|
Rules:
|
|
120
193
|
- Only pick elements that HIDE content until clicked (menus, dropdowns, accordions, tabs)
|
|
@@ -136,6 +209,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
136
209
|
|
|
137
210
|
Look for: overflow/ellipsis menus, chevron dropdowns, hamburger menus,
|
|
138
211
|
gear/settings buttons, accordion toggles, tab switches, filter buttons.
|
|
212
|
+
${missingHint}
|
|
139
213
|
|
|
140
214
|
Rules:
|
|
141
215
|
- For repeated icons (same icon on every list row), pick only the FIRST one
|
|
@@ -258,48 +332,15 @@ export function WithDeepAnalysis(Base) {
|
|
|
258
332
|
await this._restorePageState(state.url, originalAria);
|
|
259
333
|
}
|
|
260
334
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (await action.attempt(cmd, undefined, false)) {
|
|
265
|
-
clickCode = cmd;
|
|
266
|
-
break;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
if (!clickCode) {
|
|
270
|
-
debugLog(`Click failed: ${el.description.slice(0, 80)}`);
|
|
335
|
+
const outcome = await this._executeAndAnalyze(el.commands, el.description, state, originalAria, this._summarizeExpanded(expandedSections));
|
|
336
|
+
if (outcome.status === 'navigated') {
|
|
337
|
+
navigationLinks.push({ code: outcome.code, url: outcome.url });
|
|
271
338
|
continue;
|
|
272
339
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
try {
|
|
276
|
-
await this.explorer.createAction().capturePageState();
|
|
277
|
-
const currAR = ActionResult.fromState(this.stateManager.getCurrentState());
|
|
278
|
-
diff = await currAR.diff(previousState);
|
|
279
|
-
await diff.calculate();
|
|
280
|
-
}
|
|
281
|
-
catch (err) {
|
|
282
|
-
tag('warning').log(`State capture failed after click: ${err instanceof Error ? err.message : err}`);
|
|
283
|
-
await this._restorePageState(state.url, originalAria);
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
if (diff.urlHasChanged()) {
|
|
287
|
-
debugLog(`Click navigated to ${this.stateManager.getCurrentState()?.url}`);
|
|
288
|
-
navigationLinks.push({ code: clickCode, url: this.stateManager.getCurrentState()?.url || '' });
|
|
289
|
-
await this.navigateTo(state.url);
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
const clickHtmlSize = diff.htmlParts.reduce((sum, p) => sum + p.subtree.length, 0);
|
|
293
|
-
if (!diff.ariaChanged && clickHtmlSize <= 150) {
|
|
294
|
-
debugLog(`No changes from: ${el.description.slice(0, 80)}`);
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
const sectionMarkdown = await this._analyzeExpandedAction(clickCode, el.description, diff, this._summarizeExpanded(expandedSections));
|
|
298
|
-
if (sectionMarkdown) {
|
|
299
|
-
expandedSections.push(sectionMarkdown);
|
|
340
|
+
if (outcome.status === 'revealed') {
|
|
341
|
+
expandedSections.push(outcome.sectionMarkdown);
|
|
300
342
|
debugLog(`Captured section from: ${el.description.slice(0, 80)}`);
|
|
301
343
|
}
|
|
302
|
-
await this._restorePageState(state.url, originalAria);
|
|
303
344
|
}
|
|
304
345
|
catch (err) {
|
|
305
346
|
tag('warning').log(`Expandable click failed for "${el.description.slice(0, 80)}": ${err instanceof Error ? err.message : err}`);
|
|
@@ -310,6 +351,50 @@ export function WithDeepAnalysis(Base) {
|
|
|
310
351
|
}
|
|
311
352
|
}
|
|
312
353
|
}
|
|
354
|
+
async _executeAndAnalyze(commands, description, state, originalAria, alreadyExpanded) {
|
|
355
|
+
const previousState = ActionResult.fromState(this.stateManager.getCurrentState());
|
|
356
|
+
let clickCode = null;
|
|
357
|
+
const action = this.explorer.createAction();
|
|
358
|
+
for (const cmd of commands) {
|
|
359
|
+
if (await action.attempt(cmd, undefined, false)) {
|
|
360
|
+
clickCode = cmd;
|
|
361
|
+
break;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (!clickCode) {
|
|
365
|
+
debugLog(`Click failed: ${description.slice(0, 80)}`);
|
|
366
|
+
return { status: 'failed' };
|
|
367
|
+
}
|
|
368
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
369
|
+
let diff;
|
|
370
|
+
try {
|
|
371
|
+
await this.explorer.createAction().capturePageState();
|
|
372
|
+
const currAR = ActionResult.fromState(this.stateManager.getCurrentState());
|
|
373
|
+
diff = await currAR.diff(previousState);
|
|
374
|
+
await diff.calculate();
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
tag('warning').log(`State capture failed after click: ${err instanceof Error ? err.message : err}`);
|
|
378
|
+
await this._restorePageState(state.url, originalAria);
|
|
379
|
+
return { status: 'failed' };
|
|
380
|
+
}
|
|
381
|
+
if (diff.urlHasChanged()) {
|
|
382
|
+
const url = this.stateManager.getCurrentState()?.url || '';
|
|
383
|
+
debugLog(`Click navigated to ${url}`);
|
|
384
|
+
await this.navigateTo(state.url);
|
|
385
|
+
return { status: 'navigated', code: clickCode, url };
|
|
386
|
+
}
|
|
387
|
+
const clickHtmlSize = diff.htmlParts.reduce((sum, p) => sum + p.subtree.length, 0);
|
|
388
|
+
if (!diff.ariaChanged && clickHtmlSize <= 150) {
|
|
389
|
+
debugLog(`No changes from: ${description.slice(0, 80)}`);
|
|
390
|
+
return { status: 'none', code: clickCode };
|
|
391
|
+
}
|
|
392
|
+
const sectionMarkdown = await this._analyzeExpandedAction(clickCode, description, diff, alreadyExpanded);
|
|
393
|
+
await this._restorePageState(state.url, originalAria);
|
|
394
|
+
if (!sectionMarkdown)
|
|
395
|
+
return { status: 'none', code: clickCode };
|
|
396
|
+
return { status: 'revealed', code: clickCode, sectionMarkdown };
|
|
397
|
+
}
|
|
313
398
|
async _restorePageState(url, originalAria) {
|
|
314
399
|
try {
|
|
315
400
|
await this.cancelInUi();
|
package/package.json
CHANGED
|
@@ -49,6 +49,13 @@ export function getCachedResearch(hash: string): string {
|
|
|
49
49
|
return cached;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export function getPreviousResearch(hash: string): string {
|
|
53
|
+
if (!hash) return '';
|
|
54
|
+
const researchFile = outputPath('research', `${hash}.md`);
|
|
55
|
+
if (!existsSync(researchFile)) return '';
|
|
56
|
+
return readFileSync(researchFile, 'utf8');
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
export function saveResearch(hash: string, text: string, combinedHtml?: string): string {
|
|
53
60
|
const researchDir = outputPath('research');
|
|
54
61
|
const researchFile = join(researchDir, `${hash}.md`);
|
|
@@ -5,10 +5,11 @@ import type Explorer from '../../explorer.ts';
|
|
|
5
5
|
import type { StateManager } from '../../state-manager.js';
|
|
6
6
|
import { WebPageState } from '../../state-manager.js';
|
|
7
7
|
import { detectFocusArea, diffAriaSnapshots } from '../../utils/aria.ts';
|
|
8
|
+
import { extractCodeBlocks } from '../../utils/code-extractor.ts';
|
|
8
9
|
import { tag } from '../../utils/logger.js';
|
|
9
10
|
import { mdq } from '../../utils/markdown-query.ts';
|
|
10
11
|
import type { Provider } from '../provider.js';
|
|
11
|
-
import { getCachedResearch, saveResearch } from './cache.ts';
|
|
12
|
+
import { getCachedResearch, getPreviousResearch, saveResearch } from './cache.ts';
|
|
12
13
|
import { type Constructor, debugLog } from './mixin.ts';
|
|
13
14
|
import { type ResearchElement, parseResearchSections } from './parser.ts';
|
|
14
15
|
import type { ResearchResult } from './research-result.ts';
|
|
@@ -26,14 +27,30 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
26
27
|
tag('info').log('Starting deep analysis of expandable elements');
|
|
27
28
|
await (this as any).navigateTo(state.fullUrl || state.url);
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
const maxClicks = (this.explorer.getConfig().ai?.agents?.researcher as any)?.maxExpandableClicks ?? DEFAULT_MAX_EXPANDABLE_CLICKS;
|
|
31
|
+
|
|
32
|
+
const expandedSections: string[] = [];
|
|
33
|
+
const navigationLinks: Array<{ code: string; url: string }> = [];
|
|
34
|
+
let verifiedCodes: string[] = [];
|
|
35
|
+
let missing: PreviousSection[] = [];
|
|
36
|
+
|
|
37
|
+
const previousSections = this._loadPreviousExtendedSections(state.hash || '');
|
|
38
|
+
if (previousSections.length > 0) {
|
|
39
|
+
tag('substep').log(`Replaying ${previousSections.length} previously discovered sections`);
|
|
40
|
+
const replay = await this._replayPreviousSections(state, previousSections, maxClicks);
|
|
41
|
+
expandedSections.push(...replay.verified);
|
|
42
|
+
verifiedCodes = replay.verifiedCodes;
|
|
43
|
+
missing = replay.missing;
|
|
44
|
+
tag('info').log(`Reused ${replay.verified.length}/${previousSections.length} previous sections, ${missing.length} to re-discover`);
|
|
45
|
+
|
|
46
|
+
if (missing.length === 0 && replay.verified.length >= maxClicks) {
|
|
47
|
+
tag('info').log('Page appears unchanged, reusing previous sections and skipping discovery');
|
|
48
|
+
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
33
51
|
}
|
|
34
|
-
tag('substep').log(`Identified ${expandables.length} expandable elements`);
|
|
35
52
|
|
|
36
|
-
|
|
53
|
+
let expandables = await this._discoverExpandables(result.text, missing, verifiedCodes);
|
|
37
54
|
if (expandables.length > maxClicks) {
|
|
38
55
|
expandables = await this._selectExpandables(expandables, state.fullUrl || state.url, maxClicks);
|
|
39
56
|
tag('substep').log(`Selected ${expandables.length} expandables to click (max: ${maxClicks})`);
|
|
@@ -44,10 +61,12 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
44
61
|
commands: this._buildClickCommands(el),
|
|
45
62
|
description: el.name,
|
|
46
63
|
}))
|
|
47
|
-
.filter((el) => el.commands.length > 0)
|
|
64
|
+
.filter((el) => el.commands.length > 0)
|
|
65
|
+
.filter((el) => !el.commands.some((cmd) => verifiedCodes.includes(cmd)));
|
|
48
66
|
|
|
49
67
|
if (elements.length === 0) {
|
|
50
|
-
tag('info').log('No
|
|
68
|
+
tag('info').log('No new expandable elements to click');
|
|
69
|
+
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
51
70
|
return;
|
|
52
71
|
}
|
|
53
72
|
|
|
@@ -57,25 +76,11 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
57
76
|
for (const el of elements) debugLog(`Expandable: ${el.description} → ${el.commands[0]}`);
|
|
58
77
|
tag('substep').log(`Clicking ${elements.length} expandable elements`);
|
|
59
78
|
|
|
60
|
-
const expandedSections: string[] = [];
|
|
61
|
-
const navigationLinks: Array<{ code: string; url: string }> = [];
|
|
62
|
-
|
|
63
79
|
await this._clickExpandableElements(elements, state, expandedSections, navigationLinks);
|
|
64
80
|
|
|
65
81
|
tag('info').log(`Deep analysis complete. Sections: ${expandedSections.length}, navigation links: ${navigationLinks.length}`);
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
if (dedupedSections.length !== expandedSections.length) {
|
|
69
|
-
tag('substep').log(`Deduplicated ${expandedSections.length} → ${dedupedSections.length} extended sections`);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (dedupedSections.length > 0) {
|
|
73
|
-
result.text += `\n\n# Extended Research\n\n${dedupedSections.join('\n\n---\n\n')}`;
|
|
74
|
-
}
|
|
75
|
-
if (navigationLinks.length > 0) {
|
|
76
|
-
const links = navigationLinks.map((l) => `- \`${l.code}\` opens ${l.url}`).join('\n');
|
|
77
|
-
result.text += `\n\n## Navigation Links\n\n${links}`;
|
|
78
|
-
}
|
|
83
|
+
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
async researchOverlay(current: ActionResult, previous: ActionResult, pageStateHash: string): Promise<string | null> {
|
|
@@ -127,7 +132,70 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
127
132
|
return sectionMarkdown;
|
|
128
133
|
}
|
|
129
134
|
|
|
130
|
-
private
|
|
135
|
+
private _loadPreviousExtendedSections(hash: string): PreviousSection[] {
|
|
136
|
+
if (!hash) return [];
|
|
137
|
+
const previous = getPreviousResearch(hash);
|
|
138
|
+
if (!previous) return [];
|
|
139
|
+
|
|
140
|
+
const sections: PreviousSection[] = [];
|
|
141
|
+
for (const section of parseResearchSections(previous)) {
|
|
142
|
+
if (!section.isExtended) continue;
|
|
143
|
+
const code = extractCodeBlocks(section.rawMarkdown)[0];
|
|
144
|
+
if (!code) continue;
|
|
145
|
+
sections.push({ name: section.name, code });
|
|
146
|
+
}
|
|
147
|
+
return sections;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private async _replayPreviousSections(state: WebPageState, prevSections: PreviousSection[], maxClicks: number): Promise<{ verified: string[]; verifiedCodes: string[]; missing: PreviousSection[] }> {
|
|
151
|
+
const originalAria = state.ariaSnapshot || '';
|
|
152
|
+
const verified: string[] = [];
|
|
153
|
+
const verifiedCodes: string[] = [];
|
|
154
|
+
const missing: PreviousSection[] = [];
|
|
155
|
+
|
|
156
|
+
for (const section of prevSections.slice(0, maxClicks)) {
|
|
157
|
+
if (executionController.isInterrupted()) break;
|
|
158
|
+
|
|
159
|
+
let outcome: ExpansionOutcome;
|
|
160
|
+
try {
|
|
161
|
+
outcome = await this._executeAndAnalyze([section.code], section.name, state, originalAria, this._summarizeExpanded(verified));
|
|
162
|
+
} catch (err) {
|
|
163
|
+
tag('warning').log(`Replay failed for "${section.name}": ${err instanceof Error ? err.message : err}`);
|
|
164
|
+
await this._restorePageState(state.url, originalAria).catch(() => {});
|
|
165
|
+
missing.push(section);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (outcome.status === 'revealed') {
|
|
170
|
+
verified.push(outcome.sectionMarkdown);
|
|
171
|
+
verifiedCodes.push(section.code);
|
|
172
|
+
debugLog(`Replayed and verified section: ${section.name}`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
debugLog(`Could not replay previous section: ${section.name}`);
|
|
177
|
+
missing.push(section);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { verified, verifiedCodes, missing };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private _appendExtendedResearch(result: ResearchResult, expandedSections: string[], navigationLinks: Array<{ code: string; url: string }>): void {
|
|
184
|
+
const dedupedSections = this._deduplicateExpandedSections(expandedSections);
|
|
185
|
+
if (dedupedSections.length !== expandedSections.length) {
|
|
186
|
+
tag('substep').log(`Deduplicated ${expandedSections.length} → ${dedupedSections.length} extended sections`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (dedupedSections.length > 0) {
|
|
190
|
+
result.text += `\n\n# Extended Research\n\n${dedupedSections.join('\n\n---\n\n')}`;
|
|
191
|
+
}
|
|
192
|
+
if (navigationLinks.length > 0) {
|
|
193
|
+
const links = navigationLinks.map((l) => `- \`${l.code}\` opens ${l.url}`).join('\n');
|
|
194
|
+
result.text += `\n\n## Navigation Links\n\n${links}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private async _discoverExpandables(researchText: string, missing: PreviousSection[] = [], verifiedCodes: string[] = []): Promise<ExpandableElement[]> {
|
|
131
199
|
const allElements = new Map<string, ExpandableElement>();
|
|
132
200
|
for (const section of parseResearchSections(researchText)) {
|
|
133
201
|
for (const el of section.elements) {
|
|
@@ -138,6 +206,16 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
138
206
|
|
|
139
207
|
const eidxList = [...allElements.keys()].join(', ');
|
|
140
208
|
|
|
209
|
+
let missingHint = '';
|
|
210
|
+
if (missing.length > 0) {
|
|
211
|
+
const list = missing.map((s) => `- "${s.name}" (previously revealed via ${s.code})`).join('\n');
|
|
212
|
+
missingHint = dedent`
|
|
213
|
+
|
|
214
|
+
These sections were present on a previous visit but their trigger could not be replayed now — the element may have moved or been renamed. Prioritize finding the element that now reveals each:
|
|
215
|
+
${list}
|
|
216
|
+
`;
|
|
217
|
+
}
|
|
218
|
+
|
|
141
219
|
const textPrompt = dedent`
|
|
142
220
|
From this UI research, identify elements that could reveal hidden UI when clicked
|
|
143
221
|
(dropdown menus, popups, expandable panels, accordion sections, overflow menus, tab switches).
|
|
@@ -145,6 +223,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
145
223
|
Available eidx refs: ${eidxList}
|
|
146
224
|
|
|
147
225
|
${researchText}
|
|
226
|
+
${missingHint}
|
|
148
227
|
|
|
149
228
|
Rules:
|
|
150
229
|
- Only pick elements that HIDE content until clicked (menus, dropdowns, accordions, tabs)
|
|
@@ -168,6 +247,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
168
247
|
|
|
169
248
|
Look for: overflow/ellipsis menus, chevron dropdowns, hamburger menus,
|
|
170
249
|
gear/settings buttons, accordion toggles, tab switches, filter buttons.
|
|
250
|
+
${missingHint}
|
|
171
251
|
|
|
172
252
|
Rules:
|
|
173
253
|
- For repeated icons (same icon on every list row), pick only the FIRST one
|
|
@@ -302,53 +382,15 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
302
382
|
}
|
|
303
383
|
}
|
|
304
384
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (await action.attempt(cmd, undefined, false)) {
|
|
309
|
-
clickCode = cmd;
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
if (!clickCode) {
|
|
314
|
-
debugLog(`Click failed: ${el.description.slice(0, 80)}`);
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
319
|
-
|
|
320
|
-
let diff: Diff;
|
|
321
|
-
try {
|
|
322
|
-
await this.explorer.createAction().capturePageState();
|
|
323
|
-
const currAR = ActionResult.fromState(this.stateManager.getCurrentState()!);
|
|
324
|
-
diff = await currAR.diff(previousState);
|
|
325
|
-
await diff.calculate();
|
|
326
|
-
} catch (err) {
|
|
327
|
-
tag('warning').log(`State capture failed after click: ${err instanceof Error ? err.message : err}`);
|
|
328
|
-
await this._restorePageState(state.url, originalAria);
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (diff.urlHasChanged()) {
|
|
333
|
-
debugLog(`Click navigated to ${this.stateManager.getCurrentState()?.url}`);
|
|
334
|
-
navigationLinks.push({ code: clickCode, url: this.stateManager.getCurrentState()?.url || '' });
|
|
335
|
-
await (this as any).navigateTo(state.url);
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const clickHtmlSize = diff.htmlParts.reduce((sum, p) => sum + p.subtree.length, 0);
|
|
340
|
-
if (!diff.ariaChanged && clickHtmlSize <= 150) {
|
|
341
|
-
debugLog(`No changes from: ${el.description.slice(0, 80)}`);
|
|
385
|
+
const outcome = await this._executeAndAnalyze(el.commands, el.description, state, originalAria, this._summarizeExpanded(expandedSections));
|
|
386
|
+
if (outcome.status === 'navigated') {
|
|
387
|
+
navigationLinks.push({ code: outcome.code, url: outcome.url });
|
|
342
388
|
continue;
|
|
343
389
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (sectionMarkdown) {
|
|
347
|
-
expandedSections.push(sectionMarkdown);
|
|
390
|
+
if (outcome.status === 'revealed') {
|
|
391
|
+
expandedSections.push(outcome.sectionMarkdown);
|
|
348
392
|
debugLog(`Captured section from: ${el.description.slice(0, 80)}`);
|
|
349
393
|
}
|
|
350
|
-
|
|
351
|
-
await this._restorePageState(state.url, originalAria);
|
|
352
394
|
} catch (err) {
|
|
353
395
|
tag('warning').log(`Expandable click failed for "${el.description.slice(0, 80)}": ${err instanceof Error ? err.message : err}`);
|
|
354
396
|
try {
|
|
@@ -358,6 +400,55 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
358
400
|
}
|
|
359
401
|
}
|
|
360
402
|
|
|
403
|
+
private async _executeAndAnalyze(commands: string[], description: string, state: WebPageState, originalAria: string, alreadyExpanded: string[]): Promise<ExpansionOutcome> {
|
|
404
|
+
const previousState = ActionResult.fromState(this.stateManager.getCurrentState()!);
|
|
405
|
+
|
|
406
|
+
let clickCode: string | null = null;
|
|
407
|
+
const action = this.explorer.createAction();
|
|
408
|
+
for (const cmd of commands) {
|
|
409
|
+
if (await action.attempt(cmd, undefined, false)) {
|
|
410
|
+
clickCode = cmd;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (!clickCode) {
|
|
415
|
+
debugLog(`Click failed: ${description.slice(0, 80)}`);
|
|
416
|
+
return { status: 'failed' };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
420
|
+
|
|
421
|
+
let diff: Diff;
|
|
422
|
+
try {
|
|
423
|
+
await this.explorer.createAction().capturePageState();
|
|
424
|
+
const currAR = ActionResult.fromState(this.stateManager.getCurrentState()!);
|
|
425
|
+
diff = await currAR.diff(previousState);
|
|
426
|
+
await diff.calculate();
|
|
427
|
+
} catch (err) {
|
|
428
|
+
tag('warning').log(`State capture failed after click: ${err instanceof Error ? err.message : err}`);
|
|
429
|
+
await this._restorePageState(state.url, originalAria);
|
|
430
|
+
return { status: 'failed' };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (diff.urlHasChanged()) {
|
|
434
|
+
const url = this.stateManager.getCurrentState()?.url || '';
|
|
435
|
+
debugLog(`Click navigated to ${url}`);
|
|
436
|
+
await (this as any).navigateTo(state.url);
|
|
437
|
+
return { status: 'navigated', code: clickCode, url };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const clickHtmlSize = diff.htmlParts.reduce((sum, p) => sum + p.subtree.length, 0);
|
|
441
|
+
if (!diff.ariaChanged && clickHtmlSize <= 150) {
|
|
442
|
+
debugLog(`No changes from: ${description.slice(0, 80)}`);
|
|
443
|
+
return { status: 'none', code: clickCode };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const sectionMarkdown = await this._analyzeExpandedAction(clickCode, description, diff, alreadyExpanded);
|
|
447
|
+
await this._restorePageState(state.url, originalAria);
|
|
448
|
+
if (!sectionMarkdown) return { status: 'none', code: clickCode };
|
|
449
|
+
return { status: 'revealed', code: clickCode, sectionMarkdown };
|
|
450
|
+
}
|
|
451
|
+
|
|
361
452
|
private async _restorePageState(url: string, originalAria: string): Promise<void> {
|
|
362
453
|
try {
|
|
363
454
|
await (this as any).cancelInUi();
|
|
@@ -484,6 +575,13 @@ interface ExpandableElement extends ResearchElement {
|
|
|
484
575
|
container: string | null;
|
|
485
576
|
}
|
|
486
577
|
|
|
578
|
+
interface PreviousSection {
|
|
579
|
+
name: string;
|
|
580
|
+
code: string;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
type ExpansionOutcome = { status: 'revealed'; code: string; sectionMarkdown: string } | { status: 'navigated'; code: string; url: string } | { status: 'none'; code: string } | { status: 'failed' };
|
|
584
|
+
|
|
487
585
|
export interface DeepAnalysisMethods {
|
|
488
586
|
performDeepAnalysis(state: WebPageState, result: ResearchResult): Promise<void>;
|
|
489
587
|
researchOverlay(current: ActionResult, previous: ActionResult, pageStateHash: string): Promise<string | null>;
|