pi-agent-browser-native 0.2.47 → 0.2.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +46 -19
  2. package/README.md +38 -15
  3. package/docs/ARCHITECTURE.md +10 -10
  4. package/docs/COMMAND_REFERENCE.md +35 -21
  5. package/docs/ELECTRON.md +3 -3
  6. package/docs/RELEASE.md +28 -19
  7. package/docs/REQUIREMENTS.md +1 -1
  8. package/docs/SUPPORT_MATRIX.md +34 -106
  9. package/docs/TOOL_CONTRACT.md +23 -21
  10. package/extensions/agent-browser/index.ts +13 -4
  11. package/extensions/agent-browser/lib/config.ts +2 -0
  12. package/extensions/agent-browser/lib/input-modes/job.ts +138 -62
  13. package/extensions/agent-browser/lib/input-modes/params.ts +2 -2
  14. package/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.ts +44 -0
  15. package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +42 -19
  16. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +6 -4
  17. package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +18 -9
  18. package/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.ts +158 -0
  19. package/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.ts +116 -0
  20. package/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.ts +147 -0
  21. package/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.ts +183 -0
  22. package/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.ts +58 -0
  23. package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +19 -653
  24. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +1 -6
  25. package/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.ts +8 -0
  26. package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +1 -0
  27. package/extensions/agent-browser/lib/pi-tool-rendering.ts +34 -19
  28. package/extensions/agent-browser/lib/playbook.ts +4 -4
  29. package/extensions/agent-browser/lib/results/action-recommendations.ts +3 -3
  30. package/extensions/agent-browser/lib/web-search.ts +11 -4
  31. package/package.json +4 -4
  32. package/scripts/agent-browser-capability-baseline.mjs +6 -3
  33. package/scripts/doctor.mjs +11 -10
  34. package/scripts/platform-smoke.mjs +1 -1
@@ -1,12 +1,16 @@
1
- import { copyFile, mkdir, stat, writeFile } from "node:fs/promises";
2
- import { dirname, extname, isAbsolute, resolve } from "node:path";
1
+ import { copyFile, mkdir } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
3
 
4
4
  import { launchElectronApp, type ElectronLaunchSuccess } from "../../electron/launch.js";
5
5
  import { pathExists } from "../../fs-utils.js";
6
6
  import { getCompiledSemanticActionSessionPrefix, type CompiledAgentBrowserSemanticAction } from "../../input-modes.js";
7
- import { SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS } from "../../process.js";
7
+ import { tryDirectAnchorDownload } from "./prepare/direct-anchor-download.js";
8
+ import { tryNetworkRequestsPageFilter } from "./prepare/network-page-filter.js";
9
+ import { tryContainerScroll, tryPageScrollTo } from "./prepare/scroll-shims.js";
10
+ import { trySnapshotFilter } from "./prepare/snapshot-filter.js";
11
+ import { getWaitAwareProcessTimeoutMs } from "./prepare/wait-timeouts.js";
12
+ import { getPersistentSessionArtifactStore } from "./session-artifacts.js";
8
13
  import { buildAgentBrowserResultCategoryDetails } from "../../results.js";
9
- import { buildSnapshotPresentation } from "../../results/snapshot.js";
10
14
  import { buildSessionAwareStaleRefNextActions, buildSessionTabRecoveryNextActions } from "../../results/recovery-next-actions.js";
11
15
  import { resolveVisibleRefActionFromSnapshot } from "../../results/selector-recovery.js";
12
16
  import { extractRefSnapshotFromData, type SessionRefSnapshot, type SessionTabTarget } from "../../session-page-state.js";
@@ -30,17 +34,18 @@ import {
30
34
  runSessionCommandData,
31
35
  shouldPinSessionTabForCommand,
32
36
  } from "./session-state.js";
33
- import { isRecord } from "../../parsing.js";
34
37
  import { parseBatchStdinJsonArray, parseValidBatchStepEntries } from "../batch-stdin.js";
35
38
  import { buildElectronHostFailureResult, getElectronLaunchFailureCategory, redactRecoveryHint } from "./final-result.js";
36
39
  import { prepareClickDispatchProbe } from "./click-dispatch.js";
37
- import { buildScrollNoopNextActions, collectScrollPositionSnapshot, validateQaAttachedPrecondition } from "./diagnostics.js";
40
+ import { collectScrollPositionSnapshot, validateQaAttachedPrecondition } from "./diagnostics.js";
41
+ import { getScreenshotPathTokenIndex } from "./artifact-paths.js";
38
42
  import { findRequestedArtifactCloseViolation } from "./prompt-guards.js";
39
43
 
