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.
@@ -1,4 +1,4 @@
1
- import { detectFocusArea } from '../../utils/aria.ts';
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
- export function detectFocusFromAria(ariaSnapshot: string | null, sections: ResearchSection[]): string | null {
14
- const focusArea = detectFocusArea(ariaSnapshot);
15
- if (!focusArea.detected) return null;
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
- if (focusArea.type === 'dialog' || focusArea.type === 'modal') {
18
- const dialogSection = sections.find((s) => s.containerCss && (s.containerCss.includes('[role="dialog"]') || s.containerCss.includes('[role="alertdialog"]') || s.containerCss.includes('[aria-modal')));
19
- if (dialogSection) return dialogSection.name;
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
- let merged = parts.join('\n\n');
58
- if (focusCss) merged += '\n\n> Focused: Focus';
59
- return merged;
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> {
@@ -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 { detectFocusFromAria, hasFocusedSection, markSectionAsFocused, pickDefaultFocusedSection } from './researcher/focus.ts';
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: parse AI declaration, then ARIA fallback
238
- const focusMatch = result.text.match(/^>\s*Focused:\s*(.+)/m);
239
- if (focusMatch) {
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 ariaSnapshot = this.actionResult?.getCompactARIA() || '';
246
- const focusedName = detectFocusFromAria(ariaSnapshot, sections);
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: ${error instanceof Error ? error.message : String(error)}`);
392
+ task.addNote(`Execution error: ${message}`);
392
393
  }
393
- stop();
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.