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.
@@ -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: AgentBrowserSemanticLocator;
106
- value: string;
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: AgentBrowserSemanticLocator;
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
- url: string;
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 networkSourceLookup is provided.",
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
- value: Type.String({ description: "Locator value, such as visible text, label text, placeholder text, test id, title, alt text, or role." }),
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 it must match value for locator=role." })),
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 find command." })),
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.Object({
282
- url: Type.String({ description: "URL to open for a lightweight QA preset." }),
283
- expectedText: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())], { description: "Text that must appear on the page." })),
284
- expectedSelector: Type.Optional(Type.String({ description: "Selector or @ref that must appear on the page." })),
285
- screenshotPath: Type.Optional(Type.String({ description: "Optional evidence screenshot path captured at the end of the QA preset." })),
286
- checkConsole: Type.Optional(Type.Boolean({ description: "Whether to fail on console error messages. Defaults to true." })),
287
- checkErrors: Type.Optional(Type.Boolean({ description: "Whether to fail on page errors. Defaults to true." })),
288
- checkNetwork: Type.Optional(Type.Boolean({ description: "Whether to inspect network requests and fail on actionable request failures; benign icon misses warn. Defaults to true." })),
289
- 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." })),
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/get-like steps." })),
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 (typeof url !== "string" || url.trim().length === 0) {
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
- { action: "open", args: ["open", url] },
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(data: unknown, compiled: CompiledAgentBrowserSourceLookup, cwd: string): Promise<AgentBrowserSourceLookupAnalysis> {
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
- : "Source lookup found no candidate locations.",
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
- return { compiled: { args: ["batch"], query: { filter, maxWorkspaceFiles: maxWorkspaceFiles.value as number, requestId, url }, stdin: JSON.stringify(steps.map((step) => step.args)), steps } };
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 : args.indexOf("find");
1038
- if (findIndex < 0) return undefined;
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 && (locator !== "role" || role !== value)) {
1226
- return { error: "semanticAction.role is only supported for locator=role and must match value." };
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
- if (session !== undefined && (typeof session !== "string" || session.trim().length === 0)) {
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
- : (semanticAction.compiled?.args ?? generatedBatch?.args ?? []);
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 = extractSessionTabTargetFromData(result);
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 = isRecord(details.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 redactExactSensitiveValue(value: unknown, sensitiveValues: string[]): unknown {
2435
- if (sensitiveValues.length === 0) {
2436
- return value;
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 (typeof value === "string") {
2439
- return redactExactSensitiveText(value, sensitiveValues);
3816
+ if (context.note) {
3817
+ return `Probe context: current managed session ${context.sessionName}; ${context.note}`;
2440
3818
  }
2441
- if (Array.isArray(value)) {
2442
- return value.map((item) => redactExactSensitiveValue(item, sensitiveValues));
3819
+ if (context.launchId) {
3820
+ return `Probe context: current managed session ${context.sessionName} maps to Electron launch ${context.launchId}.`;
2443
3821
  }
2444
- if (!isRecord(value)) {
2445
- return value;
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 Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, redactExactSensitiveValue(entryValue, sensitiveValues)]));
3831
+ return lines.join("\n");
2448
3832
  }
2449
3833
 
2450
- function redactToolDetails(details: Record<string, unknown>, sensitiveValues: string[]): Record<string, unknown> {
2451
- return redactSensitiveValue(redactExactSensitiveValue(details, sensitiveValues)) as Record<string, unknown>;
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
- extractSessionTabTargetFromData(options.data) ??
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.commandInfo.command === "get" && options.commandInfo.subcommand === "text"
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 isEmptyRecord(value: unknown): boolean {
3357
- return isRecord(value) && Object.keys(value).length === 0;
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 (!isEmptyRecord(result)) return undefined;
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<void> {
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: ["--session", options.sessionName, "close"],
5584
+ args: closeArgs,
3844
5585
  cwd: options.cwd,
3845
5586
  signal: controller.signal,
3846
5587
  });
3847
5588
  stdoutSpillPath = processResult.stdoutSpillPath;
3848
- } catch {
3849
- // Best-effort cleanup only.
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
- if (event?.reason === "quit") {
3902
- await managedSessionExecutionQueue.run(async () => {
3903
- if (!managedSessionActive) return;
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 networkSourceLookup."
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 = compiledGeneratedBatch && params.stdin !== undefined ? "Do not provide stdin with job, qa, sourceLookup, or networkSourceLookup; those modes generate their own batch stdin." : undefined;
3993
- const validationError = semanticActionError ?? jobError ?? qaError ?? sourceLookupError ?? networkSourceLookupError ?? inputModeError ?? generatedStdinError ?? validateToolArgs(toolArgs) ?? getBatchAnnotateValidationError(toolArgs, toolStdin);
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
- details: {
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
- const preparedArgs = await prepareAgentBrowserArgs(toolArgs, toolStdin, ctx.cwd);
4038
- const userRequestedJson = toolArgs.includes("--json");
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
- const sessionMode = params.sessionMode ?? DEFAULT_SESSION_MODE;
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
- details: {
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: toolStdin,
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: toolStdin,
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, toolStdin);
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: toolStdin,
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 ?? toolStdin;
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: toolStdin,
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" && toolStdin !== undefined) {
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: toolStdin,
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: managedSessionOutcomeText ? `${errorText}\n\n${managedSessionOutcomeText}` : errorText }],
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(toolArgs) &&
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
- extractSessionTabTargetFromData(presentationEnvelope?.data);
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: toolStdin,
6552
+ stdin: runtimeToolStdin,
4517
6553
  }) : undefined;
4518
- if (succeeded && !sessionTabCorrection && !aboutBlankSessionMismatch) {
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, toolStdin),
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 sourceLookup = compiledSourceLookup ? await analyzeSourceLookupResults(presentationEnvelope?.data, compiledSourceLookup, ctx.cwd) : undefined;
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: toolStdin,
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 = aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
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(toolArgs);
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
- const content = shouldAppendDiagnosticText && redactedContent[0]?.type === "text"
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),