explorbot 0.1.32-beta.1 → 0.2.0

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 (168) hide show
  1. package/README.md +14 -14
  2. package/bin/explorbot-cli.ts +4 -3
  3. package/boat/doc-collector/src/ai/documentarian.ts +22 -27
  4. package/boat/doc-collector/src/ai/tools.ts +67 -10
  5. package/boat/doc-collector/src/docbot.ts +28 -7
  6. package/boat/doc-collector/src/docs-renderer.ts +19 -24
  7. package/boat/doc-collector/src/screenshots.ts +33 -1
  8. package/boat/doc-collector/src/state-diagram.ts +281 -0
  9. package/dist/bin/explorbot-cli.js +3 -3
  10. package/dist/boat/doc-collector/src/ai/documentarian.js +16 -6
  11. package/dist/boat/doc-collector/src/ai/tools.js +47 -10
  12. package/dist/boat/doc-collector/src/docbot.js +25 -6
  13. package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
  14. package/dist/boat/doc-collector/src/screenshots.js +29 -0
  15. package/dist/boat/doc-collector/src/state-diagram.js +200 -0
  16. package/dist/package.json +20 -3
  17. package/dist/src/action-result.d.ts +8 -10
  18. package/dist/src/action-result.js +72 -164
  19. package/dist/src/action.d.ts +3 -10
  20. package/dist/src/action.js +18 -111
  21. package/dist/src/ai/captain/web-mode.js +0 -7
  22. package/dist/src/ai/captain.d.ts +1 -3
  23. package/dist/src/ai/captain.js +4 -19
  24. package/dist/src/ai/conversation.d.ts +2 -5
  25. package/dist/src/ai/conversation.js +9 -28
  26. package/dist/src/ai/driller.d.ts +0 -4
  27. package/dist/src/ai/driller.js +3 -12
  28. package/dist/src/ai/experience-compactor.d.ts +2 -2
  29. package/dist/src/ai/experience-compactor.js +16 -33
  30. package/dist/src/ai/fisherman-tools.js +1 -1
  31. package/dist/src/ai/fisherman.d.ts +0 -1
  32. package/dist/src/ai/fisherman.js +0 -3
  33. package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
  34. package/dist/src/ai/historian/codeceptjs.js +3 -2
  35. package/dist/src/ai/historian/playwright.d.ts +2 -0
  36. package/dist/src/ai/historian/playwright.js +3 -2
  37. package/dist/src/ai/historian/utils.d.ts +0 -1
  38. package/dist/src/ai/historian/utils.js +0 -1
  39. package/dist/src/ai/historian.d.ts +5 -4
  40. package/dist/src/ai/historian.js +1 -5
  41. package/dist/src/ai/navigator.d.ts +3 -5
  42. package/dist/src/ai/navigator.js +11 -46
  43. package/dist/src/ai/pilot.d.ts +1 -3
  44. package/dist/src/ai/pilot.js +6 -17
  45. package/dist/src/ai/planner.d.ts +3 -3
  46. package/dist/src/ai/planner.js +49 -37
  47. package/dist/src/ai/provider.d.ts +4 -1
  48. package/dist/src/ai/provider.js +77 -136
  49. package/dist/src/ai/quartermaster.d.ts +21 -22
  50. package/dist/src/ai/quartermaster.js +4 -10
  51. package/dist/src/ai/rerunner.d.ts +1 -2
  52. package/dist/src/ai/rerunner.js +14 -22
  53. package/dist/src/ai/researcher/cache.js +11 -18
  54. package/dist/src/ai/researcher/deep-analysis.js +3 -6
  55. package/dist/src/ai/researcher/parser.js +1 -1
  56. package/dist/src/ai/researcher/research-result.d.ts +0 -1
  57. package/dist/src/ai/researcher/research-result.js +0 -19
  58. package/dist/src/ai/researcher/sections.js +5 -1
  59. package/dist/src/ai/researcher.js +12 -53
  60. package/dist/src/ai/rules.d.ts +1 -0
  61. package/dist/src/ai/rules.js +22 -15
  62. package/dist/src/ai/session-analyst.js +8 -5
  63. package/dist/src/ai/task-agent.d.ts +1 -1
  64. package/dist/src/ai/task-agent.js +2 -27
  65. package/dist/src/ai/tester.d.ts +0 -2
  66. package/dist/src/ai/tester.js +10 -61
  67. package/dist/src/ai/tools.d.ts +18 -7
  68. package/dist/src/ai/tools.js +119 -101
  69. package/dist/src/commands/compact-command.d.ts +1 -1
  70. package/dist/src/commands/compact-command.js +6 -6
  71. package/dist/src/commands/context-command.js +1 -1
  72. package/dist/src/commands/context-knowledge-command.js +1 -1
  73. package/dist/src/commands/experience-command.js +1 -1
  74. package/dist/src/commands/knows-command.js +1 -1
  75. package/dist/src/commands/learn-command.js +3 -1
  76. package/dist/src/components/AddKnowledge.d.ts +2 -0
  77. package/dist/src/components/AddKnowledge.js +3 -7
  78. package/dist/src/components/App.js +2 -2
  79. package/dist/src/config.d.ts +2 -0
  80. package/dist/src/config.js +12 -0
  81. package/dist/src/experience-tracker.d.ts +11 -6
  82. package/dist/src/experience-tracker.js +81 -154
  83. package/dist/src/explorbot.d.ts +4 -5
  84. package/dist/src/explorbot.js +17 -51
  85. package/dist/src/explorer.d.ts +4 -15
  86. package/dist/src/explorer.js +18 -68
  87. package/dist/src/knowledge-tracker.d.ts +2 -2
  88. package/dist/src/knowledge-tracker.js +38 -45
  89. package/dist/src/state-manager.d.ts +6 -31
  90. package/dist/src/state-manager.js +10 -129
  91. package/dist/src/utils/aria.d.ts +6 -1
  92. package/dist/src/utils/aria.js +4 -1
  93. package/dist/src/utils/cache.d.ts +15 -0
  94. package/dist/src/utils/cache.js +34 -0
  95. package/dist/src/utils/context-formatter.d.ts +6 -0
  96. package/dist/src/utils/context-formatter.js +15 -27
  97. package/dist/src/utils/html.d.ts +5 -0
  98. package/dist/src/utils/html.js +71 -0
  99. package/dist/src/utils/markdown-files.d.ts +10 -0
  100. package/dist/src/utils/markdown-files.js +21 -0
  101. package/dist/src/utils/markdown-query.d.ts +6 -0
  102. package/dist/src/utils/markdown-query.js +14 -1
  103. package/dist/src/utils/page-readiness.d.ts +1 -0
  104. package/dist/src/utils/page-readiness.js +1 -1
  105. package/dist/src/utils/secrets.js +2 -2
  106. package/dist/src/utils/strings.d.ts +2 -0
  107. package/dist/src/utils/strings.js +10 -0
  108. package/dist/src/utils/web-element.d.ts +2 -1
  109. package/dist/src/utils/web-element.js +3 -0
  110. package/dist/src/utils/web-sandbox.d.ts +4 -0
  111. package/dist/src/utils/web-sandbox.js +37 -0
  112. package/package.json +20 -3
  113. package/src/action-result.ts +70 -173
  114. package/src/action.ts +16 -130
  115. package/src/ai/captain/web-mode.ts +0 -7
  116. package/src/ai/captain.ts +5 -17
  117. package/src/ai/conversation.ts +9 -32
  118. package/src/ai/driller.ts +3 -16
  119. package/src/ai/experience-compactor.ts +16 -35
  120. package/src/ai/fisherman-tools.ts +1 -1
  121. package/src/ai/fisherman.ts +0 -4
  122. package/src/ai/historian/codeceptjs.ts +4 -2
  123. package/src/ai/historian/playwright.ts +4 -2
  124. package/src/ai/historian/utils.ts +0 -1
  125. package/src/ai/historian.ts +4 -7
  126. package/src/ai/navigator.ts +13 -49
  127. package/src/ai/pilot.ts +6 -16
  128. package/src/ai/planner.ts +56 -44
  129. package/src/ai/provider.ts +85 -145
  130. package/src/ai/quartermaster.ts +34 -41
  131. package/src/ai/rerunner.ts +17 -21
  132. package/src/ai/researcher/cache.ts +10 -16
  133. package/src/ai/researcher/deep-analysis.ts +3 -6
  134. package/src/ai/researcher/parser.ts +1 -1
  135. package/src/ai/researcher/research-result.ts +0 -18
  136. package/src/ai/researcher/sections.ts +5 -1
  137. package/src/ai/researcher.ts +11 -59
  138. package/src/ai/rules.ts +24 -13
  139. package/src/ai/session-analyst.ts +8 -5
  140. package/src/ai/task-agent.ts +3 -30
  141. package/src/ai/tester.ts +12 -73
  142. package/src/ai/tools.ts +119 -106
  143. package/src/commands/compact-command.ts +7 -8
  144. package/src/commands/context-command.ts +1 -1
  145. package/src/commands/context-knowledge-command.ts +1 -1
  146. package/src/commands/experience-command.ts +1 -1
  147. package/src/commands/knows-command.ts +1 -1
  148. package/src/commands/learn-command.ts +3 -1
  149. package/src/components/AddKnowledge.tsx +5 -7
  150. package/src/components/App.tsx +2 -2
  151. package/src/config.ts +12 -0
  152. package/src/experience-tracker.ts +88 -151
  153. package/src/explorbot.ts +17 -51
  154. package/src/explorer.ts +20 -72
  155. package/src/knowledge-tracker.ts +42 -49
  156. package/src/state-manager.ts +15 -156
  157. package/src/utils/aria.ts +11 -2
  158. package/src/utils/cache.ts +40 -0
  159. package/src/utils/context-formatter.ts +12 -15
  160. package/src/utils/html.ts +79 -0
  161. package/src/utils/markdown-files.ts +30 -0
  162. package/src/utils/markdown-query.ts +15 -1
  163. package/src/utils/page-readiness.ts +1 -1
  164. package/src/utils/secrets.ts +2 -3
  165. package/src/utils/strings.ts +12 -0
  166. package/src/utils/web-element.ts +4 -0
  167. package/src/utils/web-sandbox.ts +43 -0
  168. package/dist/boat/api-tester/example/apibot.config.js +0 -30
