explorbot 0.1.27 → 0.1.28

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.
Files changed (48) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/action-result.js +2 -0
  3. package/dist/src/action.js +88 -7
  4. package/dist/src/ai/captain/file-tools.js +100 -0
  5. package/dist/src/ai/captain/idle-mode.js +70 -6
  6. package/dist/src/ai/captain/web-mode.js +36 -6
  7. package/dist/src/ai/captain.js +87 -19
  8. package/dist/src/ai/historian/screencast.js +11 -2
  9. package/dist/src/ai/navigator.js +5 -2
  10. package/dist/src/ai/pilot.js +33 -5
  11. package/dist/src/ai/researcher/coordinates.js +2 -3
  12. package/dist/src/ai/researcher/deep-analysis.js +3 -4
  13. package/dist/src/ai/researcher/locators.js +1 -2
  14. package/dist/src/ai/researcher.js +17 -18
  15. package/dist/src/ai/task-agent.js +1 -0
  16. package/dist/src/ai/tester.js +91 -39
  17. package/dist/src/ai/tools.js +17 -7
  18. package/dist/src/commands/explore-command.js +6 -1
  19. package/dist/src/components/LogPane.js +4 -3
  20. package/dist/src/explorer.js +270 -35
  21. package/dist/src/utils/browser-errors.js +23 -0
  22. package/dist/src/utils/error-page.js +17 -2
  23. package/dist/src/utils/logger.js +2 -2
  24. package/package.json +1 -1
  25. package/src/action-result.ts +2 -0
  26. package/src/action.ts +83 -7
  27. package/src/ai/captain/file-tools.ts +126 -0
  28. package/src/ai/captain/idle-mode.ts +72 -6
  29. package/src/ai/captain/mixin.ts +1 -1
  30. package/src/ai/captain/web-mode.ts +40 -5
  31. package/src/ai/captain.ts +94 -20
  32. package/src/ai/historian/screencast.ts +11 -2
  33. package/src/ai/navigator.ts +6 -2
  34. package/src/ai/pilot.ts +34 -5
  35. package/src/ai/researcher/coordinates.ts +2 -3
  36. package/src/ai/researcher/deep-analysis.ts +3 -4
  37. package/src/ai/researcher/locators.ts +1 -2
  38. package/src/ai/researcher.ts +17 -18
  39. package/src/ai/task-agent.ts +1 -1
  40. package/src/ai/tester.ts +101 -41
  41. package/src/ai/tools.ts +17 -7
  42. package/src/commands/explore-command.ts +6 -1
  43. package/src/components/LogPane.tsx +4 -3
  44. package/src/explorer.ts +295 -38
  45. package/src/state-manager.ts +2 -0
  46. package/src/utils/browser-errors.ts +25 -0
  47. package/src/utils/error-page.ts +16 -3
  48. package/src/utils/logger.ts +3 -3
