dsh-browser-playwright-codex 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +17 -7
  3. package/SECURITY.md +15 -10
  4. package/lib/browser-lifecycle.d.ts +12 -0
  5. package/lib/browser-lifecycle.js +186 -0
  6. package/lib/challenge.d.ts +20 -0
  7. package/lib/challenge.js +40 -0
  8. package/lib/config.d.ts +81 -0
  9. package/lib/config.js +61 -0
  10. package/lib/extract.d.ts +13 -0
  11. package/lib/extract.js +34 -0
  12. package/lib/index.d.ts +4 -2
  13. package/lib/index.js +3 -1
  14. package/lib/login-state.d.ts +10 -0
  15. package/lib/login-state.js +83 -0
  16. package/lib/page-actions.d.ts +134 -0
  17. package/lib/page-actions.js +725 -0
  18. package/lib/page-diagnostics.d.ts +65 -0
  19. package/lib/page-diagnostics.js +181 -0
  20. package/lib/page-probes.d.ts +78 -0
  21. package/lib/page-probes.js +281 -0
  22. package/lib/page-snapshot.d.ts +96 -0
  23. package/lib/page-snapshot.js +280 -0
  24. package/lib/playwright.d.ts +5 -644
  25. package/lib/playwright.js +28 -2602
  26. package/lib/provider.d.ts +157 -0
  27. package/lib/provider.js +352 -0
  28. package/lib/session.d.ts +410 -0
  29. package/lib/session.js +633 -0
  30. package/lib/tool-render.d.ts +20 -0
  31. package/lib/tool-render.js +137 -0
  32. package/lib/tool-schemas.d.ts +76 -0
  33. package/lib/tool-schemas.js +46 -0
  34. package/lib/tool-types.d.ts +82 -0
  35. package/lib/tool-types.js +1 -0
  36. package/lib/tool.d.ts +2 -26
  37. package/lib/tool.js +5 -252
  38. package/lib/url-policy.d.ts +2 -0
  39. package/lib/url-policy.js +26 -0
  40. package/lib/window-focus.d.ts +27 -0
  41. package/lib/window-focus.js +54 -0
  42. package/package.json +1 -1
  43. package/scripts/dsh-browser-verify.mjs +2 -1
  44. package/scripts/prompt-cost.mjs +1 -1
@@ -1,655 +1,16 @@
1
- /**
2
- * Playwright provider for the browser capability: owns the browser binary,
3
- * one context per owner key, idle disposal, and the snapshot engine.
4
- * @module dsh-browser-playwright-codex/playwright
5
- */
1
+ import { type PlaywrightConfig } from './config.ts';
6
2
  import type { Context } from '@deepseek-ai/cordis';
7
- import { type Browser, type BrowserContext, type CDPSession, type Dialog, type Locator, type Page, type Request } from 'playwright-core';
8
- import z from '@deepseek-ai/schemastery';
9
- import type { BrowserNode, BrowserSnapshot, DiagnosticsEntry, LoadState } from './types.ts';
10
3
  /** Cordis plugin name used by loader diagnostics. */
11
4
  export declare const name = "browser-playwright";
12
5
  /** The browser runtime this provider registers into. */
13
6
  export declare const inject: string[];
