explorbot 0.1.3 → 0.1.5

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.
@@ -0,0 +1,15 @@
1
+ <container_rules>
2
+ Container CSS must be a SINGLE semantic selector — one class, one id, or one attribute. No spaces, no combinators, no descendant paths.
3
+
4
+ - INVALID: bare tags (`div`, `section`, `nav`), combinators (`div > .content`, `.a .b`), layout/utility classes (flex-, col-, mt-, p-, bg-, text-, items-, rounded-)
5
+ - VALID: semantic class names that describe what the section IS (`.product-list`, `.sidebar-menu`, `.user-profile`, `.search-results`), semantic roles (`[role="dialog"]`), semantic ids (`#main-content`)
6
+
7
+ The container must uniquely identify a semantic wrapper, not a path through the DOM.
8
+ </container_rules>
9
+
10
+ <css_selector_rules>
11
+ CSS selectors inside the UI map must point to the actual interactive element (input, button, a, select), not to wrapper divs.
12
+
13
+ - Prefer distinguishing attributes on the interactive element (`type`, `value`, `name`, `href`, `aria-label`) over wrapper ids.
14
+ - For buttons with similar text, include `type` or `value` or form context to stay unique.
15
+ </css_selector_rules>
@@ -0,0 +1,12 @@
1
+ <section_example>
2
+ ## List
3
+
4
+ Product catalog showing available items with sorting and filtering.
5
+
6
+ > Container: '.product-list'
7
+
8
+ | Element | ARIA | CSS | eidx |
9
+ | 'Sort by price' | { role: 'button', text: 'Sort by price' } | '.sort-btn' | 3 |
10
+ | 'Add to cart' | { role: 'button', text: 'Add to cart' } | '.add-btn' | 4 |
11
+ | 'Product name' | { role: 'link', text: 'Widget Pro' } | 'a.product-link' | 5 |
12
+ </section_example>
@@ -17,10 +17,16 @@ function getStatesDir() {
17
17
  function getFingerprintWorker() {
18
18
  if (!fingerprintWorker) {
19
19
  const ext = import.meta.url.endsWith('.ts') ? '.ts' : '.js';
20
- fingerprintWorker = new Worker(new URL(`./fingerprint-worker${ext}`, import.meta.url).href);
20
+ fingerprintWorker = new Worker(new URL(`./fingerprint-worker${ext}`, import.meta.url));
21
21
  }
22
22
  return fingerprintWorker;
23
23
  }
24
+ export function clearResearchCache() {
25
+ for (const key of Object.keys(memoryCache))
26
+ delete memoryCache[key];
27
+ for (const key of Object.keys(memoryCacheTimestamps))
28
+ delete memoryCacheTimestamps[key];
29
+ }
24
30
  export function getCachedResearch(hash) {
25
31
  if (!hash)
26
32
  return '';
@@ -71,10 +71,6 @@ export class Researcher extends ResearcherBase {
71
71
  You are senior QA focused on exploritary testig of web application.
72
72
  </role>
73
73
 
74
- <wording>
75
- In the UI map and all descriptions, name concrete UI parts (visible labels, headings, regions, ARIA roles). Do not use vague placeholders like "the page", "the element", "the button", "the input", "the link", "the form", "the table", "the list", or "the item". Do not use filler such as "comprehensive", "All required", "All elements", or "All necessary".
76
- </wording>
77
-
78
74
  ${customPrompt || ''}
79
75
  `;
80
76
  }
@@ -135,6 +131,7 @@ export class Researcher extends ResearcherBase {
135
131
  const prompt = await this.buildResearchPrompt();
136
132
  conversation.addUserText(prompt);
137
133
  let invocationResult;
134
+ let activeConversation = conversation;
138
135
  try {
139
136
  invocationResult = await this.provider.invokeConversation(conversation, undefined, { agentName: 'researcher' });
140
137
  }
@@ -145,29 +142,16 @@ export class Researcher extends ResearcherBase {
145
142
  }
146
143
  throw error;
147
144
  }
148
- tag('warning').log('Output truncated, retrying with focused instructions...');
145
+ tag('warning').log('Output truncated, retrying with fresh focused conversation (ARIA only)...');
149
146
  retriesLeft = 0;
150
- conversation.addUserText(this.buildFocusedRetryPrompt());
151
- invocationResult = await this.provider.invokeConversation(conversation, undefined, { agentName: 'researcher' });
147
+ activeConversation = this.provider.startConversation(this.getSystemMessage(), 'researcher');
148
+ activeConversation.addUserText(this.buildFocusedRetryPrompt());
149
+ invocationResult = await this.provider.invokeConversation(activeConversation, undefined, { agentName: 'researcher' });
152
150
  }
153
151
  if (!invocationResult)
154
152
  throw new Error('Failed to get response from provider');
155
153
  const result = new ResearchResult(invocationResult.response.text, state.url);
156
154
  debugLog(`Original research response length: ${result.text.length} chars`);
157
- const errorSection = mdq(result.text).query('section("Error Page Detected")');
158
- if (errorSection.count() > 0) {
159
- if (result.text.length < 500) {
160
- if (!opts._skipErrorPageRetry && (await this.waitForPageLoad(screenshot))) {
161
- return this.research(state, { ...opts, force: true, _skipErrorPageRetry: true });
162
- }
163
- tag('warning').log(`AI detected error page at ${state.url}`);
164
- if (stateHash)
165
- saveResearch(stateHash, result.text);
166
- await this.hooksRunner.runAfterHook('researcher', state.url);
167
- return result.text;
168
- }
169
- result.text = errorSection.replace('');
170
- }
171
155
  const interrupted = () => executionController.isInterrupted();
172
156
  // Stage 2: Test containers + locators
173
157
  result.parseLocators();
@@ -204,7 +188,7 @@ export class Researcher extends ResearcherBase {
204
188
  }
205
189
  // Stage 3: Fix broken sections via AI conversation continuation
206
190
  if (!interrupted() && fix && result.locators.some((l) => l.valid === false)) {
207
- await this.fixBrokenSections(result, conversation);
191
+ await this.fixBrokenSections(result, activeConversation);
208
192
  }
209
193
  // Focused section: parse AI declaration, then ARIA fallback
210
194
  const focusMatch = result.text.match(/^>\s*Focused:\s*(.+)/m);
@@ -357,32 +341,29 @@ export class Researcher extends ResearcherBase {
357
341
  }
358
342
  researchRules() {
359
343
  const sections = this.getConfiguredSections();
344
+ const currentUrl = this.stateManager.getCurrentState()?.url || '';
360
345
  return dedent `
361
346
  <task>
362
- Examine the provided page and explain its main purpose from the user perspective.
363
- Identify the main user actions of this page.
364
- Break down the page by sections and identify structural patterns.
365
- Provide a comprehensive UI map report in markdown format.
347
+ Examine the page and explain its main purpose from the user perspective.
348
+ Identify the primary user actions and break the page into sections.
349
+ Provide a UI map report in markdown.
366
350
  </task>
367
351
 
368
352
  <rules>
369
353
  - Explain what the user can achieve on this page.
370
354
  - Focus on primary user actions and interactive elements only.
371
355
  - Research all menus and navigational areas.
372
- - Ignore purely decorative sidebars, footer-only links, and external links.
356
+ - Ignore decorative sidebars, footer-only links, and external links.
373
357
  - Detect layout patterns: list/detail split, 2-pane, or 3-pane layouts.
374
- - If multiple elements match, pick the element inside the most relevant section and closest to recent UI context.
375
- - UI map table must include ARIA and CSS for every element.
376
- - Every element MUST have a CSS selector. NEVER leave CSS as "-".
377
- - For icon-only buttons with empty aria-label, set ARIA to "-" but ALWAYS provide CSS.
378
- - NEVER skip elements that have an eidx attribute. Every element with eidx MUST appear in the UI map table, even if it has no text or accessible name. Describe icon-only elements using their SVG class (e.g., md-icon-dots-horizontal → "More actions (ellipsis)") or their visual appearance.
379
- - ARIA locator must be JSON with role and text keys (NOT "name").
380
- - Note elements likely to have hover interactions (elements with title attribute, aria-describedby, navigation menu items with submenus) and mark them with "(hover)" in the UI map.
358
+ - Every element with an eidx attribute MUST appear in the UI map describe icon-only buttons by their visual role.
359
+ - Every UI map row needs a CSS selector; ARIA may be "-" for icon-only buttons, CSS must never be "-".
360
+ - ARIA locator JSON uses keys "role" and "text" (NOT "name").
361
+ - Mark elements with likely hover interactions (title, aria-describedby, menu items with submenus) as "(hover)".
381
362
  </rules>
382
363
 
383
364
  ${generalLocatorRuleText}
384
365
 
385
- ${RulesLoader.loadRules('researcher', ['ui-map-table', 'list-element'], this.stateManager.getCurrentState()?.url || '')}
366
+ ${RulesLoader.loadRules('researcher', ['ui-map-table', 'list-element', 'container-rules'], currentUrl)}
386
367
 
387
368
  <section_identification>
388
369
  Identify page sections in this priority order:
@@ -390,23 +371,12 @@ export class Researcher extends ResearcherBase {
390
371
  .map(([name, description]) => `* ${name}: ${description}`)
391
372
  .join('\n')}
392
373
 
393
- - Sections can overlap, prefer more detailed sections over broader ones.
394
- - Never name a section "Focus" or "Focused" — describe what the section actually contains (e.g., "Detail", "Modal", "Form", "Content", "List").
395
- - If a proposed section is not relevant or not detected, do not include it.
396
- - Each section must have a container CSS locator.
397
- - UI map CSS locators must be relative to the section container.
374
+ - Sections can overlap; prefer more detailed sections over broader ones.
375
+ - Never name a section "Focus" or "Focused" — use what it contains (Detail, Modal, Form, Content, List).
376
+ - Omit sections that are not present or not relevant.
377
+ - Each section needs a container CSS locator; UI map CSS locators are relative to it.
398
378
  </section_identification>
399
379
 
400
- <container_rules>
401
- CRITICAL: Container CSS must be a SINGLE selector — one class, one ID, or one attribute.
402
- No spaces, no >, no combinators, no nesting.
403
- - INVALID: '.filterbar-filter-btn-div button', 'div.static nav', 'div > .content'
404
- - INVALID: 'div', 'section', 'nav', 'div:first' (bare tags are not containers)
405
- - INVALID: Tailwind/Bootstrap utility classes that describe layout or styling (e.g. flex-none, d-flex, col-md-6, items-center, mt-4, p-2, bg-white, text-sm, rounded-lg). These are visual, not semantic.
406
- - VALID: Semantic class names that describe WHAT the section IS — e.g. '.product-list', '.sidebar-menu', '.user-profile', '[role="dialog"]', '.search-results'
407
- Container must uniquely identify a semantic wrapper, not a path through the DOM.
408
- </container_rules>
409
-
410
380
  <section_format>
411
381
  ## Section Name
412
382
 
@@ -416,40 +386,16 @@ export class Researcher extends ResearcherBase {
416
386
 
417
387
  | Element | ARIA | CSS | eidx |
418
388
  </section_format>
419
- <section_example>
420
- ## List
421
-
422
- Product catalog showing available items with sorting and filtering.
423
-
424
- > Container: '.product-list'
425
-
426
- | Element | ARIA | CSS | eidx |
427
- | 'Sort by price' | { role: 'button', text: 'Sort by price' } | '.sort-btn' | 3 |
428
- | 'Add to cart' | { role: 'button', text: 'Add to cart' } | '.add-btn' | 4 |
429
- | 'Product name' | { role: 'link', text: 'Widget Pro' } | 'a.product-link' | 5 |
430
- </section_example>
431
389
 
432
390
  <focused_section>
433
- At the very end of your output, add a single line declaring which section is the user's primary focus area:
391
+ At the end of your output, declare the primary focus area on a single line:
434
392
 
435
393
  > Focused: <exact section name>
436
394
 
437
- Rules for determining the focused section:
438
- - If the page has a dialog, modal, drawer, or overlay that section is focused
439
- - If no overlay exists, pick the section where the user performs the main business action of this page (e.g., a list section for a catalog page, a detail section for an item page, a content section for an article page)
440
- - Navigation is NEVER focused — it exists on every page
441
- - Menu/toolbar is NEVER focused — it contains actions, not the main content
442
- - The focused section is the one the user came to this page to interact with
395
+ - If a dialog/modal/drawer/overlay exists, it is focused.
396
+ - Otherwise pick the section where the main business action happens (list for catalog, detail for item page, content for article).
397
+ - Navigation and menu/toolbar are never focused.
443
398
  </focused_section>
444
-
445
- <css_selector_rules>
446
- CSS selectors MUST point to the actual interactive element (input, button, a, select), NOT to container divs.
447
- - If a submit button is inside a wrapper div, target the input/button directly
448
- - Bad: '#submit-wrapper' (div container)
449
- - Good: '#submit-wrapper input[type="submit"]' or 'input[type="submit"][value="Submit"]'
450
- - For buttons with similar text, include distinguishing attributes like type, value, or form context
451
-
452
- </css_selector_rules>
453
399
  `;
454
400
  }
455
401
  async buildResearchPrompt() {
@@ -475,17 +421,6 @@ export class Researcher extends ResearcherBase {
475
421
  return dedent `
476
422
  Analyze this web page and provide a comprehensive research report in markdown format.
477
423
 
478
- <error_detection>
479
- IMPORTANT: First check if this looks like an error page (404, 500, access denied,
480
- not found, server error, forbidden, or similar). If so, respond ONLY with:
481
-
482
- ## Error Page Detected
483
- Type: [error type]
484
- Reason: [what indicates this is an error page]
485
-
486
- Then stop - do not provide normal research output for error pages.
487
- </error_detection>
488
-
489
424
  ${this.researchRules()}
490
425
 
491
426
  URL: ${this.actionResult.url || 'Unknown'}
@@ -537,11 +472,40 @@ export class Researcher extends ResearcherBase {
537
472
  `;
538
473
  }
539
474
  buildFocusedRetryPrompt() {
475
+ const currentUrl = this.stateManager.getCurrentState()?.url || '';
476
+ const example = RulesLoader.loadRules('researcher', ['section-example'], currentUrl);
477
+ const uiMapTable = RulesLoader.loadRules('researcher', ['ui-map-table'], currentUrl);
478
+ const url = this.actionResult?.url || 'Unknown';
479
+ const title = this.actionResult?.title || 'Unknown';
480
+ const aria = this.actionResult?.getCompactARIA() || '';
540
481
  return dedent `
541
- Your previous response was truncated and could not be parsed.
482
+ Previous response was truncated. Restart with a minimal output.
483
+
484
+ <task>
485
+ Output a UI map for ONE section only — the main interactive area of this page.
486
+ Skip navigation, sidebar, and footer. Max 15 elements.
487
+ Every element with an eidx MUST appear. Every row needs CSS; ARIA may be "-" for icon-only.
488
+ End with a single line: \`> Focused: <section name>\`.
489
+ </task>
490
+
491
+ <section_format>
492
+ ## Section Name
493
+
494
+ > Container: '.container-css-selector'
495
+
496
+ | Element | ARIA | CSS | eidx |
497
+ </section_format>
498
+
499
+ ${example}
500
+
501
+ ${uiMapTable}
502
+
503
+ URL: ${url}
504
+ Title: ${title}
542
505
 
543
- Please retry with a shorter output. Focus ONLY on the main interactive section of the page.
544
- Skip navigation, sidebar, and footer sections. Output ONE section only with max 15 elements.
506
+ <aria>
507
+ ${aria}
508
+ </aria>
545
509
  `;
546
510
  }
547
511
  async textContent(state) {
@@ -18,6 +18,7 @@ import { createDebug, log, tag } from './utils/logger.js';
18
18
  import { WebElement, extractElementData } from "./utils/web-element.js";
19
19
  const debugLog = createDebug('explorbot:explorer');
20
20
  const FATAL_BROWSER_ERRORS = /Frame was detached|Target closed|Execution context was destroyed|Protocol error|Session closed/i;
21
+ const RECOVERABLE_NAVIGATION_ERRORS = /net::ERR_ABORTED|page\.screenshot.*Timeout|waiting for fonts to load/i;
21
22
  class Explorer {
22
23
  aiProvider;
23
24
  playwrightHelper;
@@ -228,7 +229,17 @@ class Explorer {
228
229
  await action.execute(`I.executeScript(() => { window.history.pushState({}, '', ${serializedUrl}); window.dispatchEvent(new PopStateEvent('popstate')); })`);
229
230
  }
230
231
  else {
231
- await action.execute(`I.amOnPage(${serializedUrl})`);
232
+ try {
233
+ await action.execute(`I.amOnPage(${serializedUrl})`);
234
+ }
235
+ catch (err) {
236
+ const msg = err instanceof Error ? err.message : String(err);
237
+ if (!RECOVERABLE_NAVIGATION_ERRORS.test(msg))
238
+ throw err;
239
+ tag('warning').log(`Navigation warning (continuing after load): ${msg.split('\n')[0]}`);
240
+ await this.playwrightHelper.page.waitForLoadState('domcontentloaded', { timeout: 10000 }).catch(() => { });
241
+ await action.capturePageState();
242
+ }
232
243
  }
233
244
  if (wait !== undefined) {
234
245
  debugLog('Waiting for', wait);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explorbot",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "CLI app built with React Ink, CodeceptJS, and Playwright",
5
5
  "license": "Elastic-2.0",
6
6
  "type": "module",
@@ -0,0 +1,15 @@
1
+ <container_rules>
2
+ Container CSS must be a SINGLE semantic selector — one class, one id, or one attribute. No spaces, no combinators, no descendant paths.
3
+
4
+ - INVALID: bare tags (`div`, `section`, `nav`), combinators (`div > .content`, `.a .b`), layout/utility classes (flex-, col-, mt-, p-, bg-, text-, items-, rounded-)
5
+ - VALID: semantic class names that describe what the section IS (`.product-list`, `.sidebar-menu`, `.user-profile`, `.search-results`), semantic roles (`[role="dialog"]`), semantic ids (`#main-content`)
6
+
7
+ The container must uniquely identify a semantic wrapper, not a path through the DOM.
8
+ </container_rules>
9
+
10
+ <css_selector_rules>
11
+ CSS selectors inside the UI map must point to the actual interactive element (input, button, a, select), not to wrapper divs.
12
+
13
+ - Prefer distinguishing attributes on the interactive element (`type`, `value`, `name`, `href`, `aria-label`) over wrapper ids.
14
+ - For buttons with similar text, include `type` or `value` or form context to stay unique.
15
+ </css_selector_rules>
@@ -0,0 +1,12 @@
1
+ <section_example>
2
+ ## List
3
+
4
+ Product catalog showing available items with sorting and filtering.
5
+
6
+ > Container: '.product-list'
7
+
8
+ | Element | ARIA | CSS | eidx |
9
+ | 'Sort by price' | { role: 'button', text: 'Sort by price' } | '.sort-btn' | 3 |
10
+ | 'Add to cart' | { role: 'button', text: 'Add to cart' } | '.add-btn' | 4 |
11
+ | 'Product name' | { role: 'link', text: 'Widget Pro' } | 'a.product-link' | 5 |
12
+ </section_example>
@@ -22,11 +22,16 @@ function getStatesDir(): string {
22
22
  function getFingerprintWorker(): Worker {
23
23
  if (!fingerprintWorker) {
24
24
  const ext = import.meta.url.endsWith('.ts') ? '.ts' : '.js';
25
- fingerprintWorker = new Worker(new URL(`./fingerprint-worker${ext}`, import.meta.url).href);
25
+ fingerprintWorker = new Worker(new URL(`./fingerprint-worker${ext}`, import.meta.url));
26
26
  }
27
27
  return fingerprintWorker;
28
28
  }
29
29
 
30
+ export function clearResearchCache(): void {
31
+ for (const key of Object.keys(memoryCache)) delete memoryCache[key];
32
+ for (const key of Object.keys(memoryCacheTimestamps)) delete memoryCacheTimestamps[key];
33
+ }
34
+
30
35
  export function getCachedResearch(hash: string): string {
31
36
  if (!hash) return '';
32
37
  const now = Date.now();
@@ -98,15 +98,11 @@ export class Researcher extends ResearcherBase implements Agent {
98
98
  You are senior QA focused on exploritary testig of web application.
99
99
  </role>
100
100
 
101
- <wording>
102
- In the UI map and all descriptions, name concrete UI parts (visible labels, headings, regions, ARIA roles). Do not use vague placeholders like "the page", "the element", "the button", "the input", "the link", "the form", "the table", "the list", or "the item". Do not use filler such as "comprehensive", "All required", "All elements", or "All necessary".
103
- </wording>
104
-
105
101
  ${customPrompt || ''}
106
102
  `;
107
103
  }
108
104
 
109
- async research(state: WebPageState, opts: { screenshot?: boolean; force?: boolean; deep?: boolean; data?: boolean; fix?: boolean; _retriesLeft?: number; _skipErrorPageRetry?: boolean } = {}): Promise<string> {
105
+ async research(state: WebPageState, opts: { screenshot?: boolean; force?: boolean; deep?: boolean; data?: boolean; fix?: boolean; _retriesLeft?: number } = {}): Promise<string> {
110
106
  const { screenshot = false, force = false, deep = false, data = false, fix = true } = opts;
111
107
  const maxRetries = (this.explorer.getConfig().ai?.agents?.researcher as any)?.retries ?? 2;
112
108
  let retriesLeft = opts._retriesLeft ?? maxRetries;
@@ -175,6 +171,7 @@ export class Researcher extends ResearcherBase implements Agent {
175
171
  conversation.addUserText(prompt);
176
172
 
177
173
  let invocationResult: Awaited<ReturnType<typeof this.provider.invokeConversation>>;
174
+ let activeConversation = conversation;
178
175
  try {
179
176
  invocationResult = await this.provider.invokeConversation(conversation, undefined, { agentName: 'researcher' });
180
177
  } catch (error) {
@@ -184,30 +181,17 @@ export class Researcher extends ResearcherBase implements Agent {
184
181
  }
185
182
  throw error;
186
183
  }
187
- tag('warning').log('Output truncated, retrying with focused instructions...');
184
+ tag('warning').log('Output truncated, retrying with fresh focused conversation (ARIA only)...');
188
185
  retriesLeft = 0;
189
- conversation.addUserText(this.buildFocusedRetryPrompt());
190
- invocationResult = await this.provider.invokeConversation(conversation, undefined, { agentName: 'researcher' });
186
+ activeConversation = this.provider.startConversation(this.getSystemMessage(), 'researcher');
187
+ activeConversation.addUserText(this.buildFocusedRetryPrompt());
188
+ invocationResult = await this.provider.invokeConversation(activeConversation, undefined, { agentName: 'researcher' });
191
189
  }
192
190
  if (!invocationResult) throw new Error('Failed to get response from provider');
193
191
 
194
192
  const result = new ResearchResult(invocationResult.response.text, state.url);
195
193
  debugLog(`Original research response length: ${result.text.length} chars`);
196
194
 
197
- const errorSection = mdq(result.text).query('section("Error Page Detected")');
198
- if (errorSection.count() > 0) {
199
- if (result.text.length < 500) {
200
- if (!opts._skipErrorPageRetry && (await this.waitForPageLoad(screenshot))) {
201
- return this.research(state, { ...opts, force: true, _skipErrorPageRetry: true });
202
- }
203
- tag('warning').log(`AI detected error page at ${state.url}`);
204
- if (stateHash) saveResearch(stateHash, result.text);
205
- await this.hooksRunner.runAfterHook('researcher', state.url);
206
- return result.text;
207
- }
208
- result.text = errorSection.replace('');
209
- }
210
-
211
195
  const interrupted = () => executionController.isInterrupted();
212
196
 
213
197
  // Stage 2: Test containers + locators
@@ -251,7 +235,7 @@ export class Researcher extends ResearcherBase implements Agent {
251
235
 
252
236
  // Stage 3: Fix broken sections via AI conversation continuation
253
237
  if (!interrupted() && fix && result.locators.some((l) => l.valid === false)) {
254
- await this.fixBrokenSections(result, conversation);
238
+ await this.fixBrokenSections(result, activeConversation);
255
239
  }
256
240
 
257
241
  // Focused section: parse AI declaration, then ARIA fallback
@@ -417,32 +401,29 @@ export class Researcher extends ResearcherBase implements Agent {
417
401
 
418
402
  private researchRules(): string {
419
403
  const sections = this.getConfiguredSections();
404
+ const currentUrl = this.stateManager.getCurrentState()?.url || '';
420
405
  return dedent`
421
406
  <task>
422
- Examine the provided page and explain its main purpose from the user perspective.
423
- Identify the main user actions of this page.
424
- Break down the page by sections and identify structural patterns.
425
- Provide a comprehensive UI map report in markdown format.
407
+ Examine the page and explain its main purpose from the user perspective.
408
+ Identify the primary user actions and break the page into sections.
409
+ Provide a UI map report in markdown.
426
410
  </task>
427
411
 
428
412
  <rules>
429
413
  - Explain what the user can achieve on this page.
430
414
  - Focus on primary user actions and interactive elements only.
431
415
  - Research all menus and navigational areas.
432
- - Ignore purely decorative sidebars, footer-only links, and external links.
416
+ - Ignore decorative sidebars, footer-only links, and external links.
433
417
  - Detect layout patterns: list/detail split, 2-pane, or 3-pane layouts.
434
- - If multiple elements match, pick the element inside the most relevant section and closest to recent UI context.
435
- - UI map table must include ARIA and CSS for every element.
436
- - Every element MUST have a CSS selector. NEVER leave CSS as "-".
437
- - For icon-only buttons with empty aria-label, set ARIA to "-" but ALWAYS provide CSS.
438
- - NEVER skip elements that have an eidx attribute. Every element with eidx MUST appear in the UI map table, even if it has no text or accessible name. Describe icon-only elements using their SVG class (e.g., md-icon-dots-horizontal → "More actions (ellipsis)") or their visual appearance.
439
- - ARIA locator must be JSON with role and text keys (NOT "name").
440
- - Note elements likely to have hover interactions (elements with title attribute, aria-describedby, navigation menu items with submenus) and mark them with "(hover)" in the UI map.
418
+ - Every element with an eidx attribute MUST appear in the UI map describe icon-only buttons by their visual role.
419
+ - Every UI map row needs a CSS selector; ARIA may be "-" for icon-only buttons, CSS must never be "-".
420
+ - ARIA locator JSON uses keys "role" and "text" (NOT "name").
421
+ - Mark elements with likely hover interactions (title, aria-describedby, menu items with submenus) as "(hover)".
441
422
  </rules>
442
423
 
443
424
  ${generalLocatorRuleText}
444
425
 
445
- ${RulesLoader.loadRules('researcher', ['ui-map-table', 'list-element'], this.stateManager.getCurrentState()?.url || '')}
426
+ ${RulesLoader.loadRules('researcher', ['ui-map-table', 'list-element', 'container-rules'], currentUrl)}
446
427
 
447
428
  <section_identification>
448
429
  Identify page sections in this priority order:
@@ -450,23 +431,12 @@ export class Researcher extends ResearcherBase implements Agent {
450
431
  .map(([name, description]) => `* ${name}: ${description}`)
451
432
  .join('\n')}
452
433
 
453
- - Sections can overlap, prefer more detailed sections over broader ones.
454
- - Never name a section "Focus" or "Focused" — describe what the section actually contains (e.g., "Detail", "Modal", "Form", "Content", "List").
455
- - If a proposed section is not relevant or not detected, do not include it.
456
- - Each section must have a container CSS locator.
457
- - UI map CSS locators must be relative to the section container.
434
+ - Sections can overlap; prefer more detailed sections over broader ones.
435
+ - Never name a section "Focus" or "Focused" — use what it contains (Detail, Modal, Form, Content, List).
436
+ - Omit sections that are not present or not relevant.
437
+ - Each section needs a container CSS locator; UI map CSS locators are relative to it.
458
438
  </section_identification>
459
439
 
460
- <container_rules>
461
- CRITICAL: Container CSS must be a SINGLE selector — one class, one ID, or one attribute.
462
- No spaces, no >, no combinators, no nesting.
463
- - INVALID: '.filterbar-filter-btn-div button', 'div.static nav', 'div > .content'
464
- - INVALID: 'div', 'section', 'nav', 'div:first' (bare tags are not containers)
465
- - INVALID: Tailwind/Bootstrap utility classes that describe layout or styling (e.g. flex-none, d-flex, col-md-6, items-center, mt-4, p-2, bg-white, text-sm, rounded-lg). These are visual, not semantic.
466
- - VALID: Semantic class names that describe WHAT the section IS — e.g. '.product-list', '.sidebar-menu', '.user-profile', '[role="dialog"]', '.search-results'
467
- Container must uniquely identify a semantic wrapper, not a path through the DOM.
468
- </container_rules>
469
-
470
440
  <section_format>
471
441
  ## Section Name
472
442
 
@@ -476,40 +446,16 @@ export class Researcher extends ResearcherBase implements Agent {
476
446
 
477
447
  | Element | ARIA | CSS | eidx |
478
448
  </section_format>
479
- <section_example>
480
- ## List
481
-
482
- Product catalog showing available items with sorting and filtering.
483
-
484
- > Container: '.product-list'
485
-
486
- | Element | ARIA | CSS | eidx |
487
- | 'Sort by price' | { role: 'button', text: 'Sort by price' } | '.sort-btn' | 3 |
488
- | 'Add to cart' | { role: 'button', text: 'Add to cart' } | '.add-btn' | 4 |
489
- | 'Product name' | { role: 'link', text: 'Widget Pro' } | 'a.product-link' | 5 |
490
- </section_example>
491
449
 
492
450
  <focused_section>
493
- At the very end of your output, add a single line declaring which section is the user's primary focus area:
451
+ At the end of your output, declare the primary focus area on a single line:
494
452
 
495
453
  > Focused: <exact section name>
496
454
 
497
- Rules for determining the focused section:
498
- - If the page has a dialog, modal, drawer, or overlay that section is focused
499
- - If no overlay exists, pick the section where the user performs the main business action of this page (e.g., a list section for a catalog page, a detail section for an item page, a content section for an article page)
500
- - Navigation is NEVER focused — it exists on every page
501
- - Menu/toolbar is NEVER focused — it contains actions, not the main content
502
- - The focused section is the one the user came to this page to interact with
455
+ - If a dialog/modal/drawer/overlay exists, it is focused.
456
+ - Otherwise pick the section where the main business action happens (list for catalog, detail for item page, content for article).
457
+ - Navigation and menu/toolbar are never focused.
503
458
  </focused_section>
504
-
505
- <css_selector_rules>
506
- CSS selectors MUST point to the actual interactive element (input, button, a, select), NOT to container divs.
507
- - If a submit button is inside a wrapper div, target the input/button directly
508
- - Bad: '#submit-wrapper' (div container)
509
- - Good: '#submit-wrapper input[type="submit"]' or 'input[type="submit"][value="Submit"]'
510
- - For buttons with similar text, include distinguishing attributes like type, value, or form context
511
-
512
- </css_selector_rules>
513
459
  `;
514
460
  }
515
461
 
@@ -540,17 +486,6 @@ export class Researcher extends ResearcherBase implements Agent {
540
486
  return dedent`
541
487
  Analyze this web page and provide a comprehensive research report in markdown format.
542
488
 
543
- <error_detection>
544
- IMPORTANT: First check if this looks like an error page (404, 500, access denied,
545
- not found, server error, forbidden, or similar). If so, respond ONLY with:
546
-
547
- ## Error Page Detected
548
- Type: [error type]
549
- Reason: [what indicates this is an error page]
550
-
551
- Then stop - do not provide normal research output for error pages.
552
- </error_detection>
553
-
554
489
  ${this.researchRules()}
555
490
 
556
491
  URL: ${this.actionResult.url || 'Unknown'}
@@ -603,11 +538,40 @@ export class Researcher extends ResearcherBase implements Agent {
603
538
  }
604
539
 
605
540
  private buildFocusedRetryPrompt(): string {
541
+ const currentUrl = this.stateManager.getCurrentState()?.url || '';
542
+ const example = RulesLoader.loadRules('researcher', ['section-example'], currentUrl);
543
+ const uiMapTable = RulesLoader.loadRules('researcher', ['ui-map-table'], currentUrl);
544
+ const url = this.actionResult?.url || 'Unknown';
545
+ const title = this.actionResult?.title || 'Unknown';
546
+ const aria = this.actionResult?.getCompactARIA() || '';
606
547
  return dedent`
607
- Your previous response was truncated and could not be parsed.
548
+ Previous response was truncated. Restart with a minimal output.
549
+
550
+ <task>
551
+ Output a UI map for ONE section only — the main interactive area of this page.
552
+ Skip navigation, sidebar, and footer. Max 15 elements.
553
+ Every element with an eidx MUST appear. Every row needs CSS; ARIA may be "-" for icon-only.
554
+ End with a single line: \`> Focused: <section name>\`.
555
+ </task>
556
+
557
+ <section_format>
558
+ ## Section Name
559
+
560
+ > Container: '.container-css-selector'
561
+
562
+ | Element | ARIA | CSS | eidx |
563
+ </section_format>
564
+
565
+ ${example}
566
+
567
+ ${uiMapTable}
568
+
569
+ URL: ${url}
570
+ Title: ${title}
608
571
 
609
- Please retry with a shorter output. Focus ONLY on the main interactive section of the page.
610
- Skip navigation, sidebar, and footer sections. Output ONE section only with max 15 elements.
572
+ <aria>
573
+ ${aria}
574
+ </aria>
611
575
  `;
612
576
  }
613
577
 
package/src/explorer.ts CHANGED
@@ -37,6 +37,7 @@ declare namespace CodeceptJS {
37
37
 
38
38
  const debugLog = createDebug('explorbot:explorer');
39
39
  const FATAL_BROWSER_ERRORS = /Frame was detached|Target closed|Execution context was destroyed|Protocol error|Session closed/i;
40
+ const RECOVERABLE_NAVIGATION_ERRORS = /net::ERR_ABORTED|page\.screenshot.*Timeout|waiting for fonts to load/i;
40
41
 
41
42
  interface TabInfo {
42
43
  url: string;
@@ -289,7 +290,15 @@ class Explorer {
289
290
  if (statePush) {
290
291
  await action.execute(`I.executeScript(() => { window.history.pushState({}, '', ${serializedUrl}); window.dispatchEvent(new PopStateEvent('popstate')); })`);
291
292
  } else {
292
- await action.execute(`I.amOnPage(${serializedUrl})`);
293
+ try {
294
+ await action.execute(`I.amOnPage(${serializedUrl})`);
295
+ } catch (err) {
296
+ const msg = err instanceof Error ? err.message : String(err);
297
+ if (!RECOVERABLE_NAVIGATION_ERRORS.test(msg)) throw err;
298
+ tag('warning').log(`Navigation warning (continuing after load): ${msg.split('\n')[0]}`);
299
+ await this.playwrightHelper.page.waitForLoadState('domcontentloaded', { timeout: 10000 }).catch(() => {});
300
+ await action.capturePageState();
301
+ }
293
302
  }
294
303
 
295
304
  if (wait !== undefined) {