pi-agent-browser-native 0.2.24 → 0.2.25

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.
@@ -6,7 +6,7 @@
6
6
  * Invariants/Assumptions: agent-browser is installed separately on PATH, the wrapper targets the current locally installed upstream version only, and no backward-compatibility shims are provided.
7
7
  */
8
8
 
9
- import { copyFile, mkdir, readFile, rm, stat } from "node:fs/promises";
9
+ import { copyFile, mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
10
10
  import { dirname, extname, isAbsolute, join, resolve } from "node:path";
11
11
 
12
12
  import { StringEnum } from "@earendil-works/pi-ai";
@@ -27,6 +27,8 @@ import {
27
27
  } from "./lib/playbook.js";
28
28
  import { SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS, runAgentBrowserProcess } from "./lib/process.js";
29
29
  import {
30
+ buildAgentBrowserNextActions,
31
+ buildAgentBrowserResultCategoryDetails,
30
32
  buildToolPresentation,
31
33
  getAgentBrowserErrorText,
32
34
  parseAgentBrowserEnvelope,
@@ -76,16 +78,202 @@ const DEFAULT_SESSION_MODE = "auto" as const;
76
78
  const DIRECT_AGENT_BROWSER_BASH_BYPASS_ENV = "PI_AGENT_BROWSER_ALLOW_DIRECT_BASH";
77
79
  const PACKAGE_NAME = "pi-agent-browser-native";
78
80
 
81
+ const AGENT_BROWSER_SEMANTIC_ACTIONS = ["check", "click", "fill", "select", "uncheck"] as const;
82
+ const AGENT_BROWSER_SEMANTIC_LOCATORS = ["alt", "label", "placeholder", "role", "testid", "text", "title"] as const;
83
+ const AGENT_BROWSER_JOB_STEP_ACTIONS = ["open", "click", "fill", "wait", "assertText", "assertUrl", "waitForDownload", "screenshot"] as const;
84
+ const SOURCE_LOOKUP_WORKSPACE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
85
+ const SOURCE_LOOKUP_IGNORED_DIRECTORIES = new Set([".git", "node_modules", "dist", "build", "coverage", ".next", "out", "tmp", "temp"]);
86
+ const SOURCE_LOOKUP_DEFAULT_MAX_WORKSPACE_FILES = 2_000;
87
+ const SOURCE_LOOKUP_MAX_WORKSPACE_FILES = 5_000;
88
+
89
+ type AgentBrowserSemanticActionName = (typeof AGENT_BROWSER_SEMANTIC_ACTIONS)[number];
90
+ type AgentBrowserSemanticLocator = (typeof AGENT_BROWSER_SEMANTIC_LOCATORS)[number];
91
+ type AgentBrowserJobStepAction = (typeof AGENT_BROWSER_JOB_STEP_ACTIONS)[number];
92
+ type AgentBrowserSourceLookupStatus = "candidates-found" | "no-candidates" | "unsupported";
93
+ type AgentBrowserNetworkSourceLookupStatus = "failed-requests-found" | "no-failed-requests" | "no-candidates";
94
+
95
+ interface AgentBrowserSemanticActionInput {
96
+ action: AgentBrowserSemanticActionName;
97
+ locator: AgentBrowserSemanticLocator;
98
+ value: string;
99
+ text?: string;
100
+ role?: string;
101
+ name?: string;
102
+ }
103
+
104
+ interface CompiledAgentBrowserSemanticAction {
105
+ action: AgentBrowserSemanticActionName;
106
+ locator: AgentBrowserSemanticLocator;
107
+ args: string[];
108
+ }
109
+
110
+ interface CompiledAgentBrowserJobStep {
111
+ action: AgentBrowserJobStepAction;
112
+ args: string[];
113
+ }
114
+
115
+ interface CompiledAgentBrowserJob {
116
+ args: string[];
117
+ stdin: string;
118
+ steps: CompiledAgentBrowserJobStep[];
119
+ }
120
+
121
+ interface CompiledAgentBrowserQaPreset extends CompiledAgentBrowserJob {
122
+ checks: {
123
+ checkConsole: boolean;
124
+ checkErrors: boolean;
125
+ checkNetwork: boolean;
126
+ expectedText: string[];
127
+ expectedSelector?: string;
128
+ screenshotPath?: string;
129
+ url: string;
130
+ };
131
+ }
132
+
133
+ interface CompiledAgentBrowserSourceLookupStep {
134
+ action: "dom" | "react";
135
+ args: string[];
136
+ }
137
+
138
+ interface CompiledAgentBrowserSourceLookup {
139
+ args: string[];
140
+ stdin: string;
141
+ steps: CompiledAgentBrowserSourceLookupStep[];
142
+ query: {
143
+ componentName?: string;
144
+ includeDomHints: boolean;
145
+ maxWorkspaceFiles: number;
146
+ reactFiberId?: string;
147
+ selector?: string;
148
+ };
149
+ }
150
+
151
+ interface AgentBrowserSourceLookupCandidate {
152
+ column?: number;
153
+ componentName?: string;
154
+ confidence: "high" | "medium" | "low";
155
+ evidence: string[];
156
+ file?: string;
157
+ line?: number;
158
+ source: "react-inspect" | "dom-attribute" | "workspace-search";
159
+ }
160
+
161
+ interface AgentBrowserSourceLookupAnalysis {
162
+ candidates: AgentBrowserSourceLookupCandidate[];
163
+ limitations: string[];
164
+ status: AgentBrowserSourceLookupStatus;
165
+ summary: string;
166
+ }
167
+
168
+ interface CompiledAgentBrowserNetworkSourceLookup {
169
+ args: string[];
170
+ stdin: string;
171
+ steps: Array<{ action: "network"; args: string[] }>;
172
+ query: {
173
+ filter?: string;
174
+ maxWorkspaceFiles: number;
175
+ requestId?: string;
176
+ url?: string;
177
+ };
178
+ }
179
+
180
+ interface AgentBrowserNetworkSourceLookupRequest {
181
+ error?: string;
182
+ method?: string;
183
+ requestId?: string;
184
+ status?: number;
185
+ url?: string;
186
+ }
187
+
188
+ interface AgentBrowserNetworkSourceLookupCandidate {
189
+ confidence: "high" | "medium" | "low";
190
+ evidence: string[];
191
+ file?: string;
192
+ line?: number;
193
+ requestUrl?: string;
194
+ source: "initiator" | "workspace-search";
195
+ }
196
+
197
+ interface AgentBrowserNetworkSourceLookupAnalysis {
198
+ candidates: AgentBrowserNetworkSourceLookupCandidate[];
199
+ failedRequests: AgentBrowserNetworkSourceLookupRequest[];
200
+ limitations: string[];
201
+ status: AgentBrowserNetworkSourceLookupStatus;
202
+ summary: string;
203
+ }
204
+
79
205
  const AGENT_BROWSER_PARAMS = Type.Object({
80
- args: Type.Array(Type.String({ description: "Exact agent-browser CLI arguments, excluding the binary name." }), {
81
- description: "Exact agent-browser CLI arguments, excluding the binary name and any shell operators.",
82
- minItems: 1,
83
- }),
84
- stdin: Type.Optional(Type.String({ description: "Optional raw stdin content; only supported for batch, eval --stdin, and auth save --password-stdin." })),
206
+
207
+ args: Type.Optional(
208
+ Type.Array(Type.String({ description: "Exact agent-browser CLI arguments, excluding the binary name." }), {
209
+ description: "Exact agent-browser CLI arguments, excluding the binary name and any shell operators. Required unless semanticAction, job, qa, sourceLookup, or networkSourceLookup is provided.",
210
+ minItems: 1,
211
+ }),
212
+ ),
213
+ semanticAction: Type.Optional(
214
+ Type.Object({
215
+ action: StringEnum(AGENT_BROWSER_SEMANTIC_ACTIONS, {
216
+ description: "Intent action to compile to an existing agent-browser find command.",
217
+ }),
218
+ locator: StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, {
219
+ description: "Upstream find locator family to use.",
220
+ }),
221
+ value: Type.String({ description: "Locator value, such as visible text, label text, placeholder text, test id, title, alt text, or role." }),
222
+ text: Type.Optional(Type.String({ description: "Text/value argument for fill or select actions." })),
223
+ role: Type.Optional(Type.String({ description: "Role locator value; when set it must match value for locator=role." })),
224
+ name: Type.Optional(Type.String({ description: "Accessible name filter for locator=role; compiles to --name <name>." })),
225
+ }),
226
+ ),
227
+ qa: Type.Optional(
228
+ Type.Object({
229
+ url: Type.String({ description: "URL to open for a lightweight QA preset." }),
230
+ expectedText: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())], { description: "Text that must appear on the page." })),
231
+ expectedSelector: Type.Optional(Type.String({ description: "Selector or @ref that must appear on the page." })),
232
+ screenshotPath: Type.Optional(Type.String({ description: "Optional evidence screenshot path captured at the end of the QA preset." })),
233
+ checkConsole: Type.Optional(Type.Boolean({ description: "Whether to fail on console error messages. Defaults to true." })),
234
+ checkErrors: Type.Optional(Type.Boolean({ description: "Whether to fail on page errors. Defaults to true." })),
235
+ checkNetwork: Type.Optional(Type.Boolean({ description: "Whether to fail on failed network requests. Defaults to true." })),
236
+ }),
237
+ ),
238
+ sourceLookup: Type.Optional(
239
+ Type.Object({
240
+ selector: Type.Optional(Type.String({ description: "Visible selector or @ref whose DOM metadata should be inspected for source hints." })),
241
+ reactFiberId: Type.Optional(Type.String({ description: "React fiber id to inspect with upstream react inspect. Requires a session opened with --enable react-devtools." })),
242
+ componentName: Type.Optional(Type.String({ description: "Component name to correlate with react tree output and bounded local workspace search." })),
243
+ includeDomHints: Type.Optional(Type.Boolean({ description: "Whether selector lookups should inspect DOM HTML attributes for source-like metadata. Defaults to true." })),
244
+ maxWorkspaceFiles: Type.Optional(Type.Number({ description: "Maximum local source files to scan when componentName is provided. Defaults to 2000 and cannot exceed 5000.", minimum: 1, maximum: SOURCE_LOOKUP_MAX_WORKSPACE_FILES })),
245
+ }),
246
+ ),
247
+ networkSourceLookup: Type.Optional(
248
+ Type.Object({
249
+ filter: Type.Optional(Type.String({ description: "Optional upstream network requests filter pattern." })),
250
+ requestId: Type.Optional(Type.String({ description: "Optional network request id to inspect with network request <id>." })),
251
+ url: Type.Optional(Type.String({ description: "Optional failed request URL or URL fragment to correlate with local source." })),
252
+ maxWorkspaceFiles: Type.Optional(Type.Number({ description: "Maximum local source files to scan for URL literals. Defaults to 2000 and cannot exceed 5000.", minimum: 1, maximum: SOURCE_LOOKUP_MAX_WORKSPACE_FILES })),
253
+ }),
254
+ ),
255
+ job: Type.Optional(
256
+ Type.Object({
257
+ steps: Type.Array(
258
+ Type.Object({
259
+ action: StringEnum(AGENT_BROWSER_JOB_STEP_ACTIONS, {
260
+ description: "Constrained one-call job step compiled to existing upstream batch commands.",
261
+ }),
262
+ url: Type.Optional(Type.String({ description: "URL for open steps, or URL pattern for assertUrl steps." })),
263
+ selector: Type.Optional(Type.String({ description: "Selector or @ref for click/fill/get-like steps." })),
264
+ text: Type.Optional(Type.String({ description: "Text for fill steps or visible text for assertText steps." })),
265
+ path: Type.Optional(Type.String({ description: "Artifact/download path for waitForDownload or screenshot steps." })),
266
+ milliseconds: Type.Optional(Type.Number({ description: "Milliseconds for wait steps." })),
267
+ }),
268
+ { minItems: 1 },
269
+ ),
270
+ }),
271
+ ),
272
+ stdin: Type.Optional(Type.String({ description: "Optional raw stdin content; only supported for batch, eval --stdin, auth save --password-stdin, and is generated internally by job, qa, sourceLookup, or networkSourceLookup mode." })),
85
273
  sessionMode: Type.Optional(
86
274
  StringEnum(["auto", "fresh"] as const, {
87
275
  description:
88
- "Session handling mode. `auto` reuses the extension-managed pi-scoped session when possible. `fresh` switches that managed session to a fresh upstream launch so launch-scoped flags like --profile, --session-name, --cdp, --state, --auto-connect, --init-script, or --enable apply and later auto calls follow the new browser.",
276
+ "Session handling mode. `auto` reuses the extension-managed pi-scoped session when possible. `fresh` switches that managed session to a fresh upstream launch so launch-scoped flags like --profile, --session-name, --cdp, --state, --auto-connect, --init-script, --enable, -p/--provider, or iOS --device apply and later auto calls follow the new browser.",
89
277
  default: DEFAULT_SESSION_MODE,
90
278
  }),
91
279
  ),
