agent-browser 0.3.1 → 0.3.3
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/agent-browser +13 -2
- package/bin/agent-browser-darwin-arm64 +0 -0
- package/bin/agent-browser-darwin-x64 +0 -0
- package/bin/agent-browser-linux-arm64 +0 -0
- package/bin/agent-browser-linux-x64 +0 -0
- package/package.json +6 -1
- package/.prettierrc +0 -7
- package/AGENTS.md +0 -26
- package/benchmark/benchmark.ts +0 -521
- package/benchmark/run.ts +0 -322
- package/cli/Cargo.lock +0 -114
- package/cli/Cargo.toml +0 -17
- package/cli/src/main.rs +0 -332
- package/docker/Dockerfile.build +0 -31
- package/docker/docker-compose.yml +0 -68
- package/src/actions.ts +0 -1670
- package/src/browser.test.ts +0 -157
- package/src/browser.ts +0 -686
- package/src/cli-light.ts +0 -457
- package/src/client.ts +0 -150
- package/src/daemon.ts +0 -187
- package/src/index.ts +0 -1185
- package/src/protocol.test.ts +0 -216
- package/src/protocol.ts +0 -852
- package/src/snapshot.ts +0 -380
- package/src/types.ts +0 -913
- package/tsconfig.json +0 -28
- package/vitest.config.ts +0 -9
package/src/snapshot.ts
DELETED
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enhanced snapshot with element refs for deterministic element selection.
|
|
3
|
-
*
|
|
4
|
-
* This module generates accessibility snapshots with embedded refs that can be
|
|
5
|
-
* used to click/fill/interact with elements without re-querying the DOM.
|
|
6
|
-
*
|
|
7
|
-
* Example output:
|
|
8
|
-
* - heading "Example Domain" [ref=e1] [level=1]
|
|
9
|
-
* - paragraph: Some text content
|
|
10
|
-
* - button "Submit" [ref=e2]
|
|
11
|
-
* - textbox "Email" [ref=e3]
|
|
12
|
-
*
|
|
13
|
-
* Usage:
|
|
14
|
-
* agent-browser snapshot # Full snapshot
|
|
15
|
-
* agent-browser snapshot -i # Interactive elements only
|
|
16
|
-
* agent-browser snapshot --depth 3 # Limit depth
|
|
17
|
-
* agent-browser click @e2 # Click element by ref
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import type { Page, Locator } from 'playwright-core';
|
|
21
|
-
|
|
22
|
-
export interface RefMap {
|
|
23
|
-
[ref: string]: {
|
|
24
|
-
selector: string;
|
|
25
|
-
role: string;
|
|
26
|
-
name?: string;
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface EnhancedSnapshot {
|
|
31
|
-
tree: string;
|
|
32
|
-
refs: RefMap;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface SnapshotOptions {
|
|
36
|
-
/** Only include interactive elements (buttons, links, inputs, etc.) */
|
|
37
|
-
interactive?: boolean;
|
|
38
|
-
/** Maximum depth of tree to include (0 = root only) */
|
|
39
|
-
maxDepth?: number;
|
|
40
|
-
/** Remove structural elements without meaningful content */
|
|
41
|
-
compact?: boolean;
|
|
42
|
-
/** CSS selector to scope the snapshot */
|
|
43
|
-
selector?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Counter for generating refs
|
|
47
|
-
let refCounter = 0;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Reset ref counter (call at start of each snapshot)
|
|
51
|
-
*/
|
|
52
|
-
export function resetRefs(): void {
|
|
53
|
-
refCounter = 0;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Generate next ref ID
|
|
58
|
-
*/
|
|
59
|
-
function nextRef(): string {
|
|
60
|
-
return `e${++refCounter}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Roles that are interactive and should get refs
|
|
65
|
-
*/
|
|
66
|
-
const INTERACTIVE_ROLES = new Set([
|
|
67
|
-
'button',
|
|
68
|
-
'link',
|
|
69
|
-
'textbox',
|
|
70
|
-
'checkbox',
|
|
71
|
-
'radio',
|
|
72
|
-
'combobox',
|
|
73
|
-
'listbox',
|
|
74
|
-
'menuitem',
|
|
75
|
-
'menuitemcheckbox',
|
|
76
|
-
'menuitemradio',
|
|
77
|
-
'option',
|
|
78
|
-
'searchbox',
|
|
79
|
-
'slider',
|
|
80
|
-
'spinbutton',
|
|
81
|
-
'switch',
|
|
82
|
-
'tab',
|
|
83
|
-
'treeitem',
|
|
84
|
-
]);
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Roles that provide structure/context (get refs for text extraction)
|
|
88
|
-
*/
|
|
89
|
-
const CONTENT_ROLES = new Set([
|
|
90
|
-
'heading',
|
|
91
|
-
'cell',
|
|
92
|
-
'gridcell',
|
|
93
|
-
'columnheader',
|
|
94
|
-
'rowheader',
|
|
95
|
-
'listitem',
|
|
96
|
-
'article',
|
|
97
|
-
'region',
|
|
98
|
-
'main',
|
|
99
|
-
'navigation',
|
|
100
|
-
]);
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Roles that are purely structural (can be filtered in compact mode)
|
|
104
|
-
*/
|
|
105
|
-
const STRUCTURAL_ROLES = new Set([
|
|
106
|
-
'generic',
|
|
107
|
-
'group',
|
|
108
|
-
'list',
|
|
109
|
-
'table',
|
|
110
|
-
'row',
|
|
111
|
-
'rowgroup',
|
|
112
|
-
'grid',
|
|
113
|
-
'treegrid',
|
|
114
|
-
'menu',
|
|
115
|
-
'menubar',
|
|
116
|
-
'toolbar',
|
|
117
|
-
'tablist',
|
|
118
|
-
'tree',
|
|
119
|
-
'directory',
|
|
120
|
-
'document',
|
|
121
|
-
'application',
|
|
122
|
-
'presentation',
|
|
123
|
-
'none',
|
|
124
|
-
]);
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Build a selector string for storing in ref map
|
|
128
|
-
*/
|
|
129
|
-
function buildSelector(role: string, name?: string): string {
|
|
130
|
-
if (name) {
|
|
131
|
-
const escapedName = name.replace(/"/g, '\\"');
|
|
132
|
-
return `getByRole('${role}', { name: "${escapedName}" })`;
|
|
133
|
-
}
|
|
134
|
-
return `getByRole('${role}')`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Get enhanced snapshot with refs and optional filtering
|
|
139
|
-
*/
|
|
140
|
-
export async function getEnhancedSnapshot(
|
|
141
|
-
page: Page,
|
|
142
|
-
options: SnapshotOptions = {}
|
|
143
|
-
): Promise<EnhancedSnapshot> {
|
|
144
|
-
resetRefs();
|
|
145
|
-
const refs: RefMap = {};
|
|
146
|
-
|
|
147
|
-
// Get ARIA snapshot from Playwright
|
|
148
|
-
const locator = options.selector ? page.locator(options.selector) : page.locator(':root');
|
|
149
|
-
const ariaTree = await locator.ariaSnapshot();
|
|
150
|
-
|
|
151
|
-
if (!ariaTree) {
|
|
152
|
-
return {
|
|
153
|
-
tree: '(empty)',
|
|
154
|
-
refs: {},
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Parse and enhance the ARIA tree
|
|
159
|
-
const enhancedTree = processAriaTree(ariaTree, refs, options);
|
|
160
|
-
|
|
161
|
-
return { tree: enhancedTree, refs };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Process ARIA snapshot: add refs and apply filters
|
|
166
|
-
*/
|
|
167
|
-
function processAriaTree(ariaTree: string, refs: RefMap, options: SnapshotOptions): string {
|
|
168
|
-
const lines = ariaTree.split('\n');
|
|
169
|
-
const result: string[] = [];
|
|
170
|
-
|
|
171
|
-
// For interactive-only mode, we collect just interactive elements
|
|
172
|
-
if (options.interactive) {
|
|
173
|
-
for (const line of lines) {
|
|
174
|
-
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
175
|
-
if (!match) continue;
|
|
176
|
-
|
|
177
|
-
const [, , role, name, suffix] = match;
|
|
178
|
-
const roleLower = role.toLowerCase();
|
|
179
|
-
|
|
180
|
-
if (INTERACTIVE_ROLES.has(roleLower)) {
|
|
181
|
-
const ref = nextRef();
|
|
182
|
-
refs[ref] = {
|
|
183
|
-
selector: buildSelector(roleLower, name),
|
|
184
|
-
role: roleLower,
|
|
185
|
-
name,
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
let enhanced = `- ${role}`;
|
|
189
|
-
if (name) enhanced += ` "${name}"`;
|
|
190
|
-
enhanced += ` [ref=${ref}]`;
|
|
191
|
-
if (suffix && suffix.includes('[')) enhanced += suffix;
|
|
192
|
-
|
|
193
|
-
result.push(enhanced);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return result.join('\n') || '(no interactive elements)';
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// Normal processing with depth/compact filters
|
|
200
|
-
for (const line of lines) {
|
|
201
|
-
const processed = processLine(line, refs, options);
|
|
202
|
-
if (processed !== null) {
|
|
203
|
-
result.push(processed);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// If compact mode, remove empty structural elements
|
|
208
|
-
if (options.compact) {
|
|
209
|
-
return compactTree(result.join('\n'));
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return result.join('\n');
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Get indentation level (number of spaces / 2)
|
|
217
|
-
*/
|
|
218
|
-
function getIndentLevel(line: string): number {
|
|
219
|
-
const match = line.match(/^(\s*)/);
|
|
220
|
-
return match ? Math.floor(match[1].length / 2) : 0;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Process a single line: add ref if needed, filter if requested
|
|
225
|
-
*/
|
|
226
|
-
function processLine(
|
|
227
|
-
line: string,
|
|
228
|
-
refs: RefMap,
|
|
229
|
-
options: SnapshotOptions
|
|
230
|
-
): string | null {
|
|
231
|
-
const depth = getIndentLevel(line);
|
|
232
|
-
|
|
233
|
-
// Check max depth
|
|
234
|
-
if (options.maxDepth !== undefined && depth > options.maxDepth) {
|
|
235
|
-
return null;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Match lines like:
|
|
239
|
-
// - button "Submit"
|
|
240
|
-
// - heading "Title" [level=1]
|
|
241
|
-
// - link "Click me":
|
|
242
|
-
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
243
|
-
|
|
244
|
-
if (!match) {
|
|
245
|
-
// Metadata lines (like /url:) or text content
|
|
246
|
-
if (options.interactive) {
|
|
247
|
-
// In interactive mode, only keep metadata under interactive elements
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
return line;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const [, prefix, role, name, suffix] = match;
|
|
254
|
-
const roleLower = role.toLowerCase();
|
|
255
|
-
|
|
256
|
-
// Skip metadata lines (like /url:)
|
|
257
|
-
if (role.startsWith('/')) {
|
|
258
|
-
return line;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const isInteractive = INTERACTIVE_ROLES.has(roleLower);
|
|
262
|
-
const isContent = CONTENT_ROLES.has(roleLower);
|
|
263
|
-
const isStructural = STRUCTURAL_ROLES.has(roleLower);
|
|
264
|
-
|
|
265
|
-
// In interactive-only mode, filter non-interactive elements
|
|
266
|
-
if (options.interactive && !isInteractive) {
|
|
267
|
-
return null;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// In compact mode, skip unnamed structural elements
|
|
271
|
-
if (options.compact && isStructural && !name) {
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Add ref for interactive or named content elements
|
|
276
|
-
const shouldHaveRef = isInteractive || (isContent && name);
|
|
277
|
-
|
|
278
|
-
if (shouldHaveRef) {
|
|
279
|
-
const ref = nextRef();
|
|
280
|
-
|
|
281
|
-
refs[ref] = {
|
|
282
|
-
selector: buildSelector(roleLower, name),
|
|
283
|
-
role: roleLower,
|
|
284
|
-
name,
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
// Build enhanced line with ref
|
|
288
|
-
let enhanced = `${prefix}${role}`;
|
|
289
|
-
if (name) enhanced += ` "${name}"`;
|
|
290
|
-
enhanced += ` [ref=${ref}]`;
|
|
291
|
-
if (suffix) enhanced += suffix;
|
|
292
|
-
|
|
293
|
-
return enhanced;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return line;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Remove empty structural branches in compact mode
|
|
301
|
-
*/
|
|
302
|
-
function compactTree(tree: string): string {
|
|
303
|
-
const lines = tree.split('\n');
|
|
304
|
-
const result: string[] = [];
|
|
305
|
-
|
|
306
|
-
// Simple pass: keep lines that have content or refs
|
|
307
|
-
for (let i = 0; i < lines.length; i++) {
|
|
308
|
-
const line = lines[i];
|
|
309
|
-
|
|
310
|
-
// Always keep lines with refs
|
|
311
|
-
if (line.includes('[ref=')) {
|
|
312
|
-
result.push(line);
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Keep lines with text content (after :)
|
|
317
|
-
if (line.includes(':') && !line.endsWith(':')) {
|
|
318
|
-
result.push(line);
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Check if this structural element has children with refs
|
|
323
|
-
const currentIndent = getIndentLevel(line);
|
|
324
|
-
let hasRelevantChildren = false;
|
|
325
|
-
|
|
326
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
327
|
-
const childIndent = getIndentLevel(lines[j]);
|
|
328
|
-
if (childIndent <= currentIndent) break;
|
|
329
|
-
if (lines[j].includes('[ref=')) {
|
|
330
|
-
hasRelevantChildren = true;
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (hasRelevantChildren) {
|
|
336
|
-
result.push(line);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return result.join('\n');
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Parse a ref from command argument (e.g., "@e1" -> "e1")
|
|
345
|
-
*/
|
|
346
|
-
export function parseRef(arg: string): string | null {
|
|
347
|
-
if (arg.startsWith('@')) {
|
|
348
|
-
return arg.slice(1);
|
|
349
|
-
}
|
|
350
|
-
if (arg.startsWith('ref=')) {
|
|
351
|
-
return arg.slice(4);
|
|
352
|
-
}
|
|
353
|
-
if (/^e\d+$/.test(arg)) {
|
|
354
|
-
return arg;
|
|
355
|
-
}
|
|
356
|
-
return null;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/**
|
|
360
|
-
* Get snapshot statistics
|
|
361
|
-
*/
|
|
362
|
-
export function getSnapshotStats(tree: string, refs: RefMap): {
|
|
363
|
-
lines: number;
|
|
364
|
-
chars: number;
|
|
365
|
-
tokens: number;
|
|
366
|
-
refs: number;
|
|
367
|
-
interactive: number;
|
|
368
|
-
} {
|
|
369
|
-
const interactive = Object.values(refs).filter(r =>
|
|
370
|
-
INTERACTIVE_ROLES.has(r.role)
|
|
371
|
-
).length;
|
|
372
|
-
|
|
373
|
-
return {
|
|
374
|
-
lines: tree.split('\n').length,
|
|
375
|
-
chars: tree.length,
|
|
376
|
-
tokens: Math.ceil(tree.length / 4),
|
|
377
|
-
refs: Object.keys(refs).length,
|
|
378
|
-
interactive,
|
|
379
|
-
};
|
|
380
|
-
}
|