explorbot 0.3.1 → 0.3.2
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/bin/explorbot-cli.ts +3 -3
- package/dist/bin/explorbot-cli.js +3 -2
- package/dist/package.json +1 -1
- package/dist/src/action-result.d.ts +3 -0
- package/dist/src/action-result.js +3 -0
- package/dist/src/action.d.ts +1 -0
- package/dist/src/action.js +12 -1
- package/dist/src/ai/driller.js +3 -1
- package/dist/src/ai/pilot.js +2 -2
- package/dist/src/ai/provider.d.ts +2 -0
- package/dist/src/ai/provider.js +18 -1
- package/dist/src/ai/researcher/deep-analysis.js +2 -2
- package/dist/src/ai/tester.js +1 -2
- package/dist/src/commands/exit-command.js +1 -1
- package/dist/src/explorbot.js +1 -0
- package/dist/src/state-manager.d.ts +2 -0
- package/dist/src/state-manager.js +3 -3
- package/dist/src/utils/html.d.ts +2 -1
- package/dist/src/utils/html.js +10 -4
- package/dist/src/utils/overlay.d.ts +24 -0
- package/dist/src/utils/overlay.js +43 -0
- package/package.json +1 -1
- package/src/action-result.ts +5 -0
- package/src/action.ts +15 -1
- package/src/ai/driller.ts +3 -1
- package/src/ai/pilot.ts +2 -2
- package/src/ai/provider.ts +19 -1
- package/src/ai/researcher/deep-analysis.ts +2 -2
- package/src/ai/tester.ts +1 -2
- package/src/commands/exit-command.ts +1 -1
- package/src/explorbot.ts +1 -0
- package/src/state-manager.ts +4 -3
- package/src/utils/html.ts +13 -4
- package/src/utils/overlay.ts +51 -0
package/bin/explorbot-cli.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { render } from 'ink';
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { App } from '../src/components/App.js';
|
|
11
11
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
12
|
+
import { flushTelemetry } from '../src/ai/provider.js';
|
|
12
13
|
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
13
14
|
import { ExplorBot, type ExplorBotOptions } from '../src/explorbot.js';
|
|
14
15
|
import { remote } from '../src/remote.js';
|
|
@@ -103,9 +104,7 @@ async function startTUI(explorBot: ExplorBot): Promise<void> {
|
|
|
103
104
|
async function showStatsAndExit(code: number): Promise<never> {
|
|
104
105
|
if (remote.isAttached()) {
|
|
105
106
|
await remote.close(code);
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
if (Stats.hasActivity()) {
|
|
107
|
+
} else if (Stats.hasActivity()) {
|
|
109
108
|
await new Promise<void>((resolve) => {
|
|
110
109
|
const { unmount } = render(
|
|
111
110
|
React.createElement(StatusPane, {
|
|
@@ -121,6 +120,7 @@ async function showStatsAndExit(code: number): Promise<never> {
|
|
|
121
120
|
);
|
|
122
121
|
});
|
|
123
122
|
}
|
|
123
|
+
await flushTelemetry();
|
|
124
124
|
process.exit(code);
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -9,6 +9,7 @@ import { render } from 'ink';
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { App } from '../src/components/App.js';
|
|
11
11
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
12
|
+
import { flushTelemetry } from '../src/ai/provider.js';
|
|
12
13
|
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
13
14
|
import { ExplorBot } from '../src/explorbot.js';
|
|
14
15
|
import { remote } from '../src/remote.js';
|
|
@@ -78,9 +79,8 @@ async function startTUI(explorBot) {
|
|
|
78
79
|
async function showStatsAndExit(code) {
|
|
79
80
|
if (remote.isAttached()) {
|
|
80
81
|
await remote.close(code);
|
|
81
|
-
process.exit(code);
|
|
82
82
|
}
|
|
83
|
-
if (Stats.hasActivity()) {
|
|
83
|
+
else if (Stats.hasActivity()) {
|
|
84
84
|
await new Promise((resolve) => {
|
|
85
85
|
const { unmount } = render(React.createElement(StatusPane, {
|
|
86
86
|
onComplete: () => {
|
|
@@ -93,6 +93,7 @@ async function showStatsAndExit(code) {
|
|
|
93
93
|
});
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
|
+
await flushTelemetry();
|
|
96
97
|
process.exit(code);
|
|
97
98
|
}
|
|
98
99
|
addCommonOptions(program.command('start [path]').description('Start web exploration')).action(async (startPath, options) => {
|
package/dist/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import { type HtmlConfig } from './config.js';
|
|
|
2
2
|
import type { Link, WebPageState } from './state-manager.js';
|
|
3
3
|
import { TTLCache } from './utils/cache.js';
|
|
4
4
|
import { type HtmlDiffPart, type HtmlDiffResult } from './utils/html-diff.js';
|
|
5
|
+
import { Overlay } from './utils/overlay.js';
|
|
5
6
|
interface ActionResultData extends WebPageState {
|
|
6
7
|
html?: string;
|
|
7
8
|
fullUrl?: string | undefined;
|
|
@@ -28,6 +29,7 @@ interface ActionResultData extends WebPageState {
|
|
|
28
29
|
focusedElement?: FocusedElement | null;
|
|
29
30
|
iframeURL?: string;
|
|
30
31
|
links?: Link[];
|
|
32
|
+
overlayHtml?: string;
|
|
31
33
|
}
|
|
32
34
|
export interface PageDiff {
|
|
33
35
|
urlChanged: boolean;
|
|
@@ -81,6 +83,7 @@ export declare class ActionResult implements ActionResultData {
|
|
|
81
83
|
notes: string[];
|
|
82
84
|
links: Link[];
|
|
83
85
|
verifications?: Record<string, boolean>;
|
|
86
|
+
overlay: Overlay;
|
|
84
87
|
constructor(data: ActionResultData);
|
|
85
88
|
get hash(): string;
|
|
86
89
|
get html(): string;
|
|
@@ -5,6 +5,7 @@ import { TTLCache } from "./utils/cache.js";
|
|
|
5
5
|
import { htmlDiff, liveRegionMessages } from "./utils/html-diff.js";
|
|
6
6
|
import { extractHeadings, extractLinks, extractTargetedHtml, htmlCombinedSnapshot, htmlMinimalUISnapshot, htmlTextSnapshot, minifyHtml } from "./utils/html.js";
|
|
7
7
|
import { createDebug } from "./utils/logger.js";
|
|
8
|
+
import { Overlay } from "./utils/overlay.js";
|
|
8
9
|
import { slugify } from "./utils/strings.js";
|
|
9
10
|
import { extractStatePath, matchesUrl } from "./utils/url-matcher.js";
|
|
10
11
|
const debugLog = createDebug('explorbot:state');
|
|
@@ -37,6 +38,7 @@ export class ActionResult {
|
|
|
37
38
|
notes = [];
|
|
38
39
|
links = [];
|
|
39
40
|
verifications;
|
|
41
|
+
overlay = new Overlay();
|
|
40
42
|
constructor(data) {
|
|
41
43
|
this.id = data.id;
|
|
42
44
|
this.timestamp = data.timestamp ?? new Date();
|
|
@@ -77,6 +79,7 @@ export class ActionResult {
|
|
|
77
79
|
if (data.ariaSnapshot !== undefined) {
|
|
78
80
|
this._ariaSnapshot = data.ariaSnapshot;
|
|
79
81
|
}
|
|
82
|
+
this.overlay = Overlay.resolve(data);
|
|
80
83
|
if (!this.fullUrl && this.url) {
|
|
81
84
|
this.fullUrl = this.url;
|
|
82
85
|
}
|
package/dist/src/action.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ declare class Action {
|
|
|
32
32
|
includeScreenshot?: boolean;
|
|
33
33
|
codeBlock?: string;
|
|
34
34
|
}): Promise<ActionResult>;
|
|
35
|
+
captureOverlayHtml(): Promise<string>;
|
|
35
36
|
captureMainDocumentStatus(): Promise<number | undefined>;
|
|
36
37
|
captureResponses(): () => void;
|
|
37
38
|
recordNetworkCall(request: any, status: number): void;
|
package/dist/src/action.js
CHANGED
|
@@ -8,8 +8,9 @@ import { clearActivity, setActivity } from "./activity.js";
|
|
|
8
8
|
import { ConfigParser, outputPath } from './config.js';
|
|
9
9
|
import { Observability } from "./observability.js";
|
|
10
10
|
import { browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
|
|
11
|
-
import { captureHtmlForSnapshot, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
11
|
+
import { captureHtmlForSnapshot, getVisibleOverlayHtmlExtractorSource, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
12
12
|
import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
|
|
13
|
+
import { Overlay } from './utils/overlay.js';
|
|
13
14
|
import { sleep, waitForPageReadiness } from "./utils/page-readiness.js";
|
|
14
15
|
import { safeFilename } from "./utils/strings.js";
|
|
15
16
|
import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from "./utils/web-sandbox.js";
|
|
@@ -131,10 +132,13 @@ class Action {
|
|
|
131
132
|
let ariaSnapshot = null;
|
|
132
133
|
let ariaSnapshotFile = undefined;
|
|
133
134
|
let focusedElement = null;
|
|
135
|
+
let overlayHtml = '';
|
|
134
136
|
try {
|
|
135
137
|
const page = this.playwrightHelper.page;
|
|
136
138
|
ariaSnapshot = await page.locator('body').ariaSnapshot();
|
|
137
139
|
focusedElement = await page.evaluate(readFocusedElement);
|
|
140
|
+
if (!frame)
|
|
141
|
+
overlayHtml = await this.captureOverlayHtml();
|
|
138
142
|
}
|
|
139
143
|
catch (err) {
|
|
140
144
|
debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
|
|
@@ -161,6 +165,7 @@ class Action {
|
|
|
161
165
|
ariaSnapshot,
|
|
162
166
|
ariaSnapshotFile,
|
|
163
167
|
focusedElement,
|
|
168
|
+
overlayHtml: overlayHtml || undefined,
|
|
164
169
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
165
170
|
});
|
|
166
171
|
this.stateManager.updateState(result, codeBlock);
|
|
@@ -175,6 +180,12 @@ class Action {
|
|
|
175
180
|
return new ActionResult({ url, error: msg });
|
|
176
181
|
}
|
|
177
182
|
}
|
|
183
|
+
async captureOverlayHtml() {
|
|
184
|
+
return this.playwrightHelper.page.evaluate(({ extractorSource, config }) => {
|
|
185
|
+
const extract = new Function(`return ${extractorSource}`)();
|
|
186
|
+
return extract(config);
|
|
187
|
+
}, { extractorSource: getVisibleOverlayHtmlExtractorSource(), config: Overlay.captureConfig() });
|
|
188
|
+
}
|
|
178
189
|
async captureMainDocumentStatus() {
|
|
179
190
|
if (this.mainDocumentStatus)
|
|
180
191
|
return this.mainDocumentStatus;
|
package/dist/src/ai/driller.js
CHANGED
|
@@ -9,6 +9,7 @@ import { collectInteractiveNodes } from "../utils/aria.js";
|
|
|
9
9
|
import { EXPLORBOT_ATTRS, HTML_COMPOSITE_AREA_HINTS, HTML_COMPOSITE_TARGET_ROLES, HTML_EXTRACTION_LIMITS, HTML_FORM_CONTROL_ROLES, HTML_FORM_CONTROL_TAGS, HTML_INTERACTIVE_ROLES, HTML_SELECTORS, HTML_VISIBILITY_LIMITS, getComponentScopeHtmlExtractorSource, getVisibleOverlayHtmlExtractorSource, inferHtmlRole, } from "../utils/html.js";
|
|
10
10
|
import { createDebug, tag } from "../utils/logger.js";
|
|
11
11
|
import { loop, pause } from "../utils/loop.js";
|
|
12
|
+
import { OVERLAY_SELECTORS } from "../utils/overlay.js";
|
|
12
13
|
import { annotatePageElements } from "../utils/web-annotate.js";
|
|
13
14
|
import { eidxInContainer } from "../utils/web-eidx.js";
|
|
14
15
|
import { WebElement } from "../utils/web-element.js";
|
|
@@ -578,7 +579,8 @@ export class Driller extends TaskAgent {
|
|
|
578
579
|
config: {
|
|
579
580
|
interactiveContentSelector: HTML_SELECTORS.interactiveContent,
|
|
580
581
|
limits: HTML_EXTRACTION_LIMITS,
|
|
581
|
-
overlaySelectors:
|
|
582
|
+
overlaySelectors: OVERLAY_SELECTORS.semanticOverlays,
|
|
583
|
+
overlaySemanticSelector: OVERLAY_SELECTORS.overlaySemanticSelector,
|
|
582
584
|
visibilityLimits: HTML_VISIBILITY_LIMITS,
|
|
583
585
|
},
|
|
584
586
|
}));
|
package/dist/src/ai/pilot.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ActionResult } from "../action-result.js";
|
|
|
5
5
|
import { ConfigParser } from "../config.js";
|
|
6
6
|
import { Stats } from "../stats.js";
|
|
7
7
|
import { TestResult } from "../test-plan.js";
|
|
8
|
-
import { collectInteractiveNodes
|
|
8
|
+
import { collectInteractiveNodes } from "../utils/aria.js";
|
|
9
9
|
import { ErrorPageError } from "../utils/error-page.js";
|
|
10
10
|
import { createDebug, tag } from "../utils/logger.js";
|
|
11
11
|
const debugLog = createDebug('explorbot:pilot');
|
|
@@ -738,7 +738,7 @@ export class Pilot {
|
|
|
738
738
|
lines.push(`h2: ${state.h2 || ''}`);
|
|
739
739
|
lines.push(`h3: ${state.h3 || ''}`);
|
|
740
740
|
lines.push(`h4: ${state.h4 || ''}`);
|
|
741
|
-
const focusArea =
|
|
741
|
+
const focusArea = state.overlay;
|
|
742
742
|
if (focusArea.detected) {
|
|
743
743
|
lines.push(`modal: ${focusArea.name || focusArea.type}`);
|
|
744
744
|
}
|
|
@@ -7,6 +7,7 @@ declare class AiError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
export declare class ContextLengthError extends Error {
|
|
9
9
|
}
|
|
10
|
+
export declare function flushTelemetry(): Promise<void>;
|
|
10
11
|
export declare class Provider {
|
|
11
12
|
config: AIConfig;
|
|
12
13
|
telemetryEnabled: boolean;
|
|
@@ -17,6 +18,7 @@ export declare class Provider {
|
|
|
17
18
|
modelCallWaiters: (() => void)[];
|
|
18
19
|
constructor(config: AIConfig);
|
|
19
20
|
validateConnection(): Promise<void>;
|
|
21
|
+
stop(): Promise<void>;
|
|
20
22
|
getModelForAgent(agentName?: string): any;
|
|
21
23
|
getAgenticModel(agentName?: string): any;
|
|
22
24
|
getVisionModel(): any;
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -31,6 +31,15 @@ function createHarmonyChannelFallbackTool() {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
let telemetryRegistered = false;
|
|
34
|
+
let beforeExitFlushHooked = false;
|
|
35
|
+
let activeOtelSdk = null;
|
|
36
|
+
export async function flushTelemetry() {
|
|
37
|
+
const sdk = activeOtelSdk;
|
|
38
|
+
activeOtelSdk = null;
|
|
39
|
+
if (!sdk)
|
|
40
|
+
return;
|
|
41
|
+
await sdk.shutdown().catch((error) => debugLog(`Telemetry flush failed: ${error instanceof Error ? error.message : error}`));
|
|
42
|
+
}
|
|
34
43
|
const CONTEXT_LENGTH_PATTERNS = ['reduce the length', 'context length', 'maximum context', 'token limit', 'too many tokens', 'max_tokens', 'context_length_exceeded', 'output truncated at maxtokens'];
|
|
35
44
|
function extractCachedTokens(usage) {
|
|
36
45
|
if (!usage)
|
|
@@ -105,6 +114,9 @@ export class Provider {
|
|
|
105
114
|
throw new AiError(`AI connection failed: ${error.message}`);
|
|
106
115
|
}
|
|
107
116
|
}
|
|
117
|
+
async stop() {
|
|
118
|
+
await flushTelemetry();
|
|
119
|
+
}
|
|
108
120
|
getModelForAgent(agentName) {
|
|
109
121
|
if (!agentName) {
|
|
110
122
|
return this.config.model;
|
|
@@ -251,7 +263,12 @@ export class Provider {
|
|
|
251
263
|
spanProcessors: [processor],
|
|
252
264
|
instrumentations: [],
|
|
253
265
|
});
|
|
266
|
+
activeOtelSdk = this.otelSdk;
|
|
254
267
|
void this.otelSdk.start();
|
|
268
|
+
if (!beforeExitFlushHooked) {
|
|
269
|
+
process.on('beforeExit', () => void flushTelemetry());
|
|
270
|
+
beforeExitFlushHooked = true;
|
|
271
|
+
}
|
|
255
272
|
if (!telemetryRegistered) {
|
|
256
273
|
registerTelemetry(new OpenTelemetry());
|
|
257
274
|
telemetryRegistered = true;
|
|
@@ -293,7 +310,7 @@ export class Provider {
|
|
|
293
310
|
}
|
|
294
311
|
async invokeConversation(conversation, tools, options = {}) {
|
|
295
312
|
const response = tools ? await this.generateWithTools(conversation.messages, conversation.model, tools, options) : await this.chat(conversation.messages, conversation.model, options);
|
|
296
|
-
const responseMessages = response.
|
|
313
|
+
const responseMessages = response.responseMessages || [];
|
|
297
314
|
if (responseMessages.length > 0) {
|
|
298
315
|
conversation.messages.push(...responseMessages);
|
|
299
316
|
tag('debug').log('Added', responseMessages.length, 'messages from response');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { ActionResult } from '../../action-result.js';
|
|
3
3
|
import { executionController } from "../../execution-controller.js";
|
|
4
|
-
import {
|
|
4
|
+
import { diffAriaSnapshots } from "../../utils/aria.js";
|
|
5
5
|
import { extractCodeBlocks } from "../../utils/code-extractor.js";
|
|
6
6
|
import { tag } from '../../utils/logger.js';
|
|
7
7
|
import { mdq } from "../../utils/markdown-query.js";
|
|
@@ -60,7 +60,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
60
60
|
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
61
61
|
}
|
|
62
62
|
async researchOverlay(current, previous, pageStateHash) {
|
|
63
|
-
const focusArea =
|
|
63
|
+
const focusArea = current.overlay;
|
|
64
64
|
if (!focusArea.detected || !focusArea.name)
|
|
65
65
|
return null;
|
|
66
66
|
if (focusArea.type !== 'dialog' && focusArea.type !== 'modal')
|
package/dist/src/ai/tester.js
CHANGED
|
@@ -9,7 +9,6 @@ import { Observability } from "../observability.js";
|
|
|
9
9
|
import { normalizeUrl } from "../state-manager.js";
|
|
10
10
|
import { Stats } from "../stats.js";
|
|
11
11
|
import { TestResult } from "../test-plan.js";
|
|
12
|
-
import { detectFocusArea } from "../utils/aria.js";
|
|
13
12
|
import { ErrorPageError, isErrorPage } from "../utils/error-page.js";
|
|
14
13
|
import { createDebug, tag } from "../utils/logger.js";
|
|
15
14
|
import { loop } from "../utils/loop.js";
|
|
@@ -466,7 +465,7 @@ export class Tester extends TaskAgent {
|
|
|
466
465
|
this.previousUrl = currentUrl;
|
|
467
466
|
this.previousStateHash = currentStateHash;
|
|
468
467
|
let context = '';
|
|
469
|
-
const focusArea =
|
|
468
|
+
const focusArea = currentState.overlay;
|
|
470
469
|
const focusedElement = currentState.focusedElement;
|
|
471
470
|
if (focusedElement) {
|
|
472
471
|
const isTextInput = ['textbox', 'combobox', 'searchbox'].includes(focusedElement.role);
|
|
@@ -9,7 +9,7 @@ export class ExitCommand extends BaseCommand {
|
|
|
9
9
|
aliases = ['quit'];
|
|
10
10
|
async execute(_args) {
|
|
11
11
|
await this.explorBot.printSessionAnalysis();
|
|
12
|
-
await this.explorBot.
|
|
12
|
+
await this.explorBot.stop();
|
|
13
13
|
if (Stats.hasActivity()) {
|
|
14
14
|
await new Promise((resolve) => {
|
|
15
15
|
const { unmount } = render(React.createElement(StatusPane, {
|
package/dist/src/explorbot.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ActionResult, type FocusedElement } from './action-result.js';
|
|
2
2
|
import type { ExperienceTracker } from './experience-tracker.js';
|
|
3
3
|
import type { Knowledge, KnowledgeTracker } from './knowledge-tracker.js';
|
|
4
|
+
import { Overlay } from './utils/overlay.js';
|
|
4
5
|
export interface Link {
|
|
5
6
|
title: string;
|
|
6
7
|
url: string;
|
|
@@ -40,6 +41,7 @@ export interface WebPageState {
|
|
|
40
41
|
focusedElement?: FocusedElement | null;
|
|
41
42
|
links?: Link[];
|
|
42
43
|
verifications?: Record<string, boolean>;
|
|
44
|
+
overlay?: Overlay;
|
|
43
45
|
}
|
|
44
46
|
export interface StateTransition {
|
|
45
47
|
/** Previous state (null if this is the first state) */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ActionResult } from './action-result.js';
|
|
2
|
-
import { detectFocusArea } from './utils/aria.js';
|
|
3
2
|
import { createDebug, tag } from './utils/logger.js';
|
|
3
|
+
import { Overlay } from './utils/overlay.js';
|
|
4
4
|
import { slugify } from './utils/strings.js';
|
|
5
5
|
import { extractStatePath } from './utils/url-matcher.js';
|
|
6
6
|
const debugLog = createDebug('explorbot:state');
|
|
@@ -114,8 +114,8 @@ export class StateManager {
|
|
|
114
114
|
return newState;
|
|
115
115
|
}
|
|
116
116
|
hasDialogAppeared(previousState, newState) {
|
|
117
|
-
const prevFocus =
|
|
118
|
-
const newFocus =
|
|
117
|
+
const prevFocus = previousState?.overlay ?? Overlay.fromAria(previousState?.ariaSnapshot ?? null);
|
|
118
|
+
const newFocus = newState.overlay ?? Overlay.fromAria(newState.ariaSnapshot ?? null);
|
|
119
119
|
return !prevFocus.detected && newFocus.detected;
|
|
120
120
|
}
|
|
121
121
|
/**
|
package/dist/src/utils/html.d.ts
CHANGED
|
@@ -14,7 +14,6 @@ export declare const HTML_SELECTORS: {
|
|
|
14
14
|
readonly interactiveControl: "button, a[href], input, select, textarea, [role=\"button\"], [role=\"link\"], [role=\"checkbox\"], [role=\"radio\"], [role=\"switch\"], [role=\"tab\"], [role=\"menuitem\"]";
|
|
15
15
|
readonly labelLike: "h1, h2, h3, h4, h5, h6, legend, caption, label, [role=\"heading\"], [class*=\"title\"], [class*=\"label\"], [class*=\"header\"], [class*=\"name\"]";
|
|
16
16
|
readonly semanticContextContainer: "section, article, form, fieldset, li, tr, td, th, [role=\"group\"], [role=\"tabpanel\"], [role=\"region\"], [class*=\"card\"], [class*=\"panel\"], [class*=\"item\"], [class*=\"usage\"], [class*=\"group\"]";
|
|
17
|
-
readonly semanticOverlays: readonly ["[role=\"dialog\"]", "[role=\"listbox\"]", "[role=\"menu\"]", "[role=\"tooltip\"]:not([style*=\"display: none\"]):not([style*=\"visibility: hidden\"])"];
|
|
18
17
|
};
|
|
19
18
|
export declare const HTML_VISIBILITY_LIMITS: {
|
|
20
19
|
readonly maxViewportOverlayRatio: 0.95;
|
|
@@ -67,7 +66,9 @@ export type VisibleOverlayExtractionConfig = {
|
|
|
67
66
|
interactiveContentSelector: string;
|
|
68
67
|
limits: typeof HTML_EXTRACTION_LIMITS;
|
|
69
68
|
overlaySelectors: readonly string[];
|
|
69
|
+
overlaySemanticSelector: string;
|
|
70
70
|
visibilityLimits: typeof HTML_VISIBILITY_LIMITS;
|
|
71
|
+
geometryFallback?: boolean;
|
|
71
72
|
};
|
|
72
73
|
export type ComponentScopeExtractionConfig = {
|
|
73
74
|
eidxAttr: string;
|
package/dist/src/utils/html.js
CHANGED
|
@@ -79,7 +79,6 @@ export const HTML_SELECTORS = {
|
|
|
79
79
|
interactiveControl: 'button, a[href], input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"], [role="menuitem"]',
|
|
80
80
|
labelLike: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"], [class*="title"], [class*="label"], [class*="header"], [class*="name"]',
|
|
81
81
|
semanticContextContainer: 'section, article, form, fieldset, li, tr, td, th, [role="group"], [role="tabpanel"], [role="region"], [class*="card"], [class*="panel"], [class*="item"], [class*="usage"], [class*="group"]',
|
|
82
|
-
semanticOverlays: ['[role="dialog"]', '[role="listbox"]', '[role="menu"]', '[role="tooltip"]:not([style*="display: none"]):not([style*="visibility: hidden"])'],
|
|
83
82
|
};
|
|
84
83
|
export const HTML_VISIBILITY_LIMITS = {
|
|
85
84
|
maxViewportOverlayRatio: 0.95,
|
|
@@ -447,8 +446,12 @@ export function extractVisibleOverlayHtml(config) {
|
|
|
447
446
|
const { interactiveCount, text } = getUsefulContent(element);
|
|
448
447
|
return interactiveCount > 0 || text.length > 0;
|
|
449
448
|
}
|
|
450
|
-
|
|
449
|
+
function isFloatingOverlay(element) {
|
|
450
|
+
const style = window.getComputedStyle(element);
|
|
451
|
+
return style.position === 'fixed' || style.position === 'absolute' || Number.parseInt(style.zIndex || '0', 10) > 0;
|
|
452
|
+
}
|
|
451
453
|
const seen = new Set();
|
|
454
|
+
const collected = [];
|
|
452
455
|
for (const selector of config.overlaySelectors) {
|
|
453
456
|
for (const element of Array.from(document.querySelectorAll(selector))) {
|
|
454
457
|
if (seen.has(element))
|
|
@@ -456,13 +459,16 @@ export function extractVisibleOverlayHtml(config) {
|
|
|
456
459
|
seen.add(element);
|
|
457
460
|
if (!isVisible(element))
|
|
458
461
|
continue;
|
|
462
|
+
if (!element.matches(config.overlaySemanticSelector) && !isFloatingOverlay(element))
|
|
463
|
+
continue;
|
|
459
464
|
const { interactiveCount, text } = getUsefulContent(element);
|
|
460
465
|
if (interactiveCount === 0 && text.length === 0)
|
|
461
466
|
continue;
|
|
462
|
-
|
|
467
|
+
collected.push(element);
|
|
463
468
|
}
|
|
464
469
|
}
|
|
465
|
-
|
|
470
|
+
const overlays = collected.filter((element) => !collected.some((other) => other !== element && element.contains(other))).map((element) => element.outerHTML.slice(0, config.limits.overlayHtmlLength));
|
|
471
|
+
if (overlays.length === 0 && config.geometryFallback !== false) {
|
|
466
472
|
const floatingCandidates = Array.from(document.body.querySelectorAll('*'))
|
|
467
473
|
.filter((element) => !seen.has(element) && isVisible(element) && isLikelyFloatingOverlay(element))
|
|
468
474
|
.sort((left, right) => {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type VisibleOverlayExtractionConfig } from './html.js';
|
|
2
|
+
export declare const OVERLAY_SELECTORS: {
|
|
3
|
+
readonly semanticOverlays: readonly ["[role=\"dialog\"]", "[role=\"listbox\"]", "[role=\"menu\"]", "[role=\"tooltip\"]:not([style*=\"display: none\"]):not([style*=\"visibility: hidden\"])", "[class*=\"modal\"]", "[class*=\"dialog\"]", "[class*=\"overlay\"]", "[class*=\"popup\"]", "[class*=\"drawer\"]", "[class*=\"lightbox\"]"];
|
|
4
|
+
readonly modalOverlays: readonly ["[role=\"dialog\"]", "[role=\"alertdialog\"]", "[aria-modal=\"true\"]", "[class*=\"modal\"]", "[class*=\"dialog\"]", "[class*=\"overlay\"]", "[class*=\"popup\"]", "[class*=\"drawer\"]", "[class*=\"lightbox\"]"];
|
|
5
|
+
readonly overlaySemanticSelector: "[role=\"dialog\"], [role=\"alertdialog\"], [aria-modal=\"true\"], [role=\"listbox\"], [role=\"menu\"], [role=\"tooltip\"]";
|
|
6
|
+
};
|
|
7
|
+
export type OverlayData = {
|
|
8
|
+
type?: 'dialog' | 'modal' | null;
|
|
9
|
+
name?: string | null;
|
|
10
|
+
};
|
|
11
|
+
export declare class Overlay {
|
|
12
|
+
readonly type: 'dialog' | 'modal' | null;
|
|
13
|
+
readonly name: string | null;
|
|
14
|
+
constructor(data?: OverlayData);
|
|
15
|
+
get detected(): boolean;
|
|
16
|
+
static fromHtml(html: string): Overlay;
|
|
17
|
+
static fromAria(snapshot: string | null): Overlay;
|
|
18
|
+
static resolve(data: {
|
|
19
|
+
overlayHtml?: string;
|
|
20
|
+
overlay?: OverlayData | null;
|
|
21
|
+
ariaSnapshot?: string | null;
|
|
22
|
+
}): Overlay;
|
|
23
|
+
static captureConfig(): VisibleOverlayExtractionConfig;
|
|
24
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { detectFocusArea } from './aria.js';
|
|
2
|
+
import { HTML_EXTRACTION_LIMITS, HTML_SELECTORS, HTML_VISIBILITY_LIMITS, extractHeadings } from './html.js';
|
|
3
|
+
export const OVERLAY_SELECTORS = {
|
|
4
|
+
semanticOverlays: ['[role="dialog"]', '[role="listbox"]', '[role="menu"]', '[role="tooltip"]:not([style*="display: none"]):not([style*="visibility: hidden"])', '[class*="modal"]', '[class*="dialog"]', '[class*="overlay"]', '[class*="popup"]', '[class*="drawer"]', '[class*="lightbox"]'],
|
|
5
|
+
modalOverlays: ['[role="dialog"]', '[role="alertdialog"]', '[aria-modal="true"]', '[class*="modal"]', '[class*="dialog"]', '[class*="overlay"]', '[class*="popup"]', '[class*="drawer"]', '[class*="lightbox"]'],
|
|
6
|
+
overlaySemanticSelector: '[role="dialog"], [role="alertdialog"], [aria-modal="true"], [role="listbox"], [role="menu"], [role="tooltip"]',
|
|
7
|
+
};
|
|
8
|
+
export class Overlay {
|
|
9
|
+
type;
|
|
10
|
+
name;
|
|
11
|
+
constructor(data = {}) {
|
|
12
|
+
this.type = data.type ?? null;
|
|
13
|
+
this.name = data.name ?? null;
|
|
14
|
+
}
|
|
15
|
+
get detected() {
|
|
16
|
+
return this.type !== null;
|
|
17
|
+
}
|
|
18
|
+
static fromHtml(html) {
|
|
19
|
+
const headings = extractHeadings(html);
|
|
20
|
+
const name = [headings.h1, headings.h2, headings.h3, headings.h4].filter(Boolean).join(' ');
|
|
21
|
+
return new Overlay({ type: 'modal', name: name || null });
|
|
22
|
+
}
|
|
23
|
+
static fromAria(snapshot) {
|
|
24
|
+
return new Overlay(detectFocusArea(snapshot));
|
|
25
|
+
}
|
|
26
|
+
static resolve(data) {
|
|
27
|
+
if (data.overlayHtml)
|
|
28
|
+
return Overlay.fromHtml(data.overlayHtml);
|
|
29
|
+
if (data.overlay)
|
|
30
|
+
return new Overlay(data.overlay);
|
|
31
|
+
return Overlay.fromAria(data.ariaSnapshot ?? null);
|
|
32
|
+
}
|
|
33
|
+
static captureConfig() {
|
|
34
|
+
return {
|
|
35
|
+
interactiveContentSelector: HTML_SELECTORS.interactiveContent,
|
|
36
|
+
limits: HTML_EXTRACTION_LIMITS,
|
|
37
|
+
overlaySelectors: OVERLAY_SELECTORS.modalOverlays,
|
|
38
|
+
overlaySemanticSelector: OVERLAY_SELECTORS.overlaySemanticSelector,
|
|
39
|
+
visibilityLimits: HTML_VISIBILITY_LIMITS,
|
|
40
|
+
geometryFallback: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
package/package.json
CHANGED
package/src/action-result.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { TTLCache } from './utils/cache.ts';
|
|
|
7
7
|
import { type HtmlDiffPart, type HtmlDiffResult, htmlDiff, liveRegionMessages } from './utils/html-diff.ts';
|
|
8
8
|
import { extractHeadings, extractLinks, extractTargetedHtml, htmlCombinedSnapshot, htmlMinimalUISnapshot, htmlTextSnapshot, minifyHtml } from './utils/html.ts';
|
|
9
9
|
import { createDebug } from './utils/logger.ts';
|
|
10
|
+
import { Overlay } from './utils/overlay.ts';
|
|
10
11
|
import { slugify } from './utils/strings.ts';
|
|
11
12
|
import { extractStatePath, matchesUrl } from './utils/url-matcher.ts';
|
|
12
13
|
|
|
@@ -34,6 +35,7 @@ interface ActionResultData extends WebPageState {
|
|
|
34
35
|
focusedElement?: FocusedElement | null;
|
|
35
36
|
iframeURL?: string;
|
|
36
37
|
links?: Link[];
|
|
38
|
+
overlayHtml?: string;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export interface PageDiff {
|
|
@@ -86,6 +88,7 @@ export class ActionResult implements ActionResultData {
|
|
|
86
88
|
notes: string[] = [];
|
|
87
89
|
public links: Link[] = [];
|
|
88
90
|
public verifications?: Record<string, boolean>;
|
|
91
|
+
public overlay: Overlay = new Overlay();
|
|
89
92
|
|
|
90
93
|
constructor(data: ActionResultData) {
|
|
91
94
|
this.id = data.id;
|
|
@@ -131,6 +134,8 @@ export class ActionResult implements ActionResultData {
|
|
|
131
134
|
this._ariaSnapshot = data.ariaSnapshot;
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
this.overlay = Overlay.resolve(data);
|
|
138
|
+
|
|
134
139
|
if (!this.fullUrl && this.url) {
|
|
135
140
|
this.fullUrl = this.url;
|
|
136
141
|
}
|
package/src/action.ts
CHANGED
|
@@ -11,8 +11,9 @@ import { Observability } from './observability.ts';
|
|
|
11
11
|
import type { PlaywrightRecorder } from './playwright-recorder.ts';
|
|
12
12
|
import type { StateManager } from './state-manager.js';
|
|
13
13
|
import { browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from './utils/browser-errors.ts';
|
|
14
|
-
import { captureHtmlForSnapshot, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
14
|
+
import { captureHtmlForSnapshot, getVisibleOverlayHtmlExtractorSource, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
15
15
|
import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
|
|
16
|
+
import { Overlay } from './utils/overlay.js';
|
|
16
17
|
import { sleep, waitForPageReadiness } from './utils/page-readiness.ts';
|
|
17
18
|
import { safeFilename } from './utils/strings.ts';
|
|
18
19
|
import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from './utils/web-sandbox.ts';
|
|
@@ -144,11 +145,13 @@ class Action {
|
|
|
144
145
|
let ariaSnapshot: string | null = null;
|
|
145
146
|
let ariaSnapshotFile: string | undefined = undefined;
|
|
146
147
|
let focusedElement: FocusedElement | null = null;
|
|
148
|
+
let overlayHtml = '';
|
|
147
149
|
|
|
148
150
|
try {
|
|
149
151
|
const page = this.playwrightHelper.page;
|
|
150
152
|
ariaSnapshot = await page.locator('body').ariaSnapshot();
|
|
151
153
|
focusedElement = await page.evaluate(readFocusedElement);
|
|
154
|
+
if (!frame) overlayHtml = await this.captureOverlayHtml();
|
|
152
155
|
} catch (err) {
|
|
153
156
|
debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
|
|
154
157
|
}
|
|
@@ -177,6 +180,7 @@ class Action {
|
|
|
177
180
|
ariaSnapshot,
|
|
178
181
|
ariaSnapshotFile,
|
|
179
182
|
focusedElement,
|
|
183
|
+
overlayHtml: overlayHtml || undefined,
|
|
180
184
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
181
185
|
});
|
|
182
186
|
this.stateManager.updateState(result, codeBlock);
|
|
@@ -190,6 +194,16 @@ class Action {
|
|
|
190
194
|
}
|
|
191
195
|
}
|
|
192
196
|
|
|
197
|
+
private async captureOverlayHtml(): Promise<string> {
|
|
198
|
+
return this.playwrightHelper.page.evaluate(
|
|
199
|
+
({ extractorSource, config }: { extractorSource: string; config: any }) => {
|
|
200
|
+
const extract = new Function(`return ${extractorSource}`)() as (config: any) => string;
|
|
201
|
+
return extract(config);
|
|
202
|
+
},
|
|
203
|
+
{ extractorSource: getVisibleOverlayHtmlExtractorSource(), config: Overlay.captureConfig() }
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
193
207
|
private async captureMainDocumentStatus(): Promise<number | undefined> {
|
|
194
208
|
if (this.mainDocumentStatus) return this.mainDocumentStatus;
|
|
195
209
|
|
package/src/ai/driller.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '../utils/html.ts';
|
|
23
23
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
24
24
|
import { loop, pause } from '../utils/loop.ts';
|
|
25
|
+
import { OVERLAY_SELECTORS } from '../utils/overlay.ts';
|
|
25
26
|
import { annotatePageElements } from '../utils/web-annotate.ts';
|
|
26
27
|
import { eidxInContainer } from '../utils/web-eidx.ts';
|
|
27
28
|
import { WebElement } from '../utils/web-element.ts';
|
|
@@ -682,7 +683,8 @@ export class Driller extends TaskAgent implements Agent {
|
|
|
682
683
|
config: {
|
|
683
684
|
interactiveContentSelector: HTML_SELECTORS.interactiveContent,
|
|
684
685
|
limits: HTML_EXTRACTION_LIMITS,
|
|
685
|
-
overlaySelectors:
|
|
686
|
+
overlaySelectors: OVERLAY_SELECTORS.semanticOverlays,
|
|
687
|
+
overlaySemanticSelector: OVERLAY_SELECTORS.overlaySemanticSelector,
|
|
686
688
|
visibilityLimits: HTML_VISIBILITY_LIMITS,
|
|
687
689
|
},
|
|
688
690
|
}
|
package/src/ai/pilot.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { PlaywrightRecorder } from '../playwright-recorder.ts';
|
|
|
9
9
|
import type { StateManager } from '../state-manager.ts';
|
|
10
10
|
import { Stats } from '../stats.ts';
|
|
11
11
|
import { type Test, TestResult } from '../test-plan.ts';
|
|
12
|
-
import { collectInteractiveNodes
|
|
12
|
+
import { collectInteractiveNodes } from '../utils/aria.ts';
|
|
13
13
|
import { ErrorPageError } from '../utils/error-page.ts';
|
|
14
14
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
15
15
|
|
|
@@ -824,7 +824,7 @@ export class Pilot implements Agent {
|
|
|
824
824
|
lines.push(`h3: ${state.h3 || ''}`);
|
|
825
825
|
lines.push(`h4: ${state.h4 || ''}`);
|
|
826
826
|
|
|
827
|
-
const focusArea =
|
|
827
|
+
const focusArea = state.overlay;
|
|
828
828
|
if (focusArea.detected) {
|
|
829
829
|
lines.push(`modal: ${focusArea.name || focusArea.type}`);
|
|
830
830
|
} else {
|
package/src/ai/provider.ts
CHANGED
|
@@ -37,6 +37,15 @@ function createHarmonyChannelFallbackTool() {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
let telemetryRegistered = false;
|
|
40
|
+
let beforeExitFlushHooked = false;
|
|
41
|
+
let activeOtelSdk: NodeSDK | null = null;
|
|
42
|
+
|
|
43
|
+
export async function flushTelemetry(): Promise<void> {
|
|
44
|
+
const sdk = activeOtelSdk;
|
|
45
|
+
activeOtelSdk = null;
|
|
46
|
+
if (!sdk) return;
|
|
47
|
+
await sdk.shutdown().catch((error) => debugLog(`Telemetry flush failed: ${error instanceof Error ? error.message : error}`));
|
|
48
|
+
}
|
|
40
49
|
|
|
41
50
|
const CONTEXT_LENGTH_PATTERNS = ['reduce the length', 'context length', 'maximum context', 'token limit', 'too many tokens', 'max_tokens', 'context_length_exceeded', 'output truncated at maxtokens'];
|
|
42
51
|
|
|
@@ -117,6 +126,10 @@ export class Provider {
|
|
|
117
126
|
}
|
|
118
127
|
}
|
|
119
128
|
|
|
129
|
+
async stop(): Promise<void> {
|
|
130
|
+
await flushTelemetry();
|
|
131
|
+
}
|
|
132
|
+
|
|
120
133
|
getModelForAgent(agentName?: string): any {
|
|
121
134
|
if (!agentName) {
|
|
122
135
|
return this.config.model;
|
|
@@ -270,7 +283,12 @@ export class Provider {
|
|
|
270
283
|
spanProcessors: [processor],
|
|
271
284
|
instrumentations: [],
|
|
272
285
|
});
|
|
286
|
+
activeOtelSdk = this.otelSdk;
|
|
273
287
|
void this.otelSdk.start();
|
|
288
|
+
if (!beforeExitFlushHooked) {
|
|
289
|
+
process.on('beforeExit', () => void flushTelemetry());
|
|
290
|
+
beforeExitFlushHooked = true;
|
|
291
|
+
}
|
|
274
292
|
if (!telemetryRegistered) {
|
|
275
293
|
registerTelemetry(new OpenTelemetry());
|
|
276
294
|
telemetryRegistered = true;
|
|
@@ -324,7 +342,7 @@ export class Provider {
|
|
|
324
342
|
async invokeConversation(conversation: Conversation, tools?: any, options: any = {}): Promise<{ conversation: Conversation; response: any; toolExecutions?: any[] } | null> {
|
|
325
343
|
const response = tools ? await this.generateWithTools(conversation.messages, conversation.model, tools, options) : await this.chat(conversation.messages, conversation.model, options);
|
|
326
344
|
|
|
327
|
-
const responseMessages = response.
|
|
345
|
+
const responseMessages = response.responseMessages || [];
|
|
328
346
|
if (responseMessages.length > 0) {
|
|
329
347
|
conversation.messages.push(...responseMessages);
|
|
330
348
|
tag('debug').log('Added', responseMessages.length, 'messages from response');
|
|
@@ -5,7 +5,7 @@ import { executionController } from '../../execution-controller.ts';
|
|
|
5
5
|
import type Explorer from '../../explorer.ts';
|
|
6
6
|
import type { StateManager } from '../../state-manager.js';
|
|
7
7
|
import { WebPageState } from '../../state-manager.js';
|
|
8
|
-
import {
|
|
8
|
+
import { diffAriaSnapshots } from '../../utils/aria.ts';
|
|
9
9
|
import { extractCodeBlocks } from '../../utils/code-extractor.ts';
|
|
10
10
|
import { tag } from '../../utils/logger.js';
|
|
11
11
|
import { mdq } from '../../utils/markdown-query.ts';
|
|
@@ -86,7 +86,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
async researchOverlay(current: ActionResult, previous: ActionResult, pageStateHash: string): Promise<string | null> {
|
|
89
|
-
const focusArea =
|
|
89
|
+
const focusArea = current.overlay;
|
|
90
90
|
if (!focusArea.detected || !focusArea.name) return null;
|
|
91
91
|
if (focusArea.type !== 'dialog' && focusArea.type !== 'modal') return null;
|
|
92
92
|
|
package/src/ai/tester.ts
CHANGED
|
@@ -11,7 +11,6 @@ import { Observability } from '../observability.ts';
|
|
|
11
11
|
import { type StateTransition, normalizeUrl } from '../state-manager.ts';
|
|
12
12
|
import { Stats } from '../stats.ts';
|
|
13
13
|
import { type Test, TestResult, type TestResultType } from '../test-plan.ts';
|
|
14
|
-
import { detectFocusArea } from '../utils/aria.ts';
|
|
15
14
|
import { ErrorPageError, isErrorPage } from '../utils/error-page.ts';
|
|
16
15
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
17
16
|
import { loop } from '../utils/loop.ts';
|
|
@@ -531,7 +530,7 @@ export class Tester extends TaskAgent implements Agent {
|
|
|
531
530
|
|
|
532
531
|
let context = '';
|
|
533
532
|
|
|
534
|
-
const focusArea =
|
|
533
|
+
const focusArea = currentState.overlay;
|
|
535
534
|
|
|
536
535
|
const focusedElement = currentState.focusedElement;
|
|
537
536
|
if (focusedElement) {
|
|
@@ -11,7 +11,7 @@ export class ExitCommand extends BaseCommand {
|
|
|
11
11
|
|
|
12
12
|
async execute(_args: string): Promise<void> {
|
|
13
13
|
await this.explorBot.printSessionAnalysis();
|
|
14
|
-
await this.explorBot.
|
|
14
|
+
await this.explorBot.stop();
|
|
15
15
|
|
|
16
16
|
if (Stats.hasActivity()) {
|
|
17
17
|
await new Promise<void>((resolve) => {
|
package/src/explorbot.ts
CHANGED
package/src/state-manager.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ActionResult, type FocusedElement } from './action-result.js';
|
|
2
2
|
import type { ExperienceTracker } from './experience-tracker.js';
|
|
3
3
|
import type { Knowledge, KnowledgeTracker } from './knowledge-tracker.js';
|
|
4
|
-
import { detectFocusArea } from './utils/aria.js';
|
|
5
4
|
import { createDebug, tag } from './utils/logger.js';
|
|
5
|
+
import { Overlay } from './utils/overlay.js';
|
|
6
6
|
import { slugify } from './utils/strings.js';
|
|
7
7
|
import { extractStatePath } from './utils/url-matcher.js';
|
|
8
8
|
|
|
@@ -49,6 +49,7 @@ export interface WebPageState {
|
|
|
49
49
|
focusedElement?: FocusedElement | null;
|
|
50
50
|
links?: Link[];
|
|
51
51
|
verifications?: Record<string, boolean>;
|
|
52
|
+
overlay?: Overlay;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export interface StateTransition {
|
|
@@ -206,8 +207,8 @@ export class StateManager {
|
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
private hasDialogAppeared(previousState: WebPageState | null, newState: WebPageState): boolean {
|
|
209
|
-
const prevFocus =
|
|
210
|
-
const newFocus =
|
|
210
|
+
const prevFocus = previousState?.overlay ?? Overlay.fromAria(previousState?.ariaSnapshot ?? null);
|
|
211
|
+
const newFocus = newState.overlay ?? Overlay.fromAria(newState.ariaSnapshot ?? null);
|
|
211
212
|
return !prevFocus.detected && newFocus.detected;
|
|
212
213
|
}
|
|
213
214
|
|
package/src/utils/html.ts
CHANGED
|
@@ -98,7 +98,6 @@ export const HTML_SELECTORS = {
|
|
|
98
98
|
interactiveControl: 'button, a[href], input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"], [role="menuitem"]',
|
|
99
99
|
labelLike: 'h1, h2, h3, h4, h5, h6, legend, caption, label, [role="heading"], [class*="title"], [class*="label"], [class*="header"], [class*="name"]',
|
|
100
100
|
semanticContextContainer: 'section, article, form, fieldset, li, tr, td, th, [role="group"], [role="tabpanel"], [role="region"], [class*="card"], [class*="panel"], [class*="item"], [class*="usage"], [class*="group"]',
|
|
101
|
-
semanticOverlays: ['[role="dialog"]', '[role="listbox"]', '[role="menu"]', '[role="tooltip"]:not([style*="display: none"]):not([style*="visibility: hidden"])'],
|
|
102
101
|
} as const;
|
|
103
102
|
|
|
104
103
|
export const HTML_VISIBILITY_LIMITS = {
|
|
@@ -163,7 +162,9 @@ export type VisibleOverlayExtractionConfig = {
|
|
|
163
162
|
interactiveContentSelector: string;
|
|
164
163
|
limits: typeof HTML_EXTRACTION_LIMITS;
|
|
165
164
|
overlaySelectors: readonly string[];
|
|
165
|
+
overlaySemanticSelector: string;
|
|
166
166
|
visibilityLimits: typeof HTML_VISIBILITY_LIMITS;
|
|
167
|
+
geometryFallback?: boolean;
|
|
167
168
|
};
|
|
168
169
|
export type ComponentScopeExtractionConfig = {
|
|
169
170
|
eidxAttr: string;
|
|
@@ -479,20 +480,28 @@ export function extractVisibleOverlayHtml(config: VisibleOverlayExtractionConfig
|
|
|
479
480
|
return interactiveCount > 0 || text.length > 0;
|
|
480
481
|
}
|
|
481
482
|
|
|
482
|
-
|
|
483
|
+
function isFloatingOverlay(element: Element): boolean {
|
|
484
|
+
const style = window.getComputedStyle(element as HTMLElement);
|
|
485
|
+
return style.position === 'fixed' || style.position === 'absolute' || Number.parseInt(style.zIndex || '0', 10) > 0;
|
|
486
|
+
}
|
|
487
|
+
|
|
483
488
|
const seen = new Set<Element>();
|
|
489
|
+
const collected: Element[] = [];
|
|
484
490
|
for (const selector of config.overlaySelectors) {
|
|
485
491
|
for (const element of Array.from(document.querySelectorAll(selector))) {
|
|
486
492
|
if (seen.has(element)) continue;
|
|
487
493
|
seen.add(element);
|
|
488
494
|
if (!isVisible(element)) continue;
|
|
495
|
+
if (!element.matches(config.overlaySemanticSelector) && !isFloatingOverlay(element)) continue;
|
|
489
496
|
const { interactiveCount, text } = getUsefulContent(element);
|
|
490
497
|
if (interactiveCount === 0 && text.length === 0) continue;
|
|
491
|
-
|
|
498
|
+
collected.push(element);
|
|
492
499
|
}
|
|
493
500
|
}
|
|
494
501
|
|
|
495
|
-
|
|
502
|
+
const overlays = collected.filter((element) => !collected.some((other) => other !== element && element.contains(other))).map((element) => (element as HTMLElement).outerHTML.slice(0, config.limits.overlayHtmlLength));
|
|
503
|
+
|
|
504
|
+
if (overlays.length === 0 && config.geometryFallback !== false) {
|
|
496
505
|
const floatingCandidates = Array.from(document.body.querySelectorAll('*'))
|
|
497
506
|
.filter((element) => !seen.has(element) && isVisible(element) && isLikelyFloatingOverlay(element))
|
|
498
507
|
.sort((left, right) => {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { detectFocusArea } from './aria.js';
|
|
2
|
+
import { HTML_EXTRACTION_LIMITS, HTML_SELECTORS, HTML_VISIBILITY_LIMITS, type VisibleOverlayExtractionConfig, extractHeadings } from './html.js';
|
|
3
|
+
|
|
4
|
+
export const OVERLAY_SELECTORS = {
|
|
5
|
+
semanticOverlays: ['[role="dialog"]', '[role="listbox"]', '[role="menu"]', '[role="tooltip"]:not([style*="display: none"]):not([style*="visibility: hidden"])', '[class*="modal"]', '[class*="dialog"]', '[class*="overlay"]', '[class*="popup"]', '[class*="drawer"]', '[class*="lightbox"]'],
|
|
6
|
+
modalOverlays: ['[role="dialog"]', '[role="alertdialog"]', '[aria-modal="true"]', '[class*="modal"]', '[class*="dialog"]', '[class*="overlay"]', '[class*="popup"]', '[class*="drawer"]', '[class*="lightbox"]'],
|
|
7
|
+
overlaySemanticSelector: '[role="dialog"], [role="alertdialog"], [aria-modal="true"], [role="listbox"], [role="menu"], [role="tooltip"]',
|
|
8
|
+
} as const;
|
|
9
|
+
|
|
10
|
+
export type OverlayData = { type?: 'dialog' | 'modal' | null; name?: string | null };
|
|
11
|
+
|
|
12
|
+
export class Overlay {
|
|
13
|
+
readonly type: 'dialog' | 'modal' | null;
|
|
14
|
+
readonly name: string | null;
|
|
15
|
+
|
|
16
|
+
constructor(data: OverlayData = {}) {
|
|
17
|
+
this.type = data.type ?? null;
|
|
18
|
+
this.name = data.name ?? null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get detected(): boolean {
|
|
22
|
+
return this.type !== null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static fromHtml(html: string): Overlay {
|
|
26
|
+
const headings = extractHeadings(html);
|
|
27
|
+
const name = [headings.h1, headings.h2, headings.h3, headings.h4].filter(Boolean).join(' ');
|
|
28
|
+
return new Overlay({ type: 'modal', name: name || null });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static fromAria(snapshot: string | null): Overlay {
|
|
32
|
+
return new Overlay(detectFocusArea(snapshot));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static resolve(data: { overlayHtml?: string; overlay?: OverlayData | null; ariaSnapshot?: string | null }): Overlay {
|
|
36
|
+
if (data.overlayHtml) return Overlay.fromHtml(data.overlayHtml);
|
|
37
|
+
if (data.overlay) return new Overlay(data.overlay);
|
|
38
|
+
return Overlay.fromAria(data.ariaSnapshot ?? null);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static captureConfig(): VisibleOverlayExtractionConfig {
|
|
42
|
+
return {
|
|
43
|
+
interactiveContentSelector: HTML_SELECTORS.interactiveContent,
|
|
44
|
+
limits: HTML_EXTRACTION_LIMITS,
|
|
45
|
+
overlaySelectors: OVERLAY_SELECTORS.modalOverlays,
|
|
46
|
+
overlaySemanticSelector: OVERLAY_SELECTORS.overlaySemanticSelector,
|
|
47
|
+
visibilityLimits: HTML_VISIBILITY_LIMITS,
|
|
48
|
+
geometryFallback: false,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|