@@ -105,6 +293,635 @@ function buildInvocationPreview(effectiveArgs: string[]): string {
105
293
  return preview.length > 120 ? `${preview.slice(0, 117)}...` : preview;
106
294
  }
107
295
 
296
+ function getRequiredJobString(step: Record<string, unknown>, field: "path" | "selector" | "text" | "url", action: AgentBrowserJobStepAction): { value?: string; error?: string } {
297
+ const value = step[field];
298
+ if (typeof value !== "string" || value.trim().length === 0) {
299
+ return { error: `job step ${action} requires a non-empty ${field} string.` };
300
+ }
301
+ return { value };
302
+ }
303
+
304
+ function compileAgentBrowserJob(input: unknown): { compiled?: CompiledAgentBrowserJob; error?: string } {
305
+ if (!isRecord(input)) {
306
+ return { error: "job must be an object." };
307
+ }
308
+ const rawSteps = input.steps;
309
+ if (!Array.isArray(rawSteps) || rawSteps.length === 0) {
310
+ return { error: "job.steps must be a non-empty array." };
311
+ }
312
+ const steps: CompiledAgentBrowserJobStep[] = [];
313
+ for (const [index, rawStep] of rawSteps.entries()) {
314
+ if (!isRecord(rawStep)) {
315
+ return { error: `job.steps[${index}] must be an object.` };
316
+ }
317
+ const action = rawStep.action;
318
+ if (typeof action !== "string" || !AGENT_BROWSER_JOB_STEP_ACTIONS.includes(action as AgentBrowserJobStepAction)) {
319
+ return { error: `job.steps[${index}].action must be one of: ${AGENT_BROWSER_JOB_STEP_ACTIONS.join(", ")}.` };
320
+ }
321
+ const jobAction = action as AgentBrowserJobStepAction;
322
+ let args: string[];
323
+ if (jobAction === "open") {
324
+ const result = getRequiredJobString(rawStep, "url", jobAction);
325
+ if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
326
+ args = ["open", result.value as string];
327
+ } else if (jobAction === "click") {
328
+ const result = getRequiredJobString(rawStep, "selector", jobAction);
329
+ if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
330
+ args = ["click", result.value as string];
331
+ } else if (jobAction === "fill") {
332
+ const selector = getRequiredJobString(rawStep, "selector", jobAction);
333
+ if (selector.error) return { error: `job.steps[${index}]: ${selector.error}` };
334
+ const text = getRequiredJobString(rawStep, "text", jobAction);
335
+ if (text.error) return { error: `job.steps[${index}]: ${text.error}` };
336
+ args = ["fill", selector.value as string, text.value as string];
337
+ } else if (jobAction === "wait") {
338
+ const milliseconds = rawStep.milliseconds;
339
+ if (typeof milliseconds !== "number" || !Number.isInteger(milliseconds) || milliseconds <= 0) {
340
+ return { error: `job.steps[${index}]: job step wait requires a positive integer milliseconds value.` };
341
+ }
342
+ args = ["wait", String(milliseconds)];
343
+ } else if (jobAction === "assertText") {
344
+ const result = getRequiredJobString(rawStep, "text", jobAction);
345
+ if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
346
+ args = ["wait", "--text", result.value as string];
347
+ } else if (jobAction === "assertUrl") {
348
+ const result = getRequiredJobString(rawStep, "url", jobAction);
349
+ if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
350
+ args = ["wait", "--url", result.value as string];
351
+ } else if (jobAction === "waitForDownload") {
352
+ const result = getRequiredJobString(rawStep, "path", jobAction);
353
+ if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
354
+ args = ["wait", "--download", result.value as string];
355
+ } else {
356
+ const result = getRequiredJobString(rawStep, "path", jobAction);
357
+ if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
358
+ args = ["screenshot", result.value as string];
359
+ }
360
+ steps.push({ action: jobAction, args });
361
+ }
362
+ return { compiled: { args: ["batch"], stdin: JSON.stringify(steps.map((step) => step.args)), steps } };
363
+ }
364
+
365
+ interface AgentBrowserQaPresetAnalysis {
366
+ failedChecks: string[];
367
+ passed: boolean;
368
+ summary: string;
369
+ }
370
+
371
+ function getBatchResultItems(data: unknown): Array<Record<string, unknown>> {
372
+ return Array.isArray(data) ? data.filter(isRecord) : [];
373
+ }
374
+
375
+ function getCommandNameFromBatchItem(item: Record<string, unknown>): string | undefined {
376
+ const command = item.command;
377
+ return Array.isArray(command) && typeof command[0] === "string" ? command[0] : undefined;
378
+ }
379
+
380
+ function analyzeQaPresetResults(data: unknown): AgentBrowserQaPresetAnalysis | undefined {
381
+ const items = getBatchResultItems(data);
382
+ if (items.length === 0) return undefined;
383
+ const failedChecks: string[] = [];
384
+ for (const item of items) {
385
+ if (item.success === false) {
386
+ failedChecks.push(`${getCommandNameFromBatchItem(item) ?? "step"} failed`);
387
+ }
388
+ const result = isRecord(item.result) ? item.result : undefined;
389
+ const commandName = getCommandNameFromBatchItem(item);
390
+ if (commandName === "errors" && Array.isArray(result?.errors) && result.errors.length > 0) {
391
+ failedChecks.push(`${result.errors.length} page error(s)`);
392
+ }
393
+ if (commandName === "console" && Array.isArray(result?.messages)) {
394
+ const errorCount = result.messages.filter((message) => isRecord(message) && /error/i.test(String(message.type ?? message.level ?? ""))).length;
395
+ if (errorCount > 0) failedChecks.push(`${errorCount} console error message(s)`);
396
+ }
397
+ if (commandName === "network" && Array.isArray(result?.requests)) {
398
+ const failedRequestCount = result.requests.filter((request) => isRecord(request) && ((typeof request.status === "number" && request.status >= 400) || request.failed === true || typeof request.error === "string")).length;
399
+ if (failedRequestCount > 0) failedChecks.push(`${failedRequestCount} failed network request(s)`);
400
+ }
401
+ }
402
+ const uniqueFailures = [...new Set(failedChecks)];
403
+ return {
404
+ failedChecks: uniqueFailures,
405
+ passed: uniqueFailures.length === 0,
406
+ summary: uniqueFailures.length === 0 ? "QA preset passed." : `QA preset failed: ${uniqueFailures.join("; ")}.`,
407
+ };
408
+ }
409
+
410
+ function compileAgentBrowserQaPreset(input: unknown): { compiled?: CompiledAgentBrowserQaPreset; error?: string } {
411
+ if (!isRecord(input)) {
412
+ return { error: "qa must be an object." };
413
+ }
414
+ const url = input.url;
415
+ if (typeof url !== "string" || url.trim().length === 0) {
416
+ return { error: "qa.url must be a non-empty string." };
417
+ }
418
+ const expectedText = input.expectedText === undefined
419
+ ? []
420
+ : typeof input.expectedText === "string"
421
+ ? [input.expectedText]
422
+ : Array.isArray(input.expectedText)
423
+ ? input.expectedText
424
+ : undefined;
425
+ if (!expectedText || expectedText.some((text) => typeof text !== "string" || text.trim().length === 0)) {
426
+ return { error: "qa.expectedText must be a non-empty string or array of non-empty strings when provided." };
427
+ }
428
+ const expectedSelector = input.expectedSelector;
429
+ if (expectedSelector !== undefined && (typeof expectedSelector !== "string" || expectedSelector.trim().length === 0)) {
430
+ return { error: "qa.expectedSelector must be a non-empty string when provided." };
431
+ }
432
+ const screenshotPath = input.screenshotPath;
433
+ if (screenshotPath !== undefined && (typeof screenshotPath !== "string" || screenshotPath.trim().length === 0)) {
434
+ return { error: "qa.screenshotPath must be a non-empty string when provided." };
435
+ }
436
+ for (const field of ["checkConsole", "checkErrors", "checkNetwork"] as const) {
437
+ if (input[field] !== undefined && typeof input[field] !== "boolean") {
438
+ return { error: `qa.${field} must be a boolean when provided.` };
439
+ }
440
+ }
441
+ const checkConsole = input.checkConsole !== false;
442
+ const checkErrors = input.checkErrors !== false;
443
+ const checkNetwork = input.checkNetwork !== false;
444
+ const steps: CompiledAgentBrowserJobStep[] = [];
445
+ if (checkNetwork) steps.push({ action: "wait", args: ["network", "requests", "--clear"] });
446
+ if (checkConsole) steps.push({ action: "wait", args: ["console", "--clear"] });
447
+ if (checkErrors) steps.push({ action: "wait", args: ["errors", "--clear"] });
448
+ steps.push(
449
+ { action: "open", args: ["open", url] },
450
+ { action: "wait", args: ["wait", "--load", "networkidle"] },
451
+ );
452
+ for (const text of expectedText) {
453
+ steps.push({ action: "assertText", args: ["wait", "--text", text] });
454
+ }
455
+ if (typeof expectedSelector === "string") {
456
+ steps.push({ action: "wait", args: ["wait", expectedSelector] });
457
+ }
458
+ if (checkNetwork) steps.push({ action: "wait", args: ["network", "requests"] });
459
+ if (checkConsole) steps.push({ action: "wait", args: ["console"] });
460
+ if (checkErrors) steps.push({ action: "wait", args: ["errors"] });
461
+ if (typeof screenshotPath === "string") steps.push({ action: "screenshot", args: ["screenshot", screenshotPath] });
462
+ return {
463
+ compiled: {
464
+ args: ["batch"],
465
+ checks: { checkConsole, checkErrors, checkNetwork, expectedSelector, expectedText, screenshotPath, url },
466
+ stdin: JSON.stringify(steps.map((step) => step.args)),
467
+ steps,
468
+ },
469
+ };
470
+ }
471
+
472
+ function compileAgentBrowserSourceLookup(input: unknown): { compiled?: CompiledAgentBrowserSourceLookup; error?: string } {
473
+ if (!isRecord(input)) {
474
+ return { error: "sourceLookup must be an object." };
475
+ }
476
+ const selector = input.selector;
477
+ const reactFiberId = input.reactFiberId;
478
+ const componentName = input.componentName;
479
+ if (selector !== undefined && (typeof selector !== "string" || selector.trim().length === 0)) {
480
+ return { error: "sourceLookup.selector must be a non-empty string when provided." };
481
+ }
482
+ if (reactFiberId !== undefined && (typeof reactFiberId !== "string" || reactFiberId.trim().length === 0)) {
483
+ return { error: "sourceLookup.reactFiberId must be a non-empty string when provided." };
484
+ }
485
+ if (componentName !== undefined && (typeof componentName !== "string" || componentName.trim().length === 0)) {
486
+ return { error: "sourceLookup.componentName must be a non-empty string when provided." };
487
+ }
488
+ if (selector === undefined && reactFiberId === undefined && componentName === undefined) {
489
+ return { error: "sourceLookup requires selector, reactFiberId, or componentName." };
490
+ }
491
+ if (input.includeDomHints !== undefined && typeof input.includeDomHints !== "boolean") {
492
+ return { error: "sourceLookup.includeDomHints must be a boolean when provided." };
493
+ }
494
+ const rawMaxWorkspaceFiles = input.maxWorkspaceFiles;
495
+ if (rawMaxWorkspaceFiles !== undefined && (typeof rawMaxWorkspaceFiles !== "number" || !Number.isInteger(rawMaxWorkspaceFiles) || rawMaxWorkspaceFiles <= 0)) {
496
+ return { error: "sourceLookup.maxWorkspaceFiles must be a positive integer when provided." };
497
+ }
498
+ if (typeof rawMaxWorkspaceFiles === "number" && rawMaxWorkspaceFiles > SOURCE_LOOKUP_MAX_WORKSPACE_FILES) {
499
+ return { error: `sourceLookup.maxWorkspaceFiles must be ${SOURCE_LOOKUP_MAX_WORKSPACE_FILES} or less.` };
500
+ }
501
+ const includeDomHints = input.includeDomHints !== false;
502
+ const maxWorkspaceFiles = (rawMaxWorkspaceFiles as number | undefined) ?? SOURCE_LOOKUP_DEFAULT_MAX_WORKSPACE_FILES;
503
+ const steps: CompiledAgentBrowserSourceLookupStep[] = [];
504
+ if (typeof selector === "string") {
505
+ steps.push({ action: "dom", args: ["is", "visible", selector] });
506
+ if (includeDomHints) {
507
+ steps.push({ action: "dom", args: ["get", "html", selector] });
508
+ }
509
+ }
510
+ if (typeof reactFiberId === "string") {
511
+ steps.push({ action: "react", args: ["react", "inspect", reactFiberId] });
512
+ }
513
+ if (typeof componentName === "string") {
514
+ steps.push({ action: "react", args: ["react", "tree"] });
515
+ }
516
+ return {
517
+ compiled: {
518
+ args: ["batch"],
519
+ query: { componentName, includeDomHints, maxWorkspaceFiles, reactFiberId, selector },
520
+ stdin: JSON.stringify(steps.map((step) => step.args)),
521
+ steps,
522
+ },
523
+ };
524
+ }
525
+
526
+ function extractStringField(value: Record<string, unknown>, names: string[]): string | undefined {
527
+ for (const name of names) {
528
+ const field = value[name];
529
+ if (typeof field === "string" && field.trim().length > 0) return field;
530
+ }
531
+ return undefined;
532
+ }
533
+
534
+ function extractNumberField(value: Record<string, unknown>, names: string[]): number | undefined {
535
+ for (const name of names) {
536
+ const field = value[name];
537
+ if (typeof field === "number" && Number.isFinite(field)) return field;
538
+ if (typeof field === "string" && /^\d+$/.test(field)) return Number(field);
539
+ }
540
+ return undefined;
541
+ }
542
+
543
+ function candidateKey(candidate: AgentBrowserSourceLookupCandidate): string {
544
+ return [candidate.source, candidate.file ?? "", candidate.line ?? "", candidate.column ?? "", candidate.componentName ?? ""].join(":");
545
+ }
546
+
547
+ function addSourceLookupCandidate(candidates: AgentBrowserSourceLookupCandidate[], candidate: AgentBrowserSourceLookupCandidate): void {
548
+ if (!candidates.some((existing) => candidateKey(existing) === candidateKey(candidate))) {
549
+ candidates.push(candidate);
550
+ }
551
+ }
552
+
553
+ function collectSourceCandidatesFromValue(value: unknown, source: "react-inspect" | "dom-attribute", candidates: AgentBrowserSourceLookupCandidate[], evidence: string[], depth = 0): void {
554
+ if (depth > 6 || value === undefined || value === null) return;
555
+ if (typeof value === "string") {
556
+ const sourcePattern = /([A-Za-z0-9_./@-]+\.(?:tsx|jsx|ts|js))(?:[:#](\d+))?(?:[:#](\d+))?/g;
557
+ for (const match of value.matchAll(sourcePattern)) {
558
+ addSourceLookupCandidate(candidates, {
559
+ column: match[3] ? Number(match[3]) : undefined,
560
+ confidence: source === "react-inspect" ? "high" : "medium",
561
+ evidence,
562
+ file: match[1],
563
+ line: match[2] ? Number(match[2]) : undefined,
564
+ source,
565
+ });
566
+ }
567
+ return;
568
+ }
569
+ if (Array.isArray(value)) {
570
+ for (const item of value) collectSourceCandidatesFromValue(item, source, candidates, evidence, depth + 1);
571
+ return;
572
+ }
573
+ if (!isRecord(value)) return;
574
+ const file = extractStringField(value, ["file", "fileName", "filename", "filePath", "path", "source", "url"]);
575
+ if (file && /\.(?:tsx|jsx|ts|js)(?:$|[:?#])/.test(file)) {
576
+ addSourceLookupCandidate(candidates, {
577
+ column: extractNumberField(value, ["column", "columnNumber", "col"]),
578
+ confidence: source === "react-inspect" ? "high" : "medium",
579
+ evidence,
580
+ file,
581
+ line: extractNumberField(value, ["line", "lineNumber"]),
582
+ source,
583
+ });
584
+ }
585
+ for (const nested of Object.values(value)) {
586
+ collectSourceCandidatesFromValue(nested, source, candidates, evidence, depth + 1);
587
+ }
588
+ }
589
+
590
+ function getHtmlAttributeValue(html: string, name: string): string | undefined {
591
+ const pattern = new RegExp(`${name}=["']([^"']+)["']`, "i");
592
+ return pattern.exec(html)?.[1];
593
+ }
594
+
595
+ function collectDomSourceCandidates(html: unknown, candidates: AgentBrowserSourceLookupCandidate[]): void {
596
+ if (typeof html !== "string") return;
597
+ const file = getHtmlAttributeValue(html, "(?:data-source-file|data-file|data-component-file|data-source)");
598
+ if (file && /\.(?:tsx|jsx|ts|js)$/.test(file)) {
599
+ const line = getHtmlAttributeValue(html, "(?:data-source-line|data-line)");
600
+ const column = getHtmlAttributeValue(html, "(?:data-source-column|data-column)");
601
+ addSourceLookupCandidate(candidates, {
602
+ column: column && /^\d+$/.test(column) ? Number(column) : undefined,
603
+ confidence: "medium",
604
+ evidence: ["selector HTML contained source-like data attributes"],
605
+ file,
606
+ line: line && /^\d+$/.test(line) ? Number(line) : undefined,
607
+ source: "dom-attribute",
608
+ });
609
+ }
610
+ collectSourceCandidatesFromValue(html, "dom-attribute", candidates, ["selector HTML contained source-like text"]);
611
+ }
612
+
613
+ async function walkWorkspaceSourceFiles(root: string, maxFiles: number): Promise<string[]> {
614
+ const files: string[] = [];
615
+ async function visit(directory: string): Promise<void> {
616
+ if (files.length >= maxFiles) return;
617
+ let entries: Array<{ isDirectory: () => boolean; isFile: () => boolean; name: string }>;
618
+ try {
619
+ entries = await readdir(directory, { withFileTypes: true });
620
+ } catch {
621
+ return;
622
+ }
623
+ for (const entry of entries) {
624
+ if (files.length >= maxFiles) return;
625
+ const path = join(directory, entry.name);
626
+ if (entry.isDirectory()) {
627
+ if (!SOURCE_LOOKUP_IGNORED_DIRECTORIES.has(entry.name)) await visit(path);
628
+ } else if (entry.isFile() && SOURCE_LOOKUP_WORKSPACE_EXTENSIONS.has(extname(entry.name))) {
629
+ files.push(path);
630
+ }
631
+ }
632
+ }
633
+ await visit(root);
634
+ return files;
635
+ }
636
+
637
+ function escapeRegExp(value: string): string {
638
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
639
+ }
640
+
641
+ async function collectWorkspaceComponentCandidates(query: CompiledAgentBrowserSourceLookup["query"], cwd: string, candidates: AgentBrowserSourceLookupCandidate[], limitations: string[]): Promise<void> {
642
+ if (!query.componentName) return;
643
+ const files = await walkWorkspaceSourceFiles(cwd, query.maxWorkspaceFiles);
644
+ if (files.length >= query.maxWorkspaceFiles) {
645
+ limitations.push(`Workspace source scan stopped at ${query.maxWorkspaceFiles} files.`);
646
+ }
647
+ const componentPattern = new RegExp(`(?:function|class)\\s+${escapeRegExp(query.componentName)}\\b|(?:const|let|var)\\s+${escapeRegExp(query.componentName)}\\s*=|export\\s+default\\s+function\\s+${escapeRegExp(query.componentName)}\\b`);
648
+ for (const file of files) {
649
+ let text: string;
650
+ try {
651
+ text = await readFile(file, "utf8");
652
+ } catch {
653
+ continue;
654
+ }
655
+ const match = componentPattern.exec(text);
656
+ if (!match) continue;
657
+ const line = text.slice(0, match.index).split("\n").length;
658
+ addSourceLookupCandidate(candidates, {
659
+ componentName: query.componentName,
660
+ confidence: "low",
661
+ evidence: [`local workspace contains a matching ${query.componentName} declaration`],
662
+ file,
663
+ line,
664
+ source: "workspace-search",
665
+ });
666
+ if (candidates.filter((candidate) => candidate.source === "workspace-search").length >= 10) break;
667
+ }
668
+ }
669
+
670
+ function validateLookupMaxWorkspaceFiles(value: unknown, fieldName: string): { value?: number; error?: string } {
671
+ if (value === undefined) return { value: SOURCE_LOOKUP_DEFAULT_MAX_WORKSPACE_FILES };
672
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
673
+ return { error: `${fieldName} must be a positive integer when provided.` };
674
+ }
675
+ if (value > SOURCE_LOOKUP_MAX_WORKSPACE_FILES) {
676
+ return { error: `${fieldName} must be ${SOURCE_LOOKUP_MAX_WORKSPACE_FILES} or less.` };
677
+ }
678
+ return { value };
679
+ }
680
+
681
+ async function analyzeSourceLookupResults(data: unknown, compiled: CompiledAgentBrowserSourceLookup, cwd: string): Promise<AgentBrowserSourceLookupAnalysis> {
682
+ const items = getBatchResultItems(data);
683
+ const candidates: AgentBrowserSourceLookupCandidate[] = [];
684
+ const limitations = [
685
+ "Experimental lookup only reports candidates with evidence; it cannot guarantee a DOM node maps to one source file.",
686
+ "React source hints require the page to be opened with --enable react-devtools and source information from the app build.",
687
+ ];
688
+ let unsupported = false;
689
+ for (const item of items) {
690
+ const command = Array.isArray(item.command) ? item.command : [];
691
+ const result = isRecord(item.result) && "data" in item.result ? item.result.data : item.result;
692
+ if (item.success === false && command[0] === "react") unsupported = true;
693
+ if (command[0] === "react" && command[1] === "inspect") {
694
+ collectSourceCandidatesFromValue(result, "react-inspect", candidates, ["react inspect returned source-like metadata"]);
695
+ }
696
+ if (command[0] === "get" && command[1] === "html") {
697
+ collectDomSourceCandidates(result, candidates);
698
+ }
699
+ }
700
+ await collectWorkspaceComponentCandidates(compiled.query, cwd, candidates, limitations);
701
+ const status: AgentBrowserSourceLookupStatus = candidates.length > 0 ? "candidates-found" : unsupported ? "unsupported" : "no-candidates";
702
+ return {
703
+ candidates,
704
+ limitations,
705
+ status,
706
+ summary: candidates.length > 0
707
+ ? `Source lookup found ${candidates.length} candidate location(s).`
708
+ : unsupported
709
+ ? "Source lookup could not inspect React metadata in this session."
710
+ : "Source lookup found no candidate locations.",
711
+ };
712
+ }
713
+
714
+ function compileAgentBrowserNetworkSourceLookup(input: unknown): { compiled?: CompiledAgentBrowserNetworkSourceLookup; error?: string } {
715
+ if (!isRecord(input)) return { error: "networkSourceLookup must be an object." };
716
+ const filter = input.filter;
717
+ const requestId = input.requestId;
718
+ const url = input.url;
719
+ if (filter !== undefined && (typeof filter !== "string" || filter.trim().length === 0)) return { error: "networkSourceLookup.filter must be a non-empty string when provided." };
720
+ if (requestId !== undefined && (typeof requestId !== "string" || requestId.trim().length === 0)) return { error: "networkSourceLookup.requestId must be a non-empty string when provided." };
721
+ if (url !== undefined && (typeof url !== "string" || url.trim().length === 0)) return { error: "networkSourceLookup.url must be a non-empty string when provided." };
722
+ if (filter === undefined && requestId === undefined && url === undefined) return { error: "networkSourceLookup requires requestId, filter, or url." };
723
+ const maxWorkspaceFiles = validateLookupMaxWorkspaceFiles(input.maxWorkspaceFiles, "networkSourceLookup.maxWorkspaceFiles");
724
+ if (maxWorkspaceFiles.error) return { error: maxWorkspaceFiles.error };
725
+ const steps: Array<{ action: "network"; args: string[] }> = [];
726
+ if (typeof requestId === "string") {
727
+ steps.push({ action: "network", args: ["network", "request", requestId] });
728
+ }
729
+ const effectiveFilter = typeof filter === "string" ? filter : typeof url === "string" ? url : undefined;
730
+ if (effectiveFilter) {
731
+ steps.push({ action: "network", args: ["network", "requests", "--filter", effectiveFilter] });
732
+ }
733
+ return { compiled: { args: ["batch"], query: { filter, maxWorkspaceFiles: maxWorkspaceFiles.value as number, requestId, url }, stdin: JSON.stringify(steps.map((step) => step.args)), steps } };
734
+ }
735
+
736
+ function getResultPayload(item: Record<string, unknown>): unknown {
737
+ return isRecord(item.result) && "data" in item.result ? item.result.data : item.result;
738
+ }
739
+
740
+ function networkRequestMatchesQuery(url: string | undefined, queryText: string | undefined): boolean {
741
+ return queryText === undefined || url === undefined || url.includes(queryText) || queryText.includes(url);
742
+ }
743
+
744
+ function isFailedNetworkRecord(request: Record<string, unknown>): boolean {
745
+ const status = typeof request.status === "number" ? request.status : undefined;
746
+ const error = typeof request.error === "string" ? request.error : undefined;
747
+ return request.failed === true || error !== undefined || (status !== undefined && status >= 400);
748
+ }
749
+
750
+ function getFailedNetworkRequests(data: unknown, queryText?: string): AgentBrowserNetworkSourceLookupRequest[] {
751
+ const failed: AgentBrowserNetworkSourceLookupRequest[] = [];
752
+ for (const item of getBatchResultItems(data)) {
753
+ const payload = getResultPayload(item);
754
+ const requests = isRecord(payload) && Array.isArray(payload.requests) ? payload.requests : Array.isArray(payload) ? payload : isRecord(payload) ? [payload] : [];
755
+ for (const request of requests) {
756
+ if (!isRecord(request)) continue;
757
+ const url = typeof request.url === "string" ? request.url : undefined;
758
+ if (!networkRequestMatchesQuery(url, queryText) || !isFailedNetworkRecord(request)) continue;
759
+ failed.push({
760
+ error: typeof request.error === "string" ? request.error : undefined,
761
+ method: typeof request.method === "string" ? request.method : undefined,
762
+ requestId: typeof request.id === "string" ? request.id : typeof request.requestId === "string" ? request.requestId : undefined,
763
+ status: typeof request.status === "number" ? request.status : undefined,
764
+ url,
765
+ });
766
+ }
767
+ }
768
+ return failed;
769
+ }
770
+
771
+ function addNetworkCandidate(candidates: AgentBrowserNetworkSourceLookupCandidate[], candidate: AgentBrowserNetworkSourceLookupCandidate): void {
772
+ const key = [candidate.source, candidate.file ?? "", candidate.line ?? "", candidate.requestUrl ?? ""].join(":");
773
+ if (!candidates.some((existing) => [existing.source, existing.file ?? "", existing.line ?? "", existing.requestUrl ?? ""].join(":") === key)) candidates.push(candidate);
774
+ }
775
+
776
+ function collectInitiatorCandidates(data: unknown, failedRequests: AgentBrowserNetworkSourceLookupRequest[], candidates: AgentBrowserNetworkSourceLookupCandidate[]): void {
777
+ const failedRequestIds = new Set(failedRequests.map((request) => request.requestId).filter((value): value is string => value !== undefined));
778
+ const failedRequestUrls = new Set(failedRequests.map((request) => request.url).filter((value): value is string => value !== undefined));
779
+ for (const item of getBatchResultItems(data)) {
780
+ const payload = getResultPayload(item);
781
+ const requestValues = isRecord(payload) && Array.isArray(payload.requests) ? payload.requests : [payload];
782
+ for (const value of requestValues) {
783
+ if (!isRecord(value)) continue;
784
+ const requestUrl = typeof value.url === "string" ? value.url : undefined;
785
+ const requestId = typeof value.id === "string" ? value.id : typeof value.requestId === "string" ? value.requestId : undefined;
786
+ const correlatesWithFailedRequest = (requestId !== undefined && failedRequestIds.has(requestId)) || (requestUrl !== undefined && failedRequestUrls.has(requestUrl));
787
+ if (!correlatesWithFailedRequest && !isFailedNetworkRecord(value)) continue;
788
+ for (const field of [value.initiator, value.stack, value.source, value.trace]) {
789
+ const localCandidates: AgentBrowserSourceLookupCandidate[] = [];
790
+ collectSourceCandidatesFromValue(field, "dom-attribute", localCandidates, ["failed network request included source-like initiator metadata"]);
791
+ for (const candidate of localCandidates) {
792
+ addNetworkCandidate(candidates, { confidence: "medium", evidence: candidate.evidence, file: candidate.file, line: candidate.line, requestUrl, source: "initiator" });
793
+ }
794
+ }
795
+ }
796
+ }
797
+ }
798
+
799
+ async function collectWorkspaceRequestCandidates(query: CompiledAgentBrowserNetworkSourceLookup["query"], failedRequests: AgentBrowserNetworkSourceLookupRequest[], cwd: string, candidates: AgentBrowserNetworkSourceLookupCandidate[], limitations: string[]): Promise<void> {
800
+ const needles = [...new Set([query.url, query.filter, ...failedRequests.map((request) => request.url)].filter((value): value is string => typeof value === "string" && value.length > 0).flatMap((value) => {
801
+ try {
802
+ const parsed = new URL(value);
803
+ return [value, parsed.pathname].filter((item) => item && item !== "/");
804
+ } catch {
805
+ return [value];
806
+ }
807
+ }))].slice(0, 8);
808
+ if (needles.length === 0) return;
809
+ const files = await walkWorkspaceSourceFiles(cwd, query.maxWorkspaceFiles);
810
+ if (files.length >= query.maxWorkspaceFiles) limitations.push(`Workspace source scan stopped at ${query.maxWorkspaceFiles} files.`);
811
+ for (const file of files) {
812
+ let text: string;
813
+ try { text = await readFile(file, "utf8"); } catch { continue; }
814
+ for (const needle of needles) {
815
+ const index = text.indexOf(needle);
816
+ if (index === -1) continue;
817
+ addNetworkCandidate(candidates, { confidence: "low", evidence: [`local workspace contains request URL literal ${needle}`], file, line: text.slice(0, index).split("\n").length, requestUrl: needle, source: "workspace-search" });
818
+ if (candidates.filter((candidate) => candidate.source === "workspace-search").length >= 10) return;
819
+ }
820
+ }
821
+ }
822
+
823
+ function redactNetworkSourceLookupUrl(value: string | undefined): string | undefined {
824
+ if (!value) return value;
825
+ try {
826
+ const isRelative = value.startsWith("/");
827
+ const url = new URL(value, isRelative ? "https://redacted.invalid" : undefined);
828
+ url.username = url.username ? "[REDACTED]" : "";
829
+ url.password = url.password ? "[REDACTED]" : "";
830
+ for (const key of [...url.searchParams.keys()]) {
831
+ url.searchParams.set(key, "[REDACTED]");
832
+ }
833
+ if (/(?:token|secret|password|passwd|pwd|key|auth|session|jwt|credential)/i.test(url.hash)) {
834
+ url.hash = "#[REDACTED]";
835
+ }
836
+ return isRelative ? `${url.pathname}${url.search}${url.hash}` : url.toString();
837
+ } catch {
838
+ return redactSensitiveText(value
839
+ .replace(/([a-z][a-z0-9+.-]*:\/\/)\S+:\S+@/gi, "$1[REDACTED]@")
840
+ .replace(/([?&][^=]+)=([^&#\s"'\]]+)/g, "$1=[REDACTED]"));
841
+ }
842
+ }
843
+
844
+ function redactNetworkSourceLookupArgs(args: string[]): string[] {
845
+ return redactInvocationArgs(args).map((arg) => redactNetworkSourceLookupUrl(arg) ?? arg);
846
+ }
847
+
848
+ function redactNetworkSourceLookupSurface(value: unknown): unknown {
849
+ if (typeof value === "string") return redactNetworkSourceLookupUrl(value) ?? value;
850
+ if (Array.isArray(value)) return value.map((item) => redactNetworkSourceLookupSurface(item));
851
+ if (!isRecord(value)) return value;
852
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactNetworkSourceLookupSurface(item)]));
853
+ }
854
+
855
+ function redactNetworkSourceLookupAnalysis(analysis: AgentBrowserNetworkSourceLookupAnalysis): AgentBrowserNetworkSourceLookupAnalysis {
856
+ return {
857
+ ...analysis,
858
+ candidates: analysis.candidates.map((candidate) => ({
859
+ ...candidate,
860
+ evidence: candidate.evidence.map((item) => redactNetworkSourceLookupUrl(item) ?? redactSensitiveText(item)),
861
+ file: redactNetworkSourceLookupUrl(candidate.file),
862
+ requestUrl: redactNetworkSourceLookupUrl(candidate.requestUrl),
863
+ })),
864
+ failedRequests: analysis.failedRequests.map((request) => ({ ...request, error: redactNetworkSourceLookupUrl(request.error), url: redactNetworkSourceLookupUrl(request.url) })),
865
+ };
866
+ }
867
+
868
+ async function analyzeNetworkSourceLookupResults(data: unknown, compiled: CompiledAgentBrowserNetworkSourceLookup, cwd: string): Promise<AgentBrowserNetworkSourceLookupAnalysis> {
869
+ const limitations = [
870
+ "Experimental network source hints report candidates only; failed requests can be triggered indirectly by frameworks, caches, service workers, or third-party scripts.",
871
+ "Initiator/source-map metadata is upstream/browser-build dependent and may be absent.",
872
+ ];
873
+ const failedRequests = getFailedNetworkRequests(data, compiled.query.url ?? compiled.query.filter);
874
+ const candidates: AgentBrowserNetworkSourceLookupCandidate[] = [];
875
+ collectInitiatorCandidates(data, failedRequests, candidates);
876
+ await collectWorkspaceRequestCandidates(compiled.query, failedRequests, cwd, candidates, limitations);
877
+ const status: AgentBrowserNetworkSourceLookupStatus = failedRequests.length === 0 ? "no-failed-requests" : candidates.length > 0 ? "failed-requests-found" : "no-candidates";
878
+ return { candidates, failedRequests, limitations, status, summary: failedRequests.length === 0 ? "Network source lookup found no failed requests." : candidates.length > 0 ? `Network source lookup found ${failedRequests.length} failed request(s) and ${candidates.length} candidate source hint(s).` : `Network source lookup found ${failedRequests.length} failed request(s) but no source candidates.` };
879
+ }
880
+
881
+ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: CompiledAgentBrowserSemanticAction; error?: string } {
882
+ if (!isRecord(input)) {
883
+ return { error: "semanticAction must be an object." };
884
+ }
885
+ const action = input.action;
886
+ const locator = input.locator;
887
+ const value = input.value;
888
+ const text = input.text;
889
+ const role = input.role;
890
+ const name = input.name;
891
+ if (typeof action !== "string" || !AGENT_BROWSER_SEMANTIC_ACTIONS.includes(action as AgentBrowserSemanticActionName)) {
892
+ return { error: `semanticAction.action must be one of: ${AGENT_BROWSER_SEMANTIC_ACTIONS.join(", ")}.` };
893
+ }
894
+ if (typeof locator !== "string" || !AGENT_BROWSER_SEMANTIC_LOCATORS.includes(locator as AgentBrowserSemanticLocator)) {
895
+ return { error: `semanticAction.locator must be one of: ${AGENT_BROWSER_SEMANTIC_LOCATORS.join(", ")}.` };
896
+ }
897
+ if (typeof value !== "string" || value.trim().length === 0) {
898
+ return { error: "semanticAction.value must be a non-empty string." };
899
+ }
900
+ if (text !== undefined && typeof text !== "string") {
901
+ return { error: "semanticAction.text must be a string when provided." };
902
+ }
903
+ if ((action === "fill" || action === "select") && (typeof text !== "string" || text.length === 0)) {
904
+ return { error: `semanticAction.text is required for ${action}.` };
905
+ }
906
+ if (action !== "fill" && action !== "select" && text !== undefined) {
907
+ return { error: `semanticAction.text is only supported for fill and select actions.` };
908
+ }
909
+ if (role !== undefined && (locator !== "role" || role !== value)) {
910
+ return { error: "semanticAction.role is only supported for locator=role and must match value." };
911
+ }
912
+ if (name !== undefined && (locator !== "role" || typeof name !== "string" || name.length === 0)) {
913
+ return { error: "semanticAction.name is only supported as a non-empty string for locator=role." };
914
+ }
915
+ const args = ["find", locator, value, action];
916
+ if (action === "fill" || action === "select") {
917
+ args.push(text as string);
918
+ }
919
+ if (locator === "role" && typeof name === "string") {
920
+ args.push("--name", name);
921
+ }
922
+ return { compiled: { action: action as AgentBrowserSemanticActionName, locator: locator as AgentBrowserSemanticLocator, args } };
923
+ }
924
+
108
925
  const TUI_COLLAPSED_OUTPUT_MAX_LINES = 10;
109
926
  const TUI_INVOCATION_PREVIEW_MAX_CHARS = 120;
110
927
  const ANSI_CONTROL_SEQUENCE_PATTERN = /\x1B(?:\][^\x07\x1B]*(?:\x07|\x1B\\)|\[[0-?]*[ -/]*[@-~]|P[^\x1B]*(?:\x1B\\)|_[^\x1B]*(?:\x1B\\)|\^[^\x1B]*(?:\x1B\\)|[@-Z\\-_])/g;
@@ -178,7 +995,15 @@ function formatVisualTruncationNotice(remainingLines: number, totalLines: number
178
995
 
179
996
  function formatAgentBrowserRenderCall(args: unknown, theme: Theme): string {
180
997
  const input = isRecord(args) ? args : {};
181
- const rawArgs = Array.isArray(input.args) ? input.args.filter((value): value is string => typeof value === "string") : [];
998
+ const semanticAction = compileAgentBrowserSemanticAction(input.semanticAction);
999
+ const job = compileAgentBrowserJob(input.job);
1000
+ const qa = compileAgentBrowserQaPreset(input.qa);
1001
+ const sourceLookup = compileAgentBrowserSourceLookup(input.sourceLookup);
1002
+ const networkSourceLookup = compileAgentBrowserNetworkSourceLookup(input.networkSourceLookup);
1003
+ const generatedBatch = networkSourceLookup.compiled ?? sourceLookup.compiled ?? job.compiled ?? qa.compiled;
1004
+ const rawArgs = Array.isArray(input.args)
1005
+ ? input.args.filter((value): value is string => typeof value === "string")
1006
+ : (semanticAction.compiled?.args ?? generatedBatch?.args ?? []);
182
1007
  const redactedArgs = redactInvocationArgs(rawArgs);
183
1008
  const invocation = sanitizeDisplayText(redactedArgs.join(" ")).replace(/\s+/g, " ").trim();
184
1009
  const invocationPreview =
@@ -1723,17 +2548,77 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1723
2548
  return component;
1724
2549
  },
1725
2550
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
1726
- const redactedArgs = redactInvocationArgs(params.args);
1727
- const validationError = validateToolArgs(params.args) ?? getBatchAnnotateValidationError(params.args, params.stdin);
2551
+ const semanticActionResult = params.semanticAction === undefined ? {} : compileAgentBrowserSemanticAction(params.semanticAction);
2552
+ const jobResult = params.job === undefined ? {} : compileAgentBrowserJob(params.job);
2553
+ const qaResult = params.qa === undefined ? {} : compileAgentBrowserQaPreset(params.qa);
2554
+ const sourceLookupResult = params.sourceLookup === undefined ? {} : compileAgentBrowserSourceLookup(params.sourceLookup);
2555
+ const networkSourceLookupResult = params.networkSourceLookup === undefined ? {} : compileAgentBrowserNetworkSourceLookup(params.networkSourceLookup);
2556
+ const hasExplicitArgs = Array.isArray(params.args);
2557
+ const explicitInputModes = [hasExplicitArgs, Boolean(semanticActionResult.compiled), Boolean(jobResult.compiled), Boolean(qaResult.compiled), Boolean(sourceLookupResult.compiled), Boolean(networkSourceLookupResult.compiled)].filter(Boolean).length;
2558
+ const semanticActionError = semanticActionResult.error;
2559
+ const jobError = jobResult.error;
2560
+ const qaError = qaResult.error;
2561
+ const sourceLookupError = sourceLookupResult.error;
2562
+ const networkSourceLookupError = networkSourceLookupResult.error;
2563
+ const inputModeError = explicitInputModes !== 1
2564
+ ? "Provide exactly one of args, semanticAction, job, qa, sourceLookup, or networkSourceLookup."
2565
+ : undefined;
2566
+ const compiledSemanticAction = semanticActionResult.compiled;
2567
+ const compiledQaPreset = qaResult.compiled;
2568
+ const compiledSourceLookup = sourceLookupResult.compiled;
2569
+ const compiledNetworkSourceLookup = networkSourceLookupResult.compiled;
2570
+ const compiledJob = jobResult.compiled ?? compiledQaPreset;
2571
+ const compiledGeneratedBatch = compiledNetworkSourceLookup ?? compiledSourceLookup ?? compiledJob;
2572
+ const toolArgs = compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? params.args ?? [];
2573
+ const toolStdin = compiledGeneratedBatch?.stdin ?? params.stdin;
2574
+ const redactedArgs = redactInvocationArgs(toolArgs);
2575
+ const generatedStdinError = compiledGeneratedBatch && params.stdin !== undefined ? "Do not provide stdin with job, qa, sourceLookup, or networkSourceLookup; those modes generate their own batch stdin." : undefined;
2576
+ const validationError = semanticActionError ?? jobError ?? qaError ?? sourceLookupError ?? networkSourceLookupError ?? inputModeError ?? generatedStdinError ?? validateToolArgs(toolArgs) ?? getBatchAnnotateValidationError(toolArgs, toolStdin);
2577
+ const redactedCompiledSemanticAction = compiledSemanticAction
2578
+ ? { ...compiledSemanticAction, args: redactInvocationArgs(compiledSemanticAction.args) }
2579
+ : undefined;
2580
+ const redactedCompiledJobSteps = compiledJob?.steps.map((step) => ({ ...step, args: redactInvocationArgs(step.args) }));
2581
+ const redactedCompiledJob = compiledJob && redactedCompiledJobSteps
2582
+ ? { ...compiledJob, stdin: JSON.stringify(redactedCompiledJobSteps.map((step) => step.args)), steps: redactedCompiledJobSteps }
2583
+ : undefined;
2584
+ const redactedCompiledQaPreset = compiledQaPreset && redactedCompiledJob
2585
+ ? { ...redactedCompiledJob, checks: compiledQaPreset.checks }
2586
+ : undefined;
2587
+ const redactedCompiledSourceLookupSteps = compiledSourceLookup?.steps.map((step) => ({ ...step, args: redactInvocationArgs(step.args) }));
2588
+ const redactedCompiledSourceLookup = compiledSourceLookup && redactedCompiledSourceLookupSteps
2589
+ ? { ...compiledSourceLookup, stdin: JSON.stringify(redactedCompiledSourceLookupSteps.map((step) => step.args)), steps: redactedCompiledSourceLookupSteps }
2590
+ : undefined;
2591
+ const redactedCompiledNetworkSourceLookupSteps = compiledNetworkSourceLookup?.steps.map((step) => ({ ...step, args: redactNetworkSourceLookupArgs(step.args) }));
2592
+ const redactedCompiledNetworkSourceLookup = compiledNetworkSourceLookup && redactedCompiledNetworkSourceLookupSteps
2593
+ ? {
2594
+ ...compiledNetworkSourceLookup,
2595
+ query: {
2596
+ ...compiledNetworkSourceLookup.query,
2597
+ filter: redactNetworkSourceLookupUrl(compiledNetworkSourceLookup.query.filter),
2598
+ url: redactNetworkSourceLookupUrl(compiledNetworkSourceLookup.query.url),
2599
+ },
2600
+ stdin: JSON.stringify(redactedCompiledNetworkSourceLookupSteps.map((step) => step.args)),
2601
+ steps: redactedCompiledNetworkSourceLookupSteps,
2602
+ }
2603
+ : undefined;
1728
2604
  if (validationError) {
1729
2605
  return {
1730
2606
  content: [{ type: "text", text: validationError }],
1731
- details: { args: redactedArgs, validationError },
2607
+ details: {
2608
+ args: redactedArgs,
2609
+ compiledJob: redactedCompiledJob,
2610
+ compiledQaPreset: redactedCompiledQaPreset,
2611
+ compiledSourceLookup: redactedCompiledSourceLookup,
2612
+ compiledNetworkSourceLookup: redactedCompiledNetworkSourceLookup,
2613
+ compiledSemanticAction: redactedCompiledSemanticAction,
2614
+ ...buildAgentBrowserResultCategoryDetails({ args: redactedArgs, errorText: validationError, succeeded: false, validationError }),
2615
+ validationError,
2616
+ },
1732
2617
  isError: true,
1733
2618
  };
1734
2619
  }
1735
- const preparedArgs = await prepareAgentBrowserArgs(params.args, params.stdin, ctx.cwd);
1736
- const userRequestedJson = params.args.includes("--json");
2620
+ const preparedArgs = await prepareAgentBrowserArgs(toolArgs, toolStdin, ctx.cwd);
2621
+ const userRequestedJson = toolArgs.includes("--json");
1737
2622
 
1738
2623
  const tabTargetUpdateOrder = ++sessionTabTargetUpdateOrder;
1739
2624
  const runTool = async (): Promise<AgentBrowserToolResult> => {
@@ -1757,10 +2642,15 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1757
2642
  content: [{ type: "text", text: executionPlan.validationError }],
1758
2643
  details: {
1759
2644
  args: redactedArgs,
2645
+ compiledJob: redactedCompiledJob,
2646
+ compiledQaPreset: redactedCompiledQaPreset,
2647
+ compiledSourceLookup: redactedCompiledSourceLookup,
2648
+ compiledNetworkSourceLookup: redactedCompiledNetworkSourceLookup,
1760
2649
  invalidValueFlag: executionPlan.invalidValueFlag,
1761
2650
  sessionMode,
1762
2651
  sessionRecoveryHint: redactedRecoveryHint,
1763
2652
  startupScopedFlags: executionPlan.startupScopedFlags,
2653
+ ...buildAgentBrowserResultCategoryDetails({ args: redactedArgs, command: executionPlan.commandInfo.command, errorText: executionPlan.validationError, succeeded: false, validationError: executionPlan.validationError }),
1764
2654
  validationError: executionPlan.validationError,
1765
2655
  },
1766
2656
  isError: true,
@@ -1771,7 +2661,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1771
2661
  const exactSensitiveValues = getExactSensitiveStdinValues({
1772
2662
  command: executionPlan.commandInfo.command,
1773
2663
  commandTokens,
1774
- stdin: params.stdin,
2664
+ stdin: toolStdin,
1775
2665
  });
1776
2666
  const traceOwnerGuardMessage = getTraceOwnerGuardMessage({
1777
2667
  command: executionPlan.commandInfo.command,
@@ -1788,6 +2678,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1788
2678
  compatibilityWorkaround,
1789
2679
  effectiveArgs: redactedEffectiveArgs,
1790
2680
  sessionMode,
2681
+ ...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: traceOwnerGuardMessage, succeeded: false, validationError: traceOwnerGuardMessage }),
1791
2682
  validationError: traceOwnerGuardMessage,
1792
2683
  ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
1793
2684
  },
@@ -1797,7 +2688,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1797
2688
  const stdinValidationError = validateStdinCommandContract({
1798
2689
  command: executionPlan.commandInfo.command,
1799
2690
  commandTokens,
1800
- stdin: params.stdin,
2691
+ stdin: toolStdin,
1801
2692
  });
1802
2693
  if (stdinValidationError) {
1803
2694
  return {
@@ -1808,13 +2699,14 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1808
2699
  compatibilityWorkaround,
1809
2700
  effectiveArgs: redactedEffectiveArgs,
1810
2701
  sessionMode,
2702
+ ...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: stdinValidationError, succeeded: false, validationError: stdinValidationError }),
1811
2703
  validationError: stdinValidationError,
1812
2704
  ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
1813
2705
  },
1814
2706
  isError: true,
1815
2707
  };
1816
2708
  }
1817
- const waitIpcTimeoutError = validateWaitIpcTimeoutContract(commandTokens, params.stdin);
2709
+ const waitIpcTimeoutError = validateWaitIpcTimeoutContract(commandTokens, toolStdin);
1818
2710
  if (waitIpcTimeoutError) {
1819
2711
  return {
1820
2712
  content: [{ type: "text", text: waitIpcTimeoutError }],
@@ -1824,6 +2716,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1824
2716
  compatibilityWorkaround,
1825
2717
  effectiveArgs: redactedEffectiveArgs,
1826
2718
  sessionMode,
2719
+ ...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: waitIpcTimeoutError, succeeded: false, timedOut: true, validationError: waitIpcTimeoutError }),
1827
2720
  validationError: waitIpcTimeoutError,
1828
2721
  ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
1829
2722
  },