40
44
  import type {
41
45
  AgentBrowserToolResult,
42
46
  BrowserRunInputFields,
43
47
  BrowserRunOptions,
48
+ BrowserRunStatePatch,
44
49
  PreparedAgentBrowserArgs,
45
50
  PreparedBrowserRun,
46
51
  PrepareBrowserRunResult,
@@ -50,10 +55,6 @@ import type {
50
55
  StaleRefPreflight,
51
56
  } from "./types.js";
52
57
 
53
- const DIRECT_ANCHOR_DOWNLOAD_MAX_BYTES = 2 * 1024 * 1024;
54
- const SCREENSHOT_VALUE_FLAGS = new Set(["--screenshot-dir", "--screenshot-format", "--screenshot-quality"]);
55
- const SCREENSHOT_IMAGE_EXTENSIONS = new Set([".jpeg", ".jpg", ".png", ".webp"]);
56
-
57
58
  export function normalizeRunInput(input: BrowserRunOptions["input"]): BrowserRunInputFields {
58
59
  const base = { redactedArgs: input.redactedArgs, toolArgs: input.toolArgs, toolStdin: input.toolStdin };
59
60
  switch (input.kind) {
@@ -79,46 +80,6 @@ export function buildInvocationPreview(effectiveArgs: string[]): string {
79
80
  return preview.length > 120 ? `${preview.slice(0, 117)}...` : preview;
80
81
  }
81
82
 
82
- function isImagePathToken(token: string): boolean {
83
- const extension = extname(token).toLowerCase();
84
- return SCREENSHOT_IMAGE_EXTENSIONS.has(extension);
85
- }
86
-
87
- export function getScreenshotPathTokenIndex(commandTokens: string[]): number | undefined {
88
- if (commandTokens[0] !== "screenshot") {
89
- return undefined;
90
- }
91
-
92
- const positionalIndices: number[] = [];
93
- for (let index = 1; index < commandTokens.length; index += 1) {
94
- const token = commandTokens[index];
95
- if (token === "--") {
96
- for (let positionalIndex = index + 1; positionalIndex < commandTokens.length; positionalIndex += 1) {
97
- positionalIndices.push(positionalIndex);
98
- }
99
- break;
100
- }
101
- if (token.startsWith("-")) {
102
- const normalizedToken = token.split("=", 1)[0] ?? token;
103
- if (SCREENSHOT_VALUE_FLAGS.has(normalizedToken) && !token.includes("=")) {
104
- index += 1;
105
- }
106
- continue;
107
- }
108
- positionalIndices.push(index);
109
- }
110
-
111
- if (positionalIndices.length === 0) {
112
- return undefined;
113
- }
114
- const candidateIndex = positionalIndices[positionalIndices.length - 1];
115
- const candidate = commandTokens[candidateIndex];
116
- if (positionalIndices.length >= 2 || isImagePathToken(candidate) || isAbsolute(candidate) || candidate.startsWith("./") || candidate.startsWith("../")) {
117
- return candidateIndex;
118
- }
119
- return undefined;
120
- }
121
-
122
83
  function getArtifactParentPathTokenIndex(commandTokens: string[]): number | undefined {
123
84
  if (commandTokens[0] === "download" && commandTokens.length >= 3) return 2;
124
85
  if (commandTokens[0] === "pdf" && commandTokens.length >= 2) return 1;
@@ -139,123 +100,6 @@ async function ensureArtifactParentDirectory(commandTokens: string[], cwd: strin
139
100
  await mkdir(dirname(resolve(cwd, requestedPath)), { recursive: true });
140
101
  }
141
102
 
142
- function getDirectDownloadRequest(commandTokens: string[]): { path: string; selector: string } | undefined {
143
- if (commandTokens[0] !== "download" || commandTokens.length !== 3) return undefined;
144
- const selector = commandTokens[1];
145
- const path = commandTokens[2];
146
- if (!selector || !path || selector.startsWith("@")) return undefined;
147
- return { path, selector };
148
- }
149
-
150
- function buildAnchorDownloadProbe(selector: string): string {
151
- return `(async () => {\n const selector = ${JSON.stringify(selector)};\n const maxBytes = ${DIRECT_ANCHOR_DOWNLOAD_MAX_BYTES};\n const isLoopbackHttpUrl = (url) => (url.protocol === "http:" || url.protocol === "https:") && (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]");\n const element = document.querySelector(selector);\n const anchor = element?.closest?.("a[href]");\n const pageUrl = location.href;\n const page = new URL(pageUrl);\n if (!anchor) return { status: "no-anchor", pageUrl };\n const href = anchor.href;\n const anchorUrl = new URL(href, pageUrl);\n if (!isLoopbackHttpUrl(page)) return { download: anchor.getAttribute("download") || "", href, pageUrl, status: "not-loopback-page" };\n if (anchorUrl.origin !== page.origin) return { download: anchor.getAttribute("download") || "", href, pageUrl, status: "not-same-origin" };\n if (!isLoopbackHttpUrl(anchorUrl)) return { download: anchor.getAttribute("download") || "", href, pageUrl, status: "not-loopback-href" };\n const response = await fetch(anchorUrl.href, { credentials: "include", redirect: "manual" });\n if (!response.ok) return { download: anchor.getAttribute("download") || "", href, pageUrl, responseUrl: response.url, status: "fetch-failed", statusCode: response.status };\n const responseUrl = new URL(response.url);\n if (!isLoopbackHttpUrl(responseUrl) || responseUrl.origin !== page.origin) return { download: anchor.getAttribute("download") || "", href, pageUrl, responseUrl: response.url, status: "not-loopback-response" };\n const buffer = await response.arrayBuffer();\n if (buffer.byteLength > maxBytes) return { download: anchor.getAttribute("download") || "", href, pageUrl, responseUrl: response.url, sizeBytes: buffer.byteLength, status: "too-large" };\n const bytes = new Uint8Array(buffer);\n let binary = "";\n for (let index = 0; index < bytes.length; index += 32768) binary += String.fromCharCode(...bytes.subarray(index, index + 32768));\n return { bodyBase64: btoa(binary), contentType: response.headers.get("content-type") || "", download: anchor.getAttribute("download") || "", href, pageUrl, responseUrl: response.url, sizeBytes: buffer.byteLength, status: "fetched-anchor" };\n})()`;
152
- }
153
-
154
- function isLoopbackHttpUrl(url: URL): boolean {
155
- return (url.protocol === "http:" || url.protocol === "https:") && (url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]");
156
- }
157
-
158
- async function tryDirectAnchorDownload(options: {
159
- commandTokens: string[];
160
- compatibilityWorkaround?: CompatibilityWorkaround;
161
- cwd: string;
162
- effectiveArgs: string[];
163
- redactedArgs: string[];
164
- sessionMode: "auto" | "fresh";
165
- sessionName?: string;
166
- signal?: AbortSignal;
167
- usedImplicitSession: boolean;
168
- }): Promise<AgentBrowserToolResult | undefined> {
169
- const request = getDirectDownloadRequest(options.commandTokens);
170
- if (!request || !options.sessionName) return undefined;
171
- try {
172
- const probeData = await runSessionCommandData({
173
- args: ["eval", "--stdin"],
174
- cwd: options.cwd,
175
- sessionName: options.sessionName,
176
- signal: options.signal,
177
- stdin: buildAnchorDownloadProbe(request.selector),
178
- });
179
- const probe = isRecord(probeData) && isRecord(probeData.result) ? probeData.result : probeData;
180
- if (!isRecord(probe) || probe.status !== "fetched-anchor" || typeof probe.href !== "string" || typeof probe.pageUrl !== "string" || typeof probe.bodyBase64 !== "string") return undefined;
181
- const href = new URL(probe.href);
182
- const pageUrl = new URL(probe.pageUrl);
183
- const responseUrl = typeof probe.responseUrl === "string" ? new URL(probe.responseUrl) : href;
184
- if (!isLoopbackHttpUrl(pageUrl) || !isLoopbackHttpUrl(href) || !isLoopbackHttpUrl(responseUrl) || href.origin !== pageUrl.origin || responseUrl.origin !== pageUrl.origin) return undefined;
185
- const body = Buffer.from(probe.bodyBase64, "base64");
186
- if (body.byteLength > DIRECT_ANCHOR_DOWNLOAD_MAX_BYTES) return undefined;
187
- if (typeof probe.sizeBytes === "number" && probe.sizeBytes !== body.byteLength) return undefined;
188
- const absolutePath = resolve(options.cwd, request.path);
189
- await mkdir(dirname(absolutePath), { recursive: true });
190
- await writeFile(absolutePath, body);
191
- const fileStat = await stat(absolutePath);
192
- const mediaType = typeof probe.contentType === "string" && probe.contentType.length > 0 ? probe.contentType : undefined;
193
- const artifact = {
194
- absolutePath,
195
- artifactType: "download" as const,
196
- command: "download",
197
- cwd: options.cwd,
198
- exists: true,
199
- kind: "download" as const,
200
- mediaType,
201
- path: absolutePath,
202
- requestedPath: request.path,
203
- session: options.sessionName,
204
- sizeBytes: fileStat.size,
205
- status: "saved" as const,
206
- };
207
- const artifactVerification = {
208
- artifacts: [{
209
- absolutePath,
210
- exists: true,
211
- kind: "download" as const,
212
- mediaType,
213
- path: absolutePath,
214
- requestedPath: request.path,
215
- sizeBytes: fileStat.size,
216
- state: "verified" as const,
217
- status: "saved" as const,
218
- }],
219
- missingCount: 0,
220
- pendingCount: 0,
221
- unverifiedCount: 0,
222
- verified: true,
223
- verifiedCount: 1,
224
- };
225
- const savedFile = { command: "download" as const, kind: "download" as const, metadata: { download: probe.download, href: redactSensitiveText(href.href), method: "direct-anchor-fetch" }, path: absolutePath };
226
- return {
227
- content: [{
228
- type: "text",
229
- text: [
230
- `Download completed: ${absolutePath}`,
231
- `Requested path: ${request.path}`,
232
- `Source: ${redactSensitiveText(href.href)}`,
233
- `Size: ${fileStat.size} bytes`,
234
- "Method: direct anchor fetch before upstream download fallback.",
235
- ].join("\n"),
236
- }],
237
- details: {
238
- args: options.redactedArgs,
239
- artifacts: [artifact],
240
- artifactVerification,
241
- command: "download",
242
- compatibilityWorkaround: options.compatibilityWorkaround,
243
- downloadRecovery: { href: redactSensitiveText(href.href), method: "direct-anchor-fetch", selector: request.selector },
244
- effectiveArgs: options.effectiveArgs,
245
- savedFile,
246
- savedFilePath: absolutePath,
247
- sessionMode: options.sessionMode,
248
- ...buildAgentBrowserResultCategoryDetails({ artifacts: [artifact], args: options.effectiveArgs, command: "download", savedFile, succeeded: true }),
249
- ...buildSessionDetailFields(options.sessionName, options.usedImplicitSession),
250
- summary: `Download completed: ${absolutePath}`,
251
- },
252
- isError: false,
253
- };
254
- } catch {
255
- return undefined;
256
- }
257
- }
258
-
259
103
  async function normalizeScreenshotPathInTokens(commandTokens: string[], cwd: string): Promise<{
260
104
  request?: ScreenshotPathRequest;
261
105
  tokens: string[];
@@ -377,60 +221,6 @@ async function repairScreenshotData(options: {
377
221
 
378
222
  export { repairScreenshotData };
379
223
 
380
- function parseMillisecondsToken(token: string | undefined): number | undefined {
381
- if (token === undefined || !/^\d+$/.test(token)) {
382
- return undefined;
383
- }
384
- const parsed = Number(token);
385
- return Number.isSafeInteger(parsed) ? parsed : undefined;
386
- }
387
-
388
- function findWaitTimeoutMs(commandTokens: string[]): { timeoutMs: number; source: string } | undefined {
389
- if (commandTokens[0] !== "wait") {
390
- return undefined;
391
- }
392
- for (let index = 1; index < commandTokens.length; index += 1) {
393
- const token = commandTokens[index];
394
- if (token === "--timeout") {
395
- const timeoutMs = parseMillisecondsToken(commandTokens[index + 1]);
396
- return timeoutMs === undefined ? undefined : { source: "wait --timeout", timeoutMs };
397
- }
398
- if (token.startsWith("--timeout=")) {
399
- const timeoutMs = parseMillisecondsToken(token.slice("--timeout=".length));
400
- return timeoutMs === undefined ? undefined : { source: "wait --timeout", timeoutMs };
401
- }
402
- if (!token.startsWith("-")) {
403
- const timeoutMs = parseMillisecondsToken(token);
404
- if (timeoutMs !== undefined) {
405
- return { source: "wait", timeoutMs };
406
- }
407
- }
408
- }
409
- return undefined;
410
- }
411
-
412
- function buildIpcUnsafeWaitError(source: string, timeoutMs: number, batchStep?: number): string {
413
- const location = batchStep === undefined ? source : `batch step ${batchStep + 1} (${source})`;
414
- return `${location} requests ${timeoutMs}ms, but upstream agent-browser CLI calls must stay under its 30s IPC read timeout. Use ${SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS}ms or less per wait, split long waits into multiple tool calls, or use a page-specific shorter condition.`;
415
- }
416
-
417
- export function validateWaitIpcTimeoutContract(commandTokens: string[], stdin: string | undefined): string | undefined {
418
- const directWaitTimeout = findWaitTimeoutMs(commandTokens);
419
- if (directWaitTimeout && directWaitTimeout.timeoutMs > SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS) {
420
- return buildIpcUnsafeWaitError(directWaitTimeout.source, directWaitTimeout.timeoutMs);
421
- }
422
- if (commandTokens[0] !== "batch" || stdin === undefined) {
423
- return undefined;
424
- }
425
- for (const { index, step } of parseValidBatchStepEntries(stdin)) {
426
- const waitTimeout = findWaitTimeoutMs(step);
427
- if (waitTimeout && waitTimeout.timeoutMs > SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS) {
428
- return buildIpcUnsafeWaitError(waitTimeout.source, waitTimeout.timeoutMs, index);
429
- }
430
- }
431
- return undefined;
432
- }
433
-
434
224
  const DIALOG_COMMAND_PROCESS_TIMEOUT_MS = 5_000;
435
225
  const DIALOG_COMMAND_PROCESS_TIMEOUT_ENV = "PI_AGENT_BROWSER_DIALOG_PROCESS_TIMEOUT_MS";
436
226
  const LIKELY_DIALOG_TRIGGER_PROCESS_TIMEOUT_MS = 8_000;
@@ -518,416 +308,6 @@ async function collectSamePageRefFreshnessPreflight(options: {
518
308
  return { message: mismatch.message, refIds: mismatch.refIds, snapshot: snapshotWithTarget };
519
309
  }
520
310
 
521
- const SCROLL_CONTAINER_DIRECTIONS = new Set(["down", "left", "right", "up"]);
522
-
523
- function getContainerScrollRequest(commandTokens: string[]): { amount?: string; direction: string; selector: string } | undefined {
524
- if (commandTokens[0] !== "scroll" || commandTokens.length < 3) return undefined;
525
- const selector = commandTokens[1];
526
- const direction = commandTokens[2]?.toLowerCase();
527
- if (!selector || selector.startsWith("-") || selector.startsWith("@") || SCROLL_CONTAINER_DIRECTIONS.has(selector.toLowerCase())) return undefined;
528
- if (!SCROLL_CONTAINER_DIRECTIONS.has(direction)) return undefined;
529
- return { amount: commandTokens[3], direction, selector };
530
- }
531
-
532
- function buildContainerScrollScript(request: { amount?: string; direction: string; selector: string }): string {
533
- return `(() => {
534
- const selector = ${JSON.stringify(request.selector)};
535
- const direction = ${JSON.stringify(request.direction)};
536
- const amountToken = ${JSON.stringify(request.amount ?? "")};
537
- let element;
538
- try { element = document.querySelector(selector); } catch (error) { return { status: "invalid-selector", selector, error: String(error && error.message || error) }; }
539
- if (!(element instanceof HTMLElement)) return { status: "not-found", selector };
540
- const axis = direction === "left" || direction === "right" ? "x" : "y";
541
- const before = { scrollLeft: element.scrollLeft, scrollTop: element.scrollTop, scrollHeight: element.scrollHeight, scrollWidth: element.scrollWidth, clientHeight: element.clientHeight, clientWidth: element.clientWidth };
542
- const parseAmount = () => {
543
- const token = String(amountToken || "").trim().toLowerCase();
544
- const extent = axis === "x" ? element.clientWidth : element.clientHeight;
545
- if (!token) return Math.max(1, Math.floor(extent * 0.8));
546
- if (token.endsWith("%")) {
547
- const value = Number(token.slice(0, -1));
548
- return Number.isFinite(value) ? Math.max(1, Math.floor(extent * value / 100)) : Math.max(1, Math.floor(extent * 0.8));
549
- }
550
- const pixels = Number(token.replace(/px$/, ""));
551
- return Number.isFinite(pixels) && pixels > 0 ? Math.floor(pixels) : Math.max(1, Math.floor(extent * 0.8));
552
- };
553
- const delta = parseAmount() * (direction === "up" || direction === "left" ? -1 : 1);
554
- if (axis === "x") element.scrollLeft += delta;
555
- else element.scrollTop += delta;
556
- const after = { scrollLeft: element.scrollLeft, scrollTop: element.scrollTop, scrollHeight: element.scrollHeight, scrollWidth: element.scrollWidth, clientHeight: element.clientHeight, clientWidth: element.clientWidth };
557
- const moved = before.scrollLeft !== after.scrollLeft || before.scrollTop !== after.scrollTop;
558
- return { status: moved ? "scrolled" : "no-movement", selector, direction, amount: amountToken || undefined, before, after };
559
- })()`;
560
- }
561
-
562
- function buildScrollResult(options: {
563
- command: "scroll";
564
- compatibilityWorkaround?: CompatibilityWorkaround;
565
- effectiveArgs: string[];
566
- message: string;
567
- redactedArgs: string[];
568
- result: Record<string, unknown>;
569
- scrollField: "scrollContainer" | "scrollPage";
570
- scrollValue: unknown;
571
- sessionMode: "auto" | "fresh";
572
- sessionName?: string;
573
- succeeded: boolean;
574
- usedImplicitSession: boolean;
575
- }): AgentBrowserToolResult {
576
- return {
577
- content: [{ type: "text", text: options.message }],
578
- details: {
579
- args: options.redactedArgs,
580
- command: options.command,
581
- compatibilityWorkaround: options.compatibilityWorkaround,
582
- data: options.result,
583
- effectiveArgs: options.effectiveArgs,
584
- nextActions: options.succeeded ? undefined : buildScrollNoopNextActions(options.sessionName),
585
- [options.scrollField]: options.scrollValue,
586
- sessionMode: options.sessionMode,
587
- ...buildAgentBrowserResultCategoryDetails({ args: options.effectiveArgs, command: options.command, errorText: options.succeeded ? undefined : options.message, succeeded: options.succeeded, validationError: options.succeeded ? undefined : options.message }),
588
- ...buildSessionDetailFields(options.sessionName, options.usedImplicitSession),
589
- summary: options.message,
590
- validationError: options.succeeded ? undefined : options.message,
591
- },
592
- isError: !options.succeeded,
593
- };
594
- }
595
-
596
- async function tryContainerScroll(options: {
597
- commandTokens: string[];
598
- compatibilityWorkaround?: CompatibilityWorkaround;
599
- cwd: string;
600
- effectiveArgs: string[];
601
- redactedArgs: string[];
602
- sessionMode: "auto" | "fresh";
603
- sessionName?: string;
604
- signal?: AbortSignal;
605
- usedImplicitSession: boolean;
606
- }): Promise<AgentBrowserToolResult | undefined> {
607
- const request = getContainerScrollRequest(options.commandTokens);
608
- if (!request || !options.sessionName) return undefined;
609
- const data = await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, stdin: buildContainerScrollScript(request) });
610
- const result = isRecord(data) && isRecord(data.result) ? data.result : data;
611
- if (!isRecord(result) || typeof result.status !== "string") return undefined;
612
- const succeeded = result.status === "scrolled";
613
- const message = succeeded
614
- ? `Scrolled container ${request.selector} ${request.direction}${request.amount ? ` by ${request.amount}` : ""}.`
615
- : `Scroll container ${request.selector} did not move (${result.status}).`;
616
- return buildScrollResult({ ...options, command: "scroll", message, result, scrollField: "scrollContainer", scrollValue: { request, result }, succeeded });
617
- }
618
-
619
- function getPageScrollToRequest(commandTokens: string[]): { target: "end" | "top" } | undefined {
620
- if (commandTokens[0] !== "scroll" || commandTokens[1]?.toLowerCase() !== "to") return undefined;
621
- const target = commandTokens[2]?.toLowerCase();
622
- return target === "end" || target === "top" ? { target } : undefined;
623
- }
624
-
625
- function buildPageScrollToScript(request: { target: "end" | "top" }): string {
626
- return `(() => {
627
- const target = ${JSON.stringify(request.target)};
628
- const scroller = document.scrollingElement || document.documentElement || document.body;
629
- if (!scroller) return { status: "no-scroller", target };
630
- const before = { scrollLeft: scroller.scrollLeft, scrollTop: scroller.scrollTop, scrollHeight: scroller.scrollHeight, scrollWidth: scroller.scrollWidth, clientHeight: scroller.clientHeight, clientWidth: scroller.clientWidth };
631
- const nextTop = target === "top" ? 0 : Math.max(0, scroller.scrollHeight - scroller.clientHeight);
632
- const nextLeft = scroller.scrollLeft;
633
- scroller.scrollTop = nextTop;
634
- window.scrollTo(nextLeft, nextTop);
635
- const after = { scrollLeft: scroller.scrollLeft, scrollTop: scroller.scrollTop, scrollHeight: scroller.scrollHeight, scrollWidth: scroller.scrollWidth, clientHeight: scroller.clientHeight, clientWidth: scroller.clientWidth };
636
- const moved = before.scrollLeft !== after.scrollLeft || before.scrollTop !== after.scrollTop;
637
- return { status: moved ? "scrolled" : "no-movement", target, before, after };
638
- })()`;
639
- }
640
-
641
- async function tryPageScrollTo(options: {
642
- commandTokens: string[];
643
- compatibilityWorkaround?: CompatibilityWorkaround;
644
- cwd: string;
645
- effectiveArgs: string[];
646
- redactedArgs: string[];
647
- sessionMode: "auto" | "fresh";
648
- sessionName?: string;
649
- signal?: AbortSignal;
650
- usedImplicitSession: boolean;
651
- }): Promise<AgentBrowserToolResult | undefined> {
652
- const request = getPageScrollToRequest(options.commandTokens);
653
- if (!request || !options.sessionName) return undefined;
654
- const data = await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, stdin: buildPageScrollToScript(request) });
655
- const result = isRecord(data) && isRecord(data.result) ? data.result : data;
656
- if (!isRecord(result) || typeof result.status !== "string") return undefined;
657
- const succeeded = result.status === "scrolled";
658
- const message = succeeded ? `Scrolled page to ${request.target}.` : `Scroll to ${request.target} completed with no observed movement (${result.status}).`;
659
- return buildScrollResult({ ...options, command: "scroll", message, result, scrollField: "scrollPage", scrollValue: { request, result }, succeeded });
660
- }
661
-
662
- interface SnapshotFilterRequest {
663
- cleanArgs: string[];
664
- diff?: boolean;
665
- role?: string;
666
- search?: string;
667
- viewport?: boolean;
668
- }
669
-
670
- function parseSnapshotFilterRequest(commandTokens: string[]): SnapshotFilterRequest | undefined {
671
- if (commandTokens[0] !== "snapshot") return undefined;
672
- const cleanArgs: string[] = [];
673
- let role: string | undefined;
674
- let search: string | undefined;
675
- for (let index = 0; index < commandTokens.length; index += 1) {
676
- const token = commandTokens[index];
677
- if (token === "--viewport") continue;
678
- if (token === "--diff") continue;
679
- if (token === "--search") {
680
- const value = commandTokens[index + 1];
681
- if (typeof value === "string" && !value.startsWith("-")) {
682
- search = value;
683
- index += 1;
684
- continue;
685
- }
686
- }
687
- if (token === "--filter") {
688
- const value = commandTokens[index + 1];
689
- if (typeof value === "string" && !value.startsWith("-")) {
690
- const roleMatch = /^role=(.+)$/i.exec(value.trim());
691
- if (roleMatch?.[1]) role = roleMatch[1].trim().toLowerCase();
692
- index += 1;
693
- continue;
694
- }
695
- }
696
- cleanArgs.push(token);
697
- }
698
- const viewport = commandTokens.includes("--viewport");
699
- const diff = commandTokens.includes("--diff");
700
- if (!search && !role && !viewport && !diff) return undefined;
701
- return { cleanArgs, diff, role, search, viewport };
702
- }
703
-
704
- interface SnapshotDiffSummary {
705
- addedRefs: string[];
706
- changedRefs: string[];
707
- removedRefs: string[];
708
- summary: string;
709
- unchangedRefs: number;
710
- }
711
-
712
- function buildSnapshotDiff(previous: SessionRefSnapshot | undefined, current: SessionRefSnapshot | undefined): SnapshotDiffSummary | undefined {
713
- if (!current) return undefined;
714
- const currentRefs = current.refs ?? {};
715
- const previousRefs = previous?.refs ?? {};
716
- if (!previous) return { addedRefs: Object.keys(currentRefs), changedRefs: [], removedRefs: [], summary: `Snapshot diff: no previous snapshot; ${Object.keys(currentRefs).length} current refs recorded.`, unchangedRefs: 0 };
717
- const addedRefs: string[] = [];
718
- const removedRefs: string[] = [];
719
- const changedRefs: string[] = [];
720
- let unchangedRefs = 0;
721
- for (const refId of Object.keys(currentRefs)) {
722
- const currentRef = currentRefs[refId];
723
- const previousRef = previousRefs[refId];
724
- if (!previousRef) {
725
- addedRefs.push(refId);
726
- continue;
727
- }
728
- if (previousRef.role !== currentRef.role || previousRef.name !== currentRef.name) changedRefs.push(refId);
729
- else unchangedRefs += 1;
730
- }
731
- for (const refId of Object.keys(previousRefs)) if (!currentRefs[refId]) removedRefs.push(refId);
732
- return { addedRefs, changedRefs, removedRefs, summary: `Snapshot diff: +${addedRefs.length} / -${removedRefs.length} / Δ${changedRefs.length} refs versus previous snapshot.`, unchangedRefs };
733
- }
734
-
735
- function filterSnapshotData(data: unknown, request: SnapshotFilterRequest): { data: Record<string, unknown>; matchedRefs: number; totalRefs: number; totalLines: number; visibleLines: number } | undefined {
736
- if (!isRecord(data)) return undefined;
737
- const refs = isRecord(data.refs) ? data.refs : {};
738
- const snapshot = typeof data.snapshot === "string" ? data.snapshot : "";
739
- const normalizedSearch = request.search?.trim().toLowerCase();
740
- const matchingRefIds = new Set<string>();
741
- for (const [refId, refValue] of Object.entries(refs)) {
742
- if (!isRecord(refValue)) continue;
743
- const role = typeof refValue.role === "string" ? refValue.role.toLowerCase() : "";
744
- const name = typeof refValue.name === "string" ? refValue.name : "";
745
- const roleMatches = request.role ? role === request.role : true;
746
- const searchMatches = normalizedSearch ? `${role} ${name}`.toLowerCase().includes(normalizedSearch) : true;
747
- if (roleMatches && searchMatches) matchingRefIds.add(refId);
748
- }
749
- const lines = snapshot.split(/\r?\n/);
750
- const visibleLines = lines.filter((line) => {
751
- const normalizedLine = line.toLowerCase();
752
- if (normalizedSearch && normalizedLine.includes(normalizedSearch)) return true;
753
- return [...matchingRefIds].some((refId) => line.includes(`[ref=${refId}]`) || line.includes(`ref=${refId}`));
754
- });
755
- const filteredRefs = Object.fromEntries(Object.entries(refs).filter(([refId]) => matchingRefIds.has(refId)));
756
- const description = [request.role ? `role=${request.role}` : undefined, request.search ? `search=${JSON.stringify(request.search)}` : undefined].filter((part): part is string => part !== undefined).join(", ");
757
- const filteredSnapshot = visibleLines.length > 0 ? visibleLines.join("\n") : `(no snapshot lines matched ${description})`;
758
- return {
759
- data: { ...data, refs: filteredRefs, snapshot: filteredSnapshot },
760
- matchedRefs: Object.keys(filteredRefs).length,
761
- totalRefs: Object.keys(refs).length,
762
- totalLines: lines.filter((line) => line.length > 0).length,
763
- visibleLines: visibleLines.length,
764
- };
765
- }
766
-
767
- async function trySnapshotFilter(options: {
768
- commandTokens: string[];
769
- compatibilityWorkaround?: CompatibilityWorkaround;
770
- cwd: string;
771
- effectiveArgs: string[];
772
- redactedArgs: string[];
773
- previousRefSnapshot?: SessionRefSnapshot;
774
- sessionMode: "auto" | "fresh";
775
- sessionName?: string;
776
- sessionPageState: BrowserRunOptions["state"]["sessionPageState"];
777
- sessionPageStateUpdate: ReturnType<BrowserRunOptions["state"]["sessionPageState"]["beginUpdate"]>;
778
- signal?: AbortSignal;
779
- usedImplicitSession: boolean;
780
- }): Promise<AgentBrowserToolResult | undefined> {
781
- const request = parseSnapshotFilterRequest(options.commandTokens);
782
- if (!request || !options.sessionName) return undefined;
783
- const snapshotData = await runSessionCommandData({ args: request.cleanArgs, cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
784
- const filtered = request.role || request.search ? filterSnapshotData(snapshotData, request) : isRecord(snapshotData) ? { data: snapshotData, matchedRefs: isRecord(snapshotData.refs) ? Object.keys(snapshotData.refs).length : 0, totalLines: typeof snapshotData.snapshot === "string" ? snapshotData.snapshot.split(/\r?\n/).filter((line) => line.length > 0).length : 0, totalRefs: isRecord(snapshotData.refs) ? Object.keys(snapshotData.refs).length : 0, visibleLines: typeof snapshotData.snapshot === "string" ? snapshotData.snapshot.split(/\r?\n/).filter((line) => line.length > 0).length : 0 } : undefined;
785
- if (!filtered) return undefined;
786
- const viewport = request.viewport ? await collectScrollPositionSnapshot({ cwd: options.cwd, sessionName: options.sessionName, signal: options.signal }) : undefined;
787
- const fullSnapshot = extractRefSnapshotFromData(snapshotData);
788
- const diff = request.diff ? buildSnapshotDiff(options.previousRefSnapshot, fullSnapshot) : undefined;
789
- if (fullSnapshot) options.sessionPageState.applyRefSnapshot({ sessionName: options.sessionName, snapshot: fullSnapshot, update: options.sessionPageStateUpdate });
790
- const presentation = await buildSnapshotPresentation(filtered.data);
791
- const summary = request.role || request.search
792
- ? `Snapshot filter: ${filtered.matchedRefs}/${filtered.totalRefs} refs matched${request.role ? ` role=${request.role}` : ""}${request.search ? ` search ${JSON.stringify(request.search)}` : ""}.`
793
- : request.diff
794
- ? diff?.summary ?? "Snapshot diff unavailable."
795
- : "Snapshot viewport metadata collected.";
796
- const viewportText = viewport ? `Viewport: ${viewport.innerWidth}×${viewport.innerHeight}, scroll ${viewport.scrollX},${viewport.scrollY}, document ${viewport.scrollWidth}×${viewport.scrollHeight}, sampled scroll containers ${viewport.containers.length}/${viewport.containerCount}.` : undefined;
797
- const diffText = diff && (request.role || request.search) ? diff.summary : undefined;
798
- const prefix = [summary, diffText, viewportText].filter((line): line is string => line !== undefined).join("\n");
799
- if (presentation.content[0]?.type === "text") presentation.content[0] = { ...presentation.content[0], text: `${prefix}\n\n${presentation.content[0].text}` };
800
- return {
801
- content: presentation.content,
802
- details: {
803
- args: options.redactedArgs,
804
- command: "snapshot",
805
- compatibilityWorkaround: options.compatibilityWorkaround,
806
- data: presentation.data,
807
- effectiveArgs: options.effectiveArgs,
808
- refSnapshot: fullSnapshot,
809
- sessionMode: options.sessionMode,
810
- snapshotDiff: diff,
811
- snapshotFilter: request.role || request.search ? { cleanArgs: request.cleanArgs, matchedRefs: filtered.matchedRefs, role: request.role, search: request.search, totalLines: filtered.totalLines, totalRefs: filtered.totalRefs, visibleLines: filtered.visibleLines } : undefined,
812
- snapshotViewport: viewport,
813
- ...buildAgentBrowserResultCategoryDetails({ args: options.effectiveArgs, command: "snapshot", succeeded: true }),
814
- ...buildSessionDetailFields(options.sessionName, options.usedImplicitSession),
815
- summary,
816
- },
817
- isError: false,
818
- };
819
- }
820
-
821
- interface NetworkRequestsPageFilterRequest {
822
- cleanArgs: string[];
823
- mode: "origin" | "url";
824
- }
825
-
826
- function parseNetworkRequestsPageFilterRequest(commandTokens: string[]): NetworkRequestsPageFilterRequest | undefined {
827
- if (commandTokens[0] !== "network" || commandTokens[1] !== "requests") return undefined;
828
- const cleanArgs: string[] = [];
829
- let mode: NetworkRequestsPageFilterRequest["mode"] | undefined;
830
- for (const token of commandTokens) {
831
- if (token === "--current-page" || token === "--current-origin") {
832
- mode = "origin";
833
- continue;
834
- }
835
- if (token === "--current-url") {
836
- mode = "url";
837
- continue;
838
- }
839
- cleanArgs.push(token);
840
- }
841
- if (!mode) return undefined;
842
- return { cleanArgs, mode };
843
- }
844
-
845
- function extractCurrentUrl(data: unknown): string | undefined {
846
- if (typeof data === "string") return data;
847
- if (!isRecord(data)) return undefined;
848
- const candidates = [data.url, data.currentUrl, data.href, data.result];
849
- for (const candidate of candidates) if (typeof candidate === "string" && candidate.length > 0) return candidate;
850
- return undefined;
851
- }
852
-
853
- function getRequestUrl(row: unknown): string | undefined {
854
- if (!isRecord(row)) return undefined;
855
- const candidate = row.url ?? row.requestUrl ?? row.href;
856
- return typeof candidate === "string" ? candidate : undefined;
857
- }
858
-
859
- function requestMatchesCurrentPage(row: unknown, currentUrl: string, mode: NetworkRequestsPageFilterRequest["mode"]): boolean {
860
- const requestUrl = getRequestUrl(row);
861
- if (!requestUrl) return false;
862
- try {
863
- const current = new URL(currentUrl);
864
- const request = new URL(requestUrl, current);
865
- if (mode === "origin") return current.origin === request.origin;
866
- const currentComparable = `${current.origin}${current.pathname}`;
867
- const requestComparable = `${request.origin}${request.pathname}`;
868
- return requestComparable === currentComparable;
869
- } catch {
870
- return mode === "url" ? requestUrl === currentUrl : requestUrl.startsWith(currentUrl);
871
- }
872
- }
873
-
874
- function filterNetworkRequestsData(data: unknown, currentUrl: string, request: NetworkRequestsPageFilterRequest): { data: Record<string, unknown>; matchedRows: number; totalRows: number; rows: unknown[] } | undefined {
875
- if (!isRecord(data)) return undefined;
876
- const requestRows = Array.isArray(data.requests) ? data.requests : Array.isArray(data.items) ? data.items : Array.isArray(data.entries) ? data.entries : undefined;
877
- if (!requestRows) return undefined;
878
- const rows = requestRows.filter((row) => requestMatchesCurrentPage(row, currentUrl, request.mode));
879
- const key = Array.isArray(data.requests) ? "requests" : Array.isArray(data.items) ? "items" : "entries";
880
- return { data: { ...data, [key]: rows }, matchedRows: rows.length, rows, totalRows: requestRows.length };
881
- }
882
-
883
- function formatNetworkRequestRow(row: unknown): string {
884
- if (!isRecord(row)) return redactSensitiveText(String(row));
885
- const status = row.status ?? row.statusCode ?? row.responseStatus ?? "?";
886
- const method = typeof row.method === "string" ? row.method : typeof row.requestMethod === "string" ? row.requestMethod : "?";
887
- const id = typeof row.id === "string" ? ` id=${row.id}` : typeof row.requestId === "string" ? ` id=${row.requestId}` : "";
888
- const url = getRequestUrl(row) ?? "(no url)";
889
- return redactSensitiveText(`- ${status} ${method}${id} ${url}`);
890
- }
891
-
892
- async function tryNetworkRequestsPageFilter(options: {
893
- commandTokens: string[];
894
- compatibilityWorkaround?: CompatibilityWorkaround;
895
- cwd: string;
896
- effectiveArgs: string[];
897
- redactedArgs: string[];
898
- sessionMode: "auto" | "fresh";
899
- sessionName?: string;
900
- signal?: AbortSignal;
901
- usedImplicitSession: boolean;
902
- }): Promise<AgentBrowserToolResult | undefined> {
903
- const request = parseNetworkRequestsPageFilterRequest(options.commandTokens);
904
- if (!request || !options.sessionName) return undefined;
905
- const currentUrl = extractCurrentUrl(await runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal }));
906
- if (!currentUrl) return undefined;
907
- const networkData = await runSessionCommandData({ args: request.cleanArgs, cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
908
- const filtered = filterNetworkRequestsData(networkData, currentUrl, request);
909
- if (!filtered) return undefined;
910
- const summary = `Network requests filtered to current ${request.mode === "origin" ? "origin" : "URL"}: ${filtered.matchedRows}/${filtered.totalRows} rows matched.`;
911
- const preview = filtered.rows.slice(0, 12).map(formatNetworkRequestRow);
912
- const omitted = filtered.rows.length > preview.length ? [`- …${filtered.rows.length - preview.length} more matching rows omitted`] : [];
913
- return {
914
- content: [{ type: "text", text: [redactSensitiveText(summary), `Current page: ${redactSensitiveText(currentUrl)}`, ...preview, ...omitted].join("\n") }],
915
- details: {
916
- args: options.redactedArgs,
917
- command: "network",
918
- compatibilityWorkaround: options.compatibilityWorkaround,
919
- data: filtered.data,
920
- effectiveArgs: options.effectiveArgs,
921
- networkRequestsPageFilter: { cleanArgs: request.cleanArgs, currentUrl: redactSensitiveText(currentUrl), matchedRows: filtered.matchedRows, mode: request.mode, totalRows: filtered.totalRows },
922
- sessionMode: options.sessionMode,
923
- ...buildAgentBrowserResultCategoryDetails({ args: options.effectiveArgs, command: "network", succeeded: true }),
924
- ...buildSessionDetailFields(options.sessionName, options.usedImplicitSession),
925
- summary,
926
- },
927
- isError: false,
928
- };
929
- }
930
-
931
311
  function isPasswordStdinAuthSave(options: { command?: string; commandTokens: string[] }): boolean {
932
312
  return options.command === "auth" && options.commandTokens[1] === "save" && options.commandTokens.includes("--password-stdin");
933
313
  }
@@ -1049,7 +429,7 @@ export async function prepareBrowserRun(options: BrowserRunOptions): Promise<Pre
1049
429
  const redactedEffectiveArgs = redactInvocationArgs(executionPlan.effectiveArgs);
1050
430
  const redactedRecoveryHint = redactRecoveryHint(executionPlan.recoveryHint);
1051
431
  const compatibilityWorkaround: CompatibilityWorkaround | undefined = executionPlan.compatibilityWorkaround;
1052
- const statePatch = executionPlan.managedSessionName === freshSessionName
432
+ const statePatch: BrowserRunStatePatch = executionPlan.managedSessionName === freshSessionName
1053
433
  ? { freshSessionOrdinal: freshSessionOrdinal + 1 }
1054
434
  : {};
1055
435
  if (executionPlan.managedSessionName === freshSessionName) {
@@ -1126,24 +506,6 @@ export async function prepareBrowserRun(options: BrowserRunOptions): Promise<Pre
1126
506
  isError: true,
1127
507
  } };
1128
508
  }
1129
- const waitIpcTimeoutError = validateWaitIpcTimeoutContract(commandTokens, runtimeToolStdin);
1130
- if (waitIpcTimeoutError) {
1131
- return { kind: "early-result", statePatch, result: {
1132
- content: [{ type: "text", text: waitIpcTimeoutError }],
1133
- details: {
1134
- args: redactedArgs,
1135
- command: executionPlan.commandInfo.command,
1136
- compatibilityWorkaround,
1137
- effectiveArgs: redactedEffectiveArgs,
1138
- sessionMode,
1139
- ...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: waitIpcTimeoutError, succeeded: false, timedOut: true, validationError: waitIpcTimeoutError }),
1140
- validationError: waitIpcTimeoutError,
1141
- ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
1142
- },
1143
- isError: true,
1144
- } };
1145
- }
1146
-
1147
509
  const priorSessionPageState = sessionPageState.get(executionPlan.sessionName);
1148
510
  const priorSessionTabTarget = priorSessionPageState.tabTarget;
1149
511
  const sessionTabPinningReason = priorSessionPageState.pinningReason;
@@ -1252,11 +614,14 @@ export async function prepareBrowserRun(options: BrowserRunOptions): Promise<Pre
1252
614
  }
1253
615
  }