package/src/action.ts CHANGED
@@ -23,9 +23,9 @@ import { htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
23
23
  import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
24
24
  import { safeFilename } from './utils/strings.ts';
25
25
  import { throttle } from './utils/throttle.ts';
26
+ import { isFatalBrowserError } from './utils/browser-errors.ts';
26
27
 
27
28
  const debugLog = createDebug('explorbot:action');
28
- const FATAL_BROWSER_ERRORS = /Frame was detached|Target closed|Execution context was destroyed|Protocol error|Session closed/i;
29
29
 
30
30
  class Action {
31
31
  private actor: CodeceptJS.I;
@@ -41,6 +41,7 @@ class Action {
41
41
  public playwrightGroupId: string | null = null;
42
42
  public assertionSteps: Array<{ name: string; args: any[] }> = [];
43
43
  private recorder?: PlaywrightRecorder;
44
+ private mainDocumentStatus: number | undefined = undefined;
44
45
 
45
46
  constructor(actor: CodeceptJS.I, stateManager: StateManager, recorder?: PlaywrightRecorder) {
46
47
  this.actor = actor;
@@ -78,21 +79,26 @@ class Action {
78
79
  const page = this.playwrightHelper.page;
79
80
  const frame = this.playwrightHelper.frame;
80
81
  await page?.waitForLoadState('domcontentloaded', { timeout: 10000 })?.catch(() => {});
81
- const grabAll = () => Promise.all([(this.actor as any).grabSource(), (this.actor as any).grabTitle(), this.captureBrowserLogs()]);
82
+ await waitForUsablePageDom(page);
83
+ const grabAll = () => Promise.all([captureHtml(page, frame, this.actor), captureTitle(page, this.actor), this.captureBrowserLogs()]);
82
84
  const [html, title, browserLogs] = await grabAll().catch(async (err: Error) => {
83
85
  const msg = err instanceof Error ? err.message : String(err);
84
86
  if (!/navigating and changing the content/i.test(msg)) throw err;
85
87
  await page?.waitForLoadState('domcontentloaded', { timeout: 10000 })?.catch(() => {});
88
+ await waitForUsablePageDom(page);
86
89
  return grabAll();
87
90
  });
88
91
  const url = page?.url() || (await (this.actor as any).grabCurrentUrl?.());
89
92
 
90
93
  let screenshotFile: string | undefined = undefined;
94
+ const statesDir = outputPath('states');
95
+ fs.mkdirSync(statesDir, { recursive: true });
91
96
 
92
97
  if (includeScreenshot) {
93
98
  const filename = safeFilename(`${stateHash}_${timestamp}`, '.png');
94
- screenshotFile = await (this.actor as any)
95
- .saveScreenshot(filename)
99
+ const screenshotPath = join(statesDir, filename);
100
+ screenshotFile = await page
101
+ ?.screenshot({ path: screenshotPath, fullPage: true })
96
102
  .then(() => filename)
97
103
  .catch((err: Error) => {
98
104
  debugLog('Screenshot failed, continuing without it:', err);
@@ -101,8 +107,6 @@ class Action {
101
107
  }
102
108
 
103
109
  // Save HTML to file
104
- const statesDir = outputPath('states');
105
- fs.mkdirSync(statesDir, { recursive: true });
106
110
  const htmlFile = safeFilename(`${stateHash}_${timestamp}`, '.html');
107
111
  const htmlPath = join(statesDir, htmlFile);
108
112
  fs.writeFileSync(htmlPath, html, 'utf8');
@@ -144,6 +148,7 @@ class Action {
144
148
  const result = new ActionResult({
145
149
  html,
146
150
  title,
151
+ httpStatus: await this.captureMainDocumentStatus(),
147
152
  url,
148
153
  browserLogs,
149
154
  htmlFile,
@@ -158,13 +163,52 @@ class Action {
158
163
  return result;
159
164
  } catch (err) {
160
165
  const msg = err instanceof Error ? err.message : String(err);
161
- if (FATAL_BROWSER_ERRORS.test(msg)) throw err;
166
+ if (isFatalBrowserError(err)) throw err;
162
167
  debugLog('capturePageState failed with non-fatal error:', msg);
163
168
  const url = this.playwrightHelper.page?.url?.() || '';
164
169
  return new ActionResult({ url, error: msg });
165
170
  }
166
171
  }
167
172
 
173
+ private async captureMainDocumentStatus(): Promise<number | undefined> {
174
+ if (this.mainDocumentStatus) return this.mainDocumentStatus;
175
+
176
+ try {
177
+ const page = this.playwrightHelper.page;
178
+ const status = await page.evaluate(() => {
179
+ const navigation = performance.getEntriesByType('navigation').at(-1) as PerformanceNavigationTiming & { responseStatus?: number };
180
+ if (!navigation) return undefined;
181
+ if (new URL(navigation.name).href !== window.location.href) return undefined;
182
+ return navigation.responseStatus;
183
+ });
184
+ if (typeof status !== 'number') return undefined;
185
+ if (status <= 0) return undefined;
186
+ return status;
187
+ } catch {
188
+ return undefined;
189
+ }
190
+ }
191
+
192
+ private captureMainDocumentResponse(): () => void {
193
+ const page = this.playwrightHelper.page;
194
+ if (!page?.on || !page?.off) return () => {};
195
+
196
+ this.mainDocumentStatus = undefined;
197
+
198
+ const handler = (response: any) => {
199
+ const request = response.request();
200
+ if (request.resourceType() !== 'document') return;
201
+ if (response.frame() !== page.mainFrame()) return;
202
+ const status = response.status();
203
+ if (typeof status !== 'number') return;
204
+ if (status <= 0) return;
205
+ this.mainDocumentStatus = status;
206
+ };
207
+
208
+ page.on('response', handler);
209
+ return () => page.off('response', handler);
210
+ }
211
+
168
212
  /**
169
213
  * Capture HTML snapshots of all iframes on the page
170
214
  */
@@ -235,6 +279,7 @@ class Action {
235
279
  const stepListener = attachStepLogger(executedSteps, assertionSteps);
236
280
  const groupId = this.recorder ? await this.recorder.beginAction(codeString) : null;
237
281
  this.playwrightGroupId = groupId;
282
+ const detachMainDocumentResponse = this.captureMainDocumentResponse();
238
283
  const activeSpan = Observability.getSpan();
239
284
  const tracer = trace.getTracer('ai');
240
285
  const stepSpan = activeSpan ? tracer.startSpan('codeceptjs.step', undefined, trace.setSpan(context.active(), activeSpan)) : null;
@@ -280,6 +325,7 @@ class Action {
280
325
  this.assertionSteps = [];
281
326
  throw err;
282
327
  } finally {
328
+ detachMainDocumentResponse();
283
329
  if (groupId) await this.recorder!.endAction();
284
330
  detachStepLogger(stepListener);
285
331
  if (stepSpan) {
@@ -375,6 +421,7 @@ class Action {
375
421
  return true;
376
422
  } catch (error) {
377
423
  this.lastError = error as Error;
424
+ if (isFatalBrowserError(error)) throw error;
378
425
  debugLog(`Attempt failed: ${codeBlock}: ${errorToString(error) || this.lastError?.toString()}`);
379
426
  return false;
380
427
  }
@@ -406,6 +453,35 @@ function errorToString(error: any): string {
406
453
  return error.message || error.toString();
407
454
  }
408
455
 
456
+ async function waitForUsablePageDom(page: any): Promise<void> {
457
+ if (!page?.waitForFunction) return;
458
+
459
+ await page
460
+ .waitForFunction(
461
+ () => {
462
+ const body = document.body;
463
+ if (!body) return false;
464
+ return body.children.length > 0 || body.textContent?.trim().length > 0;
465
+ },
466
+ undefined,
467
+ { timeout: 5000 }
468
+ )
469
+ .catch(() => {});
470
+ }
471
+
472
+ async function captureHtml(page: any, frame: any, actor: any): Promise<string> {
473
+ if (frame?.content) return frame.content();
474
+ if (page?.content) return page.content();
475
+ if (actor?.grabSource) return actor.grabSource();
476
+ throw new Error('Playwright page is unavailable for HTML capture');
477
+ }
478
+
479
+ async function captureTitle(page: any, actor: any): Promise<string> {
480
+ if (page?.title) return page.title();
481
+ if (actor?.grabTitle) return actor.grabTitle();
482
+ return '';
483
+ }
484
+
409
485
  function sanitizeCodeBlock(code: string): string {
410
486
  return code
411
487
  .split('\n')
@@ -0,0 +1,126 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
+ import { basename, isAbsolute, join, relative, resolve } from 'node:path';
3
+
4
+ export const CAPTAIN_ARTIFACT_DIRS = ['reports', 'plans', 'tests', 'states'] as const;
5
+ export const CAPTAIN_ALLOWED_READ_DIRS = ['output', 'knowledge', 'experience'] as const;
6
+ export const CAPTAIN_ARTIFACT_SCAN_LIMIT = 200;
7
+ export const CAPTAIN_ARTIFACT_LIST_LIMIT = 20;
8
+ export const CAPTAIN_READ_FILE_DEFAULT_LIMIT = 12000;
9
+ export const CAPTAIN_READ_FILE_MAX_LIMIT = 50000;
10
+ export const CAPTAIN_READ_FILE_MIN_LIMIT = 1000;
11
+
12
+ export function listRecentArtifacts(outputDir: string): Array<{ path: string; size: number; modifiedAt: string }> {
13
+ const artifacts: Array<{ path: string; size: number; modifiedAt: string; timestamp: number }> = [];
14
+
15
+ for (const dir of CAPTAIN_ARTIFACT_DIRS) {
16
+ if (artifacts.length >= CAPTAIN_ARTIFACT_SCAN_LIMIT) break;
17
+ const targetDir = join(outputDir, dir);
18
+ if (!existsSync(targetDir)) continue;
19
+ collectArtifacts(outputDir, targetDir, artifacts);
20
+ }
21
+
22
+ return artifacts
23
+ .sort((a, b) => b.timestamp - a.timestamp)
24
+ .slice(0, CAPTAIN_ARTIFACT_LIST_LIMIT)
25
+ .map(({ timestamp, ...artifact }) => artifact);
26
+ }
27
+
28
+ export function readCaptainFile(projectRoot: string | null, input: ReadCaptainFileInput, allowedDirs: readonly string[] = CAPTAIN_ALLOWED_READ_DIRS): ReadCaptainFileResult {
29
+ const resolved = resolveReadableFile(projectRoot, input.path, allowedDirs);
30
+ if (!resolved) {
31
+ return { success: false, message: 'File is outside allowed directories' };
32
+ }
33
+ if (!existsSync(resolved)) {
34
+ return { success: false, message: `File not found: ${input.path}` };
35
+ }
36
+ if (!statSync(resolved).isFile()) {
37
+ return { success: false, message: `Not a file: ${input.path}` };
38
+ }
39
+
40
+ const maxChars = normalizeMaxChars(input.maxChars);
41
+ const fullContent = readFileSync(resolved, 'utf8');
42
+ const content = selectContent(fullContent, input);
43
+ return {
44
+ success: true,
45
+ path: relative(projectRoot || process.cwd(), resolved),
46
+ truncated: content.length > maxChars,
47
+ content: content.slice(0, maxChars),
48
+ };
49
+ }
50
+
51
+ function collectArtifacts(outputDir: string, targetDir: string, artifacts: Array<{ path: string; size: number; modifiedAt: string; timestamp: number }>): void {
52
+ for (const entry of readdirSync(targetDir, { withFileTypes: true })) {
53
+ if (artifacts.length >= CAPTAIN_ARTIFACT_SCAN_LIMIT) return;
54
+ const entryPath = join(targetDir, entry.name);
55
+ if (entry.isDirectory()) {
56
+ collectArtifacts(outputDir, entryPath, artifacts);
57
+ continue;
58
+ }
59
+
60
+ const stats = statSync(entryPath);
61
+ artifacts.push({
62
+ path: relative(outputDir, entryPath),
63
+ size: stats.size,
64
+ modifiedAt: stats.mtime.toISOString(),
65
+ timestamp: stats.mtimeMs,
66
+ });
67
+ }
68
+ }
69
+
70
+ function resolveReadableFile(projectRoot: string | null, requestedPath: string, allowedDirs: readonly string[]): string | null {
71
+ if (!projectRoot) return null;
72
+
73
+ let cleanPath = requestedPath.trim();
74
+ const projectName = basename(projectRoot);
75
+ if (cleanPath.startsWith(`${projectName}/`) || cleanPath.startsWith(`${projectName}\\`)) {
76
+ cleanPath = cleanPath.slice(projectName.length + 1);
77
+ }
78
+
79
+ const resolved = isAbsolute(cleanPath) ? resolve(cleanPath) : resolve(projectRoot, cleanPath);
80
+ const allowedRoots = allowedDirs.map((dir) => resolve(projectRoot, dir));
81
+ for (const root of allowedRoots) {
82
+ const rel = relative(root, resolved);
83
+ if (!rel || (!rel.startsWith('..') && !isAbsolute(rel))) return resolved;
84
+ }
85
+
86
+ return null;
87
+ }
88
+
89
+ function selectContent(content: string, input: ReadCaptainFileInput): string {
90
+ if (!input.startLine && !input.endLine) return content;
91
+
92
+ const lines = content.split(/\r?\n/);
93
+ const startIndex = resolveLineIndex(input.startLine, lines.length, 1);
94
+ const endIndex = resolveLineIndex(input.endLine, lines.length, lines.length);
95
+ if (endIndex < startIndex) return '';
96
+ return lines.slice(startIndex - 1, endIndex).join('\n');
97
+ }
98
+
99
+ function resolveLineIndex(line: number | undefined, totalLines: number, fallback: number): number {
100
+ if (!line) return fallback;
101
+ if (line < 0) return Math.max(1, totalLines + line + 1);
102
+ return Math.min(Math.max(1, line), totalLines);
103
+ }
104
+
105
+ function normalizeMaxChars(maxChars?: number): number {
106
+ return Math.max(CAPTAIN_READ_FILE_MIN_LIMIT, Math.min(maxChars || CAPTAIN_READ_FILE_DEFAULT_LIMIT, CAPTAIN_READ_FILE_MAX_LIMIT));
107
+ }
108
+
109
+ export interface ReadCaptainFileInput {
110
+ path: string;
111
+ startLine?: number;
112
+ endLine?: number;
113
+ maxChars?: number;
114
+ }
115
+
116
+ export type ReadCaptainFileResult =
117
+ | {
118
+ success: true;
119
+ path: string;
120
+ truncated: boolean;
121
+ content: string;
122
+ }
123
+ | {
124
+ success: false;
125
+ message: string;
126
+ };
@@ -4,6 +4,7 @@ import dedent from 'dedent';
4
4
  import { z } from 'zod';
5
5
  import { ConfigParser } from '../../config.ts';
6
6
  import { Test } from '../../test-plan.ts';
7
+ import { listRecentArtifacts, readCaptainFile } from './file-tools.ts';
7
8
  import { type Constructor, type ModeContext, resolveProjectRoot } from './mixin.ts';
8
9
 
9
10
  let cachedBashTool: Awaited<ReturnType<typeof createBashTool>> | null = null;
@@ -15,6 +16,8 @@ export function WithIdleMode<T extends Constructor>(Base: T) {
15
16
  const config = ConfigParser.getInstance().getConfig();
16
17
  const knowledgeDir = config.dirs?.knowledge || 'knowledge';
17
18
  const experienceDir = config.dirs?.experience || 'experience';
19
+ const outputDir = config.dirs?.output || 'output';
20
+ const readableDirs = [outputDir, knowledgeDir, experienceDir];
18
21
 
19
22
  if (!cachedBashTool && projectRoot) {
20
23
  cachedBashTool = await createBashTool({
@@ -75,6 +78,55 @@ export function WithIdleMode<T extends Constructor>(Base: T) {
75
78
  return { success: true, tests: plan.tests.length };
76
79
  },
77
80
  }),
81
+ project: tool({
82
+ description: dedent`
83
+ Inspect Explorbot project configuration and recent generated artifacts.
84
+ Use this before answering questions about setup, previous sessions, reports, saved plans, or output files.
85
+ `,
86
+ inputSchema: z.object({
87
+ view: z.enum(['config', 'artifacts']).optional().describe('config shows setup summary; artifacts lists recent generated files'),
88
+ }),
89
+ execute: async ({ view }) => {
90
+ const parser = ConfigParser.getInstance();
91
+ const config = parser.getConfig();
92
+ const outputDir = parser.getOutputDir();
93
+
94
+ if (view === 'artifacts') {
95
+ return {
96
+ success: true,
97
+ outputDir,
98
+ artifacts: listRecentArtifacts(outputDir),
99
+ suggestion: 'Use readFile to inspect specific reports, plans, logs, generated tests, knowledge, or experience files.',
100
+ };
101
+ }
102
+
103
+ return {
104
+ success: true,
105
+ configPath: parser.getConfigPath(),
106
+ baseUrl: config.playwright?.url,
107
+ browser: config.playwright?.browser,
108
+ headed: config.playwright?.show === true,
109
+ dirs: config.dirs,
110
+ agents: Object.fromEntries(Object.entries(config.ai?.agents || {}).map(([name, agentConfig]: [string, any]) => [name, { enabled: agentConfig?.enabled !== false, hasModelOverride: !!agentConfig?.model }])),
111
+ reporterEnabled: config.reporter?.enabled === true,
112
+ apiEnabled: !!config.api,
113
+ };
114
+ },
115
+ }),
116
+ readFile: tool({
117
+ description: dedent`
118
+ Read a specific Explorbot project file for analysis.
119
+ Use this for explicit user questions about reports, plans, logs, generated tests, knowledge, or experience files.
120
+ Prefer this over bash() for reading file contents after bash has found the file.
121
+ `,
122
+ inputSchema: z.object({
123
+ path: z.string().describe('Path inside output, knowledge, or experience directories'),
124
+ startLine: z.number().optional().describe('First line to read, 1-based. Negative values count from the end of the file'),
125
+ endLine: z.number().optional().describe('Last line to read, 1-based and inclusive. Negative values count from the end of the file'),
126
+ maxChars: z.number().optional().describe('Maximum characters to return, default 12000'),
127
+ }),
128
+ execute: async (input) => readCaptainFile(projectRoot, input, readableDirs),
129
+ }),
78
130
  };
79
131
 
80
132
  if (cachedBashTool) {
@@ -88,18 +140,32 @@ export function WithIdleMode<T extends Constructor>(Base: T) {
88
140
  const config = ConfigParser.getInstance().getConfig();
89
141
  const knowledgeDir = config.dirs?.knowledge || 'knowledge';
90
142
  const experienceDir = config.dirs?.experience || 'experience';
143
+ const outputDir = config.dirs?.output || 'output';
91
144
 
92
145
  return dedent`
93
146
  <idle_capabilities>
94
147
  - Plan management: updatePlan() — replace or append tests in the current plan
95
- - bash() — run shell commands for file operations
96
- - READ from: ${knowledgeDir}/, ${experienceDir}/, output/
97
- - WRITE to: ${knowledgeDir}/, ${experienceDir}/ only (NOT output/)
98
- - Use ls to list files, cat to read small files
99
- - Use head/tail for large files to avoid excessive output
100
- - Use grep to search file contents
148
+ - readFile() — read specific report, plan, log, generated test, knowledge, or experience file content
149
+ - bash() discover files and inspect file metadata
150
+ - READ from: ${knowledgeDir}/, ${experienceDir}/, ${outputDir}/
151
+ - WRITE to: ${knowledgeDir}/, ${experienceDir}/ only (NOT ${outputDir}/)
152
+ - Use wc -l -c file.txt to inspect size
153
+ - Use file file.txt to inspect type
154
+ - Use find . -name "*.md" to discover files
155
+ - Use grep -n "keyword" file.txt to find matching lines
156
+ - Use ls -lh to list files
101
157
  </idle_capabilities>
102
158
 
159
+ <file_reading>
160
+ Use bash() for file discovery and search. Once the needed file and line range are known,
161
+ use readFile() to read its contents. Do not use bash() to print file contents.
162
+ </file_reading>
163
+
164
+ <project_inspection>
165
+ Use project({ view: "config" }) before explaining Explorbot setup or suggesting config improvements.
166
+ Use project({ view: "artifacts" }) before answering questions about previous sessions, reports, plans, generated tests, or logs.
167
+ </project_inspection>
168
+
103
169
  <knowledge_saving>
104
170
  When user shares credentials, selectors, or important domain info during conversation,
105
171
  suggest saving it to a knowledge file using bash tool.
@@ -8,7 +8,7 @@ export type Constructor<T = object> = new (...args: any[]) => T;
8
8
 
9
9
  export const debugLog = createDebug('explorbot:captain');
10
10
 
11
- export type CaptainMode = 'idle' | 'web' | 'test';
11
+ export type CaptainMode = 'idle' | 'web' | 'test' | 'heal';
12
12
 
13
13
  export interface ModeContext {
14
14
  explorBot: ExplorBot;
@@ -47,18 +47,53 @@ export function WithWebMode<T extends Constructor>(Base: T) {
47
47
  description: dedent`
48
48
  Direct browser access via Playwright. Use for diagnostics and browser management.
49
49
  Actions:
50
+ - status: Inspect browser/page availability, URL, title, tab count
50
51
  - evaluate: Run JavaScript in browser context (localStorage, cookies, DOM, console)
51
52
  - closeTabs: Close all browser tabs except the current one
52
- - screenshot: Take a screenshot of current page
53
53
  - reload: Reload the current page
54
+ - screenshot: Take a screenshot of current page
55
+ - recover: Recover from a closed/crashed page using Explorer recovery
56
+ - restart: Restart the browser when page/context recovery is not enough
57
+ - openFreshTab: Open a fresh tab in the current browser context
54
58
  `,
55
59
  inputSchema: z.object({
56
- action: z.enum(['evaluate', 'closeTabs', 'screenshot', 'reload']).describe('Browser action to perform'),
60
+ action: z.enum(['status', 'evaluate', 'closeTabs', 'reload', 'screenshot', 'recover', 'restart', 'openFreshTab']).describe('Browser action to perform'),
57
61
  code: z.string().optional().describe('JavaScript code for evaluate action'),
58
62
  }),
59
63
  execute: async ({ action, code }) => {
60
- const page = ctx.explorBot.getExplorer().playwrightHelper?.page;
61
- if (!page) return { success: false, message: 'No browser page available' };
64
+ const explorer = ctx.explorBot.getExplorer();
65
+
66
+ if (action === 'status') {
67
+ const page = explorer.playwrightHelper?.page;
68
+ const pages = page?.context?.().pages?.() || [];
69
+ return {
70
+ success: true,
71
+ hasPage: !!page,
72
+ isClosed: page?.isClosed?.() || false,
73
+ url: page && !page.isClosed?.() ? await page.url() : null,
74
+ title: page && !page.isClosed?.() ? await page.title().catch(() => null) : null,
75
+ tabs: pages.length,
76
+ };
77
+ }
78
+
79
+ if (action === 'recover') {
80
+ const recovered = await explorer.recoverFromBrowserError();
81
+ return { success: recovered, message: recovered ? 'Browser page recovered' : 'Browser recovery failed' };
82
+ }
83
+
84
+ if (action === 'restart') {
85
+ const restarted = await explorer.restartBrowser();
86
+ return { success: restarted, message: restarted ? 'Browser restarted' : 'Browser restart failed' };
87
+ }
88
+
89
+ if (action === 'openFreshTab') {
90
+ await ctx.explorBot.openFreshTab();
91
+ const state = explorer.getStateManager().getCurrentState();
92
+ return { success: true, url: state?.url, title: state?.title };
93
+ }
94
+
95
+ const page = explorer.playwrightHelper?.page;
96
+ if (!page || page.isClosed?.()) return { success: false, message: 'No browser page available. Try browser({ action: "recover" }) first.' };
62
97
 
63
98
  if (action === 'evaluate') {
64
99
  if (!code) return { success: false, message: 'Code required for evaluate action' };
@@ -112,7 +147,7 @@ export function WithWebMode<T extends Constructor>(Base: T) {
112
147
  <web_capabilities>
113
148
  - Page actions: click, pressKey, form (CodeceptJS tools)
114
149
  - Navigation: navigate() — AI-powered navigation to URLs or page descriptions
115
- - Browser diagnostics: browser() — evaluate JS, close tabs, screenshot, reload
150
+ - Browser diagnostics: browser() — inspect status, evaluate JS, close tabs, screenshot, reload, recover closed/crashed pages, restart browser, open a fresh tab
116
151
  - Visual analysis: see() — screenshot-based page verification
117
152
  - Context refresh: context() — get fresh HTML/ARIA snapshot
118
153
  - Visual fallback: visualClick() — coordinate-based click when locators fail