@@ -1837,14 +2730,14 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1837
2730
  let includePinnedNavigationSummary = false;
1838
2731
  let sessionTabCorrection: OpenResultTabCorrection | undefined;
1839
2732
  let processArgs = executionPlan.effectiveArgs;
1840
- let processStdin = preparedArgs.stdin ?? params.stdin;
2733
+ let processStdin = preparedArgs.stdin ?? toolStdin;
1841
2734
  if (
1842
2735
  priorSessionTabTarget &&
1843
2736
  shouldPinSessionTabForCommand({
1844
2737
  command: executionPlan.commandInfo.command,
1845
2738
  commandTokens,
1846
2739
  sessionName: executionPlan.sessionName,
1847
- stdin: params.stdin,
2740
+ stdin: toolStdin,
1848
2741
  })
1849
2742
  ) {
1850
2743
  const plannedSessionTabSelection = await collectSessionTabSelection({
@@ -1854,7 +2747,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1854
2747
  target: priorSessionTabTarget,
1855
2748
  });
1856
2749
  if (plannedSessionTabSelection && executionPlan.sessionName) {
1857
- if (executionPlan.commandInfo.command === "eval" && params.stdin !== undefined) {
2750
+ if (executionPlan.commandInfo.command === "eval" && toolStdin !== undefined) {
1858
2751
  const appliedSessionTabSelection = await applyOpenResultTabCorrection({
1859
2752
  correction: plannedSessionTabSelection,
1860
2753
  cwd: ctx.cwd,
@@ -1872,6 +2765,8 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1872
2765
  effectiveArgs: redactedEffectiveArgs,
1873
2766
  sessionMode,
1874
2767
  sessionTabCorrection: plannedSessionTabSelection,
2768
+ ...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: error, failureCategory: "tab-drift", succeeded: false, tabDrift: true, validationError: error }),
2769
+ nextActions: buildAgentBrowserNextActions({ failureCategory: "tab-drift", resultCategory: "failure" }),
1875
2770
  validationError: error,
1876
2771
  ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
1877
2772
  },
@@ -1884,7 +2779,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1884
2779
  command: executionPlan.commandInfo.command,
1885
2780
  commandTokens,
1886
2781
  selectedTab: plannedSessionTabSelection.selectedTab,
1887
- stdin: params.stdin,
2782
+ stdin: toolStdin,
1888
2783
  });
1889
2784
  if (pinnedBatchPlan && "error" in pinnedBatchPlan) {
1890
2785
  return {
@@ -1896,6 +2791,8 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1896
2791
  effectiveArgs: redactedEffectiveArgs,
1897
2792
  sessionMode,
1898
2793
  sessionTabCorrection: plannedSessionTabSelection,
2794
+ ...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: pinnedBatchPlan.error, failureCategory: "tab-drift", succeeded: false, tabDrift: true, validationError: pinnedBatchPlan.error }),
2795
+ nextActions: buildAgentBrowserNextActions({ failureCategory: "tab-drift", resultCategory: "failure" }),
1899
2796
  validationError: pinnedBatchPlan.error,
1900
2797
  ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
1901
2798
  },
