explorbot 0.1.15 → 0.1.17
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 +12 -1
- package/dist/bin/explorbot-cli.js +13 -1
- package/dist/package.json +1 -1
- package/dist/src/ai/pilot.js +3 -8
- package/dist/src/ai/researcher/focus.js +51 -10
- package/dist/src/ai/researcher/sections.js +8 -4
- package/dist/src/ai/researcher.js +9 -24
- package/dist/src/ai/tester.js +8 -2
- package/dist/src/commands/explore-command.js +359 -43
- package/dist/src/explorbot.js +19 -5
- package/dist/src/utils/test-plan-markdown.js +8 -1
- package/package.json +1 -1
- package/src/ai/pilot.ts +3 -8
- package/src/ai/researcher/focus.ts +57 -8
- package/src/ai/researcher/sections.ts +7 -3
- package/src/ai/researcher.ts +8 -23
- package/src/ai/tester.ts +8 -2
- package/src/commands/explore-command.ts +362 -42
- package/src/explorbot.ts +21 -7
- package/src/utils/test-plan-markdown.ts +8 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
2
|
import { mdq } from '../../utils/markdown-query.ts';
|
|
3
3
|
import type { ResearchSection } from './parser.ts';
|
|
4
4
|
import type { ResearchResult } from './research-result.ts';
|
|
@@ -10,16 +10,65 @@ export function hasFocusedSection(text: string): boolean {
|
|
|
10
10
|
return text.includes(FOCUSED_MARKER);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
interface FocusProbe {
|
|
14
|
+
name: string;
|
|
15
|
+
isDialog: boolean;
|
|
16
|
+
zIndex: number;
|
|
17
|
+
hasShadow: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function detectFocusedSection(page: Page, sections: ResearchSection[]): Promise<string | null> {
|
|
21
|
+
const candidates: FocusProbe[] = [];
|
|
22
|
+
|
|
23
|
+
for (const section of sections) {
|
|
24
|
+
if (!section.containerCss) continue;
|
|
25
|
+
const key = section.name.toLowerCase().replace(/^section:\s*/, '');
|
|
26
|
+
if (FOCUS_SKIP_SECTIONS.has(key)) continue;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const locator = page.locator(section.containerCss).first();
|
|
30
|
+
if (!(await locator.isVisible())) continue;
|
|
31
|
+
|
|
32
|
+
const probe = await locator.evaluate((el) => {
|
|
33
|
+
const dialogSelector = '[role="dialog"], [role="alertdialog"], [aria-modal="true"]';
|
|
34
|
+
const isDialog = el.matches(dialogSelector) || !!el.querySelector(dialogSelector);
|
|
35
|
+
|
|
36
|
+
let cur: Element | null = el;
|
|
37
|
+
let maxZ = 0;
|
|
38
|
+
while (cur && cur !== document.body) {
|
|
39
|
+
const cs = window.getComputedStyle(cur);
|
|
40
|
+
if (cs.position !== 'static') {
|
|
41
|
+
const z = Number.parseInt(cs.zIndex, 10);
|
|
42
|
+
if (!Number.isNaN(z) && z > maxZ) maxZ = z;
|
|
43
|
+
}
|
|
44
|
+
cur = cur.parentElement;
|
|
45
|
+
}
|
|
16
46
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
47
|
+
const shadow = window.getComputedStyle(el).boxShadow;
|
|
48
|
+
const hasShadow = !!shadow && shadow !== 'none';
|
|
49
|
+
|
|
50
|
+
return { isDialog, zIndex: maxZ, hasShadow };
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
candidates.push({ name: section.name, ...probe });
|
|
54
|
+
} catch {}
|
|
20
55
|
}
|
|
21
56
|
|
|
22
|
-
return null;
|
|
57
|
+
if (candidates.length === 0) return null;
|
|
58
|
+
|
|
59
|
+
const dialogs = candidates.filter((c) => c.isDialog);
|
|
60
|
+
const pool = dialogs.length > 0 ? dialogs : candidates;
|
|
61
|
+
|
|
62
|
+
const winner = pool.reduce<FocusProbe | null>((best, c) => {
|
|
63
|
+
if (!best) return c;
|
|
64
|
+
if (c.zIndex !== best.zIndex) return c.zIndex > best.zIndex ? c : best;
|
|
65
|
+
if (c.hasShadow !== best.hasShadow) return c.hasShadow ? c : best;
|
|
66
|
+
return best;
|
|
67
|
+
}, null);
|
|
68
|
+
|
|
69
|
+
if (!winner) return null;
|
|
70
|
+
if (dialogs.length === 0 && winner.zIndex === 0 && !winner.hasShadow) return null;
|
|
71
|
+
return winner.name;
|
|
23
72
|
}
|
|
24
73
|
|
|
25
74
|
export function markSectionAsFocused(result: ResearchResult, sectionName: string): void {
|
|
@@ -7,7 +7,9 @@ import { tag } from '../../utils/logger.js';
|
|
|
7
7
|
import { RulesLoader } from '../../utils/rules-loader.ts';
|
|
8
8
|
import type { Provider } from '../provider.js';
|
|
9
9
|
import { locatorRule as generalLocatorRuleText } from '../rules.js';
|
|
10
|
+
import { markSectionAsFocused } from './focus.ts';
|
|
10
11
|
import type { Constructor } from './mixin.ts';
|
|
12
|
+
import { ResearchResult } from './research-result.ts';
|
|
11
13
|
|
|
12
14
|
export interface SectionMethods {
|
|
13
15
|
researchBySections(): Promise<string>;
|
|
@@ -54,9 +56,11 @@ export function WithSections<T extends Constructor>(Base: T) {
|
|
|
54
56
|
throw new Error('Per-section research produced no sections — AI responses all empty or NOT_PRESENT');
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
if (focusCss) merged
|
|
59
|
-
|
|
59
|
+
const merged = parts.join('\n\n');
|
|
60
|
+
if (!focusCss) return merged;
|
|
61
|
+
const focused = new ResearchResult(merged, this.actionResult?.url || '');
|
|
62
|
+
markSectionAsFocused(focused, 'Focus');
|
|
63
|
+
return focused.text;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
private async _detectFocusCss(): Promise<string | null> {
|
package/src/ai/researcher.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { ContextLengthError, type Provider } from './provider.js';
|
|
|
24
24
|
import { findSimilarResearch, getCachedResearch, saveResearch } from './researcher/cache.ts';
|
|
25
25
|
import { type CoordinateMethods, WithCoordinates } from './researcher/coordinates.ts';
|
|
26
26
|
import { type DeepAnalysisMethods, WithDeepAnalysis } from './researcher/deep-analysis.ts';
|
|
27
|
-
import {
|
|
27
|
+
import { detectFocusedSection, hasFocusedSection, markSectionAsFocused, pickDefaultFocusedSection } from './researcher/focus.ts';
|
|
28
28
|
import { type LocatorMethods, WithLocators } from './researcher/locators.ts';
|
|
29
29
|
import { extractValidContainers, formatResearchSummary, parseResearchSections } from './researcher/parser.ts';
|
|
30
30
|
import { ResearchResult } from './researcher/research-result.ts';
|
|
@@ -234,17 +234,12 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
234
234
|
await this.fixBrokenSections(result, activeConversation);
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
// Focused section:
|
|
238
|
-
|
|
239
|
-
if (
|
|
240
|
-
result.text = result.text.replace(focusMatch[0], '');
|
|
241
|
-
markSectionAsFocused(result, focusMatch[1].trim());
|
|
242
|
-
}
|
|
243
|
-
if (!hasFocusedSection(result.text)) {
|
|
237
|
+
// Focused section: unified Playwright probe (HTML+CSS+visibility).
|
|
238
|
+
// Must run BEFORE visuallyAnnotateContainers — annotation overlays inject z-index 99998+ which would pollute the scoring.
|
|
239
|
+
if (!interrupted() && this.hasScreenshotToAnalyze) {
|
|
244
240
|
const sections = parseResearchSections(result.text);
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
if (focusedName) markSectionAsFocused(result, focusedName);
|
|
241
|
+
const focused = await detectFocusedSection(this.explorer.playwrightHelper.page, sections);
|
|
242
|
+
if (focused) markSectionAsFocused(result, focused);
|
|
248
243
|
}
|
|
249
244
|
|
|
250
245
|
// Stage 4: Visual analysis
|
|
@@ -281,8 +276,8 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
281
276
|
await this.backfillBrokenLocators(result);
|
|
282
277
|
}
|
|
283
278
|
|
|
284
|
-
// Focused section: final fallback
|
|
285
|
-
if (!hasFocusedSection(result.text)) {
|
|
279
|
+
// Focused section: final fallback (vision-only — without a screenshot we don't infer focus)
|
|
280
|
+
if (this.hasScreenshotToAnalyze && !hasFocusedSection(result.text)) {
|
|
286
281
|
const sections = parseResearchSections(result.text);
|
|
287
282
|
const fallback = pickDefaultFocusedSection(sections);
|
|
288
283
|
if (fallback) markSectionAsFocused(result, fallback);
|
|
@@ -451,16 +446,6 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
451
446
|
|
|
452
447
|
| Element | ARIA | CSS | eidx |
|
|
453
448
|
</section_format>
|
|
454
|
-
|
|
455
|
-
<focused_section>
|
|
456
|
-
At the end of your output, declare the primary focus area on a single line:
|
|
457
|
-
|
|
458
|
-
> Focused: <exact section name>
|
|
459
|
-
|
|
460
|
-
- If a dialog/modal/drawer/overlay exists, it is focused.
|
|
461
|
-
- Otherwise pick the section where the main business action happens (list for catalog, detail for item page, content for article).
|
|
462
|
-
- Navigation and menu/toolbar are never focused.
|
|
463
|
-
</focused_section>
|
|
464
449
|
`;
|
|
465
450
|
}
|
|
466
451
|
|
package/src/ai/tester.ts
CHANGED
|
@@ -387,10 +387,15 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
387
387
|
: undefined,
|
|
388
388
|
catch: async ({ error, stop }) => {
|
|
389
389
|
tag('error').log(`Test execution error: ${error}`);
|
|
390
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
390
391
|
if (!task.hasFinished) {
|
|
391
|
-
task.addNote(`Execution error: ${
|
|
392
|
+
task.addNote(`Execution error: ${message}`);
|
|
392
393
|
}
|
|
393
|
-
|
|
394
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
395
|
+
stop();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
conversation.addUserText(`Previous AI call failed: ${message}. Take a different approach on the next step.`);
|
|
394
399
|
},
|
|
395
400
|
}
|
|
396
401
|
);
|
|
@@ -725,6 +730,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
725
730
|
- Use pressKey() for pressing special keys (Enter, Escape, Tab, Arrow keys) or key combinations with modifiers (Ctrl+A, Shift+Delete, etc.)
|
|
726
731
|
- Use container CSS locators from <page_ui_map> to interact with elements inside sections
|
|
727
732
|
- Systematically use record({ notes: ["..."] }) to write your findings, planned actions, observations, etc.
|
|
733
|
+
- When creating/editing/deleting a named entity, include its identifier verbatim in the note — Pilot uses it to confirm provenance.
|
|
728
734
|
- Call record({ notes: ["..."], status: "success" }) when you see success/info message on a page or when expected outcome is achieved
|
|
729
735
|
- Call record({ notes: ["..."], status: "fail" }) when an expected outcome cannot be achieved or has failed or you see error/alert/warning message on a page
|
|
730
736
|
- NEVER call record(status: "success") if your last verify() or see() call FAILED. A failed check means the outcome is NOT confirmed — use record(status: "fail") instead, or retry with a different approach.
|