package/README.md CHANGED
@@ -14,7 +14,7 @@ npx explorbot start https://your-app.com
14
14
 
15
15
  It runs with no babysitting and reports back what it finds. This is vibe-testing.
16
16
 
17
- New here? Read the [Getting Started guide](docs/getting-started.md).
17
+ New here? Read the [Getting Started guide](docs/basics/getting-started.md).
18
18
 
19
19
  ## Use Cases
20
20
 
@@ -54,11 +54,11 @@ Cheap, fast workers do the clicking and reading; smart managers make the calls
54
54
 
55
55
  | | | |
56
56
  |---|---|---|
57
- | [Researcher](docs/reference/agents.md) | [Planner](docs/reference/agents.md) | [Tester](docs/reference/agents.md) |
58
- | [Pilot](docs/reference/agents.md) | [Captain](docs/reference/agents.md) | [Navigator](docs/reference/agents.md) |
59
- | [Analyst](docs/reference/agents.md) | [Historian](docs/reference/agents.md) | [Fisherman](docs/reference/agents.md) |
57
+ | [Researcher](docs/web-testing/agents.md) | [Planner](docs/web-testing/agents.md) | [Tester](docs/web-testing/agents.md) |
58
+ | [Pilot](docs/web-testing/agents.md) | [Captain](docs/web-testing/agents.md) | [Navigator](docs/web-testing/agents.md) |
59
+ | [Analyst](docs/web-testing/agents.md) | [Historian](docs/web-testing/agents.md) | [Fisherman](docs/web-testing/agents.md) |
60
60
 