14
- /** Schemastery validation for {@link PlaywrightConfig}. */
15
- /** Launch configuration for the Playwright provider. */
16
- export interface PlaywrightConfig {
17
- launch: {
18
- /** Absolute path to a Chromium-family binary; takes precedence over channel. */
19
- executablePath?: string;
20
- /** Browser channel: chromium, chrome, msedge. Omitted = auto-detect in that order. */
21
- channel?: string;
22
- /** Run headful so the user can watch and rescue the browser by hand. */
23
- headless: boolean;
24
- /** Persistent mode: one shared profile-backed window whose login survives. */
25
- persistent: boolean;
26
- /** Profile directory for persistent mode; empty means ~/.dsh/browser-profiles/playwright. */
27
- profileDir: string;
28
- viewport: {
29
- width: number;
30
- height: number;
31
- };
32
- /** Per-action navigation/click timeout in milliseconds. */
33
- navigationTimeoutMs: number;
34
- /** Ignore HTTPS certificate errors. */
35
- ignoreHTTPSErrors: boolean;
36
- };
37
- /** Host suffixes the browser may visit. Empty = any http(s) host. */
38
- allowedDomains?: string[];
39
- /** Close an idle session's browser context after this many milliseconds. 0 disables. */
40
- idleTimeoutMs: number;
41
- /** Maximum concurrent browser contexts; acquiring beyond it evicts the least recently used. */
42
- maxSessions: number;
43
- snapshot: {
44
- /** 'legacy' keeps the injected DOM walker; 'aria' uses the official ariaSnapshot(mode:'ai') engine. */
45
- engine: 'legacy' | 'aria';
46
- maxNodes: number;
47
- maxNameLength: number;
48
- maxTextLength: number;
49
- /** Incremental ref-diff mode; false = always full snapshots (zero behavioral change). */
50
- diff: boolean;
51
- };
52
- }
53
- /** Bounded page data the extraction consumer feeds to a model. */
54
- export interface PageData {
55
- readonly url: string;
56
- readonly title: string;
57
- readonly text: string;
58
- readonly truncated: boolean;
59
- readonly links: readonly {
60
- readonly text: string;
61
- readonly href: string;
62
- }[];
63
- readonly inputs: readonly {
64
- readonly tag: string;
65
- readonly type: string;
66
- readonly name: string;
67
- readonly value: string;
68
- readonly label: string;
69
- readonly checked: boolean | null;
70
- }[];
71
- }
72
- /** Schemastery validation for {@link PlaywrightConfig}. */
73
- export declare const Config: z<PlaywrightConfig>;
74
7
  /**
75
8
  * Register this provider on the browser runtime for the plugin's lifetime.
76
9
  * @param ctx - plugin context carrying the browser runtime.
77
10
  * @param config - launch and fleet configuration.
78
11
  */
12
+ export { Config, type PageData, type PlaywrightConfig } from './config.ts';
13
+ export { PlaywrightProvider } from './provider.ts';
14
+ export { assertAllowedUrl } from './url-policy.ts';
15
+ export { resolveSnapshotEngine } from './page-snapshot.ts';
79
16
  export declare function apply(ctx: Context, config: PlaywrightConfig): void;
