pi-agent-browser-native 0.2.31 → 0.2.32
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 +18 -0
- package/README.md +41 -6
- package/docs/ARCHITECTURE.md +10 -8
- package/docs/COMMAND_REFERENCE.md +56 -9
- package/docs/ELECTRON.md +368 -0
- package/docs/RELEASE.md +30 -2
- package/docs/REQUIREMENTS.md +4 -2
- package/docs/SUPPORT_MATRIX.md +8 -5
- package/docs/TOOL_CONTRACT.md +172 -19
- package/extensions/agent-browser/index.ts +2225 -159
- package/extensions/agent-browser/lib/electron/cleanup.ts +287 -0
- package/extensions/agent-browser/lib/electron/discovery.ts +717 -0
- package/extensions/agent-browser/lib/electron/launch.ts +553 -0
- package/extensions/agent-browser/lib/playbook.ts +7 -6
- package/extensions/agent-browser/lib/results/presentation.ts +37 -7
- package/extensions/agent-browser/lib/results/shared.ts +88 -0
- package/extensions/agent-browser/lib/temp.ts +26 -0
- package/package.json +5 -4
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { constants as fsConstants } from "node:fs";
|
|
10
|
+
import type { ChildProcess } from "node:child_process";
|
|
10
11
|
import { access, copyFile, mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
|
|
11
12
|
import { delimiter, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
@@ -23,6 +24,25 @@ import {
|
|
|
23
24
|
import { Text } from "@earendil-works/pi-tui";
|
|
24
25
|
import { Type } from "typebox";
|
|
25
26
|
|
|
27
|
+
import {
|
|
28
|
+
discoverElectronApps,
|
|
29
|
+
ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS,
|
|
30
|
+
ELECTRON_DISCOVERY_MAX_RESULTS,
|
|
31
|
+
type ElectronDiscoveryResult,
|
|
32
|
+
} from "./lib/electron/discovery.js";
|
|
33
|
+
import {
|
|
34
|
+
cleanupElectronLaunchResources,
|
|
35
|
+
inspectElectronLaunchStatus,
|
|
36
|
+
type ElectronCleanupResult,
|
|
37
|
+
type ElectronLaunchStatus,
|
|
38
|
+
} from "./lib/electron/cleanup.js";
|
|
39
|
+
import {
|
|
40
|
+
launchElectronApp,
|
|
41
|
+
type ElectronCdpTarget,
|
|
42
|
+
type ElectronLaunchFailure,
|
|
43
|
+
type ElectronLaunchRecord,
|
|
44
|
+
type ElectronLaunchSuccess,
|
|
45
|
+
} from "./lib/electron/launch.js";
|
|
26
46
|
import {
|
|
27
47
|
PROJECT_RULE_PROMPT,
|
|
28
48
|
buildToolPromptGuidelines,
|
|
@@ -88,6 +108,28 @@ const AGENT_BROWSER_SEMANTIC_ACTIONS = ["check", "click", "fill", "select", "unc
|
|
|
88
108
|
const AGENT_BROWSER_SEMANTIC_LOCATORS = ["alt", "label", "placeholder", "role", "testid", "text", "title"] as const;
|
|
89
109
|
const AGENT_BROWSER_JOB_STEP_ACTIONS = ["open", "click", "fill", "select", "wait", "assertText", "assertUrl", "waitForDownload", "screenshot"] as const;
|
|
90
110
|
const AGENT_BROWSER_QA_LOAD_STATES = ["domcontentloaded", "load", "networkidle"] as const;
|
|
111
|
+
const AGENT_BROWSER_ELECTRON_ACTIONS = ["list", "launch", "status", "cleanup", "probe"] as const;
|
|
112
|
+
const AGENT_BROWSER_ELECTRON_HANDOFFS = ["connect", "tabs", "snapshot"] as const;
|
|
113
|
+
const AGENT_BROWSER_ELECTRON_TARGET_TYPES = ["page", "webview", "any"] as const;
|
|
114
|
+
const AGENT_BROWSER_ELECTRON_LIST_FIELDS = new Set(["action", "query", "maxResults"]);
|
|
115
|
+
const AGENT_BROWSER_ELECTRON_PROBE_FIELDS = new Set(["action", "launchId", "timeoutMs"]);
|
|
116
|
+
const AGENT_BROWSER_ELECTRON_RESERVED_APP_ARGS = ["--user-data-dir", "--remote-debugging-port", "--remote-debugging-address", "--remote-debugging-pipe"] as const;
|
|
117
|
+
const ELECTRON_PROFILE_ISOLATION_NOTE = "Profile note: electron.launch starts an isolated temporary profile; it does not reuse the app's normal signed-in profile or attach to an already-running authenticated app.";
|
|
118
|
+
const ELECTRON_EXISTING_AUTH_GUIDANCE = "For already-authenticated desktop app content, do not stop here: if host tools are allowed and the app is not running, launch the normal app with --remote-debugging-port=<port>, verify the port, then run agent_browser connect <port>; if it is already running without a debug port, ask before relaunching it.";
|
|
119
|
+
const ELECTRON_PROFILE_ISOLATION_DETAILS = {
|
|
120
|
+
attachesToAlreadyRunningApp: false,
|
|
121
|
+
existingAuthenticatedAppGuidance: ELECTRON_EXISTING_AUTH_GUIDANCE,
|
|
122
|
+
hostDebugLaunchExample: "macOS: open -a <App Name> --args --remote-debugging-port=9222 --remote-allow-origins='*'; then agent_browser connect 9222 with sessionMode=fresh",
|
|
123
|
+
isolatedLaunch: true,
|
|
124
|
+
note: ELECTRON_PROFILE_ISOLATION_NOTE,
|
|
125
|
+
reusesExistingSignedInProfile: false,
|
|
126
|
+
} as const;
|
|
127
|
+
const ELECTRON_PROBE_MAX_TABS = 6;
|
|
128
|
+
const ELECTRON_PROBE_MAX_REF_IDS = 20;
|
|
129
|
+
const ELECTRON_PROBE_MAX_SNAPSHOT_LINES = 12;
|
|
130
|
+
const ELECTRON_PROBE_MAX_SNAPSHOT_CHARS = 1_600;
|
|
131
|
+
const ELECTRON_POST_COMMAND_STATUS_SETTLE_MS = 250;
|
|
132
|
+
const ELECTRON_FILL_VERIFICATION_TIMEOUT_MS = 2_000;
|
|
91
133
|
const SOURCE_LOOKUP_WORKSPACE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
92
134
|
const SOURCE_LOOKUP_IGNORED_DIRECTORIES = new Set([".git", "node_modules", "dist", "build", "coverage", ".next", "out", "tmp", "temp"]);
|
|
93
135
|
const SOURCE_LOOKUP_DEFAULT_MAX_WORKSPACE_FILES = 2_000;
|
|
@@ -97,6 +139,7 @@ type AgentBrowserSemanticActionName = (typeof AGENT_BROWSER_SEMANTIC_ACTIONS)[nu
|
|
|
97
139
|
type AgentBrowserSemanticLocator = (typeof AGENT_BROWSER_SEMANTIC_LOCATORS)[number];
|
|
98
140
|
type AgentBrowserJobStepAction = (typeof AGENT_BROWSER_JOB_STEP_ACTIONS)[number];
|
|
99
141
|
type AgentBrowserQaLoadState = (typeof AGENT_BROWSER_QA_LOAD_STATES)[number];
|
|
142
|
+
type AgentBrowserElectronAction = (typeof AGENT_BROWSER_ELECTRON_ACTIONS)[number];
|
|
100
143
|
type AgentBrowserSourceLookupStatus = "candidates-found" | "no-candidates" | "unsupported";
|
|
101
144
|
type AgentBrowserNetworkSourceLookupStatus = "failed-requests-found" | "no-failed-requests" | "no-candidates";
|
|
102
145
|
|
|
@@ -182,7 +225,8 @@ interface CompiledAgentBrowserQaPreset extends CompiledAgentBrowserJob {
|
|
|
182
225
|
expectedText: string[];
|
|
183
226
|
expectedSelector?: string;
|
|
184
227
|
screenshotPath?: string;
|
|
185
|
-
|
|
228
|
+
attached: boolean;
|
|
229
|
+
url?: string;
|
|
186
230
|
};
|
|
187
231
|
}
|
|
188
232
|
|
|
@@ -214,11 +258,27 @@ interface AgentBrowserSourceLookupCandidate {
|
|
|
214
258
|
source: "react-inspect" | "dom-attribute" | "workspace-search";
|
|
215
259
|
}
|
|
216
260
|
|
|
261
|
+
interface AgentBrowserSourceLookupElectronContext {
|
|
262
|
+
appName?: string;
|
|
263
|
+
appPath?: string;
|
|
264
|
+
executablePath?: string;
|
|
265
|
+
launchId?: string;
|
|
266
|
+
sessionName?: string;
|
|
267
|
+
url?: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
217
270
|
interface AgentBrowserSourceLookupAnalysis {
|
|
218
271
|
candidates: AgentBrowserSourceLookupCandidate[];
|
|
272
|
+
electronContext?: AgentBrowserSourceLookupElectronContext;
|
|
219
273
|
limitations: string[];
|
|
220
274
|
status: AgentBrowserSourceLookupStatus;
|
|
221
275
|
summary: string;
|
|
276
|
+
workspaceRoot?: string;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
interface AgentBrowserSourceLookupAnalysisContext {
|
|
280
|
+
electronContext?: AgentBrowserSourceLookupElectronContext;
|
|
281
|
+
workspaceRoot: string;
|
|
222
282
|
}
|
|
223
283
|
|
|
224
284
|
interface CompiledAgentBrowserNetworkSourceLookup {
|
|
@@ -234,6 +294,37 @@ interface CompiledAgentBrowserNetworkSourceLookup {
|
|
|
234
294
|
};
|
|
235
295
|
}
|
|
236
296
|
|
|
297
|
+
type CompiledAgentBrowserElectron =
|
|
298
|
+
| {
|
|
299
|
+
action: "list";
|
|
300
|
+
maxResults?: number;
|
|
301
|
+
query?: string;
|
|
302
|
+
}
|
|
303
|
+
| {
|
|
304
|
+
action: "launch";
|
|
305
|
+
allow?: string[];
|
|
306
|
+
appArgs?: string[];
|
|
307
|
+
deny?: string[];
|
|
308
|
+
appName?: string;
|
|
309
|
+
appPath?: string;
|
|
310
|
+
bundleId?: string;
|
|
311
|
+
executablePath?: string;
|
|
312
|
+
handoff: "connect" | "snapshot" | "tabs";
|
|
313
|
+
targetType: "any" | "page" | "webview";
|
|
314
|
+
timeoutMs?: number;
|
|
315
|
+
}
|
|
316
|
+
| {
|
|
317
|
+
action: "cleanup" | "status";
|
|
318
|
+
all?: boolean;
|
|
319
|
+
launchId?: string;
|
|
320
|
+
timeoutMs?: number;
|
|
321
|
+
}
|
|
322
|
+
| {
|
|
323
|
+
action: "probe";
|
|
324
|
+
launchId?: string;
|
|
325
|
+
timeoutMs?: number;
|
|
326
|
+
};
|
|
327
|
+
|
|
237
328
|
interface AgentBrowserNetworkSourceLookupRequest {
|
|
238
329
|
error?: string;
|
|
239
330
|
method?: string;
|
|
@@ -263,7 +354,7 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
263
354
|
|
|
264
355
|
args: Type.Optional(
|
|
265
356
|
Type.Array(Type.String({ description: "Exact agent-browser CLI arguments, excluding the binary name." }), {
|
|
266
|
-
description: "Exact agent-browser CLI arguments, excluding the binary name and any shell operators. Required unless semanticAction, job, qa, sourceLookup, or
|
|
357
|
+
description: "Exact agent-browser CLI arguments, excluding the binary name and any shell operators. Required unless semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron is provided.",
|
|
267
358
|
minItems: 1,
|
|
268
359
|
}),
|
|
269
360
|
),
|
|
@@ -275,26 +366,39 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
275
366
|
locator: Type.Optional(StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, {
|
|
276
367
|
description: "Upstream find locator family to use for check/click/fill/uncheck actions.",
|
|
277
368
|
})),
|
|
278
|
-
value: Type.Optional(Type.String({ description: "Locator value for find actions, or a single option value for select actions." })),
|
|
369
|
+
value: Type.Optional(Type.String({ description: "Locator value for find actions, or a single option value for select actions. For locator=role, role may be supplied instead." })),
|
|
279
370
|
values: Type.Optional(Type.Array(Type.String({ description: "Option value for select actions." }), { description: "One or more option values for select actions.", minItems: 1 })),
|
|
280
371
|
selector: Type.Optional(Type.String({ description: "Selector or @ref for select actions; compiled to select <selector> <value...>." })),
|
|
281
372
|
text: Type.Optional(Type.String({ description: "Text/value argument for fill actions." })),
|
|
282
|
-
role: Type.Optional(Type.String({ description: "Role locator value; when set
|
|
373
|
+
role: Type.Optional(Type.String({ description: "Role locator value for locator=role. May be used instead of value; when both are set they must match." })),
|
|
283
374
|
name: Type.Optional(Type.String({ description: "Accessible name filter for locator=role; compiles to --name <name>." })),
|
|
284
375
|
session: Type.Optional(Type.String({ description: "Optional upstream session name; prepends --session <name> before the compiled command." })),
|
|
285
376
|
}),
|
|
286
377
|
),
|
|
287
378
|
qa: Type.Optional(
|
|
288
|
-
Type.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
379
|
+
Type.Union([
|
|
380
|
+
Type.Object({
|
|
381
|
+
attached: Type.Literal(true, { description: "Run the QA preset against the currently attached session instead of opening qa.url." }),
|
|
382
|
+
expectedText: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())], { description: "Text that must appear on the page." })),
|
|
383
|
+
expectedSelector: Type.Optional(Type.String({ description: "Selector or @ref that must appear on the page." })),
|
|
384
|
+
screenshotPath: Type.Optional(Type.String({ description: "Optional evidence screenshot path captured at the end of the QA preset." })),
|
|
385
|
+
checkConsole: Type.Optional(Type.Boolean({ description: "Whether to fail on console error messages. Defaults to true." })),
|
|
386
|
+
checkErrors: Type.Optional(Type.Boolean({ description: "Whether to fail on page errors. Defaults to true." })),
|
|
387
|
+
checkNetwork: Type.Optional(Type.Boolean({ description: "Whether to inspect network requests and fail on actionable request failures; benign icon misses warn. Defaults to true." })),
|
|
388
|
+
loadState: Type.Optional(StringEnum(AGENT_BROWSER_QA_LOAD_STATES, { description: "Page readiness state for the QA preset before assertions and diagnostics. Defaults to domcontentloaded; use networkidle only for pages without long-lived background requests." })),
|
|
389
|
+
}, { additionalProperties: false }),
|
|
390
|
+
Type.Object({
|
|
391
|
+
url: Type.String({ description: "URL to open for a lightweight QA preset." }),
|
|
392
|
+
attached: Type.Optional(Type.Literal(false, { description: "When omitted or false, qa.url is required and opened before checks." })),
|
|
393
|
+
expectedText: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())], { description: "Text that must appear on the page." })),
|
|
394
|
+
expectedSelector: Type.Optional(Type.String({ description: "Selector or @ref that must appear on the page." })),
|
|
395
|
+
screenshotPath: Type.Optional(Type.String({ description: "Optional evidence screenshot path captured at the end of the QA preset." })),
|
|
396
|
+
checkConsole: Type.Optional(Type.Boolean({ description: "Whether to fail on console error messages. Defaults to true." })),
|
|
397
|
+
checkErrors: Type.Optional(Type.Boolean({ description: "Whether to fail on page errors. Defaults to true." })),
|
|
398
|
+
checkNetwork: Type.Optional(Type.Boolean({ description: "Whether to inspect network requests and fail on actionable request failures; benign icon misses warn. Defaults to true." })),
|
|
399
|
+
loadState: Type.Optional(StringEnum(AGENT_BROWSER_QA_LOAD_STATES, { description: "Page readiness state for the QA preset before assertions and diagnostics. Defaults to domcontentloaded; use networkidle only for pages without long-lived background requests." })),
|
|
400
|
+
}, { additionalProperties: false }),
|
|
401
|
+
], { description: "Lightweight QA preset. Use qa.url to open a URL, or qa.attached=true to check the current attached session without opening a URL." }),
|
|
298
402
|
),
|
|
299
403
|
sourceLookup: Type.Optional(
|
|
300
404
|
Type.Object({
|
|
@@ -314,6 +418,74 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
314
418
|
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 })),
|
|
315
419
|
}),
|
|
316
420
|
),
|
|
421
|
+
electron: Type.Optional(
|
|
422
|
+
Type.Union([
|
|
423
|
+
Type.Object({
|
|
424
|
+
action: StringEnum(["list"] as const, { description: "List discovered Electron apps." }),
|
|
425
|
+
query: Type.Optional(Type.String({ description: "Optional case-insensitive substring filter for electron.list across app name, bundle id, desktop id, and paths.", minLength: 1 })),
|
|
426
|
+
maxResults: Type.Optional(Type.Integer({ description: `Maximum electron.list apps to return. Defaults to ${ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS}; values above ${ELECTRON_DISCOVERY_MAX_RESULTS} are clamped.`, minimum: 1 })),
|
|
427
|
+
}, { additionalProperties: false }),
|
|
428
|
+
Type.Object({
|
|
429
|
+
action: StringEnum(["launch"] as const, { description: "Launch an Electron app with an isolated wrapper-owned profile." }),
|
|
430
|
+
appPath: Type.String({ description: "Electron launch target: macOS .app bundle path. Exactly one launch target is required for electron.launch.", minLength: 1 }),
|
|
431
|
+
appArgs: Type.Optional(Type.Array(Type.String({ description: "Argument passed to the Electron application.", minLength: 1 }), { description: "Optional Electron app argv. Wrapper-owned lifecycle/debug flags are rejected." })),
|
|
432
|
+
handoff: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_HANDOFFS, { description: "Post-launch handoff depth. Defaults to snapshot." })),
|
|
433
|
+
targetType: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_TARGET_TYPES, { description: "Preferred CDP target type. Defaults to page." })),
|
|
434
|
+
timeoutMs: Type.Optional(Type.Integer({ description: "Bounded launch timeout in milliseconds.", minimum: 1 })),
|
|
435
|
+
allow: Type.Optional(Type.Array(Type.String({ description: "App identifier allowed by the caller for electron.launch.", minLength: 1 }), { description: "Optional caller-owned allow list for electron.launch policy checks." })),
|
|
436
|
+
deny: Type.Optional(Type.Array(Type.String({ description: "App identifier denied by the caller for electron.launch.", minLength: 1 }), { description: "Optional caller-owned deny list for electron.launch policy checks; deny wins over allow." })),
|
|
437
|
+
}, { additionalProperties: false }),
|
|
438
|
+
Type.Object({
|
|
439
|
+
action: StringEnum(["launch"] as const, { description: "Launch an Electron app with an isolated wrapper-owned profile." }),
|
|
440
|
+
appName: Type.String({ description: "Electron launch target: app display name discovered by electron.list. Exactly one launch target is required for electron.launch.", minLength: 1 }),
|
|
441
|
+
appArgs: Type.Optional(Type.Array(Type.String({ description: "Argument passed to the Electron application.", minLength: 1 }), { description: "Optional Electron app argv. Wrapper-owned lifecycle/debug flags are rejected." })),
|
|
442
|
+
handoff: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_HANDOFFS, { description: "Post-launch handoff depth. Defaults to snapshot." })),
|
|
443
|
+
targetType: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_TARGET_TYPES, { description: "Preferred CDP target type. Defaults to page." })),
|
|
444
|
+
timeoutMs: Type.Optional(Type.Integer({ description: "Bounded launch timeout in milliseconds.", minimum: 1 })),
|
|
445
|
+
allow: Type.Optional(Type.Array(Type.String({ description: "App identifier allowed by the caller for electron.launch.", minLength: 1 }), { description: "Optional caller-owned allow list for electron.launch policy checks." })),
|
|
446
|
+
deny: Type.Optional(Type.Array(Type.String({ description: "App identifier denied by the caller for electron.launch.", minLength: 1 }), { description: "Optional caller-owned deny list for electron.launch policy checks; deny wins over allow." })),
|
|
447
|
+
}, { additionalProperties: false }),
|
|
448
|
+
Type.Object({
|
|
449
|
+
action: StringEnum(["launch"] as const, { description: "Launch an Electron app with an isolated wrapper-owned profile." }),
|
|
450
|
+
bundleId: Type.String({ description: "Electron launch target: macOS bundle identifier discovered by electron.list. Exactly one launch target is required for electron.launch.", minLength: 1 }),
|
|
451
|
+
appArgs: Type.Optional(Type.Array(Type.String({ description: "Argument passed to the Electron application.", minLength: 1 }), { description: "Optional Electron app argv. Wrapper-owned lifecycle/debug flags are rejected." })),
|
|
452
|
+
handoff: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_HANDOFFS, { description: "Post-launch handoff depth. Defaults to snapshot." })),
|
|
453
|
+
targetType: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_TARGET_TYPES, { description: "Preferred CDP target type. Defaults to page." })),
|
|
454
|
+
timeoutMs: Type.Optional(Type.Integer({ description: "Bounded launch timeout in milliseconds.", minimum: 1 })),
|
|
455
|
+
allow: Type.Optional(Type.Array(Type.String({ description: "App identifier allowed by the caller for electron.launch.", minLength: 1 }), { description: "Optional caller-owned allow list for electron.launch policy checks." })),
|
|
456
|
+
deny: Type.Optional(Type.Array(Type.String({ description: "App identifier denied by the caller for electron.launch.", minLength: 1 }), { description: "Optional caller-owned deny list for electron.launch policy checks; deny wins over allow." })),
|
|
457
|
+
}, { additionalProperties: false }),
|
|
458
|
+
Type.Object({
|
|
459
|
+
action: StringEnum(["launch"] as const, { description: "Launch an Electron app with an isolated wrapper-owned profile." }),
|
|
460
|
+
executablePath: Type.String({ description: "Electron launch target: executable path. Discovery is not required when this is provided. Exactly one launch target is required for electron.launch.", minLength: 1 }),
|
|
461
|
+
appArgs: Type.Optional(Type.Array(Type.String({ description: "Argument passed to the Electron application.", minLength: 1 }), { description: "Optional Electron app argv. Wrapper-owned lifecycle/debug flags are rejected." })),
|
|
462
|
+
handoff: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_HANDOFFS, { description: "Post-launch handoff depth. Defaults to snapshot." })),
|
|
463
|
+
targetType: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_TARGET_TYPES, { description: "Preferred CDP target type. Defaults to page." })),
|
|
464
|
+
timeoutMs: Type.Optional(Type.Integer({ description: "Bounded launch timeout in milliseconds.", minimum: 1 })),
|
|
465
|
+
allow: Type.Optional(Type.Array(Type.String({ description: "App identifier allowed by the caller for electron.launch.", minLength: 1 }), { description: "Optional caller-owned allow list for electron.launch policy checks." })),
|
|
466
|
+
deny: Type.Optional(Type.Array(Type.String({ description: "App identifier denied by the caller for electron.launch.", minLength: 1 }), { description: "Optional caller-owned deny list for electron.launch policy checks; deny wins over allow." })),
|
|
467
|
+
}, { additionalProperties: false }),
|
|
468
|
+
Type.Object({
|
|
469
|
+
action: StringEnum(["status", "cleanup"] as const, { description: "Inspect or cleanup one wrapper-tracked Electron launch by launchId." }),
|
|
470
|
+
launchId: Type.String({ description: "Wrapper launch id for electron.status and electron.cleanup.", minLength: 1 }),
|
|
471
|
+
timeoutMs: Type.Optional(Type.Integer({ description: "Bounded status/cleanup timeout in milliseconds.", minimum: 1 })),
|
|
472
|
+
}, { additionalProperties: false }),
|
|
473
|
+
Type.Object({
|
|
474
|
+
action: StringEnum(["status", "cleanup"] as const, { description: "Inspect or cleanup all wrapper-tracked Electron launches." }),
|
|
475
|
+
all: Type.Literal(true, { description: "Apply electron.status or electron.cleanup to all wrapper-owned launches." }),
|
|
476
|
+
timeoutMs: Type.Optional(Type.Integer({ description: "Bounded status/cleanup timeout in milliseconds.", minimum: 1 })),
|
|
477
|
+
}, { additionalProperties: false }),
|
|
478
|
+
Type.Object({
|
|
479
|
+
action: StringEnum(["status", "cleanup"] as const, { description: "Inspect or cleanup the only active wrapper-tracked Electron launch." }),
|
|
480
|
+
timeoutMs: Type.Optional(Type.Integer({ description: "Bounded status/cleanup timeout in milliseconds.", minimum: 1 })),
|
|
481
|
+
}, { additionalProperties: false }),
|
|
482
|
+
Type.Object({
|
|
483
|
+
action: StringEnum(["probe"] as const, { description: "Probe the current attached Electron managed session; launchId is accepted for launch-scoped follow-up actions." }),
|
|
484
|
+
launchId: Type.Optional(Type.String({ description: "Wrapper launch id for electron.probe follow-up targeting.", minLength: 1 })),
|
|
485
|
+
timeoutMs: Type.Optional(Type.Integer({ description: "Bounded probe timeout in milliseconds.", minimum: 1 })),
|
|
486
|
+
}, { additionalProperties: false }),
|
|
487
|
+
], { description: "Electron wrapper action. Fields are action-specific and unsupported fields are rejected." }),
|
|
488
|
+
),
|
|
317
489
|
job: Type.Optional(
|
|
318
490
|
Type.Object({
|
|
319
491
|
steps: Type.Array(
|
|
@@ -333,7 +505,7 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
333
505
|
),
|
|
334
506
|
}),
|
|
335
507
|
),
|
|
336
|
-
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." })),
|
|
508
|
+
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. Do not use with electron mode." })),
|
|
337
509
|
sessionMode: Type.Optional(
|
|
338
510
|
StringEnum(["auto", "fresh"] as const, {
|
|
339
511
|
description:
|
|
@@ -506,10 +678,18 @@ function compileAgentBrowserQaPreset(input: unknown): { compiled?: CompiledAgent
|
|
|
506
678
|
if (!isRecord(input)) {
|
|
507
679
|
return { error: "qa must be an object." };
|
|
508
680
|
}
|
|
681
|
+
const attached = input.attached === true;
|
|
682
|
+
if (input.attached !== undefined && typeof input.attached !== "boolean") {
|
|
683
|
+
return { error: "qa.attached must be a boolean when provided." };
|
|
684
|
+
}
|
|
509
685
|
const url = input.url;
|
|
510
|
-
if (
|
|
686
|
+
if (attached && url !== undefined) {
|
|
687
|
+
return { error: "qa.url must be omitted when qa.attached is true." };
|
|
688
|
+
}
|
|
689
|
+
if (!attached && (typeof url !== "string" || url.trim().length === 0)) {
|
|
511
690
|
return { error: "qa.url must be a non-empty string." };
|
|
512
691
|
}
|
|
692
|
+
const normalizedUrl = typeof url === "string" ? url.trim() : undefined;
|
|
513
693
|
const expectedText = input.expectedText === undefined
|
|
514
694
|
? []
|
|
515
695
|
: typeof input.expectedText === "string"
|
|
@@ -545,10 +725,8 @@ function compileAgentBrowserQaPreset(input: unknown): { compiled?: CompiledAgent
|
|
|
545
725
|
if (checkNetwork) steps.push({ action: "wait", args: ["network", "requests", "--clear"] });
|
|
546
726
|
if (checkConsole) steps.push({ action: "wait", args: ["console", "--clear"] });
|
|
547
727
|
if (checkErrors) steps.push({ action: "wait", args: ["errors", "--clear"] });
|
|
548
|
-
steps.push(
|
|
549
|
-
|
|
550
|
-
{ action: "wait", args: ["wait", "--load", loadState] },
|
|
551
|
-
);
|
|
728
|
+
if (!attached && normalizedUrl) steps.push({ action: "open", args: ["open", normalizedUrl] });
|
|
729
|
+
steps.push({ action: "wait", args: ["wait", "--load", loadState] });
|
|
552
730
|
for (const text of expectedText) {
|
|
553
731
|
steps.push({ action: "assertText", args: ["wait", "--text", text] });
|
|
554
732
|
}
|
|
@@ -562,7 +740,7 @@ function compileAgentBrowserQaPreset(input: unknown): { compiled?: CompiledAgent
|
|
|
562
740
|
return {
|
|
563
741
|
compiled: {
|
|
564
742
|
args: ["batch"],
|
|
565
|
-
checks: { checkConsole, checkErrors, checkNetwork, expectedSelector, expectedText, loadState, screenshotPath, url },
|
|
743
|
+
checks: { attached, checkConsole, checkErrors, checkNetwork, expectedSelector, expectedText, loadState, screenshotPath, url: normalizedUrl },
|
|
566
744
|
stdin: JSON.stringify(steps.map((step) => step.args)),
|
|
567
745
|
steps,
|
|
568
746
|
},
|
|
@@ -778,7 +956,12 @@ function validateLookupMaxWorkspaceFiles(value: unknown, fieldName: string): { v
|
|
|
778
956
|
return { value };
|
|
779
957
|
}
|
|
780
958
|
|
|
781
|
-
async function analyzeSourceLookupResults(
|
|
959
|
+
async function analyzeSourceLookupResults(
|
|
960
|
+
data: unknown,
|
|
961
|
+
compiled: CompiledAgentBrowserSourceLookup,
|
|
962
|
+
cwd: string,
|
|
963
|
+
context?: AgentBrowserSourceLookupAnalysisContext,
|
|
964
|
+
): Promise<AgentBrowserSourceLookupAnalysis> {
|
|
782
965
|
const items = getBatchResultItems(data);
|
|
783
966
|
const candidates: AgentBrowserSourceLookupCandidate[] = [];
|
|
784
967
|
const limitations = [
|
|
@@ -799,15 +982,27 @@ async function analyzeSourceLookupResults(data: unknown, compiled: CompiledAgent
|
|
|
799
982
|
}
|
|
800
983
|
await collectWorkspaceComponentCandidates(compiled.query, cwd, candidates, limitations);
|
|
801
984
|
const status: AgentBrowserSourceLookupStatus = candidates.length > 0 ? "candidates-found" : unsupported ? "unsupported" : "no-candidates";
|
|
985
|
+
const electronContext = status === "no-candidates" ? context?.electronContext : undefined;
|
|
986
|
+
const workspaceRoot = context?.workspaceRoot ?? cwd;
|
|
987
|
+
if (electronContext) {
|
|
988
|
+
limitations.push(
|
|
989
|
+
`Workspace source scan is limited to the Pi tool session cwd: ${workspaceRoot}.`,
|
|
990
|
+
"Packaged Electron app code may live inside installed app resources or app.asar outside the workspace; the wrapper does not unpack asar files or scan app bundle resources.",
|
|
991
|
+
);
|
|
992
|
+
}
|
|
802
993
|
return {
|
|
803
994
|
candidates,
|
|
995
|
+
electronContext,
|
|
804
996
|
limitations,
|
|
805
997
|
status,
|
|
806
998
|
summary: candidates.length > 0
|
|
807
999
|
? `Source lookup found ${candidates.length} candidate location(s).`
|
|
808
1000
|
: unsupported
|
|
809
1001
|
? "Source lookup could not inspect React metadata in this session."
|
|
810
|
-
:
|
|
1002
|
+
: electronContext
|
|
1003
|
+
? `Source lookup found no candidate locations. The workspace scan was limited to ${workspaceRoot}; packaged Electron app code may live outside that cwd in app resources or app.asar.`
|
|
1004
|
+
: "Source lookup found no candidate locations.",
|
|
1005
|
+
workspaceRoot: electronContext ? workspaceRoot : undefined,
|
|
811
1006
|
};
|
|
812
1007
|
}
|
|
813
1008
|
|
|
@@ -836,6 +1031,159 @@ function compileAgentBrowserNetworkSourceLookup(input: unknown): { compiled?: Co
|
|
|
836
1031
|
return { compiled: { args, query: { filter, maxWorkspaceFiles: maxWorkspaceFiles.value as number, requestId, session, url }, stdin: JSON.stringify(steps.map((step) => step.args)), steps } };
|
|
837
1032
|
}
|
|
838
1033
|
|
|
1034
|
+
function validateOptionalNonEmptyString(input: Record<string, unknown>, fieldName: string): { value?: string; error?: string } {
|
|
1035
|
+
const value = input[fieldName];
|
|
1036
|
+
if (value === undefined) return {};
|
|
1037
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1038
|
+
return { error: `electron.${fieldName} must be a non-empty string when provided.` };
|
|
1039
|
+
}
|
|
1040
|
+
return { value: value.trim() };
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function validateOptionalElectronStringArray(input: Record<string, unknown>, fieldName: "allow" | "appArgs" | "deny"): string | undefined {
|
|
1044
|
+
const value = input[fieldName];
|
|
1045
|
+
if (value === undefined) return undefined;
|
|
1046
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) {
|
|
1047
|
+
return `electron.${fieldName} must be an array of non-empty strings when provided.`;
|
|
1048
|
+
}
|
|
1049
|
+
return undefined;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function validateOptionalElectronEnum<T extends string>(input: Record<string, unknown>, fieldName: string, values: readonly T[]): string | undefined {
|
|
1053
|
+
const value = input[fieldName];
|
|
1054
|
+
if (value === undefined) return undefined;
|
|
1055
|
+
if (typeof value !== "string" || !values.includes(value as T)) {
|
|
1056
|
+
return `electron.${fieldName} must be one of: ${values.join(", ")}.`;
|
|
1057
|
+
}
|
|
1058
|
+
return undefined;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function getReservedElectronAppArg(appArgs: string[] | undefined): string | undefined {
|
|
1062
|
+
return appArgs?.find((arg) => {
|
|
1063
|
+
const trimmed = arg.trim();
|
|
1064
|
+
return trimmed === "--" || AGENT_BROWSER_ELECTRON_RESERVED_APP_ARGS.some((reserved) => trimmed === reserved || trimmed.startsWith(`${reserved}=`));
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function validateElectronLaunchAppArgs(appArgs: string[] | undefined): string | undefined {
|
|
1069
|
+
const reservedArg = getReservedElectronAppArg(appArgs);
|
|
1070
|
+
return reservedArg
|
|
1071
|
+
? `electron.appArgs must not include wrapper-owned launch flag ${reservedArg}.`
|
|
1072
|
+
: undefined;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function validateOptionalElectronPositiveInteger(input: Record<string, unknown>, fieldName: "maxResults" | "timeoutMs"): { value?: number; error?: string } {
|
|
1076
|
+
const value = input[fieldName];
|
|
1077
|
+
if (value === undefined) return {};
|
|
1078
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
1079
|
+
return { error: `electron.${fieldName} must be a positive integer when provided.` };
|
|
1080
|
+
}
|
|
1081
|
+
return { value };
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function onlyAllowedElectronFields(input: Record<string, unknown>, action: string, allowedFields: ReadonlySet<string>): string | undefined {
|
|
1085
|
+
return Object.keys(input).find((fieldName) => !allowedFields.has(fieldName))
|
|
1086
|
+
? `electron.${action} does not support electron.${Object.keys(input).find((fieldName) => !allowedFields.has(fieldName))}.`
|
|
1087
|
+
: undefined;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function compileAgentBrowserElectron(input: unknown): { compiled?: CompiledAgentBrowserElectron; error?: string } {
|
|
1091
|
+
if (!isRecord(input)) return { error: "electron must be an object." };
|
|
1092
|
+
const action = input.action;
|
|
1093
|
+
if (typeof action !== "string" || !AGENT_BROWSER_ELECTRON_ACTIONS.includes(action as AgentBrowserElectronAction)) {
|
|
1094
|
+
return { error: `electron.action must be one of: ${AGENT_BROWSER_ELECTRON_ACTIONS.join(", ")}.` };
|
|
1095
|
+
}
|
|
1096
|
+
for (const fieldName of ["query", "appPath", "appName", "bundleId", "executablePath", "launchId"] as const) {
|
|
1097
|
+
const validation = validateOptionalNonEmptyString(input, fieldName);
|
|
1098
|
+
if (validation.error) return { error: validation.error };
|
|
1099
|
+
}
|
|
1100
|
+
for (const fieldName of ["appArgs", "allow", "deny"] as const) {
|
|
1101
|
+
const error = validateOptionalElectronStringArray(input, fieldName);
|
|
1102
|
+
if (error) return { error };
|
|
1103
|
+
}
|
|
1104
|
+
const handoffError = validateOptionalElectronEnum(input, "handoff", AGENT_BROWSER_ELECTRON_HANDOFFS);
|
|
1105
|
+
if (handoffError) return { error: handoffError };
|
|
1106
|
+
const targetTypeError = validateOptionalElectronEnum(input, "targetType", AGENT_BROWSER_ELECTRON_TARGET_TYPES);
|
|
1107
|
+
if (targetTypeError) return { error: targetTypeError };
|
|
1108
|
+
for (const fieldName of ["maxResults", "timeoutMs"] as const) {
|
|
1109
|
+
const validation = validateOptionalElectronPositiveInteger(input, fieldName);
|
|
1110
|
+
if (validation.error) return { error: validation.error };
|
|
1111
|
+
}
|
|
1112
|
+
if (input.all !== undefined && input.all !== true) {
|
|
1113
|
+
return { error: "electron.all must be true when provided." };
|
|
1114
|
+
}
|
|
1115
|
+
if (action === "list") {
|
|
1116
|
+
const unsupportedListField = Object.keys(input).find((fieldName) => !AGENT_BROWSER_ELECTRON_LIST_FIELDS.has(fieldName));
|
|
1117
|
+
if (unsupportedListField) {
|
|
1118
|
+
return { error: `electron.list only supports query and maxResults; remove electron.${unsupportedListField}.` };
|
|
1119
|
+
}
|
|
1120
|
+
return {
|
|
1121
|
+
compiled: {
|
|
1122
|
+
action: "list",
|
|
1123
|
+
maxResults: validateOptionalElectronPositiveInteger(input, "maxResults").value,
|
|
1124
|
+
query: validateOptionalNonEmptyString(input, "query").value,
|
|
1125
|
+
},
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
if (action === "probe") {
|
|
1129
|
+
const unsupportedProbeField = Object.keys(input).find((fieldName) => !AGENT_BROWSER_ELECTRON_PROBE_FIELDS.has(fieldName));
|
|
1130
|
+
if (unsupportedProbeField) {
|
|
1131
|
+
return { error: `electron.probe only supports action, launchId, and timeoutMs; remove electron.${unsupportedProbeField}.` };
|
|
1132
|
+
}
|
|
1133
|
+
const launchId = validateOptionalNonEmptyString(input, "launchId").value;
|
|
1134
|
+
const timeoutMs = validateOptionalElectronPositiveInteger(input, "timeoutMs").value;
|
|
1135
|
+
return {
|
|
1136
|
+
compiled: {
|
|
1137
|
+
action: "probe",
|
|
1138
|
+
...(launchId ? { launchId } : {}),
|
|
1139
|
+
...(timeoutMs ? { timeoutMs } : {}),
|
|
1140
|
+
},
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
if (action === "launch") {
|
|
1144
|
+
const allowedFields = new Set(["action", "allow", "appArgs", "appName", "appPath", "bundleId", "deny", "executablePath", "handoff", "targetType", "timeoutMs"]);
|
|
1145
|
+
const unsupportedFieldError = onlyAllowedElectronFields(input, action, allowedFields);
|
|
1146
|
+
if (unsupportedFieldError) return { error: unsupportedFieldError };
|
|
1147
|
+
const appArgs = (input.appArgs as string[] | undefined)?.map((item) => item.trim());
|
|
1148
|
+
const appArgsError = validateElectronLaunchAppArgs(appArgs);
|
|
1149
|
+
if (appArgsError) return { error: appArgsError };
|
|
1150
|
+
const targetFields = ["appPath", "appName", "bundleId", "executablePath"] as const;
|
|
1151
|
+
const providedTargets = targetFields.filter((fieldName) => input[fieldName] !== undefined);
|
|
1152
|
+
if (providedTargets.length !== 1) {
|
|
1153
|
+
return { error: "electron.launch requires exactly one of appPath, appName, bundleId, or executablePath." };
|
|
1154
|
+
}
|
|
1155
|
+
return {
|
|
1156
|
+
compiled: {
|
|
1157
|
+
action: "launch",
|
|
1158
|
+
allow: (input.allow as string[] | undefined)?.map((item) => item.trim()),
|
|
1159
|
+
appArgs,
|
|
1160
|
+
deny: (input.deny as string[] | undefined)?.map((item) => item.trim()),
|
|
1161
|
+
appName: validateOptionalNonEmptyString(input, "appName").value,
|
|
1162
|
+
appPath: validateOptionalNonEmptyString(input, "appPath").value,
|
|
1163
|
+
bundleId: validateOptionalNonEmptyString(input, "bundleId").value,
|
|
1164
|
+
executablePath: validateOptionalNonEmptyString(input, "executablePath").value,
|
|
1165
|
+
handoff: (input.handoff as "connect" | "snapshot" | "tabs" | undefined) ?? "snapshot",
|
|
1166
|
+
targetType: (input.targetType as "any" | "page" | "webview" | undefined) ?? "page",
|
|
1167
|
+
timeoutMs: validateOptionalElectronPositiveInteger(input, "timeoutMs").value,
|
|
1168
|
+
},
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
const allowedFields = new Set(["action", "all", "launchId", "timeoutMs"]);
|
|
1172
|
+
const unsupportedFieldError = onlyAllowedElectronFields(input, action, allowedFields);
|
|
1173
|
+
if (unsupportedFieldError) return { error: unsupportedFieldError };
|
|
1174
|
+
if (input.all === true && input.launchId !== undefined) {
|
|
1175
|
+
return { error: `electron.${action} accepts launchId or all, not both.` };
|
|
1176
|
+
}
|
|
1177
|
+
return {
|
|
1178
|
+
compiled: {
|
|
1179
|
+
action: action as "cleanup" | "status",
|
|
1180
|
+
all: input.all === true || undefined,
|
|
1181
|
+
launchId: validateOptionalNonEmptyString(input, "launchId").value,
|
|
1182
|
+
timeoutMs: validateOptionalElectronPositiveInteger(input, "timeoutMs").value,
|
|
1183
|
+
},
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
839
1187
|
function getResultPayload(item: Record<string, unknown>): unknown {
|
|
840
1188
|
return isRecord(item.result) && "data" in item.result ? item.result.data : item.result;
|
|
841
1189
|
}
|
|
@@ -1275,8 +1623,15 @@ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: Compile
|
|
|
1275
1623
|
if (typeof locator !== "string" || !AGENT_BROWSER_SEMANTIC_LOCATORS.includes(locator as AgentBrowserSemanticLocator)) {
|
|
1276
1624
|
return { error: `semanticAction.locator must be one of: ${AGENT_BROWSER_SEMANTIC_LOCATORS.join(", ")}.` };
|
|
1277
1625
|
}
|
|
1278
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1279
|
-
return { error: "semanticAction.value must be a non-empty string." };
|
|
1626
|
+
if (value !== undefined && (typeof value !== "string" || value.trim().length === 0)) {
|
|
1627
|
+
return { error: "semanticAction.value must be a non-empty string when provided." };
|
|
1628
|
+
}
|
|
1629
|
+
if (role !== undefined && (typeof role !== "string" || role.trim().length === 0)) {
|
|
1630
|
+
return { error: "semanticAction.role must be a non-empty string when provided." };
|
|
1631
|
+
}
|
|
1632
|
+
const locatorValue = locator === "role" && typeof role === "string" ? role : value;
|
|
1633
|
+
if (typeof locatorValue !== "string" || locatorValue.trim().length === 0) {
|
|
1634
|
+
return { error: locator === "role" ? "semanticAction.value or semanticAction.role must be a non-empty string for locator=role." : "semanticAction.value must be a non-empty string." };
|
|
1280
1635
|
}
|
|
1281
1636
|
if (text !== undefined && typeof text !== "string") {
|
|
1282
1637
|
return { error: "semanticAction.text must be a string when provided." };
|
|
@@ -1287,13 +1642,16 @@ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: Compile
|
|
|
1287
1642
|
if (action !== "fill" && text !== undefined) {
|
|
1288
1643
|
return { error: "semanticAction.text is only supported for fill actions." };
|
|
1289
1644
|
}
|
|
1290
|
-
if (role !== undefined &&
|
|
1291
|
-
return { error: "semanticAction.role is only supported for locator=role
|
|
1645
|
+
if (role !== undefined && locator !== "role") {
|
|
1646
|
+
return { error: "semanticAction.role is only supported for locator=role." };
|
|
1647
|
+
}
|
|
1648
|
+
if (role !== undefined && value !== undefined && role !== value) {
|
|
1649
|
+
return { error: "semanticAction.role must match value when both are provided for locator=role." };
|
|
1292
1650
|
}
|
|
1293
1651
|
if (name !== undefined && (locator !== "role" || typeof name !== "string" || name.length === 0)) {
|
|
1294
1652
|
return { error: "semanticAction.name is only supported as a non-empty string for locator=role." };
|
|
1295
1653
|
}
|
|
1296
|
-
const args = typeof session === "string" ? ["--session", session, "find", locator,
|
|
1654
|
+
const args = typeof session === "string" ? ["--session", session, "find", locator, locatorValue, action] : ["find", locator, locatorValue, action];
|
|
1297
1655
|
if (action === "fill") {
|
|
1298
1656
|
args.push(text as string);
|
|
1299
1657
|
}
|
|
@@ -1381,10 +1739,13 @@ function formatAgentBrowserRenderCall(args: unknown, theme: Theme): string {
|
|
|
1381
1739
|
const qa = compileAgentBrowserQaPreset(input.qa);
|
|
1382
1740
|
const sourceLookup = compileAgentBrowserSourceLookup(input.sourceLookup);
|
|
1383
1741
|
const networkSourceLookup = compileAgentBrowserNetworkSourceLookup(input.networkSourceLookup);
|
|
1742
|
+
const electron = compileAgentBrowserElectron(input.electron);
|
|
1384
1743
|
const generatedBatch = networkSourceLookup.compiled ?? sourceLookup.compiled ?? job.compiled ?? qa.compiled;
|
|
1385
1744
|
const rawArgs = Array.isArray(input.args)
|
|
1386
1745
|
? input.args.filter((value): value is string => typeof value === "string")
|
|
1387
|
-
:
|
|
1746
|
+
: electron.compiled
|
|
1747
|
+
? ["electron", electron.compiled.action]
|
|
1748
|
+
: (semanticAction.compiled?.args ?? generatedBatch?.args ?? []);
|
|
1388
1749
|
const redactedArgs = redactInvocationArgs(rawArgs);
|
|
1389
1750
|
const invocation = sanitizeDisplayText(redactedArgs.join(" ")).replace(/\s+/g, " ").trim();
|
|
1390
1751
|
const invocationPreview =
|
|
@@ -1736,6 +2097,22 @@ interface SelectorTextVisibilityDiagnostic {
|
|
|
1736
2097
|
visibleCount: number;
|
|
1737
2098
|
}
|
|
1738
2099
|
|
|
2100
|
+
interface ElectronBroadGetTextScopeDiagnostic {
|
|
2101
|
+
electronContext: {
|
|
2102
|
+
launchId?: string;
|
|
2103
|
+
sessionName?: string;
|
|
2104
|
+
url?: string;
|
|
2105
|
+
};
|
|
2106
|
+
selector: string;
|
|
2107
|
+
summary: string;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
interface QaAttachedTarget {
|
|
2111
|
+
sessionName: string;
|
|
2112
|
+
title?: string;
|
|
2113
|
+
url?: string;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
1739
2116
|
interface TimeoutArtifactEvidence {
|
|
1740
2117
|
absolutePath: string;
|
|
1741
2118
|
exists: boolean;
|
|
@@ -2182,14 +2559,16 @@ function shouldCaptureNavigationSummary(command: string | undefined, data: unkno
|
|
|
2182
2559
|
);
|
|
2183
2560
|
}
|
|
2184
2561
|
|
|
2185
|
-
function extractStringResultField(data: unknown, fieldName: "result" | "title" | "url"): string | undefined {
|
|
2562
|
+
function extractStringResultField(data: unknown, fieldName: "result" | "title" | "url" | "value"): string | undefined {
|
|
2186
2563
|
if (typeof data === "string") {
|
|
2564
|
+
if (fieldName === "value") return data;
|
|
2187
2565
|
const text = data.trim();
|
|
2188
2566
|
return text.length > 0 ? text : undefined;
|
|
2189
2567
|
}
|
|
2190
2568
|
if (!isRecord(data) || typeof data[fieldName] !== "string") {
|
|
2191
2569
|
return undefined;
|
|
2192
2570
|
}
|
|
2571
|
+
if (fieldName === "value") return data[fieldName];
|
|
2193
2572
|
const text = data[fieldName].trim();
|
|
2194
2573
|
return text.length > 0 ? text : undefined;
|
|
2195
2574
|
}
|
|
@@ -2555,116 +2934,1112 @@ function redactToolDetails(details: Record<string, unknown>, sensitiveValues: st
|
|
|
2555
2934
|
return redactSensitiveValue(redactExactSensitiveValue(details, sensitiveValues)) as Record<string, unknown>;
|
|
2556
2935
|
}
|
|
2557
2936
|
|
|
2558
|
-
function
|
|
2559
|
-
|
|
2560
|
-
|
|
2937
|
+
function formatElectronListVisibleText(result: ElectronDiscoveryResult): string {
|
|
2938
|
+
const visibleApps = result.apps.slice(0, 10);
|
|
2939
|
+
const visibleOmittedCount = Math.max(0, result.apps.length - visibleApps.length);
|
|
2940
|
+
const header = result.omittedCount > 0
|
|
2941
|
+
? `Electron apps (${result.apps.length} shown, ${result.omittedCount} omitted):`
|
|
2942
|
+
: `Electron apps (${result.apps.length} found):`;
|
|
2943
|
+
const lines = [header];
|
|
2944
|
+
if (visibleApps.length === 0) {
|
|
2945
|
+
lines.push(result.query ? `No Electron apps matched query "${result.query}".` : "No Electron apps found in the supported scan locations.");
|
|
2946
|
+
} else {
|
|
2947
|
+
for (const app of visibleApps) {
|
|
2948
|
+
const identifier = app.bundleId ?? app.desktopId;
|
|
2949
|
+
const path = app.appPath ?? app.executablePath;
|
|
2950
|
+
const sensitivity = app.sensitivity ? ` [likely sensitive: ${app.sensitivity.categories.join(", ")}]` : "";
|
|
2951
|
+
lines.push(`- ${app.name}${identifier ? ` (${identifier})` : ""}${sensitivity} — ${path}`);
|
|
2952
|
+
}
|
|
2561
2953
|
}
|
|
2562
|
-
if (
|
|
2563
|
-
|
|
2954
|
+
if (visibleOmittedCount > 0) {
|
|
2955
|
+
lines.push(`${visibleOmittedCount} additional app(s) omitted from visible output; see details.electron.apps.`);
|
|
2564
2956
|
}
|
|
2565
|
-
if (
|
|
2566
|
-
|
|
2957
|
+
if (result.omittedCount > 0) {
|
|
2958
|
+
lines.push(`${result.omittedCount} app(s) omitted by maxResults=${result.maxResults}.`);
|
|
2567
2959
|
}
|
|
2568
|
-
if (
|
|
2569
|
-
|
|
2960
|
+
if (result.apps.some((app) => app.sensitivity?.level === "likely-sensitive")) {
|
|
2961
|
+
lines.push("Review likely-sensitive apps and use caller-owned allow/deny policy before launch.");
|
|
2962
|
+
lines.push(ELECTRON_PROFILE_ISOLATION_NOTE);
|
|
2963
|
+
lines.push(ELECTRON_EXISTING_AUTH_GUIDANCE);
|
|
2570
2964
|
}
|
|
2571
|
-
|
|
2572
|
-
return `agent_browser stdin is only supported for \`batch\`, \`eval --stdin\`, and \`auth save --password-stdin\`; remove stdin from ${commandLabel} or use one of those command forms.`;
|
|
2965
|
+
return lines.join("\n");
|
|
2573
2966
|
}
|
|
2574
2967
|
|
|
2575
|
-
function
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2968
|
+
function buildElectronListSuccessResult(compiledElectron: CompiledAgentBrowserElectron, discovery: ElectronDiscoveryResult): AgentBrowserToolResult {
|
|
2969
|
+
const text = redactSensitiveText(formatElectronListVisibleText(discovery));
|
|
2970
|
+
const sensitiveAppCount = discovery.apps.filter((app) => app.sensitivity?.level === "likely-sensitive").length;
|
|
2971
|
+
const details = {
|
|
2972
|
+
args: [] as string[],
|
|
2973
|
+
compiledElectron,
|
|
2974
|
+
electron: {
|
|
2975
|
+
action: "list" as const,
|
|
2976
|
+
apps: discovery.apps,
|
|
2977
|
+
maxResults: discovery.maxResults,
|
|
2978
|
+
omittedCount: discovery.omittedCount || undefined,
|
|
2979
|
+
platform: discovery.platform,
|
|
2980
|
+
profileIsolation: ELECTRON_PROFILE_ISOLATION_DETAILS,
|
|
2981
|
+
query: discovery.query,
|
|
2982
|
+
sensitiveAppCount: sensitiveAppCount || undefined,
|
|
2983
|
+
skippedCount: discovery.skippedCount,
|
|
2984
|
+
status: "succeeded" as const,
|
|
2985
|
+
},
|
|
2986
|
+
...buildAgentBrowserResultCategoryDetails({ args: [], succeeded: true }),
|
|
2987
|
+
summary: discovery.omittedCount > 0
|
|
2988
|
+
? `Electron app discovery found ${discovery.apps.length} app(s) and omitted ${discovery.omittedCount}.`
|
|
2989
|
+
: `Electron app discovery found ${discovery.apps.length} app(s).`,
|
|
2990
|
+
};
|
|
2991
|
+
return {
|
|
2992
|
+
content: [{ type: "text", text }],
|
|
2993
|
+
details: redactToolDetails(details, []),
|
|
2994
|
+
isError: false,
|
|
2995
|
+
};
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
function buildElectronListFailureResult(compiledElectron: CompiledAgentBrowserElectron | undefined, error: unknown): AgentBrowserToolResult {
|
|
2999
|
+
const errorText = error instanceof Error ? error.message : String(error);
|
|
3000
|
+
const text = redactSensitiveText(`Electron app discovery failed: ${errorText}`);
|
|
3001
|
+
const details = {
|
|
3002
|
+
args: [] as string[],
|
|
3003
|
+
compiledElectron,
|
|
3004
|
+
electron: {
|
|
3005
|
+
action: "list" as const,
|
|
3006
|
+
error: errorText,
|
|
3007
|
+
status: "failed" as const,
|
|
3008
|
+
},
|
|
3009
|
+
...buildAgentBrowserResultCategoryDetails({ args: [], errorText, succeeded: false }),
|
|
3010
|
+
summary: "Electron app discovery failed.",
|
|
3011
|
+
};
|
|
3012
|
+
return {
|
|
3013
|
+
content: [{ type: "text", text }],
|
|
3014
|
+
details: redactToolDetails(details, []),
|
|
3015
|
+
isError: true,
|
|
3016
|
+
};
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
interface ElectronHandoffSummary {
|
|
3020
|
+
error?: string;
|
|
3021
|
+
handoff: "connect" | "snapshot" | "tabs";
|
|
3022
|
+
refSnapshot?: SessionRefSnapshot;
|
|
3023
|
+
snapshot?: unknown;
|
|
3024
|
+
snapshotRetryCount?: number;
|
|
3025
|
+
tabs?: unknown;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
function isElectronLaunchRecord(value: unknown): value is ElectronLaunchRecord {
|
|
3029
|
+
if (!isRecord(value)) return false;
|
|
3030
|
+
return value.version === 1 &&
|
|
3031
|
+
value.launchedByWrapper === true &&
|
|
3032
|
+
typeof value.launchId === "string" &&
|
|
3033
|
+
typeof value.appName === "string" &&
|
|
3034
|
+
typeof value.executablePath === "string" &&
|
|
3035
|
+
typeof value.userDataDir === "string" &&
|
|
3036
|
+
typeof value.port === "number" &&
|
|
3037
|
+
typeof value.createdAtMs === "number";
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
function restoreElectronLaunchRecordsFromBranch(branch: unknown[]): Map<string, ElectronLaunchRecord> {
|
|
3041
|
+
const records = new Map<string, ElectronLaunchRecord>();
|
|
3042
|
+
for (const entry of branch) {
|
|
3043
|
+
if (!isRecord(entry) || entry.type !== "message") continue;
|
|
3044
|
+
const message = isRecord(entry.message) ? entry.message : undefined;
|
|
3045
|
+
if (!message || message.toolName !== "agent_browser") continue;
|
|
3046
|
+
const details = isRecord(message.details) ? message.details : undefined;
|
|
3047
|
+
const electron = isRecord(details?.electron) ? details.electron : undefined;
|
|
3048
|
+
if (!electron) continue;
|
|
3049
|
+
const launch = isElectronLaunchRecord(electron.launch) ? electron.launch : undefined;
|
|
3050
|
+
if (launch) records.set(launch.launchId, launch);
|
|
3051
|
+
const cleanupRecords = isRecord(electron.cleanup) && Array.isArray(electron.cleanup.records) ? electron.cleanup.records : [];
|
|
3052
|
+
for (const cleanupRecord of cleanupRecords) {
|
|
3053
|
+
if (isElectronLaunchRecord(cleanupRecord)) records.set(cleanupRecord.launchId, cleanupRecord);
|
|
3054
|
+
}
|
|
2581
3055
|
}
|
|
2582
|
-
|
|
2583
|
-
|
|
3056
|
+
return records;
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
function getActiveElectronRecords(records: Map<string, ElectronLaunchRecord>): ElectronLaunchRecord[] {
|
|
3060
|
+
return [...records.values()].filter((record) => record.cleanupState === "active" || record.cleanupState === "dead" || record.cleanupState === "partial" || record.cleanupState === "failed");
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
function selectElectronRecords(compiledElectron: Extract<CompiledAgentBrowserElectron, { action: "cleanup" | "status" }>, records: Map<string, ElectronLaunchRecord>): { error?: string; records?: ElectronLaunchRecord[] } {
|
|
3064
|
+
if (compiledElectron.launchId) {
|
|
3065
|
+
const record = records.get(compiledElectron.launchId);
|
|
3066
|
+
return record ? { records: [record] } : { error: `No wrapper-tracked Electron launch found for launchId ${compiledElectron.launchId}.` };
|
|
2584
3067
|
}
|
|
2585
|
-
return
|
|
3068
|
+
if (compiledElectron.all) return { records: getActiveElectronRecords(records) };
|
|
3069
|
+
const activeRecords = getActiveElectronRecords(records);
|
|
3070
|
+
if (activeRecords.length === 0) return { records: [] };
|
|
3071
|
+
if (activeRecords.length > 1) return { error: "Multiple wrapper-tracked Electron launches are active; pass electron.launchId or electron.all." };
|
|
3072
|
+
return { records: activeRecords };
|
|
2586
3073
|
}
|
|
2587
3074
|
|
|
2588
|
-
function
|
|
3075
|
+
function formatElectronTargetLines(targets: ElectronCdpTarget[], limit = 8): string[] {
|
|
3076
|
+
const shownTargets = targets.slice(0, limit);
|
|
3077
|
+
const lines = shownTargets.map((target) => {
|
|
3078
|
+
const label = [target.type, target.title].filter(Boolean).join(" ") || target.id || "target";
|
|
3079
|
+
return `- ${label}${target.url ? ` — ${target.url}` : ""}`;
|
|
3080
|
+
});
|
|
3081
|
+
if (targets.length > shownTargets.length) lines.push(`- ... ${targets.length - shownTargets.length} more target(s) omitted`);
|
|
3082
|
+
return lines;
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
function extractTargetsFromStatus(statuses: ElectronLaunchStatus[]): ElectronCdpTarget[] {
|
|
3086
|
+
return statuses.flatMap((status) => status.targets);
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
interface ElectronManagedSessionTarget {
|
|
3090
|
+
error?: string;
|
|
3091
|
+
sessionName: string;
|
|
3092
|
+
title?: string;
|
|
3093
|
+
url?: string;
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
type ElectronSessionMismatchReason =
|
|
3097
|
+
| "launch-session-not-current"
|
|
3098
|
+
| "managed-session-about-blank-while-launch-target-live"
|
|
3099
|
+
| "managed-session-target-not-in-launch-status";
|
|
3100
|
+
|
|
3101
|
+
interface ElectronSessionMismatch {
|
|
3102
|
+
launchId: string;
|
|
3103
|
+
liveTarget?: ElectronCdpTarget;
|
|
3104
|
+
managedSession: ElectronManagedSessionTarget;
|
|
3105
|
+
nextActionIds: string[];
|
|
3106
|
+
reason: ElectronSessionMismatchReason;
|
|
3107
|
+
sessionName?: string;
|
|
3108
|
+
statusTargets: ElectronCdpTarget[];
|
|
3109
|
+
summary: string;
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
type ElectronPostCommandHealthReason = "about-blank-no-live-target" | "debug-port-dead" | "process-dead";
|
|
3113
|
+
|
|
3114
|
+
interface ElectronPostCommandHealthDiagnostic {
|
|
3115
|
+
appName: string;
|
|
2589
3116
|
command?: string;
|
|
2590
|
-
|
|
2591
|
-
|
|
3117
|
+
launchId: string;
|
|
3118
|
+
nextActionIds: string[];
|
|
3119
|
+
reason: ElectronPostCommandHealthReason;
|
|
2592
3120
|
sessionName?: string;
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
options.pinningRequired === true &&
|
|
2597
|
-
options.sessionName !== undefined &&
|
|
2598
|
-
options.command !== undefined &&
|
|
2599
|
-
!SESSION_TAB_PINNING_EXCLUDED_COMMANDS.has(options.command) &&
|
|
2600
|
-
supportsPinnedStdinCommand(options)
|
|
2601
|
-
);
|
|
3121
|
+
status: ElectronLaunchStatus;
|
|
3122
|
+
summary: string;
|
|
3123
|
+
target?: SessionTabTarget;
|
|
2602
3124
|
}
|
|
2603
3125
|
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
return {
|
|
2612
|
-
ok: false,
|
|
2613
|
-
error: `agent_browser batch stdin step ${index} must be a non-empty array of string command tokens.`,
|
|
2614
|
-
};
|
|
2615
|
-
}
|
|
2616
|
-
if (step.length === 0) {
|
|
2617
|
-
return {
|
|
2618
|
-
ok: false,
|
|
2619
|
-
error: `agent_browser batch stdin step ${index} must not be empty.`,
|
|
2620
|
-
};
|
|
2621
|
-
}
|
|
2622
|
-
const invalidTokenIndex = step.findIndex((token) => typeof token !== "string");
|
|
2623
|
-
if (invalidTokenIndex !== -1) {
|
|
2624
|
-
return {
|
|
2625
|
-
ok: false,
|
|
2626
|
-
error: `agent_browser batch stdin step ${index} token ${invalidTokenIndex} must be a string.`,
|
|
2627
|
-
};
|
|
2628
|
-
}
|
|
2629
|
-
return { ok: true, step: step as BatchCommandStep };
|
|
3126
|
+
interface FillVerificationDiagnostic {
|
|
3127
|
+
actual?: string;
|
|
3128
|
+
expected: string;
|
|
3129
|
+
nextActionIds: string[];
|
|
3130
|
+
selector: string;
|
|
3131
|
+
status: "mismatch";
|
|
3132
|
+
summary: string;
|
|
2630
3133
|
}
|
|
2631
3134
|
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
3135
|
+
interface ElectronRefFreshnessDiagnostic {
|
|
3136
|
+
command?: string;
|
|
3137
|
+
launchId: string;
|
|
3138
|
+
nextActionIds: string[];
|
|
3139
|
+
sessionName?: string;
|
|
3140
|
+
summary: string;
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
interface ElectronProbeContext {
|
|
3144
|
+
launchId?: string;
|
|
3145
|
+
mode: "current-managed-session" | "launchId";
|
|
3146
|
+
note?: string;
|
|
3147
|
+
sessionName: string;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
function isLiveElectronRendererTarget(target: ElectronCdpTarget): boolean {
|
|
3151
|
+
const normalizedUrl = normalizeComparableUrl(target.url);
|
|
3152
|
+
if (!normalizedUrl || normalizedUrl === "about:blank" || normalizedUrl.startsWith("devtools://")) return false;
|
|
3153
|
+
return target.type === undefined || target.type === "page" || target.type === "webview";
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
function getLiveElectronRendererTargets(targets: ElectronCdpTarget[]): ElectronCdpTarget[] {
|
|
3157
|
+
return targets.filter(isLiveElectronRendererTarget);
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
function electronTargetLabel(target: ElectronCdpTarget | undefined): string {
|
|
3161
|
+
if (!target) return "unknown target";
|
|
3162
|
+
return [target.title, target.url, target.id].find((value) => typeof value === "string" && value.trim().length > 0) ?? "unknown target";
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
function findElectronLaunchRecordForSession(sessionName: string | undefined, records: Map<string, ElectronLaunchRecord>): ElectronLaunchRecord | undefined {
|
|
3166
|
+
if (!sessionName) return undefined;
|
|
3167
|
+
return getActiveElectronRecords(records).find((record) => record.sessionName === sessionName);
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
function findUnambiguousActiveElectronLaunchRecord(records: Map<string, ElectronLaunchRecord>): ElectronLaunchRecord | undefined {
|
|
3171
|
+
const activeRecords = getActiveElectronRecords(records);
|
|
3172
|
+
return activeRecords.length === 1 ? activeRecords[0] : undefined;
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
function buildElectronReattachNextAction(record: ElectronLaunchRecord, liveTarget?: ElectronCdpTarget): AgentBrowserNextAction {
|
|
3176
|
+
const endpoint = liveTarget?.webSocketDebuggerUrl ?? record.webSocketDebuggerUrl ?? String(record.port);
|
|
3177
|
+
return {
|
|
3178
|
+
id: "reattach-electron-launch",
|
|
3179
|
+
params: { args: ["connect", endpoint], sessionMode: "fresh" },
|
|
3180
|
+
reason: "Attach a fresh managed session to the same wrapper-tracked Electron debug endpoint when the current session no longer matches the live renderer.",
|
|
3181
|
+
safety: "Creates a new managed browser session; it does not mutate the Electron app. Keep the launchId for later status and cleanup.",
|
|
3182
|
+
tool: "agent_browser",
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
function appendUniqueNextActions(target: AgentBrowserNextAction[], additions: AgentBrowserNextAction[]): AgentBrowserNextAction[] {
|
|
3187
|
+
const existingIds = new Set(target.map((action) => action.id));
|
|
3188
|
+
for (const action of additions) {
|
|
3189
|
+
if (existingIds.has(action.id)) continue;
|
|
3190
|
+
target.push(action);
|
|
3191
|
+
existingIds.add(action.id);
|
|
3192
|
+
}
|
|
3193
|
+
return target;
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
function buildElectronMismatchNextActions(record: ElectronLaunchRecord, liveTarget?: ElectronCdpTarget): AgentBrowserNextAction[] {
|
|
3197
|
+
const baseActions = buildAgentBrowserNextActions({
|
|
3198
|
+
electron: { launchId: record.launchId, sessionName: record.sessionName, status: record.cleanupState },
|
|
3199
|
+
resultCategory: "success",
|
|
3200
|
+
successCategory: "completed",
|
|
3201
|
+
}) ?? [];
|
|
3202
|
+
const reattachAction = buildElectronReattachNextAction(record, liveTarget);
|
|
3203
|
+
const actions: AgentBrowserNextAction[] = [];
|
|
3204
|
+
for (const action of baseActions) {
|
|
3205
|
+
actions.push(action);
|
|
3206
|
+
if (action.id === "probe-electron-launch") actions.push(reattachAction);
|
|
3207
|
+
}
|
|
3208
|
+
if (!actions.some((action) => action.id === reattachAction.id)) actions.push(reattachAction);
|
|
3209
|
+
return actions;
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
function buildElectronSessionMismatch(options: {
|
|
3213
|
+
managedSession: ElectronManagedSessionTarget;
|
|
3214
|
+
record: ElectronLaunchRecord;
|
|
3215
|
+
statusTargets: ElectronCdpTarget[];
|
|
3216
|
+
}): ElectronSessionMismatch | undefined {
|
|
3217
|
+
const liveTargets = getLiveElectronRendererTargets(options.statusTargets);
|
|
3218
|
+
if (liveTargets.length === 0) return undefined;
|
|
3219
|
+
const managedUrl = normalizeComparableUrl(options.managedSession.url);
|
|
3220
|
+
const matchingLiveTarget = managedUrl
|
|
3221
|
+
? liveTargets.find((target) => normalizeComparableUrl(target.url) === managedUrl)
|
|
3222
|
+
: undefined;
|
|
3223
|
+
if (matchingLiveTarget) return undefined;
|
|
3224
|
+
|
|
3225
|
+
const liveTarget = liveTargets[0];
|
|
3226
|
+
let reason: ElectronSessionMismatchReason | undefined;
|
|
3227
|
+
if (isAboutBlankUrl(options.managedSession.url)) {
|
|
3228
|
+
reason = "managed-session-about-blank-while-launch-target-live";
|
|
3229
|
+
} else if (options.record.sessionName && options.record.sessionName !== options.managedSession.sessionName) {
|
|
3230
|
+
reason = "launch-session-not-current";
|
|
3231
|
+
} else if (managedUrl) {
|
|
3232
|
+
reason = "managed-session-target-not-in-launch-status";
|
|
3233
|
+
}
|
|
3234
|
+
if (!reason) return undefined;
|
|
3235
|
+
|
|
3236
|
+
const managedDescription = options.managedSession.url ?? options.managedSession.title ?? options.managedSession.sessionName;
|
|
3237
|
+
const liveDescription = electronTargetLabel(liveTarget);
|
|
3238
|
+
const summary = reason === "launch-session-not-current"
|
|
3239
|
+
? `Electron session mismatch: current managed session ${options.managedSession.sessionName} is not the wrapper launch session ${options.record.sessionName ?? "unknown"}, while launch ${options.record.launchId} still has live target ${liveDescription}.`
|
|
3240
|
+
: `Electron session mismatch: managed session ${options.managedSession.sessionName} is on ${managedDescription}, but launch ${options.record.launchId} still has live target ${liveDescription}.`;
|
|
3241
|
+
const nextActions = buildElectronMismatchNextActions(options.record, liveTarget);
|
|
3242
|
+
return {
|
|
3243
|
+
launchId: options.record.launchId,
|
|
3244
|
+
liveTarget,
|
|
3245
|
+
managedSession: options.managedSession,
|
|
3246
|
+
nextActionIds: nextActions.map((action) => action.id),
|
|
3247
|
+
reason,
|
|
3248
|
+
sessionName: options.record.sessionName,
|
|
3249
|
+
statusTargets: options.statusTargets,
|
|
3250
|
+
summary,
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3254
|
+
async function collectManagedSessionCommandData(options: {
|
|
3255
|
+
args: string[];
|
|
3256
|
+
cwd: string;
|
|
3257
|
+
sessionName: string;
|
|
3258
|
+
signal?: AbortSignal;
|
|
3259
|
+
timeoutMs?: number;
|
|
3260
|
+
}): Promise<{ data?: unknown; error?: string }> {
|
|
2636
3261
|
try {
|
|
2637
|
-
|
|
2638
|
-
if (!Array.isArray(parsed)) {
|
|
2639
|
-
return { error: "agent_browser batch stdin must be a JSON array of command steps." };
|
|
2640
|
-
}
|
|
2641
|
-
const steps: BatchCommandStep[] = [];
|
|
2642
|
-
for (const [index, rawStep] of parsed.entries()) {
|
|
2643
|
-
const validated = validateUserBatchStep(rawStep, index);
|
|
2644
|
-
if (!validated.ok) {
|
|
2645
|
-
return { error: validated.error };
|
|
2646
|
-
}
|
|
2647
|
-
steps.push(validated.step);
|
|
2648
|
-
}
|
|
2649
|
-
return { steps };
|
|
3262
|
+
return { data: await runSessionCommandData(options) };
|
|
2650
3263
|
} catch (error) {
|
|
2651
|
-
|
|
2652
|
-
return { error: `agent_browser batch stdin could not be parsed as JSON: ${message}` };
|
|
3264
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
2653
3265
|
}
|
|
2654
3266
|
}
|
|
2655
3267
|
|
|
2656
|
-
|
|
3268
|
+
async function collectElectronManagedSessionTarget(options: {
|
|
3269
|
+
cwd: string;
|
|
3270
|
+
sessionName?: string;
|
|
3271
|
+
signal?: AbortSignal;
|
|
3272
|
+
timeoutMs?: number;
|
|
3273
|
+
}): Promise<ElectronManagedSessionTarget | undefined> {
|
|
3274
|
+
if (!options.sessionName) return undefined;
|
|
3275
|
+
const [titleResult, urlResult] = await Promise.all([
|
|
3276
|
+
collectManagedSessionCommandData({ args: ["get", "title"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, timeoutMs: options.timeoutMs }),
|
|
3277
|
+
collectManagedSessionCommandData({ args: ["get", "url"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, timeoutMs: options.timeoutMs }),
|
|
3278
|
+
]);
|
|
3279
|
+
const title = boundElectronProbeString(extractStringResultField(titleResult.data, "result") ?? extractStringResultField(titleResult.data, "title"), 160);
|
|
3280
|
+
const url = boundElectronProbeString(extractStringResultField(urlResult.data, "result") ?? extractStringResultField(urlResult.data, "url"), 300);
|
|
3281
|
+
const errors = [titleResult.error, urlResult.error].filter((value): value is string => value !== undefined);
|
|
3282
|
+
return { sessionName: options.sessionName, title, url, ...(errors.length > 0 ? { error: errors.join("; ") } : {}) };
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
function formatElectronSessionMismatchText(mismatch: ElectronSessionMismatch): string {
|
|
3286
|
+
return `${mismatch.summary}\nNext: run electron.status/electron.probe with launchId ${mismatch.launchId}, reattach with the reattach-electron-launch nextAction if needed, or cleanup when finished.`;
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
const ELECTRON_POST_COMMAND_HEALTH_COMMANDS = new Set([
|
|
2657
3290
|
"back",
|
|
2658
3291
|
"check",
|
|
2659
3292
|
"click",
|
|
2660
3293
|
"dblclick",
|
|
2661
|
-
"
|
|
3294
|
+
"fill",
|
|
3295
|
+
"find",
|
|
2662
3296
|
"forward",
|
|
2663
|
-
"goto",
|
|
2664
3297
|
"keyboard",
|
|
2665
3298
|
"mouse",
|
|
2666
|
-
"
|
|
2667
|
-
"
|
|
3299
|
+
"press",
|
|
3300
|
+
"reload",
|
|
3301
|
+
"select",
|
|
3302
|
+
"type",
|
|
3303
|
+
"uncheck",
|
|
3304
|
+
]);
|
|
3305
|
+
|
|
3306
|
+
function shouldInspectElectronPostCommandHealth(command: string | undefined): boolean {
|
|
3307
|
+
return command !== undefined && ELECTRON_POST_COMMAND_HEALTH_COMMANDS.has(command);
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
function buildElectronLifecycleNextActions(record: ElectronLaunchRecord): AgentBrowserNextAction[] {
|
|
3311
|
+
return buildAgentBrowserNextActions({
|
|
3312
|
+
electron: { launchId: record.launchId, sessionName: record.sessionName, status: record.cleanupState },
|
|
3313
|
+
resultCategory: "success",
|
|
3314
|
+
successCategory: "completed",
|
|
3315
|
+
}) ?? [];
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
function buildElectronPostCommandHealthDiagnostic(options: {
|
|
3319
|
+
command?: string;
|
|
3320
|
+
record: ElectronLaunchRecord;
|
|
3321
|
+
status: ElectronLaunchStatus;
|
|
3322
|
+
target?: SessionTabTarget;
|
|
3323
|
+
}): ElectronPostCommandHealthDiagnostic | undefined {
|
|
3324
|
+
let reason: ElectronPostCommandHealthReason | undefined;
|
|
3325
|
+
if (options.status.pidAlive === false) reason = "process-dead";
|
|
3326
|
+
else if (!options.status.portAlive) reason = "debug-port-dead";
|
|
3327
|
+
else if (isAboutBlankUrl(options.target?.url) && getLiveElectronRendererTargets(options.status.targets).length === 0) reason = "about-blank-no-live-target";
|
|
3328
|
+
if (!reason) return undefined;
|
|
3329
|
+
const nextActions = buildElectronLifecycleNextActions(options.record);
|
|
3330
|
+
const commandText = options.command ? `${options.command} command` : "command";
|
|
3331
|
+
const statusText = `${options.status.portAlive ? "debug port alive" : "debug port dead"}${options.status.pidAlive === undefined ? "" : options.status.pidAlive ? ", pid alive" : ", pid dead"}`;
|
|
3332
|
+
const summary = `Electron lifecycle warning: ${commandText} completed, but launch ${options.record.launchId} is no longer healthy (${statusText}).`;
|
|
3333
|
+
return {
|
|
3334
|
+
appName: options.record.appName,
|
|
3335
|
+
command: options.command,
|
|
3336
|
+
launchId: options.record.launchId,
|
|
3337
|
+
nextActionIds: nextActions.map((action) => action.id),
|
|
3338
|
+
reason,
|
|
3339
|
+
sessionName: options.record.sessionName,
|
|
3340
|
+
status: options.status,
|
|
3341
|
+
summary,
|
|
3342
|
+
target: options.target,
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
function formatElectronPostCommandHealthText(diagnostic: ElectronPostCommandHealthDiagnostic | undefined): string | undefined {
|
|
3347
|
+
if (!diagnostic) return undefined;
|
|
3348
|
+
const lines = [diagnostic.summary];
|
|
3349
|
+
if (diagnostic.target?.url) lines.push(`Current browser session target: ${diagnostic.target.url}.`);
|
|
3350
|
+
lines.push(`Status: ${diagnostic.status.portAlive ? "debug port alive" : "debug port dead"}${diagnostic.status.pidAlive === undefined ? "" : diagnostic.status.pidAlive ? ", pid alive" : ", pid dead"}; ${diagnostic.status.targets.length} CDP target(s).`);
|
|
3351
|
+
lines.push(`Next: run electron.status/electron.probe with launchId ${diagnostic.launchId}, cleanup the wrapper-owned launch if dead, or relaunch the app.`);
|
|
3352
|
+
return lines.join("\n");
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
function buildElectronIdentifiers(record: ElectronLaunchRecord): { appName: string; launchId: string; sessionName?: string } {
|
|
3356
|
+
return { appName: record.appName, launchId: record.launchId, sessionName: record.sessionName };
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
function formatElectronStatusVisibleText(statuses: ElectronLaunchStatus[], records: ElectronLaunchRecord[], mismatches: ElectronSessionMismatch[] = [], managedSessions: ElectronManagedSessionTarget[] = []): string {
|
|
3360
|
+
if (statuses.length === 0) return "Electron status: no active wrapper-tracked launches.";
|
|
3361
|
+
const recordsByLaunchId = new Map(records.map((record) => [record.launchId, record]));
|
|
3362
|
+
const managedSessionsByName = new Map(managedSessions.map((managedSession) => [managedSession.sessionName, managedSession]));
|
|
3363
|
+
const lines = [`Electron status: ${statuses.length} wrapper-tracked launch(es).`];
|
|
3364
|
+
for (const status of statuses) {
|
|
3365
|
+
const record = recordsByLaunchId.get(status.launchId);
|
|
3366
|
+
const sessionName = record?.sessionName;
|
|
3367
|
+
const appName = record?.appName ?? "Electron launch";
|
|
3368
|
+
const sessionText = sessionName ? `, sessionName ${sessionName}` : "";
|
|
3369
|
+
lines.push(`- ${status.launchId}: ${appName}${sessionText}; ${status.portAlive ? "debug port alive" : "debug port dead"}${status.pidAlive === undefined ? "" : status.pidAlive ? ", pid alive" : ", pid dead"} (port ${status.port})`);
|
|
3370
|
+
lines.push(` Identifiers: launchId ${status.launchId}; sessionName ${sessionName ?? "not attached"}.`);
|
|
3371
|
+
for (const targetLine of formatElectronTargetLines(status.targets, 4)) lines.push(` ${targetLine}`);
|
|
3372
|
+
const managedSession = sessionName ? managedSessionsByName.get(sessionName) : undefined;
|
|
3373
|
+
if (managedSession?.error) lines.push(` Managed session warning: ${managedSession.error}`);
|
|
3374
|
+
}
|
|
3375
|
+
for (const mismatch of mismatches) lines.push("", formatElectronSessionMismatchText(mismatch));
|
|
3376
|
+
return lines.join("\n");
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
function buildElectronStatusResult(options: {
|
|
3380
|
+
compiledElectron: CompiledAgentBrowserElectron;
|
|
3381
|
+
managedSessions?: ElectronManagedSessionTarget[];
|
|
3382
|
+
mismatches?: ElectronSessionMismatch[];
|
|
3383
|
+
records: ElectronLaunchRecord[];
|
|
3384
|
+
statuses: ElectronLaunchStatus[];
|
|
3385
|
+
}): AgentBrowserToolResult {
|
|
3386
|
+
const baseNextActions = options.records.flatMap((record) => buildAgentBrowserNextActions({
|
|
3387
|
+
electron: { launchId: record.launchId, sessionName: record.sessionName, status: record.cleanupState },
|
|
3388
|
+
resultCategory: "success",
|
|
3389
|
+
successCategory: "completed",
|
|
3390
|
+
}) ?? []);
|
|
3391
|
+
const mismatchNextActions = (options.mismatches ?? []).flatMap((mismatch) => {
|
|
3392
|
+
const record = options.records.find((candidate) => candidate.launchId === mismatch.launchId);
|
|
3393
|
+
return record ? buildElectronMismatchNextActions(record, mismatch.liveTarget) : [];
|
|
3394
|
+
});
|
|
3395
|
+
const nextActions = options.mismatches?.length
|
|
3396
|
+
? appendUniqueNextActions([...mismatchNextActions], baseNextActions)
|
|
3397
|
+
: appendUniqueNextActions([...baseNextActions], mismatchNextActions);
|
|
3398
|
+
const details = {
|
|
3399
|
+
args: [] as string[],
|
|
3400
|
+
compiledElectron: options.compiledElectron,
|
|
3401
|
+
electron: {
|
|
3402
|
+
action: "status" as const,
|
|
3403
|
+
identifierList: options.records.length > 1 ? options.records.map(buildElectronIdentifiers) : undefined,
|
|
3404
|
+
identifiers: options.records.length === 1 && options.records[0] ? buildElectronIdentifiers(options.records[0]) : undefined,
|
|
3405
|
+
launches: options.records,
|
|
3406
|
+
managedSession: options.managedSessions?.length === 1 ? options.managedSessions[0] : undefined,
|
|
3407
|
+
managedSessions: options.managedSessions && options.managedSessions.length > 0 ? options.managedSessions : undefined,
|
|
3408
|
+
sessionMismatch: options.mismatches?.length === 1 ? options.mismatches[0] : undefined,
|
|
3409
|
+
sessionMismatches: options.mismatches && options.mismatches.length > 1 ? options.mismatches : undefined,
|
|
3410
|
+
status: "succeeded" as const,
|
|
3411
|
+
statuses: options.statuses,
|
|
3412
|
+
targets: extractTargetsFromStatus(options.statuses),
|
|
3413
|
+
},
|
|
3414
|
+
nextActions: nextActions.length > 0 ? nextActions : undefined,
|
|
3415
|
+
...buildAgentBrowserResultCategoryDetails({ args: [], succeeded: true }),
|
|
3416
|
+
summary: options.statuses.length === 0 ? "Electron status found no active wrapper-tracked launches." : `Electron status inspected ${options.statuses.length} launch(es).`,
|
|
3417
|
+
};
|
|
3418
|
+
return { content: [{ type: "text", text: redactSensitiveText(formatElectronStatusVisibleText(options.statuses, options.records, options.mismatches, options.managedSessions)) }], details: redactToolDetails(details, []), isError: false };
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
function formatElectronCleanupVisibleText(results: ElectronCleanupResult[]): string {
|
|
3422
|
+
if (results.length === 0) return "Electron cleanup: no active wrapper-tracked launches.";
|
|
3423
|
+
const lines = [`Electron cleanup: ${results.filter((result) => !result.partial).length}/${results.length} launch(es) fully cleaned.`];
|
|
3424
|
+
for (const result of results) {
|
|
3425
|
+
lines.push(`- ${result.summary}`);
|
|
3426
|
+
for (const step of result.steps) lines.push(` - ${step.resource}: ${step.state}${step.error ? ` (${step.error})` : ""}`);
|
|
3427
|
+
}
|
|
3428
|
+
return lines.join("\n");
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3431
|
+
function buildElectronCleanupResult(compiledElectron: CompiledAgentBrowserElectron, cleanupResults: ElectronCleanupResult[]): AgentBrowserToolResult {
|
|
3432
|
+
const partial = cleanupResults.some((result) => result.partial);
|
|
3433
|
+
const records = cleanupResults.map((result) => result.record);
|
|
3434
|
+
const nextActions = cleanupResults.flatMap((result) => buildAgentBrowserNextActions({
|
|
3435
|
+
electron: { launchId: result.launchId, sessionName: result.record.sessionName, status: result.record.cleanupState },
|
|
3436
|
+
failureCategory: partial ? "cleanup-failed" : undefined,
|
|
3437
|
+
resultCategory: partial ? "failure" : "success",
|
|
3438
|
+
successCategory: partial ? undefined : "completed",
|
|
3439
|
+
}) ?? []);
|
|
3440
|
+
const errorText = partial ? cleanupResults.map((result) => result.summary).join("\n") : undefined;
|
|
3441
|
+
const details = {
|
|
3442
|
+
args: [] as string[],
|
|
3443
|
+
compiledElectron,
|
|
3444
|
+
electron: {
|
|
3445
|
+
action: "cleanup" as const,
|
|
3446
|
+
cleanup: { partial, records, results: cleanupResults },
|
|
3447
|
+
status: partial ? "partial" as const : "succeeded" as const,
|
|
3448
|
+
},
|
|
3449
|
+
nextActions: nextActions.length > 0 ? nextActions : undefined,
|
|
3450
|
+
...buildAgentBrowserResultCategoryDetails({ args: [], errorText, failureCategory: partial ? "cleanup-failed" : undefined, succeeded: !partial }),
|
|
3451
|
+
summary: partial ? "Electron cleanup was partial." : "Electron cleanup completed.",
|
|
3452
|
+
};
|
|
3453
|
+
return { content: [{ type: "text", text: redactSensitiveText(formatElectronCleanupVisibleText(cleanupResults)) }], details: redactToolDetails(details, []), isError: partial };
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
function formatElectronLaunchFailureDiagnostics(failure: ElectronLaunchFailure | undefined): string | undefined {
|
|
3457
|
+
const diagnostics = failure?.diagnostics;
|
|
3458
|
+
if (!diagnostics) return undefined;
|
|
3459
|
+
const lines = ["Electron launch diagnostics:"];
|
|
3460
|
+
if (diagnostics.pid !== undefined) {
|
|
3461
|
+
const pidState = diagnostics.pidAlive === undefined ? "state unknown" : diagnostics.pidAlive ? "alive before cleanup" : "not alive before cleanup";
|
|
3462
|
+
lines.push(`- PID: ${diagnostics.pid} (${pidState}).`);
|
|
3463
|
+
}
|
|
3464
|
+
if (diagnostics.exitCode !== undefined || diagnostics.exitSignal !== undefined) {
|
|
3465
|
+
const exitParts = [diagnostics.exitCode !== undefined ? `code ${diagnostics.exitCode}` : undefined, diagnostics.exitSignal ? `signal ${diagnostics.exitSignal}` : undefined].filter(Boolean).join(", ");
|
|
3466
|
+
lines.push(`- Process exit: ${exitParts || "not observed before cleanup"}.`);
|
|
3467
|
+
}
|
|
3468
|
+
if (diagnostics.userDataDir) lines.push(`- Wrapper profile: ${diagnostics.userDataDir}`);
|
|
3469
|
+
if (diagnostics.devToolsActivePort) {
|
|
3470
|
+
const activePort = diagnostics.devToolsActivePort;
|
|
3471
|
+
const state = activePort.port
|
|
3472
|
+
? `found port ${activePort.port}`
|
|
3473
|
+
: activePort.found
|
|
3474
|
+
? `found but invalid${activePort.error ? ` (${activePort.error})` : ""}`
|
|
3475
|
+
: `missing${activePort.error ? ` (${activePort.error})` : ""}`;
|
|
3476
|
+
lines.push(`- DevToolsActivePort: ${state} at ${activePort.path}.`);
|
|
3477
|
+
}
|
|
3478
|
+
if (diagnostics.cdpVersionReached === false) lines.push("- CDP /json/version: did not return a valid payload before timeout.");
|
|
3479
|
+
if (diagnostics.timeoutMs !== undefined || diagnostics.elapsedMs !== undefined) {
|
|
3480
|
+
lines.push(`- Timing: ${diagnostics.elapsedMs ?? "unknown"}ms elapsed${diagnostics.timeoutMs !== undefined ? ` of ${diagnostics.timeoutMs}ms timeout` : ""}.`);
|
|
3481
|
+
}
|
|
3482
|
+
if (diagnostics.outputCaptured === false) lines.push("- App stdout/stderr: not captured by this wrapper launch path.");
|
|
3483
|
+
lines.push("Retry guidance: increase electron.timeoutMs, try targetType:'any', pass an explicit appPath/executablePath, quit any already-running singleton instance, then retry launch.");
|
|
3484
|
+
return lines.join("\n");
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
function buildElectronHostFailureResult(options: {
|
|
3488
|
+
compiledElectron: CompiledAgentBrowserElectron;
|
|
3489
|
+
errorText: string;
|
|
3490
|
+
failureCategory?: "cleanup-failed" | "policy-blocked" | "timeout" | "upstream-error" | "validation-error";
|
|
3491
|
+
launchFailure?: ElectronLaunchFailure;
|
|
3492
|
+
managedSessionOutcome?: ManagedSessionOutcome;
|
|
3493
|
+
status?: string;
|
|
3494
|
+
}): AgentBrowserToolResult {
|
|
3495
|
+
const text = [
|
|
3496
|
+
options.errorText,
|
|
3497
|
+
formatElectronLaunchFailureDiagnostics(options.launchFailure),
|
|
3498
|
+
options.launchFailure?.cleanupError ? `Electron launch cleanup warning: ${options.launchFailure.cleanupError}` : undefined,
|
|
3499
|
+
].filter((item): item is string => item !== undefined && item.length > 0).join("\n");
|
|
3500
|
+
const details = {
|
|
3501
|
+
args: [] as string[],
|
|
3502
|
+
compiledElectron: options.compiledElectron,
|
|
3503
|
+
electron: {
|
|
3504
|
+
action: options.compiledElectron.action,
|
|
3505
|
+
error: options.errorText,
|
|
3506
|
+
failure: options.launchFailure,
|
|
3507
|
+
status: options.status ?? "failed",
|
|
3508
|
+
},
|
|
3509
|
+
managedSessionOutcome: options.managedSessionOutcome,
|
|
3510
|
+
...buildAgentBrowserResultCategoryDetails({ args: [], errorText: options.errorText, failureCategory: options.failureCategory, succeeded: false, timedOut: options.failureCategory === "timeout" }),
|
|
3511
|
+
summary: options.errorText,
|
|
3512
|
+
};
|
|
3513
|
+
return { content: [{ type: "text", text: redactSensitiveText(text) }], details: redactToolDetails(details, []), isError: true };
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
function getElectronLaunchFailureCategory(failure: ElectronLaunchFailure): "policy-blocked" | "timeout" | "upstream-error" | "validation-error" {
|
|
3517
|
+
if (failure.reason === "policy-blocked") return "policy-blocked";
|
|
3518
|
+
if (failure.reason === "timeout") return "timeout";
|
|
3519
|
+
if (failure.reason === "non-electron-target") return "validation-error";
|
|
3520
|
+
return "upstream-error";
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
function sleepMs(ms: number): Promise<void> {
|
|
3524
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
async function collectElectronHandoff(options: {
|
|
3528
|
+
cwd: string;
|
|
3529
|
+
handoff: "connect" | "snapshot" | "tabs";
|
|
3530
|
+
sessionName?: string;
|
|
3531
|
+
signal?: AbortSignal;
|
|
3532
|
+
}): Promise<ElectronHandoffSummary> {
|
|
3533
|
+
if (options.handoff === "connect") return { handoff: "connect" };
|
|
3534
|
+
const tabs = await runSessionCommandData({ args: ["tab", "list"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
3535
|
+
if (options.handoff === "tabs") return { handoff: "tabs", tabs };
|
|
3536
|
+
let snapshot = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
3537
|
+
let refSnapshot = extractRefSnapshotFromData(snapshot);
|
|
3538
|
+
let snapshotRetryCount = 0;
|
|
3539
|
+
while ((!refSnapshot || refSnapshot.refIds.length === 0) && snapshotRetryCount < 2) {
|
|
3540
|
+
snapshotRetryCount += 1;
|
|
3541
|
+
await sleepMs(250);
|
|
3542
|
+
snapshot = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
3543
|
+
refSnapshot = extractRefSnapshotFromData(snapshot);
|
|
3544
|
+
}
|
|
3545
|
+
return { handoff: "snapshot", refSnapshot, snapshot, ...(snapshotRetryCount > 0 ? { snapshotRetryCount } : {}), tabs };
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
interface ElectronProbeFocusedElement {
|
|
3549
|
+
ariaLabel?: string;
|
|
3550
|
+
id?: string;
|
|
3551
|
+
isContentEditable?: boolean;
|
|
3552
|
+
name?: string;
|
|
3553
|
+
placeholder?: string;
|
|
3554
|
+
role?: string;
|
|
3555
|
+
tagName?: string;
|
|
3556
|
+
textLength?: number;
|
|
3557
|
+
textPreview?: string;
|
|
3558
|
+
title?: string;
|
|
3559
|
+
type?: string;
|
|
3560
|
+
valueLength?: number;
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
interface ElectronProbeTab {
|
|
3564
|
+
active?: boolean;
|
|
3565
|
+
index?: number;
|
|
3566
|
+
tabId?: string;
|
|
3567
|
+
title?: string;
|
|
3568
|
+
type?: string;
|
|
3569
|
+
url?: string;
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
interface ElectronProbeSnapshotSummary {
|
|
3573
|
+
lineCount: number;
|
|
3574
|
+
omittedLineCount?: number;
|
|
3575
|
+
omittedRefCount?: number;
|
|
3576
|
+
refCount: number;
|
|
3577
|
+
refIds: string[];
|
|
3578
|
+
text?: string;
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
interface ElectronProbeResult {
|
|
3582
|
+
activeTab?: ElectronProbeTab;
|
|
3583
|
+
errors?: string[];
|
|
3584
|
+
focusedElement?: ElectronProbeFocusedElement;
|
|
3585
|
+
refSnapshot?: SessionRefSnapshot;
|
|
3586
|
+
sessionName: string;
|
|
3587
|
+
snapshot?: ElectronProbeSnapshotSummary;
|
|
3588
|
+
status: "partial" | "succeeded";
|
|
3589
|
+
summary: string;
|
|
3590
|
+
tabs?: {
|
|
3591
|
+
omittedCount?: number;
|
|
3592
|
+
shown: ElectronProbeTab[];
|
|
3593
|
+
total: number;
|
|
3594
|
+
};
|
|
3595
|
+
title?: string;
|
|
3596
|
+
url?: string;
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
const ELECTRON_FOCUSED_ELEMENT_EVAL = `(() => {
|
|
3600
|
+
const clean = (value, max = 80) => {
|
|
3601
|
+
if (typeof value !== "string") return undefined;
|
|
3602
|
+
const normalized = value.replace(/\\s+/g, " ").trim();
|
|
3603
|
+
if (!normalized) return undefined;
|
|
3604
|
+
return normalized.length > max ? normalized.slice(0, max - 3) + "..." : normalized;
|
|
3605
|
+
};
|
|
3606
|
+
const describeElement = (element) => {
|
|
3607
|
+
if (!element || !(element instanceof Element)) return undefined;
|
|
3608
|
+
const tagName = element.tagName.toLowerCase();
|
|
3609
|
+
const inputLike = element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement;
|
|
3610
|
+
const contentEditable = element instanceof HTMLElement && element.isContentEditable;
|
|
3611
|
+
const containerLike = tagName === "body" || tagName === "html";
|
|
3612
|
+
const rawText = element.textContent || "";
|
|
3613
|
+
const exposeText = !inputLike && !contentEditable && !containerLike;
|
|
3614
|
+
const text = exposeText ? clean(rawText) : undefined;
|
|
3615
|
+
return {
|
|
3616
|
+
tagName: clean(tagName, 40),
|
|
3617
|
+
role: clean(element.getAttribute("role") || "", 60),
|
|
3618
|
+
name: clean(element.getAttribute("aria-label") || element.getAttribute("title") || text || "", 80),
|
|
3619
|
+
id: clean(element.id || "", 80),
|
|
3620
|
+
type: clean(element.getAttribute("type") || "", 40),
|
|
3621
|
+
placeholder: clean(element.getAttribute("placeholder") || "", 80),
|
|
3622
|
+
ariaLabel: clean(element.getAttribute("aria-label") || "", 80),
|
|
3623
|
+
title: clean(element.getAttribute("title") || "", 80),
|
|
3624
|
+
textLength: !exposeText && rawText ? rawText.length : undefined,
|
|
3625
|
+
textPreview: text,
|
|
3626
|
+
valueLength: inputLike && typeof element.value === "string" ? element.value.length : undefined,
|
|
3627
|
+
isContentEditable: contentEditable || undefined,
|
|
3628
|
+
};
|
|
3629
|
+
};
|
|
3630
|
+
return { focusedElement: describeElement(document.activeElement) };
|
|
3631
|
+
})()`;
|
|
3632
|
+
|
|
3633
|
+
function boundElectronProbeString(value: string | undefined, maxLength = 240): string | undefined {
|
|
3634
|
+
const trimmed = value?.trim();
|
|
3635
|
+
if (!trimmed) return undefined;
|
|
3636
|
+
return trimmed.length > maxLength ? `${trimmed.slice(0, Math.max(0, maxLength - 3))}...` : trimmed;
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
function getTrimmedString(value: unknown): string | undefined {
|
|
3640
|
+
return typeof value === "string" ? boundElectronProbeString(value) : undefined;
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
function getOptionalBoolean(value: unknown): boolean | undefined {
|
|
3644
|
+
return typeof value === "boolean" ? value : undefined;
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
function getOptionalNumber(value: unknown): number | undefined {
|
|
3648
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
function extractElectronFocusedElement(data: unknown): ElectronProbeFocusedElement | undefined {
|
|
3652
|
+
const payload = isRecord(data) && isRecord(data.result) ? data.result : data;
|
|
3653
|
+
const rawFocusedElement = isRecord(payload) && isRecord(payload.focusedElement) ? payload.focusedElement : isRecord(payload) ? payload : undefined;
|
|
3654
|
+
if (!rawFocusedElement) return undefined;
|
|
3655
|
+
const focusedElement: ElectronProbeFocusedElement = {
|
|
3656
|
+
ariaLabel: getTrimmedString(rawFocusedElement.ariaLabel),
|
|
3657
|
+
id: getTrimmedString(rawFocusedElement.id),
|
|
3658
|
+
isContentEditable: getOptionalBoolean(rawFocusedElement.isContentEditable),
|
|
3659
|
+
name: getTrimmedString(rawFocusedElement.name),
|
|
3660
|
+
placeholder: getTrimmedString(rawFocusedElement.placeholder),
|
|
3661
|
+
role: getTrimmedString(rawFocusedElement.role),
|
|
3662
|
+
tagName: getTrimmedString(rawFocusedElement.tagName),
|
|
3663
|
+
textLength: getOptionalNumber(rawFocusedElement.textLength),
|
|
3664
|
+
textPreview: getTrimmedString(rawFocusedElement.textPreview),
|
|
3665
|
+
title: getTrimmedString(rawFocusedElement.title),
|
|
3666
|
+
type: getTrimmedString(rawFocusedElement.type),
|
|
3667
|
+
valueLength: getOptionalNumber(rawFocusedElement.valueLength),
|
|
3668
|
+
};
|
|
3669
|
+
return Object.values(focusedElement).some((value) => value !== undefined) ? focusedElement : undefined;
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
function extractElectronProbeTabs(data: unknown): { activeTab?: ElectronProbeTab; tabs?: ElectronProbeResult["tabs"] } {
|
|
3673
|
+
const rawTabs = isRecord(data) && Array.isArray(data.tabs) ? data.tabs : Array.isArray(data) ? data : [];
|
|
3674
|
+
const allTabs = rawTabs.filter(isRecord).map((tab, index): ElectronProbeTab => ({
|
|
3675
|
+
active: getOptionalBoolean(tab.active),
|
|
3676
|
+
index: typeof tab.index === "number" && Number.isInteger(tab.index) ? tab.index : index,
|
|
3677
|
+
tabId: getTrimmedString(tab.tabId) ?? getTrimmedString(tab.id),
|
|
3678
|
+
title: getTrimmedString(tab.title) ?? getTrimmedString(tab.label),
|
|
3679
|
+
type: getTrimmedString(tab.type),
|
|
3680
|
+
url: getTrimmedString(tab.url),
|
|
3681
|
+
}));
|
|
3682
|
+
if (allTabs.length === 0) return {};
|
|
3683
|
+
const shown = allTabs.slice(0, ELECTRON_PROBE_MAX_TABS);
|
|
3684
|
+
return {
|
|
3685
|
+
activeTab: allTabs.find((tab) => tab.active) ?? allTabs[0],
|
|
3686
|
+
tabs: {
|
|
3687
|
+
omittedCount: allTabs.length > shown.length ? allTabs.length - shown.length : undefined,
|
|
3688
|
+
shown,
|
|
3689
|
+
total: allTabs.length,
|
|
3690
|
+
},
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
function truncateElectronProbeSnapshotText(snapshotText: string | undefined): { lineCount: number; omittedLineCount?: number; text?: string } {
|
|
3695
|
+
if (!snapshotText) return { lineCount: 0 };
|
|
3696
|
+
const lines = snapshotText.split(/\r?\n/);
|
|
3697
|
+
const shownLines: string[] = [];
|
|
3698
|
+
let usedChars = 0;
|
|
3699
|
+
for (const line of lines) {
|
|
3700
|
+
if (shownLines.length >= ELECTRON_PROBE_MAX_SNAPSHOT_LINES) break;
|
|
3701
|
+
const nextLength = usedChars + line.length + (shownLines.length > 0 ? 1 : 0);
|
|
3702
|
+
if (nextLength > ELECTRON_PROBE_MAX_SNAPSHOT_CHARS) {
|
|
3703
|
+
if (shownLines.length === 0) shownLines.push(`${line.slice(0, ELECTRON_PROBE_MAX_SNAPSHOT_CHARS - 3)}...`);
|
|
3704
|
+
break;
|
|
3705
|
+
}
|
|
3706
|
+
shownLines.push(line);
|
|
3707
|
+
usedChars = nextLength;
|
|
3708
|
+
}
|
|
3709
|
+
return {
|
|
3710
|
+
lineCount: lines.length,
|
|
3711
|
+
omittedLineCount: lines.length > shownLines.length ? lines.length - shownLines.length : undefined,
|
|
3712
|
+
text: shownLines.length > 0 ? shownLines.join("\n") : undefined,
|
|
3713
|
+
};
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
function summarizeElectronProbeSnapshot(data: unknown): { refSnapshot?: SessionRefSnapshot; snapshot?: ElectronProbeSnapshotSummary } {
|
|
3717
|
+
const refSnapshot = extractRefSnapshotFromData(data);
|
|
3718
|
+
const rawSnapshotText = isRecord(data) ? getTrimmedString(data.snapshot) : undefined;
|
|
3719
|
+
const truncatedText = truncateElectronProbeSnapshotText(rawSnapshotText);
|
|
3720
|
+
const refIds = refSnapshot?.refIds ?? [];
|
|
3721
|
+
const shownRefIds = refIds.slice(0, ELECTRON_PROBE_MAX_REF_IDS);
|
|
3722
|
+
const snapshot = refSnapshot || truncatedText.text
|
|
3723
|
+
? {
|
|
3724
|
+
lineCount: truncatedText.lineCount,
|
|
3725
|
+
omittedLineCount: truncatedText.omittedLineCount,
|
|
3726
|
+
omittedRefCount: refIds.length > shownRefIds.length ? refIds.length - shownRefIds.length : undefined,
|
|
3727
|
+
refCount: refIds.length,
|
|
3728
|
+
refIds: shownRefIds,
|
|
3729
|
+
text: truncatedText.text,
|
|
3730
|
+
}
|
|
3731
|
+
: undefined;
|
|
3732
|
+
return { refSnapshot, snapshot };
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
function getElectronProbeSummary(probe: Omit<ElectronProbeResult, "summary">): string {
|
|
3736
|
+
const parts = [
|
|
3737
|
+
probe.title ? `title "${probe.title}"` : undefined,
|
|
3738
|
+
probe.url ? `url ${probe.url}` : undefined,
|
|
3739
|
+
probe.focusedElement ? "focused element" : undefined,
|
|
3740
|
+
probe.tabs ? `${probe.tabs.total} tab(s)` : undefined,
|
|
3741
|
+
probe.snapshot ? `${probe.snapshot.refCount} ref(s)` : undefined,
|
|
3742
|
+
].filter((item): item is string => item !== undefined);
|
|
3743
|
+
return parts.length > 0 ? `Electron probe collected ${parts.join(", ")}.` : "Electron probe did not return current session state.";
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
async function runElectronProbeCommandData(options: {
|
|
3747
|
+
args: string[];
|
|
3748
|
+
cwd: string;
|
|
3749
|
+
sessionName: string;
|
|
3750
|
+
signal?: AbortSignal;
|
|
3751
|
+
stdin?: string;
|
|
3752
|
+
timeoutMs?: number;
|
|
3753
|
+
}): Promise<{ data?: unknown; error?: string }> {
|
|
3754
|
+
try {
|
|
3755
|
+
return { data: await runSessionCommandData(options) };
|
|
3756
|
+
} catch (error) {
|
|
3757
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
async function collectElectronProbe(options: {
|
|
3762
|
+
cwd: string;
|
|
3763
|
+
sessionName: string;
|
|
3764
|
+
signal?: AbortSignal;
|
|
3765
|
+
timeoutMs?: number;
|
|
3766
|
+
}): Promise<ElectronProbeResult> {
|
|
3767
|
+
const titleResult = await runElectronProbeCommandData({ args: ["get", "title"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, timeoutMs: options.timeoutMs });
|
|
3768
|
+
const urlResult = await runElectronProbeCommandData({ args: ["get", "url"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, timeoutMs: options.timeoutMs });
|
|
3769
|
+
const focusedResult = await runElectronProbeCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, stdin: ELECTRON_FOCUSED_ELEMENT_EVAL, timeoutMs: options.timeoutMs });
|
|
3770
|
+
const tabsResult = await runElectronProbeCommandData({ args: ["tab", "list"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, timeoutMs: options.timeoutMs });
|
|
3771
|
+
const snapshotResult = await runElectronProbeCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, timeoutMs: options.timeoutMs });
|
|
3772
|
+
const errors = [
|
|
3773
|
+
titleResult.error ? `get title: ${titleResult.error}` : undefined,
|
|
3774
|
+
urlResult.error ? `get url: ${urlResult.error}` : undefined,
|
|
3775
|
+
focusedResult.error ? `focused element: ${focusedResult.error}` : undefined,
|
|
3776
|
+
tabsResult.error ? `tab list: ${tabsResult.error}` : undefined,
|
|
3777
|
+
snapshotResult.error ? `snapshot: ${snapshotResult.error}` : undefined,
|
|
3778
|
+
].filter((item): item is string => item !== undefined).map((error) => boundElectronProbeString(error, 240) ?? "probe command failed");
|
|
3779
|
+
const title = boundElectronProbeString(extractStringResultField(titleResult.data, "result") ?? extractStringResultField(titleResult.data, "title"), 160);
|
|
3780
|
+
const url = boundElectronProbeString(extractStringResultField(urlResult.data, "result") ?? extractStringResultField(urlResult.data, "url"), 300);
|
|
3781
|
+
const focusedElement = extractElectronFocusedElement(focusedResult.data);
|
|
3782
|
+
const { activeTab, tabs } = extractElectronProbeTabs(tabsResult.data);
|
|
3783
|
+
const { refSnapshot, snapshot } = summarizeElectronProbeSnapshot(snapshotResult.data);
|
|
3784
|
+
const probeWithoutSummary = {
|
|
3785
|
+
activeTab,
|
|
3786
|
+
focusedElement,
|
|
3787
|
+
errors: errors.length > 0 ? errors : undefined,
|
|
3788
|
+
refSnapshot,
|
|
3789
|
+
sessionName: options.sessionName,
|
|
3790
|
+
snapshot,
|
|
3791
|
+
status: errors.length === 0 && (title || url || focusedElement || tabs || snapshot) ? "succeeded" as const : "partial" as const,
|
|
3792
|
+
tabs,
|
|
3793
|
+
title,
|
|
3794
|
+
url,
|
|
3795
|
+
};
|
|
3796
|
+
return { ...probeWithoutSummary, summary: getElectronProbeSummary(probeWithoutSummary) };
|
|
3797
|
+
}
|
|
3798
|
+
|
|
3799
|
+
function formatElectronProbeFocusedElement(focusedElement: ElectronProbeFocusedElement | undefined): string | undefined {
|
|
3800
|
+
if (!focusedElement) return undefined;
|
|
3801
|
+
const label = focusedElement.name ?? focusedElement.textPreview ?? focusedElement.placeholder ?? focusedElement.ariaLabel ?? focusedElement.title;
|
|
3802
|
+
const descriptor = [focusedElement.role, focusedElement.tagName].filter(Boolean).join("/") || "element";
|
|
3803
|
+
const suffix = [
|
|
3804
|
+
focusedElement.id ? `#${focusedElement.id}` : undefined,
|
|
3805
|
+
focusedElement.type ? `type=${focusedElement.type}` : undefined,
|
|
3806
|
+
focusedElement.valueLength !== undefined ? `valueLength=${focusedElement.valueLength}` : undefined,
|
|
3807
|
+
focusedElement.textLength !== undefined ? `textLength=${focusedElement.textLength}` : undefined,
|
|
3808
|
+
].filter((item): item is string => item !== undefined).join(", ");
|
|
3809
|
+
return `Focused: ${descriptor}${label ? ` "${label}"` : ""}${suffix ? ` (${suffix})` : ""}`;
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
function formatElectronProbeContextText(context: ElectronProbeContext): string {
|
|
3813
|
+
if (context.mode === "launchId") {
|
|
3814
|
+
return `Probe context: wrapper launch ${context.launchId} session ${context.sessionName}.`;
|
|
3815
|
+
}
|
|
3816
|
+
if (context.note) {
|
|
3817
|
+
return `Probe context: current managed session ${context.sessionName}; ${context.note}`;
|
|
3818
|
+
}
|
|
3819
|
+
if (context.launchId) {
|
|
3820
|
+
return `Probe context: current managed session ${context.sessionName} maps to Electron launch ${context.launchId}.`;
|
|
3821
|
+
}
|
|
3822
|
+
return `Probe context: current managed session ${context.sessionName} only; pass electron.probe.launchId to compare wrapper-tracked launch status.`;
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
function formatElectronProbeLaunchStatusText(status: ElectronLaunchStatus | undefined, probe: ElectronProbeResult): string | undefined {
|
|
3826
|
+
if (!status) return undefined;
|
|
3827
|
+
const lines = [`Launch status: ${status.portAlive ? "debug port alive" : "debug port dead"}${status.pidAlive === undefined ? "" : status.pidAlive ? ", pid alive" : ", pid dead"}; ${status.targets.length} CDP target(s).`];
|
|
3828
|
+
if (isAboutBlankUrl(probe.url) && (!status.portAlive || status.pidAlive === false || getLiveElectronRendererTargets(status.targets).length === 0)) {
|
|
3829
|
+
lines.push("Electron lifecycle warning: the browser session is on about:blank and the wrapper launch has no live renderer target to reattach. Run electron.status, cleanup if dead, or relaunch the app.");
|
|
3830
|
+
}
|
|
3831
|
+
return lines.join("\n");
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
function formatElectronProbeVisibleText(options: {
|
|
3835
|
+
context?: ElectronProbeContext;
|
|
3836
|
+
mismatch?: ElectronSessionMismatch;
|
|
3837
|
+
probe: ElectronProbeResult;
|
|
3838
|
+
status?: ElectronLaunchStatus;
|
|
3839
|
+
}): string {
|
|
3840
|
+
const { context, mismatch, probe, status } = options;
|
|
3841
|
+
const page = [probe.title, probe.url].filter(Boolean).join(" — ");
|
|
3842
|
+
const lines = [`Electron probe: ${page || probe.sessionName}`];
|
|
3843
|
+
if (context) lines.push(formatElectronProbeContextText(context));
|
|
3844
|
+
const launchStatusText = formatElectronProbeLaunchStatusText(status, probe);
|
|
3845
|
+
if (launchStatusText) lines.push(launchStatusText);
|
|
3846
|
+
if (mismatch) lines.push(formatElectronSessionMismatchText(mismatch));
|
|
3847
|
+
const focusedLine = formatElectronProbeFocusedElement(probe.focusedElement);
|
|
3848
|
+
if (focusedLine) lines.push(focusedLine);
|
|
3849
|
+
if (probe.tabs) {
|
|
3850
|
+
const active = probe.activeTab;
|
|
3851
|
+
lines.push(`Tabs: ${probe.tabs.total} total${probe.tabs.omittedCount ? ` (${probe.tabs.omittedCount} omitted)` : ""}${active ? `; active ${active.index ?? "?"}: ${[active.title, active.url].filter(Boolean).join(" — ") || active.tabId || "tab"}` : ""}`);
|
|
3852
|
+
}
|
|
3853
|
+
if (probe.snapshot) {
|
|
3854
|
+
lines.push(`Snapshot: ${probe.snapshot.refCount} interactive ref(s)${probe.snapshot.omittedRefCount ? ` (${probe.snapshot.omittedRefCount} ref id(s) omitted)` : ""}.`);
|
|
3855
|
+
if (probe.snapshot.text) lines.push(probe.snapshot.text);
|
|
3856
|
+
if (probe.snapshot.omittedLineCount) lines.push(`... ${probe.snapshot.omittedLineCount} snapshot line(s) omitted`);
|
|
3857
|
+
}
|
|
3858
|
+
if (probe.status === "partial") lines.push("Some probe commands did not return data; use raw agent_browser commands for deeper diagnostics.");
|
|
3859
|
+
if (probe.errors && probe.errors.length > 0) lines.push(`Probe warning: ${probe.errors.slice(0, 2).join("; ")}${probe.errors.length > 2 ? "; ..." : ""}`);
|
|
3860
|
+
return lines.join("\n");
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
function buildElectronProbeResult(options: {
|
|
3864
|
+
compiledElectron: CompiledAgentBrowserElectron;
|
|
3865
|
+
mismatch?: ElectronSessionMismatch;
|
|
3866
|
+
probe: ElectronProbeResult;
|
|
3867
|
+
probeContext: ElectronProbeContext;
|
|
3868
|
+
record?: ElectronLaunchRecord;
|
|
3869
|
+
sessionTabTarget?: SessionTabTarget;
|
|
3870
|
+
status?: ElectronLaunchStatus;
|
|
3871
|
+
}): AgentBrowserToolResult {
|
|
3872
|
+
const { refSnapshot: _refSnapshot, ...boundedProbe } = options.probe;
|
|
3873
|
+
const baseNextActions = options.record ? buildAgentBrowserNextActions({
|
|
3874
|
+
electron: { launchId: options.record.launchId, sessionName: options.record.sessionName, status: options.record.cleanupState },
|
|
3875
|
+
resultCategory: "success",
|
|
3876
|
+
successCategory: "completed",
|
|
3877
|
+
}) ?? [] : [];
|
|
3878
|
+
const mismatchNextActions = options.mismatch && options.record ? buildElectronMismatchNextActions(options.record, options.mismatch.liveTarget) : [];
|
|
3879
|
+
const nextActions = options.mismatch
|
|
3880
|
+
? appendUniqueNextActions([...mismatchNextActions], baseNextActions)
|
|
3881
|
+
: appendUniqueNextActions([...baseNextActions], mismatchNextActions);
|
|
3882
|
+
const details = {
|
|
3883
|
+
args: [] as string[],
|
|
3884
|
+
compiledElectron: options.compiledElectron,
|
|
3885
|
+
electron: {
|
|
3886
|
+
action: "probe" as const,
|
|
3887
|
+
identifiers: options.record ? buildElectronIdentifiers(options.record) : undefined,
|
|
3888
|
+
probe: boundedProbe,
|
|
3889
|
+
probeContext: options.probeContext,
|
|
3890
|
+
sessionMismatch: options.mismatch,
|
|
3891
|
+
status: options.probe.status,
|
|
3892
|
+
statusTargets: options.status?.targets,
|
|
3893
|
+
launchStatus: options.status,
|
|
3894
|
+
},
|
|
3895
|
+
nextActions: nextActions.length > 0 ? nextActions : undefined,
|
|
3896
|
+
...buildAgentBrowserResultCategoryDetails({ args: [], succeeded: true }),
|
|
3897
|
+
sessionName: options.probe.sessionName,
|
|
3898
|
+
sessionTabTarget: options.sessionTabTarget,
|
|
3899
|
+
summary: options.mismatch?.summary ?? options.probe.summary,
|
|
3900
|
+
usedImplicitSession: options.probeContext.mode === "current-managed-session",
|
|
3901
|
+
};
|
|
3902
|
+
return {
|
|
3903
|
+
content: [{ type: "text", text: redactSensitiveText(formatElectronProbeVisibleText({ context: options.probeContext, mismatch: options.mismatch, probe: options.probe, status: options.status })) }],
|
|
3904
|
+
details: redactToolDetails(details, []),
|
|
3905
|
+
isError: false,
|
|
3906
|
+
};
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
function formatElectronLaunchText(options: {
|
|
3910
|
+
handoff?: ElectronHandoffSummary;
|
|
3911
|
+
record: ElectronLaunchRecord;
|
|
3912
|
+
targets: ElectronCdpTarget[];
|
|
3913
|
+
upstreamText: string;
|
|
3914
|
+
}): string {
|
|
3915
|
+
const lines = [
|
|
3916
|
+
`Electron launch: ${options.record.appName} attached as ${options.record.sessionName ?? "managed session"} (launchId ${options.record.launchId}, port ${options.record.port}).`,
|
|
3917
|
+
`Identifiers: launchId ${options.record.launchId} for electron.status/electron.cleanup/electron.probe; sessionName ${options.record.sessionName ?? "not attached"} for browser snapshot/tab commands.`,
|
|
3918
|
+
ELECTRON_PROFILE_ISOLATION_NOTE,
|
|
3919
|
+
ELECTRON_EXISTING_AUTH_GUIDANCE,
|
|
3920
|
+
...formatElectronTargetLines(options.targets),
|
|
3921
|
+
];
|
|
3922
|
+
if (options.handoff?.handoff === "snapshot") lines.push(options.handoff.refSnapshot && options.handoff.refSnapshot.refIds.length > 0
|
|
3923
|
+
? `Snapshot handoff: ${options.handoff.refSnapshot.refIds.length} interactive ref(s)${options.handoff.snapshotRetryCount ? ` after ${options.handoff.snapshotRetryCount} retry attempt(s)` : ""}.`
|
|
3924
|
+
: "Snapshot handoff: no interactive refs returned after a short readiness retry; run snapshot -i once more before assuming the Electron UI is unusable.");
|
|
3925
|
+
else if (options.handoff?.handoff === "tabs") lines.push("Tabs handoff completed: safer diagnostic starting point; no interactive refs were captured.");
|
|
3926
|
+
else if (options.handoff?.handoff === "connect") lines.push("Connect handoff completed: run snapshot -i before using interactive refs.");
|
|
3927
|
+
lines.push(`Cleanup: use details.nextActions cleanup-electron-launch or call electron.cleanup with launchId ${options.record.launchId} when finished.`);
|
|
3928
|
+
if (options.handoff?.error) lines.push(`Handoff warning: ${options.handoff.error}`);
|
|
3929
|
+
if (options.upstreamText.trim().length > 0) lines.push("", options.upstreamText.trim());
|
|
3930
|
+
return lines.join("\n");
|
|
3931
|
+
}
|
|
3932
|
+
|
|
3933
|
+
function validateStdinCommandContract(options: { command?: string; commandTokens: string[]; stdin?: string }): string | undefined {
|
|
3934
|
+
if (options.stdin === undefined) {
|
|
3935
|
+
return undefined;
|
|
3936
|
+
}
|
|
3937
|
+
if (options.command === "batch") {
|
|
3938
|
+
return undefined;
|
|
3939
|
+
}
|
|
3940
|
+
if (options.command === "eval" && options.commandTokens.includes("--stdin")) {
|
|
3941
|
+
return undefined;
|
|
3942
|
+
}
|
|
3943
|
+
if (isPasswordStdinAuthSave(options)) {
|
|
3944
|
+
return undefined;
|
|
3945
|
+
}
|
|
3946
|
+
const commandLabel = options.command ? `\`${options.command}\`` : "the requested command";
|
|
3947
|
+
return `agent_browser stdin is only supported for \`batch\`, \`eval --stdin\`, and \`auth save --password-stdin\`; remove stdin from ${commandLabel} or use one of those command forms.`;
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
function supportsPinnedStdinCommand(options: { command?: string; commandTokens: string[]; stdin?: string }): boolean {
|
|
3951
|
+
if (options.command === "batch") {
|
|
3952
|
+
return options.stdin !== undefined;
|
|
3953
|
+
}
|
|
3954
|
+
if (options.stdin === undefined) {
|
|
3955
|
+
return true;
|
|
3956
|
+
}
|
|
3957
|
+
if (options.command === "eval") {
|
|
3958
|
+
return options.commandTokens.includes("--stdin");
|
|
3959
|
+
}
|
|
3960
|
+
return false;
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
function shouldPinSessionTabForCommand(options: {
|
|
3964
|
+
command?: string;
|
|
3965
|
+
commandTokens: string[];
|
|
3966
|
+
pinningRequired?: boolean;
|
|
3967
|
+
sessionName?: string;
|
|
3968
|
+
stdin?: string;
|
|
3969
|
+
}): boolean {
|
|
3970
|
+
return (
|
|
3971
|
+
options.pinningRequired === true &&
|
|
3972
|
+
options.sessionName !== undefined &&
|
|
3973
|
+
options.command !== undefined &&
|
|
3974
|
+
!SESSION_TAB_PINNING_EXCLUDED_COMMANDS.has(options.command) &&
|
|
3975
|
+
supportsPinnedStdinCommand(options)
|
|
3976
|
+
);
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
function validateUserBatchStep(
|
|
3980
|
+
step: unknown,
|
|
3981
|
+
index: number,
|
|
3982
|
+
):
|
|
3983
|
+
| { ok: true; step: BatchCommandStep }
|
|
3984
|
+
| { ok: false; error: string } {
|
|
3985
|
+
if (!Array.isArray(step)) {
|
|
3986
|
+
return {
|
|
3987
|
+
ok: false,
|
|
3988
|
+
error: `agent_browser batch stdin step ${index} must be a non-empty array of string command tokens.`,
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
if (step.length === 0) {
|
|
3992
|
+
return {
|
|
3993
|
+
ok: false,
|
|
3994
|
+
error: `agent_browser batch stdin step ${index} must not be empty.`,
|
|
3995
|
+
};
|
|
3996
|
+
}
|
|
3997
|
+
const invalidTokenIndex = step.findIndex((token) => typeof token !== "string");
|
|
3998
|
+
if (invalidTokenIndex !== -1) {
|
|
3999
|
+
return {
|
|
4000
|
+
ok: false,
|
|
4001
|
+
error: `agent_browser batch stdin step ${index} token ${invalidTokenIndex} must be a string.`,
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
return { ok: true, step: step as BatchCommandStep };
|
|
4005
|
+
}
|
|
4006
|
+
|
|
4007
|
+
function parseUserBatchStdin(stdin: string | undefined): { error?: string; steps?: BatchCommandStep[] } {
|
|
4008
|
+
if (stdin === undefined) {
|
|
4009
|
+
return { steps: [] };
|
|
4010
|
+
}
|
|
4011
|
+
try {
|
|
4012
|
+
const parsed = JSON.parse(stdin) as unknown;
|
|
4013
|
+
if (!Array.isArray(parsed)) {
|
|
4014
|
+
return { error: "agent_browser batch stdin must be a JSON array of command steps." };
|
|
4015
|
+
}
|
|
4016
|
+
const steps: BatchCommandStep[] = [];
|
|
4017
|
+
for (const [index, rawStep] of parsed.entries()) {
|
|
4018
|
+
const validated = validateUserBatchStep(rawStep, index);
|
|
4019
|
+
if (!validated.ok) {
|
|
4020
|
+
return { error: validated.error };
|
|
4021
|
+
}
|
|
4022
|
+
steps.push(validated.step);
|
|
4023
|
+
}
|
|
4024
|
+
return { steps };
|
|
4025
|
+
} catch (error) {
|
|
4026
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4027
|
+
return { error: `agent_browser batch stdin could not be parsed as JSON: ${message}` };
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
const REF_INVALIDATING_BATCH_COMMANDS = new Set([
|
|
4032
|
+
"back",
|
|
4033
|
+
"check",
|
|
4034
|
+
"click",
|
|
4035
|
+
"dblclick",
|
|
4036
|
+
"drag",
|
|
4037
|
+
"forward",
|
|
4038
|
+
"goto",
|
|
4039
|
+
"keyboard",
|
|
4040
|
+
"mouse",
|
|
4041
|
+
"navigate",
|
|
4042
|
+
"open",
|
|
2668
4043
|
"press",
|
|
2669
4044
|
"reload",
|
|
2670
4045
|
"select",
|
|
@@ -2933,8 +4308,9 @@ async function runSessionCommandData(options: {
|
|
|
2933
4308
|
sessionName?: string;
|
|
2934
4309
|
signal?: AbortSignal;
|
|
2935
4310
|
stdin?: string;
|
|
4311
|
+
timeoutMs?: number;
|
|
2936
4312
|
}): Promise<unknown | undefined> {
|
|
2937
|
-
const { args, cwd, sessionName, signal, stdin } = options;
|
|
4313
|
+
const { args, cwd, sessionName, signal, stdin, timeoutMs } = options;
|
|
2938
4314
|
if (!sessionName) return undefined;
|
|
2939
4315
|
|
|
2940
4316
|
const processResult = await runAgentBrowserProcess({
|
|
@@ -2942,6 +4318,7 @@ async function runSessionCommandData(options: {
|
|
|
2942
4318
|
cwd,
|
|
2943
4319
|
signal,
|
|
2944
4320
|
stdin,
|
|
4321
|
+
timeoutMs,
|
|
2945
4322
|
});
|
|
2946
4323
|
try {
|
|
2947
4324
|
if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
|
|
@@ -2962,6 +4339,114 @@ async function runSessionCommandData(options: {
|
|
|
2962
4339
|
}
|
|
2963
4340
|
}
|
|
2964
4341
|
|
|
4342
|
+
function getTopLevelFillInvocation(commandTokens: string[]): { expected: string; selector: string } | undefined {
|
|
4343
|
+
if (commandTokens[0] !== "fill" || commandTokens.length < 3) return undefined;
|
|
4344
|
+
const selector = commandTokens[1];
|
|
4345
|
+
const expected = commandTokens.slice(2).join(" ");
|
|
4346
|
+
if (!selector || expected.length === 0) return undefined;
|
|
4347
|
+
return { expected, selector };
|
|
4348
|
+
}
|
|
4349
|
+
|
|
4350
|
+
function buildFillVerificationNextActions(diagnostic: FillVerificationDiagnostic, sessionName: string | undefined): AgentBrowserNextAction[] {
|
|
4351
|
+
return [
|
|
4352
|
+
{
|
|
4353
|
+
id: "inspect-after-fill-verification",
|
|
4354
|
+
params: { args: sessionPrefixArgs(sessionName, ["snapshot", "-i"]) },
|
|
4355
|
+
reason: "Refresh the UI after a fill that reported success but did not appear to update the input value.",
|
|
4356
|
+
safety: "Read-only snapshot; use current refs before retrying.",
|
|
4357
|
+
tool: "agent_browser",
|
|
4358
|
+
},
|
|
4359
|
+
{
|
|
4360
|
+
id: "verify-filled-value",
|
|
4361
|
+
params: { args: sessionPrefixArgs(sessionName, ["get", "value", diagnostic.selector]) },
|
|
4362
|
+
reason: "Check the target input value directly before submitting or creating files.",
|
|
4363
|
+
safety: "Read-only value check; selector may still be stale if the Electron UI rerendered.",
|
|
4364
|
+
tool: "agent_browser",
|
|
4365
|
+
},
|
|
4366
|
+
];
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4369
|
+
function extractFillVerificationValue(data: unknown): string | undefined {
|
|
4370
|
+
if (typeof data === "string") return data;
|
|
4371
|
+
if (!isRecord(data)) return undefined;
|
|
4372
|
+
if (typeof data.value === "string") return data.value;
|
|
4373
|
+
if (typeof data.result === "string") return data.result;
|
|
4374
|
+
return undefined;
|
|
4375
|
+
}
|
|
4376
|
+
|
|
4377
|
+
async function collectFillVerificationDiagnostic(options: {
|
|
4378
|
+
commandTokens: string[];
|
|
4379
|
+
cwd: string;
|
|
4380
|
+
sessionName?: string;
|
|
4381
|
+
signal?: AbortSignal;
|
|
4382
|
+
}): Promise<FillVerificationDiagnostic | undefined> {
|
|
4383
|
+
const fill = getTopLevelFillInvocation(options.commandTokens);
|
|
4384
|
+
if (!fill || !options.sessionName) return undefined;
|
|
4385
|
+
let valueData: unknown | undefined;
|
|
4386
|
+
try {
|
|
4387
|
+
valueData = await runSessionCommandData({
|
|
4388
|
+
args: ["get", "value", fill.selector],
|
|
4389
|
+
cwd: options.cwd,
|
|
4390
|
+
sessionName: options.sessionName,
|
|
4391
|
+
signal: options.signal,
|
|
4392
|
+
timeoutMs: ELECTRON_FILL_VERIFICATION_TIMEOUT_MS,
|
|
4393
|
+
});
|
|
4394
|
+
} catch {
|
|
4395
|
+
return undefined;
|
|
4396
|
+
}
|
|
4397
|
+
const actual = extractFillVerificationValue(valueData);
|
|
4398
|
+
if (actual === undefined || actual === fill.expected) return undefined;
|
|
4399
|
+
const diagnostic: FillVerificationDiagnostic = {
|
|
4400
|
+
actual: actual.length > 0 ? boundElectronProbeString(actual, 160) : "",
|
|
4401
|
+
expected: boundElectronProbeString(fill.expected, 160) ?? fill.expected,
|
|
4402
|
+
nextActionIds: [],
|
|
4403
|
+
selector: fill.selector,
|
|
4404
|
+
status: "mismatch",
|
|
4405
|
+
summary: `Fill verification warning: fill ${fill.selector} reported success, but get value returned ${actual.length > 0 ? `"${boundElectronProbeString(actual, 80)}"` : "an empty value"}.`,
|
|
4406
|
+
};
|
|
4407
|
+
diagnostic.nextActionIds = buildFillVerificationNextActions(diagnostic, options.sessionName).map((action) => action.id);
|
|
4408
|
+
return diagnostic;
|
|
4409
|
+
}
|
|
4410
|
+
|
|
4411
|
+
function formatFillVerificationText(diagnostic: FillVerificationDiagnostic | undefined): string | undefined {
|
|
4412
|
+
if (!diagnostic) return undefined;
|
|
4413
|
+
const actual = diagnostic.actual !== undefined ? `actual "${diagnostic.actual}"` : "actual value unavailable";
|
|
4414
|
+
return `${diagnostic.summary}\nExpected: "${diagnostic.expected}"; ${actual}.\nNext: re-run snapshot -i, then prefer click/focus plus keyboard type for custom Electron quick-input controls before submitting.`;
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
function buildElectronRefFreshnessNextActions(sessionName: string | undefined): AgentBrowserNextAction[] {
|
|
4418
|
+
return [{
|
|
4419
|
+
id: "refresh-electron-refs-after-rerender",
|
|
4420
|
+
params: { args: sessionPrefixArgs(sessionName, ["snapshot", "-i"]) },
|
|
4421
|
+
reason: "Electron UIs often rerender without changing URL; refresh refs before using old @e handles again.",
|
|
4422
|
+
safety: "Read-only snapshot; avoids stale same-URL refs after quick-pick, modal, theme, or editor rerenders.",
|
|
4423
|
+
tool: "agent_browser",
|
|
4424
|
+
}];
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4427
|
+
function buildElectronRefFreshnessDiagnostic(options: {
|
|
4428
|
+
command?: string;
|
|
4429
|
+
commandTokens: string[];
|
|
4430
|
+
record?: ElectronLaunchRecord;
|
|
4431
|
+
sessionName?: string;
|
|
4432
|
+
stdin?: string;
|
|
4433
|
+
}): ElectronRefFreshnessDiagnostic | undefined {
|
|
4434
|
+
if (!options.record || !shouldInspectElectronPostCommandHealth(options.command)) return undefined;
|
|
4435
|
+
if (getGuardedRefUsage(options.commandTokens, options.stdin).length === 0) return undefined;
|
|
4436
|
+
const nextActions = buildElectronRefFreshnessNextActions(options.sessionName);
|
|
4437
|
+
return {
|
|
4438
|
+
command: options.command,
|
|
4439
|
+
launchId: options.record.launchId,
|
|
4440
|
+
nextActionIds: nextActions.map((action) => action.id),
|
|
4441
|
+
sessionName: options.sessionName,
|
|
4442
|
+
summary: `Electron ref freshness: ${options.command ?? "mutation"} used page-scoped refs in an Electron UI. Re-run snapshot -i before reusing old @e refs, even if the URL did not change.`,
|
|
4443
|
+
};
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4446
|
+
function formatElectronRefFreshnessText(diagnostic: ElectronRefFreshnessDiagnostic | undefined): string | undefined {
|
|
4447
|
+
return diagnostic?.summary;
|
|
4448
|
+
}
|
|
4449
|
+
|
|
2965
4450
|
async function collectNavigationSummary(options: {
|
|
2966
4451
|
cwd: string;
|
|
2967
4452
|
sessionName?: string;
|
|
@@ -3420,6 +4905,14 @@ function getBatchGetTextSelectors(data: unknown): string[] {
|
|
|
3420
4905
|
});
|
|
3421
4906
|
}
|
|
3422
4907
|
|
|
4908
|
+
function getSuccessfulGetTextSelectors(options: { commandInfo: CommandInfo; commandTokens: string[]; data: unknown }): string[] {
|
|
4909
|
+
return options.commandInfo.command === "get" && options.commandInfo.subcommand === "text"
|
|
4910
|
+
? [options.commandTokens[2]].filter((selector): selector is string => typeof selector === "string" && selector.length > 0)
|
|
4911
|
+
: options.commandInfo.command === "batch"
|
|
4912
|
+
? getBatchGetTextSelectors(options.data)
|
|
4913
|
+
: [];
|
|
4914
|
+
}
|
|
4915
|
+
|
|
3423
4916
|
async function collectSelectorTextVisibilityDiagnostics(options: {
|
|
3424
4917
|
commandInfo: CommandInfo;
|
|
3425
4918
|
commandTokens: string[];
|
|
@@ -3428,11 +4921,7 @@ async function collectSelectorTextVisibilityDiagnostics(options: {
|
|
|
3428
4921
|
sessionName?: string;
|
|
3429
4922
|
signal?: AbortSignal;
|
|
3430
4923
|
}): Promise<SelectorTextVisibilityDiagnostic[]> {
|
|
3431
|
-
const selectors = options
|
|
3432
|
-
? [options.commandTokens[2]]
|
|
3433
|
-
: options.commandInfo.command === "batch"
|
|
3434
|
-
? getBatchGetTextSelectors(options.data)
|
|
3435
|
-
: [];
|
|
4924
|
+
const selectors = getSuccessfulGetTextSelectors(options);
|
|
3436
4925
|
const diagnostics: SelectorTextVisibilityDiagnostic[] = [];
|
|
3437
4926
|
for (const selector of selectors) {
|
|
3438
4927
|
const diagnostic = await collectSelectorTextVisibilityDiagnosticForSelector({
|
|
@@ -3455,6 +4944,129 @@ function formatSelectorTextVisibilityText(diagnostics: SelectorTextVisibilityDia
|
|
|
3455
4944
|
}).join("\n");
|
|
3456
4945
|
}
|
|
3457
4946
|
|
|
4947
|
+
function isElectronLikeRendererUrl(url: string | undefined): boolean {
|
|
4948
|
+
if (!url) return false;
|
|
4949
|
+
return /^(?:app|file|vscode-file|vscode|chrome-extension):/i.test(url);
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4952
|
+
function normalizeSelectorForScopeHeuristic(selector: string): string {
|
|
4953
|
+
return selector.trim().replace(/\s+/g, " ").toLowerCase();
|
|
4954
|
+
}
|
|
4955
|
+
|
|
4956
|
+
function isBroadGetTextSelector(selector: string | undefined): selector is string {
|
|
4957
|
+
if (!selector || /^@e\d+$/.test(selector) || selectorMayExposeSensitiveLiteral(selector)) return false;
|
|
4958
|
+
const normalized = normalizeSelectorForScopeHeuristic(selector);
|
|
4959
|
+
return normalized === "body" ||
|
|
4960
|
+
normalized === "html" ||
|
|
4961
|
+
normalized === ":root" ||
|
|
4962
|
+
normalized === "*" ||
|
|
4963
|
+
normalized === "main" ||
|
|
4964
|
+
normalized === "div" ||
|
|
4965
|
+
normalized === "section" ||
|
|
4966
|
+
normalized === "article" ||
|
|
4967
|
+
/^\[role=(?:"application"|'application'|application)\]$/i.test(normalized);
|
|
4968
|
+
}
|
|
4969
|
+
|
|
4970
|
+
function getElectronTextScopeContext(options: {
|
|
4971
|
+
currentTarget?: SessionTabTarget;
|
|
4972
|
+
electronLaunchRecords: Map<string, ElectronLaunchRecord>;
|
|
4973
|
+
priorTarget?: SessionTabTarget;
|
|
4974
|
+
sessionName?: string;
|
|
4975
|
+
}): ElectronBroadGetTextScopeDiagnostic["electronContext"] | undefined {
|
|
4976
|
+
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords);
|
|
4977
|
+
const url = options.currentTarget?.url ?? options.priorTarget?.url;
|
|
4978
|
+
if (record) return { launchId: record.launchId, sessionName: record.sessionName ?? options.sessionName, url };
|
|
4979
|
+
if (isElectronLikeRendererUrl(url)) return { sessionName: options.sessionName, url };
|
|
4980
|
+
return undefined;
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4983
|
+
function getSourceLookupElectronContext(options: {
|
|
4984
|
+
currentTarget?: SessionTabTarget;
|
|
4985
|
+
electronLaunchRecords: Map<string, ElectronLaunchRecord>;
|
|
4986
|
+
priorTarget?: SessionTabTarget;
|
|
4987
|
+
sessionName?: string;
|
|
4988
|
+
}): AgentBrowserSourceLookupElectronContext | undefined {
|
|
4989
|
+
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords);
|
|
4990
|
+
if (!record) return undefined;
|
|
4991
|
+
const url = options.currentTarget?.url ?? options.priorTarget?.url;
|
|
4992
|
+
return {
|
|
4993
|
+
appName: record.appName,
|
|
4994
|
+
appPath: record.appPath,
|
|
4995
|
+
executablePath: record.executablePath,
|
|
4996
|
+
launchId: record.launchId,
|
|
4997
|
+
sessionName: record.sessionName ?? options.sessionName,
|
|
4998
|
+
url,
|
|
4999
|
+
};
|
|
5000
|
+
}
|
|
5001
|
+
|
|
5002
|
+
function buildSourceLookupElectronNextActions(sourceLookup: AgentBrowserSourceLookupAnalysis | undefined): AgentBrowserNextAction[] {
|
|
5003
|
+
if (sourceLookup?.status !== "no-candidates" || !sourceLookup.electronContext) return [];
|
|
5004
|
+
const actions: AgentBrowserNextAction[] = [];
|
|
5005
|
+
const { launchId, sessionName } = sourceLookup.electronContext;
|
|
5006
|
+
if (sessionName) {
|
|
5007
|
+
actions.push({
|
|
5008
|
+
id: "snapshot-electron-session",
|
|
5009
|
+
params: { args: sessionPrefixArgs(sessionName, ["snapshot", "-i"]) },
|
|
5010
|
+
reason: "Refresh interactive refs in the attached Electron session before retrying source lookup with a narrower target.",
|
|
5011
|
+
safety: "Read-only snapshot; no app mutation.",
|
|
5012
|
+
tool: "agent_browser",
|
|
5013
|
+
});
|
|
5014
|
+
}
|
|
5015
|
+
if (launchId) {
|
|
5016
|
+
actions.push({
|
|
5017
|
+
id: "probe-electron-launch",
|
|
5018
|
+
params: { electron: { action: "probe", launchId } },
|
|
5019
|
+
reason: "Collect bounded wrapper/session context for the packaged Electron launch after sourceLookup found no candidates.",
|
|
5020
|
+
safety: "Read-only probe of title, URL, focus, tabs, and compact snapshot metadata.",
|
|
5021
|
+
tool: "agent_browser",
|
|
5022
|
+
});
|
|
5023
|
+
}
|
|
5024
|
+
if (sessionName) {
|
|
5025
|
+
actions.push({
|
|
5026
|
+
id: "list-electron-tabs",
|
|
5027
|
+
params: { args: sessionPrefixArgs(sessionName, ["tab", "list"]) },
|
|
5028
|
+
reason: "Check current Electron tabs/targets before choosing a narrower selector or @ref.",
|
|
5029
|
+
safety: "Read-only tab listing.",
|
|
5030
|
+
tool: "agent_browser",
|
|
5031
|
+
});
|
|
5032
|
+
}
|
|
5033
|
+
return actions;
|
|
5034
|
+
}
|
|
5035
|
+
|
|
5036
|
+
function collectElectronBroadGetTextScopeDiagnostics(options: {
|
|
5037
|
+
commandInfo: CommandInfo;
|
|
5038
|
+
commandTokens: string[];
|
|
5039
|
+
currentTarget?: SessionTabTarget;
|
|
5040
|
+
data: unknown;
|
|
5041
|
+
electronLaunchRecords: Map<string, ElectronLaunchRecord>;
|
|
5042
|
+
priorTarget?: SessionTabTarget;
|
|
5043
|
+
sessionName?: string;
|
|
5044
|
+
}): ElectronBroadGetTextScopeDiagnostic[] {
|
|
5045
|
+
const electronContext = getElectronTextScopeContext(options);
|
|
5046
|
+
if (!electronContext) return [];
|
|
5047
|
+
return getSuccessfulGetTextSelectors(options)
|
|
5048
|
+
.filter(isBroadGetTextSelector)
|
|
5049
|
+
.map((selector) => ({
|
|
5050
|
+
electronContext,
|
|
5051
|
+
selector,
|
|
5052
|
+
summary: `Broad Electron get text selector warning: selector ${JSON.stringify(selector)} may read the entire app shell; prefer snapshot -i and a current @ref or a narrower panel selector.`,
|
|
5053
|
+
}));
|
|
5054
|
+
}
|
|
5055
|
+
|
|
5056
|
+
function formatElectronBroadGetTextScopeText(diagnostics: ElectronBroadGetTextScopeDiagnostic[]): string | undefined {
|
|
5057
|
+
return diagnostics.length > 0 ? diagnostics.map((diagnostic) => diagnostic.summary).join("\n") : undefined;
|
|
5058
|
+
}
|
|
5059
|
+
|
|
5060
|
+
function buildElectronBroadGetTextScopeNextActions(options: { diagnostics: ElectronBroadGetTextScopeDiagnostic[]; sessionName?: string }): AgentBrowserNextAction[] {
|
|
5061
|
+
return options.diagnostics.map((diagnostic, index) => ({
|
|
5062
|
+
id: index === 0 ? "snapshot-for-electron-text-scope" : `snapshot-for-electron-text-scope-${index + 1}`,
|
|
5063
|
+
params: { args: sessionPrefixArgs(options.sessionName, ["snapshot", "-i"]) },
|
|
5064
|
+
reason: `Refresh Electron refs before trusting broad get text selector ${JSON.stringify(diagnostic.selector)}.`,
|
|
5065
|
+
safety: "Read-only snapshot; prefer a current @ref or narrower selector before extracting app-shell text.",
|
|
5066
|
+
tool: "agent_browser" as const,
|
|
5067
|
+
}));
|
|
5068
|
+
}
|
|
5069
|
+
|
|
3458
5070
|
function looksLikeFunctionEvalStdin(stdin: string | undefined): boolean {
|
|
3459
5071
|
const trimmed = stdin?.trim();
|
|
3460
5072
|
if (!trimmed) return false;
|
|
@@ -3520,6 +5132,24 @@ function formatArtifactCleanupGuidanceText(guidance: ArtifactCleanupGuidance | u
|
|
|
3520
5132
|
return lines.join("\n");
|
|
3521
5133
|
}
|
|
3522
5134
|
|
|
5135
|
+
async function collectQaAttachedTarget(options: {
|
|
5136
|
+
currentTarget?: SessionTabTarget;
|
|
5137
|
+
cwd: string;
|
|
5138
|
+
sessionName?: string;
|
|
5139
|
+
signal?: AbortSignal;
|
|
5140
|
+
}): Promise<QaAttachedTarget | undefined> {
|
|
5141
|
+
if (!options.sessionName) return undefined;
|
|
5142
|
+
if (options.currentTarget?.title || options.currentTarget?.url) {
|
|
5143
|
+
return { sessionName: options.sessionName, title: options.currentTarget.title, url: options.currentTarget.url };
|
|
5144
|
+
}
|
|
5145
|
+
return collectElectronManagedSessionTarget({ cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5148
|
+
function formatQaAttachedTargetText(target: QaAttachedTarget | undefined): string | undefined {
|
|
5149
|
+
if (!target) return undefined;
|
|
5150
|
+
return ["QA attached target:", target.sessionName, target.title, target.url].filter((part): part is string => typeof part === "string" && part.length > 0).join(" — ");
|
|
5151
|
+
}
|
|
5152
|
+
|
|
3523
5153
|
function buildSelectorTextVisibilityNextActions(options: { diagnostics: SelectorTextVisibilityDiagnostic[]; sessionName?: string }): AgentBrowserNextAction[] {
|
|
3524
5154
|
return options.diagnostics.map((diagnostic, index) => ({
|
|
3525
5155
|
id: index === 0 ? "inspect-visible-text-candidates" : `inspect-visible-text-candidates-${index + 1}`,
|
|
@@ -3944,19 +5574,31 @@ class AsyncExecutionQueue {
|
|
|
3944
5574
|
}
|
|
3945
5575
|
}
|
|
3946
5576
|
|
|
3947
|
-
async function closeManagedSession(options: { cwd: string; sessionName: string; timeoutMs: number }): Promise<
|
|
5577
|
+
async function closeManagedSession(options: { cwd: string; sessionName: string; timeoutMs: number }): Promise<string | undefined> {
|
|
3948
5578
|
const controller = new AbortController();
|
|
3949
5579
|
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
3950
5580
|
let stdoutSpillPath: string | undefined;
|
|
5581
|
+
const closeArgs = ["--session", options.sessionName, "close"];
|
|
3951
5582
|
try {
|
|
3952
5583
|
const processResult = await runAgentBrowserProcess({
|
|
3953
|
-
args:
|
|
5584
|
+
args: closeArgs,
|
|
3954
5585
|
cwd: options.cwd,
|
|
3955
5586
|
signal: controller.signal,
|
|
3956
5587
|
});
|
|
3957
5588
|
stdoutSpillPath = processResult.stdoutSpillPath;
|
|
3958
|
-
|
|
3959
|
-
|
|
5589
|
+
return getAgentBrowserErrorText({
|
|
5590
|
+
aborted: processResult.aborted,
|
|
5591
|
+
command: "close",
|
|
5592
|
+
effectiveArgs: redactInvocationArgs(closeArgs),
|
|
5593
|
+
exitCode: processResult.exitCode,
|
|
5594
|
+
plainTextInspection: false,
|
|
5595
|
+
spawnError: processResult.spawnError,
|
|
5596
|
+
stderr: processResult.stderr,
|
|
5597
|
+
timedOut: processResult.timedOut,
|
|
5598
|
+
timeoutMs: processResult.timeoutMs,
|
|
5599
|
+
});
|
|
5600
|
+
} catch (error) {
|
|
5601
|
+
return error instanceof Error ? error.message : String(error);
|
|
3960
5602
|
} finally {
|
|
3961
5603
|
clearTimeout(timer);
|
|
3962
5604
|
if (stdoutSpillPath) {
|
|
@@ -3991,8 +5633,38 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3991
5633
|
let sessionTabTargetUpdateOrder = 0;
|
|
3992
5634
|
let traceOwners = new Map<string, TraceOwner>();
|
|
3993
5635
|
let artifactManifest: SessionArtifactManifest | undefined;
|
|
5636
|
+
let electronLaunchRecords = new Map<string, ElectronLaunchRecord>();
|
|
5637
|
+
let electronChildProcesses = new Map<string, ChildProcess>();
|
|
3994
5638
|
const managedSessionExecutionQueue = new AsyncExecutionQueue();
|
|
3995
5639
|
|
|
5640
|
+
const cleanupTrackedElectronLaunches = async (records: ElectronLaunchRecord[], cwd: string, timeoutMs = implicitSessionCloseTimeoutMs): Promise<ElectronCleanupResult[]> => {
|
|
5641
|
+
const results: ElectronCleanupResult[] = [];
|
|
5642
|
+
for (const record of records) {
|
|
5643
|
+
const managedSessionCloseError = record.sessionName
|
|
5644
|
+
? await closeManagedSession({ cwd, sessionName: record.sessionName, timeoutMs })
|
|
5645
|
+
: undefined;
|
|
5646
|
+
const cleanupResult = await cleanupElectronLaunchResources({
|
|
5647
|
+
child: electronChildProcesses.get(record.launchId),
|
|
5648
|
+
record,
|
|
5649
|
+
timeoutMs,
|
|
5650
|
+
});
|
|
5651
|
+
const result: ElectronCleanupResult = managedSessionCloseError
|
|
5652
|
+
? {
|
|
5653
|
+
...cleanupResult,
|
|
5654
|
+
partial: true,
|
|
5655
|
+
record: { ...cleanupResult.record, cleanupState: "partial" },
|
|
5656
|
+
remainingResources: [...new Set(["managed-session", ...cleanupResult.remainingResources])],
|
|
5657
|
+
steps: [{ error: managedSessionCloseError, resource: "managed-session", state: "failed" }, ...cleanupResult.steps],
|
|
5658
|
+
summary: `Electron cleanup for ${record.launchId} is partial; managed session close failed.`,
|
|
5659
|
+
}
|
|
5660
|
+
: cleanupResult;
|
|
5661
|
+
results.push(result);
|
|
5662
|
+
electronLaunchRecords.set(record.launchId, result.record);
|
|
5663
|
+
if (!result.partial) electronChildProcesses.delete(record.launchId);
|
|
5664
|
+
}
|
|
5665
|
+
return results;
|
|
5666
|
+
};
|
|
5667
|
+
|
|
3996
5668
|
pi.on("session_start", async (_event, ctx) => {
|
|
3997
5669
|
managedSessionBaseName = createImplicitSessionName(ctx.sessionManager.getSessionId(), ctx.cwd, ephemeralSessionSeed);
|
|
3998
5670
|
const restoredState = restoreManagedSessionStateFromBranch(ctx.sessionManager.getBranch(), managedSessionBaseName);
|
|
@@ -4005,19 +5677,24 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4005
5677
|
sessionTabPinningReasons = new Map([...sessionTabTargets.keys()].map((sessionName) => [sessionName, "restore"]));
|
|
4006
5678
|
sessionTabTargetUpdateOrder = Math.max(getLatestSessionTabTargetOrder(sessionTabTargets), getLatestSessionTabTargetOrder(sessionRefSnapshots));
|
|
4007
5679
|
artifactManifest = restoreArtifactManifestFromBranch(ctx.sessionManager.getBranch());
|
|
5680
|
+
electronLaunchRecords = restoreElectronLaunchRecordsFromBranch(ctx.sessionManager.getBranch());
|
|
5681
|
+
electronChildProcesses = new Map<string, ChildProcess>();
|
|
4008
5682
|
});
|
|
4009
5683
|
|
|
4010
|
-
pi.on("session_shutdown", async (event) => {
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
5684
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
5685
|
+
await managedSessionExecutionQueue.run(async () => {
|
|
5686
|
+
const activeElectronRecords = getActiveElectronRecords(electronLaunchRecords);
|
|
5687
|
+
if (activeElectronRecords.length > 0) {
|
|
5688
|
+
await cleanupTrackedElectronLaunches(activeElectronRecords, ctx?.cwd ?? managedSessionCwd);
|
|
5689
|
+
}
|
|
5690
|
+
if (event?.reason === "quit" && managedSessionActive) {
|
|
4014
5691
|
await closeManagedSession({
|
|
4015
5692
|
cwd: managedSessionCwd,
|
|
4016
5693
|
sessionName: managedSessionName,
|
|
4017
5694
|
timeoutMs: implicitSessionCloseTimeoutMs,
|
|
4018
5695
|
});
|
|
4019
|
-
}
|
|
4020
|
-
}
|
|
5696
|
+
}
|
|
5697
|
+
});
|
|
4021
5698
|
managedSessionActive = false;
|
|
4022
5699
|
sessionTabTargets = new Map<string, OrderedSessionTabTarget>();
|
|
4023
5700
|
sessionRefSnapshots = new Map<string, OrderedSessionRefSnapshot>();
|
|
@@ -4025,6 +5702,8 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4025
5702
|
sessionTabTargetUpdateOrder = 0;
|
|
4026
5703
|
traceOwners = new Map<string, TraceOwner>();
|
|
4027
5704
|
artifactManifest = undefined;
|
|
5705
|
+
electronLaunchRecords = new Map<string, ElectronLaunchRecord>();
|
|
5706
|
+
electronChildProcesses = new Map<string, ChildProcess>();
|
|
4028
5707
|
await cleanupSecureTempArtifacts();
|
|
4029
5708
|
});
|
|
4030
5709
|
|
|
@@ -4080,30 +5759,53 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4080
5759
|
const qaResult = params.qa === undefined ? {} : compileAgentBrowserQaPreset(params.qa);
|
|
4081
5760
|
const sourceLookupResult = params.sourceLookup === undefined ? {} : compileAgentBrowserSourceLookup(params.sourceLookup);
|
|
4082
5761
|
const networkSourceLookupResult = params.networkSourceLookup === undefined ? {} : compileAgentBrowserNetworkSourceLookup(params.networkSourceLookup);
|
|
5762
|
+
const electronResult = params.electron === undefined ? {} : compileAgentBrowserElectron(params.electron);
|
|
4083
5763
|
const hasExplicitArgs = Array.isArray(params.args);
|
|
4084
|
-
const explicitInputModes = [hasExplicitArgs, Boolean(semanticActionResult.compiled), Boolean(jobResult.compiled), Boolean(qaResult.compiled), Boolean(sourceLookupResult.compiled), Boolean(networkSourceLookupResult.compiled)].filter(Boolean).length;
|
|
5764
|
+
const explicitInputModes = [hasExplicitArgs, Boolean(semanticActionResult.compiled), Boolean(jobResult.compiled), Boolean(qaResult.compiled), Boolean(sourceLookupResult.compiled), Boolean(networkSourceLookupResult.compiled), Boolean(electronResult.compiled)].filter(Boolean).length;
|
|
4085
5765
|
const semanticActionError = semanticActionResult.error;
|
|
4086
5766
|
const jobError = jobResult.error;
|
|
4087
5767
|
const qaError = qaResult.error;
|
|
4088
5768
|
const sourceLookupError = sourceLookupResult.error;
|
|
4089
5769
|
const networkSourceLookupError = networkSourceLookupResult.error;
|
|
5770
|
+
const electronError = electronResult.error;
|
|
4090
5771
|
const inputModeError = explicitInputModes !== 1
|
|
4091
|
-
? "Provide exactly one of args, semanticAction, job, qa, sourceLookup, or
|
|
5772
|
+
? "Provide exactly one of args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron."
|
|
4092
5773
|
: undefined;
|
|
4093
5774
|
const compiledSemanticAction = semanticActionResult.compiled;
|
|
4094
5775
|
const compiledQaPreset = qaResult.compiled;
|
|
4095
5776
|
const compiledSourceLookup = sourceLookupResult.compiled;
|
|
4096
5777
|
const compiledNetworkSourceLookup = networkSourceLookupResult.compiled;
|
|
5778
|
+
const compiledElectron = electronResult.compiled;
|
|
4097
5779
|
const compiledJob = jobResult.compiled ?? compiledQaPreset;
|
|
4098
5780
|
const compiledGeneratedBatch = compiledNetworkSourceLookup ?? compiledSourceLookup ?? compiledJob;
|
|
4099
|
-
const toolArgs = compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? params.args ?? [];
|
|
5781
|
+
const toolArgs = compiledElectron ? [] : compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? params.args ?? [];
|
|
4100
5782
|
const toolStdin = compiledGeneratedBatch?.stdin ?? params.stdin;
|
|
4101
5783
|
const redactedArgs = redactInvocationArgs(toolArgs);
|
|
4102
|
-
const generatedStdinError =
|
|
4103
|
-
|
|
5784
|
+
const generatedStdinError = params.stdin !== undefined
|
|
5785
|
+
? compiledGeneratedBatch
|
|
5786
|
+
? "Do not provide stdin with job, qa, sourceLookup, or networkSourceLookup; those modes generate their own batch stdin."
|
|
5787
|
+
: compiledElectron
|
|
5788
|
+
? "Do not provide stdin with electron; electron mode is host-only or manages its own input."
|
|
5789
|
+
: undefined
|
|
5790
|
+
: undefined;
|
|
5791
|
+
const attachedQaSessionError = compiledQaPreset?.checks.attached
|
|
5792
|
+
? params.sessionMode === "fresh"
|
|
5793
|
+
? "qa.attached cannot be used with sessionMode=fresh; attach or launch a session first, then run qa.attached with the current session."
|
|
5794
|
+
: !managedSessionActive
|
|
5795
|
+
? "qa.attached requires an active attached session. Run electron.launch or connect to an Electron debug port first."
|
|
5796
|
+
: undefined
|
|
5797
|
+
: undefined;
|
|
5798
|
+
const validationError = semanticActionError ?? jobError ?? qaError ?? sourceLookupError ?? networkSourceLookupError ?? electronError ?? inputModeError ?? generatedStdinError ?? attachedQaSessionError ?? (compiledElectron ? undefined : validateToolArgs(toolArgs) ?? getBatchAnnotateValidationError(toolArgs, toolStdin));
|
|
4104
5799
|
const redactedCompiledSemanticAction = compiledSemanticAction
|
|
4105
5800
|
? { ...compiledSemanticAction, args: redactInvocationArgs(compiledSemanticAction.args) }
|
|
4106
5801
|
: undefined;
|
|
5802
|
+
const redactedCompiledElectron: CompiledAgentBrowserElectron | undefined = compiledElectron
|
|
5803
|
+
? compiledElectron.action === "list"
|
|
5804
|
+
? { ...compiledElectron, query: compiledElectron.query ? redactSensitiveText(compiledElectron.query) : undefined }
|
|
5805
|
+
: compiledElectron.action === "launch"
|
|
5806
|
+
? { ...compiledElectron, appArgs: compiledElectron.appArgs ? redactInvocationArgs(compiledElectron.appArgs) : undefined }
|
|
5807
|
+
: { ...compiledElectron }
|
|
5808
|
+
: undefined;
|
|
4107
5809
|
const redactedCompiledJobSteps = compiledJob?.steps.map((step) => ({ ...step, args: redactInvocationArgs(step.args) }));
|
|
4108
5810
|
const redactedCompiledJob = compiledJob && redactedCompiledJobSteps
|
|
4109
5811
|
? { ...compiledJob, stdin: JSON.stringify(redactedCompiledJobSteps.map((step) => step.args)), steps: redactedCompiledJobSteps }
|
|
@@ -4132,8 +5834,9 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4132
5834
|
if (validationError) {
|
|
4133
5835
|
return {
|
|
4134
5836
|
content: [{ type: "text", text: validationError }],
|
|
4135
|
-
|
|
5837
|
+
details: {
|
|
4136
5838
|
args: redactedArgs,
|
|
5839
|
+
compiledElectron: redactedCompiledElectron,
|
|
4137
5840
|
compiledJob: redactedCompiledJob,
|
|
4138
5841
|
compiledQaPreset: redactedCompiledQaPreset,
|
|
4139
5842
|
compiledSourceLookup: redactedCompiledSourceLookup,
|
|
@@ -4145,13 +5848,170 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4145
5848
|
isError: true,
|
|
4146
5849
|
};
|
|
4147
5850
|
}
|
|
4148
|
-
|
|
4149
|
-
|
|
5851
|
+
if (compiledElectron?.action === "list") {
|
|
5852
|
+
try {
|
|
5853
|
+
const discovery = await discoverElectronApps({ maxResults: compiledElectron.maxResults, query: compiledElectron.query });
|
|
5854
|
+
return buildElectronListSuccessResult(redactedCompiledElectron ?? compiledElectron, discovery);
|
|
5855
|
+
} catch (error) {
|
|
5856
|
+
return buildElectronListFailureResult(redactedCompiledElectron ?? compiledElectron, error);
|
|
5857
|
+
}
|
|
5858
|
+
}
|
|
5859
|
+
if (compiledElectron?.action === "status") {
|
|
5860
|
+
return managedSessionExecutionQueue.run(async () => {
|
|
5861
|
+
const selection = selectElectronRecords(compiledElectron, electronLaunchRecords);
|
|
5862
|
+
if (selection.error) return buildElectronHostFailureResult({ compiledElectron: redactedCompiledElectron ?? compiledElectron, errorText: selection.error, failureCategory: "validation-error" });
|
|
5863
|
+
const records = selection.records ?? [];
|
|
5864
|
+
const statuses = await Promise.all(records.map((record) => inspectElectronLaunchStatus(record)));
|
|
5865
|
+
const managedSessions = (await Promise.all(records.map((record) => collectElectronManagedSessionTarget({
|
|
5866
|
+
cwd: ctx.cwd,
|
|
5867
|
+
sessionName: record.sessionName,
|
|
5868
|
+
signal,
|
|
5869
|
+
timeoutMs: compiledElectron.timeoutMs,
|
|
5870
|
+
})))).filter((managedSession): managedSession is ElectronManagedSessionTarget => managedSession !== undefined);
|
|
5871
|
+
const mismatches = managedSessions
|
|
5872
|
+
.map((managedSession) => {
|
|
5873
|
+
const record = records.find((candidate) => candidate.sessionName === managedSession.sessionName);
|
|
5874
|
+
const status = record ? statuses.find((candidate) => candidate.launchId === record.launchId) : undefined;
|
|
5875
|
+
return record && status ? buildElectronSessionMismatch({ managedSession, record, statusTargets: status.targets }) : undefined;
|
|
5876
|
+
})
|
|
5877
|
+
.filter((mismatch): mismatch is ElectronSessionMismatch => mismatch !== undefined);
|
|
5878
|
+
return buildElectronStatusResult({
|
|
5879
|
+
compiledElectron: redactedCompiledElectron ?? compiledElectron,
|
|
5880
|
+
managedSessions,
|
|
5881
|
+
mismatches,
|
|
5882
|
+
records,
|
|
5883
|
+
statuses,
|
|
5884
|
+
});
|
|
5885
|
+
});
|
|
5886
|
+
}
|
|
5887
|
+
if (compiledElectron?.action === "probe") {
|
|
5888
|
+
return managedSessionExecutionQueue.run(async () => {
|
|
5889
|
+
const launchRecord = compiledElectron.launchId
|
|
5890
|
+
? electronLaunchRecords.get(compiledElectron.launchId)
|
|
5891
|
+
: findElectronLaunchRecordForSession(managedSessionName, electronLaunchRecords) ?? findUnambiguousActiveElectronLaunchRecord(electronLaunchRecords);
|
|
5892
|
+
if (compiledElectron.launchId && !launchRecord) {
|
|
5893
|
+
return buildElectronHostFailureResult({
|
|
5894
|
+
compiledElectron: redactedCompiledElectron ?? compiledElectron,
|
|
5895
|
+
errorText: `No wrapper-tracked Electron launch found for launchId ${compiledElectron.launchId}.`,
|
|
5896
|
+
failureCategory: "validation-error",
|
|
5897
|
+
});
|
|
5898
|
+
}
|
|
5899
|
+
if (compiledElectron.launchId && !launchRecord?.sessionName) {
|
|
5900
|
+
return buildElectronHostFailureResult({
|
|
5901
|
+
compiledElectron: redactedCompiledElectron ?? compiledElectron,
|
|
5902
|
+
errorText: `electron.probe launchId ${compiledElectron.launchId} has no attached managed sessionName; reattach with connect or run electron.launch again.`,
|
|
5903
|
+
failureCategory: "validation-error",
|
|
5904
|
+
});
|
|
5905
|
+
}
|
|
5906
|
+
if (!compiledElectron.launchId && !managedSessionActive) {
|
|
5907
|
+
return buildElectronHostFailureResult({
|
|
5908
|
+
compiledElectron: redactedCompiledElectron ?? compiledElectron,
|
|
5909
|
+
errorText: "electron.probe requires an active attached session. Run electron.launch or connect to an Electron debug port first.",
|
|
5910
|
+
failureCategory: "validation-error",
|
|
5911
|
+
});
|
|
5912
|
+
}
|
|
5913
|
+
const probeSessionName = compiledElectron.launchId ? launchRecord?.sessionName : managedSessionName;
|
|
5914
|
+
if (!probeSessionName) {
|
|
5915
|
+
return buildElectronHostFailureResult({
|
|
5916
|
+
compiledElectron: redactedCompiledElectron ?? compiledElectron,
|
|
5917
|
+
errorText: "electron.probe could not resolve a managed session to inspect.",
|
|
5918
|
+
failureCategory: "validation-error",
|
|
5919
|
+
});
|
|
5920
|
+
}
|
|
5921
|
+
try {
|
|
5922
|
+
const status = launchRecord ? await inspectElectronLaunchStatus(launchRecord) : undefined;
|
|
5923
|
+
const probe = await collectElectronProbe({ cwd: ctx.cwd, sessionName: probeSessionName, signal, timeoutMs: compiledElectron.timeoutMs });
|
|
5924
|
+
const managedSession: ElectronManagedSessionTarget = {
|
|
5925
|
+
sessionName: probe.sessionName,
|
|
5926
|
+
title: probe.title ?? probe.activeTab?.title,
|
|
5927
|
+
url: probe.url ?? probe.activeTab?.url,
|
|
5928
|
+
};
|
|
5929
|
+
const sessionMismatch = launchRecord && status
|
|
5930
|
+
? buildElectronSessionMismatch({ managedSession, record: launchRecord, statusTargets: status.targets })
|
|
5931
|
+
: undefined;
|
|
5932
|
+
const probeContextNote = !launchRecord
|
|
5933
|
+
? "No wrapper-tracked Electron launch matched this current managed session."
|
|
5934
|
+
: !compiledElectron.launchId && launchRecord.sessionName && launchRecord.sessionName !== probe.sessionName
|
|
5935
|
+
? `single active Electron launch ${launchRecord.launchId} uses wrapper session ${launchRecord.sessionName}; pass electron.probe.launchId to inspect that launch session directly.`
|
|
5936
|
+
: undefined;
|
|
5937
|
+
const probeContext: ElectronProbeContext = {
|
|
5938
|
+
launchId: launchRecord?.launchId,
|
|
5939
|
+
mode: compiledElectron.launchId ? "launchId" : "current-managed-session",
|
|
5940
|
+
note: probeContextNote,
|
|
5941
|
+
sessionName: probe.sessionName,
|
|
5942
|
+
};
|
|
5943
|
+
const sessionTabTarget = normalizeSessionTabTarget({
|
|
5944
|
+
title: probe.title ?? probe.activeTab?.title ?? probe.refSnapshot?.target?.title,
|
|
5945
|
+
url: probe.url ?? probe.activeTab?.url ?? probe.refSnapshot?.target?.url,
|
|
5946
|
+
});
|
|
5947
|
+
const order = ++sessionTabTargetUpdateOrder;
|
|
5948
|
+
if (sessionTabTarget) sessionTabTargets.set(probe.sessionName, { order, target: sessionTabTarget });
|
|
5949
|
+
if (probe.refSnapshot) {
|
|
5950
|
+
sessionRefSnapshots.set(probe.sessionName, {
|
|
5951
|
+
...probe.refSnapshot,
|
|
5952
|
+
order,
|
|
5953
|
+
target: probe.refSnapshot.target ?? sessionTabTarget,
|
|
5954
|
+
});
|
|
5955
|
+
}
|
|
5956
|
+
return buildElectronProbeResult({
|
|
5957
|
+
compiledElectron: redactedCompiledElectron ?? compiledElectron,
|
|
5958
|
+
mismatch: sessionMismatch,
|
|
5959
|
+
probe,
|
|
5960
|
+
probeContext,
|
|
5961
|
+
record: launchRecord,
|
|
5962
|
+
sessionTabTarget,
|
|
5963
|
+
status,
|
|
5964
|
+
});
|
|
5965
|
+
} catch (error) {
|
|
5966
|
+
const errorText = error instanceof Error ? error.message : String(error);
|
|
5967
|
+
return buildElectronHostFailureResult({ compiledElectron: redactedCompiledElectron ?? compiledElectron, errorText: `Electron probe failed: ${errorText}`, failureCategory: "upstream-error" });
|
|
5968
|
+
}
|
|
5969
|
+
});
|
|
5970
|
+
}
|
|
5971
|
+
if (compiledElectron?.action === "cleanup") {
|
|
5972
|
+
const selection = selectElectronRecords(compiledElectron, electronLaunchRecords);
|
|
5973
|
+
if (selection.error) return buildElectronHostFailureResult({ compiledElectron: redactedCompiledElectron ?? compiledElectron, errorText: selection.error, failureCategory: "validation-error" });
|
|
5974
|
+
const cleanupResults = await cleanupTrackedElectronLaunches(selection.records ?? [], ctx.cwd, compiledElectron.timeoutMs ?? implicitSessionCloseTimeoutMs);
|
|
5975
|
+
return buildElectronCleanupResult(redactedCompiledElectron ?? compiledElectron, cleanupResults);
|
|
5976
|
+
}
|
|
4150
5977
|
|
|
4151
5978
|
const tabTargetUpdateOrder = ++sessionTabTargetUpdateOrder;
|
|
4152
5979
|
const runTool = async (): Promise<AgentBrowserToolResult> => {
|
|
4153
|
-
|
|
5980
|
+
let runtimeToolArgs = toolArgs;
|
|
5981
|
+
let runtimeToolStdin = toolStdin;
|
|
5982
|
+
let electronLaunch: ElectronLaunchSuccess | undefined;
|
|
5983
|
+
let electronHandoff: ElectronHandoffSummary | undefined;
|
|
5984
|
+
let electronFailedConnectCleanup: ElectronCleanupResult | undefined;
|
|
5985
|
+
const sessionMode = compiledElectron?.action === "launch" ? "fresh" : params.sessionMode ?? DEFAULT_SESSION_MODE;
|
|
4154
5986
|
const freshSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal + 1);
|
|
5987
|
+
if (compiledElectron?.action === "launch") {
|
|
5988
|
+
const launchResult = await launchElectronApp(compiledElectron);
|
|
5989
|
+
if (!launchResult.ok) {
|
|
5990
|
+
const managedSessionOutcome = buildManagedSessionOutcome({
|
|
5991
|
+
activeAfter: managedSessionActive,
|
|
5992
|
+
activeBefore: managedSessionActive,
|
|
5993
|
+
attemptedSessionName: freshSessionName,
|
|
5994
|
+
command: "connect",
|
|
5995
|
+
currentSessionName: managedSessionName,
|
|
5996
|
+
previousSessionName: managedSessionName,
|
|
5997
|
+
sessionMode: "fresh",
|
|
5998
|
+
succeeded: false,
|
|
5999
|
+
});
|
|
6000
|
+
return buildElectronHostFailureResult({
|
|
6001
|
+
compiledElectron: redactedCompiledElectron ?? compiledElectron,
|
|
6002
|
+
errorText: launchResult.failure.error,
|
|
6003
|
+
failureCategory: getElectronLaunchFailureCategory(launchResult.failure),
|
|
6004
|
+
launchFailure: launchResult.failure,
|
|
6005
|
+
managedSessionOutcome,
|
|
6006
|
+
status: launchResult.failure.reason,
|
|
6007
|
+
});
|
|
6008
|
+
}
|
|
6009
|
+
electronLaunch = launchResult.value;
|
|
6010
|
+
runtimeToolArgs = ["connect", electronLaunch.connectArg];
|
|
6011
|
+
runtimeToolStdin = undefined;
|
|
6012
|
+
}
|
|
6013
|
+
const preparedArgs = await prepareAgentBrowserArgs(runtimeToolArgs, runtimeToolStdin, ctx.cwd);
|
|
6014
|
+
const userRequestedJson = runtimeToolArgs.includes("--json");
|
|
4155
6015
|
let executionPlan = buildExecutionPlan(preparedArgs.args, {
|
|
4156
6016
|
freshSessionName,
|
|
4157
6017
|
managedSessionActive,
|
|
@@ -4185,8 +6045,9 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4185
6045
|
if (executionPlan.validationError) {
|
|
4186
6046
|
return {
|
|
4187
6047
|
content: [{ type: "text", text: executionPlan.validationError }],
|
|
4188
|
-
|
|
6048
|
+
details: {
|
|
4189
6049
|
args: redactedArgs,
|
|
6050
|
+
compiledElectron: redactedCompiledElectron,
|
|
4190
6051
|
compiledJob: redactedCompiledJob,
|
|
4191
6052
|
compiledQaPreset: redactedCompiledQaPreset,
|
|
4192
6053
|
compiledSourceLookup: redactedCompiledSourceLookup,
|
|
@@ -4206,7 +6067,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4206
6067
|
const exactSensitiveValues = getExactSensitiveStdinValues({
|
|
4207
6068
|
command: executionPlan.commandInfo.command,
|
|
4208
6069
|
commandTokens,
|
|
4209
|
-
stdin:
|
|
6070
|
+
stdin: runtimeToolStdin,
|
|
4210
6071
|
});
|
|
4211
6072
|
const traceOwnerGuardMessage = getTraceOwnerGuardMessage({
|
|
4212
6073
|
command: executionPlan.commandInfo.command,
|
|
@@ -4233,7 +6094,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4233
6094
|
const stdinValidationError = validateStdinCommandContract({
|
|
4234
6095
|
command: executionPlan.commandInfo.command,
|
|
4235
6096
|
commandTokens,
|
|
4236
|
-
stdin:
|
|
6097
|
+
stdin: runtimeToolStdin,
|
|
4237
6098
|
});
|
|
4238
6099
|
if (stdinValidationError) {
|
|
4239
6100
|
return {
|
|
@@ -4251,7 +6112,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4251
6112
|
isError: true,
|
|
4252
6113
|
};
|
|
4253
6114
|
}
|
|
4254
|
-
const waitIpcTimeoutError = validateWaitIpcTimeoutContract(commandTokens,
|
|
6115
|
+
const waitIpcTimeoutError = validateWaitIpcTimeoutContract(commandTokens, runtimeToolStdin);
|
|
4255
6116
|
if (waitIpcTimeoutError) {
|
|
4256
6117
|
return {
|
|
4257
6118
|
content: [{ type: "text", text: waitIpcTimeoutError }],
|
|
@@ -4280,7 +6141,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4280
6141
|
commandTokens,
|
|
4281
6142
|
currentTarget: priorSessionTabTarget,
|
|
4282
6143
|
refSnapshot: resolvedSemanticActionRefSnapshot ?? priorRefSnapshotState,
|
|
4283
|
-
stdin:
|
|
6144
|
+
stdin: runtimeToolStdin,
|
|
4284
6145
|
});
|
|
4285
6146
|
if (staleRefPreflight) {
|
|
4286
6147
|
return {
|
|
@@ -4304,7 +6165,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4304
6165
|
let includePinnedNavigationSummary = false;
|
|
4305
6166
|
let sessionTabCorrection: OpenResultTabCorrection | undefined;
|
|
4306
6167
|
let processArgs = executionPlan.effectiveArgs;
|
|
4307
|
-
let processStdin = preparedArgs.stdin ??
|
|
6168
|
+
let processStdin = preparedArgs.stdin ?? runtimeToolStdin;
|
|
4308
6169
|
if (
|
|
4309
6170
|
priorSessionTabTarget &&
|
|
4310
6171
|
shouldPinSessionTabForCommand({
|
|
@@ -4312,7 +6173,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4312
6173
|
commandTokens,
|
|
4313
6174
|
pinningRequired: sessionTabPinningReason !== undefined,
|
|
4314
6175
|
sessionName: executionPlan.sessionName,
|
|
4315
|
-
stdin:
|
|
6176
|
+
stdin: runtimeToolStdin,
|
|
4316
6177
|
})
|
|
4317
6178
|
) {
|
|
4318
6179
|
const plannedSessionTabSelection = await collectSessionTabSelection({
|
|
@@ -4322,7 +6183,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4322
6183
|
target: priorSessionTabTarget,
|
|
4323
6184
|
});
|
|
4324
6185
|
if (plannedSessionTabSelection && executionPlan.sessionName) {
|
|
4325
|
-
if (executionPlan.commandInfo.command === "eval" &&
|
|
6186
|
+
if (executionPlan.commandInfo.command === "eval" && runtimeToolStdin !== undefined) {
|
|
4326
6187
|
const appliedSessionTabSelection = await applyOpenResultTabCorrection({
|
|
4327
6188
|
correction: plannedSessionTabSelection,
|
|
4328
6189
|
cwd: ctx.cwd,
|
|
@@ -4354,7 +6215,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4354
6215
|
command: executionPlan.commandInfo.command,
|
|
4355
6216
|
commandTokens,
|
|
4356
6217
|
selectedTab: plannedSessionTabSelection.selectedTab,
|
|
4357
|
-
stdin:
|
|
6218
|
+
stdin: runtimeToolStdin,
|
|
4358
6219
|
});
|
|
4359
6220
|
if (pinnedBatchPlan && "error" in pinnedBatchPlan) {
|
|
4360
6221
|
return {
|
|
@@ -4426,12 +6287,32 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4426
6287
|
succeeded: false,
|
|
4427
6288
|
});
|
|
4428
6289
|
const managedSessionOutcomeText = formatManagedSessionOutcomeText(managedSessionOutcome);
|
|
6290
|
+
let missingBinaryElectronCleanup: ElectronCleanupResult | undefined;
|
|
6291
|
+
let missingBinaryElectronRecord: ElectronLaunchRecord | undefined;
|
|
6292
|
+
if (electronLaunch) {
|
|
6293
|
+
missingBinaryElectronCleanup = await cleanupElectronLaunchResources({
|
|
6294
|
+
child: electronLaunch.child,
|
|
6295
|
+
record: electronLaunch.record,
|
|
6296
|
+
timeoutMs: implicitSessionCloseTimeoutMs,
|
|
6297
|
+
});
|
|
6298
|
+
missingBinaryElectronRecord = missingBinaryElectronCleanup.record;
|
|
6299
|
+
}
|
|
6300
|
+
const textParts = [errorText, managedSessionOutcomeText, missingBinaryElectronCleanup ? `Electron cleanup after failed attach: ${missingBinaryElectronCleanup.summary}` : undefined]
|
|
6301
|
+
.filter((part): part is string => part !== undefined && part.length > 0);
|
|
4429
6302
|
return {
|
|
4430
|
-
content: [{ type: "text", text:
|
|
6303
|
+
content: [{ type: "text", text: textParts.join("\n\n") }],
|
|
4431
6304
|
details: {
|
|
4432
6305
|
args: redactedArgs,
|
|
4433
6306
|
compatibilityWorkaround,
|
|
4434
6307
|
effectiveArgs: redactedProcessArgs,
|
|
6308
|
+
electron: missingBinaryElectronRecord ? {
|
|
6309
|
+
action: "launch" as const,
|
|
6310
|
+
cleanup: missingBinaryElectronCleanup,
|
|
6311
|
+
launch: missingBinaryElectronRecord,
|
|
6312
|
+
status: "failed" as const,
|
|
6313
|
+
targets: electronLaunch?.targets,
|
|
6314
|
+
version: electronLaunch?.version,
|
|
6315
|
+
} : undefined,
|
|
4435
6316
|
managedSessionOutcome,
|
|
4436
6317
|
sessionMode,
|
|
4437
6318
|
sessionTabCorrection,
|
|
@@ -4519,7 +6400,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4519
6400
|
if (
|
|
4520
6401
|
succeeded &&
|
|
4521
6402
|
executionPlan.sessionName &&
|
|
4522
|
-
hasLaunchScopedTabCorrectionFlag(
|
|
6403
|
+
hasLaunchScopedTabCorrectionFlag(runtimeToolArgs) &&
|
|
4523
6404
|
(executionPlan.commandInfo.command === "goto" ||
|
|
4524
6405
|
executionPlan.commandInfo.command === "navigate" ||
|
|
4525
6406
|
executionPlan.commandInfo.command === "open")
|
|
@@ -4555,6 +6436,10 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4555
6436
|
subcommand: executionPlan.commandInfo.subcommand,
|
|
4556
6437
|
});
|
|
4557
6438
|
let aboutBlankSessionMismatch: AboutBlankSessionMismatch | undefined;
|
|
6439
|
+
let electronPostCommandHealth: ElectronPostCommandHealthDiagnostic | undefined;
|
|
6440
|
+
let electronRefFreshnessDiagnostic: ElectronRefFreshnessDiagnostic | undefined;
|
|
6441
|
+
let electronSessionMismatch: ElectronSessionMismatch | undefined;
|
|
6442
|
+
let electronStatusAfterCommand: ElectronLaunchStatus | undefined;
|
|
4558
6443
|
const shouldTreatAboutBlankAsMismatch =
|
|
4559
6444
|
succeeded &&
|
|
4560
6445
|
priorSessionTabTarget !== undefined &&
|
|
@@ -4562,6 +6447,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4562
6447
|
isAboutBlankSessionTabTarget(observedSessionTabTarget ?? currentSessionTabTarget) &&
|
|
4563
6448
|
!commandExplicitlyTargetsAboutBlank(commandTokens);
|
|
4564
6449
|
if (shouldTreatAboutBlankAsMismatch && priorSessionTabTarget) {
|
|
6450
|
+
const aboutBlankObservedTarget = observedSessionTabTarget ?? currentSessionTabTarget;
|
|
4565
6451
|
const aboutBlankRecovery = await collectSessionTabSelection({
|
|
4566
6452
|
cwd: ctx.cwd,
|
|
4567
6453
|
sessionName: executionPlan.sessionName,
|
|
@@ -4586,6 +6472,19 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4586
6472
|
targetTitle: priorSessionTabTarget.title,
|
|
4587
6473
|
targetUrl: priorSessionTabTarget.url,
|
|
4588
6474
|
};
|
|
6475
|
+
const electronRecord = findElectronLaunchRecordForSession(executionPlan.sessionName, electronLaunchRecords);
|
|
6476
|
+
if (electronRecord && executionPlan.sessionName) {
|
|
6477
|
+
electronStatusAfterCommand = await inspectElectronLaunchStatus(electronRecord);
|
|
6478
|
+
electronSessionMismatch = buildElectronSessionMismatch({
|
|
6479
|
+
managedSession: {
|
|
6480
|
+
sessionName: executionPlan.sessionName,
|
|
6481
|
+
title: aboutBlankObservedTarget?.title,
|
|
6482
|
+
url: aboutBlankObservedTarget?.url ?? "about:blank",
|
|
6483
|
+
},
|
|
6484
|
+
record: electronRecord,
|
|
6485
|
+
statusTargets: electronStatusAfterCommand.targets,
|
|
6486
|
+
});
|
|
6487
|
+
}
|
|
4589
6488
|
currentSessionTabTarget = priorSessionTabTarget;
|
|
4590
6489
|
}
|
|
4591
6490
|
if (
|
|
@@ -4619,15 +6518,55 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4619
6518
|
}
|
|
4620
6519
|
}
|
|
4621
6520
|
}
|
|
6521
|
+
const electronRecordForCommand = findElectronLaunchRecordForSession(executionPlan.sessionName, electronLaunchRecords);
|
|
6522
|
+
if (succeeded && electronRecordForCommand && shouldInspectElectronPostCommandHealth(executionPlan.commandInfo.command)) {
|
|
6523
|
+
electronStatusAfterCommand ??= await inspectElectronLaunchStatus(electronRecordForCommand);
|
|
6524
|
+
electronPostCommandHealth = buildElectronPostCommandHealthDiagnostic({
|
|
6525
|
+
command: executionPlan.commandInfo.command,
|
|
6526
|
+
record: electronRecordForCommand,
|
|
6527
|
+
status: electronStatusAfterCommand,
|
|
6528
|
+
target: observedSessionTabTarget ?? currentSessionTabTarget,
|
|
6529
|
+
});
|
|
6530
|
+
if (electronPostCommandHealth && electronPostCommandHealth.reason !== "process-dead") {
|
|
6531
|
+
await sleepMs(ELECTRON_POST_COMMAND_STATUS_SETTLE_MS);
|
|
6532
|
+
electronStatusAfterCommand = await inspectElectronLaunchStatus(electronRecordForCommand);
|
|
6533
|
+
electronPostCommandHealth = buildElectronPostCommandHealthDiagnostic({
|
|
6534
|
+
command: executionPlan.commandInfo.command,
|
|
6535
|
+
record: electronRecordForCommand,
|
|
6536
|
+
status: electronStatusAfterCommand,
|
|
6537
|
+
target: observedSessionTabTarget ?? currentSessionTabTarget,
|
|
6538
|
+
});
|
|
6539
|
+
}
|
|
6540
|
+
if (electronPostCommandHealth) {
|
|
6541
|
+
succeeded = false;
|
|
6542
|
+
}
|
|
6543
|
+
}
|
|
6544
|
+
let fillVerificationDiagnostic: FillVerificationDiagnostic | undefined;
|
|
4622
6545
|
let selectorTextVisibilityDiagnostics: SelectorTextVisibilityDiagnostic[] = [];
|
|
6546
|
+
let electronBroadGetTextScopeDiagnostics: ElectronBroadGetTextScopeDiagnostic[] = [];
|
|
4623
6547
|
const timeoutPartialProgress = processResult.timedOut ? await collectTimeoutPartialProgress({
|
|
4624
6548
|
command: executionPlan.commandInfo.command,
|
|
4625
6549
|
compiledJob,
|
|
4626
6550
|
cwd: ctx.cwd,
|
|
4627
6551
|
sessionName: executionPlan.sessionName,
|
|
4628
|
-
stdin:
|
|
6552
|
+
stdin: runtimeToolStdin,
|
|
4629
6553
|
}) : undefined;
|
|
4630
|
-
if (succeeded &&
|
|
6554
|
+
if (succeeded && electronRecordForCommand) {
|
|
6555
|
+
fillVerificationDiagnostic = await collectFillVerificationDiagnostic({
|
|
6556
|
+
commandTokens,
|
|
6557
|
+
cwd: ctx.cwd,
|
|
6558
|
+
sessionName: executionPlan.sessionName,
|
|
6559
|
+
signal,
|
|
6560
|
+
});
|
|
6561
|
+
electronRefFreshnessDiagnostic = buildElectronRefFreshnessDiagnostic({
|
|
6562
|
+
command: executionPlan.commandInfo.command,
|
|
6563
|
+
commandTokens,
|
|
6564
|
+
record: electronRecordForCommand,
|
|
6565
|
+
sessionName: executionPlan.sessionName,
|
|
6566
|
+
stdin: runtimeToolStdin,
|
|
6567
|
+
});
|
|
6568
|
+
}
|
|
6569
|
+
if (succeeded && !sessionTabCorrection && !aboutBlankSessionMismatch && !electronRecordForCommand) {
|
|
4631
6570
|
overlayBlockerDiagnostic = await collectOverlayBlockerDiagnostic({
|
|
4632
6571
|
command: executionPlan.commandInfo.command,
|
|
4633
6572
|
cwd: ctx.cwd,
|
|
@@ -4647,6 +6586,15 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4647
6586
|
sessionName: executionPlan.sessionName,
|
|
4648
6587
|
signal,
|
|
4649
6588
|
});
|
|
6589
|
+
electronBroadGetTextScopeDiagnostics = collectElectronBroadGetTextScopeDiagnostics({
|
|
6590
|
+
commandInfo: executionPlan.commandInfo,
|
|
6591
|
+
commandTokens,
|
|
6592
|
+
currentTarget: currentSessionTabTarget,
|
|
6593
|
+
data: presentationEnvelope?.data,
|
|
6594
|
+
electronLaunchRecords,
|
|
6595
|
+
priorTarget: priorSessionTabTarget,
|
|
6596
|
+
sessionName: executionPlan.sessionName,
|
|
6597
|
+
});
|
|
4650
6598
|
}
|
|
4651
6599
|
const comboboxFocusDiagnostic = succeeded
|
|
4652
6600
|
? await collectComboboxFocusDiagnostic({
|
|
@@ -4750,6 +6698,37 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4750
6698
|
});
|
|
4751
6699
|
}
|
|
4752
6700
|
|
|
6701
|
+
let electronLaunchRecord: ElectronLaunchRecord | undefined;
|
|
6702
|
+
if (electronLaunch) {
|
|
6703
|
+
if (succeeded && executionPlan.sessionName) {
|
|
6704
|
+
electronLaunchRecord = { ...electronLaunch.record, sessionName: executionPlan.sessionName };
|
|
6705
|
+
electronLaunchRecords.set(electronLaunchRecord.launchId, electronLaunchRecord);
|
|
6706
|
+
electronChildProcesses.set(electronLaunchRecord.launchId, electronLaunch.child);
|
|
6707
|
+
const electronHandoffMode = compiledElectron?.action === "launch" ? compiledElectron.handoff : "connect";
|
|
6708
|
+
try {
|
|
6709
|
+
electronHandoff = await collectElectronHandoff({
|
|
6710
|
+
cwd: ctx.cwd,
|
|
6711
|
+
handoff: electronHandoffMode,
|
|
6712
|
+
sessionName: executionPlan.sessionName,
|
|
6713
|
+
signal,
|
|
6714
|
+
});
|
|
6715
|
+
} catch (error) {
|
|
6716
|
+
electronHandoff = { error: error instanceof Error ? error.message : String(error), handoff: electronHandoffMode };
|
|
6717
|
+
}
|
|
6718
|
+
if (electronHandoff.refSnapshot) {
|
|
6719
|
+
currentRefSnapshot = electronHandoff.refSnapshot;
|
|
6720
|
+
sessionRefSnapshots.set(executionPlan.sessionName, { ...electronHandoff.refSnapshot, order: tabTargetUpdateOrder });
|
|
6721
|
+
if (electronHandoff.refSnapshot.target) {
|
|
6722
|
+
currentSessionTabTarget = electronHandoff.refSnapshot.target;
|
|
6723
|
+
sessionTabTargets.set(executionPlan.sessionName, { order: tabTargetUpdateOrder, target: electronHandoff.refSnapshot.target });
|
|
6724
|
+
}
|
|
6725
|
+
}
|
|
6726
|
+
} else {
|
|
6727
|
+
electronFailedConnectCleanup = await cleanupElectronLaunchResources({ child: electronLaunch.child, record: electronLaunch.record, timeoutMs: implicitSessionCloseTimeoutMs });
|
|
6728
|
+
electronLaunchRecord = electronFailedConnectCleanup.record;
|
|
6729
|
+
}
|
|
6730
|
+
}
|
|
6731
|
+
|
|
4753
6732
|
const errorText = getAgentBrowserErrorText({
|
|
4754
6733
|
aborted: processResult.aborted,
|
|
4755
6734
|
command: executionPlan.commandInfo.command,
|
|
@@ -4758,7 +6737,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4758
6737
|
exitCode: processResult.exitCode,
|
|
4759
6738
|
parseError,
|
|
4760
6739
|
plainTextInspection,
|
|
4761
|
-
staleRefArgs: getStaleRefArgs(commandTokens,
|
|
6740
|
+
staleRefArgs: getStaleRefArgs(commandTokens, runtimeToolStdin),
|
|
4762
6741
|
spawnError: processResult.spawnError,
|
|
4763
6742
|
stderr: processResult.stderr,
|
|
4764
6743
|
timedOut: processResult.timedOut,
|
|
@@ -4815,7 +6794,19 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4815
6794
|
artifactManifest = presentation.artifactManifest;
|
|
4816
6795
|
}
|
|
4817
6796
|
const qaPreset = compiledQaPreset ? analyzeQaPresetResults(presentationEnvelope?.data) : undefined;
|
|
4818
|
-
const
|
|
6797
|
+
const qaAttachedTarget = compiledQaPreset?.checks.attached
|
|
6798
|
+
? await collectQaAttachedTarget({ currentTarget: currentSessionTabTarget ?? priorSessionTabTarget, cwd: ctx.cwd, sessionName: executionPlan.sessionName, signal })
|
|
6799
|
+
: undefined;
|
|
6800
|
+
const sourceLookupElectronContext = compiledSourceLookup ? getSourceLookupElectronContext({
|
|
6801
|
+
currentTarget: currentSessionTabTarget,
|
|
6802
|
+
electronLaunchRecords,
|
|
6803
|
+
priorTarget: priorSessionTabTarget,
|
|
6804
|
+
sessionName: executionPlan.sessionName,
|
|
6805
|
+
}) : undefined;
|
|
6806
|
+
const sourceLookup = compiledSourceLookup ? await analyzeSourceLookupResults(presentationEnvelope?.data, compiledSourceLookup, ctx.cwd, {
|
|
6807
|
+
electronContext: sourceLookupElectronContext,
|
|
6808
|
+
workspaceRoot: ctx.cwd,
|
|
6809
|
+
}) : undefined;
|
|
4819
6810
|
const networkSourceLookup = compiledNetworkSourceLookup ? redactNetworkSourceLookupAnalysis(await analyzeNetworkSourceLookupResults(presentationEnvelope?.data, compiledNetworkSourceLookup, ctx.cwd)) : undefined;
|
|
4820
6811
|
if (networkSourceLookup && presentation.content[0]?.type === "text") {
|
|
4821
6812
|
presentation.content[0] = { ...presentation.content[0], text: `${networkSourceLookup.summary}\n\n${presentation.content[0].text}` };
|
|
@@ -4839,13 +6830,19 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4839
6830
|
presentation.content.unshift({ type: "text", text: qaPreset.summary });
|
|
4840
6831
|
}
|
|
4841
6832
|
}
|
|
6833
|
+
const qaAttachedTargetText = formatQaAttachedTargetText(qaAttachedTarget);
|
|
6834
|
+
if (qaAttachedTargetText && presentation.content[0]?.type === "text") {
|
|
6835
|
+
presentation.content[0] = { ...presentation.content[0], text: `${qaAttachedTargetText}\n\n${presentation.content[0].text}` };
|
|
6836
|
+
} else if (qaAttachedTargetText) {
|
|
6837
|
+
presentation.content.unshift({ type: "text", text: qaAttachedTargetText });
|
|
6838
|
+
}
|
|
4842
6839
|
if (managedSessionOutcome && managedSessionOutcome.succeeded !== succeeded) {
|
|
4843
6840
|
managedSessionOutcome = { ...managedSessionOutcome, succeeded };
|
|
4844
6841
|
}
|
|
4845
6842
|
const evalStdinHint = getEvalStdinHint({
|
|
4846
6843
|
command: executionPlan.commandInfo.command,
|
|
4847
6844
|
data: presentationEnvelope?.data,
|
|
4848
|
-
stdin:
|
|
6845
|
+
stdin: runtimeToolStdin,
|
|
4849
6846
|
});
|
|
4850
6847
|
const resultArtifactManifest = presentation.artifactManifest ?? artifactManifest;
|
|
4851
6848
|
const artifactCleanup = await getArtifactCleanupGuidance({
|
|
@@ -4854,7 +6851,11 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4854
6851
|
manifest: resultArtifactManifest,
|
|
4855
6852
|
succeeded,
|
|
4856
6853
|
});
|
|
4857
|
-
const warningText =
|
|
6854
|
+
const warningText = electronPostCommandHealth
|
|
6855
|
+
? formatElectronPostCommandHealthText(electronPostCommandHealth)
|
|
6856
|
+
: electronSessionMismatch
|
|
6857
|
+
? formatElectronSessionMismatchText(electronSessionMismatch)
|
|
6858
|
+
: aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
|
|
4858
6859
|
const contentWithSessionWarnings = userRequestedJson && !plainTextInspection
|
|
4859
6860
|
? buildJsonVisibleContent({
|
|
4860
6861
|
error: presentationEnvelope?.error,
|
|
@@ -4888,18 +6889,18 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4888
6889
|
command: executionPlan.commandInfo.command,
|
|
4889
6890
|
confirmationRequired: presentation.summary.startsWith("Confirmation required"),
|
|
4890
6891
|
errorText: errorText ?? presentation.summary,
|
|
4891
|
-
failureCategory: presentation.failureCategory ?? presentation.batchFailure?.failedStep.failureCategory,
|
|
6892
|
+
failureCategory: presentation.failureCategory ?? presentation.batchFailure?.failedStep.failureCategory ?? (electronPostCommandHealth ? "tab-drift" : undefined),
|
|
4892
6893
|
inspection: plainTextInspection,
|
|
4893
6894
|
parseError,
|
|
4894
6895
|
savedFile: presentation.savedFile,
|
|
4895
6896
|
spawnError: processResult.spawnError?.message,
|
|
4896
6897
|
succeeded,
|
|
4897
|
-
tabDrift: !succeeded && (aboutBlankSessionMismatch !== undefined || sessionTabCorrection !== undefined),
|
|
6898
|
+
tabDrift: !succeeded && (aboutBlankSessionMismatch !== undefined || electronPostCommandHealth !== undefined || sessionTabCorrection !== undefined),
|
|
4898
6899
|
timedOut: processResult.timedOut,
|
|
4899
6900
|
validationError: undefined,
|
|
4900
6901
|
});
|
|
4901
6902
|
let visibleRefFallbackDiagnostic: VisibleRefFallbackDiagnostic | undefined;
|
|
4902
|
-
const visibleRefFallbackSessionName = executionPlan.sessionName ?? extractExplicitSessionName(
|
|
6903
|
+
const visibleRefFallbackSessionName = executionPlan.sessionName ?? extractExplicitSessionName(runtimeToolArgs);
|
|
4903
6904
|
if (categoryDetails.failureCategory === "selector-not-found") {
|
|
4904
6905
|
visibleRefFallbackDiagnostic = await collectVisibleRefFallbackDiagnostic({
|
|
4905
6906
|
commandTokens,
|
|
@@ -4920,6 +6921,18 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4920
6921
|
if (visibleRefFallbackDiagnostic) {
|
|
4921
6922
|
(nextActions ??= []).push(...buildVisibleRefFallbackNextActions({ diagnostic: visibleRefFallbackDiagnostic, sessionName: visibleRefFallbackSessionName }));
|
|
4922
6923
|
}
|
|
6924
|
+
if (electronPostCommandHealth) {
|
|
6925
|
+
const electronRecord = electronLaunchRecords.get(electronPostCommandHealth.launchId);
|
|
6926
|
+
if (electronRecord) {
|
|
6927
|
+
nextActions = appendUniqueNextActions(nextActions ?? [], buildElectronLifecycleNextActions(electronRecord));
|
|
6928
|
+
}
|
|
6929
|
+
}
|
|
6930
|
+
if (electronSessionMismatch) {
|
|
6931
|
+
const electronRecord = electronLaunchRecords.get(electronSessionMismatch.launchId);
|
|
6932
|
+
if (electronRecord) {
|
|
6933
|
+
nextActions = appendUniqueNextActions(nextActions ?? [], buildElectronMismatchNextActions(electronRecord, electronSessionMismatch.liveTarget));
|
|
6934
|
+
}
|
|
6935
|
+
}
|
|
4923
6936
|
if (categoryDetails.failureCategory === "selector-not-found" && redactedCompiledSemanticAction) {
|
|
4924
6937
|
const candidateActions = buildSemanticActionCandidateActions(redactedCompiledSemanticAction);
|
|
4925
6938
|
if (candidateActions.length > 0) {
|
|
@@ -4929,9 +6942,21 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4929
6942
|
if (overlayBlockerDiagnostic) {
|
|
4930
6943
|
(nextActions ??= []).push(...buildOverlayBlockerNextActions({ diagnostic: overlayBlockerDiagnostic, sessionName: executionPlan.sessionName }));
|
|
4931
6944
|
}
|
|
6945
|
+
if (fillVerificationDiagnostic) {
|
|
6946
|
+
nextActions = appendUniqueNextActions(nextActions ?? [], buildFillVerificationNextActions(fillVerificationDiagnostic, executionPlan.sessionName));
|
|
6947
|
+
}
|
|
6948
|
+
if (electronRefFreshnessDiagnostic) {
|
|
6949
|
+
nextActions = appendUniqueNextActions(nextActions ?? [], buildElectronRefFreshnessNextActions(executionPlan.sessionName));
|
|
6950
|
+
}
|
|
4932
6951
|
if (selectorTextVisibilityDiagnostics.length > 0) {
|
|
4933
6952
|
(nextActions ??= []).push(...buildSelectorTextVisibilityNextActions({ diagnostics: selectorTextVisibilityDiagnostics, sessionName: executionPlan.sessionName }));
|
|
4934
6953
|
}
|
|
6954
|
+
if (electronBroadGetTextScopeDiagnostics.length > 0) {
|
|
6955
|
+
(nextActions ??= []).push(...buildElectronBroadGetTextScopeNextActions({ diagnostics: electronBroadGetTextScopeDiagnostics, sessionName: executionPlan.sessionName }));
|
|
6956
|
+
}
|
|
6957
|
+
if (sourceLookup?.electronContext) {
|
|
6958
|
+
nextActions = appendUniqueNextActions(nextActions ?? [], buildSourceLookupElectronNextActions(sourceLookup));
|
|
6959
|
+
}
|
|
4935
6960
|
if (scrollNoopDiagnostic) {
|
|
4936
6961
|
(nextActions ??= []).push(...buildScrollNoopNextActions(executionPlan.sessionName));
|
|
4937
6962
|
}
|
|
@@ -4947,11 +6972,20 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4947
6972
|
tool: "agent_browser" as const,
|
|
4948
6973
|
});
|
|
4949
6974
|
}
|
|
6975
|
+
if (electronLaunchRecord) {
|
|
6976
|
+
(nextActions ??= []).push(...(buildAgentBrowserNextActions({
|
|
6977
|
+
electron: { launchId: electronLaunchRecord.launchId, sessionName: electronLaunchRecord.sessionName, status: electronLaunchRecord.cleanupState },
|
|
6978
|
+
failureCategory: categoryDetails.failureCategory,
|
|
6979
|
+
resultCategory: categoryDetails.resultCategory,
|
|
6980
|
+
successCategory: categoryDetails.successCategory,
|
|
6981
|
+
}) ?? []));
|
|
6982
|
+
}
|
|
4950
6983
|
const pageChangeSummary = (scrollNoopDiagnostic || comboboxFocusDiagnostic) && presentation.pageChangeSummary
|
|
4951
6984
|
? { ...presentation.pageChangeSummary, nextActionIds: nextActions?.map((action) => action.id) }
|
|
4952
6985
|
: presentation.pageChangeSummary;
|
|
4953
6986
|
const details = {
|
|
4954
6987
|
args: redactedArgs,
|
|
6988
|
+
compiledElectron: redactedCompiledElectron,
|
|
4955
6989
|
compiledJob: redactedCompiledJob,
|
|
4956
6990
|
compiledQaPreset: redactedCompiledQaPreset,
|
|
4957
6991
|
compiledSourceLookup: redactedCompiledSourceLookup,
|
|
@@ -4971,8 +7005,22 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4971
7005
|
error: plainTextInspection ? undefined : presentationEnvelope?.error,
|
|
4972
7006
|
inspection: plainTextInspection || undefined,
|
|
4973
7007
|
navigationSummary,
|
|
7008
|
+
electron: electronLaunchRecord ? {
|
|
7009
|
+
action: "launch" as const,
|
|
7010
|
+
cleanup: electronFailedConnectCleanup,
|
|
7011
|
+
handoff: electronHandoff,
|
|
7012
|
+
identifiers: buildElectronIdentifiers(electronLaunchRecord),
|
|
7013
|
+
launch: electronLaunchRecord,
|
|
7014
|
+
profileIsolation: ELECTRON_PROFILE_ISOLATION_DETAILS,
|
|
7015
|
+
status: succeeded ? "succeeded" as const : "failed" as const,
|
|
7016
|
+
targets: electronLaunch?.targets,
|
|
7017
|
+
version: electronLaunch?.version,
|
|
7018
|
+
} : undefined,
|
|
4974
7019
|
...categoryDetails,
|
|
4975
7020
|
aboutBlankSessionMismatch,
|
|
7021
|
+
electronPostCommandHealth,
|
|
7022
|
+
electronRefFreshness: electronRefFreshnessDiagnostic,
|
|
7023
|
+
electronSessionMismatch,
|
|
4976
7024
|
openResultTabCorrection,
|
|
4977
7025
|
effectiveArgs: redactedProcessArgs,
|
|
4978
7026
|
exitCode: processResult.exitCode,
|
|
@@ -4985,11 +7033,15 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4985
7033
|
nextActions,
|
|
4986
7034
|
pageChangeSummary,
|
|
4987
7035
|
overlayBlockers: overlayBlockerDiagnostic,
|
|
7036
|
+
fillVerification: fillVerificationDiagnostic,
|
|
4988
7037
|
visibleRefFallback: visibleRefFallbackDiagnostic,
|
|
4989
7038
|
comboboxFocus: comboboxFocusDiagnostic,
|
|
4990
7039
|
recordingDependencyWarning,
|
|
4991
7040
|
scrollNoop: scrollNoopDiagnostic,
|
|
4992
7041
|
qaPreset,
|
|
7042
|
+
qaAttachedTarget,
|
|
7043
|
+
electronGetTextScopeWarning: electronBroadGetTextScopeDiagnostics[0],
|
|
7044
|
+
electronGetTextScopeWarnings: electronBroadGetTextScopeDiagnostics.length > 1 ? electronBroadGetTextScopeDiagnostics : undefined,
|
|
4993
7045
|
selectorTextVisibility: selectorTextVisibilityDiagnostics[0],
|
|
4994
7046
|
selectorTextVisibilityAll: selectorTextVisibilityDiagnostics.length > 1 ? selectorTextVisibilityDiagnostics : undefined,
|
|
4995
7047
|
evalStdinHint,
|
|
@@ -5016,7 +7068,10 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
5016
7068
|
const visibleRefFallbackText = formatVisibleRefFallbackText(visibleRefFallbackDiagnostic);
|
|
5017
7069
|
const semanticActionCandidateText = nextActions ? formatSemanticActionCandidateText(nextActions) : undefined;
|
|
5018
7070
|
const overlayBlockerText = overlayBlockerDiagnostic ? formatOverlayBlockerText(overlayBlockerDiagnostic) : undefined;
|
|
7071
|
+
const fillVerificationText = formatFillVerificationText(fillVerificationDiagnostic);
|
|
7072
|
+
const electronRefFreshnessText = formatElectronRefFreshnessText(electronRefFreshnessDiagnostic);
|
|
5019
7073
|
const selectorTextVisibilityText = formatSelectorTextVisibilityText(selectorTextVisibilityDiagnostics);
|
|
7074
|
+
const electronBroadGetTextScopeText = formatElectronBroadGetTextScopeText(electronBroadGetTextScopeDiagnostics);
|
|
5020
7075
|
const scrollNoopDiagnosticText = formatScrollNoopDiagnosticText(scrollNoopDiagnostic);
|
|
5021
7076
|
const comboboxFocusDiagnosticText = formatComboboxFocusDiagnosticText(comboboxFocusDiagnostic);
|
|
5022
7077
|
const recordingDependencyWarningText = formatRecordingDependencyWarningText(recordingDependencyWarning);
|
|
@@ -5024,15 +7079,26 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
5024
7079
|
const artifactCleanupText = formatArtifactCleanupGuidanceText(artifactCleanup);
|
|
5025
7080
|
const timeoutPartialProgressText = timeoutPartialProgress ? formatTimeoutPartialProgressText(timeoutPartialProgress) : undefined;
|
|
5026
7081
|
const managedSessionOutcomeText = formatManagedSessionOutcomeText(managedSessionOutcome);
|
|
5027
|
-
const rawAppendedDiagnosticText = [visibleRefFallbackText, semanticActionCandidateText, overlayBlockerText, selectorTextVisibilityText, scrollNoopDiagnosticText, comboboxFocusDiagnosticText, recordingDependencyWarningText, evalStdinHintText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText].filter((item): item is string => item !== undefined).join("\n\n");
|
|
7082
|
+
const rawAppendedDiagnosticText = [visibleRefFallbackText, semanticActionCandidateText, overlayBlockerText, fillVerificationText, electronRefFreshnessText, selectorTextVisibilityText, electronBroadGetTextScopeText, scrollNoopDiagnosticText, comboboxFocusDiagnosticText, recordingDependencyWarningText, evalStdinHintText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText].filter((item): item is string => item !== undefined).join("\n\n");
|
|
5028
7083
|
const appendedDiagnosticText = redactSensitiveText(redactExactSensitiveText(rawAppendedDiagnosticText, exactSensitiveValues));
|
|
5029
7084
|
const shouldAppendDiagnosticText = appendedDiagnosticText.length > 0 && (!userRequestedJson || plainTextInspection);
|
|
5030
|
-
|
|
7085
|
+
let content = shouldAppendDiagnosticText && redactedContent[0]?.type === "text"
|
|
5031
7086
|
? [
|
|
5032
7087
|
{ ...redactedContent[0], text: `${redactedContent[0].text}\n\n${appendedDiagnosticText}` },
|
|
5033
7088
|
...redactedContent.slice(1),
|
|
5034
7089
|
]
|
|
5035
7090
|
: redactedContent;
|
|
7091
|
+
if (electronLaunchRecord && succeeded && content[0]?.type === "text") {
|
|
7092
|
+
content = [{
|
|
7093
|
+
...content[0],
|
|
7094
|
+
text: redactSensitiveText(formatElectronLaunchText({
|
|
7095
|
+
handoff: electronHandoff,
|
|
7096
|
+
record: electronLaunchRecord,
|
|
7097
|
+
targets: electronLaunch?.targets ?? [],
|
|
7098
|
+
upstreamText: content[0].text,
|
|
7099
|
+
})),
|
|
7100
|
+
}, ...content.slice(1)];
|
|
7101
|
+
}
|
|
5036
7102
|
const result = {
|
|
5037
7103
|
content,
|
|
5038
7104
|
details: redactToolDetails(details, exactSensitiveValues),
|