61
- See [Agents](docs/reference/agents.md) for what each one does.
61
+ See [Agents](docs/web-testing/agents.md) for what each one does.
62
62
 
63
63
  ## Core Philosophy
64
64
 
@@ -74,7 +74,7 @@ See [Agents](docs/reference/agents.md) for what each one does.
74
74
 
75
75
  When tuned, Explorbot **runs autonomously for hours**, trying new scenarios on its own. The more it runs, the more it learns.
76
76
 
77
- ## What you get
77
+ ## Tests, reports, videos
78
78
 
79
79
  Every run leaves behind:
80
80
 
@@ -83,7 +83,7 @@ Every run leaves behind:
83
83
  - **Videos** — step-by-step screencasts of every run.
84
84
  - **Experience** — what Explorbot learned, reused to test smarter next time.
85
85
 
86
- See [Automated Tests](docs/guides/automated-tests.md) for the test output and [Reporting](docs/guides/reporting.md) for reports.
86
+ See [Automated Tests](docs/web-testing/automated-tests.md) for the test output and [Reporting](docs/workflow/reporting.md) for reports.
87
87
 
88
88
  ## It works with your suite
89
89
 
@@ -94,7 +94,7 @@ Explorbot won't replace your regression tests — it covers what they can't. You
94
94
  - Node.js 24+ or **Bun**
95
95
  - An **AI provider key** — OpenRouter recommended; Groq, Cerebras, OpenAI, Anthropic, and others via the [Vercel AI SDK](https://sdk.vercel.ai/providers)
96
96
  - A **modern terminal** — iTerm2, WARP, Kitty, Ghostty, or Windows Terminal with WSL
97
- - A **compatible web app** — CRUD-heavy apps fit best. See [Prerequisites](docs/reference/prerequisites.md)
97
+ - A **compatible web app** — CRUD-heavy apps fit best. See [Prerequisites](docs/basics/prerequisites.md)
98
98
 
99
99
  If your CI runs Playwright, it runs Explorbot. No GPUs, no special runners.
100
100
 
@@ -123,25 +123,25 @@ npx explorbot start /admin/users
123
123
 
124
124
  Type `/explore`, and Explorbot runs its loop on its own — research, plan, test, repeat — learning from every run.
125
125
 
126
- That's the gist. The [**Getting Started guide**](docs/getting-started.md) walks through the full setup — choosing models, teaching Explorbot to log in, and picking the right feature to start on.
126
+ That's the gist. The [**Getting Started guide**](docs/basics/getting-started.md) walks through the full setup — choosing models, teaching Explorbot to log in, and picking the right feature to start on.
127
127
 
128
128
  ## Teaching Explorbot
129
129
 
130
130
  Explorbot gets better when you tell it about your app:
131
131
 
132
- - **Knowledge** — credentials, form rules, navigation quirks. See [Knowledge](docs/guides/knowledge.md).
132
+ - **Knowledge** — credentials, form rules, navigation quirks. See [Knowledge](docs/workflow/knowledge.md).
133
133
  - **Rules** — per-agent, per-page instructions. See [Configuration](docs/reference/configuration.md#rules).
134
134
  - **Experience** — learned automatically from what works.
135
135
 
136
- Handling logins, cookie banners, modals, and test data takes a few lines — see [Customization](docs/guides/customization.md).
136
+ Handling logins, cookie banners, modals, and test data takes a few lines — see [Customization](docs/web-testing/customization.md).
137
137
 
138
138
  ## It also tests REST APIs
139
139
 
140
- Point Explorbot at an OpenAPI spec and it plans and runs API tests too. See [API Testing](docs/guides/api-testing.md).
140
+ Point Explorbot at an OpenAPI spec and it plans and runs API tests too. See [API Testing](docs/api-testing/basics.md).
141
141
 
142
142
  ## Keep going
143
143
 
144
- When you're ready to go deeper, the [full documentation](docs/README.md) covers everything, starting with the [Getting Started guide](docs/getting-started.md).
144
+ When you're ready to go deeper, the [full documentation](docs/) covers everything, starting with the [Getting Started guide](docs/basics/getting-started.md).
145
145
 
146
146
  ## FAQ
147
147
 
@@ -163,7 +163,7 @@ No. With fast open models (e.g. `openai/gpt-oss-20b` on OpenRouter or Groq), exp
163
163
  Not yet.
164
164
 
165
165
  **Can I build my own agents with it?**
166
- Yes, use the programmatic API. See [Scripting](docs/contributing/scripting.md).
166
+ Yes, use the programmatic API. See [Scripting](docs/reference/scripting.md).
167
167
 
168
168
  **Can I do the same in Cursor with Playwright MCP?**
169
169
  Good luck running it on CI!
@@ -509,9 +509,10 @@ program
509
509
  path: options.path || process.cwd(),
510
510
  });
