pi-agent-browser-native 0.2.30 → 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 +31 -0
- package/README.md +51 -13
- package/docs/ARCHITECTURE.md +12 -10
- package/docs/COMMAND_REFERENCE.md +66 -15
- package/docs/ELECTRON.md +368 -0
- package/docs/RELEASE.md +40 -12
- package/docs/REQUIREMENTS.md +7 -4
- package/docs/SUPPORT_MATRIX.md +21 -10
- package/docs/TOOL_CONTRACT.md +200 -37
- package/extensions/agent-browser/index.ts +2305 -127
- 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 +14 -13
- package/extensions/agent-browser/lib/results/presentation.ts +191 -9
- package/extensions/agent-browser/lib/results/shared.ts +95 -1
- 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,
|
|
@@ -84,10 +104,32 @@ const DEFAULT_SESSION_MODE = "auto" as const;
|
|
|
84
104
|
const DIRECT_AGENT_BROWSER_BASH_BYPASS_ENV = "PI_AGENT_BROWSER_ALLOW_DIRECT_BASH";
|
|
85
105
|
const PACKAGE_NAME = "pi-agent-browser-native";
|
|
86
106
|
|
|
87
|
-
const AGENT_BROWSER_SEMANTIC_ACTIONS = ["check", "click", "fill", "uncheck"] as const;
|
|
107
|
+
const AGENT_BROWSER_SEMANTIC_ACTIONS = ["check", "click", "fill", "select", "uncheck"] as const;
|
|
88
108
|
const AGENT_BROWSER_SEMANTIC_LOCATORS = ["alt", "label", "placeholder", "role", "testid", "text", "title"] as const;
|
|
89
|
-
const AGENT_BROWSER_JOB_STEP_ACTIONS = ["open", "click", "fill", "wait", "assertText", "assertUrl", "waitForDownload", "screenshot"] as const;
|
|
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,13 +139,16 @@ 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
|
|
|
103
146
|
interface AgentBrowserSemanticActionInput {
|
|
104
147
|
action: AgentBrowserSemanticActionName;
|
|
105
|
-
locator
|
|
106
|
-
value
|
|
148
|
+
locator?: AgentBrowserSemanticLocator;
|
|
149
|
+
value?: string;
|
|
150
|
+
values?: string[];
|
|
151
|
+
selector?: string;
|
|
107
152
|
text?: string;
|
|
108
153
|
role?: string;
|
|
109
154
|
name?: string;
|
|
@@ -112,7 +157,9 @@ interface AgentBrowserSemanticActionInput {
|
|
|
112
157
|
|
|
113
158
|
interface CompiledAgentBrowserSemanticAction {
|
|
114
159
|
action: AgentBrowserSemanticActionName;
|
|
115
|
-
locator
|
|
160
|
+
locator?: AgentBrowserSemanticLocator;
|
|
161
|
+
selector?: string;
|
|
162
|
+
values?: string[];
|
|
116
163
|
args: string[];
|
|
117
164
|
}
|
|
118
165
|
|
|
@@ -178,7 +225,8 @@ interface CompiledAgentBrowserQaPreset extends CompiledAgentBrowserJob {
|
|
|
178
225
|
expectedText: string[];
|
|
179
226
|
expectedSelector?: string;
|
|
180
227
|
screenshotPath?: string;
|
|
181
|
-
|
|
228
|
+
attached: boolean;
|
|
229
|
+
url?: string;
|
|
182
230
|
};
|
|
183
231
|
}
|
|
184
232
|
|
|
@@ -210,11 +258,27 @@ interface AgentBrowserSourceLookupCandidate {
|
|
|
210
258
|
source: "react-inspect" | "dom-attribute" | "workspace-search";
|
|
211
259
|
}
|
|
212
260
|
|
|
261
|
+
interface AgentBrowserSourceLookupElectronContext {
|
|
262
|
+
appName?: string;
|
|
263
|
+
appPath?: string;
|
|
264
|
+
executablePath?: string;
|
|
265
|
+
launchId?: string;
|
|
266
|
+
sessionName?: string;
|
|
267
|
+
url?: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
213
270
|
interface AgentBrowserSourceLookupAnalysis {
|
|
214
271
|
candidates: AgentBrowserSourceLookupCandidate[];
|
|
272
|
+
electronContext?: AgentBrowserSourceLookupElectronContext;
|
|
215
273
|
limitations: string[];
|
|
216
274
|
status: AgentBrowserSourceLookupStatus;
|
|
217
275
|
summary: string;
|
|
276
|
+
workspaceRoot?: string;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
interface AgentBrowserSourceLookupAnalysisContext {
|
|
280
|
+
electronContext?: AgentBrowserSourceLookupElectronContext;
|
|
281
|
+
workspaceRoot: string;
|
|
218
282
|
}
|
|
219
283
|
|
|
220
284
|
interface CompiledAgentBrowserNetworkSourceLookup {
|
|
@@ -225,10 +289,42 @@ interface CompiledAgentBrowserNetworkSourceLookup {
|
|
|
225
289
|
filter?: string;
|
|
226
290
|
maxWorkspaceFiles: number;
|
|
227
291
|
requestId?: string;
|
|
292
|
+
session?: string;
|
|
228
293
|
url?: string;
|
|
229
294
|
};
|
|
230
295
|
}
|
|
231
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
|
+
|
|
232
328
|
interface AgentBrowserNetworkSourceLookupRequest {
|
|
233
329
|
error?: string;
|
|
234
330
|
method?: string;
|
|
@@ -258,36 +354,51 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
258
354
|
|
|
259
355
|
args: Type.Optional(
|
|
260
356
|
Type.Array(Type.String({ description: "Exact agent-browser CLI arguments, excluding the binary name." }), {
|
|
261
|
-
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.",
|
|
262
358
|
minItems: 1,
|
|
263
359
|
}),
|
|
264
360
|
),
|
|
265
361
|
semanticAction: Type.Optional(
|
|
266
362
|
Type.Object({
|
|
267
363
|
action: StringEnum(AGENT_BROWSER_SEMANTIC_ACTIONS, {
|
|
268
|
-
description: "Intent action to compile to an existing agent-browser find command.",
|
|
269
|
-
}),
|
|
270
|
-
locator: StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, {
|
|
271
|
-
description: "Upstream find locator family to use.",
|
|
364
|
+
description: "Intent action to compile to an existing agent-browser find command, or to upstream select when action=select.",
|
|
272
365
|
}),
|
|
273
|
-
|
|
366
|
+
locator: Type.Optional(StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, {
|
|
367
|
+
description: "Upstream find locator family to use for check/click/fill/uncheck actions.",
|
|
368
|
+
})),
|
|
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." })),
|
|
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 })),
|
|
371
|
+
selector: Type.Optional(Type.String({ description: "Selector or @ref for select actions; compiled to select <selector> <value...>." })),
|
|
274
372
|
text: Type.Optional(Type.String({ description: "Text/value argument for fill actions." })),
|
|
275
|
-
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." })),
|
|
276
374
|
name: Type.Optional(Type.String({ description: "Accessible name filter for locator=role; compiles to --name <name>." })),
|
|
277
|
-
session: Type.Optional(Type.String({ description: "Optional upstream session name; prepends --session <name> before the compiled
|
|
375
|
+
session: Type.Optional(Type.String({ description: "Optional upstream session name; prepends --session <name> before the compiled command." })),
|
|
278
376
|
}),
|
|
279
377
|
),
|
|
280
378
|
qa: Type.Optional(
|
|
281
|
-
Type.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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." }),
|
|
291
402
|
),
|
|
292
403
|
sourceLookup: Type.Optional(
|
|
293
404
|
Type.Object({
|
|
@@ -302,10 +413,79 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
302
413
|
Type.Object({
|
|
303
414
|
filter: Type.Optional(Type.String({ description: "Optional upstream network requests filter pattern." })),
|
|
304
415
|
requestId: Type.Optional(Type.String({ description: "Optional network request id to inspect with network request <id>." })),
|
|
416
|
+
session: Type.Optional(Type.String({ description: "Optional upstream session name; prepends --session <name> before the generated batch." })),
|
|
305
417
|
url: Type.Optional(Type.String({ description: "Optional failed request URL or URL fragment to correlate with local source." })),
|
|
306
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 })),
|
|
307
419
|
}),
|
|
308
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
|
+
),
|
|
309
489
|
job: Type.Optional(
|
|
310
490
|
Type.Object({
|
|
311
491
|
steps: Type.Array(
|
|
@@ -314,8 +494,10 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
314
494
|
description: "Constrained one-call job step compiled to existing upstream batch commands.",
|
|
315
495
|
}),
|
|
316
496
|
url: Type.Optional(Type.String({ description: "URL for open steps, or URL pattern for assertUrl steps." })),
|
|
317
|
-
selector: Type.Optional(Type.String({ description: "Selector or @ref for click/fill/
|
|
497
|
+
selector: Type.Optional(Type.String({ description: "Selector or @ref for click/fill/select-like steps." })),
|
|
318
498
|
text: Type.Optional(Type.String({ description: "Text for fill steps or visible text for assertText steps." })),
|
|
499
|
+
value: Type.Optional(Type.String({ description: "Single option value for select steps." })),
|
|
500
|
+
values: Type.Optional(Type.Array(Type.String({ description: "Option value for select steps." }), { description: "One or more option values for select steps.", minItems: 1 })),
|
|
319
501
|
path: Type.Optional(Type.String({ description: "Artifact/download path for waitForDownload or screenshot steps." })),
|
|
320
502
|
milliseconds: Type.Optional(Type.Number({ description: "Milliseconds for wait steps." })),
|
|
321
503
|
}),
|
|
@@ -323,7 +505,7 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
323
505
|
),
|
|
324
506
|
}),
|
|
325
507
|
),
|
|
326
|
-
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." })),
|
|
327
509
|
sessionMode: Type.Optional(
|
|
328
510
|
StringEnum(["auto", "fresh"] as const, {
|
|
329
511
|
description:
|
|
@@ -355,6 +537,24 @@ function getRequiredJobString(step: Record<string, unknown>, field: "path" | "se
|
|
|
355
537
|
return { value };
|
|
356
538
|
}
|
|
357
539
|
|
|
540
|
+
function getSelectValues(input: Record<string, unknown>, context: string): { values?: string[]; error?: string } {
|
|
541
|
+
const rawValue = input.value;
|
|
542
|
+
const rawValues = input.values;
|
|
543
|
+
if (rawValue !== undefined && rawValues !== undefined) {
|
|
544
|
+
return { error: `${context}.value and ${context}.values cannot both be provided for select.` };
|
|
545
|
+
}
|
|
546
|
+
if (rawValues !== undefined) {
|
|
547
|
+
if (!Array.isArray(rawValues) || rawValues.length === 0 || rawValues.some((value) => typeof value !== "string" || value.trim().length === 0)) {
|
|
548
|
+
return { error: `${context}.values must be a non-empty array of non-empty strings for select.` };
|
|
549
|
+
}
|
|
550
|
+
return { values: rawValues };
|
|
551
|
+
}
|
|
552
|
+
if (typeof rawValue === "string" && rawValue.trim().length > 0) {
|
|
553
|
+
return { values: [rawValue] };
|
|
554
|
+
}
|
|
555
|
+
return { error: `${context}.value or ${context}.values is required for select.` };
|
|
556
|
+
}
|
|
557
|
+
|
|
358
558
|
function compileAgentBrowserJob(input: unknown): { compiled?: CompiledAgentBrowserJob; error?: string } {
|
|
359
559
|
if (!isRecord(input)) {
|
|
360
560
|
return { error: "job must be an object." };
|
|
@@ -388,6 +588,12 @@ function compileAgentBrowserJob(input: unknown): { compiled?: CompiledAgentBrows
|
|
|
388
588
|
const text = getRequiredJobString(rawStep, "text", jobAction);
|
|
389
589
|
if (text.error) return { error: `job.steps[${index}]: ${text.error}` };
|
|
390
590
|
args = ["fill", selector.value as string, text.value as string];
|
|
591
|
+
} else if (jobAction === "select") {
|
|
592
|
+
const selector = getRequiredJobString(rawStep, "selector", jobAction);
|
|
593
|
+
if (selector.error) return { error: `job.steps[${index}]: ${selector.error}` };
|
|
594
|
+
const values = getSelectValues(rawStep, `job.steps[${index}]`);
|
|
595
|
+
if (values.error) return { error: values.error };
|
|
596
|
+
args = ["select", selector.value as string, ...(values.values as string[])];
|
|
391
597
|
} else if (jobAction === "wait") {
|
|
392
598
|
const milliseconds = rawStep.milliseconds;
|
|
393
599
|
if (typeof milliseconds !== "number" || !Number.isInteger(milliseconds) || milliseconds <= 0) {
|
|
@@ -472,10 +678,18 @@ function compileAgentBrowserQaPreset(input: unknown): { compiled?: CompiledAgent
|
|
|
472
678
|
if (!isRecord(input)) {
|
|
473
679
|
return { error: "qa must be an object." };
|
|
474
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
|
+
}
|
|
475
685
|
const url = input.url;
|
|
476
|
-
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)) {
|
|
477
690
|
return { error: "qa.url must be a non-empty string." };
|
|
478
691
|
}
|
|
692
|
+
const normalizedUrl = typeof url === "string" ? url.trim() : undefined;
|
|
479
693
|
const expectedText = input.expectedText === undefined
|
|
480
694
|
? []
|
|
481
695
|
: typeof input.expectedText === "string"
|
|
@@ -511,10 +725,8 @@ function compileAgentBrowserQaPreset(input: unknown): { compiled?: CompiledAgent
|
|
|
511
725
|
if (checkNetwork) steps.push({ action: "wait", args: ["network", "requests", "--clear"] });
|
|
512
726
|
if (checkConsole) steps.push({ action: "wait", args: ["console", "--clear"] });
|
|
513
727
|
if (checkErrors) steps.push({ action: "wait", args: ["errors", "--clear"] });
|
|
514
|
-
steps.push(
|
|
515
|
-
|
|
516
|
-
{ action: "wait", args: ["wait", "--load", loadState] },
|
|
517
|
-
);
|
|
728
|
+
if (!attached && normalizedUrl) steps.push({ action: "open", args: ["open", normalizedUrl] });
|
|
729
|
+
steps.push({ action: "wait", args: ["wait", "--load", loadState] });
|
|
518
730
|
for (const text of expectedText) {
|
|
519
731
|
steps.push({ action: "assertText", args: ["wait", "--text", text] });
|
|
520
732
|
}
|
|
@@ -528,7 +740,7 @@ function compileAgentBrowserQaPreset(input: unknown): { compiled?: CompiledAgent
|
|
|
528
740
|
return {
|
|
529
741
|
compiled: {
|
|
530
742
|
args: ["batch"],
|
|
531
|
-
checks: { checkConsole, checkErrors, checkNetwork, expectedSelector, expectedText, loadState, screenshotPath, url },
|
|
743
|
+
checks: { attached, checkConsole, checkErrors, checkNetwork, expectedSelector, expectedText, loadState, screenshotPath, url: normalizedUrl },
|
|
532
744
|
stdin: JSON.stringify(steps.map((step) => step.args)),
|
|
533
745
|
steps,
|
|
534
746
|
},
|
|
@@ -744,7 +956,12 @@ function validateLookupMaxWorkspaceFiles(value: unknown, fieldName: string): { v
|
|
|
744
956
|
return { value };
|
|
745
957
|
}
|
|
746
958
|
|
|
747
|
-
async function analyzeSourceLookupResults(
|
|
959
|
+
async function analyzeSourceLookupResults(
|
|
960
|
+
data: unknown,
|
|
961
|
+
compiled: CompiledAgentBrowserSourceLookup,
|
|
962
|
+
cwd: string,
|
|
963
|
+
context?: AgentBrowserSourceLookupAnalysisContext,
|
|
964
|
+
): Promise<AgentBrowserSourceLookupAnalysis> {
|
|
748
965
|
const items = getBatchResultItems(data);
|
|
749
966
|
const candidates: AgentBrowserSourceLookupCandidate[] = [];
|
|
750
967
|
const limitations = [
|
|
@@ -765,15 +982,27 @@ async function analyzeSourceLookupResults(data: unknown, compiled: CompiledAgent
|
|
|
765
982
|
}
|
|
766
983
|
await collectWorkspaceComponentCandidates(compiled.query, cwd, candidates, limitations);
|
|
767
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
|
+
}
|
|
768
993
|
return {
|
|
769
994
|
candidates,
|
|
995
|
+
electronContext,
|
|
770
996
|
limitations,
|
|
771
997
|
status,
|
|
772
998
|
summary: candidates.length > 0
|
|
773
999
|
? `Source lookup found ${candidates.length} candidate location(s).`
|
|
774
1000
|
: unsupported
|
|
775
1001
|
? "Source lookup could not inspect React metadata in this session."
|
|
776
|
-
:
|
|
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,
|
|
777
1006
|
};
|
|
778
1007
|
}
|
|
779
1008
|
|
|
@@ -781,9 +1010,11 @@ function compileAgentBrowserNetworkSourceLookup(input: unknown): { compiled?: Co
|
|
|
781
1010
|
if (!isRecord(input)) return { error: "networkSourceLookup must be an object." };
|
|
782
1011
|
const filter = input.filter;
|
|
783
1012
|
const requestId = input.requestId;
|
|
1013
|
+
const session = input.session;
|
|
784
1014
|
const url = input.url;
|
|
785
1015
|
if (filter !== undefined && (typeof filter !== "string" || filter.trim().length === 0)) return { error: "networkSourceLookup.filter must be a non-empty string when provided." };
|
|
786
1016
|
if (requestId !== undefined && (typeof requestId !== "string" || requestId.trim().length === 0)) return { error: "networkSourceLookup.requestId must be a non-empty string when provided." };
|
|
1017
|
+
if (session !== undefined && (typeof session !== "string" || session.trim().length === 0)) return { error: "networkSourceLookup.session must be a non-empty string when provided." };
|
|
787
1018
|
if (url !== undefined && (typeof url !== "string" || url.trim().length === 0)) return { error: "networkSourceLookup.url must be a non-empty string when provided." };
|
|
788
1019
|
if (filter === undefined && requestId === undefined && url === undefined) return { error: "networkSourceLookup requires requestId, filter, or url." };
|
|
789
1020
|
const maxWorkspaceFiles = validateLookupMaxWorkspaceFiles(input.maxWorkspaceFiles, "networkSourceLookup.maxWorkspaceFiles");
|
|
@@ -796,7 +1027,161 @@ function compileAgentBrowserNetworkSourceLookup(input: unknown): { compiled?: Co
|
|
|
796
1027
|
if (effectiveFilter) {
|
|
797
1028
|
steps.push({ action: "network", args: ["network", "requests", "--filter", effectiveFilter] });
|
|
798
1029
|
}
|
|
799
|
-
|
|
1030
|
+
const args = typeof session === "string" ? ["--session", session, "batch"] : ["batch"];
|
|
1031
|
+
return { compiled: { args, query: { filter, maxWorkspaceFiles: maxWorkspaceFiles.value as number, requestId, session, url }, stdin: JSON.stringify(steps.map((step) => step.args)), steps } };
|
|
1032
|
+
}
|
|
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
|
+
};
|
|
800
1185
|
}
|
|
801
1186
|
|
|
802
1187
|
function getResultPayload(item: Record<string, unknown>): unknown {
|
|
@@ -967,6 +1352,11 @@ function getCompiledSemanticActionSessionPrefix(compiled: CompiledAgentBrowserSe
|
|
|
967
1352
|
return commandIndex > 0 ? compiled.args.slice(0, commandIndex) : [];
|
|
968
1353
|
}
|
|
969
1354
|
|
|
1355
|
+
function isCompiledSemanticActionFindCommand(compiled: CompiledAgentBrowserSemanticAction | undefined): boolean {
|
|
1356
|
+
if (!compiled) return false;
|
|
1357
|
+
return compiled.args[getCompiledSemanticActionCommandIndex(compiled)] === "find";
|
|
1358
|
+
}
|
|
1359
|
+
|
|
970
1360
|
const SEMANTIC_ACTION_CANDIDATE_ACTION_IDS = new Set([
|
|
971
1361
|
"try-searchbox-name-candidate",
|
|
972
1362
|
"try-textbox-name-candidate",
|
|
@@ -986,7 +1376,7 @@ function formatSemanticActionCandidateText(actions: AgentBrowserNextAction[]): s
|
|
|
986
1376
|
|
|
987
1377
|
function buildSemanticActionCandidateActions(compiled: CompiledAgentBrowserSemanticAction): AgentBrowserNextAction[] {
|
|
988
1378
|
const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
|
|
989
|
-
if (commandIndex < 0) return [];
|
|
1379
|
+
if (commandIndex < 0 || compiled.args[commandIndex] !== "find") return [];
|
|
990
1380
|
const locator = compiled.args[commandIndex + 1];
|
|
991
1381
|
const value = compiled.args[commandIndex + 2];
|
|
992
1382
|
if (!locator || !value) return [];
|
|
@@ -1034,12 +1424,12 @@ function getFindNameFlagValue(args: string[], startIndex: number): string | unde
|
|
|
1034
1424
|
}
|
|
1035
1425
|
|
|
1036
1426
|
function getFindVisibleRefFallbackTarget(args: string[]): VisibleRefFallbackTarget | undefined {
|
|
1037
|
-
const findIndex = args[0] === "--session" ? 2 :
|
|
1038
|
-
if (findIndex
|
|
1427
|
+
const findIndex = args[0] === "--session" ? 2 : 0;
|
|
1428
|
+
if (args[findIndex] !== "find") return undefined;
|
|
1039
1429
|
const locator = args[findIndex + 1];
|
|
1040
1430
|
const value = args[findIndex + 2];
|
|
1041
1431
|
const action = args[findIndex + 3];
|
|
1042
|
-
if (!locator || !value || !isAgentBrowserSemanticActionName(action)) return undefined;
|
|
1432
|
+
if (!locator || !value || !isAgentBrowserSemanticActionName(action) || action === "select") return undefined;
|
|
1043
1433
|
const text = action === "fill" ? args[findIndex + 4] : undefined;
|
|
1044
1434
|
if (action === "fill" && (!text || text.startsWith("-"))) return undefined;
|
|
1045
1435
|
if (locator === "role") {
|
|
@@ -1200,6 +1590,8 @@ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: Compile
|
|
|
1200
1590
|
const action = input.action;
|
|
1201
1591
|
const locator = input.locator;
|
|
1202
1592
|
const value = input.value;
|
|
1593
|
+
const values = input.values;
|
|
1594
|
+
const selector = input.selector;
|
|
1203
1595
|
const text = input.text;
|
|
1204
1596
|
const role = input.role;
|
|
1205
1597
|
const name = input.name;
|
|
@@ -1207,11 +1599,39 @@ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: Compile
|
|
|
1207
1599
|
if (typeof action !== "string" || !AGENT_BROWSER_SEMANTIC_ACTIONS.includes(action as AgentBrowserSemanticActionName)) {
|
|
1208
1600
|
return { error: `semanticAction.action must be one of: ${AGENT_BROWSER_SEMANTIC_ACTIONS.join(", ")}.` };
|
|
1209
1601
|
}
|
|
1602
|
+
if (session !== undefined && (typeof session !== "string" || session.trim().length === 0)) {
|
|
1603
|
+
return { error: "semanticAction.session must be a non-empty string when provided." };
|
|
1604
|
+
}
|
|
1605
|
+
if (action === "select") {
|
|
1606
|
+
if (locator !== undefined || role !== undefined || name !== undefined) {
|
|
1607
|
+
return { error: "semanticAction.locator, role, and name are not supported for select; use selector plus value or values." };
|
|
1608
|
+
}
|
|
1609
|
+
if (text !== undefined) {
|
|
1610
|
+
return { error: "semanticAction.text is not supported for select; use value or values for option values." };
|
|
1611
|
+
}
|
|
1612
|
+
if (typeof selector !== "string" || selector.trim().length === 0) {
|
|
1613
|
+
return { error: "semanticAction.selector is required for select." };
|
|
1614
|
+
}
|
|
1615
|
+
const selectedValues = getSelectValues(input, "semanticAction");
|
|
1616
|
+
if (selectedValues.error) return { error: selectedValues.error };
|
|
1617
|
+
const args = typeof session === "string" ? ["--session", session, "select", selector, ...(selectedValues.values as string[])] : ["select", selector, ...(selectedValues.values as string[])];
|
|
1618
|
+
return { compiled: { action: "select", selector, values: selectedValues.values, args } };
|
|
1619
|
+
}
|
|
1620
|
+
if (selector !== undefined || values !== undefined) {
|
|
1621
|
+
return { error: "semanticAction.selector and values are only supported for select actions." };
|
|
1622
|
+
}
|
|
1210
1623
|
if (typeof locator !== "string" || !AGENT_BROWSER_SEMANTIC_LOCATORS.includes(locator as AgentBrowserSemanticLocator)) {
|
|
1211
1624
|
return { error: `semanticAction.locator must be one of: ${AGENT_BROWSER_SEMANTIC_LOCATORS.join(", ")}.` };
|
|
1212
1625
|
}
|
|
1213
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1214
|
-
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." };
|
|
1215
1635
|
}
|
|
1216
1636
|
if (text !== undefined && typeof text !== "string") {
|
|
1217
1637
|
return { error: "semanticAction.text must be a string when provided." };
|
|
@@ -1222,16 +1642,16 @@ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: Compile
|
|
|
1222
1642
|
if (action !== "fill" && text !== undefined) {
|
|
1223
1643
|
return { error: "semanticAction.text is only supported for fill actions." };
|
|
1224
1644
|
}
|
|
1225
|
-
if (role !== undefined &&
|
|
1226
|
-
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." };
|
|
1227
1650
|
}
|
|
1228
1651
|
if (name !== undefined && (locator !== "role" || typeof name !== "string" || name.length === 0)) {
|
|
1229
1652
|
return { error: "semanticAction.name is only supported as a non-empty string for locator=role." };
|
|
1230
1653
|
}
|
|
1231
|
-
|
|
1232
|
-
return { error: "semanticAction.session must be a non-empty string when provided." };
|
|
1233
|
-
}
|
|
1234
|
-
const args = typeof session === "string" ? ["--session", session, "find", locator, value, action] : ["find", locator, value, action];
|
|
1654
|
+
const args = typeof session === "string" ? ["--session", session, "find", locator, locatorValue, action] : ["find", locator, locatorValue, action];
|
|
1235
1655
|
if (action === "fill") {
|
|
1236
1656
|
args.push(text as string);
|
|
1237
1657
|
}
|
|
@@ -1319,10 +1739,13 @@ function formatAgentBrowserRenderCall(args: unknown, theme: Theme): string {
|
|
|
1319
1739
|
const qa = compileAgentBrowserQaPreset(input.qa);
|
|
1320
1740
|
const sourceLookup = compileAgentBrowserSourceLookup(input.sourceLookup);
|
|
1321
1741
|
const networkSourceLookup = compileAgentBrowserNetworkSourceLookup(input.networkSourceLookup);
|
|
1742
|
+
const electron = compileAgentBrowserElectron(input.electron);
|
|
1322
1743
|
const generatedBatch = networkSourceLookup.compiled ?? sourceLookup.compiled ?? job.compiled ?? qa.compiled;
|
|
1323
1744
|
const rawArgs = Array.isArray(input.args)
|
|
1324
1745
|
? input.args.filter((value): value is string => typeof value === "string")
|
|
1325
|
-
:
|
|
1746
|
+
: electron.compiled
|
|
1747
|
+
? ["electron", electron.compiled.action]
|
|
1748
|
+
: (semanticAction.compiled?.args ?? generatedBatch?.args ?? []);
|
|
1326
1749
|
const redactedArgs = redactInvocationArgs(rawArgs);
|
|
1327
1750
|
const invocation = sanitizeDisplayText(redactedArgs.join(" ")).replace(/\s+/g, " ").trim();
|
|
1328
1751
|
const invocationPreview =
|
|
@@ -1614,6 +2037,9 @@ async function isDirectAgentBrowserBashAllowed(cwd: string): Promise<boolean> {
|
|
|
1614
2037
|
|
|
1615
2038
|
const NAVIGATION_SUMMARY_COMMANDS = new Set(["back", "click", "dblclick", "forward", "reload"]);
|
|
1616
2039
|
const NAVIGATION_SUMMARY_EVAL = `({ title: document.title, url: location.href })`;
|
|
2040
|
+
// These commands can expose URLs for inspected resources (request URLs, cookie/storage scope, or log sources),
|
|
2041
|
+
// but they do not navigate the active tab and must not poison page-scoped ref guards.
|
|
2042
|
+
const READ_ONLY_DIAGNOSTIC_SESSION_TARGET_COMMANDS = new Set(["console", "cookies", "errors", "network", "storage"]);
|
|
1617
2043
|
|
|
1618
2044
|
interface NavigationSummary {
|
|
1619
2045
|
title?: string;
|
|
@@ -1671,6 +2097,22 @@ interface SelectorTextVisibilityDiagnostic {
|
|
|
1671
2097
|
visibleCount: number;
|
|
1672
2098
|
}
|
|
1673
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
|
+
|
|
1674
2116
|
interface TimeoutArtifactEvidence {
|
|
1675
2117
|
absolutePath: string;
|
|
1676
2118
|
exists: boolean;
|
|
@@ -2117,14 +2559,16 @@ function shouldCaptureNavigationSummary(command: string | undefined, data: unkno
|
|
|
2117
2559
|
);
|
|
2118
2560
|
}
|
|
2119
2561
|
|
|
2120
|
-
function extractStringResultField(data: unknown, fieldName: "result" | "title" | "url"): string | undefined {
|
|
2562
|
+
function extractStringResultField(data: unknown, fieldName: "result" | "title" | "url" | "value"): string | undefined {
|
|
2121
2563
|
if (typeof data === "string") {
|
|
2564
|
+
if (fieldName === "value") return data;
|
|
2122
2565
|
const text = data.trim();
|
|
2123
2566
|
return text.length > 0 ? text : undefined;
|
|
2124
2567
|
}
|
|
2125
2568
|
if (!isRecord(data) || typeof data[fieldName] !== "string") {
|
|
2126
2569
|
return undefined;
|
|
2127
2570
|
}
|
|
2571
|
+
if (fieldName === "value") return data[fieldName];
|
|
2128
2572
|
const text = data[fieldName].trim();
|
|
2129
2573
|
return text.length > 0 ? text : undefined;
|
|
2130
2574
|
}
|
|
@@ -2259,6 +2703,15 @@ function extractSessionTabTargetFromData(data: unknown): SessionTabTarget | unde
|
|
|
2259
2703
|
return undefined;
|
|
2260
2704
|
}
|
|
2261
2705
|
|
|
2706
|
+
function isReadOnlyDiagnosticSessionTargetCommand(command: string | undefined, _subcommand: string | undefined): boolean {
|
|
2707
|
+
return command !== undefined && READ_ONLY_DIAGNOSTIC_SESSION_TARGET_COMMANDS.has(command);
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
function extractSessionTabTargetFromCommandData(commandTokens: string[], data: unknown): SessionTabTarget | undefined {
|
|
2711
|
+
const [command, subcommand] = commandTokens;
|
|
2712
|
+
return isReadOnlyDiagnosticSessionTargetCommand(command, subcommand) ? undefined : extractSessionTabTargetFromData(data);
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2262
2715
|
function extractBatchResultCommand(item: Record<string, unknown>): string[] {
|
|
2263
2716
|
return Array.isArray(item.command) ? item.command.filter((token): token is string => typeof token === "string") : [];
|
|
2264
2717
|
}
|
|
@@ -2290,7 +2743,7 @@ function extractSessionTabTargetFromBatchResults(data: unknown): SessionTabTarge
|
|
|
2290
2743
|
pendingTitle = undefined;
|
|
2291
2744
|
continue;
|
|
2292
2745
|
}
|
|
2293
|
-
const resultTarget =
|
|
2746
|
+
const resultTarget = extractSessionTabTargetFromCommandData([name, subcommand].filter((token): token is string => token !== undefined), result);
|
|
2294
2747
|
if (resultTarget) {
|
|
2295
2748
|
currentTarget = resultTarget;
|
|
2296
2749
|
}
|
|
@@ -2299,6 +2752,40 @@ function extractSessionTabTargetFromBatchResults(data: unknown): SessionTabTarge
|
|
|
2299
2752
|
return currentTarget;
|
|
2300
2753
|
}
|
|
2301
2754
|
|
|
2755
|
+
function batchContainsOnlyReadOnlyDiagnosticTargets(data: unknown): boolean {
|
|
2756
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
2757
|
+
return false;
|
|
2758
|
+
}
|
|
2759
|
+
return data.every((item) => {
|
|
2760
|
+
if (!isRecord(item)) return false;
|
|
2761
|
+
const [command, subcommand] = extractBatchResultCommand(item);
|
|
2762
|
+
return isReadOnlyDiagnosticSessionTargetCommand(command, subcommand);
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
function getRestoredSessionTabTarget(details: Record<string, unknown>, command: string | undefined, subcommand: string | undefined): SessionTabTarget | undefined {
|
|
2767
|
+
if (isReadOnlyDiagnosticSessionTargetCommand(command, subcommand)) {
|
|
2768
|
+
return undefined;
|
|
2769
|
+
}
|
|
2770
|
+
const storedTarget = isRecord(details.sessionTabTarget)
|
|
2771
|
+
? normalizeSessionTabTarget({
|
|
2772
|
+
title: typeof details.sessionTabTarget.title === "string" ? details.sessionTabTarget.title : undefined,
|
|
2773
|
+
url: typeof details.sessionTabTarget.url === "string" ? details.sessionTabTarget.url : undefined,
|
|
2774
|
+
})
|
|
2775
|
+
: undefined;
|
|
2776
|
+
if (command !== "batch") {
|
|
2777
|
+
return storedTarget;
|
|
2778
|
+
}
|
|
2779
|
+
const batchTarget = extractSessionTabTargetFromBatchResults(details.data);
|
|
2780
|
+
if (batchTarget) {
|
|
2781
|
+
return batchTarget;
|
|
2782
|
+
}
|
|
2783
|
+
if (isRecord(details.compiledNetworkSourceLookup) || batchContainsOnlyReadOnlyDiagnosticTargets(details.data)) {
|
|
2784
|
+
return undefined;
|
|
2785
|
+
}
|
|
2786
|
+
return storedTarget;
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2302
2789
|
function restoreSessionTabTargetsFromBranch(branch: unknown[]): Map<string, OrderedSessionTabTarget> {
|
|
2303
2790
|
const restoredTargets = new Map<string, OrderedSessionTabTarget>();
|
|
2304
2791
|
let restoredOrder = 0;
|
|
@@ -2319,17 +2806,13 @@ function restoreSessionTabTargetsFromBranch(branch: unknown[]): Map<string, Orde
|
|
|
2319
2806
|
continue;
|
|
2320
2807
|
}
|
|
2321
2808
|
const command = typeof details.command === "string" ? details.command : undefined;
|
|
2809
|
+
const subcommand = typeof details.subcommand === "string" ? details.subcommand : undefined;
|
|
2322
2810
|
if (command === "close" && message.isError !== true) {
|
|
2323
2811
|
restoredOrder += 1;
|
|
2324
2812
|
restoredTargets.delete(sessionName);
|
|
2325
2813
|
continue;
|
|
2326
2814
|
}
|
|
2327
|
-
const sessionTabTarget =
|
|
2328
|
-
? normalizeSessionTabTarget({
|
|
2329
|
-
title: typeof details.sessionTabTarget.title === "string" ? details.sessionTabTarget.title : undefined,
|
|
2330
|
-
url: typeof details.sessionTabTarget.url === "string" ? details.sessionTabTarget.url : undefined,
|
|
2331
|
-
})
|
|
2332
|
-
: undefined;
|
|
2815
|
+
const sessionTabTarget = getRestoredSessionTabTarget(details, command, subcommand);
|
|
2333
2816
|
if (sessionTabTarget) {
|
|
2334
2817
|
restoredOrder += 1;
|
|
2335
2818
|
restoredTargets.set(sessionName, { order: restoredOrder, target: sessionTabTarget });
|
|
@@ -2423,32 +2906,1028 @@ function getExactSensitiveStdinValues(options: { command?: string; commandTokens
|
|
|
2423
2906
|
return [...new Set([options.stdin, options.stdin.trimEnd(), options.stdin.trim()].filter((value) => value.length > 0))];
|
|
2424
2907
|
}
|
|
2425
2908
|
|
|
2426
|
-
function redactExactSensitiveText(text: string, sensitiveValues: string[]): string {
|
|
2427
|
-
let redacted = text;
|
|
2428
|
-
for (const value of sensitiveValues) {
|
|
2429
|
-
redacted = redacted.split(value).join("[REDACTED]");
|
|
2430
|
-
}
|
|
2431
|
-
return redacted;
|
|
2909
|
+
function redactExactSensitiveText(text: string, sensitiveValues: string[]): string {
|
|
2910
|
+
let redacted = text;
|
|
2911
|
+
for (const value of sensitiveValues) {
|
|
2912
|
+
redacted = redacted.split(value).join("[REDACTED]");
|
|
2913
|
+
}
|
|
2914
|
+
return redacted;
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
function redactExactSensitiveValue(value: unknown, sensitiveValues: string[]): unknown {
|
|
2918
|
+
if (sensitiveValues.length === 0) {
|
|
2919
|
+
return value;
|
|
2920
|
+
}
|
|
2921
|
+
if (typeof value === "string") {
|
|
2922
|
+
return redactExactSensitiveText(value, sensitiveValues);
|
|
2923
|
+
}
|
|
2924
|
+
if (Array.isArray(value)) {
|
|
2925
|
+
return value.map((item) => redactExactSensitiveValue(item, sensitiveValues));
|
|
2926
|
+
}
|
|
2927
|
+
if (!isRecord(value)) {
|
|
2928
|
+
return value;
|
|
2929
|
+
}
|
|
2930
|
+
return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, redactExactSensitiveValue(entryValue, sensitiveValues)]));
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
function redactToolDetails(details: Record<string, unknown>, sensitiveValues: string[]): Record<string, unknown> {
|
|
2934
|
+
return redactSensitiveValue(redactExactSensitiveValue(details, sensitiveValues)) as Record<string, unknown>;
|
|
2935
|
+
}
|
|
2936
|
+
|
|
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
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
if (visibleOmittedCount > 0) {
|
|
2955
|
+
lines.push(`${visibleOmittedCount} additional app(s) omitted from visible output; see details.electron.apps.`);
|
|
2956
|
+
}
|
|
2957
|
+
if (result.omittedCount > 0) {
|
|
2958
|
+
lines.push(`${result.omittedCount} app(s) omitted by maxResults=${result.maxResults}.`);
|
|
2959
|
+
}
|
|
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);
|
|
2964
|
+
}
|
|
2965
|
+
return lines.join("\n");
|
|
2966
|
+
}
|
|
2967
|
+
|
|
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
|
+
}
|
|
3055
|
+
}
|
|
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}.` };
|
|
3067
|
+
}
|
|
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 };
|
|
3073
|
+
}
|
|
3074
|
+
|
|
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;
|
|
3116
|
+
command?: string;
|
|
3117
|
+
launchId: string;
|
|
3118
|
+
nextActionIds: string[];
|
|
3119
|
+
reason: ElectronPostCommandHealthReason;
|
|
3120
|
+
sessionName?: string;
|
|
3121
|
+
status: ElectronLaunchStatus;
|
|
3122
|
+
summary: string;
|
|
3123
|
+
target?: SessionTabTarget;
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
interface FillVerificationDiagnostic {
|
|
3127
|
+
actual?: string;
|
|
3128
|
+
expected: string;
|
|
3129
|
+
nextActionIds: string[];
|
|
3130
|
+
selector: string;
|
|
3131
|
+
status: "mismatch";
|
|
3132
|
+
summary: string;
|
|
3133
|
+
}
|
|
3134
|
+
|
|
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 }> {
|
|
3261
|
+
try {
|
|
3262
|
+
return { data: await runSessionCommandData(options) };
|
|
3263
|
+
} catch (error) {
|
|
3264
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
|
|
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([
|
|
3290
|
+
"back",
|
|
3291
|
+
"check",
|
|
3292
|
+
"click",
|
|
3293
|
+
"dblclick",
|
|
3294
|
+
"fill",
|
|
3295
|
+
"find",
|
|
3296
|
+
"forward",
|
|
3297
|
+
"keyboard",
|
|
3298
|
+
"mouse",
|
|
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})` : ""}`;
|
|
2432
3810
|
}
|
|
2433
3811
|
|
|
2434
|
-
function
|
|
2435
|
-
if (
|
|
2436
|
-
return
|
|
3812
|
+
function formatElectronProbeContextText(context: ElectronProbeContext): string {
|
|
3813
|
+
if (context.mode === "launchId") {
|
|
3814
|
+
return `Probe context: wrapper launch ${context.launchId} session ${context.sessionName}.`;
|
|
2437
3815
|
}
|
|
2438
|
-
if (
|
|
2439
|
-
return
|
|
3816
|
+
if (context.note) {
|
|
3817
|
+
return `Probe context: current managed session ${context.sessionName}; ${context.note}`;
|
|
2440
3818
|
}
|
|
2441
|
-
if (
|
|
2442
|
-
return
|
|
3819
|
+
if (context.launchId) {
|
|
3820
|
+
return `Probe context: current managed session ${context.sessionName} maps to Electron launch ${context.launchId}.`;
|
|
2443
3821
|
}
|
|
2444
|
-
|
|
2445
|
-
|
|
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.");
|
|
2446
3830
|
}
|
|
2447
|
-
return
|
|
3831
|
+
return lines.join("\n");
|
|
2448
3832
|
}
|
|
2449
3833
|
|
|
2450
|
-
function
|
|
2451
|
-
|
|
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");
|
|
2452
3931
|
}
|
|
2453
3932
|
|
|
2454
3933
|
function validateStdinCommandContract(options: { command?: string; commandTokens: string[]; stdin?: string }): string | undefined {
|
|
@@ -2751,14 +4230,18 @@ function deriveSessionTabTarget(options: {
|
|
|
2751
4230
|
data: unknown;
|
|
2752
4231
|
navigationSummary?: NavigationSummary;
|
|
2753
4232
|
previousTarget?: SessionTabTarget;
|
|
4233
|
+
subcommand?: string;
|
|
2754
4234
|
}): SessionTabTarget | undefined {
|
|
2755
4235
|
if (options.command === "close") {
|
|
2756
4236
|
return undefined;
|
|
2757
4237
|
}
|
|
4238
|
+
const commandDataTarget = isReadOnlyDiagnosticSessionTargetCommand(options.command, options.subcommand)
|
|
4239
|
+
? undefined
|
|
4240
|
+
: extractSessionTabTargetFromData(options.data);
|
|
2758
4241
|
return (
|
|
2759
4242
|
normalizeSessionTabTarget(options.navigationSummary) ??
|
|
2760
4243
|
extractSessionTabTargetFromBatchResults(options.data) ??
|
|
2761
|
-
|
|
4244
|
+
commandDataTarget ??
|
|
2762
4245
|
options.previousTarget
|
|
2763
4246
|
);
|
|
2764
4247
|
}
|
|
@@ -2825,8 +4308,9 @@ async function runSessionCommandData(options: {
|
|
|
2825
4308
|
sessionName?: string;
|
|
2826
4309
|
signal?: AbortSignal;
|
|
2827
4310
|
stdin?: string;
|
|
4311
|
+
timeoutMs?: number;
|
|
2828
4312
|
}): Promise<unknown | undefined> {
|
|
2829
|
-
const { args, cwd, sessionName, signal, stdin } = options;
|
|
4313
|
+
const { args, cwd, sessionName, signal, stdin, timeoutMs } = options;
|
|
2830
4314
|
if (!sessionName) return undefined;
|
|
2831
4315
|
|
|
2832
4316
|
const processResult = await runAgentBrowserProcess({
|
|
@@ -2834,6 +4318,7 @@ async function runSessionCommandData(options: {
|
|
|
2834
4318
|
cwd,
|
|
2835
4319
|
signal,
|
|
2836
4320
|
stdin,
|
|
4321
|
+
timeoutMs,
|
|
2837
4322
|
});
|
|
2838
4323
|
try {
|
|
2839
4324
|
if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
|
|
@@ -2854,6 +4339,114 @@ async function runSessionCommandData(options: {
|
|
|
2854
4339
|
}
|
|
2855
4340
|
}
|
|
2856
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
|
+
|
|
2857
4450
|
async function collectNavigationSummary(options: {
|
|
2858
4451
|
cwd: string;
|
|
2859
4452
|
sessionName?: string;
|
|
@@ -3312,6 +4905,14 @@ function getBatchGetTextSelectors(data: unknown): string[] {
|
|
|
3312
4905
|
});
|
|
3313
4906
|
}
|
|
3314
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
|
+
|
|
3315
4916
|
async function collectSelectorTextVisibilityDiagnostics(options: {
|
|
3316
4917
|
commandInfo: CommandInfo;
|
|
3317
4918
|
commandTokens: string[];
|
|
@@ -3320,11 +4921,7 @@ async function collectSelectorTextVisibilityDiagnostics(options: {
|
|
|
3320
4921
|
sessionName?: string;
|
|
3321
4922
|
signal?: AbortSignal;
|
|
3322
4923
|
}): Promise<SelectorTextVisibilityDiagnostic[]> {
|
|
3323
|
-
const selectors = options
|
|
3324
|
-
? [options.commandTokens[2]]
|
|
3325
|
-
: options.commandInfo.command === "batch"
|
|
3326
|
-
? getBatchGetTextSelectors(options.data)
|
|
3327
|
-
: [];
|
|
4924
|
+
const selectors = getSuccessfulGetTextSelectors(options);
|
|
3328
4925
|
const diagnostics: SelectorTextVisibilityDiagnostic[] = [];
|
|
3329
4926
|
for (const selector of selectors) {
|
|
3330
4927
|
const diagnostic = await collectSelectorTextVisibilityDiagnosticForSelector({
|
|
@@ -3347,20 +4944,145 @@ function formatSelectorTextVisibilityText(diagnostics: SelectorTextVisibilityDia
|
|
|
3347
4944
|
}).join("\n");
|
|
3348
4945
|
}
|
|
3349
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
|
+
|
|
3350
5070
|
function looksLikeFunctionEvalStdin(stdin: string | undefined): boolean {
|
|
3351
5071
|
const trimmed = stdin?.trim();
|
|
3352
5072
|
if (!trimmed) return false;
|
|
3353
5073
|
return /^(?:async\s+)?function\b/.test(trimmed) || /^(?:async\s*)?\([^)]*\)\s*=>/.test(trimmed) || /^(?:async\s+)?[A-Za-z_$][\w$]*\s*=>/.test(trimmed);
|
|
3354
5074
|
}
|
|
3355
5075
|
|
|
3356
|
-
function
|
|
3357
|
-
|
|
5076
|
+
function isPlainEmptyObject(value: unknown): boolean {
|
|
5077
|
+
if (!isRecord(value) || Array.isArray(value)) return false;
|
|
5078
|
+
const prototype = Object.getPrototypeOf(value);
|
|
5079
|
+
return (prototype === Object.prototype || prototype === null) && Object.keys(value).length === 0;
|
|
3358
5080
|
}
|
|
3359
5081
|
|
|
3360
5082
|
function getEvalStdinHint(options: { command?: string; data: unknown; stdin?: string }): EvalStdinHint | undefined {
|
|
3361
5083
|
if (options.command !== "eval" || !looksLikeFunctionEvalStdin(options.stdin) || !isRecord(options.data)) return undefined;
|
|
3362
5084
|
const result = options.data.result;
|
|
3363
|
-
if (!
|
|
5085
|
+
if (!isPlainEmptyObject(result)) return undefined;
|
|
3364
5086
|
return {
|
|
3365
5087
|
reason: "eval --stdin received a function-shaped snippet and the upstream JSON result was an empty object, which often means the function itself was returned or serialized instead of invoked.",
|
|
3366
5088
|
suggestion: "Pass a plain expression such as `({ title: document.title })`, or invoke the function explicitly, for example `(() => ({ title: document.title }))()`.",
|
|
@@ -3410,6 +5132,24 @@ function formatArtifactCleanupGuidanceText(guidance: ArtifactCleanupGuidance | u
|
|
|
3410
5132
|
return lines.join("\n");
|
|
3411
5133
|
}
|
|
3412
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
|
+
|
|
3413
5153
|
function buildSelectorTextVisibilityNextActions(options: { diagnostics: SelectorTextVisibilityDiagnostic[]; sessionName?: string }): AgentBrowserNextAction[] {
|
|
3414
5154
|
return options.diagnostics.map((diagnostic, index) => ({
|
|
3415
5155
|
id: index === 0 ? "inspect-visible-text-candidates" : `inspect-visible-text-candidates-${index + 1}`,
|
|
@@ -3834,19 +5574,31 @@ class AsyncExecutionQueue {
|
|
|
3834
5574
|
}
|
|
3835
5575
|
}
|
|
3836
5576
|
|
|
3837
|
-
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> {
|
|
3838
5578
|
const controller = new AbortController();
|
|
3839
5579
|
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
3840
5580
|
let stdoutSpillPath: string | undefined;
|
|
5581
|
+
const closeArgs = ["--session", options.sessionName, "close"];
|
|
3841
5582
|
try {
|
|
3842
5583
|
const processResult = await runAgentBrowserProcess({
|
|
3843
|
-
args:
|
|
5584
|
+
args: closeArgs,
|
|
3844
5585
|
cwd: options.cwd,
|
|
3845
5586
|
signal: controller.signal,
|
|
3846
5587
|
});
|
|
3847
5588
|
stdoutSpillPath = processResult.stdoutSpillPath;
|
|
3848
|
-
|
|
3849
|
-
|
|
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);
|
|
3850
5602
|
} finally {
|
|
3851
5603
|
clearTimeout(timer);
|
|
3852
5604
|
if (stdoutSpillPath) {
|
|
@@ -3881,8 +5633,38 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3881
5633
|
let sessionTabTargetUpdateOrder = 0;
|
|
3882
5634
|
let traceOwners = new Map<string, TraceOwner>();
|
|
3883
5635
|
let artifactManifest: SessionArtifactManifest | undefined;
|
|
5636
|
+
let electronLaunchRecords = new Map<string, ElectronLaunchRecord>();
|
|
5637
|
+
let electronChildProcesses = new Map<string, ChildProcess>();
|
|
3884
5638
|
const managedSessionExecutionQueue = new AsyncExecutionQueue();
|
|
3885
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
|
+
|
|
3886
5668
|
pi.on("session_start", async (_event, ctx) => {
|
|
3887
5669
|
managedSessionBaseName = createImplicitSessionName(ctx.sessionManager.getSessionId(), ctx.cwd, ephemeralSessionSeed);
|
|
3888
5670
|
const restoredState = restoreManagedSessionStateFromBranch(ctx.sessionManager.getBranch(), managedSessionBaseName);
|
|
@@ -3895,19 +5677,24 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3895
5677
|
sessionTabPinningReasons = new Map([...sessionTabTargets.keys()].map((sessionName) => [sessionName, "restore"]));
|
|
3896
5678
|
sessionTabTargetUpdateOrder = Math.max(getLatestSessionTabTargetOrder(sessionTabTargets), getLatestSessionTabTargetOrder(sessionRefSnapshots));
|
|
3897
5679
|
artifactManifest = restoreArtifactManifestFromBranch(ctx.sessionManager.getBranch());
|
|
5680
|
+
electronLaunchRecords = restoreElectronLaunchRecordsFromBranch(ctx.sessionManager.getBranch());
|
|
5681
|
+
electronChildProcesses = new Map<string, ChildProcess>();
|
|
3898
5682
|
});
|
|
3899
5683
|
|
|
3900
|
-
pi.on("session_shutdown", async (event) => {
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
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) {
|
|
3904
5691
|
await closeManagedSession({
|
|
3905
5692
|
cwd: managedSessionCwd,
|
|
3906
5693
|
sessionName: managedSessionName,
|
|
3907
5694
|
timeoutMs: implicitSessionCloseTimeoutMs,
|
|
3908
5695
|
});
|
|
3909
|
-
}
|
|
3910
|
-
}
|
|
5696
|
+
}
|
|
5697
|
+
});
|
|
3911
5698
|
managedSessionActive = false;
|
|
3912
5699
|
sessionTabTargets = new Map<string, OrderedSessionTabTarget>();
|
|
3913
5700
|
sessionRefSnapshots = new Map<string, OrderedSessionRefSnapshot>();
|
|
@@ -3915,6 +5702,8 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3915
5702
|
sessionTabTargetUpdateOrder = 0;
|
|
3916
5703
|
traceOwners = new Map<string, TraceOwner>();
|
|
3917
5704
|
artifactManifest = undefined;
|
|
5705
|
+
electronLaunchRecords = new Map<string, ElectronLaunchRecord>();
|
|
5706
|
+
electronChildProcesses = new Map<string, ChildProcess>();
|
|
3918
5707
|
await cleanupSecureTempArtifacts();
|
|
3919
5708
|
});
|
|
3920
5709
|
|
|
@@ -3970,30 +5759,53 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3970
5759
|
const qaResult = params.qa === undefined ? {} : compileAgentBrowserQaPreset(params.qa);
|
|
3971
5760
|
const sourceLookupResult = params.sourceLookup === undefined ? {} : compileAgentBrowserSourceLookup(params.sourceLookup);
|
|
3972
5761
|
const networkSourceLookupResult = params.networkSourceLookup === undefined ? {} : compileAgentBrowserNetworkSourceLookup(params.networkSourceLookup);
|
|
5762
|
+
const electronResult = params.electron === undefined ? {} : compileAgentBrowserElectron(params.electron);
|
|
3973
5763
|
const hasExplicitArgs = Array.isArray(params.args);
|
|
3974
|
-
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;
|
|
3975
5765
|
const semanticActionError = semanticActionResult.error;
|
|
3976
5766
|
const jobError = jobResult.error;
|
|
3977
5767
|
const qaError = qaResult.error;
|
|
3978
5768
|
const sourceLookupError = sourceLookupResult.error;
|
|
3979
5769
|
const networkSourceLookupError = networkSourceLookupResult.error;
|
|
5770
|
+
const electronError = electronResult.error;
|
|
3980
5771
|
const inputModeError = explicitInputModes !== 1
|
|
3981
|
-
? "Provide exactly one of args, semanticAction, job, qa, sourceLookup, or
|
|
5772
|
+
? "Provide exactly one of args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron."
|
|
3982
5773
|
: undefined;
|
|
3983
5774
|
const compiledSemanticAction = semanticActionResult.compiled;
|
|
3984
5775
|
const compiledQaPreset = qaResult.compiled;
|
|
3985
5776
|
const compiledSourceLookup = sourceLookupResult.compiled;
|
|
3986
5777
|
const compiledNetworkSourceLookup = networkSourceLookupResult.compiled;
|
|
5778
|
+
const compiledElectron = electronResult.compiled;
|
|
3987
5779
|
const compiledJob = jobResult.compiled ?? compiledQaPreset;
|
|
3988
5780
|
const compiledGeneratedBatch = compiledNetworkSourceLookup ?? compiledSourceLookup ?? compiledJob;
|
|
3989
|
-
const toolArgs = compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? params.args ?? [];
|
|
5781
|
+
const toolArgs = compiledElectron ? [] : compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? params.args ?? [];
|
|
3990
5782
|
const toolStdin = compiledGeneratedBatch?.stdin ?? params.stdin;
|
|
3991
5783
|
const redactedArgs = redactInvocationArgs(toolArgs);
|
|
3992
|
-
const generatedStdinError =
|
|
3993
|
-
|
|
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));
|
|
3994
5799
|
const redactedCompiledSemanticAction = compiledSemanticAction
|
|
3995
5800
|
? { ...compiledSemanticAction, args: redactInvocationArgs(compiledSemanticAction.args) }
|
|
3996
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;
|
|
3997
5809
|
const redactedCompiledJobSteps = compiledJob?.steps.map((step) => ({ ...step, args: redactInvocationArgs(step.args) }));
|
|
3998
5810
|
const redactedCompiledJob = compiledJob && redactedCompiledJobSteps
|
|
3999
5811
|
? { ...compiledJob, stdin: JSON.stringify(redactedCompiledJobSteps.map((step) => step.args)), steps: redactedCompiledJobSteps }
|
|
@@ -4009,6 +5821,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4009
5821
|
const redactedCompiledNetworkSourceLookup = compiledNetworkSourceLookup && redactedCompiledNetworkSourceLookupSteps
|
|
4010
5822
|
? {
|
|
4011
5823
|
...compiledNetworkSourceLookup,
|
|
5824
|
+
args: redactNetworkSourceLookupArgs(compiledNetworkSourceLookup.args),
|
|
4012
5825
|
query: {
|
|
4013
5826
|
...compiledNetworkSourceLookup.query,
|
|
4014
5827
|
filter: redactNetworkSourceLookupUrl(compiledNetworkSourceLookup.query.filter),
|
|
@@ -4021,8 +5834,9 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4021
5834
|
if (validationError) {
|
|
4022
5835
|
return {
|
|
4023
5836
|
content: [{ type: "text", text: validationError }],
|
|
4024
|
-
|
|
5837
|
+
details: {
|
|
4025
5838
|
args: redactedArgs,
|
|
5839
|
+
compiledElectron: redactedCompiledElectron,
|
|
4026
5840
|
compiledJob: redactedCompiledJob,
|
|
4027
5841
|
compiledQaPreset: redactedCompiledQaPreset,
|
|
4028
5842
|
compiledSourceLookup: redactedCompiledSourceLookup,
|
|
@@ -4034,13 +5848,170 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4034
5848
|
isError: true,
|
|
4035
5849
|
};
|
|
4036
5850
|
}
|
|
4037
|
-
|
|
4038
|
-
|
|
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
|
+
}
|
|
4039
5977
|
|
|
4040
5978
|
const tabTargetUpdateOrder = ++sessionTabTargetUpdateOrder;
|
|
4041
5979
|
const runTool = async (): Promise<AgentBrowserToolResult> => {
|
|
4042
|
-
|
|
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;
|
|
4043
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");
|
|
4044
6015
|
let executionPlan = buildExecutionPlan(preparedArgs.args, {
|
|
4045
6016
|
freshSessionName,
|
|
4046
6017
|
managedSessionActive,
|
|
@@ -4074,8 +6045,9 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4074
6045
|
if (executionPlan.validationError) {
|
|
4075
6046
|
return {
|
|
4076
6047
|
content: [{ type: "text", text: executionPlan.validationError }],
|
|
4077
|
-
|
|
6048
|
+
details: {
|
|
4078
6049
|
args: redactedArgs,
|
|
6050
|
+
compiledElectron: redactedCompiledElectron,
|
|
4079
6051
|
compiledJob: redactedCompiledJob,
|
|
4080
6052
|
compiledQaPreset: redactedCompiledQaPreset,
|
|
4081
6053
|
compiledSourceLookup: redactedCompiledSourceLookup,
|
|
@@ -4095,7 +6067,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4095
6067
|
const exactSensitiveValues = getExactSensitiveStdinValues({
|
|
4096
6068
|
command: executionPlan.commandInfo.command,
|
|
4097
6069
|
commandTokens,
|
|
4098
|
-
stdin:
|
|
6070
|
+
stdin: runtimeToolStdin,
|
|
4099
6071
|
});
|
|
4100
6072
|
const traceOwnerGuardMessage = getTraceOwnerGuardMessage({
|
|
4101
6073
|
command: executionPlan.commandInfo.command,
|
|
@@ -4122,7 +6094,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4122
6094
|
const stdinValidationError = validateStdinCommandContract({
|
|
4123
6095
|
command: executionPlan.commandInfo.command,
|
|
4124
6096
|
commandTokens,
|
|
4125
|
-
stdin:
|
|
6097
|
+
stdin: runtimeToolStdin,
|
|
4126
6098
|
});
|
|
4127
6099
|
if (stdinValidationError) {
|
|
4128
6100
|
return {
|
|
@@ -4140,7 +6112,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4140
6112
|
isError: true,
|
|
4141
6113
|
};
|
|
4142
6114
|
}
|
|
4143
|
-
const waitIpcTimeoutError = validateWaitIpcTimeoutContract(commandTokens,
|
|
6115
|
+
const waitIpcTimeoutError = validateWaitIpcTimeoutContract(commandTokens, runtimeToolStdin);
|
|
4144
6116
|
if (waitIpcTimeoutError) {
|
|
4145
6117
|
return {
|
|
4146
6118
|
content: [{ type: "text", text: waitIpcTimeoutError }],
|
|
@@ -4169,7 +6141,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4169
6141
|
commandTokens,
|
|
4170
6142
|
currentTarget: priorSessionTabTarget,
|
|
4171
6143
|
refSnapshot: resolvedSemanticActionRefSnapshot ?? priorRefSnapshotState,
|
|
4172
|
-
stdin:
|
|
6144
|
+
stdin: runtimeToolStdin,
|
|
4173
6145
|
});
|
|
4174
6146
|
if (staleRefPreflight) {
|
|
4175
6147
|
return {
|
|
@@ -4193,7 +6165,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4193
6165
|
let includePinnedNavigationSummary = false;
|
|
4194
6166
|
let sessionTabCorrection: OpenResultTabCorrection | undefined;
|
|
4195
6167
|
let processArgs = executionPlan.effectiveArgs;
|
|
4196
|
-
let processStdin = preparedArgs.stdin ??
|
|
6168
|
+
let processStdin = preparedArgs.stdin ?? runtimeToolStdin;
|
|
4197
6169
|
if (
|
|
4198
6170
|
priorSessionTabTarget &&
|
|
4199
6171
|
shouldPinSessionTabForCommand({
|
|
@@ -4201,7 +6173,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4201
6173
|
commandTokens,
|
|
4202
6174
|
pinningRequired: sessionTabPinningReason !== undefined,
|
|
4203
6175
|
sessionName: executionPlan.sessionName,
|
|
4204
|
-
stdin:
|
|
6176
|
+
stdin: runtimeToolStdin,
|
|
4205
6177
|
})
|
|
4206
6178
|
) {
|
|
4207
6179
|
const plannedSessionTabSelection = await collectSessionTabSelection({
|
|
@@ -4211,7 +6183,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4211
6183
|
target: priorSessionTabTarget,
|
|
4212
6184
|
});
|
|
4213
6185
|
if (plannedSessionTabSelection && executionPlan.sessionName) {
|
|
4214
|
-
if (executionPlan.commandInfo.command === "eval" &&
|
|
6186
|
+
if (executionPlan.commandInfo.command === "eval" && runtimeToolStdin !== undefined) {
|
|
4215
6187
|
const appliedSessionTabSelection = await applyOpenResultTabCorrection({
|
|
4216
6188
|
correction: plannedSessionTabSelection,
|
|
4217
6189
|
cwd: ctx.cwd,
|
|
@@ -4243,7 +6215,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4243
6215
|
command: executionPlan.commandInfo.command,
|
|
4244
6216
|
commandTokens,
|
|
4245
6217
|
selectedTab: plannedSessionTabSelection.selectedTab,
|
|
4246
|
-
stdin:
|
|
6218
|
+
stdin: runtimeToolStdin,
|
|
4247
6219
|
});
|
|
4248
6220
|
if (pinnedBatchPlan && "error" in pinnedBatchPlan) {
|
|
4249
6221
|
return {
|
|
@@ -4315,12 +6287,32 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4315
6287
|
succeeded: false,
|
|
4316
6288
|
});
|
|
4317
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);
|
|
4318
6302
|
return {
|
|
4319
|
-
content: [{ type: "text", text:
|
|
6303
|
+
content: [{ type: "text", text: textParts.join("\n\n") }],
|
|
4320
6304
|
details: {
|
|
4321
6305
|
args: redactedArgs,
|
|
4322
6306
|
compatibilityWorkaround,
|
|
4323
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,
|
|
4324
6316
|
managedSessionOutcome,
|
|
4325
6317
|
sessionMode,
|
|
4326
6318
|
sessionTabCorrection,
|
|
@@ -4408,7 +6400,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4408
6400
|
if (
|
|
4409
6401
|
succeeded &&
|
|
4410
6402
|
executionPlan.sessionName &&
|
|
4411
|
-
hasLaunchScopedTabCorrectionFlag(
|
|
6403
|
+
hasLaunchScopedTabCorrectionFlag(runtimeToolArgs) &&
|
|
4412
6404
|
(executionPlan.commandInfo.command === "goto" ||
|
|
4413
6405
|
executionPlan.commandInfo.command === "navigate" ||
|
|
4414
6406
|
executionPlan.commandInfo.command === "open")
|
|
@@ -4435,14 +6427,19 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4435
6427
|
const observedSessionTabTarget =
|
|
4436
6428
|
normalizeSessionTabTarget(navigationSummary) ??
|
|
4437
6429
|
extractSessionTabTargetFromBatchResults(presentationEnvelope?.data) ??
|
|
4438
|
-
|
|
6430
|
+
extractSessionTabTargetFromCommandData(commandTokens, presentationEnvelope?.data);
|
|
4439
6431
|
let currentSessionTabTarget = deriveSessionTabTarget({
|
|
4440
6432
|
command: executionPlan.commandInfo.command,
|
|
4441
6433
|
data: presentationEnvelope?.data,
|
|
4442
6434
|
navigationSummary,
|
|
4443
6435
|
previousTarget: priorSessionTabTarget,
|
|
6436
|
+
subcommand: executionPlan.commandInfo.subcommand,
|
|
4444
6437
|
});
|
|
4445
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;
|
|
4446
6443
|
const shouldTreatAboutBlankAsMismatch =
|
|
4447
6444
|
succeeded &&
|
|
4448
6445
|
priorSessionTabTarget !== undefined &&
|
|
@@ -4450,6 +6447,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4450
6447
|
isAboutBlankSessionTabTarget(observedSessionTabTarget ?? currentSessionTabTarget) &&
|
|
4451
6448
|
!commandExplicitlyTargetsAboutBlank(commandTokens);
|
|
4452
6449
|
if (shouldTreatAboutBlankAsMismatch && priorSessionTabTarget) {
|
|
6450
|
+
const aboutBlankObservedTarget = observedSessionTabTarget ?? currentSessionTabTarget;
|
|
4453
6451
|
const aboutBlankRecovery = await collectSessionTabSelection({
|
|
4454
6452
|
cwd: ctx.cwd,
|
|
4455
6453
|
sessionName: executionPlan.sessionName,
|
|
@@ -4474,6 +6472,19 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4474
6472
|
targetTitle: priorSessionTabTarget.title,
|
|
4475
6473
|
targetUrl: priorSessionTabTarget.url,
|
|
4476
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
|
+
}
|
|
4477
6488
|
currentSessionTabTarget = priorSessionTabTarget;
|
|
4478
6489
|
}
|
|
4479
6490
|
if (
|
|
@@ -4507,15 +6518,55 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4507
6518
|
}
|
|
4508
6519
|
}
|
|
4509
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;
|
|
4510
6545
|
let selectorTextVisibilityDiagnostics: SelectorTextVisibilityDiagnostic[] = [];
|
|
6546
|
+
let electronBroadGetTextScopeDiagnostics: ElectronBroadGetTextScopeDiagnostic[] = [];
|
|
4511
6547
|
const timeoutPartialProgress = processResult.timedOut ? await collectTimeoutPartialProgress({
|
|
4512
6548
|
command: executionPlan.commandInfo.command,
|
|
4513
6549
|
compiledJob,
|
|
4514
6550
|
cwd: ctx.cwd,
|
|
4515
6551
|
sessionName: executionPlan.sessionName,
|
|
4516
|
-
stdin:
|
|
6552
|
+
stdin: runtimeToolStdin,
|
|
4517
6553
|
}) : undefined;
|
|
4518
|
-
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) {
|
|
4519
6570
|
overlayBlockerDiagnostic = await collectOverlayBlockerDiagnostic({
|
|
4520
6571
|
command: executionPlan.commandInfo.command,
|
|
4521
6572
|
cwd: ctx.cwd,
|
|
@@ -4535,6 +6586,15 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4535
6586
|
sessionName: executionPlan.sessionName,
|
|
4536
6587
|
signal,
|
|
4537
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
|
+
});
|
|
4538
6598
|
}
|
|
4539
6599
|
const comboboxFocusDiagnostic = succeeded
|
|
4540
6600
|
? await collectComboboxFocusDiagnostic({
|
|
@@ -4638,6 +6698,37 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4638
6698
|
});
|
|
4639
6699
|
}
|
|
4640
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
|
+
|
|
4641
6732
|
const errorText = getAgentBrowserErrorText({
|
|
4642
6733
|
aborted: processResult.aborted,
|
|
4643
6734
|
command: executionPlan.commandInfo.command,
|
|
@@ -4646,7 +6737,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4646
6737
|
exitCode: processResult.exitCode,
|
|
4647
6738
|
parseError,
|
|
4648
6739
|
plainTextInspection,
|
|
4649
|
-
staleRefArgs: getStaleRefArgs(commandTokens,
|
|
6740
|
+
staleRefArgs: getStaleRefArgs(commandTokens, runtimeToolStdin),
|
|
4650
6741
|
spawnError: processResult.spawnError,
|
|
4651
6742
|
stderr: processResult.stderr,
|
|
4652
6743
|
timedOut: processResult.timedOut,
|
|
@@ -4703,7 +6794,19 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4703
6794
|
artifactManifest = presentation.artifactManifest;
|
|
4704
6795
|
}
|
|
4705
6796
|
const qaPreset = compiledQaPreset ? analyzeQaPresetResults(presentationEnvelope?.data) : undefined;
|
|
4706
|
-
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;
|
|
4707
6810
|
const networkSourceLookup = compiledNetworkSourceLookup ? redactNetworkSourceLookupAnalysis(await analyzeNetworkSourceLookupResults(presentationEnvelope?.data, compiledNetworkSourceLookup, ctx.cwd)) : undefined;
|
|
4708
6811
|
if (networkSourceLookup && presentation.content[0]?.type === "text") {
|
|
4709
6812
|
presentation.content[0] = { ...presentation.content[0], text: `${networkSourceLookup.summary}\n\n${presentation.content[0].text}` };
|
|
@@ -4727,13 +6830,19 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4727
6830
|
presentation.content.unshift({ type: "text", text: qaPreset.summary });
|
|
4728
6831
|
}
|
|
4729
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
|
+
}
|
|
4730
6839
|
if (managedSessionOutcome && managedSessionOutcome.succeeded !== succeeded) {
|
|
4731
6840
|
managedSessionOutcome = { ...managedSessionOutcome, succeeded };
|
|
4732
6841
|
}
|
|
4733
6842
|
const evalStdinHint = getEvalStdinHint({
|
|
4734
6843
|
command: executionPlan.commandInfo.command,
|
|
4735
6844
|
data: presentationEnvelope?.data,
|
|
4736
|
-
stdin:
|
|
6845
|
+
stdin: runtimeToolStdin,
|
|
4737
6846
|
});
|
|
4738
6847
|
const resultArtifactManifest = presentation.artifactManifest ?? artifactManifest;
|
|
4739
6848
|
const artifactCleanup = await getArtifactCleanupGuidance({
|
|
@@ -4742,7 +6851,11 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4742
6851
|
manifest: resultArtifactManifest,
|
|
4743
6852
|
succeeded,
|
|
4744
6853
|
});
|
|
4745
|
-
const warningText =
|
|
6854
|
+
const warningText = electronPostCommandHealth
|
|
6855
|
+
? formatElectronPostCommandHealthText(electronPostCommandHealth)
|
|
6856
|
+
: electronSessionMismatch
|
|
6857
|
+
? formatElectronSessionMismatchText(electronSessionMismatch)
|
|
6858
|
+
: aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
|
|
4746
6859
|
const contentWithSessionWarnings = userRequestedJson && !plainTextInspection
|
|
4747
6860
|
? buildJsonVisibleContent({
|
|
4748
6861
|
error: presentationEnvelope?.error,
|
|
@@ -4776,18 +6889,18 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4776
6889
|
command: executionPlan.commandInfo.command,
|
|
4777
6890
|
confirmationRequired: presentation.summary.startsWith("Confirmation required"),
|
|
4778
6891
|
errorText: errorText ?? presentation.summary,
|
|
4779
|
-
failureCategory: presentation.failureCategory ?? presentation.batchFailure?.failedStep.failureCategory,
|
|
6892
|
+
failureCategory: presentation.failureCategory ?? presentation.batchFailure?.failedStep.failureCategory ?? (electronPostCommandHealth ? "tab-drift" : undefined),
|
|
4780
6893
|
inspection: plainTextInspection,
|
|
4781
6894
|
parseError,
|
|
4782
6895
|
savedFile: presentation.savedFile,
|
|
4783
6896
|
spawnError: processResult.spawnError?.message,
|
|
4784
6897
|
succeeded,
|
|
4785
|
-
tabDrift: !succeeded && (aboutBlankSessionMismatch !== undefined || sessionTabCorrection !== undefined),
|
|
6898
|
+
tabDrift: !succeeded && (aboutBlankSessionMismatch !== undefined || electronPostCommandHealth !== undefined || sessionTabCorrection !== undefined),
|
|
4786
6899
|
timedOut: processResult.timedOut,
|
|
4787
6900
|
validationError: undefined,
|
|
4788
6901
|
});
|
|
4789
6902
|
let visibleRefFallbackDiagnostic: VisibleRefFallbackDiagnostic | undefined;
|
|
4790
|
-
const visibleRefFallbackSessionName = executionPlan.sessionName ?? extractExplicitSessionName(
|
|
6903
|
+
const visibleRefFallbackSessionName = executionPlan.sessionName ?? extractExplicitSessionName(runtimeToolArgs);
|
|
4791
6904
|
if (categoryDetails.failureCategory === "selector-not-found") {
|
|
4792
6905
|
visibleRefFallbackDiagnostic = await collectVisibleRefFallbackDiagnostic({
|
|
4793
6906
|
commandTokens,
|
|
@@ -4808,6 +6921,18 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4808
6921
|
if (visibleRefFallbackDiagnostic) {
|
|
4809
6922
|
(nextActions ??= []).push(...buildVisibleRefFallbackNextActions({ diagnostic: visibleRefFallbackDiagnostic, sessionName: visibleRefFallbackSessionName }));
|
|
4810
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
|
+
}
|
|
4811
6936
|
if (categoryDetails.failureCategory === "selector-not-found" && redactedCompiledSemanticAction) {
|
|
4812
6937
|
const candidateActions = buildSemanticActionCandidateActions(redactedCompiledSemanticAction);
|
|
4813
6938
|
if (candidateActions.length > 0) {
|
|
@@ -4817,16 +6942,28 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4817
6942
|
if (overlayBlockerDiagnostic) {
|
|
4818
6943
|
(nextActions ??= []).push(...buildOverlayBlockerNextActions({ diagnostic: overlayBlockerDiagnostic, sessionName: executionPlan.sessionName }));
|
|
4819
6944
|
}
|
|
6945
|
+
if (fillVerificationDiagnostic) {
|
|
6946
|
+
nextActions = appendUniqueNextActions(nextActions ?? [], buildFillVerificationNextActions(fillVerificationDiagnostic, executionPlan.sessionName));
|
|
6947
|
+
}
|
|
6948
|
+
if (electronRefFreshnessDiagnostic) {
|
|
6949
|
+
nextActions = appendUniqueNextActions(nextActions ?? [], buildElectronRefFreshnessNextActions(executionPlan.sessionName));
|
|
6950
|
+
}
|
|
4820
6951
|
if (selectorTextVisibilityDiagnostics.length > 0) {
|
|
4821
6952
|
(nextActions ??= []).push(...buildSelectorTextVisibilityNextActions({ diagnostics: selectorTextVisibilityDiagnostics, sessionName: executionPlan.sessionName }));
|
|
4822
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
|
+
}
|
|
4823
6960
|
if (scrollNoopDiagnostic) {
|
|
4824
6961
|
(nextActions ??= []).push(...buildScrollNoopNextActions(executionPlan.sessionName));
|
|
4825
6962
|
}
|
|
4826
6963
|
if (comboboxFocusDiagnostic) {
|
|
4827
6964
|
(nextActions ??= []).push(...buildComboboxFocusNextActions(executionPlan.sessionName));
|
|
4828
6965
|
}
|
|
4829
|
-
if (categoryDetails.failureCategory === "stale-ref" && redactedCompiledSemanticAction) {
|
|
6966
|
+
if (categoryDetails.failureCategory === "stale-ref" && redactedCompiledSemanticAction && isCompiledSemanticActionFindCommand(compiledSemanticAction)) {
|
|
4830
6967
|
(nextActions ??= []).push({
|
|
4831
6968
|
id: "retry-semantic-action-after-stale-ref",
|
|
4832
6969
|
params: { args: redactedCompiledSemanticAction.args },
|
|
@@ -4835,11 +6972,20 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4835
6972
|
tool: "agent_browser" as const,
|
|
4836
6973
|
});
|
|
4837
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
|
+
}
|
|
4838
6983
|
const pageChangeSummary = (scrollNoopDiagnostic || comboboxFocusDiagnostic) && presentation.pageChangeSummary
|
|
4839
6984
|
? { ...presentation.pageChangeSummary, nextActionIds: nextActions?.map((action) => action.id) }
|
|
4840
6985
|
: presentation.pageChangeSummary;
|
|
4841
6986
|
const details = {
|
|
4842
6987
|
args: redactedArgs,
|
|
6988
|
+
compiledElectron: redactedCompiledElectron,
|
|
4843
6989
|
compiledJob: redactedCompiledJob,
|
|
4844
6990
|
compiledQaPreset: redactedCompiledQaPreset,
|
|
4845
6991
|
compiledSourceLookup: redactedCompiledSourceLookup,
|
|
@@ -4859,8 +7005,22 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4859
7005
|
error: plainTextInspection ? undefined : presentationEnvelope?.error,
|
|
4860
7006
|
inspection: plainTextInspection || undefined,
|
|
4861
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,
|
|
4862
7019
|
...categoryDetails,
|
|
4863
7020
|
aboutBlankSessionMismatch,
|
|
7021
|
+
electronPostCommandHealth,
|
|
7022
|
+
electronRefFreshness: electronRefFreshnessDiagnostic,
|
|
7023
|
+
electronSessionMismatch,
|
|
4864
7024
|
openResultTabCorrection,
|
|
4865
7025
|
effectiveArgs: redactedProcessArgs,
|
|
4866
7026
|
exitCode: processResult.exitCode,
|
|
@@ -4873,11 +7033,15 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4873
7033
|
nextActions,
|
|
4874
7034
|
pageChangeSummary,
|
|
4875
7035
|
overlayBlockers: overlayBlockerDiagnostic,
|
|
7036
|
+
fillVerification: fillVerificationDiagnostic,
|
|
4876
7037
|
visibleRefFallback: visibleRefFallbackDiagnostic,
|
|
4877
7038
|
comboboxFocus: comboboxFocusDiagnostic,
|
|
4878
7039
|
recordingDependencyWarning,
|
|
4879
7040
|
scrollNoop: scrollNoopDiagnostic,
|
|
4880
7041
|
qaPreset,
|
|
7042
|
+
qaAttachedTarget,
|
|
7043
|
+
electronGetTextScopeWarning: electronBroadGetTextScopeDiagnostics[0],
|
|
7044
|
+
electronGetTextScopeWarnings: electronBroadGetTextScopeDiagnostics.length > 1 ? electronBroadGetTextScopeDiagnostics : undefined,
|
|
4881
7045
|
selectorTextVisibility: selectorTextVisibilityDiagnostics[0],
|
|
4882
7046
|
selectorTextVisibilityAll: selectorTextVisibilityDiagnostics.length > 1 ? selectorTextVisibilityDiagnostics : undefined,
|
|
4883
7047
|
evalStdinHint,
|
|
@@ -4904,7 +7068,10 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4904
7068
|
const visibleRefFallbackText = formatVisibleRefFallbackText(visibleRefFallbackDiagnostic);
|
|
4905
7069
|
const semanticActionCandidateText = nextActions ? formatSemanticActionCandidateText(nextActions) : undefined;
|
|
4906
7070
|
const overlayBlockerText = overlayBlockerDiagnostic ? formatOverlayBlockerText(overlayBlockerDiagnostic) : undefined;
|
|
7071
|
+
const fillVerificationText = formatFillVerificationText(fillVerificationDiagnostic);
|
|
7072
|
+
const electronRefFreshnessText = formatElectronRefFreshnessText(electronRefFreshnessDiagnostic);
|
|
4907
7073
|
const selectorTextVisibilityText = formatSelectorTextVisibilityText(selectorTextVisibilityDiagnostics);
|
|
7074
|
+
const electronBroadGetTextScopeText = formatElectronBroadGetTextScopeText(electronBroadGetTextScopeDiagnostics);
|
|
4908
7075
|
const scrollNoopDiagnosticText = formatScrollNoopDiagnosticText(scrollNoopDiagnostic);
|
|
4909
7076
|
const comboboxFocusDiagnosticText = formatComboboxFocusDiagnosticText(comboboxFocusDiagnostic);
|
|
4910
7077
|
const recordingDependencyWarningText = formatRecordingDependencyWarningText(recordingDependencyWarning);
|
|
@@ -4912,15 +7079,26 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4912
7079
|
const artifactCleanupText = formatArtifactCleanupGuidanceText(artifactCleanup);
|
|
4913
7080
|
const timeoutPartialProgressText = timeoutPartialProgress ? formatTimeoutPartialProgressText(timeoutPartialProgress) : undefined;
|
|
4914
7081
|
const managedSessionOutcomeText = formatManagedSessionOutcomeText(managedSessionOutcome);
|
|
4915
|
-
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");
|
|
4916
7083
|
const appendedDiagnosticText = redactSensitiveText(redactExactSensitiveText(rawAppendedDiagnosticText, exactSensitiveValues));
|
|
4917
7084
|
const shouldAppendDiagnosticText = appendedDiagnosticText.length > 0 && (!userRequestedJson || plainTextInspection);
|
|
4918
|
-
|
|
7085
|
+
let content = shouldAppendDiagnosticText && redactedContent[0]?.type === "text"
|
|
4919
7086
|
? [
|
|
4920
7087
|
{ ...redactedContent[0], text: `${redactedContent[0].text}\n\n${appendedDiagnosticText}` },
|
|
4921
7088
|
...redactedContent.slice(1),
|
|
4922
7089
|
]
|
|
4923
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
|
+
}
|
|
4924
7102
|
const result = {
|
|
4925
7103
|
content,
|
|
4926
7104
|
details: redactToolDetails(details, exactSensitiveValues),
|