dsh-browser-playwright-codex 0.5.1 → 0.5.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.
@@ -0,0 +1,410 @@
1
+ /**
2
+ * 一次浏览器会话:持有 page/context、处理弹窗、截图、页面数据、evaluate/CDP 等交互,并把
3
+ * 动作、快照、诊断三块转发给对应模块(page-actions / page-snapshot / page-diagnostics)。
4
+ * @module dsh-browser-playwright-codex/session
5
+ */
6
+ import type { BrowserContext, CDPSession, Locator, Page, Request } from 'playwright-core';
7
+ import type { PlaywrightProvider } from './provider.ts';
8
+ import type { BrowserNode, BrowserSnapshot, DiagnosticsEntry, LoadState } from './types.ts';
9
+ import type { RefSignature } from './page-snapshot.ts';
10
+ import type { ActionResult, PendingDialogRecord } from './provider.ts';
11
+ /** Live session over one browser context owned by one caller. */
12
+ export declare class PlaywrightSession {
13
+ provider: PlaywrightProvider;
14
+ owner: string;
15
+ context: BrowserContext;
16
+ currentIndex: number;
17
+ closed: boolean;
18
+ /** Set while answering a dialog or reporting it: page access must not be refused then. */
19
+ bypassDialogGuard: boolean;
20
+ /** Reused CDP session for the current page (DOM nodeIds are session-bound). */
21
+ cdpSession: CDPSession | undefined;
22
+ /** The page the cached CDP session is attached to. */
23
+ cdpPage: Page | undefined;
24
+ /** Pages already wired for console/network capture (WeakSet: pages stay collectable). */
25
+ trackedPages: WeakSet<Page>;
26
+ /** Stable id per page, so diagnostics can be reported for one page only. */
27
+ pageIds: WeakMap<Page, number>;
28
+ /** Ids handed out so far (monotonic within the session). */
29
+ pageIdSeq: number;
30
+ /** Bounded console + pageerror ring, shared by every tracked page of this session. */
31
+ consoleLog: DiagnosticsEntry[];
32
+ /** Bounded network ring, shared by every tracked page of this session. */
33
+ networkLog: DiagnosticsEntry[];
34
+ /** Monotonic sequence handed to each captured entry. */
35
+ diagSeq: number;
36
+ /** Evicted entries, so truncation is visible instead of silent. */
37
+ diagDropped: {
38
+ console: number;
39
+ network: number;
40
+ };
41
+ /** In-flight requests keyed by request object, for status and duration attribution. */
42
+ requestEntries: WeakMap<Request, {
43
+ entry: DiagnosticsEntry;
44
+ startedAt: number;
45
+ }>;
46
+ /** Ring capacity per stream. */
47
+ diagLimit: number;
48
+ /** ref -> feature signature of the previous capture (diff baseline). */
49
+ lastRefSignatures: Map<string, RefSignature> | undefined;
50
+ /** Nonce of the baseline ref space; a change means navigation reset. */
51
+ lastRefNonce: string | undefined;
52
+ constructor(provider: PlaywrightProvider, owner: string, context: BrowserContext);
53
+ /** Run one operation under busy tracking: idle disposal defers until it settles. */
54
+ run<T>(operation: () => Promise<T>): Promise<T>;
55
+ /** Fast actionability pre-check (visible/enabled/editable) before an action. */
56
+ assertActionable(locator: Locator, options?: {
57
+ editable?: boolean;
58
+ }): Promise<void>;
59
+ /** Wait for an in-flight navigation (started by a click) to settle. */
60
+ settleNavigation(page: Page): Promise<void>;
61
+ /** Page-level render stability, bounded — a busy page never stalls the agent. */
62
+ waitForPageStable(page: Page, timeoutMs?: number): Promise<void>;
63
+ /** Element-level stability before a pointer/keyboard action, bounded. Returns true when stable. */
64
+ waitForElementStable(locator: Locator): Promise<boolean>;
65
+ /** Post-action settle: wait out a real navigation, then render-stabilize before snapshotting. */
66
+ settleAndSnapshot(page: Page, navChanged: boolean): Promise<BrowserSnapshot>;
67
+ /**
68
+ * Snapshot substitute while a native dialog blocks the page: only locally
69
+ * cached data (page.url() needs no round trip) — page.evaluate() and
70
+ * page.title() would both stall behind the dialog.
71
+ */
72
+ blockedSnapshot(page: Page, pending: PendingDialogRecord | undefined): {
73
+ url: string;
74
+ title: string;
75
+ nodes: never[];
76
+ totalRefs: number;
77
+ truncated: boolean;
78
+ dialogNote: string;
79
+ };
80
+ /**
81
+ * Run one page action that may raise a native dialog. Such an action never
82
+ * resolves while the dialog is up, so race it against the dialog appearing
83
+ * and hand the caller the pending state instead (Playwright-MCP pattern).
84
+ */
85
+ runAction(page: Page, action: () => Promise<unknown>): Promise<ActionResult>;
86
+ /**
87
+ * Answer the pending native dialog and let the action blocked behind it
88
+ * finish. This is the only place a dialog is ever accepted or dismissed.
89
+ */
90
+ resolveDialog(accept: boolean, promptText: string | undefined, signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
91
+ /** Read the snapshot tree from one page (must run inside a run()). */
92
+ capture(page: Page, opts: {
93
+ interactiveOnly?: boolean;
94
+ } | undefined): Promise<BrowserSnapshot>;
95
+ /**
96
+ * Semantic-tree capture via Playwright's official ariaSnapshot(mode:'ai').
97
+ * An unavailable or unreadable aria tree (`missing`) degrades to the legacy
98
+ * DOM walker instead of returning an empty snapshot: the model must never
99
+ * receive "the page has no elements" because the engine failed to parse.
100
+ */
101
+ captureAria(page: Page, opts: {
102
+ interactiveOnly?: boolean;
103
+ } | undefined): Promise<{
104
+ engineNote: string;
105
+ url: string;
106
+ title: string;
107
+ nodes: BrowserNode[];
108
+ totalRefs: number;
109
+ truncated: boolean;
110
+ } | {
111
+ url: string;
112
+ title: string;
113
+ nodes: BrowserNode[];
114
+ totalRefs: number;
115
+ truncated: boolean;
116
+ }>;
117
+ /** Legacy capture: injected DOM walker (default engine, byte-identical output). */
118
+ captureLegacy(page: Page, opts: {
119
+ interactiveOnly?: boolean;
120
+ } | undefined): Promise<{
121
+ url: string;
122
+ title: string;
123
+ nodes: BrowserNode[];
124
+ totalRefs: number;
125
+ truncated: boolean;
126
+ }>;
127
+ /**
128
+ * Attach an incremental diff when snapshot.diff is enabled. Absence of the
129
+ * diff field means "full snapshot follows": first capture of the session,
130
+ * navigation reset, or a truncated tree. The diff is keyed by the stable
131
+ * data-dsh-ref, so legacy and aria engines share the same contract.
132
+ */
133
+ attachDiff(snap: BrowserSnapshot): BrowserSnapshot;
134
+ /** Build ref -> feature signature map from a captured tree, with parent refs. */
135
+ buildRefSignatures(nodes: readonly BrowserNode[]): Map<string, RefSignature>;
136
+ /** The document nonce shared by this snapshot's refs, or undefined. */
137
+ refNonce(nodes: readonly BrowserNode[]): string | undefined;
138
+ navigate(url: string, waitUntil: LoadState, signal: AbortSignal | undefined): Promise<BrowserSnapshot | {
139
+ url: string;
140
+ title: string;
141
+ nodes: never[];
142
+ totalRefs: number;
143
+ truncated: boolean;
144
+ dialogNote: string;
145
+ }>;
146
+ snapshot(opts?: {
147
+ interactiveOnly?: boolean;
148
+ }): Promise<BrowserSnapshot | {
149
+ url: string;
150
+ title: string;
151
+ nodes: never[];
152
+ totalRefs: number;
153
+ truncated: boolean;
154
+ dialogNote: string;
155
+ }>;
156
+ /**
157
+ * Codex-style click-point resolution. Tries a scroll-alignment ladder
158
+ * (centre, then end, then start). For each alignment: scroll the element
159
+ * into view with a native scrollIntoView, wait for its bounding rect to
160
+ * stop moving, sample the rect centre in-page (sub-pixel, no screenshot
161
+ * measurement), then hit-test that exact point with elementFromPoint — so
162
+ * an overlaying element can never swallow the click silently. Returns the
163
+ * first unobstructed {x, y}, or throws describing what intercepted it.
164
+ */
165
+ resolveClickPoint(locator: Locator, signal: AbortSignal | undefined): Promise<{
166
+ x: number;
167
+ y: number;
168
+ hitDesc: string;
169
+ hitSelf: boolean;
170
+ }>;
171
+ click(ref: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot | {
172
+ url: string;
173
+ title: string;
174
+ nodes: never[];
175
+ totalRefs: number;
176
+ truncated: boolean;
177
+ dialogNote: string;
178
+ }>;
179
+ /**
180
+ * Click an element the snapshot gave no ref, addressed by its visible label
181
+ * (e.g. a menu item revealed by browser_hover that is plain text). The
182
+ * label must equal an element's own direct text; the deepest match wins,
183
+ * and the landing note names what was actually clicked plus how many
184
+ * elements shared the label.
185
+ */
186
+ clickText(text: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot | {
187
+ url: string;
188
+ title: string;
189
+ nodes: never[];
190
+ totalRefs: number;
191
+ truncated: boolean;
192
+ dialogNote: string;
193
+ }>;
194
+ /**
195
+ * Enforce the navigation policy for the link a click is about to follow.
196
+ * Click navigation does not run through navigate(), so this is the only place
197
+ * the rule lives and every click path (ref, text, geometric ref, raw x/y)
198
+ * calls it — a new path cannot quietly skip the allow-list.
199
+ *
200
+ * The policy is about which HOSTS this browser may visit, so it applies to
201
+ * http(s) destinations. An in-page `javascript:` link runs the page's own
202
+ * code without navigating anywhere and is allowed (refusing it made ordinary
203
+ * JS links unclickable); every other scheme — file:, data:, mailto:, tel:,
204
+ * custom ones — stays refused.
205
+ */
206
+ assertClickTargetAllowed(href: string | null): void;
207
+ /**
208
+ * Enforce the host policy on a tab before this session starts driving it. A
209
+ * page can open a tab by itself (`target="_blank"`, `window.open`) and the
210
+ * plugin never sees that navigation, so the policy is applied at the point of
211
+ * use instead: a tab outside the allow-list stays open — the user may well be
212
+ * using it — but this agent refuses to switch to it.
213
+ *
214
+ * http(s) is checked against allowedDomains; `about:blank` is allowed because
215
+ * it is the plugin's own interim state and has no host; every other scheme
216
+ * (file:, data:, chrome:, devtools:) is refused outright.
217
+ */
218
+ assertTabAllowed(page: Page): void;
219
+ /**
220
+ * Shared click core: enforce the URL policy for link targets, run the
221
+ * Codex-style geometry ladder with the Playwright actionability fallback,
222
+ * then report what actually received the click. The label names the target
223
+ * in the landing note; shared is how many elements matched it (0 for refs).
224
+ */
225
+ clickLocator(locator: Locator, signal: AbortSignal | undefined, label: string, shared: number): Promise<BrowserSnapshot | {
226
+ url: string;
227
+ title: string;
228
+ nodes: never[];
229
+ totalRefs: number;
230
+ truncated: boolean;
231
+ dialogNote: string;
232
+ }>;
233
+ clickAt(x: number, y: number, signal: AbortSignal | undefined): Promise<BrowserSnapshot | {
234
+ url: string;
235
+ title: string;
236
+ nodes: never[];
237
+ totalRefs: number;
238
+ truncated: boolean;
239
+ dialogNote: string;
240
+ }>;
241
+ clickAtRef(ref: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot | {
242
+ url: string;
243
+ title: string;
244
+ nodes: never[];
245
+ totalRefs: number;
246
+ truncated: boolean;
247
+ dialogNote: string;
248
+ }>;
249
+ fill(ref: string, text: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
250
+ /** Read the field back and decide whether the fill landed. */
251
+ verifyFill(locator: Locator, tag: string, text: string): Promise<boolean>;
252
+ press(ref: string, key: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot | {
253
+ url: string;
254
+ title: string;
255
+ nodes: never[];
256
+ totalRefs: number;
257
+ truncated: boolean;
258
+ dialogNote: string;
259
+ }>;
260
+ /**
261
+ * Hover the referenced element: rest the pointer on it so hover-revealed
262
+ * content appears (CSS :hover rules, JS mouseenter handlers, submenus,
263
+ * tooltips, chart values, player controls). Hover never activates
264
+ * anything: it only reveals. The ref count before and after the pointer
265
+ * moves is the only cheap proof that something appeared, so the returned
266
+ * snapshot carries a note comparing them.
267
+ */
268
+ hover(ref: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
269
+ /**
270
+ * Hover a trigger the snapshot gave no ref, addressed by its visible label
271
+ * (e.g. a plain span that reveals a menu on mouseenter). The label must
272
+ * equal an element's own direct text; the deepest match wins, and the
273
+ * landing note names what was hovered plus how many elements shared it.
274
+ */
275
+ hoverText(text: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
276
+ /**
277
+ * Shared hover core: rest the pointer on a resolved locator, then report
278
+ * what the actionable ref count did. The label names the target in the
279
+ * landing note; shared is how many elements matched that label (0 when the
280
+ * target came from a ref).
281
+ */
282
+ hoverLocator(locator: Locator, signal: AbortSignal | undefined, label: string, shared: number): Promise<BrowserSnapshot>;
283
+ scroll(direction: 'up' | 'down', amount: number, ref: string, signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
284
+ back(signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
285
+ forward(signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
286
+ wait(ms: number, signal: AbortSignal | undefined): Promise<void>;
287
+ tabs(): Promise<{
288
+ index: number;
289
+ url: string;
290
+ title: string;
291
+ active: boolean;
292
+ }[]>;
293
+ switchTab(index: number, signal: AbortSignal | undefined): Promise<BrowserSnapshot | {
294
+ url: string;
295
+ title: string;
296
+ nodes: never[];
297
+ totalRefs: number;
298
+ truncated: boolean;
299
+ dialogNote: string;
300
+ }>;
301
+ openTab(url: string, waitUntil: LoadState, signal: AbortSignal | undefined): Promise<BrowserSnapshot>;
302
+ closeTab(index: number, signal: AbortSignal | undefined): Promise<void>;
303
+ /**
304
+ * Screenshots get their own floor instead of reusing the navigation budget: a
305
+ * full-page capture is sized by the page and encoded by the CPU, so a loaded machine
306
+ * legitimately needs seconds (2026-10-05: the Windows runner blew through the then-2500ms
307
+ * navigation-timeout budget while encoding a full-page PNG — a slow runner, not a defect).
308
+ * The floor only bounds failures; a capture that is ready returns immediately.
309
+ */
310
+ screenshotTimeoutMs(): number;
311
+ screenshot(opts: {
312
+ fullPage?: boolean;
313
+ ref?: string;
314
+ } | undefined, signal: AbortSignal | undefined): Promise<{
315
+ mime: "image/png";
316
+ bytes: Buffer<ArrayBufferLike>;
317
+ }>;
318
+ pageData(): Promise<unknown>;
319
+ evaluate(expression: string, signal: AbortSignal | undefined): Promise<unknown>;
320
+ /**
321
+ * Send one raw CDP command on the current page's session and return its
322
+ * result. This is the "direct protocol" escape hatch behind every other
323
+ * tool — the model can inspect geometry, dispatch trusted input events or
324
+ * query the DOM the same way Chrome DevTools does. Policy (which methods
325
+ * are allowed) lives in the tool layer via lib/cdp-policy.js; this method
326
+ * itself performs no filtering so internal callers keep full control.
327
+ */
328
+ cdp(method: string, params: Record<string, unknown>, signal: AbortSignal | undefined): Promise<unknown>;
329
+ close(): Promise<void>;
330
+ timeoutMs(): number;
331
+ ensurePage(): Promise<Page>;
332
+ /**
333
+ * Wire bounded console/network capture onto one page, exactly once. Capture is
334
+ * best-effort observation for the model: it must never throw into a page that is
335
+ * otherwise healthy, and it must never grow without a bound.
336
+ */
337
+ trackPage(page: Page): void;
338
+ /** Stable id for one page of this session (used to scope diagnostics). */
339
+ pageIdFor(page: Page): number;
340
+ /** Id of the page this session is driving, or undefined when there is none. */
341
+ currentPageId(): number | undefined;
342
+ /**
343
+ * Split diagnostics into "the page this session is driving" and "everything
344
+ * else". The rings keep every page's entries so chronology and eviction stay
345
+ * honest, but a reader that promises one tab must not serve another tab's
346
+ * lines: that is how a model diagnoses an error that belongs to a page it is
347
+ * not even looking at.
348
+ */
349
+ scopeToCurrentPage(entries: readonly DiagnosticsEntry[]): {
350
+ kept: DiagnosticsEntry[];
351
+ otherPages: number;
352
+ };
353
+ /** URL of the tab this session is driving right now, or null. */
354
+ currentUrl(): string | null;
355
+ /** Recent console messages and uncaught page errors, newest last. */
356
+ consoleMessages(args: {
357
+ level?: string;
358
+ limit?: number;
359
+ }): {
360
+ url: string | null;
361
+ level: string;
362
+ total: number;
363
+ returned: number;
364
+ dropped: number;
365
+ otherPages: number;
366
+ note: string;
367
+ entries: {
368
+ n: number;
369
+ level: string | undefined;
370
+ text: string;
371
+ source: string;
372
+ }[];
373
+ };
374
+ /** Recent network requests, newest last, with status and duration where known. */
375
+ networkRequests(args: {
376
+ urlContains?: string;
377
+ failedOnly?: boolean;
378
+ limit?: number;
379
+ }): {
380
+ url: string | null;
381
+ total: number;
382
+ returned: number;
383
+ dropped: number;
384
+ otherPages: number;
385
+ note: string;
386
+ entries: {
387
+ n: number;
388
+ method: string | undefined;
389
+ url: string;
390
+ status: number | null | undefined;
391
+ kind: string | undefined;
392
+ ms: number | null | undefined;
393
+ failure: string | null | undefined;
394
+ }[];
395
+ };
396
+ /**
397
+ * Refuse page access while a native dialog blocks the page: every call
398
+ * would otherwise stall until the action timeout. Only browser_dialog and
399
+ * the blocked-snapshot report are allowed through (DIALOG-POLICY.md).
400
+ */
401
+ guardBlockedDialog(page: Page): void;
402
+ /** Refuse context-wide tab work while any page in it has a parked dialog. */
403
+ guardBlockedContext(): void;
404
+ /** Run one internal step with the dialog guard lifted. */
405
+ withDialogGuardLifted<T>(fn: () => Promise<T>): Promise<T>;
406
+ pageAt(index: number): Page | undefined;
407
+ refLocator(ref: string): Promise<Locator>;
408
+ currentPageSync(): Page;
409
+ assertLive(): void;
410
+ }