511
511
 
512
+ const { KnowledgeTracker } = await import('../src/knowledge-tracker.js');
513
+ const tracker = new KnowledgeTracker();
514
+
512
515
  if (url && description) {
513
- const { KnowledgeTracker } = await import('../src/knowledge-tracker.js');
514
- const tracker = new KnowledgeTracker();
515
516
  const result = tracker.addKnowledge(url, description);
516
517
  const action = result.isNewFile ? 'Created' : 'Updated';
517
518
  console.log(`Knowledge ${action} in: ${result.filename}`);
@@ -519,7 +520,7 @@ program
519
520
  }
520
521
 
521
522
  const AddKnowledge = (await import('../src/components/AddKnowledge.js')).default;
522
- render(React.createElement(AddKnowledge, { initialUrl: url || '' }), {
523
+ render(React.createElement(AddKnowledge, { initialUrl: url || '', knowledgeTracker: tracker }), {
523
524
  exitOnCtrlC: false,
524
525
  patchConsole: false,
525
526
  });
@@ -5,7 +5,7 @@ import type Explorer from '../../../../src/explorer.ts';
5
5
  import type { WebPageState } from '../../../../src/state-manager.ts';
6
6
  import { tag } from '../../../../src/utils/logger.ts';
7
7
  import type { DocbotConfig } from '../config.ts';
8
- import { collectDocInteractions } from './tools.ts';
8
+ import { type CaptureInteractionState, type DocStateTransition, collectDocInteractions } from './tools.ts';
9
9
 
10
10
  class Documentarian {
11
11
  private provider: AIProvider;
@@ -18,7 +18,7 @@ class Documentarian {
18
18
  this.explorer = explorer;
19
19
  }
20
20
 
21
- async document(state: WebPageState, research: string): Promise<PageDocumentation> {
21
+ async document(state: WebPageState, research: string, captureState?: CaptureInteractionState): Promise<PageDocumentation> {
22
22
  const interactiveEnabled = this.config.docs?.interactive === true && this.explorer;
23
23
  if (!interactiveEnabled) {
24
24
  tag('info').log('Documentarian: Using static mode (interactive disabled or no explorer)');
@@ -26,7 +26,7 @@ class Documentarian {
26
26
  }
27
27
 
28
28
  tag('info').log('Documentarian: Using interactive mode with tools');
29
- return this.documentWithInteraction(state, research);
29
+ return this.documentWithInteraction(state, research, captureState);
30
30
  }
31
31
 
32
32
  private async documentStatic(state: WebPageState, research: string): Promise<PageDocumentation> {
@@ -41,12 +41,13 @@ class Documentarian {
41
41
  }
42
42
  }
43
43
 
44
- private async documentWithInteraction(state: WebPageState, research: string): Promise<PageDocumentation> {
44
+ private async documentWithInteraction(state: WebPageState, research: string, captureState?: CaptureInteractionState): Promise<PageDocumentation> {
45
+ let meaningfulInteractions: StateTransition[] = [];
45
46
  try {
46
47
  tag('info').log('Starting interactive exploration...');
47
48
 
48
- const deterministicInteractions = await collectDocInteractions(this.explorer!, state, research, this.config);
49
- const meaningfulInteractions = this.getMeaningfulInteractions(deterministicInteractions);
49
+ const deterministicInteractions = await collectDocInteractions(this.explorer!, state, research, this.config, captureState);
50
+ meaningfulInteractions = this.getMeaningfulInteractions(deterministicInteractions);
50
51
  if (meaningfulInteractions.length > 0) {
51
52
  tag('success').log(`Collected ${meaningfulInteractions.length} deterministic interactions`);
52
53
  return await this.generateDocumentationWithInteractions(state, research, meaningfulInteractions);
@@ -61,7 +62,20 @@ class Documentarian {
61
62
  return this.documentStatic(state, research);
62
63
  } catch (error) {
63
64
  const message = error instanceof Error ? error.message : String(error);
64
- tag('warning').log(`Interactive documentation failed: ${message}. Falling back to static.`);
65
+ tag('warning').log(`Interactive documentation failed: ${message}.`);
66
+ if (meaningfulInteractions.length > 0) {
67
+ tag('info').log(`Preserving ${meaningfulInteractions.length} observed interaction(s) without AI summary.`);
68
+ return this.normalizeDocumentation(
69
+ {
70
+ summary: `Observed ${meaningfulInteractions.length} interaction(s); AI-generated summary was unavailable.`,
71
+ can: [],
72
+ might: [],
73
+ interactions: meaningfulInteractions,
74
+ },
75
+ state,
76
+ research
77
+ );
78
+ }
65
79
  return this.documentStatic(state, research);
66
80
  }
67
81
  }
@@ -354,26 +368,7 @@ const pageDocumentationSchema = z.object({
354
368
  interactions: z.array(stateTransitionSchema).nullable(),
355
369
  });
356
370
 
357
- type StateTransition = {
358
- action: string;
359
- before: string;
360
- after: string;
361
- targetUrl?: string | null;
362
- discoveredUrls?: string[] | null;
363
- newCapabilities?: string[] | null;
364
- element?: {
365
- role: string;
366
- name: string;
367
- section: string;
368
- container?: string | null;
369
- locator?: string | null;
370
- } | null;
371
- changes?: {
372
- urlChanged: boolean;
373
- newElements: number;
374
- removedElements: number;
375
- } | null;
376
- };
371
+ type StateTransition = DocStateTransition;
377
372
  type PageDocumentation = Omit<z.infer<typeof pageDocumentationSchema>, 'interactions'> & {
378
373
  interactions?: StateTransition[];
379
374
  qualityNotes?: string[];
@@ -1,6 +1,7 @@
1
1
  import { type ResearchElement, parseResearchSections } from '../../../../src/ai/researcher/parser.ts';
2
2
  import type Explorer from '../../../../src/explorer.ts';
3
3
  import type { WebPageState } from '../../../../src/state-manager.ts';
4
+ import { detectFocusArea } from '../../../../src/utils/aria.ts';
4
5
  import type { DocbotConfig } from '../config.ts';
5
6
 
6
7
  export interface DocStateTransition {
@@ -12,6 +13,8 @@ export interface DocStateTransition {
12
13
  newCapabilities?: string[];
13
14
  element?: InteractionElement;
14
15
  changes?: InteractionChanges;
16
+ targetState?: InteractionState;
17
+ screenshot?: InteractionScreenshot;
15
18
  }
16
19
 
17
20
  interface InteractionCandidate {
@@ -35,6 +38,19 @@ interface InteractionChanges {
35
38
  removedElements: number;
36
39
  }
37
40
 
41
+ export interface InteractionState {
42
+ kind: 'page' | 'dialog' | 'modal' | 'section';
43
+ label: string;
44
+ url: string;
45
+ }
46
+
47
+ export interface InteractionScreenshot {
48
+ title: string;
49
+ relativePath: string;
50
+ }
51
+
52
+ export type CaptureInteractionState = (state: WebPageState, transition: DocStateTransition) => Promise<InteractionScreenshot | null>;
53
+
38
54
  const DEFAULT_MAX_PRIMARY_CANDIDATES = 3;
39
55
  const DEFAULT_MAX_INTERACTIONS = 5;
40
56
  const MAX_LINKS = 15;
@@ -42,14 +58,14 @@ const DEFAULT_WAIT_MS = 700;
42
58
  const TAB_WAIT_MS = 500;
43
59
  const DEFAULT_DENIED_ACTION_LABELS = ['delete', 'remove', 'destroy', 'archive', 'discard', 'logout', 'sign out', 'signout', 'sign_out', 'erase', 'drop'];
44
60
 
45
- export async function collectDocInteractions(explorer: Explorer, state: WebPageState, research: string, config: DocbotConfig = {}): Promise<DocStateTransition[]> {
61
+ export async function collectDocInteractions(explorer: Explorer, state: WebPageState, research: string, config: DocbotConfig = {}, captureState?: CaptureInteractionState): Promise<DocStateTransition[]> {
46
62
  const sections = parseResearchSections(research);
47
63
  const transitions: DocStateTransition[] = [];
48
64
  const maxInteractions = getPositiveConfigNumber(config.docs?.maxInteractions, DEFAULT_MAX_INTERACTIONS);
49
65
  const tabGroup = findTabGroup(sections);
50
66
 
51
67
  if (tabGroup) {
52
- transitions.push(...(await exploreTabGroup(explorer, tabGroup, state.url, maxInteractions)));
68
+ transitions.push(...(await exploreTabGroup(explorer, tabGroup, state.url, maxInteractions, captureState)));
53
69
  }
54
70
 
55
71
  for (const candidate of findActionCandidates(sections, config)) {
@@ -57,7 +73,7 @@ export async function collectDocInteractions(explorer: Explorer, state: WebPageS
57
73
  break;
58
74
  }
59
75
 
60
- const transition = await executeInteraction(explorer, candidate, state.url, DEFAULT_WAIT_MS);
76
+ const transition = await executeInteraction(explorer, candidate, state.url, DEFAULT_WAIT_MS, captureState);
61
77
  if (!transition) {
62
78
  continue;
63
79
  }
@@ -76,7 +92,7 @@ export function pickDocActionCandidates(research: string, config: DocbotConfig =
76
92
  }));
77
93
  }
78
94
 
79
- async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: ResearchElement[]; container?: string; sectionName: string }, restoreUrl: string, maxInteractions: number): Promise<DocStateTransition[]> {
95
+ async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: ResearchElement[]; container?: string; sectionName: string }, restoreUrl: string, maxInteractions: number, captureState?: CaptureInteractionState): Promise<DocStateTransition[]> {
80
96
  const transitions: DocStateTransition[] = [];
81
97
 
82
98
  for (const element of tabGroup.elements) {
@@ -93,7 +109,8 @@ async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: Researc
93
109
  sectionName: tabGroup.sectionName,
94
110
  },
95
111
  restoreUrl,
96
- TAB_WAIT_MS
112
+ TAB_WAIT_MS,
113
+ captureState
97
114
  );
98
115
  if (!transition) {
99
116
  continue;
@@ -106,7 +123,7 @@ async function exploreTabGroup(explorer: Explorer, tabGroup: { elements: Researc
106
123
  return transitions;
107
124
  }
108
125
 
109
- async function executeInteraction(explorer: Explorer, candidate: InteractionCandidate, restoreUrl: string, waitMs: number): Promise<DocStateTransition | null> {
126
+ async function executeInteraction(explorer: Explorer, candidate: InteractionCandidate, restoreUrl: string, waitMs: number, captureState?: CaptureInteractionState): Promise<DocStateTransition | null> {
110
127
  const beforeState = explorer.getStateManager().getCurrentState();
111
128
  if (!beforeState) {
112
129
  return null;
@@ -132,7 +149,14 @@ async function executeInteraction(explorer: Explorer, candidate: InteractionCand
132
149
  removedElements: ariaChanges.removedCount,
133
150
  });
134
151
 
135
- if (urlChanged) {
152
+ if (captureState && isMeaningfulStateTransition(transition)) {
153
+ const screenshot = await captureState(afterState, transition);
154
+ if (screenshot) {
155
+ transition.screenshot = screenshot;
156
+ }
157
+ }
158
+
159
+ if (urlChanged || ariaChanges.newCount > 0) {
136
160
  await restoreInteractionState(explorer, restoreUrl);
137
161
  }
138
162
 
@@ -143,7 +167,7 @@ async function attemptInteraction(explorer: Explorer, candidate: InteractionCand
143
167
  const action = explorer.createAction();
144
168
 
145
169
  for (const command of buildClickCommands(candidate.element, candidate.container)) {
146
- const success = await action.attempt(command, buildPurpose(candidate), false);
170
+ const success = await action.attempt(command, buildPurpose(candidate));
147
171
  if (success) {
148
172
  return true;
149
173
  }
@@ -155,7 +179,7 @@ async function attemptInteraction(explorer: Explorer, candidate: InteractionCand
155
179
  async function restoreInteractionState(explorer: Explorer, restoreUrl: string, primaryCommand?: string | null): Promise<void> {
156
180
  if (primaryCommand) {
157
181
  const action = explorer.createAction();
158
- const restored = await action.attempt(primaryCommand, `Restore initial state on ${restoreUrl}`, false);
182
+ const restored = await action.attempt(primaryCommand, `Restore initial state on ${restoreUrl}`);
159
183
  if (restored) {
160
184
  await wait(TAB_WAIT_MS);
161
185
  return;
@@ -163,7 +187,7 @@ async function restoreInteractionState(explorer: Explorer, restoreUrl: string, p
163
187
  }
164
188
 
165
189
  const action = explorer.createAction();
166
- await action.attempt(`I.amOnPage(${JSON.stringify(restoreUrl)})`, `Restore page ${restoreUrl}`, false);
190
+ await action.attempt(`I.amOnPage(${JSON.stringify(restoreUrl)})`, `Restore page ${restoreUrl}`);
167
191
  }
168
192
 
169
193
  function buildTransition(candidate: InteractionCandidate, beforeState: WebPageState, afterState: WebPageState, changes: InteractionChanges): DocStateTransition {
@@ -175,6 +199,7 @@ function buildTransition(candidate: InteractionCandidate, beforeState: WebPageSt
175
199
  newCapabilities: collectDiscoveryNotes(afterState, changes),
176
200
  element: buildInteractionElement(candidate),
177
201
  changes,
202
+ targetState: describeTargetState(beforeState, afterState, candidate),
178
203
  };
179
204
 
180
205
  if (changes.urlChanged) {
@@ -184,6 +209,38 @@ function buildTransition(candidate: InteractionCandidate, beforeState: WebPageSt
184
209
  return transition;
185
210
  }
186
211
 
212
+ function describeTargetState(beforeState: WebPageState, afterState: WebPageState, candidate: InteractionCandidate): InteractionState {
213
+ const beforeFocus = detectFocusArea(beforeState.ariaSnapshot || null);
214
+ const afterFocus = detectFocusArea(afterState.ariaSnapshot || null);
215
+ if (afterFocus.detected && (!beforeFocus.detected || beforeFocus.name !== afterFocus.name)) {
216
+ return {
217
+ kind: afterFocus.type || 'dialog',
218
+ label: afterFocus.name || candidate.element.name.trim(),
219
+ url: afterState.url,
220
+ };
221
+ }
222
+
223
+ const beforePath = beforeState.url.split('?')[0].split('#')[0];
224
+ const afterPath = afterState.url.split('?')[0].split('#')[0];
225
+ const headings = collectHeadings(afterState);
226
+ let kind: InteractionState['kind'] = 'page';
227
+ if (beforePath === afterPath) {
228
+ kind = 'section';
229
+ }
230
+ return {
231
+ kind,
232
+ label: headings[0] || afterState.title || candidate.element.name.trim(),
233
+ url: afterState.url,
234
+ };
235
+ }
236
+
237
+ function isMeaningfulStateTransition(transition: DocStateTransition): boolean {
238
+ if (transition.targetUrl || transition.changes?.urlChanged) {
239
+ return true;
240
+ }
241
+ return (transition.changes?.newElements || 0) > 0;
242
+ }
243
+
187
244
  function buildInteractionElement(candidate: InteractionCandidate): InteractionElement {
188
245
  const element: InteractionElement = {
189
246
  role: candidate.role,
@@ -8,9 +8,10 @@ import { sanitizeFilename } from '../../../src/utils/strings.ts';
8
8
  import { Documentarian, type PageDocumentation } from './ai/documentarian.ts';
9
9
  import { type DocbotConfig, DocbotConfigParser } from './config.ts';
10
10
  import { type DocumentedPage, type SkippedPage, renderPageDocumentation, renderSpecIndex } from './docs-renderer.ts';
11
+ import { renderMermaidBody } from './state-diagram.ts';
11
12
  import { getDocPageKey, shouldCrawlDocPath } from './path-filter.ts';
12
13
  import { extractResearchNavigationTargets } from './research-navigation.ts';
13
- import { type DocumentationScreenshot, captureDocumentationScreenshots } from './screenshots.ts';
14
+ import { type DocumentationScreenshot, captureDocumentationScreenshots, captureInteractionScreenshot } from './screenshots.ts';
14
15
 
15
16
  class DocBot {
16
17
  private explorBot: ExplorBot;
@@ -111,7 +112,17 @@ class DocBot {
111
112
  screenshot: this.shouldUseScreenshots(),
112
113
  force: true,
113
114
  });
114
- const documentation = await this.documentarian.document(state, research);
115
+ const pagePath = this.getPageFilePath(state.url);
116
+ const documentation = await this.documentarian.document(state, research, async (interactionState, transition) => {
117
+ if (!this.shouldUseScreenshots()) {
118
+ return null;
119
+ }
120
+ return captureInteractionScreenshot(this.explorBot.getExplorer(), interactionState, transition, {
121
+ pageFilePath: pagePath,
122
+ screenshotsDir: this.getScreenshotsDir(),
123
+ config: this.config,
124
+ });
125
+ });
115
126
  const lowSignalReason = this.getLowSignalReason(documentation, research);
116
127
  if (lowSignalReason) {
117
128
  skipped.push({
@@ -134,6 +145,7 @@ class DocBot {
134
145
  mightActions: documentation.might.map((item) => item.action),
135
146
  interactionActions: (documentation.interactions || []).map((item) => item.action),
136
147
  qualityNotes: documentation.qualityNotes || [],
148
+ interactions: documentation.interactions || [],
137
149
  filePath,
138
150
  });
139
151
  documented.add(pageKey);
@@ -159,12 +171,13 @@ class DocBot {
159
171
  }
160
172
  }
161
173
 
162
- const indexPath = this.saveIndex(effectiveStartPath, pages, skipped, effectiveMaxPages);
174
+ const { indexPath, diagramPath } = this.saveIndex(effectiveStartPath, pages, skipped, effectiveMaxPages);
163
175
 
164
176
  return {
165
177
  pages,
166
178
  skipped,
167
179
  indexPath,
180
+ diagramPath,
168
181
  outputDir: this.configParser.getOutputDir(),
169
182
  };
170
183
  }
@@ -385,6 +398,10 @@ class DocBot {
385
398
  return null;
386
399
  }
387
400
 
401
+ if ((documentation.interactions || []).length > 0) {
402
+ return null;
403
+ }
404
+
388
405
  const interactiveCount = this.countInteractiveElements(research);
389
406
  if (interactiveCount >= minInteractiveElements) {
390
407
  return null;
@@ -417,10 +434,13 @@ class DocBot {
417
434
  });
418
435
  }
419
436
 
420
- private saveIndex(startPath: string, pages: DocumentedPage[], skipped: SkippedPage[], maxPages: number): string {
421
- const indexPath = path.join(this.configParser.getOutputDir(), 'spec.md');
422
- writeFileSync(indexPath, renderSpecIndex(this.configParser.getOutputDir(), startPath, pages, skipped, maxPages), 'utf8');
423
- return indexPath;
437
+ private saveIndex(startPath: string, pages: DocumentedPage[], skipped: SkippedPage[], maxPages: number): { indexPath: string; diagramPath: string } {
438
+ const outputDir = this.configParser.getOutputDir();
439
+ const indexPath = path.join(outputDir, 'index.md');
440
+ writeFileSync(indexPath, renderSpecIndex(outputDir, startPath, pages, skipped, maxPages), 'utf8');
441
+ const diagramPath = path.join(outputDir, 'state-diagram.mmd');
442
+ writeFileSync(diagramPath, renderMermaidBody(outputDir, pages), 'utf8');
443
+ return { indexPath, diagramPath };
424
444
  }
425
445
 
426
446
  private getPagesDir(): string {
@@ -461,6 +481,7 @@ interface CollectionResult {
461
481
  pages: DocumentedPage[];
462
482
  skipped: SkippedPage[];
463
483
  indexPath: string;
484
+ diagramPath: string;
464
485
  outputDir: string;
465
486
  }
466
487
 
@@ -1,7 +1,9 @@
1
1
  import path from 'node:path';
2
- import type { WebPageState } from '../../../src/state-manager.ts';
2
+ import { type WebPageState } from '../../../src/state-manager.ts';
3
3
  import type { PageDocumentation, StateTransition } from './ai/documentarian.ts';
4
4
  import type { DocumentationScreenshot } from './screenshots.ts';
5
+ import { buildStateGraph, renderMermaidFromGraph, renderStateMapFromGraph, type DocumentedPage, type SkippedPage } from './state-diagram.ts';
6
+ import { normalizeInlineText } from '../../../src/utils/strings.ts';
5
7
 
6
8
  function renderPageDocumentation(state: WebPageState, documentation: PageDocumentation, screenshots: DocumentationScreenshot[] = []): string {
7
9
  const lines: string[] = [];
@@ -48,6 +50,10 @@ function renderPageDocumentation(state: WebPageState, documentation: PageDocumen
48
50
  }
49
51
  lines.push('');
50
52
  }
53
+ if (transition.screenshot) {
54
+ lines.push(`![${normalizeInlineText(transition.screenshot.title)}](${transition.screenshot.relativePath})`);
55
+ lines.push('');
56
+ }
51
57
  }
52
58
  }
53
59
 
@@ -109,6 +115,18 @@ function renderSpecIndex(outputDir: string, startPath: string, pages: Documented
109
115
  lines.push(`Pages skipped: ${skipped.length}`);
110
116
  lines.push(`Max pages: ${maxPages}`);
111
117
  lines.push('');
118
+ const graph = buildStateGraph(outputDir, pages);
119
+ lines.push('## State Transitions');
120
+ lines.push('');
121
+ lines.push(`\`\`\`mermaid\n${renderMermaidFromGraph(graph)}\n\`\`\``);
122
+ lines.push('');
123
+ const stateMap = renderStateMapFromGraph(graph);
124
+ if (stateMap) {
125
+ lines.push('### State Index');
126
+ lines.push('');
127
+ lines.push(stateMap);
128
+ lines.push('');
129
+ }
112
130
  lines.push('## Pages');
113
131
  lines.push('');
114
132
 
@@ -227,28 +245,5 @@ function ensureSentence(text: string): string {
227
245
  return `${trimmed}.`;
228
246
  }
229
247
 
230
- function normalizeInlineText(text: string): string {
231
- return text.normalize('NFKC').replace(/\s+/g, ' ').trim();
232
- }
233
-
234
- interface DocumentedPage {
235
- url: string;
236
- title: string;
237
- summary: string;
238
- canCount: number;
239
- mightCount: number;
240
- interactionCount: number;
241
- canActions: string[];
242
- mightActions: string[];
243
- interactionActions: string[];
244
- qualityNotes: string[];
245
- filePath: string;
246
- }
247
-
248
- interface SkippedPage {
249
- url: string;
250
- reason: string;
251
- }
252
-
253
248
  export { renderPageDocumentation, renderSpecIndex, ensureSentence, normalizeAction };
254
249
  export type { DocumentedPage, SkippedPage, StateTransition };
@@ -3,7 +3,9 @@ import path from 'node:path';
3
3
  import { parseResearchSections } from '../../../src/ai/researcher/parser.ts';
4
4
  import type Explorer from '../../../src/explorer.ts';
5
5
  import type { WebPageState } from '../../../src/state-manager.ts';
6
+ import { detectFocusArea } from '../../../src/utils/aria.ts';
6
7
  import { safeFilename, sanitizeFilename } from '../../../src/utils/strings.ts';
8
+ import type { DocStateTransition } from './ai/tools.ts';
7
9
  import type { DocbotConfig } from './config.ts';
8
10
 
9
11
  const DEFAULT_MAX_SECTION_SCREENSHOTS = 8;
@@ -59,6 +61,36 @@ export function getScreenshotSections(research: string): ScreenshotSection[] {
59
61
  return sections;
60
62
  }
61
63
 
64
+ export async function captureInteractionScreenshot(explorer: Explorer, state: WebPageState, transition: DocStateTransition, options: DocumentationScreenshotOptions): Promise<DocumentationScreenshot | null> {
65
+ const page = explorer.playwrightHelper?.page;
66
+ if (!page) {
67
+ return null;
68
+ }
69
+
70
+ mkdirSync(options.screenshotsDir, { recursive: true });
71
+ const pageName = sanitizeFilename(state.url || 'page') || 'page';
72
+ const stateName = sanitizeFilename(transition.targetState?.label || transition.action) || 'state';
73
+ const filePath = path.join(options.screenshotsDir, safeFilename(`${pageName}_${stateName}`, '.png'));
74
+ const focus = detectFocusArea(state.ariaSnapshot || null);
75
+
76
+ try {
77
+ if (focus.detected) {
78
+ await page.locator('[role="dialog"], [role="alertdialog"], [aria-modal="true"]').last().screenshot({ path: filePath });
79
+ } else {
80
+ await page.screenshot({ path: filePath });
81
+ }
82
+ } catch {
83
+ return null;
84
+ }
85
+
86
+ return {
87
+ title: transition.targetState?.label || transition.action,
88
+ path: filePath,
89
+ relativePath: toMarkdownPath(options.pageFilePath, filePath),
90
+ kind: 'state',
91
+ };
92
+ }
93
+
62
94
  async function captureFullPageScreenshot(page: any, pageName: string, options: DocumentationScreenshotOptions): Promise<DocumentationScreenshot | null> {
63
95
  const filePath = path.join(options.screenshotsDir, safeFilename(`${pageName}_page`, '.png'));
64
96
  try {
@@ -110,7 +142,7 @@ export interface DocumentationScreenshot {
110
142
  title: string;
111
143
  path: string;
112
144
  relativePath: string;
113
- kind: 'page' | 'section';
145
+ kind: 'page' | 'section' | 'state';
114
146
  selector?: string;
115
147
  }
116
148