@agenticmail/enterprise 0.5.430 → 0.5.432

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,2325 @@
1
+ import {
2
+ appendCdpPath,
3
+ assertBrowserNavigationAllowed,
4
+ fetchJson,
5
+ formatAriaSnapshot,
6
+ getChromeWebSocketUrl,
7
+ getHeadersWithAuth,
8
+ normalizeCdpWsUrl,
9
+ withBrowserNavigationPolicy,
10
+ withCdpSocket
11
+ } from "./chunk-MCZNCJQ2.js";
12
+ import {
13
+ formatCliCommand,
14
+ formatErrorMessage,
15
+ resolvePreferredAgenticMailTmpDir
16
+ } from "./chunk-A3PUJDNH.js";
17
+ import "./chunk-KFQGP6VL.js";
18
+
19
+ // src/browser/pw-ai-state.ts
20
+ var pwAiLoaded = false;
21
+ function markPwAiLoaded() {
22
+ pwAiLoaded = true;
23
+ }
24
+
25
+ // src/browser/pw-session.ts
26
+ import { chromium } from "playwright-core";
27
+ var pageStates = /* @__PURE__ */ new WeakMap();
28
+ var contextStates = /* @__PURE__ */ new WeakMap();
29
+ var observedContexts = /* @__PURE__ */ new WeakSet();
30
+ var observedPages = /* @__PURE__ */ new WeakSet();
31
+ var roleRefsByTarget = /* @__PURE__ */ new Map();
32
+ var MAX_ROLE_REFS_CACHE = 50;
33
+ var MAX_CONSOLE_MESSAGES = 500;
34
+ var MAX_PAGE_ERRORS = 200;
35
+ var MAX_NETWORK_REQUESTS = 500;
36
+ var cached = null;
37
+ var connecting = null;
38
+ function normalizeCdpUrl(raw) {
39
+ return raw.replace(/\/$/, "");
40
+ }
41
+ function findNetworkRequestById(state, id) {
42
+ for (let i = state.requests.length - 1; i >= 0; i -= 1) {
43
+ const candidate = state.requests[i];
44
+ if (candidate && candidate.id === id) {
45
+ return candidate;
46
+ }
47
+ }
48
+ return void 0;
49
+ }
50
+ function roleRefsKey(cdpUrl, targetId) {
51
+ return `${normalizeCdpUrl(cdpUrl)}::${targetId}`;
52
+ }
53
+ function rememberRoleRefsForTarget(opts) {
54
+ const targetId = opts.targetId.trim();
55
+ if (!targetId) {
56
+ return;
57
+ }
58
+ roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
59
+ refs: opts.refs,
60
+ ...opts.frameSelector ? { frameSelector: opts.frameSelector } : {},
61
+ ...opts.mode ? { mode: opts.mode } : {}
62
+ });
63
+ while (roleRefsByTarget.size > MAX_ROLE_REFS_CACHE) {
64
+ const first = roleRefsByTarget.keys().next();
65
+ if (first.done) {
66
+ break;
67
+ }
68
+ roleRefsByTarget.delete(first.value);
69
+ }
70
+ }
71
+ function storeRoleRefsForTarget(opts) {
72
+ const state = ensurePageState(opts.page);
73
+ state.roleRefs = opts.refs;
74
+ state.roleRefsFrameSelector = opts.frameSelector;
75
+ state.roleRefsMode = opts.mode;
76
+ if (!opts.targetId?.trim()) {
77
+ return;
78
+ }
79
+ rememberRoleRefsForTarget({
80
+ cdpUrl: opts.cdpUrl,
81
+ targetId: opts.targetId,
82
+ refs: opts.refs,
83
+ frameSelector: opts.frameSelector,
84
+ mode: opts.mode
85
+ });
86
+ }
87
+ function restoreRoleRefsForTarget(opts) {
88
+ const targetId = opts.targetId?.trim() || "";
89
+ if (!targetId) {
90
+ return;
91
+ }
92
+ const cached2 = roleRefsByTarget.get(roleRefsKey(opts.cdpUrl, targetId));
93
+ if (!cached2) {
94
+ return;
95
+ }
96
+ const state = ensurePageState(opts.page);
97
+ if (state.roleRefs) {
98
+ return;
99
+ }
100
+ state.roleRefs = cached2.refs;
101
+ state.roleRefsFrameSelector = cached2.frameSelector;
102
+ state.roleRefsMode = cached2.mode;
103
+ }
104
+ function ensurePageState(page) {
105
+ const existing = pageStates.get(page);
106
+ if (existing) {
107
+ return existing;
108
+ }
109
+ const state = {
110
+ console: [],
111
+ errors: [],
112
+ requests: [],
113
+ requestIds: /* @__PURE__ */ new WeakMap(),
114
+ nextRequestId: 0,
115
+ armIdUpload: 0,
116
+ armIdDialog: 0,
117
+ armIdDownload: 0
118
+ };
119
+ pageStates.set(page, state);
120
+ if (!observedPages.has(page)) {
121
+ observedPages.add(page);
122
+ page.on("console", (msg) => {
123
+ const entry = {
124
+ type: msg.type(),
125
+ text: msg.text(),
126
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
127
+ location: msg.location()
128
+ };
129
+ state.console.push(entry);
130
+ if (state.console.length > MAX_CONSOLE_MESSAGES) {
131
+ state.console.shift();
132
+ }
133
+ });
134
+ page.on("pageerror", (err) => {
135
+ state.errors.push({
136
+ message: err?.message ? String(err.message) : String(err),
137
+ name: err?.name ? String(err.name) : void 0,
138
+ stack: err?.stack ? String(err.stack) : void 0,
139
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
140
+ });
141
+ if (state.errors.length > MAX_PAGE_ERRORS) {
142
+ state.errors.shift();
143
+ }
144
+ });
145
+ page.on("request", (req) => {
146
+ state.nextRequestId += 1;
147
+ const id = `r${state.nextRequestId}`;
148
+ state.requestIds.set(req, id);
149
+ state.requests.push({
150
+ id,
151
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
152
+ method: req.method(),
153
+ url: req.url(),
154
+ resourceType: req.resourceType()
155
+ });
156
+ if (state.requests.length > MAX_NETWORK_REQUESTS) {
157
+ state.requests.shift();
158
+ }
159
+ });
160
+ page.on("response", (resp) => {
161
+ const req = resp.request();
162
+ const id = state.requestIds.get(req);
163
+ if (!id) {
164
+ return;
165
+ }
166
+ const rec = findNetworkRequestById(state, id);
167
+ if (!rec) {
168
+ return;
169
+ }
170
+ rec.status = resp.status();
171
+ rec.ok = resp.ok();
172
+ });
173
+ page.on("requestfailed", (req) => {
174
+ const id = state.requestIds.get(req);
175
+ if (!id) {
176
+ return;
177
+ }
178
+ const rec = findNetworkRequestById(state, id);
179
+ if (!rec) {
180
+ return;
181
+ }
182
+ rec.failureText = req.failure()?.errorText;
183
+ rec.ok = false;
184
+ });
185
+ page.on("close", () => {
186
+ pageStates.delete(page);
187
+ observedPages.delete(page);
188
+ });
189
+ }
190
+ return state;
191
+ }
192
+ function observeContext(context) {
193
+ if (observedContexts.has(context)) {
194
+ return;
195
+ }
196
+ observedContexts.add(context);
197
+ ensureContextState(context);
198
+ for (const page of context.pages()) {
199
+ ensurePageState(page);
200
+ }
201
+ context.on("page", (page) => ensurePageState(page));
202
+ }
203
+ function ensureContextState(context) {
204
+ const existing = contextStates.get(context);
205
+ if (existing) {
206
+ return existing;
207
+ }
208
+ const state = { traceActive: false };
209
+ contextStates.set(context, state);
210
+ return state;
211
+ }
212
+ function observeBrowser(browser) {
213
+ for (const context of browser.contexts()) {
214
+ observeContext(context);
215
+ }
216
+ }
217
+ async function connectBrowser(cdpUrl) {
218
+ const normalized = normalizeCdpUrl(cdpUrl);
219
+ if (cached?.cdpUrl === normalized) {
220
+ return cached;
221
+ }
222
+ if (connecting) {
223
+ return await connecting;
224
+ }
225
+ const connectWithRetry = async () => {
226
+ let lastErr;
227
+ for (let attempt = 0; attempt < 3; attempt += 1) {
228
+ try {
229
+ const timeout = 5e3 + attempt * 2e3;
230
+ const wsUrl = await getChromeWebSocketUrl(normalized, timeout).catch(() => null);
231
+ const endpoint = wsUrl ?? normalized;
232
+ const headers = getHeadersWithAuth(endpoint);
233
+ const browser = await chromium.connectOverCDP(endpoint, { timeout, headers });
234
+ const onDisconnected = () => {
235
+ if (cached?.browser === browser) {
236
+ cached = null;
237
+ }
238
+ };
239
+ const connected = { browser, cdpUrl: normalized, onDisconnected };
240
+ cached = connected;
241
+ browser.on("disconnected", onDisconnected);
242
+ observeBrowser(browser);
243
+ return connected;
244
+ } catch (err) {
245
+ lastErr = err;
246
+ const delay = 250 + attempt * 250;
247
+ await new Promise((r) => setTimeout(r, delay));
248
+ }
249
+ }
250
+ if (lastErr instanceof Error) {
251
+ throw lastErr;
252
+ }
253
+ const message = lastErr ? formatErrorMessage(lastErr) : "CDP connect failed";
254
+ throw new Error(message);
255
+ };
256
+ connecting = connectWithRetry().finally(() => {
257
+ connecting = null;
258
+ });
259
+ return await connecting;
260
+ }
261
+ async function getAllPages(browser) {
262
+ const contexts = browser.contexts();
263
+ const pages = contexts.flatMap((c) => c.pages());
264
+ return pages;
265
+ }
266
+ async function pageTargetId(page) {
267
+ const session = await page.context().newCDPSession(page);
268
+ try {
269
+ const info = await session.send("Target.getTargetInfo");
270
+ const targetId = String(info?.targetInfo?.targetId ?? "").trim();
271
+ return targetId || null;
272
+ } finally {
273
+ await session.detach().catch(() => {
274
+ });
275
+ }
276
+ }
277
+ async function findPageByTargetId(browser, targetId, cdpUrl) {
278
+ const pages = await getAllPages(browser);
279
+ let resolvedViaCdp = false;
280
+ for (const page of pages) {
281
+ let tid = null;
282
+ try {
283
+ tid = await pageTargetId(page);
284
+ resolvedViaCdp = true;
285
+ } catch {
286
+ tid = null;
287
+ }
288
+ if (tid && tid === targetId) {
289
+ return page;
290
+ }
291
+ }
292
+ if (!resolvedViaCdp && pages.length === 1) {
293
+ return pages[0];
294
+ }
295
+ if (cdpUrl) {
296
+ try {
297
+ const baseUrl = cdpUrl.replace(/\/+$/, "").replace(/^ws:/, "http:").replace(/\/cdp$/, "");
298
+ const listUrl = `${baseUrl}/json/list`;
299
+ const response = await fetch(listUrl, { headers: getHeadersWithAuth(listUrl) });
300
+ if (response.ok) {
301
+ const targets = await response.json();
302
+ const target = targets.find((t) => t.id === targetId);
303
+ if (target) {
304
+ const urlMatch = pages.filter((p) => p.url() === target.url);
305
+ if (urlMatch.length === 1) {
306
+ return urlMatch[0];
307
+ }
308
+ if (urlMatch.length > 1) {
309
+ const sameUrlTargets = targets.filter((t) => t.url === target.url);
310
+ if (sameUrlTargets.length === urlMatch.length) {
311
+ const idx = sameUrlTargets.findIndex((t) => t.id === targetId);
312
+ if (idx >= 0 && idx < urlMatch.length) {
313
+ return urlMatch[idx];
314
+ }
315
+ }
316
+ }
317
+ }
318
+ }
319
+ } catch {
320
+ }
321
+ }
322
+ return null;
323
+ }
324
+ async function getPageForTargetId(opts) {
325
+ const { browser } = await connectBrowser(opts.cdpUrl);
326
+ const pages = await getAllPages(browser);
327
+ if (!pages.length) {
328
+ throw new Error("No pages available in the connected browser.");
329
+ }
330
+ const first = pages[0];
331
+ if (!opts.targetId) {
332
+ return first;
333
+ }
334
+ const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
335
+ if (!found) {
336
+ if (pages.length === 1) {
337
+ return first;
338
+ }
339
+ throw new Error("tab not found");
340
+ }
341
+ return found;
342
+ }
343
+ function refLocator(page, ref) {
344
+ const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
345
+ if (/^e\d+$/.test(normalized)) {
346
+ const state = pageStates.get(page);
347
+ if (state?.roleRefsMode === "aria") {
348
+ const scope2 = state.roleRefsFrameSelector ? page.frameLocator(state.roleRefsFrameSelector) : page;
349
+ return scope2.locator(`aria-ref=${normalized}`);
350
+ }
351
+ const info = state?.roleRefs?.[normalized];
352
+ if (!info) {
353
+ throw new Error(
354
+ `Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`
355
+ );
356
+ }
357
+ const scope = state?.roleRefsFrameSelector ? page.frameLocator(state.roleRefsFrameSelector) : page;
358
+ const locAny = scope;
359
+ const locator = info.name ? locAny.getByRole(info.role, { name: info.name, exact: true }) : locAny.getByRole(info.role);
360
+ return info.nth !== void 0 ? locator.nth(info.nth) : locator;
361
+ }
362
+ return page.locator(`aria-ref=${normalized}`);
363
+ }
364
+ async function closePlaywrightBrowserConnection() {
365
+ const cur = cached;
366
+ cached = null;
367
+ connecting = null;
368
+ if (!cur) {
369
+ return;
370
+ }
371
+ if (cur.onDisconnected && typeof cur.browser.off === "function") {
372
+ cur.browser.off("disconnected", cur.onDisconnected);
373
+ }
374
+ await cur.browser.close().catch(() => {
375
+ });
376
+ }
377
+ function normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) {
378
+ try {
379
+ const url = new URL(cdpUrl);
380
+ if (url.protocol === "ws:") {
381
+ url.protocol = "http:";
382
+ } else if (url.protocol === "wss:") {
383
+ url.protocol = "https:";
384
+ }
385
+ url.pathname = url.pathname.replace(/\/devtools\/browser\/.*$/, "");
386
+ url.pathname = url.pathname.replace(/\/cdp$/, "");
387
+ return url.toString().replace(/\/$/, "");
388
+ } catch {
389
+ return cdpUrl.replace(/^ws:/, "http:").replace(/^wss:/, "https:").replace(/\/devtools\/browser\/.*$/, "").replace(/\/cdp$/, "").replace(/\/$/, "");
390
+ }
391
+ }
392
+ function cdpSocketNeedsAttach(wsUrl) {
393
+ try {
394
+ const pathname = new URL(wsUrl).pathname;
395
+ return pathname === "/cdp" || pathname.endsWith("/cdp") || pathname.includes("/devtools/browser/");
396
+ } catch {
397
+ return false;
398
+ }
399
+ }
400
+ async function tryTerminateExecutionViaCdp(opts) {
401
+ const cdpHttpBase = normalizeCdpHttpBaseForJsonEndpoints(opts.cdpUrl);
402
+ const listUrl = appendCdpPath(cdpHttpBase, "/json/list");
403
+ const pages = await fetchJson(listUrl, 2e3).catch(() => null);
404
+ if (!pages || pages.length === 0) {
405
+ return;
406
+ }
407
+ const target = pages.find((p) => String(p.id ?? "").trim() === opts.targetId);
408
+ const wsUrlRaw = String(target?.webSocketDebuggerUrl ?? "").trim();
409
+ if (!wsUrlRaw) {
410
+ return;
411
+ }
412
+ const wsUrl = normalizeCdpWsUrl(wsUrlRaw, cdpHttpBase);
413
+ const needsAttach = cdpSocketNeedsAttach(wsUrl);
414
+ const runWithTimeout = async (work, ms) => {
415
+ let timer;
416
+ const timeoutPromise = new Promise((_, reject) => {
417
+ timer = setTimeout(() => reject(new Error("CDP command timed out")), ms);
418
+ });
419
+ try {
420
+ return await Promise.race([work, timeoutPromise]);
421
+ } finally {
422
+ if (timer) {
423
+ clearTimeout(timer);
424
+ }
425
+ }
426
+ };
427
+ await withCdpSocket(
428
+ wsUrl,
429
+ async (send) => {
430
+ let sessionId;
431
+ try {
432
+ if (needsAttach) {
433
+ const attached = await runWithTimeout(
434
+ send("Target.attachToTarget", { targetId: opts.targetId, flatten: true }),
435
+ 1500
436
+ );
437
+ if (typeof attached?.sessionId === "string" && attached.sessionId.trim()) {
438
+ sessionId = attached.sessionId;
439
+ }
440
+ }
441
+ await runWithTimeout(send("Runtime.terminateExecution", void 0, sessionId), 1500);
442
+ if (sessionId) {
443
+ void send("Target.detachFromTarget", { sessionId }).catch(() => {
444
+ });
445
+ }
446
+ } catch {
447
+ }
448
+ },
449
+ { handshakeTimeoutMs: 2e3 }
450
+ ).catch(() => {
451
+ });
452
+ }
453
+ async function forceDisconnectPlaywrightForTarget(opts) {
454
+ const normalized = normalizeCdpUrl(opts.cdpUrl);
455
+ if (cached?.cdpUrl !== normalized) {
456
+ return;
457
+ }
458
+ const cur = cached;
459
+ cached = null;
460
+ connecting = null;
461
+ if (cur) {
462
+ if (cur.onDisconnected && typeof cur.browser.off === "function") {
463
+ cur.browser.off("disconnected", cur.onDisconnected);
464
+ }
465
+ const targetId = opts.targetId?.trim() || "";
466
+ if (targetId) {
467
+ await tryTerminateExecutionViaCdp({ cdpUrl: normalized, targetId }).catch(() => {
468
+ });
469
+ }
470
+ cur.browser.close().catch(() => {
471
+ });
472
+ }
473
+ }
474
+ async function listPagesViaPlaywright(opts) {
475
+ const { browser } = await connectBrowser(opts.cdpUrl);
476
+ const pages = await getAllPages(browser);
477
+ const results = [];
478
+ for (const page of pages) {
479
+ const tid = await pageTargetId(page).catch(() => null);
480
+ if (tid) {
481
+ results.push({
482
+ targetId: tid,
483
+ title: await page.title().catch(() => ""),
484
+ url: page.url(),
485
+ type: "page"
486
+ });
487
+ }
488
+ }
489
+ return results;
490
+ }
491
+ async function createPageViaPlaywright(opts) {
492
+ const { browser } = await connectBrowser(opts.cdpUrl);
493
+ const context = browser.contexts()[0] ?? await browser.newContext();
494
+ ensureContextState(context);
495
+ const page = await context.newPage();
496
+ ensurePageState(page);
497
+ const targetUrl = opts.url.trim() || "about:blank";
498
+ if (targetUrl !== "about:blank") {
499
+ if (!opts.navigationChecked) {
500
+ await assertBrowserNavigationAllowed({
501
+ url: targetUrl,
502
+ ...withBrowserNavigationPolicy(opts.ssrfPolicy)
503
+ });
504
+ }
505
+ await page.goto(targetUrl, { timeout: 3e4 }).catch(() => {
506
+ });
507
+ }
508
+ const tid = await pageTargetId(page).catch(() => null);
509
+ if (!tid) {
510
+ throw new Error("Failed to get targetId for new page");
511
+ }
512
+ return {
513
+ targetId: tid,
514
+ title: await page.title().catch(() => ""),
515
+ url: page.url(),
516
+ type: "page"
517
+ };
518
+ }
519
+ async function closePageByTargetIdViaPlaywright(opts) {
520
+ const { browser } = await connectBrowser(opts.cdpUrl);
521
+ const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
522
+ if (!page) {
523
+ throw new Error("tab not found");
524
+ }
525
+ await page.close();
526
+ }
527
+ async function focusPageByTargetIdViaPlaywright(opts) {
528
+ const { browser } = await connectBrowser(opts.cdpUrl);
529
+ const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
530
+ if (!page) {
531
+ throw new Error("tab not found");
532
+ }
533
+ try {
534
+ await page.bringToFront();
535
+ } catch (err) {
536
+ const session = await page.context().newCDPSession(page);
537
+ try {
538
+ await session.send("Page.bringToFront");
539
+ return;
540
+ } catch {
541
+ throw err;
542
+ } finally {
543
+ await session.detach().catch(() => {
544
+ });
545
+ }
546
+ }
547
+ }
548
+
549
+ // src/browser/pw-tools-core.activity.ts
550
+ async function getPageErrorsViaPlaywright(opts) {
551
+ const page = await getPageForTargetId(opts);
552
+ const state = ensurePageState(page);
553
+ const errors = [...state.errors];
554
+ if (opts.clear) {
555
+ state.errors = [];
556
+ }
557
+ return { errors };
558
+ }
559
+ async function getNetworkRequestsViaPlaywright(opts) {
560
+ const page = await getPageForTargetId(opts);
561
+ const state = ensurePageState(page);
562
+ const raw = [...state.requests];
563
+ const filter = typeof opts.filter === "string" ? opts.filter.trim() : "";
564
+ const requests = filter ? raw.filter((r) => r.url.includes(filter)) : raw;
565
+ if (opts.clear) {
566
+ state.requests = [];
567
+ state.requestIds = /* @__PURE__ */ new WeakMap();
568
+ }
569
+ return { requests };
570
+ }
571
+ function consolePriority(level) {
572
+ switch (level) {
573
+ case "error":
574
+ return 3;
575
+ case "warning":
576
+ return 2;
577
+ case "info":
578
+ case "log":
579
+ return 1;
580
+ case "debug":
581
+ return 0;
582
+ default:
583
+ return 1;
584
+ }
585
+ }
586
+ async function getConsoleMessagesViaPlaywright(opts) {
587
+ const page = await getPageForTargetId(opts);
588
+ const state = ensurePageState(page);
589
+ if (!opts.level) {
590
+ return [...state.console];
591
+ }
592
+ const min = consolePriority(opts.level);
593
+ return state.console.filter((msg) => consolePriority(msg.type) >= min);
594
+ }
595
+
596
+ // src/browser/pw-tools-core.downloads.ts
597
+ import crypto from "crypto";
598
+ import fs from "fs/promises";
599
+ import path from "path";
600
+
601
+ // src/browser/pw-role-snapshot.ts
602
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
603
+ "button",
604
+ "link",
605
+ "textbox",
606
+ "checkbox",
607
+ "radio",
608
+ "combobox",
609
+ "listbox",
610
+ "menuitem",
611
+ "menuitemcheckbox",
612
+ "menuitemradio",
613
+ "option",
614
+ "searchbox",
615
+ "slider",
616
+ "spinbutton",
617
+ "switch",
618
+ "tab",
619
+ "treeitem"
620
+ ]);
621
+ var CONTENT_ROLES = /* @__PURE__ */ new Set([
622
+ "heading",
623
+ "cell",
624
+ "gridcell",
625
+ "columnheader",
626
+ "rowheader",
627
+ "listitem",
628
+ "article",
629
+ "region",
630
+ "main",
631
+ "navigation"
632
+ ]);
633
+ var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
634
+ "generic",
635
+ "group",
636
+ "list",
637
+ "table",
638
+ "row",
639
+ "rowgroup",
640
+ "grid",
641
+ "treegrid",
642
+ "menu",
643
+ "menubar",
644
+ "toolbar",
645
+ "tablist",
646
+ "tree",
647
+ "directory",
648
+ "document",
649
+ "application",
650
+ "presentation",
651
+ "none"
652
+ ]);
653
+ function getRoleSnapshotStats(snapshot, refs) {
654
+ const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length;
655
+ return {
656
+ lines: snapshot.split("\n").length,
657
+ chars: snapshot.length,
658
+ refs: Object.keys(refs).length,
659
+ interactive
660
+ };
661
+ }
662
+ function getIndentLevel(line) {
663
+ const match = line.match(/^(\s*)/);
664
+ return match ? Math.floor(match[1].length / 2) : 0;
665
+ }
666
+ function matchInteractiveSnapshotLine(line, options) {
667
+ const depth = getIndentLevel(line);
668
+ if (options.maxDepth !== void 0 && depth > options.maxDepth) {
669
+ return null;
670
+ }
671
+ const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
672
+ if (!match) {
673
+ return null;
674
+ }
675
+ const [, , roleRaw, name, suffix] = match;
676
+ if (roleRaw.startsWith("/")) {
677
+ return null;
678
+ }
679
+ const role = roleRaw.toLowerCase();
680
+ return {
681
+ roleRaw,
682
+ role,
683
+ ...name ? { name } : {},
684
+ suffix
685
+ };
686
+ }
687
+ function createRoleNameTracker() {
688
+ const counts = /* @__PURE__ */ new Map();
689
+ const refsByKey = /* @__PURE__ */ new Map();
690
+ return {
691
+ counts,
692
+ refsByKey,
693
+ getKey(role, name) {
694
+ return `${role}:${name ?? ""}`;
695
+ },
696
+ getNextIndex(role, name) {
697
+ const key = this.getKey(role, name);
698
+ const current = counts.get(key) ?? 0;
699
+ counts.set(key, current + 1);
700
+ return current;
701
+ },
702
+ trackRef(role, name, ref) {
703
+ const key = this.getKey(role, name);
704
+ const list = refsByKey.get(key) ?? [];
705
+ list.push(ref);
706
+ refsByKey.set(key, list);
707
+ },
708
+ getDuplicateKeys() {
709
+ const out = /* @__PURE__ */ new Set();
710
+ for (const [key, refs] of refsByKey) {
711
+ if (refs.length > 1) {
712
+ out.add(key);
713
+ }
714
+ }
715
+ return out;
716
+ }
717
+ };
718
+ }
719
+ function removeNthFromNonDuplicates(refs, tracker) {
720
+ const duplicates = tracker.getDuplicateKeys();
721
+ for (const [ref, data] of Object.entries(refs)) {
722
+ const key = tracker.getKey(data.role, data.name);
723
+ if (!duplicates.has(key)) {
724
+ delete refs[ref]?.nth;
725
+ }
726
+ }
727
+ }
728
+ function compactTree(tree) {
729
+ const lines = tree.split("\n");
730
+ const result = [];
731
+ for (let i = 0; i < lines.length; i += 1) {
732
+ const line = lines[i];
733
+ if (line.includes("[ref=")) {
734
+ result.push(line);
735
+ continue;
736
+ }
737
+ if (line.includes(":") && !line.trimEnd().endsWith(":")) {
738
+ result.push(line);
739
+ continue;
740
+ }
741
+ const currentIndent = getIndentLevel(line);
742
+ let hasRelevantChildren = false;
743
+ for (let j = i + 1; j < lines.length; j += 1) {
744
+ const childIndent = getIndentLevel(lines[j]);
745
+ if (childIndent <= currentIndent) {
746
+ break;
747
+ }
748
+ if (lines[j]?.includes("[ref=")) {
749
+ hasRelevantChildren = true;
750
+ break;
751
+ }
752
+ }
753
+ if (hasRelevantChildren) {
754
+ result.push(line);
755
+ }
756
+ }
757
+ return result.join("\n");
758
+ }
759
+ function processLine(line, refs, options, tracker, nextRef) {
760
+ const depth = getIndentLevel(line);
761
+ if (options.maxDepth !== void 0 && depth > options.maxDepth) {
762
+ return null;
763
+ }
764
+ const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
765
+ if (!match) {
766
+ return options.interactive ? null : line;
767
+ }
768
+ const [, prefix, roleRaw, name, suffix] = match;
769
+ if (roleRaw.startsWith("/")) {
770
+ return options.interactive ? null : line;
771
+ }
772
+ const role = roleRaw.toLowerCase();
773
+ const isInteractive = INTERACTIVE_ROLES.has(role);
774
+ const isContent = CONTENT_ROLES.has(role);
775
+ const isStructural = STRUCTURAL_ROLES.has(role);
776
+ if (options.interactive && !isInteractive) {
777
+ return null;
778
+ }
779
+ if (options.compact && isStructural && !name) {
780
+ return null;
781
+ }
782
+ const shouldHaveRef = isInteractive || isContent && name;
783
+ if (!shouldHaveRef) {
784
+ return line;
785
+ }
786
+ const ref = nextRef();
787
+ const nth = tracker.getNextIndex(role, name);
788
+ tracker.trackRef(role, name, ref);
789
+ refs[ref] = {
790
+ role,
791
+ name,
792
+ nth
793
+ };
794
+ let enhanced = `${prefix}${roleRaw}`;
795
+ if (name) {
796
+ enhanced += ` "${name}"`;
797
+ }
798
+ enhanced += ` [ref=${ref}]`;
799
+ if (nth > 0) {
800
+ enhanced += ` [nth=${nth}]`;
801
+ }
802
+ if (suffix) {
803
+ enhanced += suffix;
804
+ }
805
+ return enhanced;
806
+ }
807
+ function parseRoleRef(raw) {
808
+ const trimmed = raw.trim();
809
+ if (!trimmed) {
810
+ return null;
811
+ }
812
+ const normalized = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
813
+ return /^e\d+$/.test(normalized) ? normalized : null;
814
+ }
815
+ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
816
+ const lines = ariaSnapshot.split("\n");
817
+ const refs = {};
818
+ const tracker = createRoleNameTracker();
819
+ let counter = 0;
820
+ const nextRef = () => {
821
+ counter += 1;
822
+ return `e${counter}`;
823
+ };
824
+ if (options.interactive) {
825
+ const result2 = [];
826
+ for (const line of lines) {
827
+ const parsed = matchInteractiveSnapshotLine(line, options);
828
+ if (!parsed) {
829
+ continue;
830
+ }
831
+ const { roleRaw, role, name, suffix } = parsed;
832
+ if (!INTERACTIVE_ROLES.has(role)) {
833
+ continue;
834
+ }
835
+ const ref = nextRef();
836
+ const nth = tracker.getNextIndex(role, name);
837
+ tracker.trackRef(role, name, ref);
838
+ refs[ref] = {
839
+ role,
840
+ name,
841
+ nth
842
+ };
843
+ let enhanced = `- ${roleRaw}`;
844
+ if (name) {
845
+ enhanced += ` "${name}"`;
846
+ }
847
+ enhanced += ` [ref=${ref}]`;
848
+ if (nth > 0) {
849
+ enhanced += ` [nth=${nth}]`;
850
+ }
851
+ if (suffix.includes("[")) {
852
+ enhanced += suffix;
853
+ }
854
+ result2.push(enhanced);
855
+ }
856
+ removeNthFromNonDuplicates(refs, tracker);
857
+ return {
858
+ snapshot: result2.join("\n") || "(no interactive elements)",
859
+ refs
860
+ };
861
+ }
862
+ const result = [];
863
+ for (const line of lines) {
864
+ const processed = processLine(line, refs, options, tracker, nextRef);
865
+ if (processed !== null) {
866
+ result.push(processed);
867
+ }
868
+ }
869
+ removeNthFromNonDuplicates(refs, tracker);
870
+ const tree = result.join("\n") || "(empty)";
871
+ return {
872
+ snapshot: options.compact ? compactTree(tree) : tree,
873
+ refs
874
+ };
875
+ }
876
+ function parseAiSnapshotRef(suffix) {
877
+ const match = suffix.match(/\[ref=(e\d+)\]/i);
878
+ return match ? match[1] : null;
879
+ }
880
+ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
881
+ const lines = String(aiSnapshot ?? "").split("\n");
882
+ const refs = {};
883
+ if (options.interactive) {
884
+ const out2 = [];
885
+ for (const line of lines) {
886
+ const parsed = matchInteractiveSnapshotLine(line, options);
887
+ if (!parsed) {
888
+ continue;
889
+ }
890
+ const { roleRaw, role, name, suffix } = parsed;
891
+ if (!INTERACTIVE_ROLES.has(role)) {
892
+ continue;
893
+ }
894
+ const ref = parseAiSnapshotRef(suffix);
895
+ if (!ref) {
896
+ continue;
897
+ }
898
+ refs[ref] = { role, ...name ? { name } : {} };
899
+ out2.push(`- ${roleRaw}${name ? ` "${name}"` : ""}${suffix}`);
900
+ }
901
+ return {
902
+ snapshot: out2.join("\n") || "(no interactive elements)",
903
+ refs
904
+ };
905
+ }
906
+ const out = [];
907
+ for (const line of lines) {
908
+ const depth = getIndentLevel(line);
909
+ if (options.maxDepth !== void 0 && depth > options.maxDepth) {
910
+ continue;
911
+ }
912
+ const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
913
+ if (!match) {
914
+ out.push(line);
915
+ continue;
916
+ }
917
+ const [, , roleRaw, name, suffix] = match;
918
+ if (roleRaw.startsWith("/")) {
919
+ out.push(line);
920
+ continue;
921
+ }
922
+ const role = roleRaw.toLowerCase();
923
+ const isStructural = STRUCTURAL_ROLES.has(role);
924
+ if (options.compact && isStructural && !name) {
925
+ continue;
926
+ }
927
+ const ref = parseAiSnapshotRef(suffix);
928
+ if (ref) {
929
+ refs[ref] = { role, ...name ? { name } : {} };
930
+ }
931
+ out.push(line);
932
+ }
933
+ const tree = out.join("\n") || "(empty)";
934
+ return {
935
+ snapshot: options.compact ? compactTree(tree) : tree,
936
+ refs
937
+ };
938
+ }
939
+
940
+ // src/browser/pw-tools-core.shared.ts
941
+ var nextUploadArmId = 0;
942
+ var nextDialogArmId = 0;
943
+ var nextDownloadArmId = 0;
944
+ function bumpUploadArmId() {
945
+ nextUploadArmId += 1;
946
+ return nextUploadArmId;
947
+ }
948
+ function bumpDialogArmId() {
949
+ nextDialogArmId += 1;
950
+ return nextDialogArmId;
951
+ }
952
+ function bumpDownloadArmId() {
953
+ nextDownloadArmId += 1;
954
+ return nextDownloadArmId;
955
+ }
956
+ function requireRef(value) {
957
+ const raw = typeof value === "string" ? value.trim() : "";
958
+ const roleRef = raw ? parseRoleRef(raw) : null;
959
+ const ref = roleRef ?? (raw.startsWith("@") ? raw.slice(1) : raw);
960
+ if (!ref) {
961
+ throw new Error("ref is required");
962
+ }
963
+ return ref;
964
+ }
965
+ function normalizeTimeoutMs(timeoutMs, fallback) {
966
+ return Math.max(500, Math.min(12e4, timeoutMs ?? fallback));
967
+ }
968
+ function toAIFriendlyError(error, selector) {
969
+ const message = error instanceof Error ? error.message : String(error);
970
+ if (message.includes("strict mode violation")) {
971
+ const countMatch = message.match(/resolved to (\d+) elements/);
972
+ const count = countMatch ? countMatch[1] : "multiple";
973
+ return new Error(
974
+ `Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`
975
+ );
976
+ }
977
+ if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
978
+ return new Error(
979
+ `Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`
980
+ );
981
+ }
982
+ if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
983
+ return new Error(
984
+ `Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
985
+ );
986
+ }
987
+ return error instanceof Error ? error : new Error(message);
988
+ }
989
+
990
+ // src/browser/pw-tools-core.downloads.ts
991
+ function sanitizeDownloadFileName(fileName) {
992
+ const trimmed = String(fileName ?? "").trim();
993
+ if (!trimmed) {
994
+ return "download.bin";
995
+ }
996
+ let base = path.posix.basename(trimmed);
997
+ base = path.win32.basename(base);
998
+ let cleaned = "";
999
+ for (let i = 0; i < base.length; i++) {
1000
+ const code = base.charCodeAt(i);
1001
+ if (code < 32 || code === 127) {
1002
+ continue;
1003
+ }
1004
+ cleaned += base[i];
1005
+ }
1006
+ base = cleaned.trim();
1007
+ if (!base || base === "." || base === "..") {
1008
+ return "download.bin";
1009
+ }
1010
+ if (base.length > 200) {
1011
+ base = base.slice(0, 200);
1012
+ }
1013
+ return base;
1014
+ }
1015
+ function buildTempDownloadPath(fileName) {
1016
+ const id = crypto.randomUUID();
1017
+ const safeName = sanitizeDownloadFileName(fileName);
1018
+ return path.join(resolvePreferredAgenticMailTmpDir(), "downloads", `${id}-${safeName}`);
1019
+ }
1020
+ function createPageDownloadWaiter(page, timeoutMs) {
1021
+ let done = false;
1022
+ let timer;
1023
+ let handler;
1024
+ const cleanup = () => {
1025
+ if (timer) {
1026
+ clearTimeout(timer);
1027
+ }
1028
+ timer = void 0;
1029
+ if (handler) {
1030
+ page.off("download", handler);
1031
+ handler = void 0;
1032
+ }
1033
+ };
1034
+ const promise = new Promise((resolve, reject) => {
1035
+ handler = (download) => {
1036
+ if (done) {
1037
+ return;
1038
+ }
1039
+ done = true;
1040
+ cleanup();
1041
+ resolve(download);
1042
+ };
1043
+ page.on("download", handler);
1044
+ timer = setTimeout(() => {
1045
+ if (done) {
1046
+ return;
1047
+ }
1048
+ done = true;
1049
+ cleanup();
1050
+ reject(new Error("Timeout waiting for download"));
1051
+ }, timeoutMs);
1052
+ });
1053
+ return {
1054
+ promise,
1055
+ cancel: () => {
1056
+ if (done) {
1057
+ return;
1058
+ }
1059
+ done = true;
1060
+ cleanup();
1061
+ }
1062
+ };
1063
+ }
1064
+ async function saveDownloadPayload(download, outPath) {
1065
+ const suggested = download.suggestedFilename?.() || "download.bin";
1066
+ const resolvedOutPath = outPath?.trim() || buildTempDownloadPath(suggested);
1067
+ await fs.mkdir(path.dirname(resolvedOutPath), { recursive: true });
1068
+ await download.saveAs?.(resolvedOutPath);
1069
+ return {
1070
+ url: download.url?.() || "",
1071
+ suggestedFilename: suggested,
1072
+ path: path.resolve(resolvedOutPath)
1073
+ };
1074
+ }
1075
+ async function awaitDownloadPayload(params) {
1076
+ try {
1077
+ const download = await params.waiter.promise;
1078
+ if (params.state.armIdDownload !== params.armId) {
1079
+ throw new Error("Download was superseded by another waiter");
1080
+ }
1081
+ return await saveDownloadPayload(download, params.outPath ?? "");
1082
+ } catch (err) {
1083
+ params.waiter.cancel();
1084
+ throw err;
1085
+ }
1086
+ }
1087
+ async function armFileUploadViaPlaywright(opts) {
1088
+ const page = await getPageForTargetId(opts);
1089
+ const state = ensurePageState(page);
1090
+ const timeout = Math.max(500, Math.min(12e4, opts.timeoutMs ?? 12e4));
1091
+ state.armIdUpload = bumpUploadArmId();
1092
+ const armId = state.armIdUpload;
1093
+ void page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
1094
+ if (state.armIdUpload !== armId) {
1095
+ return;
1096
+ }
1097
+ if (!opts.paths?.length) {
1098
+ try {
1099
+ await page.keyboard.press("Escape");
1100
+ } catch {
1101
+ }
1102
+ return;
1103
+ }
1104
+ await fileChooser.setFiles(opts.paths);
1105
+ try {
1106
+ const input = typeof fileChooser.element === "function" ? await Promise.resolve(fileChooser.element()) : null;
1107
+ if (input) {
1108
+ await input.evaluate((el) => {
1109
+ el.dispatchEvent(new Event("input", { bubbles: true }));
1110
+ el.dispatchEvent(new Event("change", { bubbles: true }));
1111
+ });
1112
+ }
1113
+ } catch {
1114
+ }
1115
+ }).catch(() => {
1116
+ });
1117
+ }
1118
+ async function armDialogViaPlaywright(opts) {
1119
+ const page = await getPageForTargetId(opts);
1120
+ const state = ensurePageState(page);
1121
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
1122
+ state.armIdDialog = bumpDialogArmId();
1123
+ const armId = state.armIdDialog;
1124
+ void page.waitForEvent("dialog", { timeout }).then(async (dialog) => {
1125
+ if (state.armIdDialog !== armId) {
1126
+ return;
1127
+ }
1128
+ if (opts.accept) {
1129
+ await dialog.accept(opts.promptText);
1130
+ } else {
1131
+ await dialog.dismiss();
1132
+ }
1133
+ }).catch(() => {
1134
+ });
1135
+ }
1136
+ async function waitForDownloadViaPlaywright(opts) {
1137
+ const page = await getPageForTargetId(opts);
1138
+ const state = ensurePageState(page);
1139
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
1140
+ state.armIdDownload = bumpDownloadArmId();
1141
+ const armId = state.armIdDownload;
1142
+ const waiter = createPageDownloadWaiter(page, timeout);
1143
+ return await awaitDownloadPayload({ waiter, state, armId, outPath: opts.path });
1144
+ }
1145
+ async function downloadViaPlaywright(opts) {
1146
+ const page = await getPageForTargetId(opts);
1147
+ const state = ensurePageState(page);
1148
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1149
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
1150
+ const ref = requireRef(opts.ref);
1151
+ const outPath = String(opts.path ?? "").trim();
1152
+ if (!outPath) {
1153
+ throw new Error("path is required");
1154
+ }
1155
+ state.armIdDownload = bumpDownloadArmId();
1156
+ const armId = state.armIdDownload;
1157
+ const waiter = createPageDownloadWaiter(page, timeout);
1158
+ try {
1159
+ const locator = refLocator(page, ref);
1160
+ try {
1161
+ await locator.click({ timeout });
1162
+ } catch (err) {
1163
+ throw toAIFriendlyError(err, ref);
1164
+ }
1165
+ return await awaitDownloadPayload({ waiter, state, armId, outPath });
1166
+ } catch (err) {
1167
+ waiter.cancel();
1168
+ throw err;
1169
+ }
1170
+ }
1171
+
1172
+ // src/browser/pw-tools-core.interactions.ts
1173
+ async function highlightViaPlaywright(opts) {
1174
+ const page = await getPageForTargetId(opts);
1175
+ ensurePageState(page);
1176
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1177
+ const ref = requireRef(opts.ref);
1178
+ try {
1179
+ await refLocator(page, ref).highlight();
1180
+ } catch (err) {
1181
+ throw toAIFriendlyError(err, ref);
1182
+ }
1183
+ }
1184
+ async function clickViaPlaywright(opts) {
1185
+ const page = await getPageForTargetId({
1186
+ cdpUrl: opts.cdpUrl,
1187
+ targetId: opts.targetId
1188
+ });
1189
+ ensurePageState(page);
1190
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1191
+ const ref = requireRef(opts.ref);
1192
+ const locator = refLocator(page, ref);
1193
+ try {
1194
+ await locator.evaluate((el) => {
1195
+ el.scrollIntoView({ block: "center", behavior: "instant" });
1196
+ });
1197
+ await page.waitForTimeout(200);
1198
+ } catch {
1199
+ }
1200
+ const quickTimeout = Math.min(5e3, Math.floor(opts.timeoutMs ?? 5e3));
1201
+ try {
1202
+ if (opts.doubleClick) {
1203
+ await locator.dblclick({
1204
+ timeout: quickTimeout,
1205
+ button: opts.button,
1206
+ modifiers: opts.modifiers
1207
+ });
1208
+ } else {
1209
+ await locator.click({
1210
+ timeout: quickTimeout,
1211
+ button: opts.button,
1212
+ modifiers: opts.modifiers
1213
+ });
1214
+ }
1215
+ return;
1216
+ } catch {
1217
+ }
1218
+ try {
1219
+ if (opts.doubleClick) {
1220
+ await locator.dblclick({
1221
+ timeout: 3e3,
1222
+ force: true,
1223
+ button: opts.button,
1224
+ modifiers: opts.modifiers
1225
+ });
1226
+ } else {
1227
+ await locator.click({
1228
+ timeout: 3e3,
1229
+ force: true,
1230
+ button: opts.button,
1231
+ modifiers: opts.modifiers
1232
+ });
1233
+ }
1234
+ return;
1235
+ } catch {
1236
+ }
1237
+ try {
1238
+ await locator.evaluate((el) => {
1239
+ el.scrollIntoView({ block: "center", behavior: "instant" });
1240
+ el.focus();
1241
+ el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
1242
+ el.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
1243
+ el.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
1244
+ });
1245
+ return;
1246
+ } catch {
1247
+ }
1248
+ try {
1249
+ const box = await locator.boundingBox({ timeout: 3e3 });
1250
+ if (box) {
1251
+ const x = box.x + box.width / 2;
1252
+ const y = box.y + box.height / 2;
1253
+ if (opts.doubleClick) {
1254
+ await page.mouse.dblclick(x, y, { button: opts.button || "left" });
1255
+ } else {
1256
+ await page.mouse.click(x, y, { button: opts.button || "left" });
1257
+ }
1258
+ return;
1259
+ }
1260
+ } catch {
1261
+ }
1262
+ throw new Error(
1263
+ `Click failed on ref "${ref}" after trying 4 strategies (normal, force, JS dispatch, coordinate). Try mouse_click with specific x,y coordinates from a screenshot, or evaluate with document.querySelector().`
1264
+ );
1265
+ }
1266
+ async function hoverViaPlaywright(opts) {
1267
+ const ref = requireRef(opts.ref);
1268
+ const page = await getPageForTargetId(opts);
1269
+ ensurePageState(page);
1270
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1271
+ try {
1272
+ await refLocator(page, ref).hover({
1273
+ timeout: Math.max(500, Math.min(6e4, opts.timeoutMs ?? 8e3))
1274
+ });
1275
+ } catch (err) {
1276
+ throw toAIFriendlyError(err, ref);
1277
+ }
1278
+ }
1279
+ async function dragViaPlaywright(opts) {
1280
+ const startRef = requireRef(opts.startRef);
1281
+ const endRef = requireRef(opts.endRef);
1282
+ if (!startRef || !endRef) {
1283
+ throw new Error("startRef and endRef are required");
1284
+ }
1285
+ const page = await getPageForTargetId(opts);
1286
+ ensurePageState(page);
1287
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1288
+ try {
1289
+ await refLocator(page, startRef).dragTo(refLocator(page, endRef), {
1290
+ timeout: Math.max(500, Math.min(6e4, opts.timeoutMs ?? 8e3))
1291
+ });
1292
+ } catch (err) {
1293
+ throw toAIFriendlyError(err, `${startRef} -> ${endRef}`);
1294
+ }
1295
+ }
1296
+ async function selectOptionViaPlaywright(opts) {
1297
+ const ref = requireRef(opts.ref);
1298
+ if (!opts.values?.length) {
1299
+ throw new Error("values are required");
1300
+ }
1301
+ const page = await getPageForTargetId(opts);
1302
+ ensurePageState(page);
1303
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1304
+ try {
1305
+ await refLocator(page, ref).selectOption(opts.values, {
1306
+ timeout: Math.max(500, Math.min(6e4, opts.timeoutMs ?? 8e3))
1307
+ });
1308
+ } catch (err) {
1309
+ throw toAIFriendlyError(err, ref);
1310
+ }
1311
+ }
1312
+ async function pressKeyViaPlaywright(opts) {
1313
+ const key = String(opts.key ?? "").trim();
1314
+ if (!key) {
1315
+ throw new Error("key is required");
1316
+ }
1317
+ const page = await getPageForTargetId(opts);
1318
+ ensurePageState(page);
1319
+ await page.keyboard.press(key, {
1320
+ delay: Math.max(0, Math.floor(opts.delayMs ?? 0))
1321
+ });
1322
+ }
1323
+ async function typeViaPlaywright(opts) {
1324
+ const text = String(opts.text ?? "");
1325
+ const page = await getPageForTargetId(opts);
1326
+ ensurePageState(page);
1327
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1328
+ const ref = requireRef(opts.ref);
1329
+ const locator = refLocator(page, ref);
1330
+ const timeout = Math.max(500, Math.min(6e4, opts.timeoutMs ?? 8e3));
1331
+ try {
1332
+ if (opts.slowly) {
1333
+ try {
1334
+ await locator.click({ timeout: 3e3 });
1335
+ } catch {
1336
+ await locator.click({ timeout: 3e3, force: true });
1337
+ }
1338
+ await locator.type(text, { timeout, delay: 75 });
1339
+ } else {
1340
+ try {
1341
+ await locator.fill(text, { timeout: 3e3 });
1342
+ } catch {
1343
+ try {
1344
+ await locator.click({ timeout: 3e3 });
1345
+ } catch {
1346
+ await locator.click({ timeout: 3e3, force: true });
1347
+ }
1348
+ await page.keyboard.press("Control+A");
1349
+ await page.keyboard.press("Backspace");
1350
+ const inserted = await page.evaluate((t) => {
1351
+ const el = document.activeElement;
1352
+ if (el && el.isContentEditable) {
1353
+ document.execCommand("insertText", false, t);
1354
+ return true;
1355
+ }
1356
+ return false;
1357
+ }, text);
1358
+ if (!inserted) {
1359
+ await page.keyboard.type(text, { delay: 20 });
1360
+ }
1361
+ }
1362
+ }
1363
+ if (opts.submit) {
1364
+ await locator.press("Enter", { timeout });
1365
+ }
1366
+ } catch (err) {
1367
+ throw toAIFriendlyError(err, ref);
1368
+ }
1369
+ }
1370
+ async function fillFormViaPlaywright(opts) {
1371
+ const page = await getPageForTargetId(opts);
1372
+ ensurePageState(page);
1373
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1374
+ const timeout = Math.max(500, Math.min(6e4, opts.timeoutMs ?? 8e3));
1375
+ for (const field of opts.fields) {
1376
+ const ref = field.ref.trim();
1377
+ const type = field.type.trim();
1378
+ const rawValue = field.value;
1379
+ const value = typeof rawValue === "string" ? rawValue : typeof rawValue === "number" || typeof rawValue === "boolean" ? String(rawValue) : "";
1380
+ if (!ref || !type) {
1381
+ continue;
1382
+ }
1383
+ const locator = refLocator(page, ref);
1384
+ if (type === "checkbox" || type === "radio") {
1385
+ const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
1386
+ try {
1387
+ await locator.setChecked(checked, { timeout });
1388
+ } catch (err) {
1389
+ throw toAIFriendlyError(err, ref);
1390
+ }
1391
+ continue;
1392
+ }
1393
+ try {
1394
+ await locator.fill(value, { timeout });
1395
+ } catch (err) {
1396
+ throw toAIFriendlyError(err, ref);
1397
+ }
1398
+ }
1399
+ }
1400
+ async function evaluateViaPlaywright(opts) {
1401
+ let fnText = String(opts.fn ?? "").trim();
1402
+ if (!fnText) {
1403
+ throw new Error("function is required");
1404
+ }
1405
+ const looksLikeStatements = /[;\n]/.test(fnText) && !fnText.startsWith("(") && !fnText.startsWith("function") && !fnText.startsWith("async") && !fnText.startsWith("()") && !/^[\w.[\]]+$/.test(fnText);
1406
+ if (looksLikeStatements) {
1407
+ fnText = `(function() { ${fnText} })()`;
1408
+ }
1409
+ const page = await getPageForTargetId(opts);
1410
+ ensurePageState(page);
1411
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1412
+ const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
1413
+ let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
1414
+ evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
1415
+ const signal = opts.signal;
1416
+ let abortListener;
1417
+ let abortReject;
1418
+ let abortPromise;
1419
+ if (signal) {
1420
+ abortPromise = new Promise((_, reject) => {
1421
+ abortReject = reject;
1422
+ });
1423
+ void abortPromise.catch(() => {
1424
+ });
1425
+ }
1426
+ if (signal) {
1427
+ const disconnect = () => {
1428
+ void forceDisconnectPlaywrightForTarget({
1429
+ cdpUrl: opts.cdpUrl,
1430
+ targetId: opts.targetId,
1431
+ reason: "evaluate aborted"
1432
+ }).catch(() => {
1433
+ });
1434
+ };
1435
+ if (signal.aborted) {
1436
+ disconnect();
1437
+ throw signal.reason ?? new Error("aborted");
1438
+ }
1439
+ abortListener = () => {
1440
+ disconnect();
1441
+ abortReject?.(signal.reason ?? new Error("aborted"));
1442
+ };
1443
+ signal.addEventListener("abort", abortListener, { once: true });
1444
+ if (signal.aborted) {
1445
+ abortListener();
1446
+ throw signal.reason ?? new Error("aborted");
1447
+ }
1448
+ }
1449
+ try {
1450
+ if (opts.ref) {
1451
+ const locator = refLocator(page, opts.ref);
1452
+ const elementEvaluator = new Function(
1453
+ "el",
1454
+ "args",
1455
+ `
1456
+ "use strict";
1457
+ var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
1458
+ try {
1459
+ var candidate = eval("(" + fnBody + ")");
1460
+ var result = typeof candidate === "function" ? candidate(el) : candidate;
1461
+ if (result && typeof result.then === "function") {
1462
+ return Promise.race([
1463
+ result,
1464
+ new Promise(function(_, reject) {
1465
+ setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
1466
+ })
1467
+ ]);
1468
+ }
1469
+ return result;
1470
+ } catch (err) {
1471
+ throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
1472
+ }
1473
+ `
1474
+ );
1475
+ const evalPromise2 = locator.evaluate(elementEvaluator, {
1476
+ fnBody: fnText,
1477
+ timeoutMs: evaluateTimeout
1478
+ });
1479
+ if (!abortPromise) {
1480
+ return await evalPromise2;
1481
+ }
1482
+ try {
1483
+ return await Promise.race([evalPromise2, abortPromise]);
1484
+ } catch (err) {
1485
+ void evalPromise2.catch(() => {
1486
+ });
1487
+ throw err;
1488
+ }
1489
+ }
1490
+ const browserEvaluator = new Function(
1491
+ "args",
1492
+ `
1493
+ "use strict";
1494
+ var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
1495
+ try {
1496
+ var candidate = eval("(" + fnBody + ")");
1497
+ var result = typeof candidate === "function" ? candidate() : candidate;
1498
+ if (result && typeof result.then === "function") {
1499
+ return Promise.race([
1500
+ result,
1501
+ new Promise(function(_, reject) {
1502
+ setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
1503
+ })
1504
+ ]);
1505
+ }
1506
+ return result;
1507
+ } catch (err) {
1508
+ throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
1509
+ }
1510
+ `
1511
+ );
1512
+ const evalPromise = page.evaluate(browserEvaluator, {
1513
+ fnBody: fnText,
1514
+ timeoutMs: evaluateTimeout
1515
+ });
1516
+ if (!abortPromise) {
1517
+ return await evalPromise;
1518
+ }
1519
+ try {
1520
+ return await Promise.race([evalPromise, abortPromise]);
1521
+ } catch (err) {
1522
+ void evalPromise.catch(() => {
1523
+ });
1524
+ throw err;
1525
+ }
1526
+ } finally {
1527
+ if (signal && abortListener) {
1528
+ signal.removeEventListener("abort", abortListener);
1529
+ }
1530
+ }
1531
+ }
1532
+ async function scrollIntoViewViaPlaywright(opts) {
1533
+ const page = await getPageForTargetId(opts);
1534
+ ensurePageState(page);
1535
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1536
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
1537
+ const ref = requireRef(opts.ref);
1538
+ const locator = refLocator(page, ref);
1539
+ try {
1540
+ await locator.scrollIntoViewIfNeeded({ timeout });
1541
+ } catch (err) {
1542
+ throw toAIFriendlyError(err, ref);
1543
+ }
1544
+ }
1545
+ async function waitForViaPlaywright(opts) {
1546
+ const page = await getPageForTargetId(opts);
1547
+ ensurePageState(page);
1548
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
1549
+ if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
1550
+ await page.waitForTimeout(Math.max(0, opts.timeMs));
1551
+ }
1552
+ if (opts.text) {
1553
+ await page.getByText(opts.text).first().waitFor({
1554
+ state: "visible",
1555
+ timeout
1556
+ });
1557
+ }
1558
+ if (opts.textGone) {
1559
+ await page.getByText(opts.textGone).first().waitFor({
1560
+ state: "hidden",
1561
+ timeout
1562
+ });
1563
+ }
1564
+ if (opts.selector) {
1565
+ const selector = String(opts.selector).trim();
1566
+ if (selector) {
1567
+ await page.locator(selector).first().waitFor({ state: "visible", timeout });
1568
+ }
1569
+ }
1570
+ if (opts.url) {
1571
+ const url = String(opts.url).trim();
1572
+ if (url) {
1573
+ await page.waitForURL(url, { timeout });
1574
+ }
1575
+ }
1576
+ if (opts.loadState) {
1577
+ await page.waitForLoadState(opts.loadState, { timeout });
1578
+ }
1579
+ if (opts.fn) {
1580
+ const fn = String(opts.fn).trim();
1581
+ if (fn) {
1582
+ await page.waitForFunction(fn, { timeout });
1583
+ }
1584
+ }
1585
+ }
1586
+ async function takeScreenshotViaPlaywright(opts) {
1587
+ const page = await getPageForTargetId(opts);
1588
+ ensurePageState(page);
1589
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1590
+ const type = opts.type ?? "png";
1591
+ if (opts.ref) {
1592
+ if (opts.fullPage) {
1593
+ throw new Error("fullPage is not supported for element screenshots");
1594
+ }
1595
+ const locator = refLocator(page, opts.ref);
1596
+ const buffer2 = await locator.screenshot({ type });
1597
+ return { buffer: buffer2 };
1598
+ }
1599
+ if (opts.element) {
1600
+ if (opts.fullPage) {
1601
+ throw new Error("fullPage is not supported for element screenshots");
1602
+ }
1603
+ const locator = page.locator(opts.element).first();
1604
+ const buffer2 = await locator.screenshot({ type });
1605
+ return { buffer: buffer2 };
1606
+ }
1607
+ const buffer = await page.screenshot({
1608
+ type,
1609
+ fullPage: Boolean(opts.fullPage)
1610
+ });
1611
+ return { buffer };
1612
+ }
1613
+ async function screenshotWithLabelsViaPlaywright(opts) {
1614
+ const page = await getPageForTargetId(opts);
1615
+ ensurePageState(page);
1616
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1617
+ const type = opts.type ?? "png";
1618
+ const maxLabels = typeof opts.maxLabels === "number" && Number.isFinite(opts.maxLabels) ? Math.max(1, Math.floor(opts.maxLabels)) : 150;
1619
+ const viewport = await page.evaluate(() => ({
1620
+ scrollX: window.scrollX || 0,
1621
+ scrollY: window.scrollY || 0,
1622
+ width: window.innerWidth || 0,
1623
+ height: window.innerHeight || 0
1624
+ }));
1625
+ const refs = Object.keys(opts.refs ?? {});
1626
+ const boxes = [];
1627
+ let skipped = 0;
1628
+ for (const ref of refs) {
1629
+ if (boxes.length >= maxLabels) {
1630
+ skipped += 1;
1631
+ continue;
1632
+ }
1633
+ try {
1634
+ const box = await refLocator(page, ref).boundingBox();
1635
+ if (!box) {
1636
+ skipped += 1;
1637
+ continue;
1638
+ }
1639
+ const x0 = box.x;
1640
+ const y0 = box.y;
1641
+ const x1 = box.x + box.width;
1642
+ const y1 = box.y + box.height;
1643
+ const vx0 = viewport.scrollX;
1644
+ const vy0 = viewport.scrollY;
1645
+ const vx1 = viewport.scrollX + viewport.width;
1646
+ const vy1 = viewport.scrollY + viewport.height;
1647
+ if (x1 < vx0 || x0 > vx1 || y1 < vy0 || y0 > vy1) {
1648
+ skipped += 1;
1649
+ continue;
1650
+ }
1651
+ boxes.push({
1652
+ ref,
1653
+ x: x0 - viewport.scrollX,
1654
+ y: y0 - viewport.scrollY,
1655
+ w: Math.max(1, box.width),
1656
+ h: Math.max(1, box.height)
1657
+ });
1658
+ } catch {
1659
+ skipped += 1;
1660
+ }
1661
+ }
1662
+ try {
1663
+ if (boxes.length > 0) {
1664
+ await page.evaluate((labels) => {
1665
+ const existing = document.querySelectorAll("[data-agenticmail-labels]");
1666
+ existing.forEach((el) => el.remove());
1667
+ const root = document.createElement("div");
1668
+ root.setAttribute("data-agenticmail-labels", "1");
1669
+ root.style.position = "fixed";
1670
+ root.style.left = "0";
1671
+ root.style.top = "0";
1672
+ root.style.zIndex = "2147483647";
1673
+ root.style.pointerEvents = "none";
1674
+ root.style.fontFamily = '"SF Mono","SFMono-Regular",Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace';
1675
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
1676
+ for (const label of labels) {
1677
+ const box = document.createElement("div");
1678
+ box.setAttribute("data-agenticmail-labels", "1");
1679
+ box.style.position = "absolute";
1680
+ box.style.left = `${label.x}px`;
1681
+ box.style.top = `${label.y}px`;
1682
+ box.style.width = `${label.w}px`;
1683
+ box.style.height = `${label.h}px`;
1684
+ box.style.border = "2px solid #ffb020";
1685
+ box.style.boxSizing = "border-box";
1686
+ const tag = document.createElement("div");
1687
+ tag.setAttribute("data-agenticmail-labels", "1");
1688
+ tag.textContent = label.ref;
1689
+ tag.style.position = "absolute";
1690
+ tag.style.left = `${label.x}px`;
1691
+ tag.style.top = `${clamp(label.y - 18, 0, 2e4)}px`;
1692
+ tag.style.background = "#ffb020";
1693
+ tag.style.color = "#1a1a1a";
1694
+ tag.style.fontSize = "12px";
1695
+ tag.style.lineHeight = "14px";
1696
+ tag.style.padding = "1px 4px";
1697
+ tag.style.borderRadius = "3px";
1698
+ tag.style.boxShadow = "0 1px 2px rgba(0,0,0,0.35)";
1699
+ tag.style.whiteSpace = "nowrap";
1700
+ root.appendChild(box);
1701
+ root.appendChild(tag);
1702
+ }
1703
+ document.documentElement.appendChild(root);
1704
+ }, boxes);
1705
+ }
1706
+ const buffer = await page.screenshot({ type });
1707
+ return { buffer, labels: boxes.length, skipped };
1708
+ } finally {
1709
+ await page.evaluate(() => {
1710
+ const existing = document.querySelectorAll("[data-agenticmail-labels]");
1711
+ existing.forEach((el) => el.remove());
1712
+ }).catch(() => {
1713
+ });
1714
+ }
1715
+ }
1716
+ async function setInputFilesViaPlaywright(opts) {
1717
+ const page = await getPageForTargetId(opts);
1718
+ ensurePageState(page);
1719
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1720
+ if (!opts.paths.length) {
1721
+ throw new Error("paths are required");
1722
+ }
1723
+ const inputRef = typeof opts.inputRef === "string" ? opts.inputRef.trim() : "";
1724
+ const element = typeof opts.element === "string" ? opts.element.trim() : "";
1725
+ if (inputRef && element) {
1726
+ throw new Error("inputRef and element are mutually exclusive");
1727
+ }
1728
+ if (!inputRef && !element) {
1729
+ throw new Error("inputRef or element is required");
1730
+ }
1731
+ const locator = inputRef ? refLocator(page, inputRef) : page.locator(element).first();
1732
+ try {
1733
+ await locator.setInputFiles(opts.paths);
1734
+ } catch (err) {
1735
+ throw toAIFriendlyError(err, inputRef || element);
1736
+ }
1737
+ try {
1738
+ const handle = await locator.elementHandle();
1739
+ if (handle) {
1740
+ await handle.evaluate((el) => {
1741
+ el.dispatchEvent(new Event("input", { bubbles: true }));
1742
+ el.dispatchEvent(new Event("change", { bubbles: true }));
1743
+ });
1744
+ }
1745
+ } catch {
1746
+ }
1747
+ }
1748
+ async function mouseClickViaPlaywright(opts) {
1749
+ const page = await getPageForTargetId({
1750
+ cdpUrl: opts.cdpUrl,
1751
+ targetId: opts.targetId
1752
+ });
1753
+ ensurePageState(page);
1754
+ const button = opts.button || "left";
1755
+ if (opts.doubleClick) {
1756
+ await page.mouse.dblclick(opts.x, opts.y, { button });
1757
+ } else {
1758
+ await page.mouse.click(opts.x, opts.y, { button });
1759
+ }
1760
+ }
1761
+ async function scrollViaPlaywright(opts) {
1762
+ const page = await getPageForTargetId({
1763
+ cdpUrl: opts.cdpUrl,
1764
+ targetId: opts.targetId
1765
+ });
1766
+ ensurePageState(page);
1767
+ const deltaX = opts.deltaX ?? 0;
1768
+ const deltaY = opts.deltaY ?? 0;
1769
+ await page.evaluate(({ dx, dy }) => {
1770
+ window.scrollBy({ left: dx, top: dy, behavior: "smooth" });
1771
+ }, { dx: deltaX, dy: deltaY });
1772
+ await page.waitForTimeout(300);
1773
+ }
1774
+
1775
+ // src/browser/pw-tools-core.responses.ts
1776
+ function matchUrlPattern(pattern, url) {
1777
+ const p = pattern.trim();
1778
+ if (!p) {
1779
+ return false;
1780
+ }
1781
+ if (p === url) {
1782
+ return true;
1783
+ }
1784
+ if (p.includes("*")) {
1785
+ const escaped = p.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
1786
+ const regex = new RegExp(`^${escaped.replace(/\*\*/g, ".*").replace(/\*/g, ".*")}$`);
1787
+ return regex.test(url);
1788
+ }
1789
+ return url.includes(p);
1790
+ }
1791
+ async function responseBodyViaPlaywright(opts) {
1792
+ const pattern = String(opts.url ?? "").trim();
1793
+ if (!pattern) {
1794
+ throw new Error("url is required");
1795
+ }
1796
+ const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
1797
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
1798
+ const page = await getPageForTargetId(opts);
1799
+ ensurePageState(page);
1800
+ const promise = new Promise((resolve, reject) => {
1801
+ let done = false;
1802
+ let timer;
1803
+ let handler;
1804
+ const cleanup = () => {
1805
+ if (timer) {
1806
+ clearTimeout(timer);
1807
+ }
1808
+ timer = void 0;
1809
+ if (handler) {
1810
+ page.off("response", handler);
1811
+ }
1812
+ };
1813
+ handler = (resp2) => {
1814
+ if (done) {
1815
+ return;
1816
+ }
1817
+ const r = resp2;
1818
+ const u = r.url?.() || "";
1819
+ if (!matchUrlPattern(pattern, u)) {
1820
+ return;
1821
+ }
1822
+ done = true;
1823
+ cleanup();
1824
+ resolve(resp2);
1825
+ };
1826
+ page.on("response", handler);
1827
+ timer = setTimeout(() => {
1828
+ if (done) {
1829
+ return;
1830
+ }
1831
+ done = true;
1832
+ cleanup();
1833
+ reject(
1834
+ new Error(
1835
+ `Response not found for url pattern "${pattern}". Run '${formatCliCommand("agenticmail browser requests")}' to inspect recent network activity.`
1836
+ )
1837
+ );
1838
+ }, timeout);
1839
+ });
1840
+ const resp = await promise;
1841
+ const url = resp.url?.() || "";
1842
+ const status = resp.status?.();
1843
+ const headers = resp.headers?.();
1844
+ let bodyText = "";
1845
+ try {
1846
+ if (typeof resp.text === "function") {
1847
+ bodyText = await resp.text();
1848
+ } else if (typeof resp.body === "function") {
1849
+ const buf = await resp.body();
1850
+ bodyText = new TextDecoder("utf-8").decode(buf);
1851
+ }
1852
+ } catch (err) {
1853
+ throw new Error(`Failed to read response body for "${url}": ${String(err)}`, { cause: err });
1854
+ }
1855
+ const trimmed = bodyText.length > maxChars ? bodyText.slice(0, maxChars) : bodyText;
1856
+ return {
1857
+ url,
1858
+ status,
1859
+ headers,
1860
+ body: trimmed,
1861
+ truncated: bodyText.length > maxChars ? true : void 0
1862
+ };
1863
+ }
1864
+
1865
+ // src/browser/pw-tools-core.snapshot.ts
1866
+ async function snapshotAriaViaPlaywright(opts) {
1867
+ const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
1868
+ const page = await getPageForTargetId({
1869
+ cdpUrl: opts.cdpUrl,
1870
+ targetId: opts.targetId
1871
+ });
1872
+ ensurePageState(page);
1873
+ const session = await page.context().newCDPSession(page);
1874
+ try {
1875
+ await session.send("Accessibility.enable").catch(() => {
1876
+ });
1877
+ const res = await session.send("Accessibility.getFullAXTree");
1878
+ const nodes = Array.isArray(res?.nodes) ? res.nodes : [];
1879
+ return { nodes: formatAriaSnapshot(nodes, limit) };
1880
+ } finally {
1881
+ await session.detach().catch(() => {
1882
+ });
1883
+ }
1884
+ }
1885
+ async function snapshotAiViaPlaywright(opts) {
1886
+ const page = await getPageForTargetId({
1887
+ cdpUrl: opts.cdpUrl,
1888
+ targetId: opts.targetId
1889
+ });
1890
+ ensurePageState(page);
1891
+ const maybe = page;
1892
+ if (!maybe._snapshotForAI) {
1893
+ throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core.");
1894
+ }
1895
+ const result = await maybe._snapshotForAI({
1896
+ timeout: Math.max(500, Math.min(6e4, Math.floor(opts.timeoutMs ?? 5e3))),
1897
+ track: "response"
1898
+ });
1899
+ let snapshot = String(result?.full ?? "");
1900
+ const maxChars = opts.maxChars;
1901
+ const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
1902
+ let truncated = false;
1903
+ if (limit && snapshot.length > limit) {
1904
+ snapshot = `${snapshot.slice(0, limit)}
1905
+
1906
+ [...TRUNCATED - page too large]`;
1907
+ truncated = true;
1908
+ }
1909
+ const built = buildRoleSnapshotFromAiSnapshot(snapshot);
1910
+ storeRoleRefsForTarget({
1911
+ page,
1912
+ cdpUrl: opts.cdpUrl,
1913
+ targetId: opts.targetId,
1914
+ refs: built.refs,
1915
+ mode: "aria"
1916
+ });
1917
+ return truncated ? { snapshot, truncated, refs: built.refs } : { snapshot, refs: built.refs };
1918
+ }
1919
+ async function snapshotRoleViaPlaywright(opts) {
1920
+ const page = await getPageForTargetId({
1921
+ cdpUrl: opts.cdpUrl,
1922
+ targetId: opts.targetId
1923
+ });
1924
+ ensurePageState(page);
1925
+ if (opts.refsMode === "aria") {
1926
+ if (opts.selector?.trim() || opts.frameSelector?.trim()) {
1927
+ throw new Error("refs=aria does not support selector/frame snapshots yet.");
1928
+ }
1929
+ const maybe = page;
1930
+ if (!maybe._snapshotForAI) {
1931
+ throw new Error("refs=aria requires Playwright _snapshotForAI support.");
1932
+ }
1933
+ const result = await maybe._snapshotForAI({
1934
+ timeout: 5e3,
1935
+ track: "response"
1936
+ });
1937
+ const built2 = buildRoleSnapshotFromAiSnapshot(String(result?.full ?? ""), opts.options);
1938
+ storeRoleRefsForTarget({
1939
+ page,
1940
+ cdpUrl: opts.cdpUrl,
1941
+ targetId: opts.targetId,
1942
+ refs: built2.refs,
1943
+ mode: "aria"
1944
+ });
1945
+ return {
1946
+ snapshot: built2.snapshot,
1947
+ refs: built2.refs,
1948
+ stats: getRoleSnapshotStats(built2.snapshot, built2.refs)
1949
+ };
1950
+ }
1951
+ const frameSelector = opts.frameSelector?.trim() || "";
1952
+ const selector = opts.selector?.trim() || "";
1953
+ const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
1954
+ const ariaSnapshot = await locator.ariaSnapshot();
1955
+ const built = buildRoleSnapshotFromAriaSnapshot(String(ariaSnapshot ?? ""), opts.options);
1956
+ storeRoleRefsForTarget({
1957
+ page,
1958
+ cdpUrl: opts.cdpUrl,
1959
+ targetId: opts.targetId,
1960
+ refs: built.refs,
1961
+ frameSelector: frameSelector || void 0,
1962
+ mode: "role"
1963
+ });
1964
+ return {
1965
+ snapshot: built.snapshot,
1966
+ refs: built.refs,
1967
+ stats: getRoleSnapshotStats(built.snapshot, built.refs)
1968
+ };
1969
+ }
1970
+ async function navigateViaPlaywright(opts) {
1971
+ const url = String(opts.url ?? "").trim();
1972
+ if (!url) {
1973
+ throw new Error("url is required");
1974
+ }
1975
+ await assertBrowserNavigationAllowed({
1976
+ url,
1977
+ ...withBrowserNavigationPolicy(opts.ssrfPolicy)
1978
+ });
1979
+ const page = await getPageForTargetId(opts);
1980
+ ensurePageState(page);
1981
+ await page.goto(url, {
1982
+ timeout: Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4))
1983
+ });
1984
+ return { url: page.url() };
1985
+ }
1986
+ async function resizeViewportViaPlaywright(opts) {
1987
+ const page = await getPageForTargetId(opts);
1988
+ ensurePageState(page);
1989
+ await page.setViewportSize({
1990
+ width: Math.max(1, Math.floor(opts.width)),
1991
+ height: Math.max(1, Math.floor(opts.height))
1992
+ });
1993
+ }
1994
+ async function closePageViaPlaywright(opts) {
1995
+ const page = await getPageForTargetId(opts);
1996
+ ensurePageState(page);
1997
+ await page.close();
1998
+ }
1999
+ async function pdfViaPlaywright(opts) {
2000
+ const page = await getPageForTargetId(opts);
2001
+ ensurePageState(page);
2002
+ const buffer = await page.pdf({ printBackground: true });
2003
+ return { buffer };
2004
+ }
2005
+
2006
+ // src/browser/pw-tools-core.state.ts
2007
+ import { devices as playwrightDevices } from "playwright-core";
2008
+ async function withCdpSession(page, fn) {
2009
+ const session = await page.context().newCDPSession(page);
2010
+ try {
2011
+ return await fn(session);
2012
+ } finally {
2013
+ await session.detach().catch(() => {
2014
+ });
2015
+ }
2016
+ }
2017
+ async function setOfflineViaPlaywright(opts) {
2018
+ const page = await getPageForTargetId(opts);
2019
+ ensurePageState(page);
2020
+ await page.context().setOffline(Boolean(opts.offline));
2021
+ }
2022
+ async function setExtraHTTPHeadersViaPlaywright(opts) {
2023
+ const page = await getPageForTargetId(opts);
2024
+ ensurePageState(page);
2025
+ await page.context().setExtraHTTPHeaders(opts.headers);
2026
+ }
2027
+ async function setHttpCredentialsViaPlaywright(opts) {
2028
+ const page = await getPageForTargetId(opts);
2029
+ ensurePageState(page);
2030
+ if (opts.clear) {
2031
+ await page.context().setHTTPCredentials(null);
2032
+ return;
2033
+ }
2034
+ const username = String(opts.username ?? "");
2035
+ const password = String(opts.password ?? "");
2036
+ if (!username) {
2037
+ throw new Error("username is required (or set clear=true)");
2038
+ }
2039
+ await page.context().setHTTPCredentials({ username, password });
2040
+ }
2041
+ async function setGeolocationViaPlaywright(opts) {
2042
+ const page = await getPageForTargetId(opts);
2043
+ ensurePageState(page);
2044
+ const context = page.context();
2045
+ if (opts.clear) {
2046
+ await context.setGeolocation(null);
2047
+ await context.clearPermissions().catch(() => {
2048
+ });
2049
+ return;
2050
+ }
2051
+ if (typeof opts.latitude !== "number" || typeof opts.longitude !== "number") {
2052
+ throw new Error("latitude and longitude are required (or set clear=true)");
2053
+ }
2054
+ await context.setGeolocation({
2055
+ latitude: opts.latitude,
2056
+ longitude: opts.longitude,
2057
+ accuracy: typeof opts.accuracy === "number" ? opts.accuracy : void 0
2058
+ });
2059
+ const origin = opts.origin?.trim() || (() => {
2060
+ try {
2061
+ return new URL(page.url()).origin;
2062
+ } catch {
2063
+ return "";
2064
+ }
2065
+ })();
2066
+ if (origin) {
2067
+ await context.grantPermissions(["geolocation"], { origin }).catch(() => {
2068
+ });
2069
+ }
2070
+ }
2071
+ async function emulateMediaViaPlaywright(opts) {
2072
+ const page = await getPageForTargetId(opts);
2073
+ ensurePageState(page);
2074
+ await page.emulateMedia({ colorScheme: opts.colorScheme });
2075
+ }
2076
+ async function setLocaleViaPlaywright(opts) {
2077
+ const page = await getPageForTargetId(opts);
2078
+ ensurePageState(page);
2079
+ const locale = String(opts.locale ?? "").trim();
2080
+ if (!locale) {
2081
+ throw new Error("locale is required");
2082
+ }
2083
+ await withCdpSession(page, async (session) => {
2084
+ try {
2085
+ await session.send("Emulation.setLocaleOverride", { locale });
2086
+ } catch (err) {
2087
+ if (String(err).includes("Another locale override is already in effect")) {
2088
+ return;
2089
+ }
2090
+ throw err;
2091
+ }
2092
+ });
2093
+ }
2094
+ async function setTimezoneViaPlaywright(opts) {
2095
+ const page = await getPageForTargetId(opts);
2096
+ ensurePageState(page);
2097
+ const timezoneId = String(opts.timezoneId ?? "").trim();
2098
+ if (!timezoneId) {
2099
+ throw new Error("timezoneId is required");
2100
+ }
2101
+ await withCdpSession(page, async (session) => {
2102
+ try {
2103
+ await session.send("Emulation.setTimezoneOverride", { timezoneId });
2104
+ } catch (err) {
2105
+ const msg = String(err);
2106
+ if (msg.includes("Timezone override is already in effect")) {
2107
+ return;
2108
+ }
2109
+ if (msg.includes("Invalid timezone")) {
2110
+ throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
2111
+ }
2112
+ throw err;
2113
+ }
2114
+ });
2115
+ }
2116
+ async function setDeviceViaPlaywright(opts) {
2117
+ const page = await getPageForTargetId(opts);
2118
+ ensurePageState(page);
2119
+ const name = String(opts.name ?? "").trim();
2120
+ if (!name) {
2121
+ throw new Error("device name is required");
2122
+ }
2123
+ const descriptor = playwrightDevices[name];
2124
+ if (!descriptor) {
2125
+ throw new Error(`Unknown device "${name}".`);
2126
+ }
2127
+ if (descriptor.viewport) {
2128
+ await page.setViewportSize({
2129
+ width: descriptor.viewport.width,
2130
+ height: descriptor.viewport.height
2131
+ });
2132
+ }
2133
+ await withCdpSession(page, async (session) => {
2134
+ if (descriptor.userAgent || descriptor.locale) {
2135
+ await session.send("Emulation.setUserAgentOverride", {
2136
+ userAgent: descriptor.userAgent ?? "",
2137
+ acceptLanguage: descriptor.locale ?? void 0
2138
+ });
2139
+ }
2140
+ if (descriptor.viewport) {
2141
+ await session.send("Emulation.setDeviceMetricsOverride", {
2142
+ mobile: Boolean(descriptor.isMobile),
2143
+ width: descriptor.viewport.width,
2144
+ height: descriptor.viewport.height,
2145
+ deviceScaleFactor: descriptor.deviceScaleFactor ?? 1,
2146
+ screenWidth: descriptor.viewport.width,
2147
+ screenHeight: descriptor.viewport.height
2148
+ });
2149
+ }
2150
+ if (descriptor.hasTouch) {
2151
+ await session.send("Emulation.setTouchEmulationEnabled", {
2152
+ enabled: true
2153
+ });
2154
+ }
2155
+ });
2156
+ }
2157
+
2158
+ // src/browser/pw-tools-core.storage.ts
2159
+ async function cookiesGetViaPlaywright(opts) {
2160
+ const page = await getPageForTargetId(opts);
2161
+ ensurePageState(page);
2162
+ const cookies = await page.context().cookies();
2163
+ return { cookies };
2164
+ }
2165
+ async function cookiesSetViaPlaywright(opts) {
2166
+ const page = await getPageForTargetId(opts);
2167
+ ensurePageState(page);
2168
+ const cookie = opts.cookie;
2169
+ if (!cookie.name || cookie.value === void 0) {
2170
+ throw new Error("cookie name and value are required");
2171
+ }
2172
+ const hasUrl = typeof cookie.url === "string" && cookie.url.trim();
2173
+ const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() && typeof cookie.path === "string" && cookie.path.trim();
2174
+ if (!hasUrl && !hasDomainPath) {
2175
+ throw new Error("cookie requires url, or domain+path");
2176
+ }
2177
+ await page.context().addCookies([cookie]);
2178
+ }
2179
+ async function cookiesClearViaPlaywright(opts) {
2180
+ const page = await getPageForTargetId(opts);
2181
+ ensurePageState(page);
2182
+ await page.context().clearCookies();
2183
+ }
2184
+ async function storageGetViaPlaywright(opts) {
2185
+ const page = await getPageForTargetId(opts);
2186
+ ensurePageState(page);
2187
+ const kind = opts.kind;
2188
+ const key = typeof opts.key === "string" ? opts.key : void 0;
2189
+ const values = await page.evaluate(
2190
+ ({ kind: kind2, key: key2 }) => {
2191
+ const store = kind2 === "session" ? window.sessionStorage : window.localStorage;
2192
+ if (key2) {
2193
+ const value = store.getItem(key2);
2194
+ return value === null ? {} : { [key2]: value };
2195
+ }
2196
+ const out = {};
2197
+ for (let i = 0; i < store.length; i += 1) {
2198
+ const k = store.key(i);
2199
+ if (!k) {
2200
+ continue;
2201
+ }
2202
+ const v = store.getItem(k);
2203
+ if (v !== null) {
2204
+ out[k] = v;
2205
+ }
2206
+ }
2207
+ return out;
2208
+ },
2209
+ { kind, key }
2210
+ );
2211
+ return { values: values ?? {} };
2212
+ }
2213
+ async function storageSetViaPlaywright(opts) {
2214
+ const page = await getPageForTargetId(opts);
2215
+ ensurePageState(page);
2216
+ const key = String(opts.key ?? "");
2217
+ if (!key) {
2218
+ throw new Error("key is required");
2219
+ }
2220
+ await page.evaluate(
2221
+ ({ kind, key: k, value }) => {
2222
+ const store = kind === "session" ? window.sessionStorage : window.localStorage;
2223
+ store.setItem(k, value);
2224
+ },
2225
+ { kind: opts.kind, key, value: String(opts.value ?? "") }
2226
+ );
2227
+ }
2228
+ async function storageClearViaPlaywright(opts) {
2229
+ const page = await getPageForTargetId(opts);
2230
+ ensurePageState(page);
2231
+ await page.evaluate(
2232
+ ({ kind }) => {
2233
+ const store = kind === "session" ? window.sessionStorage : window.localStorage;
2234
+ store.clear();
2235
+ },
2236
+ { kind: opts.kind }
2237
+ );
2238
+ }
2239
+
2240
+ // src/browser/pw-tools-core.trace.ts
2241
+ async function traceStartViaPlaywright(opts) {
2242
+ const page = await getPageForTargetId(opts);
2243
+ const context = page.context();
2244
+ const ctxState = ensureContextState(context);
2245
+ if (ctxState.traceActive) {
2246
+ throw new Error("Trace already running. Stop the current trace before starting a new one.");
2247
+ }
2248
+ await context.tracing.start({
2249
+ screenshots: opts.screenshots ?? true,
2250
+ snapshots: opts.snapshots ?? true,
2251
+ sources: opts.sources ?? false
2252
+ });
2253
+ ctxState.traceActive = true;
2254
+ }
2255
+ async function traceStopViaPlaywright(opts) {
2256
+ const page = await getPageForTargetId(opts);
2257
+ const context = page.context();
2258
+ const ctxState = ensureContextState(context);
2259
+ if (!ctxState.traceActive) {
2260
+ throw new Error("No active trace. Start a trace before stopping it.");
2261
+ }
2262
+ await context.tracing.stop({ path: opts.path });
2263
+ ctxState.traceActive = false;
2264
+ }
2265
+
2266
+ // src/browser/pw-ai.ts
2267
+ markPwAiLoaded();
2268
+ export {
2269
+ armDialogViaPlaywright,
2270
+ armFileUploadViaPlaywright,
2271
+ clickViaPlaywright,
2272
+ closePageByTargetIdViaPlaywright,
2273
+ closePageViaPlaywright,
2274
+ closePlaywrightBrowserConnection,
2275
+ cookiesClearViaPlaywright,
2276
+ cookiesGetViaPlaywright,
2277
+ cookiesSetViaPlaywright,
2278
+ createPageViaPlaywright,
2279
+ downloadViaPlaywright,
2280
+ dragViaPlaywright,
2281
+ emulateMediaViaPlaywright,
2282
+ ensurePageState,
2283
+ evaluateViaPlaywright,
2284
+ fillFormViaPlaywright,
2285
+ focusPageByTargetIdViaPlaywright,
2286
+ forceDisconnectPlaywrightForTarget,
2287
+ getConsoleMessagesViaPlaywright,
2288
+ getNetworkRequestsViaPlaywright,
2289
+ getPageErrorsViaPlaywright,
2290
+ getPageForTargetId,
2291
+ highlightViaPlaywright,
2292
+ hoverViaPlaywright,
2293
+ listPagesViaPlaywright,
2294
+ mouseClickViaPlaywright,
2295
+ navigateViaPlaywright,
2296
+ pdfViaPlaywright,
2297
+ pressKeyViaPlaywright,
2298
+ refLocator,
2299
+ resizeViewportViaPlaywright,
2300
+ responseBodyViaPlaywright,
2301
+ screenshotWithLabelsViaPlaywright,
2302
+ scrollIntoViewViaPlaywright,
2303
+ scrollViaPlaywright,
2304
+ selectOptionViaPlaywright,
2305
+ setDeviceViaPlaywright,
2306
+ setExtraHTTPHeadersViaPlaywright,
2307
+ setGeolocationViaPlaywright,
2308
+ setHttpCredentialsViaPlaywright,
2309
+ setInputFilesViaPlaywright,
2310
+ setLocaleViaPlaywright,
2311
+ setOfflineViaPlaywright,
2312
+ setTimezoneViaPlaywright,
2313
+ snapshotAiViaPlaywright,
2314
+ snapshotAriaViaPlaywright,
2315
+ snapshotRoleViaPlaywright,
2316
+ storageClearViaPlaywright,
2317
+ storageGetViaPlaywright,
2318
+ storageSetViaPlaywright,
2319
+ takeScreenshotViaPlaywright,
2320
+ traceStartViaPlaywright,
2321
+ traceStopViaPlaywright,
2322
+ typeViaPlaywright,
2323
+ waitForDownloadViaPlaywright,
2324
+ waitForViaPlaywright
2325
+ };