80
- /** Validate one absolute URL against the navigation policy. */
81
- export declare function assertAllowedUrl(raw: string, allowedDomains: readonly string[]): URL;
82
- /**
83
- * Which page-reading mode a capture uses. The user's choice on the settings page
84
- * (Settings → 浏览器 → 页面识别方式) wins over the deployment default, so flipping
85
- * that switch changes the very next operation — no restart, no config edit.
86
- * @param configured - the mode from this provider's config.
87
- * @returns the effective mode.
88
- */
89
- export declare function resolveSnapshotEngine(configured: 'legacy' | 'aria'): 'legacy' | 'aria';
90
- /**
91
- * Playwright-backed {@link BrowserProvider}: one shared browser, one context
92
- * per owner key, LRU eviction, idle disposal, ref-based snapshot interaction.
93
- */
94
- /** One owner's live session handle inside the provider's map. */
95
- interface OwnerEntry {
96
- context: BrowserContext;
97
- session: PlaywrightSession;
98
- lastUsed: number;
99
- /** Operations in flight; idle disposal defers while this is non-zero. */
100
- pending: number;
101
- timer: NodeJS.Timeout | undefined;
102
- }
103
- /** One action's outcome: either a native dialog blocked it, or it produced a value. */
104
- interface ActionResult {
105
- blocked: boolean;
106
- pending?: PendingDialogRecord | undefined;
107
- value?: unknown;
108
- }
109
- /** A native dialog parked on one page, waiting for an explicit answer. */
110
- interface PendingDialogRecord {
111
- dialog: Dialog;
112
- type: string;
113
- message: string;
114
- owner: string | undefined;
115
- }
116
- export declare class PlaywrightProvider {
117
- config: PlaywrightConfig;
118
- id: string;
119
- /** Legacy (persistent:false) shared browser instance. */
120
- browser: Browser | undefined;
121
- /** Persistent-mode shared profile-backed context (the personal window). */
122
- context: BrowserContext | undefined;
123
- entries: Map<string, OwnerEntry>;
124
- launchError: unknown;
125
- /** Resolved profile directory (persistent mode only). */
126
- profileDir: string;
127
- /**
128
- * Where cookies+localStorage are exported for the session-cookie fallback.
129
- * Undefined outside persistent mode, and that is load-bearing: every writer
130
- * and reader below guards on `undefined`, while an empty string would make
131
- * `'' + '.tmp'` land in the process working directory.
132
- */
133
- stateFile: string | undefined;
134
- /** Owner that raised the dialog currently parked on a page. */
135
- lastActor: string | undefined;
136
- lastStatePersist: number;
137
- constructor(config: PlaywrightConfig);
138
- available(): boolean;
139
- /** Pages already carrying the dialog guard (WeakSet: pages stay collectable). */
140
- armedDialogs: WeakSet<WeakKey>;
141
- /** Pending native dialog per page: { dialog, type, message }. Never auto-resolved. */
142
- pendingDialogs: Map<Page, PendingDialogRecord>;
143
- /** Resolvers waiting for the next dialog on a page (the action/dialog race). */
144
- dialogWaits: Map<any, any>;
145
- /**
146
- * Make a page report native dialogs instead of having them silently
147
- * dismissed. The dialog is parked as a per-page pending state and left
148
- * untouched: the page stays blocked until browser_dialog answers it, and
149
- * whichever action raised it returns early through the dialog race.
150
- * See DIALOG-POLICY.md for why this shape beats any parameter-level check.
151
- */
152
- armDialogGuard(page: Page): void;
153
- /** The dialog currently blocking this page, or undefined. */
154
- pendingDialogFor(page: Page): PendingDialogRecord | undefined;
155
- /**
156
- * Remove and return the pending dialog (atomic: one dialog, one answer).
157
- * Only the owner that raised it may take it: in persistent mode several
158
- * sessions share one page, and answering another session's dialog would
159
- * execute its action on their behalf.
160
- */
161
- takePendingDialog(page: Page, owner: string): PendingDialogRecord | undefined;
162
- /**
163
- * Wake when a dialog appears on this page. cancel() unregisters the waiter
164
- * so a finished action leaves nothing behind.
165
- */
166
- armDialogWait(page: Page): {
167
- promise: Promise<true>;
168
- cancel: () => void;
169
- };
170
- /** One line describing a parked dialog for the tool result. */
171
- dialogNoteFor(pending: PendingDialogRecord): string;
172
- acquire(owner: string): Promise<PlaywrightSession>;
173
- disposeOwner(owner: string): Promise<void>;
174
- dispose(): Promise<void>;
175
- /** Touch the entry's last-used clock and re-arm its idle timer. */
176
- touch(owner: string): void;
177
- /** Mark one operation in flight so the idle timer cannot kill it mid-way. */
178
- beginOp(owner: string): void;
179
- /** Settle one operation and restart the idle countdown. */
180
- endOp(owner: string): void;
181
- /** Ensure the session entry for an owner still exists. */
182
- isLive(owner: string): boolean;
183
- /** Drop an owner's entry after a page-level crash without closing the window. */
184
- invalidateOwner(owner: string): void;
185
- armIdle(entry: OwnerEntry): void;
186
- /**
187
- * Ensure the shared persistent context is alive (persistent mode). A
188
- * profile-backed window is launched once and reused until closed by the
189
- * last owner, idle disposal, or an external close; login state lives in
190
- * the profile plus the exported state file, so reopening is seamless.
191
- */
192
- ensureContext(): Promise<BrowserContext>;
193
- /** Remove stale Chrome singleton lock files inside the profile directory. */
194
- clearChromeLocks(): Promise<void>;
195
- /**
196
- * Collect the login-state fallback WITHOUT ever opening a page while the
197
- * window is put away.
198
- *
199
- * `context.storageState()` is not a pure read. For every origin this context
200
- * has visited whose page is since gone, it opens a TEMPORARY PAGE, navigates
201
- * it to that origin, reads its storage, and closes it again
202
- * (playwright-core 1.62.1 `coreBundle.js:51663-51683`). The origins set is
203
- * accumulated by `addVisitedOrigin` on every navigation
204
- * (`coreBundle.js:22352` → `51634`) and only reset by `setStorageState`, so a
205
- * long-lived context always has leftovers. That temporary page is created
206
- * through `Target.createTarget` without `background: true`
207
- * (`coreBundle.js:38340-38342`) — and Chromium activates the window on every
208
- * tab creation. Same root as Cause 2 in FOCUS-STEALING.md, different trigger:
209
- * a bookkeeping step that ran after every call could drag a minimized window
210
- * back onto the screen.
211
- *
212
- * Cookies alone never open a page (`coreBundle.js:51639`), so when the window
213
- * is minimized — or its state cannot be read at all — export cookies only.
214
- * Nothing durable is lost: this file's job is the session-cookie fallback, and
215
- * localStorage already survives in the persistent profile. When the window is
216
- * positively visible we take the full `storageState()`, because a temporary
217
- * page cannot disturb someone who is already looking at the window.
218
- */
219
- collectLoginState(context: BrowserContext): Promise<{
220
- cookies: unknown[];
221
- origins: unknown[];
222
- }>;
223
- /** Export the login-state fallback to the state file (session-cookie fallback). */
224
- persistState(context: BrowserContext): Promise<void>;
225
- /**
226
- * Throttled, fire-and-forget variant called after every operation.
227
- *
228
- * The "is the window minimized?" question moved inside `collectLoginState`,
229
- * which answers it by choosing a safe API rather than by giving up: the export
230
- * now runs in either window state, so the session-cookie fallback stops having
231
- * a hole exactly where it matters most (a user who minimized the window is the
232
- * one most likely to close it by hand).
233
- */
234
- persistStateSoon(context: BrowserContext): void;
235
- /**
236
- * Re-inject a previously exported state. Durable cookies already live in
237
- * the profile; this covers session cookies the browser drops on close and
238
- * localStorage entries, so logins survive window close/reopen.
239
- */
240
- restoreState(context: BrowserContext): Promise<void>;
241
- /** Launch the browser once, probing the configured or auto-detected channel. */
242
- ensureBrowser(): Promise<Browser>;
243
- }
244
- /** Feature fingerprint of one ref-bearing node, for cross-snapshot diffing. */
245
- interface RefSignature {
246
- role?: string;
247
- name?: string;
248
- level?: number;
249
- checked?: boolean;
250
- selected?: boolean;
251
- disabled?: boolean;
252
- href?: string;
253
- parentRef?: string;
254
- }
255
- /** Live session over one browser context owned by one caller. */
256
- declare class PlaywrightSession {
257
- provider: PlaywrightProvider;
258
- owner: string;
259
- context: BrowserContext;
260
- currentIndex: number;
261
- closed: boolean;
262
- /** Set while answering a dialog or reporting it: page access must not be refused then. */
263
- bypassDialogGuard: boolean;
264
- /** Reused CDP session for the current page (DOM nodeIds are session-bound). */
265
- cdpSession: CDPSession | undefined;
266
- /** The page the cached CDP session is attached to. */
267
- cdpPage: Page | undefined;
268
- /** Pages already wired for console/network capture (WeakSet: pages stay collectable). */
269
- trackedPages: WeakSet<Page>;
270
- /** Stable id per page, so diagnostics can be reported for one page only. */
271
- pageIds: WeakMap<Page, number>;
272
- /** Ids handed out so far (monotonic within the session). */
273
- pageIdSeq: number;
274
- /** Bounded console + pageerror ring, shared by every tracked page of this session. */
275
- consoleLog: DiagnosticsEntry[];
276
- /** Bounded network ring, shared by every tracked page of this session. */
277
- networkLog: DiagnosticsEntry[];
278
- /** Monotonic sequence handed to each captured entry. */
279
- diagSeq: number;
280
- /** Evicted entries, so truncation is visible instead of silent. */
281
- diagDropped: {
282
- console: number;
283
- network: number;
284
- };
285
- /** In-flight requests keyed by request object, for status and duration attribution. */
286
- requestEntries: WeakMap<Request, {
287
- entry: DiagnosticsEntry;
288
- startedAt: number;
289
- }>;
290
- /** Ring capacity per stream. */
291
- diagLimit: number;
292
- /** ref -> feature signature of the previous capture (diff baseline). */
293
- lastRefSignatures: Map<string, RefSignature> | undefined;
294
- /** Nonce of the baseline ref space; a change means navigation reset. */
295
- lastRefNonce: string | undefined;
296
- constructor(provider: PlaywrightProvider, owner: string, context: BrowserContext);
297
- /** Run one operation under busy tracking: idle disposal defers until it settles. */
298
- run<T>(operation: () => Promise<T>): Promise<T>;
299
- /** Fast actionability pre-check (visible/enabled/editable) before an action. */
300
- assertActionable(locator: Locator, options?: {
301
- editable?: boolean;
302
- }): Promise<void>;
303
- /** Wait for an in-flight navigation (started by a click) to settle. */
304
- settleNavigation(page: Page): Promise<void>;
305
- /** Page-level render stability, bounded — a busy page never stalls the agent. */
306
- waitForPageStable(page: Page, timeoutMs?: number): Promise<void>;
307
- /** Element-level stability before a pointer/keyboard action, bounded. Returns true when stable. */
308
- waitForElementStable(locator: Locator): Promise<boolean>;
309
- /** Post-action settle: wait out a real navigation, then render-stabilize before snapshotting. */
310
- settleAndSnapshot(page: Page, navChanged: boolean): Promise<BrowserSnapshot>;
311
- /**
312
- * Snapshot substitute while a native dialog blocks the page: only locally
313
- * cached data (page.url() needs no round trip) — page.evaluate() and
314
- * page.title() would both stall behind the dialog.
315
- */
316
- blockedSnapshot(page: Page, pending: PendingDialogRecord | undefined): {
317
- url: string;
318
- title: string;
319
- nodes: never[];
320
- totalRefs: number;
321
- truncated: boolean;
322
- dialogNote: string;
323
- };
324
- /**
325
- * Run one page action that may raise a native dialog. Such an action never
326
- * resolves while the dialog is up, so race it against the dialog appearing
327
- * and hand the caller the pending state instead (Playwright-MCP pattern).
328
- */
329
- runAction(page: Page, action: () => Promise<unknown>): Promise<ActionResult>;
330
- /**
331
- * Answer the pending native dialog and let the action blocked behind it
332
- * finish. This is the only place a dialog is ever accepted or dismissed.
333
- */
334
- resolveDialog(accept: boolean, promptText: string | undefined, signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
335
- /** Read the snapshot tree from one page (must run inside a run()). */
336
- capture(page: Page, opts: {
337
- interactiveOnly?: boolean;
338
- } | undefined): Promise<BrowserSnapshot>;
339
- /**
340
- * Semantic-tree capture via Playwright's official ariaSnapshot(mode:'ai').
341
- * An unavailable or unreadable aria tree (`missing`) degrades to the legacy
342
- * DOM walker instead of returning an empty snapshot: the model must never
343
- * receive "the page has no elements" because the engine failed to parse.
344
- */
345
- captureAria(page: Page, opts: {
346
- interactiveOnly?: boolean;
347
- } | undefined): Promise<{
348
- engineNote: string;
349
- url: string;
350
- title: string;
351
- nodes: BrowserNode[];
352
- totalRefs: number;
353
- truncated: boolean;
354
- } | {
355
- url: string;
356
- title: string;
357
- nodes: BrowserNode[];
358
- totalRefs: number;
359
- truncated: boolean;
360
- }>;
361
- /** Legacy capture: injected DOM walker (default engine, byte-identical output). */
362
- captureLegacy(page: Page, opts: {
363
- interactiveOnly?: boolean;
364
- } | undefined): Promise<{
365
- url: string;
366
- title: string;
367
- nodes: BrowserNode[];
368
- totalRefs: number;
369
- truncated: boolean;
370
- }>;
371
- /**
372
- * Attach an incremental diff when snapshot.diff is enabled. Absence of the
373
- * diff field means "full snapshot follows": first capture of the session,
374
- * navigation reset, or a truncated tree. The diff is keyed by the stable
375
- * data-dsh-ref, so legacy and aria engines share the same contract.
376
- */
377
- attachDiff(snap: BrowserSnapshot): BrowserSnapshot;
378
- /** Build ref -> feature signature map from a captured tree, with parent refs. */
379
- buildRefSignatures(nodes: readonly BrowserNode[]): Map<string, RefSignature>;
380
- /** The document nonce shared by this snapshot's refs, or undefined. */
381
- refNonce(nodes: readonly BrowserNode[]): string | undefined;
382
- navigate(url: string, waitUntil: LoadState, signal: AbortSignal | undefined): Promise<BrowserSnapshot | {
383
- url: string;
384
- title: string;
385
- nodes: never[];
386
- totalRefs: number;
387
- truncated: boolean;
388
- dialogNote: string;
389
- }>;
390
- snapshot(opts?: {
391
- interactiveOnly?: boolean;
392
- }): Promise<BrowserSnapshot | {
393
- url: string;
394
- title: string;
395
- nodes: never[];
396
- totalRefs: number;
397
- truncated: boolean;
398
- dialogNote: string;
399
- }>;
400
- /**
401
- * Codex-style click-point resolution. Tries a scroll-alignment ladder
402
- * (centre, then end, then start). For each alignment: scroll the element
403
- * into view with a native scrollIntoView, wait for its bounding rect to
404
- * stop moving, sample the rect centre in-page (sub-pixel, no screenshot
405
- * measurement), then hit-test that exact point with elementFromPoint — so
406
- * an overlaying element can never swallow the click silently. Returns the
407
- * first unobstructed {x, y}, or throws describing what intercepted it.
408
- */
409
- resolveClickPoint(locator: Locator, signal: AbortSignal | undefined): Promise<{
410
- x: number;
411
- y: number;
412
- hitDesc: string;
413
- hitSelf: boolean;
414
- }>;
415
- click(ref: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot | {
416
- url: string;
417
- title: string;
418
- nodes: never[];
419
- totalRefs: number;
420
- truncated: boolean;
421
- dialogNote: string;
422
- }>;
423
- /**
424
- * Click an element the snapshot gave no ref, addressed by its visible label
425
- * (e.g. a menu item revealed by browser_hover that is plain text). The
426
- * label must equal an element's own direct text; the deepest match wins,
427
- * and the landing note names what was actually clicked plus how many
428
- * elements shared the label.
429
- */
430
- clickText(text: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot | {
431
- url: string;
432
- title: string;
433
- nodes: never[];
434
- totalRefs: number;
435
- truncated: boolean;
436
- dialogNote: string;
437
- }>;
438
- /**
439
- * Enforce the navigation policy for the link a click is about to follow.
440
- * Click navigation does not run through navigate(), so this is the only place
441
- * the rule lives and every click path (ref, text, geometric ref, raw x/y)
442
- * calls it — a new path cannot quietly skip the allow-list.
443
- *
444
- * The policy is about which HOSTS this browser may visit, so it applies to
445
- * http(s) destinations. An in-page `javascript:` link runs the page's own
446
- * code without navigating anywhere and is allowed (refusing it made ordinary
447
- * JS links unclickable); every other scheme — file:, data:, mailto:, tel:,
448
- * custom ones — stays refused.
449
- */
450
- assertClickTargetAllowed(href: string | null): void;
451
- /**
452
- * Enforce the host policy on a tab before this session starts driving it. A
453
- * page can open a tab by itself (`target="_blank"`, `window.open`) and the
454
- * plugin never sees that navigation, so the policy is applied at the point of
455
- * use instead: a tab outside the allow-list stays open — the user may well be
456
- * using it — but this agent refuses to switch to it.
457
- *
458
- * http(s) is checked against allowedDomains; `about:blank` is allowed because
459
- * it is the plugin's own interim state and has no host; every other scheme
460
- * (file:, data:, chrome:, devtools:) is refused outright.
461
- */
462
- assertTabAllowed(page: Page): void;
463
- /**
464
- * Shared click core: enforce the URL policy for link targets, run the
465
- * Codex-style geometry ladder with the Playwright actionability fallback,
466
- * then report what actually received the click. The label names the target
467
- * in the landing note; shared is how many elements matched it (0 for refs).
468
- */
469
- clickLocator(locator: Locator, signal: AbortSignal | undefined, label: string, shared: number): Promise<BrowserSnapshot | {
470
- url: string;
471
- title: string;
472
- nodes: never[];
473
- totalRefs: number;
474
- truncated: boolean;
475
- dialogNote: string;
476
- }>;
477
- clickAt(x: number, y: number, signal: AbortSignal | undefined): Promise<BrowserSnapshot | {
478
- url: string;
479
- title: string;
480
- nodes: never[];
481
- totalRefs: number;
482
- truncated: boolean;
483
- dialogNote: string;
484
- }>;
485
- clickAtRef(ref: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot | {
486
- url: string;
487
- title: string;
488
- nodes: never[];
489
- totalRefs: number;
490
- truncated: boolean;
491
- dialogNote: string;
492
- }>;
493
- fill(ref: string, text: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
494
- /** Read the field back and decide whether the fill landed. */
495
- verifyFill(locator: Locator, tag: string, text: string): Promise<boolean>;
496
- press(ref: string, key: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot | {
497
- url: string;
498
- title: string;
499
- nodes: never[];
500
- totalRefs: number;
501
- truncated: boolean;
502
- dialogNote: string;
503
- }>;
504
- /**
505
- * Hover the referenced element: rest the pointer on it so hover-revealed
506
- * content appears (CSS :hover rules, JS mouseenter handlers, submenus,
507
- * tooltips, chart values, player controls). Hover never activates
508
- * anything: it only reveals. The ref count before and after the pointer
509
- * moves is the only cheap proof that something appeared, so the returned
510
- * snapshot carries a note comparing them.
511
- */
512
- hover(ref: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
513
- /**
514
- * Hover a trigger the snapshot gave no ref, addressed by its visible label
515
- * (e.g. a plain span that reveals a menu on mouseenter). The label must
516
- * equal an element's own direct text; the deepest match wins, and the
517
- * landing note names what was hovered plus how many elements shared it.
518
- */
519
- hoverText(text: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
520
- /**
521
- * Shared hover core: rest the pointer on a resolved locator, then report
522
- * what the actionable ref count did. The label names the target in the
523
- * landing note; shared is how many elements matched that label (0 when the
524
- * target came from a ref).
525
- */
526
- hoverLocator(locator: Locator, signal: AbortSignal | undefined, label: string, shared: number): Promise<BrowserSnapshot>;
527
- scroll(direction: 'up' | 'down', amount: number, ref: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
528
- back(signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
529
- forward(signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
530
- wait(ms: number, signal: AbortSignal | undefined): Promise<void>;
531
- tabs(): Promise<{
532
- index: number;
533
- url: string;
534
- title: string;
535
- active: boolean;
536
- }[]>;
537
- switchTab(index: number, signal: AbortSignal | undefined): Promise<BrowserSnapshot | {
538
- url: string;
539
- title: string;
540
- nodes: never[];
541
- totalRefs: number;
542
- truncated: boolean;
543
- dialogNote: string;
544
- }>;
545
- openTab(url: string, waitUntil: LoadState, signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
546
- closeTab(index: number, signal: AbortSignal | undefined): Promise<void>;
547
- /**
548
- * Screenshots get their own floor instead of reusing the navigation budget: a
549
- * full-page capture is sized by the page and encoded by the CPU, so a loaded machine
550
- * legitimately needs seconds (2026-10-05: the Windows runner blew through the 2500ms
551
- * navigation timeout while encoding a full-page PNG — a slow runner, not a defect).
552
- * The floor only bounds failures; a capture that is ready returns immediately.
553
- */
554
- screenshotTimeoutMs(): number;
555
- screenshot(opts: {
556
- fullPage?: boolean;
557
- ref?: string;
558
- } | undefined, signal: AbortSignal | undefined): Promise<{
559
- mime: "image/png";
560
- bytes: Buffer<ArrayBufferLike>;
561
- }>;
562
- pageData(): Promise<unknown>;
563
- evaluate(expression: string, signal: AbortSignal | undefined): Promise<unknown>;
564
- /**
565
- * Send one raw CDP command on the current page's session and return its
566
- * result. This is the "direct protocol" escape hatch behind every other
567
- * tool — the model can inspect geometry, dispatch trusted input events or
568
- * query the DOM the same way Chrome DevTools does. Policy (which methods
569
- * are allowed) lives in the tool layer via lib/cdp-policy.js; this method
570
- * itself performs no filtering so internal callers keep full control.
571
- */
572
- cdp(method: string, params: Record<string, unknown>, signal: AbortSignal | undefined): Promise<unknown>;
573
- close(): Promise<void>;
574
- timeoutMs(): number;
575
- ensurePage(): Promise<Page>;
576
- /**
577
- * Wire bounded console/network capture onto one page, exactly once. Capture is
578
- * best-effort observation for the model: it must never throw into a page that is
579
- * otherwise healthy, and it must never grow without a bound.
580
- */
581
- trackPage(page: Page): void;
582
- /** Stable id for one page of this session (used to scope diagnostics). */
583
- pageIdFor(page: Page): number;
584
- /** Id of the page this session is driving, or undefined when there is none. */
585
- currentPageId(): number | undefined;
586
- /**
587
- * Split diagnostics into "the page this session is driving" and "everything
588
- * else". The rings keep every page's entries so chronology and eviction stay
589
- * honest, but a reader that promises one tab must not serve another tab's
590
- * lines: that is how a model diagnoses an error that belongs to a page it is
591
- * not even looking at.
592
- */
593
- scopeToCurrentPage(entries: readonly DiagnosticsEntry[]): {
594
- kept: DiagnosticsEntry[];
595
- otherPages: number;
596
- };
597
- /** URL of the tab this session is driving right now, or null. */
598
- currentUrl(): string | null;
599
- /** Recent console messages and uncaught page errors, newest last. */
600
- consoleMessages(args: {
601
- level?: string;
602
- limit?: number;
603
- }): {
604
- url: string | null;
605
- level: string;
606
- total: number;
607
- returned: number;
608
- dropped: number;
609
- otherPages: number;
610
- note: string;
611
- entries: {
612
- n: number;
613
- level: string | undefined;
614
- text: string;
615
- source: string;
616
- }[];
617
- };
618
- /** Recent network requests, newest last, with status and duration where known. */
619
- networkRequests(args: {
620
- urlContains?: string;
621
- failedOnly?: boolean;
622
- limit?: number;
623
- }): {
624
- url: string | null;
625
- total: number;
626
- returned: number;
627
- dropped: number;
628
- otherPages: number;
629
- note: string;
630
- entries: {
631
- n: number;
632
- method: string | undefined;
633
- url: string;
634
- status: number | null | undefined;
635
- kind: string | undefined;
636
- ms: number | null | undefined;
637
- failure: string | null | undefined;
638
- }[];
639
- };
640
- /**
641
- * Refuse page access while a native dialog blocks the page: every call
642
- * would otherwise stall until the action timeout. Only browser_dialog and
643
- * the blocked-snapshot report are allowed through (DIALOG-POLICY.md).
644
- */
645
- guardBlockedDialog(page: Page): void;
646
- /** Refuse context-wide tab work while any page in it has a parked dialog. */
647
- guardBlockedContext(): void;
648
- /** Run one internal step with the dialog guard lifted. */
649
- withDialogGuardLifted<T>(fn: () => Promise<T>): Promise<T>;
650
- pageAt(index: number): Page | undefined;
651
- refLocator(ref: string): Promise<Locator>;
652
- currentPageSync(): Page;
653
- assertLive(): void;
654
- }
655
- export {};