pi-agent-browser-native 0.9.4 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +58 -813
- package/dist/extensions/agent-browser/index.js +927 -883
- package/dist/extensions/agent-browser/lib/browser-execution-claims.js +5 -5
- package/dist/extensions/agent-browser/lib/config-policy.js +1 -1
- package/dist/extensions/agent-browser/lib/config-validation.js +21 -8
- package/dist/extensions/agent-browser/lib/config.js +1 -1
- package/dist/extensions/agent-browser/lib/electron/cdp.js +5 -1
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +13 -47
- package/dist/extensions/agent-browser/lib/electron/discovery-evidence.js +1 -0
- package/dist/extensions/agent-browser/lib/electron/launch.js +12 -1
- package/dist/extensions/agent-browser/lib/fs-utils.js +1 -9
- package/dist/extensions/agent-browser/lib/input-modes/job.js +7 -4
- package/dist/extensions/agent-browser/lib/input-modes/lookup-network-evidence.js +15 -3
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +3 -2
- package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -23
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/electron-observation.js +9 -8
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result-electron.js +8 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +2 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +9 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state-electron.js +4 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state-refs.js +9 -11
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/branch.js +1 -0
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/probe-collection.js +5 -6
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/probe-data.js +4 -2
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/probe-presentation.js +20 -2
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/status.js +4 -2
- package/dist/extensions/agent-browser/lib/orchestration/execution-queue.js +1 -1
- package/dist/extensions/agent-browser/lib/results/presentation/skills.js +7 -2
- package/dist/extensions/agent-browser/lib/temp-artifact-budget.js +4 -1
- package/dist/extensions/agent-browser/lib/temp-root.js +22 -18
- package/dist/extensions/agent-browser/lib/temp.js +1 -1
- package/dist/extensions/agent-browser/lib/web-search-providers.js +0 -36
- package/dist/extensions/agent-browser/lib/web-search-request.js +20 -26
- package/dist/extensions/agent-browser/lib/web-search-schema.js +5 -5
- package/dist/extensions/agent-browser/lib/web-search.js +22 -4
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CODE_QUALITY.md +28 -3
- package/docs/COMMAND_REFERENCE.md +20 -20
- package/docs/ELECTRON.md +24 -2
- package/docs/REFERENCE.md +869 -0
- package/docs/RELEASE.md +9 -9
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/SUPPORT_MATRIX.md +9 -8
- package/docs/TOOL_CONTRACT.md +13 -9
- package/docs/platform-smoke.md +3 -3
- package/package.json +3 -2
- package/scripts/config.mjs +23 -19
- package/scripts/doctor.mjs +2 -2
- package/scripts/platform-smoke/lease-evidence.mjs +2 -4
- package/scripts/platform-smoke/linux-image/Dockerfile +8 -2
- package/scripts/platform-smoke/targets.mjs +1 -5
|
@@ -123,9 +123,8 @@ async function publishTicket(claim) {
|
|
|
123
123
|
await rename(candidate, join(claim.path, LOCK_TICKET_FILE));
|
|
124
124
|
return ticket.ticket;
|
|
125
125
|
}
|
|
126
|
-
function
|
|
127
|
-
return
|
|
128
|
-
(budget.deadline !== undefined && Date.now() >= budget.deadline));
|
|
126
|
+
function acquisitionCancelled(budget) {
|
|
127
|
+
return budget.signal?.aborted === true;
|
|
129
128
|
}
|
|
130
129
|
async function waitForTurn(claim, ticket, budget, deadline) {
|
|
131
130
|
while (budget.signal?.aborted !== true) {
|
|
@@ -145,8 +144,9 @@ async function waitForTurn(claim, ticket, budget, deadline) {
|
|
|
145
144
|
// Opportunistic GC must finish before the final cancellation check.
|
|
146
145
|
// oxlint-disable-next-line no-await-in-loop
|
|
147
146
|
await cleanDeadPolicyArtifacts(claim.directory, budget);
|
|
148
|
-
//
|
|
149
|
-
|
|
147
|
+
// GC stays within the wait budget but cannot revoke a turn this claim already won;
|
|
148
|
+
// only cancellation can. Callers enforce their own operation deadlines.
|
|
149
|
+
return !acquisitionCancelled(budget);
|
|
150
150
|
}
|
|
151
151
|
if (Date.now() >= deadline) {
|
|
152
152
|
return false;
|
|
@@ -5,7 +5,7 @@ import { join, resolve } from "node:path";
|
|
|
5
5
|
import { DEFAULT_WEB_SEARCH_PROVIDER, WEB_SEARCH_PROVIDERS, getWebSearchProviderDescriptor, getWebSearchProviderEnvVar, } from "./config-providers.js";
|
|
6
6
|
import { parseAgentBrowserConfigLayer, describeUnknownError } from "./config-validation.js";
|
|
7
7
|
export * from "./config-providers.js";
|
|
8
|
-
export { parseAgentBrowserConfigLayer, validateAgentBrowserConfig, validateWebSearchProvider, } from "./config-validation.js";
|
|
8
|
+
export { parseAgentBrowserConfigJson, parseAgentBrowserConfigLayer, validateAgentBrowserConfig, validateWebSearchProvider, } from "./config-validation.js";
|
|
9
9
|
/** @typedef {import("./config-providers.js").WebSearchProvider} WebSearchProvider */
|
|
10
10
|
/** @typedef {import("./config-providers.js").ExaSearchType} ExaSearchType */
|
|
11
11
|
/** @typedef {import("./config-providers.js").WebSearchProviderConfigKey} WebSearchProviderConfigKey */
|
|
@@ -179,6 +179,23 @@ export function validateAgentBrowserConfig(value, path, errors, warnings) {
|
|
|
179
179
|
...(browser !== undefined ? { browser } : {}),
|
|
180
180
|
};
|
|
181
181
|
}
|
|
182
|
+
/** Parse config without retaining native token previews or source-bearing error causes.
|
|
183
|
+
* @param {string} raw
|
|
184
|
+
* @returns {{ readonly ok: true; readonly value: unknown } | { readonly ok: false; readonly error: string }} */
|
|
185
|
+
export function parseAgentBrowserConfigJson(raw) {
|
|
186
|
+
try {
|
|
187
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
const location = error instanceof Error
|
|
191
|
+
? error.message.match(/at position \d+(?: \(line \d+ column \d+\))?/)?.[0]
|
|
192
|
+
: undefined;
|
|
193
|
+
return {
|
|
194
|
+
ok: false,
|
|
195
|
+
error: location === undefined ? "Invalid JSON." : `Invalid JSON ${location}.`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
182
199
|
/** @param {string} raw
|
|
183
200
|
* @param {string} path
|
|
184
201
|
* @param {ConfigLayerScope} scope
|
|
@@ -186,16 +203,12 @@ export function validateAgentBrowserConfig(value, path, errors, warnings) {
|
|
|
186
203
|
* @param {ConfigDiagnostics} warnings
|
|
187
204
|
* @returns {ConfigLayer | undefined} */
|
|
188
205
|
export function parseAgentBrowserConfigLayer(raw, path, scope, errors, warnings) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
parsed = JSON.parse(raw);
|
|
193
|
-
}
|
|
194
|
-
catch (error) {
|
|
195
|
-
errors.push(`Could not parse ${scope} config ${path}: ${error instanceof Error ? error.message : describeUnknownError(error)}`);
|
|
206
|
+
const parsed = parseAgentBrowserConfigJson(raw);
|
|
207
|
+
if (!parsed.ok) {
|
|
208
|
+
errors.push(`Could not parse ${scope} config ${path}: ${parsed.error}`);
|
|
196
209
|
return;
|
|
197
210
|
}
|
|
198
|
-
const config = validateAgentBrowserConfig(parsed, path, errors, warnings);
|
|
211
|
+
const config = validateAgentBrowserConfig(parsed.value, path, errors, warnings);
|
|
199
212
|
return config ? { config, path, scope } : undefined;
|
|
200
213
|
}
|
|
201
214
|
/** @param {unknown} value */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { exec as execCallback } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
import { SECRET_COMMAND_TIMEOUT_MS, getWebSearchCredentialSource, getWebSearchProviderOrder, loadAgentBrowserConfigStateSync, resolveEnvInterpolations, } from "./config-policy.js";
|
|
4
|
-
export { AGENT_BROWSER_CONFIG_ENV, BRAVE_API_KEY_ENV, CONFIG_RELATIVE_PATH, DEFAULT_WEB_SEARCH_PROVIDER, EXA_API_KEY_ENV, EXA_SEARCH_TYPES, GLOBAL_CONFIG_RELATIVE_PATH, SECRET_COMMAND_TIMEOUT_MS, WEB_SEARCH_PROVIDER_CONFIG_KEYS,
|
|
4
|
+
export { AGENT_BROWSER_CONFIG_ENV, BRAVE_API_KEY_ENV, CONFIG_RELATIVE_PATH, DEFAULT_WEB_SEARCH_PROVIDER, EXA_API_KEY_ENV, EXA_SEARCH_TYPES, GLOBAL_CONFIG_RELATIVE_PATH, SECRET_COMMAND_TIMEOUT_MS, WEB_SEARCH_PROVIDER_CONFIG_KEYS, WEB_SEARCH_PROVIDER_ENV_VARS, WEB_SEARCH_PROVIDERS, buildAgentBrowserConfigState, buildWebSearchCredentialSources, canRegisterWebSearchTool, classifyCredentialSource, formatBrowserExecutableStatus, formatBrowserProfileStatus, getAgentBrowserConfigPaths, getCredentialSourceSummary, getGlobalAgentBrowserConfigPath, getProjectAgentBrowserConfigPath, getWebSearchCredentialSource, getWebSearchProviderDescriptor, getWebSearchProviderLabel, getWebSearchProviderOrder, hasPotentialCredentialSource, isWebSearchProvider, loadAgentBrowserConfigStateSync, mergeAgentBrowserConfig, parseAgentBrowserConfigJson, parseAgentBrowserConfigLayer, resolveEnvInterpolations, summarizeConfigFiles, validateAgentBrowserConfig, validateWebSearchProvider, } from "./config-policy.js";
|
|
5
5
|
// Node's callback-oriented exec returns ChildProcess; promisify intentionally ignores that return.
|
|
6
6
|
// oxlint-disable-next-line typescript/strict-void-return
|
|
7
7
|
const exec = promisify(execCallback);
|
|
@@ -7,8 +7,12 @@ export function parseCdpVersion(value) {
|
|
|
7
7
|
if (!isRecord(value)) {
|
|
8
8
|
return undefined;
|
|
9
9
|
}
|
|
10
|
+
const browser = asString(value.Browser) ?? asString(value.browser);
|
|
11
|
+
if (browser === undefined) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
10
14
|
return {
|
|
11
|
-
browser
|
|
15
|
+
browser,
|
|
12
16
|
protocolVersion: asString(value["Protocol-Version"]) ?? asString(value.protocolVersion),
|
|
13
17
|
userAgent: asString(value["User-Agent"]) ?? asString(value.userAgent),
|
|
14
18
|
v8Version: asString(value["V8-Version"]) ?? asString(value.v8Version),
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import { lstat, rm } from "node:fs/promises";
|
|
3
|
-
import { win32 } from "node:path";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
2
|
import { fetchCdpJson, parseCdpTargets, parseCdpVersion, } from "./cdp.js";
|
|
6
3
|
import { ELECTRON_PROFILE_DIR_PREFIX } from "./launch-types.js";
|
|
7
4
|
import { pathExists } from "../fs-utils.js";
|
|
8
5
|
import { isRecord } from "../parsing.js";
|
|
6
|
+
import { readProcessStartIdentity, processStartIdentitiesMatch } from "../process-identity.js";
|
|
9
7
|
import { stringifyUnknown } from "../results/text.js";
|
|
10
8
|
import { getSecureTempChildDirectoryValidationError } from "../temp.js";
|
|
11
9
|
const ELECTRON_CLEANUP_DEFAULT_TIMEOUT_MS = 5_000;
|
|
12
10
|
const ELECTRON_CLEANUP_POLL_INTERVAL_MS = 100;
|
|
13
|
-
const RESTORED_PROCESS_COMMAND_TIMEOUT_MS = 1_000;
|
|
14
|
-
// Node's callback-style execFile returns a ChildProcess; promisify owns its completion callback, not that return value.
|
|
15
|
-
// oxlint-disable-next-line typescript/strict-void-return
|
|
16
|
-
const execFileAsync = promisify(execFile);
|
|
17
11
|
function sleep(ms) {
|
|
18
12
|
return new Promise((resolve) => {
|
|
19
13
|
setTimeout(resolve, ms);
|
|
@@ -77,48 +71,16 @@ async function waitForProcessExit(child, pid, deadlineMs) {
|
|
|
77
71
|
}
|
|
78
72
|
return hasProcessExited(child, pid);
|
|
79
73
|
}
|
|
80
|
-
async function readPidCommandLine(pid) {
|
|
81
|
-
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
// Win32_Process.CommandLine is the native ownership evidence; process
|
|
86
|
-
// start time (used by daemon-policy locks) cannot prove profile ownership.
|
|
87
|
-
const systemRoot = process.env.SystemRoot;
|
|
88
|
-
const windowsRoot = systemRoot !== undefined && systemRoot.length > 0 && win32.isAbsolute(systemRoot)
|
|
89
|
-
? systemRoot
|
|
90
|
-
: "C:\\Windows";
|
|
91
|
-
const file = process.platform === "win32"
|
|
92
|
-
? win32.join(windowsRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
|
93
|
-
: "ps";
|
|
94
|
-
const args = process.platform === "win32"
|
|
95
|
-
? [
|
|
96
|
-
"-NoProfile",
|
|
97
|
-
"-NonInteractive",
|
|
98
|
-
"-Command",
|
|
99
|
-
`[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new(); $p = [wmi]"Win32_Process.Handle='${pid}'"; [Console]::WriteLine($p.CommandLine)`,
|
|
100
|
-
]
|
|
101
|
-
: ["-ww", "-p", String(pid), "-o", "command="];
|
|
102
|
-
const { stdout } = await execFileAsync(file, args, {
|
|
103
|
-
timeout: RESTORED_PROCESS_COMMAND_TIMEOUT_MS,
|
|
104
|
-
});
|
|
105
|
-
const commandLine = stdout.trim();
|
|
106
|
-
return commandLine.length === 0 ? undefined : commandLine;
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return undefined;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
function restoredLaunchCommandMatchesRecord(record, commandLine) {
|
|
113
|
-
return commandLine?.includes(`--user-data-dir=${record.userDataDir}`) === true;
|
|
114
|
-
}
|
|
115
74
|
async function getRestoredProcessVerificationError(record) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return `PID ${String(record.pid)} is alive, but this session has no tracked child handle and its command line could not be inspected; refusing to signal a restored PID that may have been reused.`;
|
|
75
|
+
if (record.startIdentity === undefined || record.startIdentity.length === 0) {
|
|
76
|
+
return `PID ${String(record.pid)} is alive, but this session has no tracked child handle or recorded start identity; refusing to signal a restored PID that may have been reused.`;
|
|
119
77
|
}
|
|
120
|
-
|
|
121
|
-
|
|
78
|
+
const currentIdentity = record.pid === undefined ? undefined : await readProcessStartIdentity(record.pid);
|
|
79
|
+
if (currentIdentity === undefined) {
|
|
80
|
+
return `PID ${String(record.pid)} is alive, but its current start identity could not be inspected; refusing to signal a restored PID that may have been reused.`;
|
|
81
|
+
}
|
|
82
|
+
if (!processStartIdentitiesMatch(record.startIdentity, currentIdentity)) {
|
|
83
|
+
return `PID ${String(record.pid)} is alive, but its start identity does not match the wrapper launch record; refusing to signal a restored PID that may have been reused.`;
|
|
122
84
|
}
|
|
123
85
|
return undefined;
|
|
124
86
|
}
|
|
@@ -178,6 +140,10 @@ async function cleanupRestoredProcess(record, deadlineMs) {
|
|
|
178
140
|
if (await waitForProcessExit(undefined, record.pid, deadlineMs)) {
|
|
179
141
|
return { resource: "process", state: "removed" };
|
|
180
142
|
}
|
|
143
|
+
const killVerificationError = await getRestoredProcessVerificationError(record);
|
|
144
|
+
if (killVerificationError !== undefined) {
|
|
145
|
+
return { error: killVerificationError, resource: "process", state: "failed" };
|
|
146
|
+
}
|
|
181
147
|
signalRestoredLaunchProcess(record, "SIGKILL");
|
|
182
148
|
if (await waitForProcessExit(undefined, record.pid, Date.now() + 1_000)) {
|
|
183
149
|
return { resource: "process", state: "removed" };
|
|
@@ -49,6 +49,7 @@ export async function directoryContainsChromePak(directory) {
|
|
|
49
49
|
}
|
|
50
50
|
export async function hasElectronAppPayload(resourcesDirectory) {
|
|
51
51
|
return ((await pathExists(join(resourcesDirectory, "app.asar"))) ||
|
|
52
|
+
(await pathExists(join(resourcesDirectory, "default_app.asar"))) ||
|
|
52
53
|
(await isDirectory(join(resourcesDirectory, "app"))));
|
|
53
54
|
}
|
|
54
55
|
export async function hasLinuxElectronEvidence(executablePath) {
|
|
@@ -3,6 +3,7 @@ import { rm } from "node:fs/promises";
|
|
|
3
3
|
import { discoverElectronApps, inspectElectronAppPath, inspectElectronExecutablePath, } from "./discovery.js";
|
|
4
4
|
import { createSecureTempDirectory, preserveSecureTempDirectory } from "../temp.js";
|
|
5
5
|
import { stringifyUnknown } from "../results/text.js";
|
|
6
|
+
import { readProcessStartIdentity } from "../process-identity.js";
|
|
6
7
|
import { isElectronLaunchAborted, pollCdpMetadata, pollDevToolsActivePort, } from "./launch-monitor.js";
|
|
7
8
|
import { ElectronLaunchProcess } from "./launch-process.js";
|
|
8
9
|
import { ELECTRON_LAUNCH_RECORD_VERSION, ELECTRON_PROFILE_DIR_PREFIX, } from "./launch-types.js";
|
|
@@ -93,7 +94,7 @@ function launchFailureMessage(reason, target, detail) {
|
|
|
93
94
|
case "aborted":
|
|
94
95
|
return `Electron launch was aborted${target ? ` before ${label} finished starting` : " before the app started"}.`;
|
|
95
96
|
case "non-electron-target":
|
|
96
|
-
return `Electron launch rejected: ${label} does not have Electron
|
|
97
|
+
return `Electron launch rejected: ${label} does not have the required Electron runtime and app payload evidence.`;
|
|
97
98
|
case "policy-blocked":
|
|
98
99
|
return detail ?? `Electron launch blocked by caller policy for ${label}.`;
|
|
99
100
|
case "single-instance-conflict":
|
|
@@ -116,6 +117,7 @@ class ElectronLaunchAttempt {
|
|
|
116
117
|
timeoutMs;
|
|
117
118
|
deadlineMs;
|
|
118
119
|
appArgs;
|
|
120
|
+
startIdentity;
|
|
119
121
|
constructor(options, target, userDataDir, timing) {
|
|
120
122
|
this.options = options;
|
|
121
123
|
this.target = target;
|
|
@@ -194,6 +196,7 @@ class ElectronLaunchAttempt {
|
|
|
194
196
|
platform: this.target.platform,
|
|
195
197
|
port,
|
|
196
198
|
processGroupId: process.platform === "win32" ? undefined : pid,
|
|
199
|
+
startIdentity: this.startIdentity,
|
|
197
200
|
targetType: this.options.targetType,
|
|
198
201
|
userDataDir: this.userDataDir,
|
|
199
202
|
version: ELECTRON_LAUNCH_RECORD_VERSION,
|
|
@@ -211,6 +214,14 @@ class ElectronLaunchAttempt {
|
|
|
211
214
|
if (this.process.child === undefined) {
|
|
212
215
|
return this.fail(isElectronLaunchAborted(this.options.signal) ? "aborted" : "spawn-error", this.process.spawnError?.message);
|
|
213
216
|
}
|
|
217
|
+
const pid = this.process.child.pid;
|
|
218
|
+
this.startIdentity =
|
|
219
|
+
pid === undefined
|
|
220
|
+
? undefined
|
|
221
|
+
: await readProcessStartIdentity(pid, process.platform, {
|
|
222
|
+
deadline: this.deadlineMs,
|
|
223
|
+
signal: this.options.signal,
|
|
224
|
+
});
|
|
214
225
|
const portResult = await pollDevToolsActivePort({
|
|
215
226
|
deadlineMs: this.deadlineMs,
|
|
216
227
|
getChildExit: this.process.childExit,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
2
|
export async function pathExists(path) {
|
|
3
3
|
try {
|
|
4
4
|
await access(path);
|
|
@@ -8,11 +8,3 @@ export async function pathExists(path) {
|
|
|
8
8
|
return false;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
-
export async function directoryExists(path) {
|
|
12
|
-
try {
|
|
13
|
-
return (await stat(path)).isDirectory();
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
@@ -19,9 +19,10 @@ function buildQaVisibleTextPredicate(text) {
|
|
|
19
19
|
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return false;
|
|
20
20
|
return element.getClientRects().length > 0;
|
|
21
21
|
};
|
|
22
|
-
const
|
|
22
|
+
const ancestorsAllowVisibility = (node) => {
|
|
23
23
|
for (let element = node.parentElement; element; element = element.parentElement) {
|
|
24
|
-
|
|
24
|
+
const style = window.getComputedStyle(element);
|
|
25
|
+
if (style.display === "none" || Number(style.opacity) === 0) return false;
|
|
25
26
|
if (element === root) break;
|
|
26
27
|
}
|
|
27
28
|
return true;
|
|
@@ -29,14 +30,16 @@ function buildQaVisibleTextPredicate(text) {
|
|
|
29
30
|
const textWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
30
31
|
let visitedText = 0;
|
|
31
32
|
for (let node = textWalker.nextNode(); node && visitedText < 6000; node = textWalker.nextNode(), visitedText += 1) {
|
|
32
|
-
|
|
33
|
+
const parent = node.parentElement;
|
|
34
|
+
if (!(parent instanceof HTMLElement) || skipTags.has(parent.tagName)) continue;
|
|
35
|
+
if (window.getComputedStyle(parent).visibility === "hidden" || !ancestorsAllowVisibility(node)) continue;
|
|
33
36
|
if (normalize(node.nodeValue).includes(expected)) return true;
|
|
34
37
|
}
|
|
35
38
|
const elementWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
36
39
|
let visitedElements = 0;
|
|
37
40
|
for (let node = elementWalker.nextNode(); node && visitedElements < 3000; node = elementWalker.nextNode(), visitedElements += 1) {
|
|
38
41
|
const element = node;
|
|
39
|
-
if (!isVisibleElement(element) || !("value" in element)) continue;
|
|
42
|
+
if (!isVisibleElement(element) || !ancestorsAllowVisibility(element) || !("value" in element)) continue;
|
|
40
43
|
if (normalize(element.value).includes(expected)) return true;
|
|
41
44
|
}
|
|
42
45
|
return false;
|
|
@@ -47,7 +47,17 @@ export function getFailedNetworkRequests(data, queryText) {
|
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
const seenRequestIds = new Set();
|
|
51
|
+
return failed.filter(({ requestId: id }) => {
|
|
52
|
+
if (id === undefined) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (seenRequestIds.has(id)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
seenRequestIds.add(id);
|
|
59
|
+
return true;
|
|
60
|
+
});
|
|
51
61
|
}
|
|
52
62
|
export function distinctNetworkCandidates(candidates) {
|
|
53
63
|
const keys = new Set();
|
|
@@ -84,7 +94,7 @@ function correlatesWithFailure(value, failedIds, failedUrls) {
|
|
|
84
94
|
const url = typeof value.url === "string" ? value.url : undefined;
|
|
85
95
|
return (id !== undefined && failedIds.has(id)) || (url !== undefined && failedUrls.has(url));
|
|
86
96
|
}
|
|
87
|
-
export function observeInitiatorCandidates(data, failedRequests) {
|
|
97
|
+
export function observeInitiatorCandidates(data, failedRequests, queryText) {
|
|
88
98
|
const failedIds = new Set(failedRequests.map((request) => request.requestId).filter((value) => value !== undefined));
|
|
89
99
|
const failedUrls = new Set(failedRequests.map((request) => request.url).filter((value) => value !== undefined));
|
|
90
100
|
const candidates = [];
|
|
@@ -95,7 +105,9 @@ export function observeInitiatorCandidates(data, failedRequests) {
|
|
|
95
105
|
if (!isRecord(value)) {
|
|
96
106
|
continue;
|
|
97
107
|
}
|
|
98
|
-
|
|
108
|
+
const url = typeof value.url === "string" ? value.url : undefined;
|
|
109
|
+
if (networkRequestMatchesQuery(url, queryText) &&
|
|
110
|
+
(correlatesWithFailure(value, failedIds, failedUrls) || isFailedNetworkRecord(value))) {
|
|
99
111
|
candidates.push(...initiatorCandidates(value));
|
|
100
112
|
}
|
|
101
113
|
}
|
|
@@ -64,8 +64,9 @@ function networkSummary(status, failedCount, candidateCount) {
|
|
|
64
64
|
export async function analyzeNetworkSourceLookupResults(data, compiled, cwd) {
|
|
65
65
|
const { distinctNetworkCandidates, getFailedNetworkRequests, observeInitiatorCandidates } = await import("./lookup-network-evidence.js");
|
|
66
66
|
const { scanWorkspaceRequests } = await import("./lookup-workspace.js");
|
|
67
|
-
const
|
|
68
|
-
const
|
|
67
|
+
const queryText = compiled.query.filter ?? compiled.query.url;
|
|
68
|
+
const failedRequests = getFailedNetworkRequests(data, queryText);
|
|
69
|
+
const observed = observeInitiatorCandidates(data, failedRequests, queryText);
|
|
69
70
|
const workspace = await scanWorkspaceRequests(compiled.query, failedRequests, cwd);
|
|
70
71
|
const candidates = distinctNetworkCandidates([...observed, ...workspace.candidates]);
|
|
71
72
|
const status = networkStatus(failedRequests.length, candidates.length);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { JsonSchema } from "../json-schema.js";
|
|
2
|
-
import { StringEnum
|
|
1
|
+
import { JsonSchema as Type } from "../json-schema.js";
|
|
2
|
+
import { StringEnum } from "../string-enum-schema.js";
|
|
3
3
|
import { ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS, ELECTRON_DISCOVERY_MAX_RESULTS, } from "../electron/discovery-types.js";
|
|
4
4
|
import { AGENT_BROWSER_CODE_MAX_TIMEOUT_MS, AGENT_BROWSER_ELECTRON_HANDOFFS, AGENT_BROWSER_ELECTRON_TARGET_TYPES, AGENT_BROWSER_QA_LOAD_STATES, AGENT_BROWSER_SEMANTIC_ACTIONS, AGENT_BROWSER_SEMANTIC_LOCATORS, DEFAULT_SESSION_MODE, SOURCE_LOOKUP_MAX_WORKSPACE_FILES, } from "./types.js";
|
|
5
5
|
const AGENT_BROWSER_ADVANCED_TOOLS = ["action", "qa", "electron", "source", "network"];
|
|
6
|
-
function outputProperties(
|
|
6
|
+
function outputProperties() {
|
|
7
7
|
return {
|
|
8
8
|
outputPath: Type.Optional(Type.String({
|
|
9
9
|
description: "Result-data path, separate from browser artifact paths.",
|
|
@@ -12,14 +12,14 @@ function outputProperties(Type) {
|
|
|
12
12
|
timeoutMs: Type.Optional(Type.Integer({ description: "Timeout in ms; exceed explicit waits.", minimum: 1 })),
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
-
function sessionModeProperty(
|
|
15
|
+
function sessionModeProperty() {
|
|
16
16
|
return Type.Optional(StringEnum(["auto", "fresh"], {
|
|
17
17
|
description: "Native configured sessions win; auto reuses the browser, fresh intentionally launches a separate managed browser.",
|
|
18
18
|
default: DEFAULT_SESSION_MODE,
|
|
19
19
|
}));
|
|
20
20
|
}
|
|
21
21
|
// Keep the always-on schemas small; specialized guidance belongs to each advanced tool.
|
|
22
|
-
export function createAgentBrowserParamsSchema(
|
|
22
|
+
export function createAgentBrowserParamsSchema() {
|
|
23
23
|
return Type.Unsafe(Type.Object({
|
|
24
24
|
args: Type.Array(Type.String(), {
|
|
25
25
|
description: "Native agent-browser argv without the binary or shell syntax. Fixed sequences: batch --bail with JSON-array stdin.",
|
|
@@ -28,11 +28,11 @@ export function createAgentBrowserParamsSchema(Type = JsonSchema, StringEnum = l
|
|
|
28
28
|
stdin: Type.Optional(Type.String({
|
|
29
29
|
description: "Native batch JSON, eval --stdin source, or auth save --password-stdin text.",
|
|
30
30
|
})),
|
|
31
|
-
...outputProperties(
|
|
32
|
-
sessionMode: sessionModeProperty(
|
|
31
|
+
...outputProperties(),
|
|
32
|
+
sessionMode: sessionModeProperty(),
|
|
33
33
|
}, { additionalProperties: false }));
|
|
34
34
|
}
|
|
35
|
-
export function createAgentBrowserCodeParamsSchema(
|
|
35
|
+
export function createAgentBrowserCodeParamsSchema() {
|
|
36
36
|
return Type.Unsafe(Type.Object({
|
|
37
37
|
code: Type.String({
|
|
38
38
|
description: "JavaScript with await browser({args, stdin?, timeoutMs?}), emit(JSON), and emitImage(image handle). Fresh JS context; persistent browser.",
|
|
@@ -46,11 +46,11 @@ export function createAgentBrowserCodeParamsSchema(Type = JsonSchema) {
|
|
|
46
46
|
namespace: Type.Optional(Type.String({
|
|
47
47
|
description: "Native namespace; empty string selects the default namespace.",
|
|
48
48
|
})),
|
|
49
|
-
...outputProperties(
|
|
49
|
+
...outputProperties(),
|
|
50
50
|
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, maximum: AGENT_BROWSER_CODE_MAX_TIMEOUT_MS })),
|
|
51
51
|
}, { additionalProperties: false }));
|
|
52
52
|
}
|
|
53
|
-
export function createAgentBrowserActionParamsSchema(
|
|
53
|
+
export function createAgentBrowserActionParamsSchema() {
|
|
54
54
|
return Type.Unsafe(Type.Object({
|
|
55
55
|
action: StringEnum(AGENT_BROWSER_SEMANTIC_ACTIONS),
|
|
56
56
|
locator: Type.Optional(StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, {
|
|
@@ -68,7 +68,7 @@ export function createAgentBrowserActionParamsSchema(Type = JsonSchema, StringEn
|
|
|
68
68
|
role: Type.Optional(Type.String({ description: "Role locator; select needs combobox or listbox." })),
|
|
69
69
|
name: Type.Optional(Type.String({ description: "Accessible name." })),
|
|
70
70
|
session: Type.Optional(Type.String({ description: "Upstream session name." })),
|
|
71
|
-
...outputProperties(
|
|
71
|
+
...outputProperties(),
|
|
72
72
|
}, {
|
|
73
73
|
additionalProperties: false,
|
|
74
74
|
// Top-level properties let Pi normalize optional nulls before validating branches.
|
|
@@ -81,7 +81,7 @@ export function createAgentBrowserActionParamsSchema(Type = JsonSchema, StringEn
|
|
|
81
81
|
],
|
|
82
82
|
}));
|
|
83
83
|
}
|
|
84
|
-
export function createAgentBrowserQaParamsSchema(
|
|
84
|
+
export function createAgentBrowserQaParamsSchema() {
|
|
85
85
|
return Type.Unsafe(Type.Object({
|
|
86
86
|
url: Type.Optional(Type.String()),
|
|
87
87
|
attached: Type.Optional(Type.Boolean()),
|
|
@@ -92,8 +92,8 @@ export function createAgentBrowserQaParamsSchema(Type = JsonSchema, StringEnum =
|
|
|
92
92
|
checkErrors: Type.Optional(Type.Boolean()),
|
|
93
93
|
checkNetwork: Type.Optional(Type.Boolean()),
|
|
94
94
|
loadState: Type.Optional(StringEnum(AGENT_BROWSER_QA_LOAD_STATES)),
|
|
95
|
-
...outputProperties(
|
|
96
|
-
sessionMode: sessionModeProperty(
|
|
95
|
+
...outputProperties(),
|
|
96
|
+
sessionMode: sessionModeProperty(),
|
|
97
97
|
}, {
|
|
98
98
|
additionalProperties: false,
|
|
99
99
|
anyOf: [
|
|
@@ -105,8 +105,8 @@ export function createAgentBrowserQaParamsSchema(Type = JsonSchema, StringEnum =
|
|
|
105
105
|
],
|
|
106
106
|
}));
|
|
107
107
|
}
|
|
108
|
-
export function createAgentBrowserElectronParamsSchema(
|
|
109
|
-
const common = { outputPath: outputProperties(
|
|
108
|
+
export function createAgentBrowserElectronParamsSchema() {
|
|
109
|
+
const common = { outputPath: outputProperties().outputPath };
|
|
110
110
|
const timeoutMs = Type.Optional(Type.Integer({ minimum: 1 }));
|
|
111
111
|
const list = {
|
|
112
112
|
query: Type.Optional(Type.String({ description: "Case-insensitive app filter.", minLength: 1 })),
|
|
@@ -147,7 +147,7 @@ export function createAgentBrowserElectronParamsSchema(Type = JsonSchema, String
|
|
|
147
147
|
],
|
|
148
148
|
}));
|
|
149
149
|
}
|
|
150
|
-
export function createAgentBrowserSourceParamsSchema(
|
|
150
|
+
export function createAgentBrowserSourceParamsSchema() {
|
|
151
151
|
return Type.Unsafe(Type.Object({
|
|
152
152
|
selector: Type.Optional(Type.String({ description: "Visible selector or @ref." })),
|
|
153
153
|
reactFiberId: Type.Optional(Type.String({ description: "React fiber id; requires --enable react-devtools." })),
|
|
@@ -158,11 +158,11 @@ export function createAgentBrowserSourceParamsSchema(Type = JsonSchema, StringEn
|
|
|
158
158
|
minimum: 1,
|
|
159
159
|
maximum: SOURCE_LOOKUP_MAX_WORKSPACE_FILES,
|
|
160
160
|
})),
|
|
161
|
-
...outputProperties(
|
|
162
|
-
sessionMode: sessionModeProperty(
|
|
161
|
+
...outputProperties(),
|
|
162
|
+
sessionMode: sessionModeProperty(),
|
|
163
163
|
}, { additionalProperties: false }));
|
|
164
164
|
}
|
|
165
|
-
export function createAgentBrowserNetworkSourceParamsSchema(
|
|
165
|
+
export function createAgentBrowserNetworkSourceParamsSchema() {
|
|
166
166
|
return Type.Unsafe(Type.Object({
|
|
167
167
|
filter: Type.Optional(Type.String({ description: "Network request filter." })),
|
|
168
168
|
namespace: Type.Optional(Type.String()),
|
|
@@ -174,11 +174,11 @@ export function createAgentBrowserNetworkSourceParamsSchema(Type = JsonSchema, S
|
|
|
174
174
|
minimum: 1,
|
|
175
175
|
maximum: SOURCE_LOOKUP_MAX_WORKSPACE_FILES,
|
|
176
176
|
})),
|
|
177
|
-
...outputProperties(
|
|
178
|
-
sessionMode: sessionModeProperty(
|
|
177
|
+
...outputProperties(),
|
|
178
|
+
sessionMode: sessionModeProperty(),
|
|
179
179
|
}, { additionalProperties: false }));
|
|
180
180
|
}
|
|
181
|
-
export function createAgentBrowserToolsParamsSchema(
|
|
181
|
+
export function createAgentBrowserToolsParamsSchema() {
|
|
182
182
|
return Type.Unsafe(Type.Object({
|
|
183
183
|
enable: Type.Optional(Type.Array(StringEnum(AGENT_BROWSER_ADVANCED_TOOLS), {
|
|
184
184
|
description: "Add capabilities to active tools; omit to list inventory.",
|
|
@@ -4,8 +4,9 @@ import { buildNextToolAction, withOptionalSessionArgs } from "../../results/next
|
|
|
4
4
|
import { extractRefSnapshotFromData } from "../../session-page-state.js";
|
|
5
5
|
import { extractStringResultField, runSessionCommandData } from "./session-state.js";
|
|
6
6
|
import { diagnosticErrorText } from "./diagnostic-values.js";
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import { presentElectronManagedSessionTarget } from "./final-result-electron.js";
|
|
8
|
+
function sessionResult(data, field) {
|
|
9
|
+
return extractStringResultField(data, "result") ?? extractStringResultField(data, field);
|
|
9
10
|
}
|
|
10
11
|
async function collectManagedSessionCommandData(options) {
|
|
11
12
|
try {
|
|
@@ -17,8 +18,7 @@ async function collectManagedSessionCommandData(options) {
|
|
|
17
18
|
}
|
|
18
19
|
async function collectElectronManagedSessionUrl(options) {
|
|
19
20
|
const urlResult = await collectManagedSessionCommandData({ ...options, args: ["get", "url"] });
|
|
20
|
-
const url =
|
|
21
|
-
extractStringResultField(urlResult.data, "url"), 300);
|
|
21
|
+
const url = sessionResult(urlResult.data, "url");
|
|
22
22
|
return urlResult.error !== undefined && urlResult.error !== ""
|
|
23
23
|
? { error: urlResult.error }
|
|
24
24
|
: { url };
|
|
@@ -29,8 +29,8 @@ export async function collectElectronManagedSessionTarget(options) {
|
|
|
29
29
|
}
|
|
30
30
|
const request = { ...options, sessionName: options.sessionName };
|
|
31
31
|
const urlResult = await collectManagedSessionCommandData({ ...request, args: ["get", "url"] });
|
|
32
|
-
const url =
|
|
33
|
-
if ((urlResult.error ?? "") !== "" || url === undefined || url === "") {
|
|
32
|
+
const url = sessionResult(urlResult.data, "url");
|
|
33
|
+
if ((urlResult.error ?? "") !== "" || url === undefined || url.trim() === "") {
|
|
34
34
|
return {
|
|
35
35
|
error: urlResult.error ?? "get url returned no active page URL.",
|
|
36
36
|
sessionName: options.sessionName,
|
|
@@ -40,7 +40,7 @@ export async function collectElectronManagedSessionTarget(options) {
|
|
|
40
40
|
...request,
|
|
41
41
|
args: ["get", "title"],
|
|
42
42
|
});
|
|
43
|
-
const title =
|
|
43
|
+
const title = boundElectronProbeString(sessionResult(titleResult.data, "title"), 160);
|
|
44
44
|
return {
|
|
45
45
|
sessionName: options.sessionName,
|
|
46
46
|
title,
|
|
@@ -62,12 +62,13 @@ export async function collectQaAttachedTarget(options) {
|
|
|
62
62
|
url: target.url,
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
-
|
|
65
|
+
const observed = await collectElectronManagedSessionTarget({
|
|
66
66
|
cwd: options.cwd,
|
|
67
67
|
namespace: options.namespace,
|
|
68
68
|
sessionName: options.sessionName,
|
|
69
69
|
signal: options.signal,
|
|
70
70
|
});
|
|
71
|
+
return observed ? presentElectronManagedSessionTarget(observed) : undefined;
|
|
71
72
|
}
|
|
72
73
|
export function formatQaAttachedTargetText(target) {
|
|
73
74
|
return target
|
package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result-electron.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import { boundElectronProbeString } from "../../electron/cdp.js";
|
|
1
2
|
import { buildAgentBrowserResultCategoryDetails } from "../../results/categories.js";
|
|
2
3
|
import { redactSensitiveText } from "../../runtime-redaction.js";
|
|
3
4
|
import { redactToolDetails } from "./final-result-redaction.js";
|
|
5
|
+
export function presentElectronManagedSessionTarget(target) {
|
|
6
|
+
return {
|
|
7
|
+
...target,
|
|
8
|
+
title: boundElectronProbeString(target.title, 160),
|
|
9
|
+
url: boundElectronProbeString(target.url, 300),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
4
12
|
export function getElectronLaunchFailureCategory(failure) {
|
|
5
13
|
switch (failure.reason) {
|
|
6
14
|
case "aborted":
|
|
@@ -5,7 +5,7 @@ import { buildScrollNoopNextActions } from "../interaction-diagnostics.js";
|
|
|
5
5
|
import { buildSessionDetailFields, runSessionCommandData } from "../session-state.js";
|
|
6
6
|
const SCROLL_CONTAINER_DIRECTIONS = new Set(["down", "left", "right", "up"]);
|
|
7
7
|
function getContainerScrollRequest(commandTokens) {
|
|
8
|
-
if (commandTokens[0] !== "scroll" || commandTokens.length < 3) {
|
|
8
|
+
if (commandTokens[0] !== "scroll" || commandTokens.length < 3 || commandTokens.length > 4) {
|
|
9
9
|
return undefined;
|
|
10
10
|
}
|
|
11
11
|
const selector = commandTokens[1];
|
|
@@ -138,7 +138,7 @@ function getPageScrollToRequest(commandTokens) {
|
|
|
138
138
|
return undefined;
|
|
139
139
|
}
|
|
140
140
|
if (commandTokens[1]?.toLowerCase() === "to") {
|
|
141
|
-
return getPageScrollTarget(commandTokens.at(2));
|
|
141
|
+
return commandTokens.length === 3 ? getPageScrollTarget(commandTokens.at(2)) : undefined;
|
|
142
142
|
}
|
|
143
143
|
const direction = commandTokens[1]?.toLowerCase();
|
|
144
144
|
if (!isPageScrollDirection(direction) || commandTokens.length > 3) {
|
package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js
CHANGED
|
@@ -51,6 +51,13 @@ async function collectSnapshotEvidence(options, request) {
|
|
|
51
51
|
? buildSnapshotDiff(options.previousRefSnapshot, fullSnapshot)
|
|
52
52
|
: undefined;
|
|
53
53
|
if (fullSnapshot) {
|
|
54
|
+
if (fullSnapshot.target) {
|
|
55
|
+
options.sessionPageState.applyTabTarget({
|
|
56
|
+
sessionName: options.sessionStateKey ?? options.sessionName,
|
|
57
|
+
target: fullSnapshot.target,
|
|
58
|
+
update: options.sessionPageStateUpdate,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
54
61
|
options.sessionPageState.applyRefSnapshot({
|
|
55
62
|
sessionName: options.sessionStateKey ?? options.sessionName,
|
|
56
63
|
snapshot: fullSnapshot,
|
|
@@ -133,6 +140,8 @@ function snapshotToolResult(options, request, evidence, presentation) {
|
|
|
133
140
|
fullOutputPaths: presentation.fullOutputPaths,
|
|
134
141
|
refSnapshot: evidence.fullSnapshot,
|
|
135
142
|
sessionMode: options.sessionMode,
|
|
143
|
+
sessionTabTarget: options.sessionPageState.get(options.sessionStateKey ?? options.sessionName)
|
|
144
|
+
.tabTarget,
|
|
136
145
|
snapshotDiff: evidence.diff,
|
|
137
146
|
snapshotFilter: snapshotFilterDetails(request, evidence),
|
|
138
147
|
snapshotViewport: evidence.viewport,
|
package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state-electron.js
CHANGED
|
@@ -3,6 +3,7 @@ import { isElectronPostCommandHealthCommand } from "../../command-taxonomy.js";
|
|
|
3
3
|
import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
|
|
4
4
|
import { isAboutBlankUrl, normalizeComparableUrl, } from "../../session-page-state.js";
|
|
5
5
|
import { getGuardedRefUsage } from "./session-state-refs.js";
|
|
6
|
+
import { presentElectronManagedSessionTarget } from "./final-result-electron.js";
|
|
6
7
|
export function isLiveElectronRendererTarget(target) {
|
|
7
8
|
const normalizedUrl = normalizeComparableUrl(target.url);
|
|
8
9
|
if (normalizedUrl === undefined ||
|
|
@@ -105,15 +106,16 @@ export function buildElectronSessionMismatch(options) {
|
|
|
105
106
|
return;
|
|
106
107
|
}
|
|
107
108
|
const nextActions = buildElectronMismatchNextActions(options.record, liveTarget);
|
|
109
|
+
const managedSession = presentElectronManagedSessionTarget(options.managedSession);
|
|
108
110
|
return {
|
|
109
111
|
launchId: options.record.launchId,
|
|
110
112
|
liveTarget,
|
|
111
|
-
managedSession
|
|
113
|
+
managedSession,
|
|
112
114
|
nextActionIds: nextActions.map((action) => action.id),
|
|
113
115
|
reason,
|
|
114
116
|
sessionName: options.record.sessionName,
|
|
115
117
|
statusTargets: options.statusTargets,
|
|
116
|
-
summary: describeElectronMismatch(
|
|
118
|
+
summary: describeElectronMismatch(managedSession, options.record, liveTarget, reason),
|
|
117
119
|
};
|
|
118
120
|
}
|
|
119
121
|
export function formatElectronSessionMismatchText(mismatch) {
|