@@ -1943,6 +2840,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1943
2840
  effectiveArgs: redactedProcessArgs,
1944
2841
  sessionMode,
1945
2842
  sessionTabCorrection,
2843
+ ...buildAgentBrowserResultCategoryDetails({ args: redactedProcessArgs, command: executionPlan.commandInfo.command, errorText, failureCategory: "missing-binary", spawnError: processResult.spawnError.message, succeeded: false }),
1946
2844
  spawnError: processResult.spawnError.message,
1947
2845
  },
1948
2846
  isError: true,
@@ -1997,7 +2895,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1997
2895
  const plainTextInspection = executionPlan.plainTextInspection && processSucceeded;
1998
2896
  const parseSucceeded = plainTextInspection || parseError === undefined;
1999
2897
  const envelopeSuccess = plainTextInspection ? true : presentationEnvelope?.success !== false;
2000
- const succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
2898
+ let succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
2001
2899
  const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
2002
2900
  updateTraceOwnerState({
2003
2901
  command: executionPlan.commandInfo.command,
@@ -2025,7 +2923,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2025
2923
  if (
2026
2924
  succeeded &&
2027
2925
  executionPlan.sessionName &&
2028
- hasLaunchScopedTabCorrectionFlag(params.args) &&
2926
+ hasLaunchScopedTabCorrectionFlag(toolArgs) &&
2029
2927
  (executionPlan.commandInfo.command === "goto" ||
2030
2928
  executionPlan.commandInfo.command === "navigate" ||
2031
2929
  executionPlan.commandInfo.command === "open")
@@ -2165,7 +3063,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2165
3063
  exitCode: processResult.exitCode,
2166
3064
  parseError,
2167
3065
  plainTextInspection,
2168
- staleRefArgs: getStaleRefArgs(commandTokens, params.stdin),
3066
+ staleRefArgs: getStaleRefArgs(commandTokens, toolStdin),
2169
3067
  spawnError: processResult.spawnError,
2170
3068
  stderr: processResult.stderr,
2171
3069
  timedOut: processResult.timedOut,
@@ -2189,6 +3087,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2189
3087
  summary: `${redactedArgs.join(" ")} completed`,
2190
3088
  }
2191
3089
  : await buildToolPresentation({
3090
+ args: redactedProcessArgs,
2192
3091
  artifactManifest,
2193
3092
  artifactRequest: screenshotArtifactRequest,
2194
3093
  batchArtifactRequests: batchScreenshotArtifactRequests,
@@ -2220,6 +3119,29 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2220
3119
  if (presentation.artifactManifest) {
2221
3120
  artifactManifest = presentation.artifactManifest;
2222
3121
  }
3122
+ const qaPreset = compiledQaPreset ? analyzeQaPresetResults(presentationEnvelope?.data) : undefined;
3123
+ const sourceLookup = compiledSourceLookup ? await analyzeSourceLookupResults(presentationEnvelope?.data, compiledSourceLookup, ctx.cwd) : undefined;
3124
+ const networkSourceLookup = compiledNetworkSourceLookup ? redactNetworkSourceLookupAnalysis(await analyzeNetworkSourceLookupResults(presentationEnvelope?.data, compiledNetworkSourceLookup, ctx.cwd)) : undefined;
3125
+ if (networkSourceLookup && presentation.content[0]?.type === "text") {
3126
+ presentation.content[0] = { ...presentation.content[0], text: `${networkSourceLookup.summary}\n\n${presentation.content[0].text}` };
3127
+ } else if (networkSourceLookup) {
3128
+ presentation.content.unshift({ type: "text", text: networkSourceLookup.summary });
3129
+ }
3130
+ if (sourceLookup && presentation.content[0]?.type === "text") {
3131
+ presentation.content[0] = { ...presentation.content[0], text: `${sourceLookup.summary}\n\n${presentation.content[0].text}` };
3132
+ } else if (sourceLookup) {
3133
+ presentation.content.unshift({ type: "text", text: sourceLookup.summary });
3134
+ }
3135
+ if (qaPreset && !qaPreset.passed) {
3136
+ succeeded = false;
3137
+ presentation.failureCategory = "qa-failure";
3138
+ presentation.summary = qaPreset.summary;
3139
+ if (presentation.content[0]?.type === "text") {
3140
+ presentation.content[0] = { ...presentation.content[0], text: `${qaPreset.summary}\n\n${presentation.content[0].text}` };
3141
+ } else {
3142
+ presentation.content.unshift({ type: "text", text: qaPreset.summary });
3143
+ }
3144
+ }
2223
3145
  const warningText = aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
2224
3146
  const contentWithSessionWarnings = userRequestedJson && !plainTextInspection
2225
3147
  ? buildJsonVisibleContent({
@@ -2248,20 +3170,53 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2248
3170
  ? { ...item, text: exactRedactedText }
2249
3171
  : { ...item, text: redactSensitiveText(exactRedactedText) };
2250
3172
  });
3173
+ const categoryDetails = buildAgentBrowserResultCategoryDetails({
3174
+ artifacts: presentation.artifacts,
3175
+ args: redactedProcessArgs,
3176
+ command: executionPlan.commandInfo.command,
3177
+ confirmationRequired: presentation.summary.startsWith("Confirmation required"),
3178
+ errorText: errorText ?? presentation.summary,
3179
+ failureCategory: presentation.failureCategory ?? presentation.batchFailure?.failedStep.failureCategory,
3180
+ inspection: plainTextInspection,
3181
+ parseError,
3182
+ savedFile: presentation.savedFile,
3183
+ spawnError: processResult.spawnError?.message,
3184
+ succeeded,
3185
+ tabDrift: !succeeded && (aboutBlankSessionMismatch !== undefined || sessionTabCorrection !== undefined),
3186
+ timedOut: processResult.timedOut,
3187
+ validationError: undefined,
3188
+ });
3189
+ let nextActions = presentation.nextActions ? [...presentation.nextActions] : undefined;
3190
+ if (categoryDetails.failureCategory === "stale-ref" && redactedCompiledSemanticAction) {
3191
+ (nextActions ??= []).push({
3192
+ id: "retry-semantic-action-after-stale-ref",
3193
+ params: { args: redactedCompiledSemanticAction.args },
3194
+ reason: "Retry the same semantic target via its compiled find command after the upstream stale-ref failure proves the prior action did not execute.",
3195
+ safety: "Use only for the same intended target; direct stale @refs still require a fresh snapshot or stable locator before retrying.",
3196
+ tool: "agent_browser" as const,
3197
+ });
3198
+ }
2251
3199
  const details = {
2252
3200
  args: redactedArgs,
3201
+ compiledJob: redactedCompiledJob,
3202
+ compiledQaPreset: redactedCompiledQaPreset,
3203
+ compiledSourceLookup: redactedCompiledSourceLookup,
3204
+ compiledNetworkSourceLookup: redactedCompiledNetworkSourceLookup,
2253
3205
  artifactManifest: presentation.artifactManifest,
2254
3206
  artifactRetentionSummary: presentation.artifactRetentionSummary,
3207
+ artifactVerification: presentation.artifactVerification,
2255
3208
  artifacts: presentation.artifacts,
2256
3209
  batchFailure: presentation.batchFailure,
2257
3210
  batchSteps: presentation.batchSteps,
2258
3211
  command: executionPlan.commandInfo.command,
3212
+ compiledSemanticAction: redactedCompiledSemanticAction,
2259
3213
  compatibilityWorkaround,
2260
3214
  subcommand: executionPlan.commandInfo.subcommand,
2261
3215
  data: presentation.data,
2262
3216
  error: plainTextInspection ? undefined : presentationEnvelope?.error,
2263
3217
  inspection: plainTextInspection || undefined,
2264
3218
  navigationSummary,
3219
+ ...categoryDetails,
2265
3220
  aboutBlankSessionMismatch,
2266
3221
  openResultTabCorrection,
2267
3222
  effectiveArgs: redactedProcessArgs,
@@ -2271,9 +3226,14 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2271
3226
  fullOutputUnavailable: parseFailureOutput.fullOutputUnavailable,
2272
3227
  imagePath: presentation.imagePath,
2273
3228
  imagePaths: presentation.imagePaths,
3229
+ nextActions,
3230
+ pageChangeSummary: presentation.pageChangeSummary,
3231
+ qaPreset,
2274
3232
  parseError: plainTextInspection ? undefined : parseError,
2275
3233
  savedFile: presentation.savedFile,
2276
3234
  savedFilePath: presentation.savedFilePath,
3235
+ sourceLookup,
3236
+ networkSourceLookup,
2277
3237
  sessionMode,
2278
3238
  sessionTabCorrection,
2279
3239
  sessionTabTarget: currentSessionTabTarget,
@@ -2287,11 +3247,12 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2287
3247
  timeoutMs: processResult.timeoutMs,
2288
3248
  };
2289
3249
 
2290
- return {
3250
+ const result = {
2291
3251
  content: redactedContent,
2292
3252
  details: redactToolDetails(details, exactSensitiveValues),
2293
3253
  isError: !succeeded,
2294
3254
  };
3255
+ return compiledNetworkSourceLookup ? redactNetworkSourceLookupSurface(result) as typeof result : result;
2295
3256
  } finally {
2296
3257
  if (processResult.stdoutSpillPath) {
2297
3258
  await rm(processResult.stdoutSpillPath, { force: true }).catch(() => undefined);
@@ -2299,7 +3260,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2299
3260
  }
2300
3261
  };
2301
3262
 
2302
- return extractExplicitSessionName(params.args)
3263
+ return extractExplicitSessionName(toolArgs)
2303
3264
  ? runTool()
2304
3265
  : managedSessionExecutionQueue.run(runTool);
2305
3266
  },