explorbot 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/boat/api-tester/src/apibot.ts +18 -2
- package/boat/api-tester/src/cli.ts +85 -274
- package/boat/api-tester/src/commands/api-command.ts +10 -0
- package/boat/api-tester/src/commands/explore-command.ts +52 -0
- package/boat/api-tester/src/commands/init-command.ts +119 -0
- package/boat/api-tester/src/commands/know-command.ts +44 -0
- package/boat/api-tester/src/commands/plan-command.ts +42 -0
- package/boat/api-tester/src/commands/test-command.ts +54 -0
- package/boat/prima/src/prima.ts +8 -3
- package/dist/boat/api-tester/src/apibot.js +14 -1
- package/dist/boat/api-tester/src/cli.js +87 -243
- package/dist/boat/api-tester/src/commands/api-command.js +7 -0
- package/dist/boat/api-tester/src/commands/explore-command.js +41 -0
- package/dist/boat/api-tester/src/commands/init-command.js +88 -0
- package/dist/boat/api-tester/src/commands/know-command.js +39 -0
- package/dist/boat/api-tester/src/commands/plan-command.js +37 -0
- package/dist/boat/api-tester/src/commands/test-command.js +45 -0
- package/dist/boat/prima/src/prima.js +10 -3
- package/dist/package.json +4 -4
- package/dist/src/ai/fisherman/tools.js +7 -1
- package/dist/src/ai/fisherman.js +2 -1
- package/dist/src/ai/pilot.d.ts +0 -1
- package/dist/src/ai/pilot.js +8 -24
- package/dist/src/ai/planner.d.ts +4 -0
- package/dist/src/ai/planner.js +28 -0
- package/dist/src/ai/provider.js +3 -1
- package/dist/src/ai/researcher/deep-analysis.d.ts +1 -1
- package/dist/src/ai/researcher/deep-analysis.js +14 -6
- package/dist/src/ai/rules.js +8 -7
- package/dist/src/ai/scout/tools.d.ts +17 -0
- package/dist/src/ai/scout/tools.js +130 -0
- package/dist/src/ai/scout.d.ts +21 -0
- package/dist/src/ai/scout.js +150 -0
- package/dist/src/ai/tools.d.ts +1 -1
- package/dist/src/ai/tools.js +62 -31
- package/dist/src/api/spec-reader.d.ts +1 -0
- package/dist/src/api/spec-reader.js +93 -1
- package/dist/src/application-spec.d.ts +3 -0
- package/dist/src/application-spec.js +21 -5
- package/dist/src/commands/base-command.d.ts +3 -3
- package/dist/src/commands/init-command.d.ts +3 -0
- package/dist/src/commands/init-command.js +6 -3
- package/dist/src/config.d.ts +6 -1
- package/dist/src/explorbot.d.ts +3 -0
- package/dist/src/explorbot.js +33 -0
- package/dist/src/explorer.d.ts +1 -1
- package/dist/src/explorer.js +1 -1
- package/dist/src/knowledge-tracker.d.ts +1 -0
- package/dist/src/knowledge-tracker.js +3 -0
- package/dist/src/utils/aria-ref.d.ts +16 -0
- package/dist/src/utils/aria-ref.js +47 -0
- package/dist/src/utils/aria.js +3 -3
- package/dist/src/utils/html-diff.js +4 -1
- package/dist/src/utils/web-annotate.js +3 -15
- package/dist/src/utils/web-element.d.ts +0 -2
- package/dist/src/utils/web-element.js +0 -8
- package/docs/api-testing/basics.md +26 -2
- package/docs/reference/configuration.md +28 -1
- package/docs/superpowers/specs/2026-09-09-pagination-rule-design.md +317 -0
- package/docs/web-testing/agents.md +9 -1
- package/docs/web-testing/planner.md +5 -0
- package/docs/workflow/application-spec.md +4 -0
- package/package.json +4 -4
- package/src/ai/fisherman/tools.ts +8 -1
- package/src/ai/fisherman.ts +2 -1
- package/src/ai/pilot.ts +8 -25
- package/src/ai/planner.ts +33 -0
- package/src/ai/provider.ts +2 -1
- package/src/ai/researcher/deep-analysis.ts +13 -6
- package/src/ai/rules.ts +8 -7
- package/src/ai/scout/tools.ts +150 -0
- package/src/ai/scout.ts +173 -0
- package/src/ai/tools.ts +75 -38
- package/src/api/spec-reader.ts +106 -1
- package/src/application-spec.ts +22 -4
- package/src/commands/base-command.ts +3 -3
- package/src/commands/init-command.ts +6 -3
- package/src/config.ts +7 -0
- package/src/explorbot.ts +36 -0
- package/src/explorer.ts +1 -1
- package/src/knowledge-tracker.ts +4 -0
- package/src/utils/aria-ref.ts +61 -0
- package/src/utils/aria.ts +3 -3
- package/src/utils/html-diff.ts +3 -1
- package/src/utils/web-annotate.ts +3 -15
- package/src/utils/web-element.ts +0 -9
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# Pagination rule: page numbers and infinite scroll
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
Explorbot cannot reach list content that is not already loaded. A list shows a window onto
|
|
6
|
+
a larger collection, and when the item under test is outside that window the tester concludes
|
|
7
|
+
it is absent.
|
|
8
|
+
|
|
9
|
+
Nothing in the repo scrolls anything. `<actions>` (`src/ai/rules.ts:307`) documents no scroll
|
|
10
|
+
command, so the model has no way to know scrolling is available. Where scrolling is mentioned
|
|
11
|
+
at all it is as a passing hint inside an unrelated suggestion string
|
|
12
|
+
(`src/ai/tools.ts:875`, `:989`, `:1297`).
|
|
13
|
+
|
|
14
|
+
Two strategies cover practically every paginated list on the web:
|
|
15
|
+
|
|
16
|
+
- **Controls that replace the window** — next, previous, page numbers, load more.
|
|
17
|
+
- **Appending on scroll** — new items are fetched and appended as the list is scrolled.
|
|
18
|
+
|
|
19
|
+
The second is the hard one, because the scroller is often a container with its own scrollbar
|
|
20
|
+
rather than the page. `I.scrollPageToBottom()` moves the window and leaves such a container
|
|
21
|
+
untouched.
|
|
22
|
+
|
|
23
|
+
## Approach
|
|
24
|
+
|
|
25
|
+
Detection finds out which strategy a list uses; a rule tells the tester what to do about it,
|
|
26
|
+
and is injected only when there is something to say. No new tool: the gesture already exists in
|
|
27
|
+
CodeceptJS and is reachable through `form`.
|
|
28
|
+
|
|
29
|
+
Splitting it that way is what keeps the rule short. The tester never has to discover anything
|
|
30
|
+
at run time, and never carries guidance for a strategy this page does not use.
|
|
31
|
+
|
|
32
|
+
### Why `I.scrollTo` is sufficient
|
|
33
|
+
|
|
34
|
+
`Playwright.scrollTo(locator)` calls `el.scrollIntoViewIfNeeded()`
|
|
35
|
+
(`node_modules/codeceptjs/lib/helper/Playwright.js:1679`), which scrolls **every scrollable
|
|
36
|
+
ancestor** of the target. Pointing it at the last item currently in a list therefore scrolls
|
|
37
|
+
that list's own scroller.
|
|
38
|
+
|
|
39
|
+
Verified against Chromium on a page with both a scrollable `div` and a scrollable window:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
before {"box":0, "win":0}
|
|
43
|
+
scrollIntoView {"box":2200, "win":1821}
|
|
44
|
+
after cjs tail {"box":2200, "win":1821}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Both scrollers moved. The `window.scrollBy` call CodeceptJS runs afterwards is a no-op: it
|
|
48
|
+
passes one object to a two-positional-parameter function, so both deltas coerce to `NaN` and
|
|
49
|
+
normalize to zero. Nothing needs to be worked around.
|
|
50
|
+
|
|
51
|
+
The gesture is a single line beginning with `I.`, so it passes the `form` tool's line check
|
|
52
|
+
(`src/ai/tools.ts:384`) and needs no new tool.
|
|
53
|
+
|
|
54
|
+
### Signals already on the wire
|
|
55
|
+
|
|
56
|
+
- **`pageDiff.ariaChanges` / `ariaChangeCount`** — `diffAriaSnapshots` (`src/utils/aria.ts:503`)
|
|
57
|
+
counts node summaries, so appended rows surface as counted additions.
|
|
58
|
+
- **`pageDiff.requests`** — `Action.recordNetworkCall` (`src/action.ts:314`) captures same-origin
|
|
59
|
+
xhr/fetch as `{method, path, status}`, deduped. It stores `url.pathname` only, so the query
|
|
60
|
+
string is dropped: presence of a call, never a page number.
|
|
61
|
+
|
|
62
|
+
## Design
|
|
63
|
+
|
|
64
|
+
### A. Researcher determines each list's pagination strategy
|
|
65
|
+
|
|
66
|
+
Four steps, cheapest first, stopping as soon as one answers. This is the escalation ladder from
|
|
67
|
+
CLAUDE.md end to end: a table lookup, then AI judgment, then a probe whose result converts
|
|
68
|
+
judgment back into a recorded fact.
|
|
69
|
+
|
|
70
|
+
**0. Do the ARIA/HTML conventions name it? (deterministic, no research, no AI)**
|
|
71
|
+
|
|
72
|
+
Some markup states the answer outright. These are spec-defined attributes and values, so this
|
|
73
|
+
tier is a lookup, not a guess — and it is the only step that works when research has not run.
|
|
74
|
+
|
|
75
|
+
| Marker | Means | Available in |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `[aria-current="page"]` | current page of a pagination set | HTML only |
|
|
78
|
+
| `a[rel="next"]`, `a[rel="prev"]` | sequential document relations | HTML only |
|
|
79
|
+
| `[role="feed"]` | scrollable list that grows as it is scrolled | HTML and ARIA snapshot |
|
|
80
|
+
| `[aria-setsize="-1"]` | total count unknown, so the set loads lazily | HTML only |
|
|
81
|
+
|
|
82
|
+
The first two mean `controls`, the last two mean `infinite`.
|
|
83
|
+
|
|
84
|
+
Verified against Chromium: `ariaSnapshot()` does **not** emit `aria-current`, so a link marked
|
|
85
|
+
as the current page is indistinguishable from its neighbours in the ARIA path. Explorbot also
|
|
86
|
+
dissolves `navigation` wrappers (`src/utils/aria.ts:46`, `:147`) and treats the role as
|
|
87
|
+
template chrome (`src/utils/aria.ts:543`), so a `nav` labelled "Pagination" never reaches the
|
|
88
|
+
model either. **These markers must be read from HTML.** `role="feed"` is the one exception —
|
|
89
|
+
it survives as `- feed "…"` in the snapshot.
|
|
90
|
+
|
|
91
|
+
Two constraints that keep this a lookup rather than a heuristic:
|
|
92
|
+
|
|
93
|
+
- The value must be `aria-current="page"` exactly. `aria-current="true"` is what tabs and
|
|
94
|
+
breadcrumbs use and would over-match — confirmed in the same probe.
|
|
95
|
+
- A `nav` whose `aria-label` reads "Pagination" is author prose, not closed grammar. It is not
|
|
96
|
+
part of this tier.
|
|
97
|
+
|
|
98
|
+
**Absence proves nothing.** A pager built from plain buttons, and an infinite feed built from
|
|
99
|
+
plain divs, carry none of these. That is what steps 1–3 are for.
|
|
100
|
+
|
|
101
|
+
**1. Are there pagination controls? (AI, free)**
|
|
102
|
+
|
|
103
|
+
Only asked when step 0 found nothing. Controls are named in open-ended ways — words, arrows,
|
|
104
|
+
bare numbers — so this is AI judgment, not a pattern match. Researcher is already describing
|
|
105
|
+
the section, so it costs nothing extra:
|
|
106
|
+
a new `rules/researcher/pagination.md`, loaded alongside the existing three at
|
|
107
|
+
`src/ai/researcher/sections.ts:81`, asks it to note when a section contains controls that move
|
|
108
|
+
between pages of the same collection.
|
|
109
|
+
|
|
110
|
+
If found, the section records `> Pagination: controls` and the remaining steps are skipped.
|
|
111
|
+
|
|
112
|
+
**2. Can the section scroll at all? (deterministic gate)**
|
|
113
|
+
|
|
114
|
+
Only asked when no controls were found. One `page.evaluate` per container:
|
|
115
|
+
|
|
116
|
+
- `el.scrollHeight > el.clientHeight` → the container has its own scroller.
|
|
117
|
+
- `el.getBoundingClientRect().bottom > innerHeight` → the list continues below the fold.
|
|
118
|
+
- Neither → nothing more to do; no line recorded.
|
|
119
|
+
|
|
120
|
+
This gate exists to keep step 3 from running on every short list.
|
|
121
|
+
|
|
122
|
+
**3. Probe: does scrolling load more? (deterministic measurement)**
|
|
123
|
+
|
|
124
|
+
Scroll the container to its end, wait for readiness (`waitForPageReadiness`,
|
|
125
|
+
`src/utils/page-readiness.ts`), and compare. More descendant rows than before, or a same-origin
|
|
126
|
+
xhr/fetch fired during the scroll, means the list appends. Record `> Pagination: infinite`.
|
|
127
|
+
|
|
128
|
+
Then restore `scrollTop` to what it was, so screenshots, coordinates and later research see the
|
|
129
|
+
page as they found it. Scroll position is not app state, so this needs none of the modal
|
|
130
|
+
cleanup `_restorePageState` does in `deep-analysis.ts:453` — there is nothing to reuse there.
|
|
131
|
+
|
|
132
|
+
**Recorded vocabulary:** `controls` or `infinite`, as a line in the section's container
|
|
133
|
+
blockquote. Nothing is written when a list neither paginates nor grows, which is the common
|
|
134
|
+
case and should stay silent.
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
> Container: '.semantic-container'
|
|
138
|
+
> Pagination: infinite
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**This line has a reader**, because section B injects the rule only when pagination was
|
|
142
|
+
detected, and that decision is code. `extractPaginationFromBlockquote` joins
|
|
143
|
+
`extractContainerFromBlockquote` (`src/ai/researcher/parser.ts:86`) and returns the recorded
|
|
144
|
+
value or null.
|
|
145
|
+
|
|
146
|
+
That makes `Pagination:` a closed vocabulary read deterministically by code, so the envelope
|
|
147
|
+
checklist from CLAUDE.md applies and holds: read by code, scoped to a section of a state,
|
|
148
|
+
optional with "absent" as the default, and written by one module.
|
|
149
|
+
|
|
150
|
+
### A2. Where the code goes
|
|
151
|
+
|
|
152
|
+
New `src/ai/researcher/pagination.ts` mixin, composed into `ResearcherBase`
|
|
153
|
+
(`src/ai/researcher.ts:47`), owning steps 2 and 3. Step 1 is prompt text in
|
|
154
|
+
`rules/researcher/pagination.md` and needs no code.
|
|
155
|
+
|
|
156
|
+
It runs after `validateContainers` (`src/ai/researcher/locators.ts:268`), on containers that
|
|
157
|
+
survived validation, so a probe never targets a selector already known to be broken.
|
|
158
|
+
|
|
159
|
+
Not `deep-analysis.ts`: that mixin owns the same interact-measure-restore shape, but it is
|
|
160
|
+
gated behind `deep` (`src/ai/researcher.ts:287`), and infinite scroll has to be detected on
|
|
161
|
+
ordinary research runs too. It is also already 26k.
|
|
162
|
+
|
|
163
|
+
Not `locators.ts`: that mixin owns locator validity, not list behaviour.
|
|
164
|
+
|
|
165
|
+
**One writer for the blockquote.** `updateSectionContainer`
|
|
166
|
+
(`src/ai/researcher/locators.ts:300`) currently owns that `blockquote[0]` replace. The
|
|
167
|
+
pagination mixin must not write it independently. Extract the blockquote composition into one
|
|
168
|
+
helper both call, so `Container:` and `Pagination:` are always emitted by the same code.
|
|
169
|
+
|
|
170
|
+
A new `src/utils/pagination.ts` owns the two deterministic halves — the step 0 marker scan over
|
|
171
|
+
HTML, and the in-page evaluate functions for steps 2 and 3, self-contained with no outer-scope
|
|
172
|
+
references. One concern: how a list continues. `measureLayout` in `overlay.ts` is not reused:
|
|
173
|
+
it is xpath-based and returns a modal-scoring `RegionLayout`, while sections carry CSS
|
|
174
|
+
selectors and need neither.
|
|
175
|
+
|
|
176
|
+
### B. The pagination rule, injected only when pagination was detected
|
|
177
|
+
|
|
178
|
+
Not part of the static system message. A page with no list should not carry list guidance, and
|
|
179
|
+
a page with page numbers should not be told how to scroll.
|
|
180
|
+
|
|
181
|
+
**Seam:** `reinjectContextIfNeeded` (`src/ai/tester.ts:554`), which already injects per-state
|
|
182
|
+
blocks conditionally — `focusedElementRule` when something is focused, an `<overlay>` block
|
|
183
|
+
when a region is open. A `<pagination>` block joins them. Navigator gets the same block where
|
|
184
|
+
it builds its own per-state context (`src/ai/navigator.ts:414`).
|
|
185
|
+
|
|
186
|
+
**Condition:** the state shows pagination if step 0's markers are present in the current HTML,
|
|
187
|
+
or `extractPaginationFromBlockquote` finds a recorded value for a section. Markers win when
|
|
188
|
+
both are available, since they describe the page as it is now rather than as research left it.
|
|
189
|
+
|
|
190
|
+
**Which text:** the strategy selects the fragment, so the model is never shown the other one.
|
|
191
|
+
|
|
192
|
+
`paginationControlsRule`:
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
<pagination>
|
|
196
|
+
This list pages through a larger collection. If what you need is not on screen,
|
|
197
|
+
click next or the page number you need before concluding it is absent.
|
|
198
|
+
</pagination>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
`infiniteScrollRule`:
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
<pagination>
|
|
205
|
+
This list grows as it is scrolled. If what you need is not on screen, scroll to
|
|
206
|
+
the last item in the list — every scrollable ancestor of that item scrolls, so
|
|
207
|
+
this reaches a list with its own scrollbar.
|
|
208
|
+
|
|
209
|
+
New rows in the aria changes mean more arrived; a request with none means nothing
|
|
210
|
+
was left. Stop on the first attempt that adds no rows: the end of a collection is
|
|
211
|
+
an answer, not a failure.
|
|
212
|
+
</pagination>
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Both live in `src/ai/rules.ts` as exports. Not `rules/*.md`: those load per agent, and these
|
|
216
|
+
have two consumers.
|
|
217
|
+
|
|
218
|
+
**Reach narrows from the earlier draft.** Composing into `actionRule` would have reached
|
|
219
|
+
Rerunner (`src/ai/rerunner.ts:450`) and Captain web-mode (`src/ai/captain/web-mode.ts:148`) too;
|
|
220
|
+
conditional injection reaches only agents that build per-state context, which is Tester and
|
|
221
|
+
Navigator. That is the cost of making it conditional, and it is the right trade: Rerunner heals
|
|
222
|
+
known steps rather than traversing lists.
|
|
223
|
+
|
|
224
|
+
Constraints both fragments keep: general phrasing, no selectors, no site names, no example
|
|
225
|
+
taken from a debug session, one to three lines per bullet.
|
|
226
|
+
|
|
227
|
+
### C. `actionRule` documents the gesture
|
|
228
|
+
|
|
229
|
+
`<actions>` (`src/ai/rules.ts:307`) gains a scroll entry covering `I.scrollTo(<locator>)` —
|
|
230
|
+
stating that it scrolls every scrollable ancestor of the target — and
|
|
231
|
+
`I.scrollPageToBottom()` for the window. The `form` tool description
|
|
232
|
+
(`src/ai/tools.ts:354`) gains "reach items further down a list" as a use case, since it
|
|
233
|
+
currently reads as a typing tool.
|
|
234
|
+
|
|
235
|
+
### D. Tool output that contradicts the rule
|
|
236
|
+
|
|
237
|
+
Both are wrong reporting on existing tools, not new behaviour.
|
|
238
|
+
|
|
239
|
+
**`src/ai/tools.ts:415`** — `hasObservablePageChange` (`src/ai/tools.ts:1236`) ignores
|
|
240
|
+
`pageDiff.requests`. A scroll that fires a fetch which has not yet rendered rows, and a scroll
|
|
241
|
+
at the true end of a list, both return `failedToolResult` carrying the suggestion "Treat the
|
|
242
|
+
field/form action as not completed. Re-locate the editable control…" and commit
|
|
243
|
+
`TestResult.FAILED` into the note. That note is read by final review and by Historian, so a
|
|
244
|
+
correct end-of-list check is recorded as a failed test step.
|
|
245
|
+
|
|
246
|
+
Fix: one line in `hasObservablePageChange` — `if (data.pageDiff.requests?.length) return true;`
|
|
247
|
+
— and a no-change message that states what was observed rather than prescribing a
|
|
248
|
+
form-specific recovery.
|
|
249
|
+
|
|
250
|
+
**`src/ai/tools.ts:1208`** — `isMajorPageChange` (`src/ai/tools.ts:1220`) fires at
|
|
251
|
+
`ariaChangeCount >= 50` with no URL change (`LARGE_ARIA_CHANGE_THRESHOLD`,
|
|
252
|
+
`src/utils/aria.ts:581`), producing "MAJOR PAGE CHANGE. Page entered a different mode."
|
|
253
|
+
`diffByCount` (`src/utils/aria.ts:309`) pushes one entry per surplus occurrence, so a batch of
|
|
254
|
+
appended rows clears 50 easily. The rule says growth is the same state; the tool says the mode
|
|
255
|
+
changed.
|
|
256
|
+
|
|
257
|
+
Fix: a diff consisting of additions with no corresponding removals is growth, not a mode
|
|
258
|
+
change. Mode changes churn — they remove as well as add.
|
|
259
|
+
|
|
260
|
+
The added/removed split exists inside `diffAriaSnapshots` but is flattened into a single
|
|
261
|
+
`count` before it reaches the check, so it has to be threaded through:
|
|
262
|
+
|
|
263
|
+
1. `AriaDiff` (`src/utils/aria.ts:587`) gains `added: number; removed: number` — both arrays
|
|
264
|
+
are already computed at `src/utils/aria.ts:522`.
|
|
265
|
+
2. `Diff` (`src/action-result.ts:650`) stores them alongside `_ariaChangeCount`, set where
|
|
266
|
+
`diffAriaSnapshots` is called (`src/action-result.ts:740`), and exposes them.
|
|
267
|
+
3. `PageDiff` (`src/action-result.ts:46`) gains `ariaAdded` / `ariaRemoved`, populated next to
|
|
268
|
+
`ariaChanges` / `ariaChangeCount` (`src/action-result.ts:552`).
|
|
269
|
+
4. `isMajorPageChange` then reads the split instead of the total.
|
|
270
|
+
|
|
271
|
+
## Risks
|
|
272
|
+
|
|
273
|
+
- **The probe costs a scroll per candidate section.** Steps 1 and 2 narrow it to sections that
|
|
274
|
+
have no pagination controls and can actually scroll, which on most pages is zero or one. If
|
|
275
|
+
it still proves too slow, the gate to tighten is step 2, not the probe itself.
|
|
276
|
+
- **Region misclassification.** `OverlayPage.detectRegion` (`src/utils/overlay.ts:41`) accepts
|
|
277
|
+
in-flow added content that is `sizable` (≥5,000 chars, `src/utils/region.ts:99`), dominant
|
|
278
|
+
(≥70% of added raw size) and carries a name or root. A large appended batch in one list
|
|
279
|
+
container fits all three, which would fork the state hash with `region_<name>` and record a
|
|
280
|
+
transition. The probe can trigger this during research as well as the tester during a run.
|
|
281
|
+
Watch for it on the first real run; not changed here.
|
|
282
|
+
- **Virtualized lists.** Recycled nodes keep counts flat, so the probe sees no growth and the
|
|
283
|
+
aria diff shows renames (`src/utils/aria.ts:325`) rather than additions. Such a list records
|
|
284
|
+
nothing and the rule will not know to scroll it. Out of scope.
|
|
285
|
+
- **The stop condition lives only in the prompt.** No tool enforces it, so a model that keeps
|
|
286
|
+
scrolling past an attempt which added no rows will keep scrolling. Accepted: the tester's own
|
|
287
|
+
iteration cap is the only backstop.
|
|
288
|
+
- **A REST call alone no longer stops the loop, by design.** After the D fix an empty-payload
|
|
289
|
+
200 counts as an observable change, so the tool reports success. The rule makes rows, not
|
|
290
|
+
calls, the evidence that more arrived — otherwise a list that answers every scroll with an
|
|
291
|
+
empty page would loop.
|
|
292
|
+
|
|
293
|
+
## Testing
|
|
294
|
+
|
|
295
|
+
- Unit coverage for step 0's marker scan: `aria-current="page"` and `rel=next/prev` yield
|
|
296
|
+
`controls`; `role="feed"` and `aria-setsize="-1"` yield `infinite`; `aria-current="true"` on a
|
|
297
|
+
tab list yields nothing.
|
|
298
|
+
- `tests/integration/researcher-sections.test.ts` — `> Pagination: controls` appears for a
|
|
299
|
+
section whose UI map holds next/prev controls, and step 1 is not asked when step 0 already
|
|
300
|
+
answered.
|
|
301
|
+
- A browser test for steps 2 and 3, following `tests/integration/overlay-modal-browser.test.ts`:
|
|
302
|
+
a container with its own scroller that appends on scroll (`infinite`), one that does not
|
|
303
|
+
(silent), a list with pagination controls (`controls`, no probe runs), and confirmation that
|
|
304
|
+
`scrollTop` is restored afterwards.
|
|
305
|
+
- An aimock prompt-inspection test that the matching fragment reaches the tester on a paginated
|
|
306
|
+
state, the other fragment does not, and neither appears on a state with no list, per
|
|
307
|
+
`docs/contributing/ai-integration-tests.md`.
|
|
308
|
+
- Unit coverage for the two `tools.ts` corrections: requests-only diff counts as observable;
|
|
309
|
+
an additions-only diff over the threshold is not a major page change.
|
|
310
|
+
|
|
311
|
+
## Out of scope
|
|
312
|
+
|
|
313
|
+
- Any new tool. The gesture is reachable through `form` today.
|
|
314
|
+
- Persisting the strategy across runs. Research recomputes it, and the research cache is
|
|
315
|
+
session-scoped.
|
|
316
|
+
- Virtualized list support.
|
|
317
|
+
- Changing `detectRegion` thresholds.
|
|
@@ -44,7 +44,7 @@ See [Researcher Agent](./researcher.md) for configuration and usage.
|
|
|
44
44
|
|
|
45
45
|
Generates test scenarios from research findings.
|
|
46
46
|
|
|
47
|
-
The Planner writes business-focused scenarios with priority levels (critical/important/high/normal/low) and expected outcomes for verification. It balances positive and negative cases, skips scenarios you already have, and cycles through planning styles (normal, psycho, curious) to broaden coverage across iterations. You can add your own styles and page-specific rules.
|
|
47
|
+
The Planner writes business-focused scenarios with priority levels (critical/important/high/normal/low) and expected outcomes for verification. It balances positive and negative cases, skips scenarios you already have, and cycles through planning styles (normal, psycho, curious) to broaden coverage across iterations. You can add your own styles and page-specific rules. With the Scout agent enabled, it also plans from collected documentation, weighted by `docsWeight`.
|
|
48
48
|
|
|
49
49
|
Commands that use Planner:
|
|
50
50
|
- `/plan [--focus <feature>]`
|
|
@@ -52,6 +52,14 @@ Commands that use Planner:
|
|
|
52
52
|
|
|
53
53
|
See [Planner Agent](./planner.md) for planning styles, customization, and configuration.
|
|
54
54
|
|
|
55
|
+
## Scout Agent
|
|
56
|
+
|
|
57
|
+
Retrieves documentation relevant to the page being planned.
|
|
58
|
+
|
|
59
|
+
Scout searches the collected documentation corpus — the [application spec](../workflow/application-spec.md) from `explorbot docs collect` plus any extra markdown directories you configure — and reports the documented capabilities, states and transitions that matter for the current page and focus. The Planner receives them as a `<docs_context>` block and grounds part of its scenarios in them. Pages already injected as `<application_spec>` for the current URL are not repeated. Scout is opt-in (`ai.agents.scout.enabled`) and searches with ripgrep or grep, falling back to an in-process scan when neither is installed.
|
|
60
|
+
|
|
61
|
+
See [Configuration: Scout agent](../reference/configuration.md#scout-agent).
|
|
62
|
+
|
|
55
63
|
## Tester Agent
|
|
56
64
|
|
|
57
65
|
Runs the planned scenarios.
|
|
@@ -53,6 +53,11 @@ ai: {
|
|
|
53
53
|
| `styles` | `string[]` | `['normal', 'curious', 'psycho']` | Style names and cycling order |
|
|
54
54
|
| `rules` | `RuleEntry[]` | `[]` | URL-aware rule files from `rules/planner/` |
|
|
55
55
|
| `systemPrompt` | `string` | - | Inline instructions appended to the prompt |
|
|
56
|
+
| `docsWeight` | `number` | `70` | With Scout enabled, the rough share of scenarios exercising documented behavior; the rest explore beyond the documentation |
|
|
57
|
+
|
|
58
|
+
## Planning from documentation
|
|
59
|
+
|
|
60
|
+
With the [Scout agent](../reference/configuration.md#scout-agent) enabled, the Planner also receives a `<docs_context>` block — capabilities, states and transitions retrieved from collected documentation that are relevant to the current page and focus. `docsWeight` steers the mix: at `70` roughly seven of ten scenarios exercise documented behavior and three explore what the documentation does not cover. Set it to `100` for documentation-only planning, or lower it to lean on the Planner's own reading of the page. Pages whose documentation is already injected as `<application_spec>` are not repeated in `<docs_context>`.
|
|
56
61
|
|
|
57
62
|
## Planning Styles
|
|
58
63
|
|
|
@@ -75,3 +75,7 @@ Screenshots and other relative links may be included for readers, but Explorbot
|
|
|
75
75
|
## Validation
|
|
76
76
|
|
|
77
77
|
Explorbot rejects a bundle when `index.md` or `pages/` is missing, when it contains no page files, or when a page has an unsupported format, version, or missing URL.
|
|
78
|
+
|
|
79
|
+
## Scout
|
|
80
|
+
|
|
81
|
+
Beyond the per-URL injection, the same bundle feeds the [Scout agent](../reference/configuration.md#scout-agent): when Scout is enabled, it searches `pages/` (and any extra `ai.agents.scout.dirs`) for documentation relevant to the page being planned and reports it to the Planner. Pages already injected for the current URL are excluded from scouting, so the two channels never duplicate each other.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"bash-tool": "^1.3.15",
|
|
102
102
|
"chalk": "^5.6.2",
|
|
103
103
|
"cli-highlight": "^2.1.11",
|
|
104
|
-
"codeceptjs": "4.
|
|
104
|
+
"codeceptjs": "4.2.0-beta.2",
|
|
105
105
|
"commander": "^14.0.1",
|
|
106
106
|
"debug": "^4.4.3",
|
|
107
107
|
"dedent": "^1.6.0",
|
|
@@ -123,8 +123,8 @@
|
|
|
123
123
|
"ora-classic": "^5.4.2",
|
|
124
124
|
"parse5": "^8.0.0",
|
|
125
125
|
"pixelmatch": "^7.2.0",
|
|
126
|
-
"playwright": "^1.
|
|
127
|
-
"playwright-core": "^1.
|
|
126
|
+
"playwright": "^1.63",
|
|
127
|
+
"playwright-core": "^1.63",
|
|
128
128
|
"pngjs": "^7.0.0",
|
|
129
129
|
"react": "^19.1.1",
|
|
130
130
|
"sambanova-ai-provider": "^1.2.2",
|
|
@@ -7,11 +7,13 @@ import type { RequestStore } from '../../api/request-store.ts';
|
|
|
7
7
|
import { extractEndpointDefinition } from '../../api/spec-reader.ts';
|
|
8
8
|
import type { Test } from '../../test-plan.ts';
|
|
9
9
|
import { tag } from '../../utils/logger.ts';
|
|
10
|
+
import { truncate } from '../../utils/strings.ts';
|
|
10
11
|
import { isDynamicSegment } from '../../utils/url-matcher.ts';
|
|
11
12
|
import type { Fisherman } from '../fisherman.ts';
|
|
12
13
|
import type { RequestHaul } from './request-haul.ts';
|
|
13
14
|
|
|
14
15
|
const BODY_PREVIEW_LIMIT = 2000;
|
|
16
|
+
const READS_IN_ANSWER = 3;
|
|
15
17
|
|
|
16
18
|
export function createFishermanTools(apiClient: ApiClient, requestStore: RequestStore, haul: RequestHaul, opts: { spec?: any; baseEndpoint?: string; readOnly?: boolean }) {
|
|
17
19
|
const readOnly = opts.readOnly === true;
|
|
@@ -242,7 +244,7 @@ export function createAskApiTool(fisherman: Fisherman | null, task: Test) {
|
|
|
242
244
|
}
|
|
243
245
|
|
|
244
246
|
task.addNote(`Asked API: ${question} — ${result.summary}`);
|
|
245
|
-
tag('success').log(`Ask API: ${result.summary}`);
|
|
247
|
+
tag('success').log(`Ask API: ${truncate(result.summary, 200)}`);
|
|
246
248
|
return { answered: true, answer: result.summary };
|
|
247
249
|
},
|
|
248
250
|
}),
|
|
@@ -285,6 +287,11 @@ function synthesizeResult(haul: RequestHaul, declaredDone: boolean, readOnly: bo
|
|
|
285
287
|
succeeded = haul.successfulReads();
|
|
286
288
|
successLabel = 'successful reads';
|
|
287
289
|
}
|
|
290
|
+
if (readOnly && succeeded.length > 0) {
|
|
291
|
+
const bodies = succeeded.slice(-READS_IN_ANSWER).map((read) => `${read.toEndpoint()} → ${read.rawResponseBody.substring(0, BODY_PREVIEW_LIMIT)}`);
|
|
292
|
+
return { success: true, summary: bodies.join('\n\n'), created: [], failed: [] };
|
|
293
|
+
}
|
|
294
|
+
|
|
288
295
|
let summary = `Stopped before finishing: ${made.length} requests, ${succeeded.length} ${successLabel}, ${failures.length} failed`;
|
|
289
296
|
const lastFailure = failures[failures.length - 1];
|
|
290
297
|
if (lastFailure) summary += `; last failure: ${lastFailure.toSummary()}`;
|
package/src/ai/fisherman.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ApiClient } from '../api/api-client.ts';
|
|
|
3
3
|
import { type EndpointFamily, type RequestStore, isFailedRequest } from '../api/request-store.ts';
|
|
4
4
|
import { listAllEndpoints } from '../api/spec-reader.ts';
|
|
5
5
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
6
|
+
import { truncate } from '../utils/strings.ts';
|
|
6
7
|
|
|
7
8
|
const debugLog = createDebug('explorbot:fisherman');
|
|
8
9
|
import { loop } from '../utils/loop.ts';
|
|
@@ -137,7 +138,7 @@ export class Fisherman implements Agent {
|
|
|
137
138
|
await this.runSession(conversation, tools, { haul, isFinished, finishFromText, label: `fisherman lookup: ${question.slice(0, 50)}` });
|
|
138
139
|
|
|
139
140
|
const result = getResult();
|
|
140
|
-
tag('info').log(`Fisherman answer: ${result.summary}`);
|
|
141
|
+
tag('info').log(`Fisherman answer: ${truncate(result.summary, 200)}`);
|
|
141
142
|
return result;
|
|
142
143
|
}
|
|
143
144
|
|
package/src/ai/pilot.ts
CHANGED
|
@@ -363,8 +363,6 @@ export class Pilot implements Agent {
|
|
|
363
363
|
return dedent`
|
|
364
364
|
SCENARIO: ${task.scenario}
|
|
365
365
|
|
|
366
|
-
${this.buildDeletionScope(task)}
|
|
367
|
-
|
|
368
366
|
EXPECTED RESULTS (milestones):
|
|
369
367
|
${task.expected.map((e) => `- ${e}`).join('\n')}
|
|
370
368
|
`;
|
|
@@ -372,20 +370,22 @@ export class Pilot implements Agent {
|
|
|
372
370
|
|
|
373
371
|
private buildResetSystemPrompt(task: Test): string {
|
|
374
372
|
return dedent`
|
|
375
|
-
You are Pilot — decide whether a reset is legitimate. Reset
|
|
376
|
-
iteration's work
|
|
377
|
-
|
|
373
|
+
You are Pilot — decide whether a reset is legitimate. Reset only re-navigates to the start URL:
|
|
374
|
+
it writes nothing, though it abandons this iteration's work and server-side side effects persist.
|
|
375
|
+
The hazard is the tester REDOING a completed flow afterwards — duplicate data and infinite loops.
|
|
378
376
|
|
|
379
377
|
${this.buildSharedEvidenceRules()}
|
|
380
378
|
|
|
381
379
|
DECISION:
|
|
382
|
-
- "allow": current page cannot host the scenario, irrecoverable error,
|
|
383
|
-
|
|
380
|
+
- "allow": current page cannot host the scenario, irrecoverable error, no path back, or an
|
|
381
|
+
expectation requires the outcome to survive a reload or a return to the start page and no
|
|
382
|
+
reset has been taken yet this run — there the reset IS the check, not a redo.
|
|
383
|
+
- "continue": the outcome the scenario needs is already observable on the CURRENT page — verify/finish instead. Provide guidance.
|
|
384
384
|
- "fail": resetCount >= 2 and underlying situation hasn't changed; same flow tried twice with same failure mode.
|
|
385
385
|
- "skipped": feature doesn't exist on this app or prerequisites can't be met.
|
|
386
386
|
|
|
387
387
|
PRIORITY:
|
|
388
|
-
1) Successful side effects in session_log →
|
|
388
|
+
1) Successful side effects in session_log → allow reset only to re-observe them, never to repeat them.
|
|
389
389
|
2) resetCount — each prior reset raises the bar.
|
|
390
390
|
3) Tester's stated reason — weigh against evidence, don't trust blindly.
|
|
391
391
|
|
|
@@ -1102,23 +1102,6 @@ export class Pilot implements Agent {
|
|
|
1102
1102
|
.join('\n\n');
|
|
1103
1103
|
}
|
|
1104
1104
|
|
|
1105
|
-
private buildDeletionScope(task: Test): string {
|
|
1106
|
-
const deletableItems = task.plan
|
|
1107
|
-
? task.plan
|
|
1108
|
-
.listTests()
|
|
1109
|
-
.filter((t) => t.isSuccessful && t.sessionName)
|
|
1110
|
-
.map((t) => t.sessionName!)
|
|
1111
|
-
: [];
|
|
1112
|
-
const scenarioLower = task.scenario.toLowerCase();
|
|
1113
|
-
if (deletableItems.length > 0) {
|
|
1114
|
-
return `For deletion scenarios, items can only be deleted if their title contains: ${deletableItems.join(', ')}`;
|
|
1115
|
-
}
|
|
1116
|
-
if (scenarioLower.includes('delete') || scenarioLower.includes('remove')) {
|
|
1117
|
-
return 'No items available for deletion — test should create an item first';
|
|
1118
|
-
}
|
|
1119
|
-
return '';
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
1105
|
private getSystemPrompt(task: Test, initialState: ActionResult): string {
|
|
1123
1106
|
const interactive = isInteractive();
|
|
1124
1107
|
const stepsText = task.plannedSteps.length > 0 ? task.plannedSteps.map((s, i) => `${i + 1}. ${s}`).join('\n') : 'No planned steps';
|
package/src/ai/planner.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { POSSIBLE_SECTIONS, type Researcher } from './researcher.ts';
|
|
|
25
25
|
import { findSimilarStateHash } from './researcher/cache.ts';
|
|
26
26
|
import { hasFocusedSection } from './researcher/focus.ts';
|
|
27
27
|
import { capabilityGroundingRule, dataProtectionRules, fileUploadRule } from './rules.ts';
|
|
28
|
+
import type { Scout } from './scout.ts';
|
|
28
29
|
|
|
29
30
|
const debugLog = createDebug('explorbot:planner');
|
|
30
31
|
|
|
@@ -63,6 +64,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
63
64
|
private lastSuite: Suite | null = null;
|
|
64
65
|
researcher: Researcher;
|
|
65
66
|
private fisherman: Fisherman | null = null;
|
|
67
|
+
private scout: Scout | null = null;
|
|
66
68
|
|
|
67
69
|
constructor(deps: AgentDeps, researcher: Researcher) {
|
|
68
70
|
super();
|
|
@@ -78,10 +80,19 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
78
80
|
this.fisherman = fisherman;
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
setScout(scout: Scout): void {
|
|
84
|
+
this.scout = scout;
|
|
85
|
+
}
|
|
86
|
+
|
|
81
87
|
private get sectionOrder(): string[] {
|
|
82
88
|
return ConfigParser.getInstance().getConfig().ai?.agents?.researcher?.sections || Object.keys(POSSIBLE_SECTIONS);
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
private get docsWeight(): number {
|
|
92
|
+
const value = ConfigParser.getInstance().getConfig().ai?.agents?.planner?.docsWeight ?? 70;
|
|
93
|
+
return Math.max(0, Math.min(100, value));
|
|
94
|
+
}
|
|
95
|
+
|
|
85
96
|
private getDefaultStartUrl(state: { url: string; fullUrl?: string }): string {
|
|
86
97
|
return state.fullUrl || state.url;
|
|
87
98
|
}
|
|
@@ -328,6 +339,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
328
339
|
const conversation = new Conversation([], model);
|
|
329
340
|
conversation.autoTrimTag('page_research', 20000);
|
|
330
341
|
conversation.autoTrimTag('tested_scenarios', 10000);
|
|
342
|
+
conversation.autoTrimTag('docs_context', 8000);
|
|
331
343
|
|
|
332
344
|
conversation.addUserText(this.getSystemMessage(feature));
|
|
333
345
|
|
|
@@ -385,6 +397,11 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
385
397
|
const research = await this.researcher.research(currentState || state, {
|
|
386
398
|
deep: true,
|
|
387
399
|
});
|
|
400
|
+
|
|
401
|
+
let docsPromise: Promise<string> | null = null;
|
|
402
|
+
if (this.scout && this.docsWeight > 0) {
|
|
403
|
+
docsPromise = this.scout.collectDocs({ url: state.url, title: state.title, feature, excludeUrls: this.knowledgeTracker.applicationSpecUrls(state) });
|
|
404
|
+
}
|
|
388
405
|
let plannerResearch = mdq(research).query('code').replace('');
|
|
389
406
|
plannerResearch = mdq(plannerResearch)
|
|
390
407
|
.query('table')
|
|
@@ -419,6 +436,22 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
419
436
|
conversation.addUserText(applicationContext);
|
|
420
437
|
}
|
|
421
438
|
|
|
439
|
+
if (docsPromise) {
|
|
440
|
+
const docs = await docsPromise;
|
|
441
|
+
if (docs) {
|
|
442
|
+
conversation.addUserText(dedent`
|
|
443
|
+
<docs_context>
|
|
444
|
+
Documentation retrieved from the collected corpus by the Scout agent.
|
|
445
|
+
Ground scenarios in these documented capabilities where they apply; treat them as supporting context, not a script.
|
|
446
|
+
|
|
447
|
+
Aim for roughly ${this.docsWeight}% of the scenarios to exercise behavior documented above; the remainder may explore beyond the documentation.
|
|
448
|
+
|
|
449
|
+
${docs}
|
|
450
|
+
</docs_context>
|
|
451
|
+
`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
422
455
|
conversation.addUserText(dedent`
|
|
423
456
|
${this.buildApproach(style)}
|
|
424
457
|
|
package/src/ai/provider.ts
CHANGED
|
@@ -409,7 +409,8 @@ export class Provider {
|
|
|
409
409
|
setActivity(`🤖 Asking ${modelName} with dynamic tools`, 'ai');
|
|
410
410
|
promptLog(`Using model: ${modelName}`);
|
|
411
411
|
|
|
412
|
-
|
|
412
|
+
let toolsWithCommentary = tools;
|
|
413
|
+
if (!tools?.commentary && options.toolChoice !== 'required') toolsWithCommentary = { ...tools, commentary: createHarmonyChannelFallbackTool() };
|
|
413
414
|
const toolNames = Object.keys(toolsWithCommentary || {});
|
|
414
415
|
tag('debug').log(`Tools enabled: [${toolNames.join(', ')}]`);
|
|
415
416
|
promptLog('Available tools:', toolNames);
|
|
@@ -116,7 +116,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
116
116
|
);
|
|
117
117
|
|
|
118
118
|
tag('substep').log(`Researching overlay: ${region.name}`);
|
|
119
|
-
const sectionMarkdown = await this._analyzeExpandedAction('', region.name, diff, alreadyExpanded);
|
|
119
|
+
const sectionMarkdown = await this._analyzeExpandedAction('', region.name, diff, alreadyExpanded, region.root);
|
|
120
120
|
if (!sectionMarkdown) {
|
|
121
121
|
debugLog(`Overlay "${region.name}" produced no meaningful expansion`);
|
|
122
122
|
return null;
|
|
@@ -421,9 +421,9 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
421
421
|
await new Promise((r) => setTimeout(r, 500));
|
|
422
422
|
|
|
423
423
|
let diff: Diff;
|
|
424
|
+
let currAR: ActionResult;
|
|
424
425
|
try {
|
|
425
|
-
await this.explorer.capture();
|
|
426
|
-
const currAR = ActionResult.fromState(this.stateManager.getCurrentState()!);
|
|
426
|
+
currAR = await this.explorer.capture();
|
|
427
427
|
diff = await currAR.diff(previousState);
|
|
428
428
|
} catch (err) {
|
|
429
429
|
tag('warning').log(`State capture failed after click: ${err instanceof Error ? err.message : err}`);
|
|
@@ -444,7 +444,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
444
444
|
return { status: 'none', code: clickCode };
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
-
const sectionMarkdown = await this._analyzeExpandedAction(clickCode, description, diff, alreadyExpanded);
|
|
447
|
+
const sectionMarkdown = await this._analyzeExpandedAction(clickCode, description, diff, alreadyExpanded, currAR.overlay.root);
|
|
448
448
|
await this._restorePageState(state.url, originalAria);
|
|
449
449
|
if (!sectionMarkdown) return { status: 'none', code: clickCode };
|
|
450
450
|
return { status: 'revealed', code: clickCode, sectionMarkdown };
|
|
@@ -467,7 +467,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
467
467
|
}
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
-
private async _analyzeExpandedAction(code: string, description: string, diff: Diff, alreadyExpanded: string[]): Promise<string | null> {
|
|
470
|
+
private async _analyzeExpandedAction(code: string, description: string, diff: Diff, alreadyExpanded: string[], containerCss: string | null = null): Promise<string | null> {
|
|
471
471
|
const alreadyHint = alreadyExpanded.length > 0 ? `\nAlready expanded sections:\n${alreadyExpanded.join('\n')}` : '';
|
|
472
472
|
|
|
473
473
|
let intro: string;
|
|
@@ -532,7 +532,14 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
532
532
|
const sections = parseResearchSections(text);
|
|
533
533
|
if (sections.length === 0) return null;
|
|
534
534
|
|
|
535
|
-
|
|
535
|
+
const sectionMarkdown = sections[0].rawMarkdown;
|
|
536
|
+
if (!containerCss) return sectionMarkdown;
|
|
537
|
+
|
|
538
|
+
let heading = mdq(sectionMarkdown).query('h3[0]');
|
|
539
|
+
if (heading.count() === 0) heading = mdq(sectionMarkdown).query('h2[0]');
|
|
540
|
+
if (heading.count() === 0) return sectionMarkdown;
|
|
541
|
+
|
|
542
|
+
return heading.replace(`${heading.text().trimEnd()}\n\n> Container: '${containerCss}'\n\n`);
|
|
536
543
|
}
|
|
537
544
|
|
|
538
545
|
private _deduplicateExpandedSections(sections: string[]): string[] {
|