1254
616
 
617
+ const persistentArtifactStore = getPersistentSessionArtifactStore(options.ctx);
1255
618
  const snapshotFilter = await trySnapshotFilter({
619
+ artifactManifest: state.artifactManifest,
1256
620
  commandTokens,
1257
621
  compatibilityWorkaround,
1258
622
  cwd,
1259
623
  effectiveArgs: redactedEffectiveArgs,
624
+ persistentArtifactStore,
1260
625
  previousRefSnapshot: priorRefSnapshotState,
1261
626
  redactedArgs,
1262
627
  sessionMode,
@@ -1266,7 +631,7 @@ export async function prepareBrowserRun(options: BrowserRunOptions): Promise<Pre
1266
631
  signal,
1267
632
  usedImplicitSession: executionPlan.usedImplicitSession,
1268
633
  });
1269
- if (snapshotFilter) return { kind: "early-result", statePatch, result: snapshotFilter };
634
+ if (snapshotFilter) return { kind: "early-result", statePatch: { ...statePatch, artifactManifest: snapshotFilter.artifactManifest ?? statePatch.artifactManifest }, result: snapshotFilter.result };
1270
635
 
1271
636
  const networkRequestsPageFilter = await tryNetworkRequestsPageFilter({
1272
637
  commandTokens,
@@ -1307,6 +672,7 @@ export async function prepareBrowserRun(options: BrowserRunOptions): Promise<Pre
1307
672
  if (pageScrollTo) return { kind: "early-result", statePatch, result: pageScrollTo };
1308
673
 
1309
674
  const directAnchorDownload = await tryDirectAnchorDownload({
675
+ artifactManifest: state.artifactManifest,
1310
676
  commandTokens,
1311
677
  compatibilityWorkaround,
1312
678
  cwd,
@@ -1317,7 +683,7 @@ export async function prepareBrowserRun(options: BrowserRunOptions): Promise<Pre
1317
683
  signal,
1318
684
  usedImplicitSession: executionPlan.usedImplicitSession,
1319
685
  });
1320
- if (directAnchorDownload) return { kind: "early-result", statePatch, result: directAnchorDownload };
686
+ if (directAnchorDownload) return { kind: "early-result", statePatch: { ...statePatch, artifactManifest: directAnchorDownload.artifactManifest ?? statePatch.artifactManifest }, result: directAnchorDownload.result };
1321
687
 
1322
688
  let pinnedBatchUnwrapMode: PreparedBrowserRun["pinnedBatchUnwrapMode"];
1323
689
  let includePinnedNavigationSummary = false;
@@ -1418,7 +784,7 @@ export async function prepareBrowserRun(options: BrowserRunOptions): Promise<Pre
1418
784
  const clickDispatchProbe = pinnedBatchUnwrapMode === undefined && compiledElectron === undefined
1419
785
  ? await prepareClickDispatchProbe({ commandTokens, cwd, refSnapshot: promptRefSnapshot, sessionName: executionPlan.sessionName, signal })
1420
786
  : undefined;
1421
- const processTimeoutMs = options.params.timeoutMs ?? getDialogAwareProcessTimeoutMs(commandTokens, promptRefSnapshot, processStdin);
787
+ const processTimeoutMs = options.params.timeoutMs ?? getDialogAwareProcessTimeoutMs(commandTokens, promptRefSnapshot, processStdin) ?? getWaitAwareProcessTimeoutMs(commandTokens, processStdin);
1422
788
  const redactedProcessArgs = redactInvocationArgs(processArgs);
1423
789
  const shouldProbeScrollNoop = executionPlan.commandInfo.command === "scroll" && executionPlan.startupScopedFlags.length === 0;
1424
790
  const scrollPositionBefore = shouldProbeScrollNoop