pi-agent-browser-native 0.2.23 → 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.
- package/CHANGELOG.md +31 -0
- package/README.md +123 -9
- package/docs/ARCHITECTURE.md +48 -7
- package/docs/COMMAND_REFERENCE.md +567 -40
- package/docs/RELEASE.md +57 -7
- package/docs/REQUIREMENTS.md +13 -1
- package/docs/SUPPORT_MATRIX.md +65 -0
- package/docs/TOOL_CONTRACT.md +268 -19
- package/extensions/agent-browser/index.ts +1154 -25
- package/extensions/agent-browser/lib/playbook.ts +20 -10
- package/extensions/agent-browser/lib/results/presentation.ts +624 -33
- package/extensions/agent-browser/lib/results/shared.ts +365 -0
- package/extensions/agent-browser/lib/results.ts +12 -0
- package/extensions/agent-browser/lib/runtime.ts +75 -8
- package/package.json +4 -2
- package/scripts/agent-browser-capability-baseline.mjs +499 -110
- package/scripts/doctor.mjs +1 -1
|
@@ -6,11 +6,19 @@
|
|
|
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";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
highlightCode,
|
|
15
|
+
isToolCallEventType,
|
|
16
|
+
keyHint,
|
|
17
|
+
type AgentToolResult,
|
|
18
|
+
type ExtensionAPI,
|
|
19
|
+
type Theme,
|
|
20
|
+
} from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
14
22
|
import { Type } from "typebox";
|
|
15
23
|
|
|
16
24
|
import {
|
|
@@ -19,6 +27,8 @@ import {
|
|
|
19
27
|
} from "./lib/playbook.js";
|
|
20
28
|
import { SAFE_AGENT_BROWSER_OPERATION_TIMEOUT_MS, runAgentBrowserProcess } from "./lib/process.js";
|
|
21
29
|
import {
|
|
30
|
+
buildAgentBrowserNextActions,
|
|
31
|
+
buildAgentBrowserResultCategoryDetails,
|
|
22
32
|
buildToolPresentation,
|
|
23
33
|
getAgentBrowserErrorText,
|
|
24
34
|
parseAgentBrowserEnvelope,
|
|
@@ -68,16 +78,202 @@ const DEFAULT_SESSION_MODE = "auto" as const;
|
|
|
68
78
|
const DIRECT_AGENT_BROWSER_BASH_BYPASS_ENV = "PI_AGENT_BROWSER_ALLOW_DIRECT_BASH";
|
|
69
79
|
const PACKAGE_NAME = "pi-agent-browser-native";
|
|
70
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
|
+
|
|
71
205
|
const AGENT_BROWSER_PARAMS = Type.Object({
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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." })),
|
|
77
273
|
sessionMode: Type.Optional(
|
|
78
274
|
StringEnum(["auto", "fresh"] as const, {
|
|
79
275
|
description:
|
|
80
|
-
"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 --
|
|
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.",
|
|
81
277
|
default: DEFAULT_SESSION_MODE,
|
|
82
278
|
}),
|
|
83
279
|
),
|
|
@@ -97,6 +293,791 @@ function buildInvocationPreview(effectiveArgs: string[]): string {
|
|
|
97
293
|
return preview.length > 120 ? `${preview.slice(0, 117)}...` : preview;
|
|
98
294
|
}
|
|
99
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
|
+
|
|
925
|
+
const TUI_COLLAPSED_OUTPUT_MAX_LINES = 10;
|
|
926
|
+
const TUI_INVOCATION_PREVIEW_MAX_CHARS = 120;
|
|
927
|
+
const ANSI_CONTROL_SEQUENCE_PATTERN = /\x1B(?:\][^\x07\x1B]*(?:\x07|\x1B\\)|\[[0-?]*[ -/]*[@-~]|P[^\x1B]*(?:\x1B\\)|_[^\x1B]*(?:\x1B\\)|\^[^\x1B]*(?:\x1B\\)|[@-Z\\-_])/g;
|
|
928
|
+
const UNSAFE_DISPLAY_CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/g;
|
|
929
|
+
|
|
930
|
+
function sanitizeDisplayText(text: string): string {
|
|
931
|
+
return text
|
|
932
|
+
.replace(ANSI_CONTROL_SEQUENCE_PATTERN, "")
|
|
933
|
+
.replace(/\r/g, "")
|
|
934
|
+
.replace(UNSAFE_DISPLAY_CONTROL_PATTERN, "�");
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function replaceTabsForDisplay(text: string): string {
|
|
938
|
+
return text.replaceAll("\t", " ");
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function trimTrailingBlankLines(lines: string[]): string[] {
|
|
942
|
+
let end = lines.length;
|
|
943
|
+
while (end > 0 && lines[end - 1].trim().length === 0) {
|
|
944
|
+
end -= 1;
|
|
945
|
+
}
|
|
946
|
+
return lines.slice(0, end);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function isJsonDocumentText(text: string): boolean {
|
|
950
|
+
const trimmed = text.trim();
|
|
951
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
try {
|
|
955
|
+
JSON.parse(trimmed);
|
|
956
|
+
return true;
|
|
957
|
+
} catch {
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function getPrimaryTextContent(result: AgentToolResult<unknown>): string {
|
|
963
|
+
const textContent = result.content.find((item) => item.type === "text");
|
|
964
|
+
return textContent?.type === "text" ? textContent.text : "";
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function colorizeToolOutputLines(text: string, theme: Theme, isError: boolean): string[] {
|
|
968
|
+
const normalizedLines = trimTrailingBlankLines(replaceTabsForDisplay(sanitizeDisplayText(text)).split("\n"));
|
|
969
|
+
const normalizedText = normalizedLines.join("\n");
|
|
970
|
+
if (normalizedText.length === 0) {
|
|
971
|
+
return [];
|
|
972
|
+
}
|
|
973
|
+
if (isJsonDocumentText(normalizedText)) {
|
|
974
|
+
return highlightCode(normalizedText, "json");
|
|
975
|
+
}
|
|
976
|
+
return normalizedLines.map((line) => {
|
|
977
|
+
if (line.length === 0) {
|
|
978
|
+
return "";
|
|
979
|
+
}
|
|
980
|
+
return isError ? theme.fg("error", line) : theme.fg("toolOutput", line);
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function formatExpandHint(theme: Theme): string {
|
|
985
|
+
try {
|
|
986
|
+
return keyHint("app.tools.expand", "to expand");
|
|
987
|
+
} catch {
|
|
988
|
+
return `${theme.fg("dim", "ctrl+o")} ${theme.fg("muted", "to expand")}`;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function formatVisualTruncationNotice(remainingLines: number, totalLines: number, theme: Theme): string {
|
|
993
|
+
return `${theme.fg("muted", `... (${remainingLines} more lines, ${totalLines} total, `)}${formatExpandHint(theme)}${theme.fg("muted", ")")}`;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function formatAgentBrowserRenderCall(args: unknown, theme: Theme): string {
|
|
997
|
+
const input = isRecord(args) ? args : {};
|
|
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 ?? []);
|
|
1007
|
+
const redactedArgs = redactInvocationArgs(rawArgs);
|
|
1008
|
+
const invocation = sanitizeDisplayText(redactedArgs.join(" ")).replace(/\s+/g, " ").trim();
|
|
1009
|
+
const invocationPreview =
|
|
1010
|
+
invocation.length > TUI_INVOCATION_PREVIEW_MAX_CHARS
|
|
1011
|
+
? `${invocation.slice(0, TUI_INVOCATION_PREVIEW_MAX_CHARS - 3)}...`
|
|
1012
|
+
: invocation;
|
|
1013
|
+
let text = theme.fg("toolTitle", theme.bold("agent_browser"));
|
|
1014
|
+
if (invocationPreview.length > 0) {
|
|
1015
|
+
text += ` ${theme.fg("accent", invocationPreview)}`;
|
|
1016
|
+
}
|
|
1017
|
+
if (input.sessionMode === "fresh") {
|
|
1018
|
+
text += theme.fg("dim", " sessionMode=fresh");
|
|
1019
|
+
}
|
|
1020
|
+
if (typeof input.stdin === "string") {
|
|
1021
|
+
text += theme.fg("dim", " + stdin");
|
|
1022
|
+
}
|
|
1023
|
+
return text;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function formatAgentBrowserRenderResult(
|
|
1027
|
+
result: AgentToolResult<unknown>,
|
|
1028
|
+
options: { expanded: boolean; isPartial: boolean },
|
|
1029
|
+
theme: Theme,
|
|
1030
|
+
isError: boolean,
|
|
1031
|
+
): string {
|
|
1032
|
+
if (options.isPartial) {
|
|
1033
|
+
return theme.fg("warning", "Running agent-browser...");
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const outputText = getPrimaryTextContent(result);
|
|
1037
|
+
const outputLines = colorizeToolOutputLines(outputText, theme, isError);
|
|
1038
|
+
if (outputLines.length === 0) {
|
|
1039
|
+
const details = isRecord(result.details) ? result.details : undefined;
|
|
1040
|
+
const rawSummary = typeof details?.summary === "string" ? details.summary : isError ? "agent-browser failed" : "Done";
|
|
1041
|
+
const sanitizedSummary = sanitizeDisplayText(rawSummary).trim();
|
|
1042
|
+
const summary = sanitizedSummary.length > 0 ? sanitizedSummary : isError ? "agent-browser failed" : "Done";
|
|
1043
|
+
return isError ? theme.fg("error", summary) : theme.fg("success", summary);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
return `\n${outputLines.join("\n")}`;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
class AgentBrowserResultComponent {
|
|
1050
|
+
private expanded = false;
|
|
1051
|
+
private theme: Theme | undefined;
|
|
1052
|
+
private readonly text = new Text("", 0, 0);
|
|
1053
|
+
|
|
1054
|
+
setState(value: string, expanded: boolean, theme: Theme): void {
|
|
1055
|
+
this.text.setText(value);
|
|
1056
|
+
this.expanded = expanded;
|
|
1057
|
+
this.theme = theme;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
render(width: number): string[] {
|
|
1061
|
+
const lines = this.text.render(width);
|
|
1062
|
+
if (this.expanded || lines.length <= TUI_COLLAPSED_OUTPUT_MAX_LINES) {
|
|
1063
|
+
return lines;
|
|
1064
|
+
}
|
|
1065
|
+
const theme = this.theme;
|
|
1066
|
+
if (!theme) {
|
|
1067
|
+
return lines.slice(0, TUI_COLLAPSED_OUTPUT_MAX_LINES);
|
|
1068
|
+
}
|
|
1069
|
+
const hiddenLineCount = lines.length - TUI_COLLAPSED_OUTPUT_MAX_LINES;
|
|
1070
|
+
return [
|
|
1071
|
+
...lines.slice(0, TUI_COLLAPSED_OUTPUT_MAX_LINES),
|
|
1072
|
+
formatVisualTruncationNotice(hiddenLineCount, lines.length, theme),
|
|
1073
|
+
];
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
invalidate(): void {
|
|
1077
|
+
this.text.invalidate();
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
100
1081
|
function buildWrapperRecoveryHint(options: {
|
|
101
1082
|
pinnedBatchUnwrapMode?: PinnedBatchUnwrapMode;
|
|
102
1083
|
sessionTabCorrection?: OpenResultTabCorrection;
|
|
@@ -1554,18 +2535,90 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1554
2535
|
"Browse websites, read live docs, click and fill pages, extract browser content, take screenshots, and automate real web workflows.",
|
|
1555
2536
|
promptGuidelines: toolPromptGuidelines,
|
|
1556
2537
|
parameters: AGENT_BROWSER_PARAMS,
|
|
2538
|
+
renderCall(args, theme, context) {
|
|
2539
|
+
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
2540
|
+
text.setText(formatAgentBrowserRenderCall(args, theme));
|
|
2541
|
+
return text;
|
|
2542
|
+
},
|
|
2543
|
+
renderResult(result, options, theme, context) {
|
|
2544
|
+
const component = context.lastComponent instanceof AgentBrowserResultComponent
|
|
2545
|
+
? context.lastComponent
|
|
2546
|
+
: new AgentBrowserResultComponent();
|
|
2547
|
+
component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
|
|
2548
|
+
return component;
|
|
2549
|
+
},
|
|
1557
2550
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
1558
|
-
const
|
|
1559
|
-
const
|
|
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;
|
|
1560
2604
|
if (validationError) {
|
|
1561
2605
|
return {
|
|
1562
2606
|
content: [{ type: "text", text: validationError }],
|
|
1563
|
-
details: {
|
|
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
|
+
},
|
|
1564
2617
|
isError: true,
|
|
1565
2618
|
};
|
|
1566
2619
|
}
|
|
1567
|
-
const preparedArgs = await prepareAgentBrowserArgs(
|
|
1568
|
-
const userRequestedJson =
|
|
2620
|
+
const preparedArgs = await prepareAgentBrowserArgs(toolArgs, toolStdin, ctx.cwd);
|
|
2621
|
+
const userRequestedJson = toolArgs.includes("--json");
|
|
1569
2622
|
|
|
1570
2623
|
const tabTargetUpdateOrder = ++sessionTabTargetUpdateOrder;
|
|
1571
2624
|
const runTool = async (): Promise<AgentBrowserToolResult> => {
|
|
@@ -1589,10 +2642,15 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1589
2642
|
content: [{ type: "text", text: executionPlan.validationError }],
|
|
1590
2643
|
details: {
|
|
1591
2644
|
args: redactedArgs,
|
|
2645
|
+
compiledJob: redactedCompiledJob,
|
|
2646
|
+
compiledQaPreset: redactedCompiledQaPreset,
|
|
2647
|
+
compiledSourceLookup: redactedCompiledSourceLookup,
|
|
2648
|
+
compiledNetworkSourceLookup: redactedCompiledNetworkSourceLookup,
|
|
1592
2649
|
invalidValueFlag: executionPlan.invalidValueFlag,
|
|
1593
2650
|
sessionMode,
|
|
1594
2651
|
sessionRecoveryHint: redactedRecoveryHint,
|
|
1595
2652
|
startupScopedFlags: executionPlan.startupScopedFlags,
|
|
2653
|
+
...buildAgentBrowserResultCategoryDetails({ args: redactedArgs, command: executionPlan.commandInfo.command, errorText: executionPlan.validationError, succeeded: false, validationError: executionPlan.validationError }),
|
|
1596
2654
|
validationError: executionPlan.validationError,
|
|
1597
2655
|
},
|
|
1598
2656
|
isError: true,
|
|
@@ -1603,7 +2661,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1603
2661
|
const exactSensitiveValues = getExactSensitiveStdinValues({
|
|
1604
2662
|
command: executionPlan.commandInfo.command,
|
|
1605
2663
|
commandTokens,
|
|
1606
|
-
stdin:
|
|
2664
|
+
stdin: toolStdin,
|
|
1607
2665
|
});
|
|
1608
2666
|
const traceOwnerGuardMessage = getTraceOwnerGuardMessage({
|
|
1609
2667
|
command: executionPlan.commandInfo.command,
|
|
@@ -1620,6 +2678,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1620
2678
|
compatibilityWorkaround,
|
|
1621
2679
|
effectiveArgs: redactedEffectiveArgs,
|
|
1622
2680
|
sessionMode,
|
|
2681
|
+
...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: traceOwnerGuardMessage, succeeded: false, validationError: traceOwnerGuardMessage }),
|
|
1623
2682
|
validationError: traceOwnerGuardMessage,
|
|
1624
2683
|
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
1625
2684
|
},
|
|
@@ -1629,7 +2688,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1629
2688
|
const stdinValidationError = validateStdinCommandContract({
|
|
1630
2689
|
command: executionPlan.commandInfo.command,
|
|
1631
2690
|
commandTokens,
|
|
1632
|
-
stdin:
|
|
2691
|
+
stdin: toolStdin,
|
|
1633
2692
|
});
|
|
1634
2693
|
if (stdinValidationError) {
|
|
1635
2694
|
return {
|
|
@@ -1640,13 +2699,14 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1640
2699
|
compatibilityWorkaround,
|
|
1641
2700
|
effectiveArgs: redactedEffectiveArgs,
|
|
1642
2701
|
sessionMode,
|
|
2702
|
+
...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: stdinValidationError, succeeded: false, validationError: stdinValidationError }),
|
|
1643
2703
|
validationError: stdinValidationError,
|
|
1644
2704
|
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
1645
2705
|
},
|
|
1646
2706
|
isError: true,
|
|
1647
2707
|
};
|
|
1648
2708
|
}
|
|
1649
|
-
const waitIpcTimeoutError = validateWaitIpcTimeoutContract(commandTokens,
|
|
2709
|
+
const waitIpcTimeoutError = validateWaitIpcTimeoutContract(commandTokens, toolStdin);
|
|
1650
2710
|
if (waitIpcTimeoutError) {
|
|
1651
2711
|
return {
|
|
1652
2712
|
content: [{ type: "text", text: waitIpcTimeoutError }],
|
|
@@ -1656,6 +2716,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1656
2716
|
compatibilityWorkaround,
|
|
1657
2717
|
effectiveArgs: redactedEffectiveArgs,
|
|
1658
2718
|
sessionMode,
|
|
2719
|
+
...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: waitIpcTimeoutError, succeeded: false, timedOut: true, validationError: waitIpcTimeoutError }),
|
|
1659
2720
|
validationError: waitIpcTimeoutError,
|
|
1660
2721
|
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
1661
2722
|
},
|
|
@@ -1669,14 +2730,14 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1669
2730
|
let includePinnedNavigationSummary = false;
|
|
1670
2731
|
let sessionTabCorrection: OpenResultTabCorrection | undefined;
|
|
1671
2732
|
let processArgs = executionPlan.effectiveArgs;
|
|
1672
|
-
let processStdin = preparedArgs.stdin ??
|
|
2733
|
+
let processStdin = preparedArgs.stdin ?? toolStdin;
|
|
1673
2734
|
if (
|
|
1674
2735
|
priorSessionTabTarget &&
|
|
1675
2736
|
shouldPinSessionTabForCommand({
|
|
1676
2737
|
command: executionPlan.commandInfo.command,
|
|
1677
2738
|
commandTokens,
|
|
1678
2739
|
sessionName: executionPlan.sessionName,
|
|
1679
|
-
stdin:
|
|
2740
|
+
stdin: toolStdin,
|
|
1680
2741
|
})
|
|
1681
2742
|
) {
|
|
1682
2743
|
const plannedSessionTabSelection = await collectSessionTabSelection({
|
|
@@ -1686,7 +2747,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1686
2747
|
target: priorSessionTabTarget,
|
|
1687
2748
|
});
|
|
1688
2749
|
if (plannedSessionTabSelection && executionPlan.sessionName) {
|
|
1689
|
-
if (executionPlan.commandInfo.command === "eval" &&
|
|
2750
|
+
if (executionPlan.commandInfo.command === "eval" && toolStdin !== undefined) {
|
|
1690
2751
|
const appliedSessionTabSelection = await applyOpenResultTabCorrection({
|
|
1691
2752
|
correction: plannedSessionTabSelection,
|
|
1692
2753
|
cwd: ctx.cwd,
|
|
@@ -1704,6 +2765,8 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1704
2765
|
effectiveArgs: redactedEffectiveArgs,
|
|
1705
2766
|
sessionMode,
|
|
1706
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" }),
|
|
1707
2770
|
validationError: error,
|
|
1708
2771
|
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
1709
2772
|
},
|
|
@@ -1716,7 +2779,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1716
2779
|
command: executionPlan.commandInfo.command,
|
|
1717
2780
|
commandTokens,
|
|
1718
2781
|
selectedTab: plannedSessionTabSelection.selectedTab,
|
|
1719
|
-
stdin:
|
|
2782
|
+
stdin: toolStdin,
|
|
1720
2783
|
});
|
|
1721
2784
|
if (pinnedBatchPlan && "error" in pinnedBatchPlan) {
|
|
1722
2785
|
return {
|
|
@@ -1728,6 +2791,8 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1728
2791
|
effectiveArgs: redactedEffectiveArgs,
|
|
1729
2792
|
sessionMode,
|
|
1730
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" }),
|
|
1731
2796
|
validationError: pinnedBatchPlan.error,
|
|
1732
2797
|
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
1733
2798
|
},
|
|
@@ -1775,6 +2840,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1775
2840
|
effectiveArgs: redactedProcessArgs,
|
|
1776
2841
|
sessionMode,
|
|
1777
2842
|
sessionTabCorrection,
|
|
2843
|
+
...buildAgentBrowserResultCategoryDetails({ args: redactedProcessArgs, command: executionPlan.commandInfo.command, errorText, failureCategory: "missing-binary", spawnError: processResult.spawnError.message, succeeded: false }),
|
|
1778
2844
|
spawnError: processResult.spawnError.message,
|
|
1779
2845
|
},
|
|
1780
2846
|
isError: true,
|
|
@@ -1829,7 +2895,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1829
2895
|
const plainTextInspection = executionPlan.plainTextInspection && processSucceeded;
|
|
1830
2896
|
const parseSucceeded = plainTextInspection || parseError === undefined;
|
|
1831
2897
|
const envelopeSuccess = plainTextInspection ? true : presentationEnvelope?.success !== false;
|
|
1832
|
-
|
|
2898
|
+
let succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
|
|
1833
2899
|
const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
|
|
1834
2900
|
updateTraceOwnerState({
|
|
1835
2901
|
command: executionPlan.commandInfo.command,
|
|
@@ -1857,7 +2923,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1857
2923
|
if (
|
|
1858
2924
|
succeeded &&
|
|
1859
2925
|
executionPlan.sessionName &&
|
|
1860
|
-
hasLaunchScopedTabCorrectionFlag(
|
|
2926
|
+
hasLaunchScopedTabCorrectionFlag(toolArgs) &&
|
|
1861
2927
|
(executionPlan.commandInfo.command === "goto" ||
|
|
1862
2928
|
executionPlan.commandInfo.command === "navigate" ||
|
|
1863
2929
|
executionPlan.commandInfo.command === "open")
|
|
@@ -1997,7 +3063,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1997
3063
|
exitCode: processResult.exitCode,
|
|
1998
3064
|
parseError,
|
|
1999
3065
|
plainTextInspection,
|
|
2000
|
-
staleRefArgs: getStaleRefArgs(commandTokens,
|
|
3066
|
+
staleRefArgs: getStaleRefArgs(commandTokens, toolStdin),
|
|
2001
3067
|
spawnError: processResult.spawnError,
|
|
2002
3068
|
stderr: processResult.stderr,
|
|
2003
3069
|
timedOut: processResult.timedOut,
|
|
@@ -2021,6 +3087,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2021
3087
|
summary: `${redactedArgs.join(" ")} completed`,
|
|
2022
3088
|
}
|
|
2023
3089
|
: await buildToolPresentation({
|
|
3090
|
+
args: redactedProcessArgs,
|
|
2024
3091
|
artifactManifest,
|
|
2025
3092
|
artifactRequest: screenshotArtifactRequest,
|
|
2026
3093
|
batchArtifactRequests: batchScreenshotArtifactRequests,
|
|
@@ -2052,6 +3119,29 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2052
3119
|
if (presentation.artifactManifest) {
|
|
2053
3120
|
artifactManifest = presentation.artifactManifest;
|
|
2054
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
|
+
}
|
|
2055
3145
|
const warningText = aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
|
|
2056
3146
|
const contentWithSessionWarnings = userRequestedJson && !plainTextInspection
|
|
2057
3147
|
? buildJsonVisibleContent({
|
|
@@ -2080,20 +3170,53 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2080
3170
|
? { ...item, text: exactRedactedText }
|
|
2081
3171
|
: { ...item, text: redactSensitiveText(exactRedactedText) };
|
|
2082
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
|
+
}
|
|
2083
3199
|
const details = {
|
|
2084
3200
|
args: redactedArgs,
|
|
3201
|
+
compiledJob: redactedCompiledJob,
|
|
3202
|
+
compiledQaPreset: redactedCompiledQaPreset,
|
|
3203
|
+
compiledSourceLookup: redactedCompiledSourceLookup,
|
|
3204
|
+
compiledNetworkSourceLookup: redactedCompiledNetworkSourceLookup,
|
|
2085
3205
|
artifactManifest: presentation.artifactManifest,
|
|
2086
3206
|
artifactRetentionSummary: presentation.artifactRetentionSummary,
|
|
3207
|
+
artifactVerification: presentation.artifactVerification,
|
|
2087
3208
|
artifacts: presentation.artifacts,
|
|
2088
3209
|
batchFailure: presentation.batchFailure,
|
|
2089
3210
|
batchSteps: presentation.batchSteps,
|
|
2090
3211
|
command: executionPlan.commandInfo.command,
|
|
3212
|
+
compiledSemanticAction: redactedCompiledSemanticAction,
|
|
2091
3213
|
compatibilityWorkaround,
|
|
2092
3214
|
subcommand: executionPlan.commandInfo.subcommand,
|
|
2093
3215
|
data: presentation.data,
|
|
2094
3216
|
error: plainTextInspection ? undefined : presentationEnvelope?.error,
|
|
2095
3217
|
inspection: plainTextInspection || undefined,
|
|
2096
3218
|
navigationSummary,
|
|
3219
|
+
...categoryDetails,
|
|
2097
3220
|
aboutBlankSessionMismatch,
|
|
2098
3221
|
openResultTabCorrection,
|
|
2099
3222
|
effectiveArgs: redactedProcessArgs,
|
|
@@ -2103,9 +3226,14 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2103
3226
|
fullOutputUnavailable: parseFailureOutput.fullOutputUnavailable,
|
|
2104
3227
|
imagePath: presentation.imagePath,
|
|
2105
3228
|
imagePaths: presentation.imagePaths,
|
|
3229
|
+
nextActions,
|
|
3230
|
+
pageChangeSummary: presentation.pageChangeSummary,
|
|
3231
|
+
qaPreset,
|
|
2106
3232
|
parseError: plainTextInspection ? undefined : parseError,
|
|
2107
3233
|
savedFile: presentation.savedFile,
|
|
2108
3234
|
savedFilePath: presentation.savedFilePath,
|
|
3235
|
+
sourceLookup,
|
|
3236
|
+
networkSourceLookup,
|
|
2109
3237
|
sessionMode,
|
|
2110
3238
|
sessionTabCorrection,
|
|
2111
3239
|
sessionTabTarget: currentSessionTabTarget,
|
|
@@ -2119,11 +3247,12 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2119
3247
|
timeoutMs: processResult.timeoutMs,
|
|
2120
3248
|
};
|
|
2121
3249
|
|
|
2122
|
-
|
|
3250
|
+
const result = {
|
|
2123
3251
|
content: redactedContent,
|
|
2124
3252
|
details: redactToolDetails(details, exactSensitiveValues),
|
|
2125
3253
|
isError: !succeeded,
|
|
2126
3254
|
};
|
|
3255
|
+
return compiledNetworkSourceLookup ? redactNetworkSourceLookupSurface(result) as typeof result : result;
|
|
2127
3256
|
} finally {
|
|
2128
3257
|
if (processResult.stdoutSpillPath) {
|
|
2129
3258
|
await rm(processResult.stdoutSpillPath, { force: true }).catch(() => undefined);
|
|
@@ -2131,7 +3260,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2131
3260
|
}
|
|
2132
3261
|
};
|
|
2133
3262
|
|
|
2134
|
-
return extractExplicitSessionName(
|
|
3263
|
+
return extractExplicitSessionName(toolArgs)
|
|
2135
3264
|
? runTool()
|
|
2136
3265
|
: managedSessionExecutionQueue.run(runTool);
|
|
2137
3266
|
},
|