pi-agent-browser-native 0.2.24 → 0.2.26
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 +48 -1
- package/README.md +137 -13
- package/docs/ARCHITECTURE.md +54 -7
- package/docs/COMMAND_REFERENCE.md +586 -42
- package/docs/RELEASE.md +61 -7
- package/docs/REQUIREMENTS.md +14 -1
- package/docs/SUPPORT_MATRIX.md +85 -0
- package/docs/TOOL_CONTRACT.md +301 -24
- package/extensions/agent-browser/index.ts +1983 -38
- package/extensions/agent-browser/lib/playbook.ts +23 -12
- package/extensions/agent-browser/lib/results/presentation.ts +706 -37
- package/extensions/agent-browser/lib/results/shared.ts +437 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +69 -9
- package/extensions/agent-browser/lib/results.ts +12 -0
- package/extensions/agent-browser/lib/runtime.ts +82 -10
- package/package.json +4 -2
- package/scripts/agent-browser-capability-baseline.mjs +499 -110
- package/scripts/doctor.mjs +1 -1
|
@@ -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,11 +27,14 @@ 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,
|
|
33
35
|
type AgentBrowserBatchResult,
|
|
34
36
|
type AgentBrowserEnvelope,
|
|
37
|
+
type AgentBrowserNextAction,
|
|
35
38
|
} from "./lib/results.js";
|
|
36
39
|
import {
|
|
37
40
|
buildExecutionPlan,
|
|
@@ -54,6 +57,7 @@ import {
|
|
|
54
57
|
resolveManagedSessionState,
|
|
55
58
|
shouldAppendBrowserSystemPrompt,
|
|
56
59
|
validateToolArgs,
|
|
60
|
+
type CommandInfo,
|
|
57
61
|
type CompatibilityWorkaround,
|
|
58
62
|
type OpenResultTabCorrection,
|
|
59
63
|
} from "./lib/runtime.js";
|
|
@@ -70,22 +74,211 @@ import {
|
|
|
70
74
|
formatSessionArtifactRetentionSummary,
|
|
71
75
|
isSessionArtifactManifest,
|
|
72
76
|
mergeSessionArtifactManifest,
|
|
77
|
+
summarizeNetworkFailures,
|
|
73
78
|
} from "./lib/results/shared.js";
|
|
74
79
|
|
|
75
80
|
const DEFAULT_SESSION_MODE = "auto" as const;
|
|
76
81
|
const DIRECT_AGENT_BROWSER_BASH_BYPASS_ENV = "PI_AGENT_BROWSER_ALLOW_DIRECT_BASH";
|
|
77
82
|
const PACKAGE_NAME = "pi-agent-browser-native";
|
|
78
83
|
|
|
84
|
+
const AGENT_BROWSER_SEMANTIC_ACTIONS = ["check", "click", "fill", "select", "uncheck"] as const;
|
|
85
|
+
const AGENT_BROWSER_SEMANTIC_LOCATORS = ["alt", "label", "placeholder", "role", "testid", "text", "title"] as const;
|
|
86
|
+
const AGENT_BROWSER_JOB_STEP_ACTIONS = ["open", "click", "fill", "wait", "assertText", "assertUrl", "waitForDownload", "screenshot"] as const;
|
|
87
|
+
const SOURCE_LOOKUP_WORKSPACE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
88
|
+
const SOURCE_LOOKUP_IGNORED_DIRECTORIES = new Set([".git", "node_modules", "dist", "build", "coverage", ".next", "out", "tmp", "temp"]);
|
|
89
|
+
const SOURCE_LOOKUP_DEFAULT_MAX_WORKSPACE_FILES = 2_000;
|
|
90
|
+
const SOURCE_LOOKUP_MAX_WORKSPACE_FILES = 5_000;
|
|
91
|
+
|
|
92
|
+
type AgentBrowserSemanticActionName = (typeof AGENT_BROWSER_SEMANTIC_ACTIONS)[number];
|
|
93
|
+
type AgentBrowserSemanticLocator = (typeof AGENT_BROWSER_SEMANTIC_LOCATORS)[number];
|
|
94
|
+
type AgentBrowserJobStepAction = (typeof AGENT_BROWSER_JOB_STEP_ACTIONS)[number];
|
|
95
|
+
type AgentBrowserSourceLookupStatus = "candidates-found" | "no-candidates" | "unsupported";
|
|
96
|
+
type AgentBrowserNetworkSourceLookupStatus = "failed-requests-found" | "no-failed-requests" | "no-candidates";
|
|
97
|
+
|
|
98
|
+
interface AgentBrowserSemanticActionInput {
|
|
99
|
+
action: AgentBrowserSemanticActionName;
|
|
100
|
+
locator: AgentBrowserSemanticLocator;
|
|
101
|
+
value: string;
|
|
102
|
+
text?: string;
|
|
103
|
+
role?: string;
|
|
104
|
+
name?: string;
|
|
105
|
+
session?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface CompiledAgentBrowserSemanticAction {
|
|
109
|
+
action: AgentBrowserSemanticActionName;
|
|
110
|
+
locator: AgentBrowserSemanticLocator;
|
|
111
|
+
args: string[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface CompiledAgentBrowserJobStep {
|
|
115
|
+
action: AgentBrowserJobStepAction;
|
|
116
|
+
args: string[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface CompiledAgentBrowserJob {
|
|
120
|
+
args: string[];
|
|
121
|
+
stdin: string;
|
|
122
|
+
steps: CompiledAgentBrowserJobStep[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface CompiledAgentBrowserQaPreset extends CompiledAgentBrowserJob {
|
|
126
|
+
checks: {
|
|
127
|
+
checkConsole: boolean;
|
|
128
|
+
checkErrors: boolean;
|
|
129
|
+
checkNetwork: boolean;
|
|
130
|
+
expectedText: string[];
|
|
131
|
+
expectedSelector?: string;
|
|
132
|
+
screenshotPath?: string;
|
|
133
|
+
url: string;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface CompiledAgentBrowserSourceLookupStep {
|
|
138
|
+
action: "dom" | "react";
|
|
139
|
+
args: string[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface CompiledAgentBrowserSourceLookup {
|
|
143
|
+
args: string[];
|
|
144
|
+
stdin: string;
|
|
145
|
+
steps: CompiledAgentBrowserSourceLookupStep[];
|
|
146
|
+
query: {
|
|
147
|
+
componentName?: string;
|
|
148
|
+
includeDomHints: boolean;
|
|
149
|
+
maxWorkspaceFiles: number;
|
|
150
|
+
reactFiberId?: string;
|
|
151
|
+
selector?: string;
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface AgentBrowserSourceLookupCandidate {
|
|
156
|
+
column?: number;
|
|
157
|
+
componentName?: string;
|
|
158
|
+
confidence: "high" | "medium" | "low";
|
|
159
|
+
evidence: string[];
|
|
160
|
+
file?: string;
|
|
161
|
+
line?: number;
|
|
162
|
+
source: "react-inspect" | "dom-attribute" | "workspace-search";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface AgentBrowserSourceLookupAnalysis {
|
|
166
|
+
candidates: AgentBrowserSourceLookupCandidate[];
|
|
167
|
+
limitations: string[];
|
|
168
|
+
status: AgentBrowserSourceLookupStatus;
|
|
169
|
+
summary: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
interface CompiledAgentBrowserNetworkSourceLookup {
|
|
173
|
+
args: string[];
|
|
174
|
+
stdin: string;
|
|
175
|
+
steps: Array<{ action: "network"; args: string[] }>;
|
|
176
|
+
query: {
|
|
177
|
+
filter?: string;
|
|
178
|
+
maxWorkspaceFiles: number;
|
|
179
|
+
requestId?: string;
|
|
180
|
+
url?: string;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface AgentBrowserNetworkSourceLookupRequest {
|
|
185
|
+
error?: string;
|
|
186
|
+
method?: string;
|
|
187
|
+
requestId?: string;
|
|
188
|
+
status?: number;
|
|
189
|
+
url?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface AgentBrowserNetworkSourceLookupCandidate {
|
|
193
|
+
confidence: "high" | "medium" | "low";
|
|
194
|
+
evidence: string[];
|
|
195
|
+
file?: string;
|
|
196
|
+
line?: number;
|
|
197
|
+
requestUrl?: string;
|
|
198
|
+
source: "initiator" | "workspace-search";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface AgentBrowserNetworkSourceLookupAnalysis {
|
|
202
|
+
candidates: AgentBrowserNetworkSourceLookupCandidate[];
|
|
203
|
+
failedRequests: AgentBrowserNetworkSourceLookupRequest[];
|
|
204
|
+
limitations: string[];
|
|
205
|
+
status: AgentBrowserNetworkSourceLookupStatus;
|
|
206
|
+
summary: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
79
209
|
const AGENT_BROWSER_PARAMS = Type.Object({
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
210
|
+
|
|
211
|
+
args: Type.Optional(
|
|
212
|
+
Type.Array(Type.String({ description: "Exact agent-browser CLI arguments, excluding the binary name." }), {
|
|
213
|
+
description: "Exact agent-browser CLI arguments, excluding the binary name and any shell operators. Required unless semanticAction, job, qa, sourceLookup, or networkSourceLookup is provided.",
|
|
214
|
+
minItems: 1,
|
|
215
|
+
}),
|
|
216
|
+
),
|
|
217
|
+
semanticAction: Type.Optional(
|
|
218
|
+
Type.Object({
|
|
219
|
+
action: StringEnum(AGENT_BROWSER_SEMANTIC_ACTIONS, {
|
|
220
|
+
description: "Intent action to compile to an existing agent-browser find command.",
|
|
221
|
+
}),
|
|
222
|
+
locator: StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, {
|
|
223
|
+
description: "Upstream find locator family to use.",
|
|
224
|
+
}),
|
|
225
|
+
value: Type.String({ description: "Locator value, such as visible text, label text, placeholder text, test id, title, alt text, or role." }),
|
|
226
|
+
text: Type.Optional(Type.String({ description: "Text/value argument for fill or select actions." })),
|
|
227
|
+
role: Type.Optional(Type.String({ description: "Role locator value; when set it must match value for locator=role." })),
|
|
228
|
+
name: Type.Optional(Type.String({ description: "Accessible name filter for locator=role; compiles to --name <name>." })),
|
|
229
|
+
session: Type.Optional(Type.String({ description: "Optional upstream session name; prepends --session <name> before the compiled find command." })),
|
|
230
|
+
}),
|
|
231
|
+
),
|
|
232
|
+
qa: Type.Optional(
|
|
233
|
+
Type.Object({
|
|
234
|
+
url: Type.String({ description: "URL to open for a lightweight QA preset." }),
|
|
235
|
+
expectedText: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())], { description: "Text that must appear on the page." })),
|
|
236
|
+
expectedSelector: Type.Optional(Type.String({ description: "Selector or @ref that must appear on the page." })),
|
|
237
|
+
screenshotPath: Type.Optional(Type.String({ description: "Optional evidence screenshot path captured at the end of the QA preset." })),
|
|
238
|
+
checkConsole: Type.Optional(Type.Boolean({ description: "Whether to fail on console error messages. Defaults to true." })),
|
|
239
|
+
checkErrors: Type.Optional(Type.Boolean({ description: "Whether to fail on page errors. Defaults to true." })),
|
|
240
|
+
checkNetwork: Type.Optional(Type.Boolean({ description: "Whether to inspect network requests and fail on actionable request failures; benign icon misses warn. Defaults to true." })),
|
|
241
|
+
}),
|
|
242
|
+
),
|
|
243
|
+
sourceLookup: Type.Optional(
|
|
244
|
+
Type.Object({
|
|
245
|
+
selector: Type.Optional(Type.String({ description: "Visible selector or @ref whose DOM metadata should be inspected for source hints." })),
|
|
246
|
+
reactFiberId: Type.Optional(Type.String({ description: "React fiber id to inspect with upstream react inspect. Requires a session opened with --enable react-devtools." })),
|
|
247
|
+
componentName: Type.Optional(Type.String({ description: "Component name to correlate with react tree output and bounded local workspace search." })),
|
|
248
|
+
includeDomHints: Type.Optional(Type.Boolean({ description: "Whether selector lookups should inspect DOM HTML attributes for source-like metadata. Defaults to true." })),
|
|
249
|
+
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 })),
|
|
250
|
+
}),
|
|
251
|
+
),
|
|
252
|
+
networkSourceLookup: Type.Optional(
|
|
253
|
+
Type.Object({
|
|
254
|
+
filter: Type.Optional(Type.String({ description: "Optional upstream network requests filter pattern." })),
|
|
255
|
+
requestId: Type.Optional(Type.String({ description: "Optional network request id to inspect with network request <id>." })),
|
|
256
|
+
url: Type.Optional(Type.String({ description: "Optional failed request URL or URL fragment to correlate with local source." })),
|
|
257
|
+
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 })),
|
|
258
|
+
}),
|
|
259
|
+
),
|
|
260
|
+
job: Type.Optional(
|
|
261
|
+
Type.Object({
|
|
262
|
+
steps: Type.Array(
|
|
263
|
+
Type.Object({
|
|
264
|
+
action: StringEnum(AGENT_BROWSER_JOB_STEP_ACTIONS, {
|
|
265
|
+
description: "Constrained one-call job step compiled to existing upstream batch commands.",
|
|
266
|
+
}),
|
|
267
|
+
url: Type.Optional(Type.String({ description: "URL for open steps, or URL pattern for assertUrl steps." })),
|
|
268
|
+
selector: Type.Optional(Type.String({ description: "Selector or @ref for click/fill/get-like steps." })),
|
|
269
|
+
text: Type.Optional(Type.String({ description: "Text for fill steps or visible text for assertText steps." })),
|
|
270
|
+
path: Type.Optional(Type.String({ description: "Artifact/download path for waitForDownload or screenshot steps." })),
|
|
271
|
+
milliseconds: Type.Optional(Type.Number({ description: "Milliseconds for wait steps." })),
|
|
272
|
+
}),
|
|
273
|
+
{ minItems: 1 },
|
|
274
|
+
),
|
|
275
|
+
}),
|
|
276
|
+
),
|
|
277
|
+
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
278
|
sessionMode: Type.Optional(
|
|
86
279
|
StringEnum(["auto", "fresh"] as const, {
|
|
87
280
|
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 --
|
|
281
|
+
"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
282
|
default: DEFAULT_SESSION_MODE,
|
|
90
283
|
}),
|
|
91
284
|
),
|
|
@@ -105,6 +298,725 @@ function buildInvocationPreview(effectiveArgs: string[]): string {
|
|
|
105
298
|
return preview.length > 120 ? `${preview.slice(0, 117)}...` : preview;
|
|
106
299
|
}
|
|
107
300
|
|
|
301
|
+
function getRequiredJobString(step: Record<string, unknown>, field: "path" | "selector" | "text" | "url", action: AgentBrowserJobStepAction): { value?: string; error?: string } {
|
|
302
|
+
const value = step[field];
|
|
303
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
304
|
+
return { error: `job step ${action} requires a non-empty ${field} string.` };
|
|
305
|
+
}
|
|
306
|
+
return { value };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function compileAgentBrowserJob(input: unknown): { compiled?: CompiledAgentBrowserJob; error?: string } {
|
|
310
|
+
if (!isRecord(input)) {
|
|
311
|
+
return { error: "job must be an object." };
|
|
312
|
+
}
|
|
313
|
+
const rawSteps = input.steps;
|
|
314
|
+
if (!Array.isArray(rawSteps) || rawSteps.length === 0) {
|
|
315
|
+
return { error: "job.steps must be a non-empty array." };
|
|
316
|
+
}
|
|
317
|
+
const steps: CompiledAgentBrowserJobStep[] = [];
|
|
318
|
+
for (const [index, rawStep] of rawSteps.entries()) {
|
|
319
|
+
if (!isRecord(rawStep)) {
|
|
320
|
+
return { error: `job.steps[${index}] must be an object.` };
|
|
321
|
+
}
|
|
322
|
+
const action = rawStep.action;
|
|
323
|
+
if (typeof action !== "string" || !AGENT_BROWSER_JOB_STEP_ACTIONS.includes(action as AgentBrowserJobStepAction)) {
|
|
324
|
+
return { error: `job.steps[${index}].action must be one of: ${AGENT_BROWSER_JOB_STEP_ACTIONS.join(", ")}.` };
|
|
325
|
+
}
|
|
326
|
+
const jobAction = action as AgentBrowserJobStepAction;
|
|
327
|
+
let args: string[];
|
|
328
|
+
if (jobAction === "open") {
|
|
329
|
+
const result = getRequiredJobString(rawStep, "url", jobAction);
|
|
330
|
+
if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
|
|
331
|
+
args = ["open", result.value as string];
|
|
332
|
+
} else if (jobAction === "click") {
|
|
333
|
+
const result = getRequiredJobString(rawStep, "selector", jobAction);
|
|
334
|
+
if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
|
|
335
|
+
args = ["click", result.value as string];
|
|
336
|
+
} else if (jobAction === "fill") {
|
|
337
|
+
const selector = getRequiredJobString(rawStep, "selector", jobAction);
|
|
338
|
+
if (selector.error) return { error: `job.steps[${index}]: ${selector.error}` };
|
|
339
|
+
const text = getRequiredJobString(rawStep, "text", jobAction);
|
|
340
|
+
if (text.error) return { error: `job.steps[${index}]: ${text.error}` };
|
|
341
|
+
args = ["fill", selector.value as string, text.value as string];
|
|
342
|
+
} else if (jobAction === "wait") {
|
|
343
|
+
const milliseconds = rawStep.milliseconds;
|
|
344
|
+
if (typeof milliseconds !== "number" || !Number.isInteger(milliseconds) || milliseconds <= 0) {
|
|
345
|
+
return { error: `job.steps[${index}]: job step wait requires a positive integer milliseconds value.` };
|
|
346
|
+
}
|
|
347
|
+
args = ["wait", String(milliseconds)];
|
|
348
|
+
} else if (jobAction === "assertText") {
|
|
349
|
+
const result = getRequiredJobString(rawStep, "text", jobAction);
|
|
350
|
+
if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
|
|
351
|
+
args = ["wait", "--text", result.value as string];
|
|
352
|
+
} else if (jobAction === "assertUrl") {
|
|
353
|
+
const result = getRequiredJobString(rawStep, "url", jobAction);
|
|
354
|
+
if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
|
|
355
|
+
args = ["wait", "--url", result.value as string];
|
|
356
|
+
} else if (jobAction === "waitForDownload") {
|
|
357
|
+
const result = getRequiredJobString(rawStep, "path", jobAction);
|
|
358
|
+
if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
|
|
359
|
+
args = ["wait", "--download", result.value as string];
|
|
360
|
+
} else {
|
|
361
|
+
const result = getRequiredJobString(rawStep, "path", jobAction);
|
|
362
|
+
if (result.error) return { error: `job.steps[${index}]: ${result.error}` };
|
|
363
|
+
args = ["screenshot", result.value as string];
|
|
364
|
+
}
|
|
365
|
+
steps.push({ action: jobAction, args });
|
|
366
|
+
}
|
|
367
|
+
return { compiled: { args: ["batch"], stdin: JSON.stringify(steps.map((step) => step.args)), steps } };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
interface AgentBrowserQaPresetAnalysis {
|
|
371
|
+
failedChecks: string[];
|
|
372
|
+
passed: boolean;
|
|
373
|
+
summary: string;
|
|
374
|
+
warnings: string[];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function getBatchResultItems(data: unknown): Array<Record<string, unknown>> {
|
|
378
|
+
return Array.isArray(data) ? data.filter(isRecord) : [];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function getCommandNameFromBatchItem(item: Record<string, unknown>): string | undefined {
|
|
382
|
+
const command = item.command;
|
|
383
|
+
return Array.isArray(command) && typeof command[0] === "string" ? command[0] : undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function analyzeQaPresetResults(data: unknown): AgentBrowserQaPresetAnalysis | undefined {
|
|
387
|
+
const items = getBatchResultItems(data);
|
|
388
|
+
if (items.length === 0) return undefined;
|
|
389
|
+
const failedChecks: string[] = [];
|
|
390
|
+
const warnings: string[] = [];
|
|
391
|
+
for (const item of items) {
|
|
392
|
+
if (item.success === false) {
|
|
393
|
+
failedChecks.push(`${getCommandNameFromBatchItem(item) ?? "step"} failed`);
|
|
394
|
+
}
|
|
395
|
+
const result = isRecord(item.result) ? item.result : undefined;
|
|
396
|
+
const commandName = getCommandNameFromBatchItem(item);
|
|
397
|
+
if (commandName === "errors" && Array.isArray(result?.errors) && result.errors.length > 0) {
|
|
398
|
+
failedChecks.push(`${result.errors.length} page error(s)`);
|
|
399
|
+
}
|
|
400
|
+
if (commandName === "console" && Array.isArray(result?.messages)) {
|
|
401
|
+
const errorCount = result.messages.filter((message) => isRecord(message) && /error/i.test(String(message.type ?? message.level ?? ""))).length;
|
|
402
|
+
if (errorCount > 0) failedChecks.push(`${errorCount} console error message(s)`);
|
|
403
|
+
}
|
|
404
|
+
if (commandName === "network" && Array.isArray(result?.requests)) {
|
|
405
|
+
const networkFailures = summarizeNetworkFailures(result.requests);
|
|
406
|
+
if (networkFailures.actionableCount > 0) failedChecks.push(`${networkFailures.actionableCount} actionable failed network request(s)`);
|
|
407
|
+
if (networkFailures.benignCount > 0) warnings.push(`${networkFailures.benignCount} benign network request failure(s) ignored`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const uniqueFailures = [...new Set(failedChecks)];
|
|
411
|
+
const uniqueWarnings = [...new Set(warnings)];
|
|
412
|
+
return {
|
|
413
|
+
failedChecks: uniqueFailures,
|
|
414
|
+
passed: uniqueFailures.length === 0,
|
|
415
|
+
summary: uniqueFailures.length === 0
|
|
416
|
+
? uniqueWarnings.length === 0 ? "QA preset passed." : `QA preset passed with warnings: ${uniqueWarnings.join("; ")}.`
|
|
417
|
+
: `QA preset failed: ${uniqueFailures.join("; ")}.`,
|
|
418
|
+
warnings: uniqueWarnings,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function compileAgentBrowserQaPreset(input: unknown): { compiled?: CompiledAgentBrowserQaPreset; error?: string } {
|
|
423
|
+
if (!isRecord(input)) {
|
|
424
|
+
return { error: "qa must be an object." };
|
|
425
|
+
}
|
|
426
|
+
const url = input.url;
|
|
427
|
+
if (typeof url !== "string" || url.trim().length === 0) {
|
|
428
|
+
return { error: "qa.url must be a non-empty string." };
|
|
429
|
+
}
|
|
430
|
+
const expectedText = input.expectedText === undefined
|
|
431
|
+
? []
|
|
432
|
+
: typeof input.expectedText === "string"
|
|
433
|
+
? [input.expectedText]
|
|
434
|
+
: Array.isArray(input.expectedText)
|
|
435
|
+
? input.expectedText
|
|
436
|
+
: undefined;
|
|
437
|
+
if (!expectedText || expectedText.some((text) => typeof text !== "string" || text.trim().length === 0)) {
|
|
438
|
+
return { error: "qa.expectedText must be a non-empty string or array of non-empty strings when provided." };
|
|
439
|
+
}
|
|
440
|
+
const expectedSelector = input.expectedSelector;
|
|
441
|
+
if (expectedSelector !== undefined && (typeof expectedSelector !== "string" || expectedSelector.trim().length === 0)) {
|
|
442
|
+
return { error: "qa.expectedSelector must be a non-empty string when provided." };
|
|
443
|
+
}
|
|
444
|
+
const screenshotPath = input.screenshotPath;
|
|
445
|
+
if (screenshotPath !== undefined && (typeof screenshotPath !== "string" || screenshotPath.trim().length === 0)) {
|
|
446
|
+
return { error: "qa.screenshotPath must be a non-empty string when provided." };
|
|
447
|
+
}
|
|
448
|
+
for (const field of ["checkConsole", "checkErrors", "checkNetwork"] as const) {
|
|
449
|
+
if (input[field] !== undefined && typeof input[field] !== "boolean") {
|
|
450
|
+
return { error: `qa.${field} must be a boolean when provided.` };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const checkConsole = input.checkConsole !== false;
|
|
454
|
+
const checkErrors = input.checkErrors !== false;
|
|
455
|
+
const checkNetwork = input.checkNetwork !== false;
|
|
456
|
+
const steps: CompiledAgentBrowserJobStep[] = [];
|
|
457
|
+
if (checkNetwork) steps.push({ action: "wait", args: ["network", "requests", "--clear"] });
|
|
458
|
+
if (checkConsole) steps.push({ action: "wait", args: ["console", "--clear"] });
|
|
459
|
+
if (checkErrors) steps.push({ action: "wait", args: ["errors", "--clear"] });
|
|
460
|
+
steps.push(
|
|
461
|
+
{ action: "open", args: ["open", url] },
|
|
462
|
+
{ action: "wait", args: ["wait", "--load", "networkidle"] },
|
|
463
|
+
);
|
|
464
|
+
for (const text of expectedText) {
|
|
465
|
+
steps.push({ action: "assertText", args: ["wait", "--text", text] });
|
|
466
|
+
}
|
|
467
|
+
if (typeof expectedSelector === "string") {
|
|
468
|
+
steps.push({ action: "wait", args: ["wait", expectedSelector] });
|
|
469
|
+
}
|
|
470
|
+
if (checkNetwork) steps.push({ action: "wait", args: ["network", "requests"] });
|
|
471
|
+
if (checkConsole) steps.push({ action: "wait", args: ["console"] });
|
|
472
|
+
if (checkErrors) steps.push({ action: "wait", args: ["errors"] });
|
|
473
|
+
if (typeof screenshotPath === "string") steps.push({ action: "screenshot", args: ["screenshot", screenshotPath] });
|
|
474
|
+
return {
|
|
475
|
+
compiled: {
|
|
476
|
+
args: ["batch"],
|
|
477
|
+
checks: { checkConsole, checkErrors, checkNetwork, expectedSelector, expectedText, screenshotPath, url },
|
|
478
|
+
stdin: JSON.stringify(steps.map((step) => step.args)),
|
|
479
|
+
steps,
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function compileAgentBrowserSourceLookup(input: unknown): { compiled?: CompiledAgentBrowserSourceLookup; error?: string } {
|
|
485
|
+
if (!isRecord(input)) {
|
|
486
|
+
return { error: "sourceLookup must be an object." };
|
|
487
|
+
}
|
|
488
|
+
const selector = input.selector;
|
|
489
|
+
const reactFiberId = input.reactFiberId;
|
|
490
|
+
const componentName = input.componentName;
|
|
491
|
+
if (selector !== undefined && (typeof selector !== "string" || selector.trim().length === 0)) {
|
|
492
|
+
return { error: "sourceLookup.selector must be a non-empty string when provided." };
|
|
493
|
+
}
|
|
494
|
+
if (reactFiberId !== undefined && (typeof reactFiberId !== "string" || reactFiberId.trim().length === 0)) {
|
|
495
|
+
return { error: "sourceLookup.reactFiberId must be a non-empty string when provided." };
|
|
496
|
+
}
|
|
497
|
+
if (componentName !== undefined && (typeof componentName !== "string" || componentName.trim().length === 0)) {
|
|
498
|
+
return { error: "sourceLookup.componentName must be a non-empty string when provided." };
|
|
499
|
+
}
|
|
500
|
+
if (selector === undefined && reactFiberId === undefined && componentName === undefined) {
|
|
501
|
+
return { error: "sourceLookup requires selector, reactFiberId, or componentName." };
|
|
502
|
+
}
|
|
503
|
+
if (input.includeDomHints !== undefined && typeof input.includeDomHints !== "boolean") {
|
|
504
|
+
return { error: "sourceLookup.includeDomHints must be a boolean when provided." };
|
|
505
|
+
}
|
|
506
|
+
const rawMaxWorkspaceFiles = input.maxWorkspaceFiles;
|
|
507
|
+
if (rawMaxWorkspaceFiles !== undefined && (typeof rawMaxWorkspaceFiles !== "number" || !Number.isInteger(rawMaxWorkspaceFiles) || rawMaxWorkspaceFiles <= 0)) {
|
|
508
|
+
return { error: "sourceLookup.maxWorkspaceFiles must be a positive integer when provided." };
|
|
509
|
+
}
|
|
510
|
+
if (typeof rawMaxWorkspaceFiles === "number" && rawMaxWorkspaceFiles > SOURCE_LOOKUP_MAX_WORKSPACE_FILES) {
|
|
511
|
+
return { error: `sourceLookup.maxWorkspaceFiles must be ${SOURCE_LOOKUP_MAX_WORKSPACE_FILES} or less.` };
|
|
512
|
+
}
|
|
513
|
+
const includeDomHints = input.includeDomHints !== false;
|
|
514
|
+
const maxWorkspaceFiles = (rawMaxWorkspaceFiles as number | undefined) ?? SOURCE_LOOKUP_DEFAULT_MAX_WORKSPACE_FILES;
|
|
515
|
+
const steps: CompiledAgentBrowserSourceLookupStep[] = [];
|
|
516
|
+
if (typeof selector === "string") {
|
|
517
|
+
steps.push({ action: "dom", args: ["is", "visible", selector] });
|
|
518
|
+
if (includeDomHints) {
|
|
519
|
+
steps.push({ action: "dom", args: ["get", "html", selector] });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (typeof reactFiberId === "string") {
|
|
523
|
+
steps.push({ action: "react", args: ["react", "inspect", reactFiberId] });
|
|
524
|
+
}
|
|
525
|
+
if (typeof componentName === "string") {
|
|
526
|
+
steps.push({ action: "react", args: ["react", "tree"] });
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
compiled: {
|
|
530
|
+
args: ["batch"],
|
|
531
|
+
query: { componentName, includeDomHints, maxWorkspaceFiles, reactFiberId, selector },
|
|
532
|
+
stdin: JSON.stringify(steps.map((step) => step.args)),
|
|
533
|
+
steps,
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function extractStringField(value: Record<string, unknown>, names: string[]): string | undefined {
|
|
539
|
+
for (const name of names) {
|
|
540
|
+
const field = value[name];
|
|
541
|
+
if (typeof field === "string" && field.trim().length > 0) return field;
|
|
542
|
+
}
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function extractNumberField(value: Record<string, unknown>, names: string[]): number | undefined {
|
|
547
|
+
for (const name of names) {
|
|
548
|
+
const field = value[name];
|
|
549
|
+
if (typeof field === "number" && Number.isFinite(field)) return field;
|
|
550
|
+
if (typeof field === "string" && /^\d+$/.test(field)) return Number(field);
|
|
551
|
+
}
|
|
552
|
+
return undefined;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function candidateKey(candidate: AgentBrowserSourceLookupCandidate): string {
|
|
556
|
+
return [candidate.source, candidate.file ?? "", candidate.line ?? "", candidate.column ?? "", candidate.componentName ?? ""].join(":");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function addSourceLookupCandidate(candidates: AgentBrowserSourceLookupCandidate[], candidate: AgentBrowserSourceLookupCandidate): void {
|
|
560
|
+
if (!candidates.some((existing) => candidateKey(existing) === candidateKey(candidate))) {
|
|
561
|
+
candidates.push(candidate);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function collectSourceCandidatesFromValue(value: unknown, source: "react-inspect" | "dom-attribute", candidates: AgentBrowserSourceLookupCandidate[], evidence: string[], depth = 0): void {
|
|
566
|
+
if (depth > 6 || value === undefined || value === null) return;
|
|
567
|
+
if (typeof value === "string") {
|
|
568
|
+
const sourcePattern = /([A-Za-z0-9_./@-]+\.(?:tsx|jsx|ts|js))(?:[:#](\d+))?(?:[:#](\d+))?/g;
|
|
569
|
+
for (const match of value.matchAll(sourcePattern)) {
|
|
570
|
+
addSourceLookupCandidate(candidates, {
|
|
571
|
+
column: match[3] ? Number(match[3]) : undefined,
|
|
572
|
+
confidence: source === "react-inspect" ? "high" : "medium",
|
|
573
|
+
evidence,
|
|
574
|
+
file: match[1],
|
|
575
|
+
line: match[2] ? Number(match[2]) : undefined,
|
|
576
|
+
source,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (Array.isArray(value)) {
|
|
582
|
+
for (const item of value) collectSourceCandidatesFromValue(item, source, candidates, evidence, depth + 1);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
if (!isRecord(value)) return;
|
|
586
|
+
const file = extractStringField(value, ["file", "fileName", "filename", "filePath", "path", "source", "url"]);
|
|
587
|
+
if (file && /\.(?:tsx|jsx|ts|js)(?:$|[:?#])/.test(file)) {
|
|
588
|
+
addSourceLookupCandidate(candidates, {
|
|
589
|
+
column: extractNumberField(value, ["column", "columnNumber", "col"]),
|
|
590
|
+
confidence: source === "react-inspect" ? "high" : "medium",
|
|
591
|
+
evidence,
|
|
592
|
+
file,
|
|
593
|
+
line: extractNumberField(value, ["line", "lineNumber"]),
|
|
594
|
+
source,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
for (const nested of Object.values(value)) {
|
|
598
|
+
collectSourceCandidatesFromValue(nested, source, candidates, evidence, depth + 1);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function getHtmlAttributeValue(html: string, name: string): string | undefined {
|
|
603
|
+
const pattern = new RegExp(`${name}=["']([^"']+)["']`, "i");
|
|
604
|
+
return pattern.exec(html)?.[1];
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function collectDomSourceCandidates(html: unknown, candidates: AgentBrowserSourceLookupCandidate[]): void {
|
|
608
|
+
if (typeof html !== "string") return;
|
|
609
|
+
const file = getHtmlAttributeValue(html, "(?:data-source-file|data-file|data-component-file|data-source)");
|
|
610
|
+
if (file && /\.(?:tsx|jsx|ts|js)$/.test(file)) {
|
|
611
|
+
const line = getHtmlAttributeValue(html, "(?:data-source-line|data-line)");
|
|
612
|
+
const column = getHtmlAttributeValue(html, "(?:data-source-column|data-column)");
|
|
613
|
+
addSourceLookupCandidate(candidates, {
|
|
614
|
+
column: column && /^\d+$/.test(column) ? Number(column) : undefined,
|
|
615
|
+
confidence: "medium",
|
|
616
|
+
evidence: ["selector HTML contained source-like data attributes"],
|
|
617
|
+
file,
|
|
618
|
+
line: line && /^\d+$/.test(line) ? Number(line) : undefined,
|
|
619
|
+
source: "dom-attribute",
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
collectSourceCandidatesFromValue(html, "dom-attribute", candidates, ["selector HTML contained source-like text"]);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function walkWorkspaceSourceFiles(root: string, maxFiles: number): Promise<string[]> {
|
|
626
|
+
const files: string[] = [];
|
|
627
|
+
async function visit(directory: string): Promise<void> {
|
|
628
|
+
if (files.length >= maxFiles) return;
|
|
629
|
+
let entries: Array<{ isDirectory: () => boolean; isFile: () => boolean; name: string }>;
|
|
630
|
+
try {
|
|
631
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
632
|
+
} catch {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
for (const entry of entries) {
|
|
636
|
+
if (files.length >= maxFiles) return;
|
|
637
|
+
const path = join(directory, entry.name);
|
|
638
|
+
if (entry.isDirectory()) {
|
|
639
|
+
if (!SOURCE_LOOKUP_IGNORED_DIRECTORIES.has(entry.name)) await visit(path);
|
|
640
|
+
} else if (entry.isFile() && SOURCE_LOOKUP_WORKSPACE_EXTENSIONS.has(extname(entry.name))) {
|
|
641
|
+
files.push(path);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
await visit(root);
|
|
646
|
+
return files;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function escapeRegExp(value: string): string {
|
|
650
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
async function collectWorkspaceComponentCandidates(query: CompiledAgentBrowserSourceLookup["query"], cwd: string, candidates: AgentBrowserSourceLookupCandidate[], limitations: string[]): Promise<void> {
|
|
654
|
+
if (!query.componentName) return;
|
|
655
|
+
const files = await walkWorkspaceSourceFiles(cwd, query.maxWorkspaceFiles);
|
|
656
|
+
if (files.length >= query.maxWorkspaceFiles) {
|
|
657
|
+
limitations.push(`Workspace source scan stopped at ${query.maxWorkspaceFiles} files.`);
|
|
658
|
+
}
|
|
659
|
+
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`);
|
|
660
|
+
for (const file of files) {
|
|
661
|
+
let text: string;
|
|
662
|
+
try {
|
|
663
|
+
text = await readFile(file, "utf8");
|
|
664
|
+
} catch {
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
const match = componentPattern.exec(text);
|
|
668
|
+
if (!match) continue;
|
|
669
|
+
const line = text.slice(0, match.index).split("\n").length;
|
|
670
|
+
addSourceLookupCandidate(candidates, {
|
|
671
|
+
componentName: query.componentName,
|
|
672
|
+
confidence: "low",
|
|
673
|
+
evidence: [`local workspace contains a matching ${query.componentName} declaration`],
|
|
674
|
+
file,
|
|
675
|
+
line,
|
|
676
|
+
source: "workspace-search",
|
|
677
|
+
});
|
|
678
|
+
if (candidates.filter((candidate) => candidate.source === "workspace-search").length >= 10) break;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function validateLookupMaxWorkspaceFiles(value: unknown, fieldName: string): { value?: number; error?: string } {
|
|
683
|
+
if (value === undefined) return { value: SOURCE_LOOKUP_DEFAULT_MAX_WORKSPACE_FILES };
|
|
684
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
685
|
+
return { error: `${fieldName} must be a positive integer when provided.` };
|
|
686
|
+
}
|
|
687
|
+
if (value > SOURCE_LOOKUP_MAX_WORKSPACE_FILES) {
|
|
688
|
+
return { error: `${fieldName} must be ${SOURCE_LOOKUP_MAX_WORKSPACE_FILES} or less.` };
|
|
689
|
+
}
|
|
690
|
+
return { value };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function analyzeSourceLookupResults(data: unknown, compiled: CompiledAgentBrowserSourceLookup, cwd: string): Promise<AgentBrowserSourceLookupAnalysis> {
|
|
694
|
+
const items = getBatchResultItems(data);
|
|
695
|
+
const candidates: AgentBrowserSourceLookupCandidate[] = [];
|
|
696
|
+
const limitations = [
|
|
697
|
+
"Experimental lookup only reports candidates with evidence; it cannot guarantee a DOM node maps to one source file.",
|
|
698
|
+
"React source hints require the page to be opened with --enable react-devtools and source information from the app build.",
|
|
699
|
+
];
|
|
700
|
+
let unsupported = false;
|
|
701
|
+
for (const item of items) {
|
|
702
|
+
const command = Array.isArray(item.command) ? item.command : [];
|
|
703
|
+
const result = isRecord(item.result) && "data" in item.result ? item.result.data : item.result;
|
|
704
|
+
if (item.success === false && command[0] === "react") unsupported = true;
|
|
705
|
+
if (command[0] === "react" && command[1] === "inspect") {
|
|
706
|
+
collectSourceCandidatesFromValue(result, "react-inspect", candidates, ["react inspect returned source-like metadata"]);
|
|
707
|
+
}
|
|
708
|
+
if (command[0] === "get" && command[1] === "html") {
|
|
709
|
+
collectDomSourceCandidates(result, candidates);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
await collectWorkspaceComponentCandidates(compiled.query, cwd, candidates, limitations);
|
|
713
|
+
const status: AgentBrowserSourceLookupStatus = candidates.length > 0 ? "candidates-found" : unsupported ? "unsupported" : "no-candidates";
|
|
714
|
+
return {
|
|
715
|
+
candidates,
|
|
716
|
+
limitations,
|
|
717
|
+
status,
|
|
718
|
+
summary: candidates.length > 0
|
|
719
|
+
? `Source lookup found ${candidates.length} candidate location(s).`
|
|
720
|
+
: unsupported
|
|
721
|
+
? "Source lookup could not inspect React metadata in this session."
|
|
722
|
+
: "Source lookup found no candidate locations.",
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function compileAgentBrowserNetworkSourceLookup(input: unknown): { compiled?: CompiledAgentBrowserNetworkSourceLookup; error?: string } {
|
|
727
|
+
if (!isRecord(input)) return { error: "networkSourceLookup must be an object." };
|
|
728
|
+
const filter = input.filter;
|
|
729
|
+
const requestId = input.requestId;
|
|
730
|
+
const url = input.url;
|
|
731
|
+
if (filter !== undefined && (typeof filter !== "string" || filter.trim().length === 0)) return { error: "networkSourceLookup.filter must be a non-empty string when provided." };
|
|
732
|
+
if (requestId !== undefined && (typeof requestId !== "string" || requestId.trim().length === 0)) return { error: "networkSourceLookup.requestId must be a non-empty string when provided." };
|
|
733
|
+
if (url !== undefined && (typeof url !== "string" || url.trim().length === 0)) return { error: "networkSourceLookup.url must be a non-empty string when provided." };
|
|
734
|
+
if (filter === undefined && requestId === undefined && url === undefined) return { error: "networkSourceLookup requires requestId, filter, or url." };
|
|
735
|
+
const maxWorkspaceFiles = validateLookupMaxWorkspaceFiles(input.maxWorkspaceFiles, "networkSourceLookup.maxWorkspaceFiles");
|
|
736
|
+
if (maxWorkspaceFiles.error) return { error: maxWorkspaceFiles.error };
|
|
737
|
+
const steps: Array<{ action: "network"; args: string[] }> = [];
|
|
738
|
+
if (typeof requestId === "string") {
|
|
739
|
+
steps.push({ action: "network", args: ["network", "request", requestId] });
|
|
740
|
+
}
|
|
741
|
+
const effectiveFilter = typeof filter === "string" ? filter : typeof url === "string" ? url : undefined;
|
|
742
|
+
if (effectiveFilter) {
|
|
743
|
+
steps.push({ action: "network", args: ["network", "requests", "--filter", effectiveFilter] });
|
|
744
|
+
}
|
|
745
|
+
return { compiled: { args: ["batch"], query: { filter, maxWorkspaceFiles: maxWorkspaceFiles.value as number, requestId, url }, stdin: JSON.stringify(steps.map((step) => step.args)), steps } };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function getResultPayload(item: Record<string, unknown>): unknown {
|
|
749
|
+
return isRecord(item.result) && "data" in item.result ? item.result.data : item.result;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function networkRequestMatchesQuery(url: string | undefined, queryText: string | undefined): boolean {
|
|
753
|
+
return queryText === undefined || url === undefined || url.includes(queryText) || queryText.includes(url);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function isFailedNetworkRecord(request: Record<string, unknown>): boolean {
|
|
757
|
+
const status = typeof request.status === "number" ? request.status : undefined;
|
|
758
|
+
const error = typeof request.error === "string" ? request.error : undefined;
|
|
759
|
+
return request.failed === true || error !== undefined || (status !== undefined && status >= 400);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function getFailedNetworkRequests(data: unknown, queryText?: string): AgentBrowserNetworkSourceLookupRequest[] {
|
|
763
|
+
const failed: AgentBrowserNetworkSourceLookupRequest[] = [];
|
|
764
|
+
for (const item of getBatchResultItems(data)) {
|
|
765
|
+
const payload = getResultPayload(item);
|
|
766
|
+
const requests = isRecord(payload) && Array.isArray(payload.requests) ? payload.requests : Array.isArray(payload) ? payload : isRecord(payload) ? [payload] : [];
|
|
767
|
+
for (const request of requests) {
|
|
768
|
+
if (!isRecord(request)) continue;
|
|
769
|
+
const url = typeof request.url === "string" ? request.url : undefined;
|
|
770
|
+
if (!networkRequestMatchesQuery(url, queryText) || !isFailedNetworkRecord(request)) continue;
|
|
771
|
+
failed.push({
|
|
772
|
+
error: typeof request.error === "string" ? request.error : undefined,
|
|
773
|
+
method: typeof request.method === "string" ? request.method : undefined,
|
|
774
|
+
requestId: typeof request.id === "string" ? request.id : typeof request.requestId === "string" ? request.requestId : undefined,
|
|
775
|
+
status: typeof request.status === "number" ? request.status : undefined,
|
|
776
|
+
url,
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return failed;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function addNetworkCandidate(candidates: AgentBrowserNetworkSourceLookupCandidate[], candidate: AgentBrowserNetworkSourceLookupCandidate): void {
|
|
784
|
+
const key = [candidate.source, candidate.file ?? "", candidate.line ?? "", candidate.requestUrl ?? ""].join(":");
|
|
785
|
+
if (!candidates.some((existing) => [existing.source, existing.file ?? "", existing.line ?? "", existing.requestUrl ?? ""].join(":") === key)) candidates.push(candidate);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function collectInitiatorCandidates(data: unknown, failedRequests: AgentBrowserNetworkSourceLookupRequest[], candidates: AgentBrowserNetworkSourceLookupCandidate[]): void {
|
|
789
|
+
const failedRequestIds = new Set(failedRequests.map((request) => request.requestId).filter((value): value is string => value !== undefined));
|
|
790
|
+
const failedRequestUrls = new Set(failedRequests.map((request) => request.url).filter((value): value is string => value !== undefined));
|
|
791
|
+
for (const item of getBatchResultItems(data)) {
|
|
792
|
+
const payload = getResultPayload(item);
|
|
793
|
+
const requestValues = isRecord(payload) && Array.isArray(payload.requests) ? payload.requests : [payload];
|
|
794
|
+
for (const value of requestValues) {
|
|
795
|
+
if (!isRecord(value)) continue;
|
|
796
|
+
const requestUrl = typeof value.url === "string" ? value.url : undefined;
|
|
797
|
+
const requestId = typeof value.id === "string" ? value.id : typeof value.requestId === "string" ? value.requestId : undefined;
|
|
798
|
+
const correlatesWithFailedRequest = (requestId !== undefined && failedRequestIds.has(requestId)) || (requestUrl !== undefined && failedRequestUrls.has(requestUrl));
|
|
799
|
+
if (!correlatesWithFailedRequest && !isFailedNetworkRecord(value)) continue;
|
|
800
|
+
for (const field of [value.initiator, value.stack, value.source, value.trace]) {
|
|
801
|
+
const localCandidates: AgentBrowserSourceLookupCandidate[] = [];
|
|
802
|
+
collectSourceCandidatesFromValue(field, "dom-attribute", localCandidates, ["failed network request included source-like initiator metadata"]);
|
|
803
|
+
for (const candidate of localCandidates) {
|
|
804
|
+
addNetworkCandidate(candidates, { confidence: "medium", evidence: candidate.evidence, file: candidate.file, line: candidate.line, requestUrl, source: "initiator" });
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async function collectWorkspaceRequestCandidates(query: CompiledAgentBrowserNetworkSourceLookup["query"], failedRequests: AgentBrowserNetworkSourceLookupRequest[], cwd: string, candidates: AgentBrowserNetworkSourceLookupCandidate[], limitations: string[]): Promise<void> {
|
|
812
|
+
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) => {
|
|
813
|
+
try {
|
|
814
|
+
const parsed = new URL(value);
|
|
815
|
+
return [value, parsed.pathname].filter((item) => item && item !== "/");
|
|
816
|
+
} catch {
|
|
817
|
+
return [value];
|
|
818
|
+
}
|
|
819
|
+
}))].slice(0, 8);
|
|
820
|
+
if (needles.length === 0) return;
|
|
821
|
+
const files = await walkWorkspaceSourceFiles(cwd, query.maxWorkspaceFiles);
|
|
822
|
+
if (files.length >= query.maxWorkspaceFiles) limitations.push(`Workspace source scan stopped at ${query.maxWorkspaceFiles} files.`);
|
|
823
|
+
for (const file of files) {
|
|
824
|
+
let text: string;
|
|
825
|
+
try { text = await readFile(file, "utf8"); } catch { continue; }
|
|
826
|
+
for (const needle of needles) {
|
|
827
|
+
const index = text.indexOf(needle);
|
|
828
|
+
if (index === -1) continue;
|
|
829
|
+
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" });
|
|
830
|
+
if (candidates.filter((candidate) => candidate.source === "workspace-search").length >= 10) return;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function redactNetworkSourceLookupUrl(value: string | undefined): string | undefined {
|
|
836
|
+
if (!value) return value;
|
|
837
|
+
try {
|
|
838
|
+
const isRelative = value.startsWith("/");
|
|
839
|
+
const url = new URL(value, isRelative ? "https://redacted.invalid" : undefined);
|
|
840
|
+
url.username = url.username ? "[REDACTED]" : "";
|
|
841
|
+
url.password = url.password ? "[REDACTED]" : "";
|
|
842
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
843
|
+
url.searchParams.set(key, "[REDACTED]");
|
|
844
|
+
}
|
|
845
|
+
if (/(?:token|secret|password|passwd|pwd|key|auth|session|jwt|credential)/i.test(url.hash)) {
|
|
846
|
+
url.hash = "#[REDACTED]";
|
|
847
|
+
}
|
|
848
|
+
return isRelative ? `${url.pathname}${url.search}${url.hash}` : url.toString();
|
|
849
|
+
} catch {
|
|
850
|
+
return redactSensitiveText(value
|
|
851
|
+
.replace(/([a-z][a-z0-9+.-]*:\/\/)\S+:\S+@/gi, "$1[REDACTED]@")
|
|
852
|
+
.replace(/([?&][^=]+)=([^&#\s"'\]]+)/g, "$1=[REDACTED]"));
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function redactNetworkSourceLookupArgs(args: string[]): string[] {
|
|
857
|
+
return redactInvocationArgs(args).map((arg) => redactNetworkSourceLookupUrl(arg) ?? arg);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function redactNetworkSourceLookupSurface(value: unknown): unknown {
|
|
861
|
+
if (typeof value === "string") return redactNetworkSourceLookupUrl(value) ?? value;
|
|
862
|
+
if (Array.isArray(value)) return value.map((item) => redactNetworkSourceLookupSurface(item));
|
|
863
|
+
if (!isRecord(value)) return value;
|
|
864
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactNetworkSourceLookupSurface(item)]));
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function redactNetworkSourceLookupAnalysis(analysis: AgentBrowserNetworkSourceLookupAnalysis): AgentBrowserNetworkSourceLookupAnalysis {
|
|
868
|
+
return {
|
|
869
|
+
...analysis,
|
|
870
|
+
candidates: analysis.candidates.map((candidate) => ({
|
|
871
|
+
...candidate,
|
|
872
|
+
evidence: candidate.evidence.map((item) => redactNetworkSourceLookupUrl(item) ?? redactSensitiveText(item)),
|
|
873
|
+
file: redactNetworkSourceLookupUrl(candidate.file),
|
|
874
|
+
requestUrl: redactNetworkSourceLookupUrl(candidate.requestUrl),
|
|
875
|
+
})),
|
|
876
|
+
failedRequests: analysis.failedRequests.map((request) => ({ ...request, error: redactNetworkSourceLookupUrl(request.error), url: redactNetworkSourceLookupUrl(request.url) })),
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
async function analyzeNetworkSourceLookupResults(data: unknown, compiled: CompiledAgentBrowserNetworkSourceLookup, cwd: string): Promise<AgentBrowserNetworkSourceLookupAnalysis> {
|
|
881
|
+
const limitations = [
|
|
882
|
+
"Experimental network source hints report candidates only; failed requests can be triggered indirectly by frameworks, caches, service workers, or third-party scripts.",
|
|
883
|
+
"Initiator/source-map metadata is upstream/browser-build dependent and may be absent.",
|
|
884
|
+
];
|
|
885
|
+
const failedRequests = getFailedNetworkRequests(data, compiled.query.url ?? compiled.query.filter);
|
|
886
|
+
const candidates: AgentBrowserNetworkSourceLookupCandidate[] = [];
|
|
887
|
+
collectInitiatorCandidates(data, failedRequests, candidates);
|
|
888
|
+
await collectWorkspaceRequestCandidates(compiled.query, failedRequests, cwd, candidates, limitations);
|
|
889
|
+
const status: AgentBrowserNetworkSourceLookupStatus = failedRequests.length === 0 ? "no-failed-requests" : candidates.length > 0 ? "failed-requests-found" : "no-candidates";
|
|
890
|
+
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.` };
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function appendSemanticActionTextArg(args: string[], action: string, text: string | undefined): void {
|
|
894
|
+
if ((action === "fill" || action === "select") && text) {
|
|
895
|
+
args.push(text);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function getCompiledSemanticActionCommandIndex(compiled: CompiledAgentBrowserSemanticAction): number {
|
|
900
|
+
return compiled.args[0] === "--session" ? 2 : 0;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function getCompiledSemanticActionTextArg(compiled: CompiledAgentBrowserSemanticAction): string | undefined {
|
|
904
|
+
if (compiled.action !== "fill" && compiled.action !== "select") return undefined;
|
|
905
|
+
const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
|
|
906
|
+
if (commandIndex < 0) return undefined;
|
|
907
|
+
const markerIndex = compiled.args.indexOf("--name");
|
|
908
|
+
return markerIndex >= 0 ? compiled.args[markerIndex - 1] : compiled.args[commandIndex + 4];
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function getCompiledSemanticActionSessionPrefix(compiled: CompiledAgentBrowserSemanticAction): string[] {
|
|
912
|
+
const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
|
|
913
|
+
return commandIndex > 0 ? compiled.args.slice(0, commandIndex) : [];
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const SEMANTIC_ACTION_CANDIDATE_ACTION_IDS = new Set([
|
|
917
|
+
"try-searchbox-name-candidate",
|
|
918
|
+
"try-textbox-name-candidate",
|
|
919
|
+
"try-button-name-candidate",
|
|
920
|
+
"try-link-name-candidate",
|
|
921
|
+
"try-labeled-textbox-candidate",
|
|
922
|
+
]);
|
|
923
|
+
|
|
924
|
+
function formatSemanticActionCandidateText(actions: AgentBrowserNextAction[]): string | undefined {
|
|
925
|
+
const candidateActions = actions.filter((action) => SEMANTIC_ACTION_CANDIDATE_ACTION_IDS.has(action.id) && action.params?.args);
|
|
926
|
+
if (candidateActions.length === 0) return undefined;
|
|
927
|
+
return [
|
|
928
|
+
"Agent-browser candidate fallbacks:",
|
|
929
|
+
...candidateActions.map((action) => `- ${action.id}: agent_browser ${JSON.stringify({ args: action.params?.args })} — ${action.reason}`),
|
|
930
|
+
].join("\n");
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function buildSemanticActionCandidateActions(compiled: CompiledAgentBrowserSemanticAction): AgentBrowserNextAction[] {
|
|
934
|
+
const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
|
|
935
|
+
if (commandIndex < 0) return [];
|
|
936
|
+
const locator = compiled.args[commandIndex + 1];
|
|
937
|
+
const value = compiled.args[commandIndex + 2];
|
|
938
|
+
if (!locator || !value) return [];
|
|
939
|
+
const text = getCompiledSemanticActionTextArg(compiled);
|
|
940
|
+
const sessionPrefix = getCompiledSemanticActionSessionPrefix(compiled);
|
|
941
|
+
const buildRoleCandidate = (role: string, id: string, reason: string): AgentBrowserNextAction => {
|
|
942
|
+
const args = [...sessionPrefix, "find", "role", role, compiled.action];
|
|
943
|
+
appendSemanticActionTextArg(args, compiled.action, text);
|
|
944
|
+
args.push("--name", value);
|
|
945
|
+
return {
|
|
946
|
+
id,
|
|
947
|
+
params: { args: redactInvocationArgs(args) },
|
|
948
|
+
reason,
|
|
949
|
+
safety: "Candidate locator fallback only; inspect the page if multiple elements could match the same accessible name.",
|
|
950
|
+
tool: "agent_browser" as const,
|
|
951
|
+
};
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
if (locator === "placeholder" && compiled.action === "fill") {
|
|
955
|
+
return [
|
|
956
|
+
buildRoleCandidate("searchbox", "try-searchbox-name-candidate", "Retry against a searchbox with the same accessible name; many search inputs expose names instead of placeholders."),
|
|
957
|
+
buildRoleCandidate("textbox", "try-textbox-name-candidate", "Retry against a textbox with the same accessible name when placeholder lookup misses."),
|
|
958
|
+
];
|
|
959
|
+
}
|
|
960
|
+
if (locator === "text" && compiled.action === "click") {
|
|
961
|
+
return [
|
|
962
|
+
buildRoleCandidate("button", "try-button-name-candidate", "Retry against a button with the same accessible name when text lookup misses."),
|
|
963
|
+
buildRoleCandidate("link", "try-link-name-candidate", "Retry against a link with the same accessible name when text lookup misses."),
|
|
964
|
+
];
|
|
965
|
+
}
|
|
966
|
+
if (locator === "label" && compiled.action === "fill") {
|
|
967
|
+
return [buildRoleCandidate("textbox", "try-labeled-textbox-candidate", "Retry against a textbox with the same accessible name when label lookup misses.")];
|
|
968
|
+
}
|
|
969
|
+
return [];
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function compileAgentBrowserSemanticAction(input: unknown): { compiled?: CompiledAgentBrowserSemanticAction; error?: string } {
|
|
973
|
+
if (!isRecord(input)) {
|
|
974
|
+
return { error: "semanticAction must be an object." };
|
|
975
|
+
}
|
|
976
|
+
const action = input.action;
|
|
977
|
+
const locator = input.locator;
|
|
978
|
+
const value = input.value;
|
|
979
|
+
const text = input.text;
|
|
980
|
+
const role = input.role;
|
|
981
|
+
const name = input.name;
|
|
982
|
+
const session = input.session;
|
|
983
|
+
if (typeof action !== "string" || !AGENT_BROWSER_SEMANTIC_ACTIONS.includes(action as AgentBrowserSemanticActionName)) {
|
|
984
|
+
return { error: `semanticAction.action must be one of: ${AGENT_BROWSER_SEMANTIC_ACTIONS.join(", ")}.` };
|
|
985
|
+
}
|
|
986
|
+
if (typeof locator !== "string" || !AGENT_BROWSER_SEMANTIC_LOCATORS.includes(locator as AgentBrowserSemanticLocator)) {
|
|
987
|
+
return { error: `semanticAction.locator must be one of: ${AGENT_BROWSER_SEMANTIC_LOCATORS.join(", ")}.` };
|
|
988
|
+
}
|
|
989
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
990
|
+
return { error: "semanticAction.value must be a non-empty string." };
|
|
991
|
+
}
|
|
992
|
+
if (text !== undefined && typeof text !== "string") {
|
|
993
|
+
return { error: "semanticAction.text must be a string when provided." };
|
|
994
|
+
}
|
|
995
|
+
if ((action === "fill" || action === "select") && (typeof text !== "string" || text.length === 0)) {
|
|
996
|
+
return { error: `semanticAction.text is required for ${action}.` };
|
|
997
|
+
}
|
|
998
|
+
if (action !== "fill" && action !== "select" && text !== undefined) {
|
|
999
|
+
return { error: `semanticAction.text is only supported for fill and select actions.` };
|
|
1000
|
+
}
|
|
1001
|
+
if (role !== undefined && (locator !== "role" || role !== value)) {
|
|
1002
|
+
return { error: "semanticAction.role is only supported for locator=role and must match value." };
|
|
1003
|
+
}
|
|
1004
|
+
if (name !== undefined && (locator !== "role" || typeof name !== "string" || name.length === 0)) {
|
|
1005
|
+
return { error: "semanticAction.name is only supported as a non-empty string for locator=role." };
|
|
1006
|
+
}
|
|
1007
|
+
if (session !== undefined && (typeof session !== "string" || session.trim().length === 0)) {
|
|
1008
|
+
return { error: "semanticAction.session must be a non-empty string when provided." };
|
|
1009
|
+
}
|
|
1010
|
+
const args = typeof session === "string" ? ["--session", session, "find", locator, value, action] : ["find", locator, value, action];
|
|
1011
|
+
if (action === "fill" || action === "select") {
|
|
1012
|
+
args.push(text as string);
|
|
1013
|
+
}
|
|
1014
|
+
if (locator === "role" && typeof name === "string") {
|
|
1015
|
+
args.push("--name", name);
|
|
1016
|
+
}
|
|
1017
|
+
return { compiled: { action: action as AgentBrowserSemanticActionName, locator: locator as AgentBrowserSemanticLocator, args } };
|
|
1018
|
+
}
|
|
1019
|
+
|
|
108
1020
|
const TUI_COLLAPSED_OUTPUT_MAX_LINES = 10;
|
|
109
1021
|
const TUI_INVOCATION_PREVIEW_MAX_CHARS = 120;
|
|
110
1022
|
const ANSI_CONTROL_SEQUENCE_PATTERN = /\x1B(?:\][^\x07\x1B]*(?:\x07|\x1B\\)|\[[0-?]*[ -/]*[@-~]|P[^\x1B]*(?:\x1B\\)|_[^\x1B]*(?:\x1B\\)|\^[^\x1B]*(?:\x1B\\)|[@-Z\\-_])/g;
|
|
@@ -178,7 +1090,15 @@ function formatVisualTruncationNotice(remainingLines: number, totalLines: number
|
|
|
178
1090
|
|
|
179
1091
|
function formatAgentBrowserRenderCall(args: unknown, theme: Theme): string {
|
|
180
1092
|
const input = isRecord(args) ? args : {};
|
|
181
|
-
const
|
|
1093
|
+
const semanticAction = compileAgentBrowserSemanticAction(input.semanticAction);
|
|
1094
|
+
const job = compileAgentBrowserJob(input.job);
|
|
1095
|
+
const qa = compileAgentBrowserQaPreset(input.qa);
|
|
1096
|
+
const sourceLookup = compileAgentBrowserSourceLookup(input.sourceLookup);
|
|
1097
|
+
const networkSourceLookup = compileAgentBrowserNetworkSourceLookup(input.networkSourceLookup);
|
|
1098
|
+
const generatedBatch = networkSourceLookup.compiled ?? sourceLookup.compiled ?? job.compiled ?? qa.compiled;
|
|
1099
|
+
const rawArgs = Array.isArray(input.args)
|
|
1100
|
+
? input.args.filter((value): value is string => typeof value === "string")
|
|
1101
|
+
: (semanticAction.compiled?.args ?? generatedBatch?.args ?? []);
|
|
182
1102
|
const redactedArgs = redactInvocationArgs(rawArgs);
|
|
183
1103
|
const invocation = sanitizeDisplayText(redactedArgs.join(" ")).replace(/\s+/g, " ").trim();
|
|
184
1104
|
const invocationPreview =
|
|
@@ -475,6 +1395,72 @@ interface NavigationSummary {
|
|
|
475
1395
|
url?: string;
|
|
476
1396
|
}
|
|
477
1397
|
|
|
1398
|
+
interface OverlayBlockerCandidate {
|
|
1399
|
+
args: string[];
|
|
1400
|
+
name?: string;
|
|
1401
|
+
reason: string;
|
|
1402
|
+
ref: string;
|
|
1403
|
+
role?: string;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
interface OverlayBlockerDiagnostic {
|
|
1407
|
+
candidates: OverlayBlockerCandidate[];
|
|
1408
|
+
snapshot: SessionRefSnapshot;
|
|
1409
|
+
summary: string;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
interface SelectorTextVisibilityDiagnostic {
|
|
1413
|
+
firstMatchVisible?: boolean;
|
|
1414
|
+
firstVisibleTextPreview?: string;
|
|
1415
|
+
matchCount: number;
|
|
1416
|
+
selector: string;
|
|
1417
|
+
summary: string;
|
|
1418
|
+
visibleCount: number;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
interface TimeoutArtifactEvidence {
|
|
1422
|
+
absolutePath: string;
|
|
1423
|
+
exists: boolean;
|
|
1424
|
+
path: string;
|
|
1425
|
+
sizeBytes?: number;
|
|
1426
|
+
stepIndex: number;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
interface TimeoutPartialProgress {
|
|
1430
|
+
artifacts: TimeoutArtifactEvidence[];
|
|
1431
|
+
currentPage?: {
|
|
1432
|
+
title?: string;
|
|
1433
|
+
url?: string;
|
|
1434
|
+
};
|
|
1435
|
+
steps?: Array<{ args: string[]; index: number }>;
|
|
1436
|
+
summary: string;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
interface EvalStdinHint {
|
|
1440
|
+
reason: string;
|
|
1441
|
+
suggestion: string;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
interface ArtifactCleanupGuidance {
|
|
1445
|
+
explicitArtifactPaths: string[];
|
|
1446
|
+
note: string;
|
|
1447
|
+
owner: "host-file-tools";
|
|
1448
|
+
summary: string;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
interface ManagedSessionOutcome {
|
|
1452
|
+
activeAfter: boolean;
|
|
1453
|
+
activeBefore: boolean;
|
|
1454
|
+
attemptedSessionName?: string;
|
|
1455
|
+
currentSessionName: string;
|
|
1456
|
+
previousSessionName: string;
|
|
1457
|
+
replacedSessionName?: string;
|
|
1458
|
+
sessionMode: "auto" | "fresh";
|
|
1459
|
+
status: "abandoned" | "closed" | "created" | "preserved" | "replaced" | "unchanged";
|
|
1460
|
+
succeeded: boolean;
|
|
1461
|
+
summary: string;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
478
1464
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
479
1465
|
return typeof value === "object" && value !== null;
|
|
480
1466
|
}
|
|
@@ -878,7 +1864,7 @@ function shouldCaptureNavigationSummary(command: string | undefined, data: unkno
|
|
|
878
1864
|
);
|
|
879
1865
|
}
|
|
880
1866
|
|
|
881
|
-
function extractStringResultField(data: unknown, fieldName: "title" | "url"): string | undefined {
|
|
1867
|
+
function extractStringResultField(data: unknown, fieldName: "result" | "title" | "url"): string | undefined {
|
|
882
1868
|
if (typeof data === "string") {
|
|
883
1869
|
const text = data.trim();
|
|
884
1870
|
return text.length > 0 ? text : undefined;
|
|
@@ -915,6 +1901,21 @@ interface OrderedSessionTabTarget {
|
|
|
915
1901
|
target: SessionTabTarget;
|
|
916
1902
|
}
|
|
917
1903
|
|
|
1904
|
+
interface SessionRefSnapshot {
|
|
1905
|
+
refIds: string[];
|
|
1906
|
+
target?: SessionTabTarget;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
interface OrderedSessionRefSnapshot extends SessionRefSnapshot {
|
|
1910
|
+
order: number;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
interface StaleRefPreflight {
|
|
1914
|
+
message: string;
|
|
1915
|
+
refIds: string[];
|
|
1916
|
+
snapshot?: SessionRefSnapshot;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
918
1919
|
interface AboutBlankSessionMismatch {
|
|
919
1920
|
activeUrl: "about:blank";
|
|
920
1921
|
recoveryApplied: boolean;
|
|
@@ -923,7 +1924,7 @@ interface AboutBlankSessionMismatch {
|
|
|
923
1924
|
targetUrl: string;
|
|
924
1925
|
}
|
|
925
1926
|
|
|
926
|
-
function getLatestSessionTabTargetOrder(targets: Map<string,
|
|
1927
|
+
function getLatestSessionTabTargetOrder(targets: Map<string, { order: number }>): number {
|
|
927
1928
|
let latestOrder = 0;
|
|
928
1929
|
for (const target of targets.values()) {
|
|
929
1930
|
latestOrder = Math.max(latestOrder, target.order);
|
|
@@ -932,7 +1933,7 @@ function getLatestSessionTabTargetOrder(targets: Map<string, OrderedSessionTabTa
|
|
|
932
1933
|
}
|
|
933
1934
|
|
|
934
1935
|
function shouldApplySessionTabTargetUpdate(options: {
|
|
935
|
-
current?:
|
|
1936
|
+
current?: { order: number };
|
|
936
1937
|
updateOrder: number;
|
|
937
1938
|
}): boolean {
|
|
938
1939
|
return !options.current || options.updateOrder >= options.current.order;
|
|
@@ -1072,10 +2073,70 @@ function restoreSessionTabTargetsFromBranch(branch: unknown[]): Map<string, Orde
|
|
|
1072
2073
|
: undefined;
|
|
1073
2074
|
if (sessionTabTarget) {
|
|
1074
2075
|
restoredOrder += 1;
|
|
1075
|
-
restoredTargets.set(sessionName, { order: restoredOrder, target: sessionTabTarget });
|
|
2076
|
+
restoredTargets.set(sessionName, { order: restoredOrder, target: sessionTabTarget });
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
return restoredTargets;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
function extractRefSnapshotFromData(data: unknown): SessionRefSnapshot | undefined {
|
|
2083
|
+
if (!isRecord(data)) return undefined;
|
|
2084
|
+
const refIds = isRecord(data.refs) ? Object.keys(data.refs).filter((refId) => /^e\d+$/.test(refId)) : [];
|
|
2085
|
+
if (refIds.length === 0) return undefined;
|
|
2086
|
+
return {
|
|
2087
|
+
refIds,
|
|
2088
|
+
target: extractSessionTabTargetFromData(data),
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function extractRefSnapshotFromBatchResults(data: unknown): SessionRefSnapshot | undefined {
|
|
2093
|
+
if (!Array.isArray(data)) return undefined;
|
|
2094
|
+
let latestSnapshot: SessionRefSnapshot | undefined;
|
|
2095
|
+
for (const item of data) {
|
|
2096
|
+
if (!isRecord(item) || item.success === false) continue;
|
|
2097
|
+
const [name] = extractBatchResultCommand(item);
|
|
2098
|
+
if (name !== "snapshot") continue;
|
|
2099
|
+
latestSnapshot = extractRefSnapshotFromData(item.result) ?? latestSnapshot;
|
|
2100
|
+
}
|
|
2101
|
+
return latestSnapshot;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
function restoreSessionRefSnapshotsFromBranch(branch: unknown[]): Map<string, OrderedSessionRefSnapshot> {
|
|
2105
|
+
const restoredSnapshots = new Map<string, OrderedSessionRefSnapshot>();
|
|
2106
|
+
let restoredOrder = 0;
|
|
2107
|
+
for (const entry of branch) {
|
|
2108
|
+
if (!isRecord(entry) || entry.type !== "message") continue;
|
|
2109
|
+
const message = isRecord(entry.message) ? entry.message : undefined;
|
|
2110
|
+
if (!message || message.toolName !== "agent_browser") continue;
|
|
2111
|
+
const details = isRecord(message.details) ? message.details : undefined;
|
|
2112
|
+
if (!details) continue;
|
|
2113
|
+
const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
|
|
2114
|
+
if (!sessionName) continue;
|
|
2115
|
+
const command = typeof details.command === "string" ? details.command : undefined;
|
|
2116
|
+
if (command === "close" && message.isError !== true) {
|
|
2117
|
+
restoredOrder += 1;
|
|
2118
|
+
restoredSnapshots.delete(sessionName);
|
|
2119
|
+
continue;
|
|
2120
|
+
}
|
|
2121
|
+
const refSnapshot = isRecord(details.refSnapshot)
|
|
2122
|
+
? {
|
|
2123
|
+
refIds: Array.isArray(details.refSnapshot.refIds)
|
|
2124
|
+
? details.refSnapshot.refIds.filter((refId): refId is string => typeof refId === "string" && /^e\d+$/.test(refId))
|
|
2125
|
+
: [],
|
|
2126
|
+
target: isRecord(details.refSnapshot.target)
|
|
2127
|
+
? normalizeSessionTabTarget({
|
|
2128
|
+
title: typeof details.refSnapshot.target.title === "string" ? details.refSnapshot.target.title : undefined,
|
|
2129
|
+
url: typeof details.refSnapshot.target.url === "string" ? details.refSnapshot.target.url : undefined,
|
|
2130
|
+
})
|
|
2131
|
+
: undefined,
|
|
2132
|
+
}
|
|
2133
|
+
: undefined;
|
|
2134
|
+
if (refSnapshot && refSnapshot.refIds.length > 0) {
|
|
2135
|
+
restoredOrder += 1;
|
|
2136
|
+
restoredSnapshots.set(sessionName, { ...refSnapshot, order: restoredOrder });
|
|
1076
2137
|
}
|
|
1077
2138
|
}
|
|
1078
|
-
return
|
|
2139
|
+
return restoredSnapshots;
|
|
1079
2140
|
}
|
|
1080
2141
|
|
|
1081
2142
|
function restoreArtifactManifestFromBranch(branch: unknown[]): SessionArtifactManifest | undefined {
|
|
@@ -1227,6 +2288,46 @@ function parseUserBatchStdin(stdin: string | undefined): { error?: string; steps
|
|
|
1227
2288
|
}
|
|
1228
2289
|
}
|
|
1229
2290
|
|
|
2291
|
+
const REF_INVALIDATING_BATCH_COMMANDS = new Set([
|
|
2292
|
+
"back",
|
|
2293
|
+
"check",
|
|
2294
|
+
"click",
|
|
2295
|
+
"dblclick",
|
|
2296
|
+
"drag",
|
|
2297
|
+
"fill",
|
|
2298
|
+
"forward",
|
|
2299
|
+
"goto",
|
|
2300
|
+
"keyboard",
|
|
2301
|
+
"mouse",
|
|
2302
|
+
"navigate",
|
|
2303
|
+
"open",
|
|
2304
|
+
"press",
|
|
2305
|
+
"reload",
|
|
2306
|
+
"select",
|
|
2307
|
+
"type",
|
|
2308
|
+
"uncheck",
|
|
2309
|
+
"upload",
|
|
2310
|
+
]);
|
|
2311
|
+
|
|
2312
|
+
const REF_GUARDED_COMMANDS = new Set([
|
|
2313
|
+
"check",
|
|
2314
|
+
"click",
|
|
2315
|
+
"dblclick",
|
|
2316
|
+
"download",
|
|
2317
|
+
"drag",
|
|
2318
|
+
"fill",
|
|
2319
|
+
"focus",
|
|
2320
|
+
"hover",
|
|
2321
|
+
"keyboard",
|
|
2322
|
+
"mouse",
|
|
2323
|
+
"press",
|
|
2324
|
+
"scrollintoview",
|
|
2325
|
+
"select",
|
|
2326
|
+
"type",
|
|
2327
|
+
"uncheck",
|
|
2328
|
+
"upload",
|
|
2329
|
+
]);
|
|
2330
|
+
|
|
1230
2331
|
function getStaleRefArgs(commandTokens: string[], stdin?: string): string[] {
|
|
1231
2332
|
if (commandTokens[0] !== "batch" || stdin === undefined) {
|
|
1232
2333
|
return commandTokens;
|
|
@@ -1238,6 +2339,101 @@ function getStaleRefArgs(commandTokens: string[], stdin?: string): string[] {
|
|
|
1238
2339
|
return parsed.steps.flatMap((step) => step);
|
|
1239
2340
|
}
|
|
1240
2341
|
|
|
2342
|
+
function collectRefsFromTokens(tokens: string[]): string[] {
|
|
2343
|
+
return tokens.filter((token) => /^@e\d+\b/.test(token)).map((token) => token.slice(1));
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
function getGuardedRefUsage(commandTokens: string[], stdin?: string): string[] {
|
|
2347
|
+
const collectFromStep = (step: string[]) => REF_GUARDED_COMMANDS.has(step[0] ?? "") ? collectRefsFromTokens(step) : [];
|
|
2348
|
+
if (commandTokens[0] !== "batch" || stdin === undefined) {
|
|
2349
|
+
return collectFromStep(commandTokens);
|
|
2350
|
+
}
|
|
2351
|
+
const parsed = parseUserBatchStdin(stdin);
|
|
2352
|
+
if (parsed.error || parsed.steps === undefined) {
|
|
2353
|
+
return collectFromStep(commandTokens);
|
|
2354
|
+
}
|
|
2355
|
+
const refsBeforeInBatchSnapshot: string[] = [];
|
|
2356
|
+
for (const step of parsed.steps) {
|
|
2357
|
+
if ((step[0] ?? "") === "snapshot") break;
|
|
2358
|
+
refsBeforeInBatchSnapshot.push(...collectFromStep(step));
|
|
2359
|
+
}
|
|
2360
|
+
return refsBeforeInBatchSnapshot;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
function targetsMatch(left: SessionTabTarget | undefined, right: SessionTabTarget | undefined): boolean {
|
|
2364
|
+
if (!left || !right) return true;
|
|
2365
|
+
return normalizeComparableUrl(left.url) === normalizeComparableUrl(right.url);
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
function getBatchRefInvalidationMessage(commandTokens: string[], stdin?: string): string | undefined {
|
|
2369
|
+
if (commandTokens[0] !== "batch" || stdin === undefined) return undefined;
|
|
2370
|
+
const parsed = parseUserBatchStdin(stdin);
|
|
2371
|
+
if (parsed.error || parsed.steps === undefined) return undefined;
|
|
2372
|
+
let priorStepInvalidatesRefs = false;
|
|
2373
|
+
for (const step of parsed.steps) {
|
|
2374
|
+
if ((step[0] ?? "") === "snapshot") {
|
|
2375
|
+
priorStepInvalidatesRefs = false;
|
|
2376
|
+
}
|
|
2377
|
+
const refIds = collectRefsFromTokens(step);
|
|
2378
|
+
if (refIds.length > 0 && REF_GUARDED_COMMANDS.has(step[0] ?? "") && priorStepInvalidatesRefs) {
|
|
2379
|
+
return `Batch step ${step[0]} uses page-scoped ref ${refIds.map((refId) => `@${refId}`).join(", ")} after an earlier batch step can navigate or mutate the page. Split the batch, run snapshot -i after the page-changing step, then retry with current refs.`;
|
|
2380
|
+
}
|
|
2381
|
+
if (REF_INVALIDATING_BATCH_COMMANDS.has(step[0] ?? "")) {
|
|
2382
|
+
priorStepInvalidatesRefs = true;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
return undefined;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function buildStaleRefPreflight(options: {
|
|
2389
|
+
commandTokens: string[];
|
|
2390
|
+
currentTarget?: SessionTabTarget;
|
|
2391
|
+
refSnapshot?: SessionRefSnapshot;
|
|
2392
|
+
stdin?: string;
|
|
2393
|
+
}): StaleRefPreflight | undefined {
|
|
2394
|
+
const usedRefIds = [...new Set(getGuardedRefUsage(options.commandTokens, options.stdin))];
|
|
2395
|
+
const batchInvalidationMessage = getBatchRefInvalidationMessage(options.commandTokens, options.stdin);
|
|
2396
|
+
if (batchInvalidationMessage && usedRefIds.length > 0) {
|
|
2397
|
+
return {
|
|
2398
|
+
message: batchInvalidationMessage,
|
|
2399
|
+
refIds: usedRefIds,
|
|
2400
|
+
snapshot: options.refSnapshot,
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
if (usedRefIds.length === 0 || !options.refSnapshot) return undefined;
|
|
2404
|
+
if (!targetsMatch(options.refSnapshot.target, options.currentTarget)) {
|
|
2405
|
+
return {
|
|
2406
|
+
message: `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} came from a snapshot for ${options.refSnapshot.target?.url ?? "a prior page"}, but the current session target is ${options.currentTarget?.url ?? "unknown"}. Run snapshot -i again before using page-scoped refs.`,
|
|
2407
|
+
refIds: usedRefIds,
|
|
2408
|
+
snapshot: options.refSnapshot,
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
const knownRefs = new Set(options.refSnapshot.refIds);
|
|
2412
|
+
const missingRefs = usedRefIds.filter((refId) => !knownRefs.has(refId));
|
|
2413
|
+
if (missingRefs.length > 0) {
|
|
2414
|
+
return {
|
|
2415
|
+
message: `Ref ${missingRefs.map((refId) => `@${refId}`).join(", ")} was not present in the latest snapshot for this session. Run snapshot -i again before using page-scoped refs.`,
|
|
2416
|
+
refIds: missingRefs,
|
|
2417
|
+
snapshot: options.refSnapshot,
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
return undefined;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
function sessionPrefixArgs(sessionName: string | undefined, args: string[]): string[] {
|
|
2424
|
+
return sessionName && args[0] !== "--session" ? ["--session", sessionName, ...args] : args;
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
function sessionAwareStaleRefNextActions(sessionName: string | undefined): AgentBrowserNextAction[] {
|
|
2428
|
+
return (buildAgentBrowserNextActions({ failureCategory: "stale-ref", resultCategory: "failure" }) ?? []).map((action) => {
|
|
2429
|
+
const actionArgs = action.params?.args;
|
|
2430
|
+
return {
|
|
2431
|
+
...action,
|
|
2432
|
+
params: action.params && actionArgs ? { ...action.params, args: sessionPrefixArgs(sessionName, actionArgs) } : action.params,
|
|
2433
|
+
};
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
|
|
1241
2437
|
function buildPinnedBatchPlan(options: {
|
|
1242
2438
|
command?: string;
|
|
1243
2439
|
commandTokens: string[];
|
|
@@ -1371,14 +2567,16 @@ async function runSessionCommandData(options: {
|
|
|
1371
2567
|
cwd: string;
|
|
1372
2568
|
sessionName?: string;
|
|
1373
2569
|
signal?: AbortSignal;
|
|
2570
|
+
stdin?: string;
|
|
1374
2571
|
}): Promise<unknown | undefined> {
|
|
1375
|
-
const { args, cwd, sessionName, signal } = options;
|
|
2572
|
+
const { args, cwd, sessionName, signal, stdin } = options;
|
|
1376
2573
|
if (!sessionName) return undefined;
|
|
1377
2574
|
|
|
1378
2575
|
const processResult = await runAgentBrowserProcess({
|
|
1379
2576
|
args: ["--json", "--session", sessionName, ...args],
|
|
1380
2577
|
cwd,
|
|
1381
2578
|
signal,
|
|
2579
|
+
stdin,
|
|
1382
2580
|
});
|
|
1383
2581
|
try {
|
|
1384
2582
|
if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
|
|
@@ -1424,6 +2622,401 @@ function mergeNavigationSummaryIntoData(data: unknown, navigationSummary: Naviga
|
|
|
1424
2622
|
return { navigationSummary, result: data };
|
|
1425
2623
|
}
|
|
1426
2624
|
|
|
2625
|
+
function getSnapshotRefRecord(data: unknown): Record<string, unknown> | undefined {
|
|
2626
|
+
return isRecord(data) && isRecord(data.refs) ? data.refs : undefined;
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
const OVERLAY_CLOSE_NAME_PATTERN = /(?:\b(?:close|dismiss|no thanks|not now|maybe later|hide|skip|continue without|x)\b|^\s*×\s*$)/i;
|
|
2630
|
+
const OVERLAY_CONTEXT_NAME_PATTERN = /\b(?:banner|modal|dialog|popup|pop-up|overlay|donat(?:e|ion)|subscribe|sign in|login|cookie|privacy|consent)\b/i;
|
|
2631
|
+
const OVERLAY_CONTEXT_ROLES = new Set(["alertdialog", "dialog"]);
|
|
2632
|
+
const OVERLAY_ACTION_ROLES = new Set(["button", "link", "menuitem"]);
|
|
2633
|
+
const OVERLAY_BLOCKER_CANDIDATE_LIMIT = 3;
|
|
2634
|
+
|
|
2635
|
+
function getOverlayBlockerCandidates(snapshotData: unknown): OverlayBlockerCandidate[] {
|
|
2636
|
+
const refs = getSnapshotRefRecord(snapshotData);
|
|
2637
|
+
if (!refs) return [];
|
|
2638
|
+
const hasOverlayContext = Object.values(refs).some((entry) => {
|
|
2639
|
+
if (!isRecord(entry)) return false;
|
|
2640
|
+
const role = typeof entry.role === "string" ? entry.role : "";
|
|
2641
|
+
const name = typeof entry.name === "string" ? entry.name : "";
|
|
2642
|
+
return OVERLAY_CONTEXT_ROLES.has(role.toLowerCase()) || OVERLAY_CONTEXT_NAME_PATTERN.test(name);
|
|
2643
|
+
});
|
|
2644
|
+
if (!hasOverlayContext) return [];
|
|
2645
|
+
const candidates: OverlayBlockerCandidate[] = [];
|
|
2646
|
+
for (const [ref, entry] of Object.entries(refs)) {
|
|
2647
|
+
if (!/^e\d+$/.test(ref) || !isRecord(entry)) continue;
|
|
2648
|
+
const role = typeof entry.role === "string" ? entry.role : undefined;
|
|
2649
|
+
const name = typeof entry.name === "string" ? entry.name : undefined;
|
|
2650
|
+
if (!role || !OVERLAY_ACTION_ROLES.has(role.toLowerCase()) || !name || !OVERLAY_CLOSE_NAME_PATTERN.test(name)) continue;
|
|
2651
|
+
candidates.push({
|
|
2652
|
+
args: ["click", `@${ref}`],
|
|
2653
|
+
name,
|
|
2654
|
+
reason: `Visible ${role} ${JSON.stringify(name)} appears in a snapshot that also contains overlay/banner/dialog context.`,
|
|
2655
|
+
ref: `@${ref}`,
|
|
2656
|
+
role,
|
|
2657
|
+
});
|
|
2658
|
+
if (candidates.length >= OVERLAY_BLOCKER_CANDIDATE_LIMIT) break;
|
|
2659
|
+
}
|
|
2660
|
+
return candidates;
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
function formatOverlayBlockerText(diagnostic: OverlayBlockerDiagnostic): string {
|
|
2664
|
+
return [
|
|
2665
|
+
"Possible overlay blockers:",
|
|
2666
|
+
...diagnostic.candidates.map((candidate) => `- ${candidate.ref}${candidate.role ? ` ${candidate.role}` : ""}${candidate.name ? ` ${JSON.stringify(candidate.name)}` : ""}: ${candidate.reason}`),
|
|
2667
|
+
].join("\n");
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
function buildOverlayBlockerNextActions(options: { diagnostic: OverlayBlockerDiagnostic; sessionName?: string }): AgentBrowserNextAction[] {
|
|
2671
|
+
return [
|
|
2672
|
+
{
|
|
2673
|
+
id: "inspect-overlay-state",
|
|
2674
|
+
params: { args: sessionPrefixArgs(options.sessionName, ["snapshot", "-i"]) },
|
|
2675
|
+
reason: "Refresh interactive refs and inspect whether an overlay, banner, modal, or dialog is blocking the intended click.",
|
|
2676
|
+
safety: "Read-only inspection; use current refs from this snapshot before interacting.",
|
|
2677
|
+
tool: "agent_browser" as const,
|
|
2678
|
+
},
|
|
2679
|
+
...options.diagnostic.candidates.map((candidate, index) => ({
|
|
2680
|
+
id: `try-overlay-blocker-candidate-${index + 1}`,
|
|
2681
|
+
params: { args: sessionPrefixArgs(options.sessionName, candidate.args) },
|
|
2682
|
+
reason: candidate.reason,
|
|
2683
|
+
safety: "Only click this if the candidate is clearly a close/dismiss control for an overlay that blocks the intended workflow.",
|
|
2684
|
+
tool: "agent_browser" as const,
|
|
2685
|
+
})),
|
|
2686
|
+
];
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
function buildVisibleTextProbeScript(selector: string): string {
|
|
2690
|
+
return `(() => {\n const selector = ${JSON.stringify(selector)};\n const isVisible = (element) => {\n const style = window.getComputedStyle(element);\n if (!style || style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse' || Number(style.opacity) === 0) return false;\n return Array.from(element.getClientRects()).some((rect) => rect.width > 0 && rect.height > 0);\n };\n let matches = [];\n try {\n matches = Array.from(document.querySelectorAll(selector));\n } catch (error) {\n return JSON.stringify({ selector, error: error instanceof Error ? error.message : String(error) });\n }\n const visible = matches.filter(isVisible);\n const trim = (value) => typeof value === 'string' ? value.trim().replace(/\\s+/g, ' ').slice(0, 200) : undefined;\n return JSON.stringify({\n selector,\n matchCount: matches.length,\n visibleCount: visible.length,\n firstMatchVisible: matches[0] ? isVisible(matches[0]) : undefined,\n firstTextPreview: trim(matches[0]?.textContent),\n firstVisibleTextPreview: trim(visible[0]?.textContent),\n });\n})()`;
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
function parseSelectorTextVisibilityProbe(data: unknown, selector: string): Omit<SelectorTextVisibilityDiagnostic, "summary"> | undefined {
|
|
2694
|
+
const result = extractStringResultField(data, "result");
|
|
2695
|
+
if (!result) return undefined;
|
|
2696
|
+
let parsed: unknown;
|
|
2697
|
+
try {
|
|
2698
|
+
parsed = JSON.parse(result);
|
|
2699
|
+
} catch {
|
|
2700
|
+
return undefined;
|
|
2701
|
+
}
|
|
2702
|
+
if (!isRecord(parsed) || typeof parsed.error === "string") return undefined;
|
|
2703
|
+
const matchCount = typeof parsed.matchCount === "number" ? parsed.matchCount : undefined;
|
|
2704
|
+
const visibleCount = typeof parsed.visibleCount === "number" ? parsed.visibleCount : undefined;
|
|
2705
|
+
if (matchCount === undefined || visibleCount === undefined) return undefined;
|
|
2706
|
+
return {
|
|
2707
|
+
firstMatchVisible: typeof parsed.firstMatchVisible === "boolean" ? parsed.firstMatchVisible : undefined,
|
|
2708
|
+
firstVisibleTextPreview: typeof parsed.firstVisibleTextPreview === "string" && parsed.firstVisibleTextPreview.length > 0 ? redactSensitiveText(parsed.firstVisibleTextPreview) : undefined,
|
|
2709
|
+
matchCount,
|
|
2710
|
+
selector,
|
|
2711
|
+
visibleCount,
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
function selectorMayExposeSensitiveLiteral(selector: string): boolean {
|
|
2716
|
+
return redactSensitiveText(selector) !== selector || /\[[^\]]*[~|^$*]?=\s*(?:"[^"]*"|'[^']*'|[^\]\s]+)\s*(?:[is]\s*)?\]/.test(selector);
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
async function collectSelectorTextVisibilityDiagnosticForSelector(options: {
|
|
2720
|
+
cwd: string;
|
|
2721
|
+
selector: string | undefined;
|
|
2722
|
+
sessionName?: string;
|
|
2723
|
+
signal?: AbortSignal;
|
|
2724
|
+
}): Promise<SelectorTextVisibilityDiagnostic | undefined> {
|
|
2725
|
+
const { selector } = options;
|
|
2726
|
+
if (!selector || /^@e\d+$/.test(selector) || selectorMayExposeSensitiveLiteral(selector)) return undefined;
|
|
2727
|
+
const probe = await runSessionCommandData({
|
|
2728
|
+
args: ["eval", "--stdin"],
|
|
2729
|
+
cwd: options.cwd,
|
|
2730
|
+
sessionName: options.sessionName,
|
|
2731
|
+
signal: options.signal,
|
|
2732
|
+
stdin: buildVisibleTextProbeScript(selector),
|
|
2733
|
+
});
|
|
2734
|
+
const parsed = parseSelectorTextVisibilityProbe(probe, selector);
|
|
2735
|
+
if (!parsed || parsed.matchCount <= 1 && parsed.firstMatchVisible !== false) return undefined;
|
|
2736
|
+
if (parsed.visibleCount === 0) return undefined;
|
|
2737
|
+
const visibleMatchNoun = `visible match${parsed.visibleCount === 1 ? "" : "es"}`;
|
|
2738
|
+
const visibleMatchVerb = parsed.visibleCount === 1 ? "exists" : "exist";
|
|
2739
|
+
const summary = parsed.firstMatchVisible === false
|
|
2740
|
+
? `Selector ${JSON.stringify(selector)} matched ${parsed.matchCount} elements; the first match is hidden while ${parsed.visibleCount} ${visibleMatchNoun} ${visibleMatchVerb}.`
|
|
2741
|
+
: `Selector ${JSON.stringify(selector)} matched ${parsed.matchCount} elements; get text reads the first upstream match, which may not be the intended visible tab/panel.`;
|
|
2742
|
+
return { ...parsed, summary };
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
function getBatchGetTextSelectors(data: unknown): string[] {
|
|
2746
|
+
if (!Array.isArray(data)) return [];
|
|
2747
|
+
return data.flatMap((item) => {
|
|
2748
|
+
if (!isRecord(item) || item.success === false) return [];
|
|
2749
|
+
const [command, subcommand, selector] = extractBatchResultCommand(item);
|
|
2750
|
+
return command === "get" && subcommand === "text" && selector ? [selector] : [];
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
async function collectSelectorTextVisibilityDiagnostics(options: {
|
|
2755
|
+
commandInfo: CommandInfo;
|
|
2756
|
+
commandTokens: string[];
|
|
2757
|
+
cwd: string;
|
|
2758
|
+
data: unknown;
|
|
2759
|
+
sessionName?: string;
|
|
2760
|
+
signal?: AbortSignal;
|
|
2761
|
+
}): Promise<SelectorTextVisibilityDiagnostic[]> {
|
|
2762
|
+
const selectors = options.commandInfo.command === "get" && options.commandInfo.subcommand === "text"
|
|
2763
|
+
? [options.commandTokens[2]]
|
|
2764
|
+
: options.commandInfo.command === "batch"
|
|
2765
|
+
? getBatchGetTextSelectors(options.data)
|
|
2766
|
+
: [];
|
|
2767
|
+
const diagnostics: SelectorTextVisibilityDiagnostic[] = [];
|
|
2768
|
+
for (const selector of selectors) {
|
|
2769
|
+
const diagnostic = await collectSelectorTextVisibilityDiagnosticForSelector({
|
|
2770
|
+
cwd: options.cwd,
|
|
2771
|
+
selector,
|
|
2772
|
+
sessionName: options.sessionName,
|
|
2773
|
+
signal: options.signal,
|
|
2774
|
+
});
|
|
2775
|
+
if (diagnostic) diagnostics.push(diagnostic);
|
|
2776
|
+
}
|
|
2777
|
+
return diagnostics.sort((left, right) => Number(right.firstMatchVisible === false) - Number(left.firstMatchVisible === false));
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
function formatSelectorTextVisibilityText(diagnostics: SelectorTextVisibilityDiagnostic[]): string | undefined {
|
|
2781
|
+
if (diagnostics.length === 0) return undefined;
|
|
2782
|
+
return diagnostics.flatMap((diagnostic) => {
|
|
2783
|
+
const lines = [`Selector text visibility warning: ${diagnostic.summary}`];
|
|
2784
|
+
if (diagnostic.firstVisibleTextPreview) lines.push(`First visible text preview: ${JSON.stringify(diagnostic.firstVisibleTextPreview)}`);
|
|
2785
|
+
return lines;
|
|
2786
|
+
}).join("\n");
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
function looksLikeFunctionEvalStdin(stdin: string | undefined): boolean {
|
|
2790
|
+
const trimmed = stdin?.trim();
|
|
2791
|
+
if (!trimmed) return false;
|
|
2792
|
+
return /^(?:async\s+)?function\b/.test(trimmed) || /^(?:async\s*)?\([^)]*\)\s*=>/.test(trimmed) || /^(?:async\s+)?[A-Za-z_$][\w$]*\s*=>/.test(trimmed);
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
function isEmptyRecord(value: unknown): boolean {
|
|
2796
|
+
return isRecord(value) && Object.keys(value).length === 0;
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
function getEvalStdinHint(options: { command?: string; data: unknown; stdin?: string }): EvalStdinHint | undefined {
|
|
2800
|
+
if (options.command !== "eval" || !looksLikeFunctionEvalStdin(options.stdin) || !isRecord(options.data)) return undefined;
|
|
2801
|
+
const result = options.data.result;
|
|
2802
|
+
if (!isEmptyRecord(result)) return undefined;
|
|
2803
|
+
return {
|
|
2804
|
+
reason: "eval --stdin received a function-shaped snippet and the upstream JSON result was an empty object, which often means the function itself was returned or serialized instead of invoked.",
|
|
2805
|
+
suggestion: "Pass a plain expression such as `({ title: document.title })`, or invoke the function explicitly, for example `(() => ({ title: document.title }))()`.",
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
function formatEvalStdinHintText(hint: EvalStdinHint | undefined): string | undefined {
|
|
2810
|
+
return hint ? `Eval stdin hint: ${hint.reason} ${hint.suggestion}` : undefined;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
function getArtifactCleanupGuidance(options: { command?: string; manifest?: SessionArtifactManifest; succeeded: boolean }): ArtifactCleanupGuidance | undefined {
|
|
2814
|
+
if (!options.succeeded || options.command !== "close" || !options.manifest || options.manifest.entries.length === 0) return undefined;
|
|
2815
|
+
const explicitArtifactPaths = options.manifest.entries
|
|
2816
|
+
.filter((entry) => entry.storageScope === "explicit-path")
|
|
2817
|
+
.map((entry) => entry.path)
|
|
2818
|
+
.filter((path, index, paths) => paths.indexOf(path) === index)
|
|
2819
|
+
.slice(0, 10);
|
|
2820
|
+
return {
|
|
2821
|
+
explicitArtifactPaths,
|
|
2822
|
+
note: "Closing the browser session does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; clean those paths with host file tools when no longer needed.",
|
|
2823
|
+
owner: "host-file-tools",
|
|
2824
|
+
summary: formatSessionArtifactRetentionSummary(options.manifest),
|
|
2825
|
+
};
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
function formatArtifactCleanupGuidanceText(guidance: ArtifactCleanupGuidance | undefined): string | undefined {
|
|
2829
|
+
if (!guidance) return undefined;
|
|
2830
|
+
const lines = [
|
|
2831
|
+
"Artifact lifecycle:",
|
|
2832
|
+
`- ${guidance.summary}`,
|
|
2833
|
+
`- ${guidance.note}`,
|
|
2834
|
+
];
|
|
2835
|
+
if (guidance.explicitArtifactPaths.length > 0) {
|
|
2836
|
+
lines.push(`- Explicit artifact paths to review: ${guidance.explicitArtifactPaths.join(", ")}`);
|
|
2837
|
+
}
|
|
2838
|
+
return lines.join("\n");
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
function buildSelectorTextVisibilityNextActions(options: { diagnostics: SelectorTextVisibilityDiagnostic[]; sessionName?: string }): AgentBrowserNextAction[] {
|
|
2842
|
+
return options.diagnostics.map((diagnostic, index) => ({
|
|
2843
|
+
id: index === 0 ? "inspect-visible-text-candidates" : `inspect-visible-text-candidates-${index + 1}`,
|
|
2844
|
+
params: {
|
|
2845
|
+
args: sessionPrefixArgs(options.sessionName, ["eval", "--stdin"]),
|
|
2846
|
+
stdin: buildVisibleTextProbeScript(diagnostic.selector),
|
|
2847
|
+
},
|
|
2848
|
+
reason: "Inspect selector match count and visible text before trusting get text on tabbed or hidden DOM content.",
|
|
2849
|
+
safety: "Read-only DOM inspection; use a more specific visible selector or current @ref before acting on hidden-tab text.",
|
|
2850
|
+
tool: "agent_browser" as const,
|
|
2851
|
+
}));
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
function getTimeoutProgressSteps(compiledJob: CompiledAgentBrowserJob | undefined, command: string | undefined, stdin: string | undefined): Array<{ args: string[]; index: number }> {
|
|
2855
|
+
if (compiledJob) return compiledJob.steps.map((step, index) => ({ args: step.args, index: index + 1 }));
|
|
2856
|
+
if (command !== "batch" || !stdin) return [];
|
|
2857
|
+
try {
|
|
2858
|
+
const parsed = JSON.parse(stdin) as unknown;
|
|
2859
|
+
if (!Array.isArray(parsed)) return [];
|
|
2860
|
+
return parsed.flatMap((step, index) => Array.isArray(step) && step.every((token) => typeof token === "string") ? [{ args: step as string[], index: index + 1 }] : []);
|
|
2861
|
+
} catch {
|
|
2862
|
+
return [];
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
function getLastPositionalToken(args: string[], startIndex = 1): string | undefined {
|
|
2867
|
+
for (let index = args.length - 1; index >= startIndex; index -= 1) {
|
|
2868
|
+
const token = args[index];
|
|
2869
|
+
if (token && !token.startsWith("-")) return token;
|
|
2870
|
+
}
|
|
2871
|
+
return undefined;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
function getTimeoutStepArtifactPath(args: string[]): string | undefined {
|
|
2875
|
+
const [command] = args;
|
|
2876
|
+
if (command === "screenshot") {
|
|
2877
|
+
const index = getScreenshotPathTokenIndex(args);
|
|
2878
|
+
return index === undefined ? undefined : args[index];
|
|
2879
|
+
}
|
|
2880
|
+
if (command === "pdf") return getLastPositionalToken(args);
|
|
2881
|
+
if (command === "download") return getLastPositionalToken(args, 2);
|
|
2882
|
+
if (command === "wait") {
|
|
2883
|
+
const inlineDownload = args.find((token) => token.startsWith("--download="));
|
|
2884
|
+
if (inlineDownload) return inlineDownload.slice("--download=".length) || undefined;
|
|
2885
|
+
const downloadIndex = args.indexOf("--download");
|
|
2886
|
+
const downloadPath = downloadIndex >= 0 ? args[downloadIndex + 1] : undefined;
|
|
2887
|
+
if (downloadPath && !downloadPath.startsWith("-")) return downloadPath;
|
|
2888
|
+
}
|
|
2889
|
+
return undefined;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
async function collectTimeoutArtifactEvidence(cwd: string, steps: Array<{ args: string[]; index: number }>): Promise<TimeoutArtifactEvidence[]> {
|
|
2893
|
+
const evidence: TimeoutArtifactEvidence[] = [];
|
|
2894
|
+
for (const step of steps) {
|
|
2895
|
+
const path = getTimeoutStepArtifactPath(step.args);
|
|
2896
|
+
if (!path) continue;
|
|
2897
|
+
const absolutePath = isAbsolute(path) ? path : resolve(cwd, path);
|
|
2898
|
+
try {
|
|
2899
|
+
const stats = await stat(absolutePath);
|
|
2900
|
+
evidence.push({ absolutePath, exists: true, path, sizeBytes: stats.size, stepIndex: step.index });
|
|
2901
|
+
} catch {
|
|
2902
|
+
evidence.push({ absolutePath, exists: false, path, stepIndex: step.index });
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
return evidence;
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
function getPlannedCurrentPageUrl(steps: Array<{ args: string[]; index: number }>): string | undefined {
|
|
2909
|
+
for (let index = steps.length - 1; index >= 0; index -= 1) {
|
|
2910
|
+
const args = steps[index]?.args ?? [];
|
|
2911
|
+
if (args[0] === "open" || args[0] === "navigate" || args[0] === "pushstate") {
|
|
2912
|
+
return getLastPositionalToken(args);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
return undefined;
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
async function collectTimeoutPartialProgress(options: {
|
|
2919
|
+
command?: string;
|
|
2920
|
+
compiledJob?: CompiledAgentBrowserJob;
|
|
2921
|
+
cwd: string;
|
|
2922
|
+
sessionName?: string;
|
|
2923
|
+
stdin?: string;
|
|
2924
|
+
}): Promise<TimeoutPartialProgress | undefined> {
|
|
2925
|
+
const steps = getTimeoutProgressSteps(options.compiledJob, options.command, options.stdin);
|
|
2926
|
+
const artifacts = await collectTimeoutArtifactEvidence(options.cwd, steps);
|
|
2927
|
+
const [urlData, titleData] = await Promise.all([
|
|
2928
|
+
runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, sessionName: options.sessionName }),
|
|
2929
|
+
runSessionCommandData({ args: ["get", "title"], cwd: options.cwd, sessionName: options.sessionName }),
|
|
2930
|
+
]);
|
|
2931
|
+
const recoveredUrl = extractStringResultField(urlData, "result") ?? extractStringResultField(urlData, "url");
|
|
2932
|
+
const title = extractStringResultField(titleData, "result") ?? extractStringResultField(titleData, "title");
|
|
2933
|
+
const plannedUrl = recoveredUrl ? undefined : getPlannedCurrentPageUrl(steps);
|
|
2934
|
+
const url = recoveredUrl ?? plannedUrl;
|
|
2935
|
+
if (steps.length === 0 && artifacts.length === 0 && !url && !title) return undefined;
|
|
2936
|
+
const foundArtifacts = artifacts.filter((artifact) => artifact.exists).length;
|
|
2937
|
+
const pageStateSummary = recoveredUrl || title ? " and current page state" : plannedUrl ? " and planned page URL" : "";
|
|
2938
|
+
return {
|
|
2939
|
+
artifacts,
|
|
2940
|
+
currentPage: url || title ? { title, url } : undefined,
|
|
2941
|
+
steps: steps.length > 0 ? steps : undefined,
|
|
2942
|
+
summary: `Timed out before upstream returned final results; recovered ${foundArtifacts}/${artifacts.length} declared artifact path${artifacts.length === 1 ? "" : "s"}${pageStateSummary}.`,
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
function redactSensitivePathSegmentsForDiagnostic(path: string): string {
|
|
2947
|
+
return path.split(/([/\\]+)/).map((segment) => {
|
|
2948
|
+
if (segment === "/" || segment === "\\" || /^[/\\]+$/.test(segment)) return segment;
|
|
2949
|
+
return redactSensitiveText(segment) !== segment || /(?:secret|token|password|passwd|credential|auth|api[-_]?key|bearer)/i.test(segment) ? "[REDACTED]" : segment;
|
|
2950
|
+
}).join("");
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
function sanitizeCurrentPageUrlForTimeoutDiagnostic(url: string): string {
|
|
2954
|
+
try {
|
|
2955
|
+
const parsedUrl = new URL(url);
|
|
2956
|
+
parsedUrl.pathname = parsedUrl.pathname.split("/").map((segment) => redactSensitivePathSegmentsForDiagnostic(segment)).join("/");
|
|
2957
|
+
for (const [key, value] of parsedUrl.searchParams.entries()) {
|
|
2958
|
+
if (redactSensitiveText(key) !== key || redactSensitiveText(value) !== value || /(?:secret|token|password|passwd|credential|auth|api[-_]?key|bearer)/i.test(`${key} ${value}`)) {
|
|
2959
|
+
parsedUrl.searchParams.set(key, "[REDACTED]");
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
if (parsedUrl.hash) {
|
|
2963
|
+
parsedUrl.hash = redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(parsedUrl.hash));
|
|
2964
|
+
}
|
|
2965
|
+
return redactSensitiveText(parsedUrl.toString());
|
|
2966
|
+
} catch {
|
|
2967
|
+
return redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(url));
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
function formatTimeoutPartialProgressText(progress: TimeoutPartialProgress): string {
|
|
2972
|
+
const lines = [`Timeout partial progress: ${progress.summary}`];
|
|
2973
|
+
const currentPageTitle = progress.currentPage?.title ? redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(progress.currentPage.title)) : undefined;
|
|
2974
|
+
const currentPageUrl = progress.currentPage?.url ? sanitizeCurrentPageUrlForTimeoutDiagnostic(progress.currentPage.url) : undefined;
|
|
2975
|
+
if (currentPageTitle || currentPageUrl) {
|
|
2976
|
+
lines.push(`Current page: ${[currentPageTitle, currentPageUrl].filter(Boolean).join(" — ")}`);
|
|
2977
|
+
}
|
|
2978
|
+
if (progress.steps && progress.steps.length > 0) {
|
|
2979
|
+
const shownSteps = progress.steps.slice(0, 6);
|
|
2980
|
+
lines.push("Planned steps:");
|
|
2981
|
+
for (const step of shownSteps) {
|
|
2982
|
+
const command = redactSensitivePathSegmentsForDiagnostic(redactInvocationArgs(step.args).join(" "));
|
|
2983
|
+
lines.push(`- Step ${step.index}: ${command}`);
|
|
2984
|
+
}
|
|
2985
|
+
if (progress.steps.length > shownSteps.length) {
|
|
2986
|
+
lines.push(`- ... ${progress.steps.length - shownSteps.length} more step${progress.steps.length - shownSteps.length === 1 ? "" : "s"} omitted`);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
for (const artifact of progress.artifacts) {
|
|
2990
|
+
const path = redactSensitivePathSegmentsForDiagnostic(artifact.path);
|
|
2991
|
+
lines.push(`Artifact from step ${artifact.stepIndex}: ${path} (${artifact.exists ? `exists${typeof artifact.sizeBytes === "number" ? `, ${artifact.sizeBytes} bytes` : ""}` : "missing"})`);
|
|
2992
|
+
}
|
|
2993
|
+
return lines.join("\n");
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
async function collectOverlayBlockerDiagnostic(options: {
|
|
2997
|
+
command?: string;
|
|
2998
|
+
cwd: string;
|
|
2999
|
+
data: unknown;
|
|
3000
|
+
navigationSummary?: NavigationSummary;
|
|
3001
|
+
priorTarget?: SessionTabTarget;
|
|
3002
|
+
sessionName?: string;
|
|
3003
|
+
signal?: AbortSignal;
|
|
3004
|
+
}): Promise<OverlayBlockerDiagnostic | undefined> {
|
|
3005
|
+
if (options.command !== "click" || !isRecord(options.data) || typeof options.data.clicked !== "string") return undefined;
|
|
3006
|
+
const priorUrl = normalizeComparableUrl(options.priorTarget?.url);
|
|
3007
|
+
const currentUrl = normalizeComparableUrl(options.navigationSummary?.url);
|
|
3008
|
+
if (!priorUrl || !currentUrl || priorUrl !== currentUrl) return undefined;
|
|
3009
|
+
const snapshotData = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
3010
|
+
const candidates = getOverlayBlockerCandidates(snapshotData);
|
|
3011
|
+
const snapshot = extractRefSnapshotFromData(snapshotData);
|
|
3012
|
+
if (candidates.length === 0 || !snapshot) return undefined;
|
|
3013
|
+
return {
|
|
3014
|
+
candidates,
|
|
3015
|
+
snapshot,
|
|
3016
|
+
summary: `Click completed but the page stayed on ${currentUrl}; a fresh snapshot contains likely overlay close/dismiss controls.`,
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
|
|
1427
3020
|
async function collectOpenResultTabCorrection(options: {
|
|
1428
3021
|
cwd: string;
|
|
1429
3022
|
sessionName?: string;
|
|
@@ -1489,6 +3082,68 @@ function buildSessionDetailFields(sessionName: string | undefined, usedImplicitS
|
|
|
1489
3082
|
return sessionName ? { sessionName, usedImplicitSession } : {};
|
|
1490
3083
|
}
|
|
1491
3084
|
|
|
3085
|
+
function buildManagedSessionOutcome(options: {
|
|
3086
|
+
activeAfter: boolean;
|
|
3087
|
+
activeBefore: boolean;
|
|
3088
|
+
attemptedSessionName?: string;
|
|
3089
|
+
command?: string;
|
|
3090
|
+
currentSessionName: string;
|
|
3091
|
+
previousSessionName: string;
|
|
3092
|
+
replacedSessionName?: string;
|
|
3093
|
+
sessionMode: "auto" | "fresh";
|
|
3094
|
+
succeeded: boolean;
|
|
3095
|
+
}): ManagedSessionOutcome | undefined {
|
|
3096
|
+
const { activeAfter, activeBefore, attemptedSessionName, command, currentSessionName, previousSessionName, replacedSessionName, sessionMode, succeeded } = options;
|
|
3097
|
+
if (!attemptedSessionName) return undefined;
|
|
3098
|
+
let status: ManagedSessionOutcome["status"];
|
|
3099
|
+
let summary: string;
|
|
3100
|
+
if (command === "close") {
|
|
3101
|
+
status = succeeded ? "closed" : activeBefore ? "preserved" : "abandoned";
|
|
3102
|
+
summary = succeeded
|
|
3103
|
+
? `Managed session ${attemptedSessionName} was closed.`
|
|
3104
|
+
: activeBefore
|
|
3105
|
+
? `Managed session close failed; previous managed session ${previousSessionName} remains current.`
|
|
3106
|
+
: `Managed session close failed; no managed session is active.`;
|
|
3107
|
+
} else if (succeeded) {
|
|
3108
|
+
if (replacedSessionName) {
|
|
3109
|
+
status = "replaced";
|
|
3110
|
+
summary = `Managed session ${replacedSessionName} was replaced by ${currentSessionName}.`;
|
|
3111
|
+
} else if (!activeBefore && activeAfter) {
|
|
3112
|
+
status = "created";
|
|
3113
|
+
summary = `Managed session ${currentSessionName} is now current.`;
|
|
3114
|
+
} else {
|
|
3115
|
+
status = "unchanged";
|
|
3116
|
+
summary = `Managed session ${currentSessionName} remains current.`;
|
|
3117
|
+
}
|
|
3118
|
+
} else if (activeBefore) {
|
|
3119
|
+
status = "preserved";
|
|
3120
|
+
summary = sessionMode === "fresh" && attemptedSessionName !== previousSessionName
|
|
3121
|
+
? `Fresh managed session ${attemptedSessionName} failed before becoming current; previous managed session ${previousSessionName} was preserved.`
|
|
3122
|
+
: `Managed session call failed; previous managed session ${previousSessionName} was preserved.`;
|
|
3123
|
+
} else {
|
|
3124
|
+
status = "abandoned";
|
|
3125
|
+
summary = sessionMode === "fresh"
|
|
3126
|
+
? `Fresh managed session ${attemptedSessionName} failed before becoming current; no previous managed session was active, so no managed session is current.`
|
|
3127
|
+
: `Managed session call failed before any managed session became current.`;
|
|
3128
|
+
}
|
|
3129
|
+
return {
|
|
3130
|
+
activeAfter,
|
|
3131
|
+
activeBefore,
|
|
3132
|
+
attemptedSessionName,
|
|
3133
|
+
currentSessionName,
|
|
3134
|
+
previousSessionName,
|
|
3135
|
+
replacedSessionName,
|
|
3136
|
+
sessionMode,
|
|
3137
|
+
status,
|
|
3138
|
+
succeeded,
|
|
3139
|
+
summary,
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
function formatManagedSessionOutcomeText(outcome: ManagedSessionOutcome | undefined): string | undefined {
|
|
3144
|
+
return outcome && !outcome.succeeded && outcome.sessionMode === "fresh" ? `Managed session outcome: ${outcome.summary}` : undefined;
|
|
3145
|
+
}
|
|
3146
|
+
|
|
1492
3147
|
function getPersistentSessionArtifactStore(ctx: {
|
|
1493
3148
|
sessionManager: {
|
|
1494
3149
|
getSessionDir?: () => string;
|
|
@@ -1640,6 +3295,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1640
3295
|
let managedSessionCwd = process.cwd();
|
|
1641
3296
|
let freshSessionOrdinal = 0;
|
|
1642
3297
|
let sessionTabTargets = new Map<string, OrderedSessionTabTarget>();
|
|
3298
|
+
let sessionRefSnapshots = new Map<string, OrderedSessionRefSnapshot>();
|
|
1643
3299
|
let sessionTabTargetUpdateOrder = 0;
|
|
1644
3300
|
let traceOwners = new Map<string, TraceOwner>();
|
|
1645
3301
|
let artifactManifest: SessionArtifactManifest | undefined;
|
|
@@ -1653,7 +3309,8 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1653
3309
|
managedSessionCwd = ctx.cwd;
|
|
1654
3310
|
freshSessionOrdinal = restoredState.freshSessionOrdinal;
|
|
1655
3311
|
sessionTabTargets = restoreSessionTabTargetsFromBranch(ctx.sessionManager.getBranch());
|
|
1656
|
-
|
|
3312
|
+
sessionRefSnapshots = restoreSessionRefSnapshotsFromBranch(ctx.sessionManager.getBranch());
|
|
3313
|
+
sessionTabTargetUpdateOrder = Math.max(getLatestSessionTabTargetOrder(sessionTabTargets), getLatestSessionTabTargetOrder(sessionRefSnapshots));
|
|
1657
3314
|
artifactManifest = restoreArtifactManifestFromBranch(ctx.sessionManager.getBranch());
|
|
1658
3315
|
});
|
|
1659
3316
|
|
|
@@ -1670,6 +3327,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1670
3327
|
}
|
|
1671
3328
|
managedSessionActive = false;
|
|
1672
3329
|
sessionTabTargets = new Map<string, OrderedSessionTabTarget>();
|
|
3330
|
+
sessionRefSnapshots = new Map<string, OrderedSessionRefSnapshot>();
|
|
1673
3331
|
sessionTabTargetUpdateOrder = 0;
|
|
1674
3332
|
traceOwners = new Map<string, TraceOwner>();
|
|
1675
3333
|
artifactManifest = undefined;
|
|
@@ -1723,17 +3381,77 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1723
3381
|
return component;
|
|
1724
3382
|
},
|
|
1725
3383
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
1726
|
-
const
|
|
1727
|
-
const
|
|
3384
|
+
const semanticActionResult = params.semanticAction === undefined ? {} : compileAgentBrowserSemanticAction(params.semanticAction);
|
|
3385
|
+
const jobResult = params.job === undefined ? {} : compileAgentBrowserJob(params.job);
|
|
3386
|
+
const qaResult = params.qa === undefined ? {} : compileAgentBrowserQaPreset(params.qa);
|
|
3387
|
+
const sourceLookupResult = params.sourceLookup === undefined ? {} : compileAgentBrowserSourceLookup(params.sourceLookup);
|
|
3388
|
+
const networkSourceLookupResult = params.networkSourceLookup === undefined ? {} : compileAgentBrowserNetworkSourceLookup(params.networkSourceLookup);
|
|
3389
|
+
const hasExplicitArgs = Array.isArray(params.args);
|
|
3390
|
+
const explicitInputModes = [hasExplicitArgs, Boolean(semanticActionResult.compiled), Boolean(jobResult.compiled), Boolean(qaResult.compiled), Boolean(sourceLookupResult.compiled), Boolean(networkSourceLookupResult.compiled)].filter(Boolean).length;
|
|
3391
|
+
const semanticActionError = semanticActionResult.error;
|
|
3392
|
+
const jobError = jobResult.error;
|
|
3393
|
+
const qaError = qaResult.error;
|
|
3394
|
+
const sourceLookupError = sourceLookupResult.error;
|
|
3395
|
+
const networkSourceLookupError = networkSourceLookupResult.error;
|
|
3396
|
+
const inputModeError = explicitInputModes !== 1
|
|
3397
|
+
? "Provide exactly one of args, semanticAction, job, qa, sourceLookup, or networkSourceLookup."
|
|
3398
|
+
: undefined;
|
|
3399
|
+
const compiledSemanticAction = semanticActionResult.compiled;
|
|
3400
|
+
const compiledQaPreset = qaResult.compiled;
|
|
3401
|
+
const compiledSourceLookup = sourceLookupResult.compiled;
|
|
3402
|
+
const compiledNetworkSourceLookup = networkSourceLookupResult.compiled;
|
|
3403
|
+
const compiledJob = jobResult.compiled ?? compiledQaPreset;
|
|
3404
|
+
const compiledGeneratedBatch = compiledNetworkSourceLookup ?? compiledSourceLookup ?? compiledJob;
|
|
3405
|
+
const toolArgs = compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? params.args ?? [];
|
|
3406
|
+
const toolStdin = compiledGeneratedBatch?.stdin ?? params.stdin;
|
|
3407
|
+
const redactedArgs = redactInvocationArgs(toolArgs);
|
|
3408
|
+
const generatedStdinError = compiledGeneratedBatch && params.stdin !== undefined ? "Do not provide stdin with job, qa, sourceLookup, or networkSourceLookup; those modes generate their own batch stdin." : undefined;
|
|
3409
|
+
const validationError = semanticActionError ?? jobError ?? qaError ?? sourceLookupError ?? networkSourceLookupError ?? inputModeError ?? generatedStdinError ?? validateToolArgs(toolArgs) ?? getBatchAnnotateValidationError(toolArgs, toolStdin);
|
|
3410
|
+
const redactedCompiledSemanticAction = compiledSemanticAction
|
|
3411
|
+
? { ...compiledSemanticAction, args: redactInvocationArgs(compiledSemanticAction.args) }
|
|
3412
|
+
: undefined;
|
|
3413
|
+
const redactedCompiledJobSteps = compiledJob?.steps.map((step) => ({ ...step, args: redactInvocationArgs(step.args) }));
|
|
3414
|
+
const redactedCompiledJob = compiledJob && redactedCompiledJobSteps
|
|
3415
|
+
? { ...compiledJob, stdin: JSON.stringify(redactedCompiledJobSteps.map((step) => step.args)), steps: redactedCompiledJobSteps }
|
|
3416
|
+
: undefined;
|
|
3417
|
+
const redactedCompiledQaPreset = compiledQaPreset && redactedCompiledJob
|
|
3418
|
+
? { ...redactedCompiledJob, checks: compiledQaPreset.checks }
|
|
3419
|
+
: undefined;
|
|
3420
|
+
const redactedCompiledSourceLookupSteps = compiledSourceLookup?.steps.map((step) => ({ ...step, args: redactInvocationArgs(step.args) }));
|
|
3421
|
+
const redactedCompiledSourceLookup = compiledSourceLookup && redactedCompiledSourceLookupSteps
|
|
3422
|
+
? { ...compiledSourceLookup, stdin: JSON.stringify(redactedCompiledSourceLookupSteps.map((step) => step.args)), steps: redactedCompiledSourceLookupSteps }
|
|
3423
|
+
: undefined;
|
|
3424
|
+
const redactedCompiledNetworkSourceLookupSteps = compiledNetworkSourceLookup?.steps.map((step) => ({ ...step, args: redactNetworkSourceLookupArgs(step.args) }));
|
|
3425
|
+
const redactedCompiledNetworkSourceLookup = compiledNetworkSourceLookup && redactedCompiledNetworkSourceLookupSteps
|
|
3426
|
+
? {
|
|
3427
|
+
...compiledNetworkSourceLookup,
|
|
3428
|
+
query: {
|
|
3429
|
+
...compiledNetworkSourceLookup.query,
|
|
3430
|
+
filter: redactNetworkSourceLookupUrl(compiledNetworkSourceLookup.query.filter),
|
|
3431
|
+
url: redactNetworkSourceLookupUrl(compiledNetworkSourceLookup.query.url),
|
|
3432
|
+
},
|
|
3433
|
+
stdin: JSON.stringify(redactedCompiledNetworkSourceLookupSteps.map((step) => step.args)),
|
|
3434
|
+
steps: redactedCompiledNetworkSourceLookupSteps,
|
|
3435
|
+
}
|
|
3436
|
+
: undefined;
|
|
1728
3437
|
if (validationError) {
|
|
1729
3438
|
return {
|
|
1730
3439
|
content: [{ type: "text", text: validationError }],
|
|
1731
|
-
details: {
|
|
3440
|
+
details: {
|
|
3441
|
+
args: redactedArgs,
|
|
3442
|
+
compiledJob: redactedCompiledJob,
|
|
3443
|
+
compiledQaPreset: redactedCompiledQaPreset,
|
|
3444
|
+
compiledSourceLookup: redactedCompiledSourceLookup,
|
|
3445
|
+
compiledNetworkSourceLookup: redactedCompiledNetworkSourceLookup,
|
|
3446
|
+
compiledSemanticAction: redactedCompiledSemanticAction,
|
|
3447
|
+
...buildAgentBrowserResultCategoryDetails({ args: redactedArgs, errorText: validationError, succeeded: false, validationError }),
|
|
3448
|
+
validationError,
|
|
3449
|
+
},
|
|
1732
3450
|
isError: true,
|
|
1733
3451
|
};
|
|
1734
3452
|
}
|
|
1735
|
-
const preparedArgs = await prepareAgentBrowserArgs(
|
|
1736
|
-
const userRequestedJson =
|
|
3453
|
+
const preparedArgs = await prepareAgentBrowserArgs(toolArgs, toolStdin, ctx.cwd);
|
|
3454
|
+
const userRequestedJson = toolArgs.includes("--json");
|
|
1737
3455
|
|
|
1738
3456
|
const tabTargetUpdateOrder = ++sessionTabTargetUpdateOrder;
|
|
1739
3457
|
const runTool = async (): Promise<AgentBrowserToolResult> => {
|
|
@@ -1757,10 +3475,15 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1757
3475
|
content: [{ type: "text", text: executionPlan.validationError }],
|
|
1758
3476
|
details: {
|
|
1759
3477
|
args: redactedArgs,
|
|
3478
|
+
compiledJob: redactedCompiledJob,
|
|
3479
|
+
compiledQaPreset: redactedCompiledQaPreset,
|
|
3480
|
+
compiledSourceLookup: redactedCompiledSourceLookup,
|
|
3481
|
+
compiledNetworkSourceLookup: redactedCompiledNetworkSourceLookup,
|
|
1760
3482
|
invalidValueFlag: executionPlan.invalidValueFlag,
|
|
1761
3483
|
sessionMode,
|
|
1762
3484
|
sessionRecoveryHint: redactedRecoveryHint,
|
|
1763
3485
|
startupScopedFlags: executionPlan.startupScopedFlags,
|
|
3486
|
+
...buildAgentBrowserResultCategoryDetails({ args: redactedArgs, command: executionPlan.commandInfo.command, errorText: executionPlan.validationError, succeeded: false, validationError: executionPlan.validationError }),
|
|
1764
3487
|
validationError: executionPlan.validationError,
|
|
1765
3488
|
},
|
|
1766
3489
|
isError: true,
|
|
@@ -1771,7 +3494,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1771
3494
|
const exactSensitiveValues = getExactSensitiveStdinValues({
|
|
1772
3495
|
command: executionPlan.commandInfo.command,
|
|
1773
3496
|
commandTokens,
|
|
1774
|
-
stdin:
|
|
3497
|
+
stdin: toolStdin,
|
|
1775
3498
|
});
|
|
1776
3499
|
const traceOwnerGuardMessage = getTraceOwnerGuardMessage({
|
|
1777
3500
|
command: executionPlan.commandInfo.command,
|
|
@@ -1788,6 +3511,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1788
3511
|
compatibilityWorkaround,
|
|
1789
3512
|
effectiveArgs: redactedEffectiveArgs,
|
|
1790
3513
|
sessionMode,
|
|
3514
|
+
...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: traceOwnerGuardMessage, succeeded: false, validationError: traceOwnerGuardMessage }),
|
|
1791
3515
|
validationError: traceOwnerGuardMessage,
|
|
1792
3516
|
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
1793
3517
|
},
|
|
@@ -1797,7 +3521,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1797
3521
|
const stdinValidationError = validateStdinCommandContract({
|
|
1798
3522
|
command: executionPlan.commandInfo.command,
|
|
1799
3523
|
commandTokens,
|
|
1800
|
-
stdin:
|
|
3524
|
+
stdin: toolStdin,
|
|
1801
3525
|
});
|
|
1802
3526
|
if (stdinValidationError) {
|
|
1803
3527
|
return {
|
|
@@ -1808,13 +3532,14 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1808
3532
|
compatibilityWorkaround,
|
|
1809
3533
|
effectiveArgs: redactedEffectiveArgs,
|
|
1810
3534
|
sessionMode,
|
|
3535
|
+
...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: stdinValidationError, succeeded: false, validationError: stdinValidationError }),
|
|
1811
3536
|
validationError: stdinValidationError,
|
|
1812
3537
|
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
1813
3538
|
},
|
|
1814
3539
|
isError: true,
|
|
1815
3540
|
};
|
|
1816
3541
|
}
|
|
1817
|
-
const waitIpcTimeoutError = validateWaitIpcTimeoutContract(commandTokens,
|
|
3542
|
+
const waitIpcTimeoutError = validateWaitIpcTimeoutContract(commandTokens, toolStdin);
|
|
1818
3543
|
if (waitIpcTimeoutError) {
|
|
1819
3544
|
return {
|
|
1820
3545
|
content: [{ type: "text", text: waitIpcTimeoutError }],
|
|
@@ -1824,6 +3549,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1824
3549
|
compatibilityWorkaround,
|
|
1825
3550
|
effectiveArgs: redactedEffectiveArgs,
|
|
1826
3551
|
sessionMode,
|
|
3552
|
+
...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: waitIpcTimeoutError, succeeded: false, timedOut: true, validationError: waitIpcTimeoutError }),
|
|
1827
3553
|
validationError: waitIpcTimeoutError,
|
|
1828
3554
|
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
1829
3555
|
},
|
|
@@ -1833,18 +3559,43 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1833
3559
|
|
|
1834
3560
|
const priorSessionTabTargetState = executionPlan.sessionName ? sessionTabTargets.get(executionPlan.sessionName) : undefined;
|
|
1835
3561
|
const priorSessionTabTarget = priorSessionTabTargetState?.target;
|
|
3562
|
+
const priorRefSnapshotState = executionPlan.sessionName ? sessionRefSnapshots.get(executionPlan.sessionName) : undefined;
|
|
3563
|
+
const staleRefPreflight = buildStaleRefPreflight({
|
|
3564
|
+
commandTokens,
|
|
3565
|
+
currentTarget: priorSessionTabTarget,
|
|
3566
|
+
refSnapshot: priorRefSnapshotState,
|
|
3567
|
+
stdin: toolStdin,
|
|
3568
|
+
});
|
|
3569
|
+
if (staleRefPreflight) {
|
|
3570
|
+
return {
|
|
3571
|
+
content: [{ type: "text", text: staleRefPreflight.message }],
|
|
3572
|
+
details: {
|
|
3573
|
+
args: redactedArgs,
|
|
3574
|
+
command: executionPlan.commandInfo.command,
|
|
3575
|
+
compatibilityWorkaround,
|
|
3576
|
+
effectiveArgs: redactedEffectiveArgs,
|
|
3577
|
+
nextActions: sessionAwareStaleRefNextActions(executionPlan.sessionName),
|
|
3578
|
+
refIds: staleRefPreflight.refIds,
|
|
3579
|
+
refSnapshot: staleRefPreflight.snapshot,
|
|
3580
|
+
sessionMode,
|
|
3581
|
+
...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: staleRefPreflight.message, failureCategory: "stale-ref", succeeded: false }),
|
|
3582
|
+
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
3583
|
+
},
|
|
3584
|
+
isError: true,
|
|
3585
|
+
};
|
|
3586
|
+
}
|
|
1836
3587
|
let pinnedBatchUnwrapMode: PinnedBatchUnwrapMode | undefined;
|
|
1837
3588
|
let includePinnedNavigationSummary = false;
|
|
1838
3589
|
let sessionTabCorrection: OpenResultTabCorrection | undefined;
|
|
1839
3590
|
let processArgs = executionPlan.effectiveArgs;
|
|
1840
|
-
let processStdin = preparedArgs.stdin ??
|
|
3591
|
+
let processStdin = preparedArgs.stdin ?? toolStdin;
|
|
1841
3592
|
if (
|
|
1842
3593
|
priorSessionTabTarget &&
|
|
1843
3594
|
shouldPinSessionTabForCommand({
|
|
1844
3595
|
command: executionPlan.commandInfo.command,
|
|
1845
3596
|
commandTokens,
|
|
1846
3597
|
sessionName: executionPlan.sessionName,
|
|
1847
|
-
stdin:
|
|
3598
|
+
stdin: toolStdin,
|
|
1848
3599
|
})
|
|
1849
3600
|
) {
|
|
1850
3601
|
const plannedSessionTabSelection = await collectSessionTabSelection({
|
|
@@ -1854,7 +3605,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1854
3605
|
target: priorSessionTabTarget,
|
|
1855
3606
|
});
|
|
1856
3607
|
if (plannedSessionTabSelection && executionPlan.sessionName) {
|
|
1857
|
-
if (executionPlan.commandInfo.command === "eval" &&
|
|
3608
|
+
if (executionPlan.commandInfo.command === "eval" && toolStdin !== undefined) {
|
|
1858
3609
|
const appliedSessionTabSelection = await applyOpenResultTabCorrection({
|
|
1859
3610
|
correction: plannedSessionTabSelection,
|
|
1860
3611
|
cwd: ctx.cwd,
|
|
@@ -1872,6 +3623,8 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1872
3623
|
effectiveArgs: redactedEffectiveArgs,
|
|
1873
3624
|
sessionMode,
|
|
1874
3625
|
sessionTabCorrection: plannedSessionTabSelection,
|
|
3626
|
+
...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: error, failureCategory: "tab-drift", succeeded: false, tabDrift: true, validationError: error }),
|
|
3627
|
+
nextActions: buildAgentBrowserNextActions({ failureCategory: "tab-drift", resultCategory: "failure" }),
|
|
1875
3628
|
validationError: error,
|
|
1876
3629
|
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
1877
3630
|
},
|
|
@@ -1884,7 +3637,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1884
3637
|
command: executionPlan.commandInfo.command,
|
|
1885
3638
|
commandTokens,
|
|
1886
3639
|
selectedTab: plannedSessionTabSelection.selectedTab,
|
|
1887
|
-
stdin:
|
|
3640
|
+
stdin: toolStdin,
|
|
1888
3641
|
});
|
|
1889
3642
|
if (pinnedBatchPlan && "error" in pinnedBatchPlan) {
|
|
1890
3643
|
return {
|
|
@@ -1896,6 +3649,8 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1896
3649
|
effectiveArgs: redactedEffectiveArgs,
|
|
1897
3650
|
sessionMode,
|
|
1898
3651
|
sessionTabCorrection: plannedSessionTabSelection,
|
|
3652
|
+
...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: pinnedBatchPlan.error, failureCategory: "tab-drift", succeeded: false, tabDrift: true, validationError: pinnedBatchPlan.error }),
|
|
3653
|
+
nextActions: buildAgentBrowserNextActions({ failureCategory: "tab-drift", resultCategory: "failure" }),
|
|
1899
3654
|
validationError: pinnedBatchPlan.error,
|
|
1900
3655
|
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
1901
3656
|
},
|
|
@@ -1935,14 +3690,27 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1935
3690
|
|
|
1936
3691
|
if (processResult.spawnError?.message.includes("ENOENT")) {
|
|
1937
3692
|
const errorText = buildMissingBinaryMessage();
|
|
3693
|
+
const managedSessionOutcome = buildManagedSessionOutcome({
|
|
3694
|
+
activeAfter: managedSessionActive,
|
|
3695
|
+
activeBefore: managedSessionActive,
|
|
3696
|
+
attemptedSessionName: executionPlan.managedSessionName,
|
|
3697
|
+
command: executionPlan.commandInfo.command,
|
|
3698
|
+
currentSessionName: managedSessionName,
|
|
3699
|
+
previousSessionName: managedSessionName,
|
|
3700
|
+
sessionMode,
|
|
3701
|
+
succeeded: false,
|
|
3702
|
+
});
|
|
3703
|
+
const managedSessionOutcomeText = formatManagedSessionOutcomeText(managedSessionOutcome);
|
|
1938
3704
|
return {
|
|
1939
|
-
content: [{ type: "text", text: errorText }],
|
|
3705
|
+
content: [{ type: "text", text: managedSessionOutcomeText ? `${errorText}\n\n${managedSessionOutcomeText}` : errorText }],
|
|
1940
3706
|
details: {
|
|
1941
3707
|
args: redactedArgs,
|
|
1942
3708
|
compatibilityWorkaround,
|
|
1943
3709
|
effectiveArgs: redactedProcessArgs,
|
|
3710
|
+
managedSessionOutcome,
|
|
1944
3711
|
sessionMode,
|
|
1945
3712
|
sessionTabCorrection,
|
|
3713
|
+
...buildAgentBrowserResultCategoryDetails({ args: redactedProcessArgs, command: executionPlan.commandInfo.command, errorText, failureCategory: "missing-binary", spawnError: processResult.spawnError.message, succeeded: false }),
|
|
1946
3714
|
spawnError: processResult.spawnError.message,
|
|
1947
3715
|
},
|
|
1948
3716
|
isError: true,
|
|
@@ -1997,7 +3765,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
1997
3765
|
const plainTextInspection = executionPlan.plainTextInspection && processSucceeded;
|
|
1998
3766
|
const parseSucceeded = plainTextInspection || parseError === undefined;
|
|
1999
3767
|
const envelopeSuccess = plainTextInspection ? true : presentationEnvelope?.success !== false;
|
|
2000
|
-
|
|
3768
|
+
let succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
|
|
2001
3769
|
const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
|
|
2002
3770
|
updateTraceOwnerState({
|
|
2003
3771
|
command: executionPlan.commandInfo.command,
|
|
@@ -2020,12 +3788,13 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2020
3788
|
data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary),
|
|
2021
3789
|
};
|
|
2022
3790
|
}
|
|
3791
|
+
let overlayBlockerDiagnostic: OverlayBlockerDiagnostic | undefined;
|
|
2023
3792
|
|
|
2024
3793
|
let openResultTabCorrection: OpenResultTabCorrection | undefined;
|
|
2025
3794
|
if (
|
|
2026
3795
|
succeeded &&
|
|
2027
3796
|
executionPlan.sessionName &&
|
|
2028
|
-
hasLaunchScopedTabCorrectionFlag(
|
|
3797
|
+
hasLaunchScopedTabCorrectionFlag(toolArgs) &&
|
|
2029
3798
|
(executionPlan.commandInfo.command === "goto" ||
|
|
2030
3799
|
executionPlan.commandInfo.command === "navigate" ||
|
|
2031
3800
|
executionPlan.commandInfo.command === "open")
|
|
@@ -2123,33 +3892,91 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2123
3892
|
}
|
|
2124
3893
|
}
|
|
2125
3894
|
}
|
|
3895
|
+
let selectorTextVisibilityDiagnostics: SelectorTextVisibilityDiagnostic[] = [];
|
|
3896
|
+
const timeoutPartialProgress = processResult.timedOut ? await collectTimeoutPartialProgress({
|
|
3897
|
+
command: executionPlan.commandInfo.command,
|
|
3898
|
+
compiledJob,
|
|
3899
|
+
cwd: ctx.cwd,
|
|
3900
|
+
sessionName: executionPlan.sessionName,
|
|
3901
|
+
stdin: toolStdin,
|
|
3902
|
+
}) : undefined;
|
|
3903
|
+
if (succeeded && !sessionTabCorrection && !aboutBlankSessionMismatch) {
|
|
3904
|
+
overlayBlockerDiagnostic = await collectOverlayBlockerDiagnostic({
|
|
3905
|
+
command: executionPlan.commandInfo.command,
|
|
3906
|
+
cwd: ctx.cwd,
|
|
3907
|
+
data: presentationEnvelope?.data,
|
|
3908
|
+
navigationSummary,
|
|
3909
|
+
priorTarget: priorSessionTabTarget,
|
|
3910
|
+
sessionName: executionPlan.sessionName,
|
|
3911
|
+
signal,
|
|
3912
|
+
});
|
|
3913
|
+
}
|
|
3914
|
+
if (succeeded) {
|
|
3915
|
+
selectorTextVisibilityDiagnostics = await collectSelectorTextVisibilityDiagnostics({
|
|
3916
|
+
commandInfo: executionPlan.commandInfo,
|
|
3917
|
+
commandTokens,
|
|
3918
|
+
cwd: ctx.cwd,
|
|
3919
|
+
data: presentationEnvelope?.data,
|
|
3920
|
+
sessionName: executionPlan.sessionName,
|
|
3921
|
+
signal,
|
|
3922
|
+
});
|
|
3923
|
+
}
|
|
3924
|
+
let currentRefSnapshot: SessionRefSnapshot | undefined;
|
|
2126
3925
|
if (executionPlan.sessionName) {
|
|
2127
3926
|
const activeSessionTabTargetState = sessionTabTargets.get(executionPlan.sessionName);
|
|
2128
3927
|
if (shouldApplySessionTabTargetUpdate({ current: activeSessionTabTargetState, updateOrder: tabTargetUpdateOrder })) {
|
|
2129
3928
|
if (executionPlan.commandInfo.command === "close" && succeeded) {
|
|
2130
3929
|
sessionTabTargets.delete(executionPlan.sessionName);
|
|
3930
|
+
sessionRefSnapshots.delete(executionPlan.sessionName);
|
|
2131
3931
|
} else if (currentSessionTabTarget) {
|
|
2132
3932
|
sessionTabTargets.set(executionPlan.sessionName, { order: tabTargetUpdateOrder, target: currentSessionTabTarget });
|
|
2133
3933
|
}
|
|
2134
3934
|
}
|
|
3935
|
+
const refSnapshot = succeeded
|
|
3936
|
+
? executionPlan.commandInfo.command === "snapshot"
|
|
3937
|
+
? extractRefSnapshotFromData(presentationEnvelope?.data)
|
|
3938
|
+
: executionPlan.commandInfo.command === "batch"
|
|
3939
|
+
? extractRefSnapshotFromBatchResults(presentationEnvelope?.data)
|
|
3940
|
+
: overlayBlockerDiagnostic?.snapshot
|
|
3941
|
+
: undefined;
|
|
3942
|
+
if (refSnapshot && shouldApplySessionTabTargetUpdate({ current: sessionRefSnapshots.get(executionPlan.sessionName), updateOrder: tabTargetUpdateOrder })) {
|
|
3943
|
+
currentRefSnapshot = { ...refSnapshot, target: refSnapshot.target ?? currentSessionTabTarget };
|
|
3944
|
+
sessionRefSnapshots.set(executionPlan.sessionName, { ...currentRefSnapshot, order: tabTargetUpdateOrder });
|
|
3945
|
+
} else {
|
|
3946
|
+
currentRefSnapshot = sessionRefSnapshots.get(executionPlan.sessionName);
|
|
3947
|
+
}
|
|
2135
3948
|
}
|
|
2136
3949
|
|
|
3950
|
+
const priorManagedSessionActive = managedSessionActive;
|
|
2137
3951
|
const priorManagedSessionCwd = managedSessionCwd;
|
|
3952
|
+
const priorManagedSessionName = managedSessionName;
|
|
2138
3953
|
const managedSessionState = resolveManagedSessionState({
|
|
2139
3954
|
command: executionPlan.commandInfo.command,
|
|
2140
3955
|
managedSessionName: executionPlan.managedSessionName,
|
|
2141
|
-
priorActive:
|
|
2142
|
-
priorSessionName:
|
|
3956
|
+
priorActive: priorManagedSessionActive,
|
|
3957
|
+
priorSessionName: priorManagedSessionName,
|
|
2143
3958
|
succeeded,
|
|
2144
3959
|
});
|
|
2145
3960
|
const replacedManagedSessionName = managedSessionState.replacedSessionName;
|
|
2146
3961
|
managedSessionActive = managedSessionState.active;
|
|
2147
3962
|
managedSessionName = managedSessionState.sessionName;
|
|
3963
|
+
let managedSessionOutcome = buildManagedSessionOutcome({
|
|
3964
|
+
activeAfter: managedSessionActive,
|
|
3965
|
+
activeBefore: priorManagedSessionActive,
|
|
3966
|
+
attemptedSessionName: executionPlan.managedSessionName,
|
|
3967
|
+
command: executionPlan.commandInfo.command,
|
|
3968
|
+
currentSessionName: managedSessionName,
|
|
3969
|
+
previousSessionName: priorManagedSessionName,
|
|
3970
|
+
replacedSessionName: replacedManagedSessionName,
|
|
3971
|
+
sessionMode,
|
|
3972
|
+
succeeded,
|
|
3973
|
+
});
|
|
2148
3974
|
if (executionPlan.managedSessionName && succeeded) {
|
|
2149
3975
|
managedSessionCwd = ctx.cwd;
|
|
2150
3976
|
}
|
|
2151
3977
|
if (replacedManagedSessionName) {
|
|
2152
3978
|
sessionTabTargets.delete(replacedManagedSessionName);
|
|
3979
|
+
sessionRefSnapshots.delete(replacedManagedSessionName);
|
|
2153
3980
|
await closeManagedSession({
|
|
2154
3981
|
cwd: priorManagedSessionCwd,
|
|
2155
3982
|
sessionName: replacedManagedSessionName,
|
|
@@ -2165,7 +3992,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2165
3992
|
exitCode: processResult.exitCode,
|
|
2166
3993
|
parseError,
|
|
2167
3994
|
plainTextInspection,
|
|
2168
|
-
staleRefArgs: getStaleRefArgs(commandTokens,
|
|
3995
|
+
staleRefArgs: getStaleRefArgs(commandTokens, toolStdin),
|
|
2169
3996
|
spawnError: processResult.spawnError,
|
|
2170
3997
|
stderr: processResult.stderr,
|
|
2171
3998
|
timedOut: processResult.timedOut,
|
|
@@ -2189,6 +4016,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2189
4016
|
summary: `${redactedArgs.join(" ")} completed`,
|
|
2190
4017
|
}
|
|
2191
4018
|
: await buildToolPresentation({
|
|
4019
|
+
args: redactedProcessArgs,
|
|
2192
4020
|
artifactManifest,
|
|
2193
4021
|
artifactRequest: screenshotArtifactRequest,
|
|
2194
4022
|
batchArtifactRequests: batchScreenshotArtifactRequests,
|
|
@@ -2220,6 +4048,45 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2220
4048
|
if (presentation.artifactManifest) {
|
|
2221
4049
|
artifactManifest = presentation.artifactManifest;
|
|
2222
4050
|
}
|
|
4051
|
+
const qaPreset = compiledQaPreset ? analyzeQaPresetResults(presentationEnvelope?.data) : undefined;
|
|
4052
|
+
const sourceLookup = compiledSourceLookup ? await analyzeSourceLookupResults(presentationEnvelope?.data, compiledSourceLookup, ctx.cwd) : undefined;
|
|
4053
|
+
const networkSourceLookup = compiledNetworkSourceLookup ? redactNetworkSourceLookupAnalysis(await analyzeNetworkSourceLookupResults(presentationEnvelope?.data, compiledNetworkSourceLookup, ctx.cwd)) : undefined;
|
|
4054
|
+
if (networkSourceLookup && presentation.content[0]?.type === "text") {
|
|
4055
|
+
presentation.content[0] = { ...presentation.content[0], text: `${networkSourceLookup.summary}\n\n${presentation.content[0].text}` };
|
|
4056
|
+
} else if (networkSourceLookup) {
|
|
4057
|
+
presentation.content.unshift({ type: "text", text: networkSourceLookup.summary });
|
|
4058
|
+
}
|
|
4059
|
+
if (sourceLookup && presentation.content[0]?.type === "text") {
|
|
4060
|
+
presentation.content[0] = { ...presentation.content[0], text: `${sourceLookup.summary}\n\n${presentation.content[0].text}` };
|
|
4061
|
+
} else if (sourceLookup) {
|
|
4062
|
+
presentation.content.unshift({ type: "text", text: sourceLookup.summary });
|
|
4063
|
+
}
|
|
4064
|
+
if (qaPreset && (!qaPreset.passed || qaPreset.warnings.length > 0)) {
|
|
4065
|
+
if (!qaPreset.passed) {
|
|
4066
|
+
succeeded = false;
|
|
4067
|
+
presentation.failureCategory = "qa-failure";
|
|
4068
|
+
}
|
|
4069
|
+
presentation.summary = qaPreset.summary;
|
|
4070
|
+
if (presentation.content[0]?.type === "text") {
|
|
4071
|
+
presentation.content[0] = { ...presentation.content[0], text: `${qaPreset.summary}\n\n${presentation.content[0].text}` };
|
|
4072
|
+
} else {
|
|
4073
|
+
presentation.content.unshift({ type: "text", text: qaPreset.summary });
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
if (managedSessionOutcome && managedSessionOutcome.succeeded !== succeeded) {
|
|
4077
|
+
managedSessionOutcome = { ...managedSessionOutcome, succeeded };
|
|
4078
|
+
}
|
|
4079
|
+
const evalStdinHint = getEvalStdinHint({
|
|
4080
|
+
command: executionPlan.commandInfo.command,
|
|
4081
|
+
data: presentationEnvelope?.data,
|
|
4082
|
+
stdin: toolStdin,
|
|
4083
|
+
});
|
|
4084
|
+
const resultArtifactManifest = presentation.artifactManifest ?? artifactManifest;
|
|
4085
|
+
const artifactCleanup = getArtifactCleanupGuidance({
|
|
4086
|
+
command: executionPlan.commandInfo.command,
|
|
4087
|
+
manifest: resultArtifactManifest,
|
|
4088
|
+
succeeded,
|
|
4089
|
+
});
|
|
2223
4090
|
const warningText = aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
|
|
2224
4091
|
const contentWithSessionWarnings = userRequestedJson && !plainTextInspection
|
|
2225
4092
|
? buildJsonVisibleContent({
|
|
@@ -2248,20 +4115,69 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2248
4115
|
? { ...item, text: exactRedactedText }
|
|
2249
4116
|
: { ...item, text: redactSensitiveText(exactRedactedText) };
|
|
2250
4117
|
});
|
|
4118
|
+
const categoryDetails = buildAgentBrowserResultCategoryDetails({
|
|
4119
|
+
artifacts: presentation.artifacts,
|
|
4120
|
+
args: redactedProcessArgs,
|
|
4121
|
+
command: executionPlan.commandInfo.command,
|
|
4122
|
+
confirmationRequired: presentation.summary.startsWith("Confirmation required"),
|
|
4123
|
+
errorText: errorText ?? presentation.summary,
|
|
4124
|
+
failureCategory: presentation.failureCategory ?? presentation.batchFailure?.failedStep.failureCategory,
|
|
4125
|
+
inspection: plainTextInspection,
|
|
4126
|
+
parseError,
|
|
4127
|
+
savedFile: presentation.savedFile,
|
|
4128
|
+
spawnError: processResult.spawnError?.message,
|
|
4129
|
+
succeeded,
|
|
4130
|
+
tabDrift: !succeeded && (aboutBlankSessionMismatch !== undefined || sessionTabCorrection !== undefined),
|
|
4131
|
+
timedOut: processResult.timedOut,
|
|
4132
|
+
validationError: undefined,
|
|
4133
|
+
});
|
|
4134
|
+
let nextActions = presentation.nextActions ? [...presentation.nextActions] : undefined;
|
|
4135
|
+
if (categoryDetails.failureCategory === "stale-ref") {
|
|
4136
|
+
nextActions = sessionAwareStaleRefNextActions(executionPlan.sessionName);
|
|
4137
|
+
}
|
|
4138
|
+
if (categoryDetails.failureCategory === "selector-not-found" && redactedCompiledSemanticAction) {
|
|
4139
|
+
const candidateActions = buildSemanticActionCandidateActions(redactedCompiledSemanticAction);
|
|
4140
|
+
if (candidateActions.length > 0) {
|
|
4141
|
+
(nextActions ??= []).push(...candidateActions);
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
if (overlayBlockerDiagnostic) {
|
|
4145
|
+
(nextActions ??= []).push(...buildOverlayBlockerNextActions({ diagnostic: overlayBlockerDiagnostic, sessionName: executionPlan.sessionName }));
|
|
4146
|
+
}
|
|
4147
|
+
if (selectorTextVisibilityDiagnostics.length > 0) {
|
|
4148
|
+
(nextActions ??= []).push(...buildSelectorTextVisibilityNextActions({ diagnostics: selectorTextVisibilityDiagnostics, sessionName: executionPlan.sessionName }));
|
|
4149
|
+
}
|
|
4150
|
+
if (categoryDetails.failureCategory === "stale-ref" && redactedCompiledSemanticAction) {
|
|
4151
|
+
(nextActions ??= []).push({
|
|
4152
|
+
id: "retry-semantic-action-after-stale-ref",
|
|
4153
|
+
params: { args: redactedCompiledSemanticAction.args },
|
|
4154
|
+
reason: "Retry the same semantic target via its compiled find command after the upstream stale-ref failure proves the prior action did not execute.",
|
|
4155
|
+
safety: "Use only for the same intended target; direct stale @refs still require a fresh snapshot or stable locator before retrying.",
|
|
4156
|
+
tool: "agent_browser" as const,
|
|
4157
|
+
});
|
|
4158
|
+
}
|
|
2251
4159
|
const details = {
|
|
2252
4160
|
args: redactedArgs,
|
|
2253
|
-
|
|
2254
|
-
|
|
4161
|
+
compiledJob: redactedCompiledJob,
|
|
4162
|
+
compiledQaPreset: redactedCompiledQaPreset,
|
|
4163
|
+
compiledSourceLookup: redactedCompiledSourceLookup,
|
|
4164
|
+
compiledNetworkSourceLookup: redactedCompiledNetworkSourceLookup,
|
|
4165
|
+
artifactManifest: resultArtifactManifest,
|
|
4166
|
+
artifactRetentionSummary: presentation.artifactRetentionSummary ?? (resultArtifactManifest ? formatSessionArtifactRetentionSummary(resultArtifactManifest) : undefined),
|
|
4167
|
+
artifactCleanup,
|
|
4168
|
+
artifactVerification: presentation.artifactVerification,
|
|
2255
4169
|
artifacts: presentation.artifacts,
|
|
2256
4170
|
batchFailure: presentation.batchFailure,
|
|
2257
4171
|
batchSteps: presentation.batchSteps,
|
|
2258
4172
|
command: executionPlan.commandInfo.command,
|
|
4173
|
+
compiledSemanticAction: redactedCompiledSemanticAction,
|
|
2259
4174
|
compatibilityWorkaround,
|
|
2260
4175
|
subcommand: executionPlan.commandInfo.subcommand,
|
|
2261
4176
|
data: presentation.data,
|
|
2262
4177
|
error: plainTextInspection ? undefined : presentationEnvelope?.error,
|
|
2263
4178
|
inspection: plainTextInspection || undefined,
|
|
2264
4179
|
navigationSummary,
|
|
4180
|
+
...categoryDetails,
|
|
2265
4181
|
aboutBlankSessionMismatch,
|
|
2266
4182
|
openResultTabCorrection,
|
|
2267
4183
|
effectiveArgs: redactedProcessArgs,
|
|
@@ -2269,14 +4185,26 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2269
4185
|
fullOutputPath: parseFailureOutput.fullOutputPath ?? presentation.fullOutputPath,
|
|
2270
4186
|
fullOutputPaths: presentation.fullOutputPaths,
|
|
2271
4187
|
fullOutputUnavailable: parseFailureOutput.fullOutputUnavailable,
|
|
4188
|
+
managedSessionOutcome,
|
|
2272
4189
|
imagePath: presentation.imagePath,
|
|
2273
4190
|
imagePaths: presentation.imagePaths,
|
|
4191
|
+
nextActions,
|
|
4192
|
+
pageChangeSummary: presentation.pageChangeSummary,
|
|
4193
|
+
overlayBlockers: overlayBlockerDiagnostic,
|
|
4194
|
+
qaPreset,
|
|
4195
|
+
selectorTextVisibility: selectorTextVisibilityDiagnostics[0],
|
|
4196
|
+
selectorTextVisibilityAll: selectorTextVisibilityDiagnostics.length > 1 ? selectorTextVisibilityDiagnostics : undefined,
|
|
4197
|
+
evalStdinHint,
|
|
4198
|
+
timeoutPartialProgress,
|
|
2274
4199
|
parseError: plainTextInspection ? undefined : parseError,
|
|
2275
4200
|
savedFile: presentation.savedFile,
|
|
2276
4201
|
savedFilePath: presentation.savedFilePath,
|
|
4202
|
+
sourceLookup,
|
|
4203
|
+
networkSourceLookup,
|
|
2277
4204
|
sessionMode,
|
|
2278
4205
|
sessionTabCorrection,
|
|
2279
4206
|
sessionTabTarget: currentSessionTabTarget,
|
|
4207
|
+
refSnapshot: currentRefSnapshot,
|
|
2280
4208
|
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
2281
4209
|
sessionRecoveryHint: redactedRecoveryHint,
|
|
2282
4210
|
startupScopedFlags: executionPlan.startupScopedFlags,
|
|
@@ -2287,11 +4215,28 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2287
4215
|
timeoutMs: processResult.timeoutMs,
|
|
2288
4216
|
};
|
|
2289
4217
|
|
|
2290
|
-
|
|
2291
|
-
|
|
4218
|
+
const semanticActionCandidateText = nextActions ? formatSemanticActionCandidateText(nextActions) : undefined;
|
|
4219
|
+
const overlayBlockerText = overlayBlockerDiagnostic ? formatOverlayBlockerText(overlayBlockerDiagnostic) : undefined;
|
|
4220
|
+
const selectorTextVisibilityText = formatSelectorTextVisibilityText(selectorTextVisibilityDiagnostics);
|
|
4221
|
+
const evalStdinHintText = formatEvalStdinHintText(evalStdinHint);
|
|
4222
|
+
const artifactCleanupText = formatArtifactCleanupGuidanceText(artifactCleanup);
|
|
4223
|
+
const timeoutPartialProgressText = timeoutPartialProgress ? formatTimeoutPartialProgressText(timeoutPartialProgress) : undefined;
|
|
4224
|
+
const managedSessionOutcomeText = formatManagedSessionOutcomeText(managedSessionOutcome);
|
|
4225
|
+
const rawAppendedDiagnosticText = [semanticActionCandidateText, overlayBlockerText, selectorTextVisibilityText, evalStdinHintText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText].filter((item): item is string => item !== undefined).join("\n\n");
|
|
4226
|
+
const appendedDiagnosticText = redactSensitiveText(redactExactSensitiveText(rawAppendedDiagnosticText, exactSensitiveValues));
|
|
4227
|
+
const shouldAppendDiagnosticText = appendedDiagnosticText.length > 0 && (!userRequestedJson || plainTextInspection);
|
|
4228
|
+
const content = shouldAppendDiagnosticText && redactedContent[0]?.type === "text"
|
|
4229
|
+
? [
|
|
4230
|
+
{ ...redactedContent[0], text: `${redactedContent[0].text}\n\n${appendedDiagnosticText}` },
|
|
4231
|
+
...redactedContent.slice(1),
|
|
4232
|
+
]
|
|
4233
|
+
: redactedContent;
|
|
4234
|
+
const result = {
|
|
4235
|
+
content,
|
|
2292
4236
|
details: redactToolDetails(details, exactSensitiveValues),
|
|
2293
4237
|
isError: !succeeded,
|
|
2294
4238
|
};
|
|
4239
|
+
return compiledNetworkSourceLookup ? redactNetworkSourceLookupSurface(result) as typeof result : result;
|
|
2295
4240
|
} finally {
|
|
2296
4241
|
if (processResult.stdoutSpillPath) {
|
|
2297
4242
|
await rm(processResult.stdoutSpillPath, { force: true }).catch(() => undefined);
|
|
@@ -2299,7 +4244,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2299
4244
|
}
|
|
2300
4245
|
};
|
|
2301
4246
|
|
|
2302
|
-
return extractExplicitSessionName(
|
|
4247
|
+
return extractExplicitSessionName(toolArgs)
|
|
2303
4248
|
? runTool()
|
|
2304
4249
|
: managedSessionExecutionQueue.run(runTool);
|
|
2305
4250
|
},
|