pi-agent-browser-native 0.2.31 → 0.2.33
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 +35 -0
- package/README.md +64 -18
- package/docs/ARCHITECTURE.md +13 -10
- package/docs/COMMAND_REFERENCE.md +71 -16
- package/docs/ELECTRON.md +387 -0
- package/docs/RELEASE.md +34 -4
- package/docs/REQUIREMENTS.md +5 -3
- package/docs/SUPPORT_MATRIX.md +36 -21
- package/docs/TOOL_CONTRACT.md +198 -40
- package/extensions/agent-browser/index.ts +1585 -3486
- package/extensions/agent-browser/lib/electron/cleanup.ts +287 -0
- package/extensions/agent-browser/lib/electron/discovery.ts +717 -0
- package/extensions/agent-browser/lib/electron/launch.ts +553 -0
- package/extensions/agent-browser/lib/input-modes/electron.ts +170 -0
- package/extensions/agent-browser/lib/input-modes/job.ts +203 -0
- package/extensions/agent-browser/lib/input-modes/lookups.ts +447 -0
- package/extensions/agent-browser/lib/input-modes/params.ts +188 -0
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +107 -0
- package/extensions/agent-browser/lib/input-modes/shared.ts +46 -0
- package/extensions/agent-browser/lib/input-modes/types.ts +221 -0
- package/extensions/agent-browser/lib/input-modes.ts +41 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +696 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +450 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +46 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +711 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +386 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +868 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +476 -0
- package/extensions/agent-browser/lib/orchestration/browser-run.ts +1 -0
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +338 -0
- package/extensions/agent-browser/lib/playbook.ts +15 -13
- package/extensions/agent-browser/lib/process.ts +106 -4
- package/extensions/agent-browser/lib/results/action-recommendations.ts +269 -0
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +114 -0
- package/extensions/agent-browser/lib/results/artifact-state.ts +13 -0
- package/extensions/agent-browser/lib/results/categories.ts +106 -0
- package/extensions/agent-browser/lib/results/contracts.ts +220 -0
- package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +72 -0
- package/extensions/agent-browser/lib/results/envelope.ts +2 -1
- package/extensions/agent-browser/lib/results/network.ts +64 -0
- package/extensions/agent-browser/lib/results/next-actions.ts +117 -0
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +506 -0
- package/extensions/agent-browser/lib/results/presentation/batch.ts +355 -0
- package/extensions/agent-browser/lib/results/presentation/common.ts +53 -0
- package/extensions/agent-browser/lib/results/presentation/content.ts +36 -0
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +730 -0
- package/extensions/agent-browser/lib/results/presentation/errors.ts +125 -0
- package/extensions/agent-browser/lib/results/presentation/large-output.ts +182 -0
- package/extensions/agent-browser/lib/results/presentation/navigation.ts +216 -0
- package/extensions/agent-browser/lib/results/presentation/registry.ts +154 -0
- package/extensions/agent-browser/lib/results/presentation/skills.ts +143 -0
- package/extensions/agent-browser/lib/results/presentation.ts +87 -2369
- package/extensions/agent-browser/lib/results/recovery-actions.ts +139 -0
- package/extensions/agent-browser/lib/results/recovery-next-actions.ts +71 -0
- package/extensions/agent-browser/lib/results/selector-recovery.ts +312 -0
- package/extensions/agent-browser/lib/results/shared.ts +17 -701
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +262 -0
- package/extensions/agent-browser/lib/results/snapshot-refs.ts +100 -0
- package/extensions/agent-browser/lib/results/snapshot-segments.ts +366 -0
- package/extensions/agent-browser/lib/results/snapshot-spill.ts +63 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +37 -489
- package/extensions/agent-browser/lib/results/text.ts +40 -0
- package/extensions/agent-browser/lib/results.ts +16 -5
- package/extensions/agent-browser/lib/session-page-state.ts +486 -0
- package/extensions/agent-browser/lib/temp.ts +26 -0
- package/package.json +6 -4
|
@@ -0,0 +1,868 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import type { ElectronLaunchStatus } from "../../electron/cleanup.js";
|
|
4
|
+
import type { ElectronCdpTarget, ElectronLaunchRecord } from "../../electron/launch.js";
|
|
5
|
+
import { runAgentBrowserProcess } from "../../process.js";
|
|
6
|
+
import { buildAgentBrowserNextActions, getAgentBrowserErrorText, parseAgentBrowserEnvelope, type AgentBrowserBatchResult, type AgentBrowserEnvelope, type AgentBrowserNextAction } from "../../results.js";
|
|
7
|
+
import {
|
|
8
|
+
extractRefSnapshotFromData,
|
|
9
|
+
isAboutBlankUrl,
|
|
10
|
+
normalizeComparableUrl,
|
|
11
|
+
normalizeSessionTabTarget,
|
|
12
|
+
targetsMatch,
|
|
13
|
+
type SessionRefSnapshot,
|
|
14
|
+
type SessionRefSnapshotInvalidation,
|
|
15
|
+
type SessionTabTarget,
|
|
16
|
+
} from "../../session-page-state.js";
|
|
17
|
+
import { chooseOpenResultTabCorrection, redactInvocationArgs, type OpenResultTabCorrection } from "../../runtime.js";
|
|
18
|
+
import { isRecord } from "../../parsing.js";
|
|
19
|
+
import type {
|
|
20
|
+
AboutBlankSessionMismatch,
|
|
21
|
+
BatchCommandStep,
|
|
22
|
+
BrowserRunState,
|
|
23
|
+
BrowserRunStatePatch,
|
|
24
|
+
ElectronManagedSessionTarget,
|
|
25
|
+
ElectronPostCommandHealthDiagnostic,
|
|
26
|
+
ElectronPostCommandHealthReason,
|
|
27
|
+
ElectronRefFreshnessDiagnostic,
|
|
28
|
+
ElectronSessionMismatch,
|
|
29
|
+
ElectronSessionMismatchReason,
|
|
30
|
+
ManagedSessionOutcome,
|
|
31
|
+
NavigationSummary,
|
|
32
|
+
PinnedBatchPlan,
|
|
33
|
+
PinnedBatchUnwrapMode,
|
|
34
|
+
StaleRefPreflight,
|
|
35
|
+
TraceOwner,
|
|
36
|
+
} from "./types.js";
|
|
37
|
+
|
|
38
|
+
export const NAVIGATION_SUMMARY_COMMANDS = new Set(["back", "click", "dblclick", "forward", "reload"]);
|
|
39
|
+
export const NAVIGATION_SUMMARY_EVAL = `({ title: document.title, url: location.href })`;
|
|
40
|
+
|
|
41
|
+
const SESSION_TAB_PINNING_EXCLUDED_COMMANDS = new Set(["close", "goto", "navigate", "open", "session", "tab"]);
|
|
42
|
+
const SESSION_TAB_POST_COMMAND_CORRECTION_EXCLUDED_COMMANDS = new Set(["batch", "close", "session", "tab"]);
|
|
43
|
+
const REF_INVALIDATING_BATCH_COMMANDS = new Set([
|
|
44
|
+
"back",
|
|
45
|
+
"check",
|
|
46
|
+
"click",
|
|
47
|
+
"dblclick",
|
|
48
|
+
"drag",
|
|
49
|
+
"forward",
|
|
50
|
+
"goto",
|
|
51
|
+
"keyboard",
|
|
52
|
+
"mouse",
|
|
53
|
+
"navigate",
|
|
54
|
+
"open",
|
|
55
|
+
"press",
|
|
56
|
+
"reload",
|
|
57
|
+
"select",
|
|
58
|
+
"type",
|
|
59
|
+
"uncheck",
|
|
60
|
+
"upload",
|
|
61
|
+
]);
|
|
62
|
+
const REF_GUARDED_COMMANDS = new Set([
|
|
63
|
+
"check",
|
|
64
|
+
"click",
|
|
65
|
+
"dblclick",
|
|
66
|
+
"download",
|
|
67
|
+
"drag",
|
|
68
|
+
"fill",
|
|
69
|
+
"focus",
|
|
70
|
+
"hover",
|
|
71
|
+
"keyboard",
|
|
72
|
+
"mouse",
|
|
73
|
+
"press",
|
|
74
|
+
"scrollintoview",
|
|
75
|
+
"select",
|
|
76
|
+
"type",
|
|
77
|
+
"uncheck",
|
|
78
|
+
"upload",
|
|
79
|
+
]);
|
|
80
|
+
const ELECTRON_POST_COMMAND_HEALTH_COMMANDS = new Set([
|
|
81
|
+
"back",
|
|
82
|
+
"check",
|
|
83
|
+
"click",
|
|
84
|
+
"dblclick",
|
|
85
|
+
"fill",
|
|
86
|
+
"find",
|
|
87
|
+
"forward",
|
|
88
|
+
"keyboard",
|
|
89
|
+
"mouse",
|
|
90
|
+
"press",
|
|
91
|
+
"reload",
|
|
92
|
+
"select",
|
|
93
|
+
"type",
|
|
94
|
+
"uncheck",
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
export function applyBrowserRunStatePatch(state: BrowserRunState, patch: BrowserRunStatePatch | undefined): void {
|
|
98
|
+
if (!patch) return;
|
|
99
|
+
if ("artifactManifest" in patch) state.artifactManifest = patch.artifactManifest;
|
|
100
|
+
if (patch.freshSessionOrdinal !== undefined) state.freshSessionOrdinal = patch.freshSessionOrdinal;
|
|
101
|
+
if (patch.managedSessionActive !== undefined) state.managedSessionActive = patch.managedSessionActive;
|
|
102
|
+
if (patch.managedSessionCwd !== undefined) state.managedSessionCwd = patch.managedSessionCwd;
|
|
103
|
+
if (patch.managedSessionName !== undefined) state.managedSessionName = patch.managedSessionName;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function mergeBrowserRunStatePatch(left: BrowserRunStatePatch | undefined, right: BrowserRunStatePatch | undefined): BrowserRunStatePatch {
|
|
107
|
+
return { ...(left ?? {}), ...(right ?? {}) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function buildSessionDetailFields(sessionName: string | undefined, usedImplicitSession: boolean): Record<string, unknown> {
|
|
111
|
+
return sessionName ? { sessionName, usedImplicitSession } : {};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function buildManagedSessionOutcome(options: {
|
|
115
|
+
activeAfter: boolean;
|
|
116
|
+
activeBefore: boolean;
|
|
117
|
+
attemptedSessionName?: string;
|
|
118
|
+
command?: string;
|
|
119
|
+
currentSessionName: string;
|
|
120
|
+
previousSessionName: string;
|
|
121
|
+
replacedSessionName?: string;
|
|
122
|
+
sessionMode: "auto" | "fresh";
|
|
123
|
+
succeeded: boolean;
|
|
124
|
+
}): ManagedSessionOutcome | undefined {
|
|
125
|
+
const { activeAfter, activeBefore, attemptedSessionName, command, currentSessionName, previousSessionName, replacedSessionName, sessionMode, succeeded } = options;
|
|
126
|
+
if (!attemptedSessionName) return undefined;
|
|
127
|
+
let status: ManagedSessionOutcome["status"];
|
|
128
|
+
let summary: string;
|
|
129
|
+
if (command === "close") {
|
|
130
|
+
status = succeeded ? "closed" : activeBefore ? "preserved" : "abandoned";
|
|
131
|
+
summary = succeeded
|
|
132
|
+
? `Managed session ${attemptedSessionName} was closed.`
|
|
133
|
+
: activeBefore
|
|
134
|
+
? `Managed session close failed; previous managed session ${previousSessionName} remains current.`
|
|
135
|
+
: `Managed session close failed; no managed session is active.`;
|
|
136
|
+
} else if (succeeded) {
|
|
137
|
+
if (replacedSessionName) {
|
|
138
|
+
status = "replaced";
|
|
139
|
+
summary = `Managed session ${replacedSessionName} was replaced by ${currentSessionName}.`;
|
|
140
|
+
} else if (!activeBefore && activeAfter) {
|
|
141
|
+
status = "created";
|
|
142
|
+
summary = `Managed session ${currentSessionName} is now current.`;
|
|
143
|
+
} else {
|
|
144
|
+
status = "unchanged";
|
|
145
|
+
summary = `Managed session ${currentSessionName} remains current.`;
|
|
146
|
+
}
|
|
147
|
+
} else if (activeBefore) {
|
|
148
|
+
status = "preserved";
|
|
149
|
+
summary = sessionMode === "fresh" && attemptedSessionName !== previousSessionName
|
|
150
|
+
? `Fresh managed session ${attemptedSessionName} failed before becoming current; previous managed session ${previousSessionName} was preserved.`
|
|
151
|
+
: `Managed session call failed; previous managed session ${previousSessionName} was preserved.`;
|
|
152
|
+
} else {
|
|
153
|
+
status = "abandoned";
|
|
154
|
+
summary = sessionMode === "fresh"
|
|
155
|
+
? `Fresh managed session ${attemptedSessionName} failed before becoming current; no previous managed session was active, so no managed session is current.`
|
|
156
|
+
: `Managed session call failed before any managed session became current.`;
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
activeAfter,
|
|
160
|
+
activeBefore,
|
|
161
|
+
attemptedSessionName,
|
|
162
|
+
currentSessionName,
|
|
163
|
+
previousSessionName,
|
|
164
|
+
replacedSessionName,
|
|
165
|
+
sessionMode,
|
|
166
|
+
status,
|
|
167
|
+
succeeded,
|
|
168
|
+
summary,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function formatManagedSessionOutcomeText(outcome: ManagedSessionOutcome | undefined): string | undefined {
|
|
173
|
+
return outcome && !outcome.succeeded && outcome.sessionMode === "fresh" ? `Managed session outcome: ${outcome.summary}` : undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getTraceOwner(command: string | undefined): TraceOwner | undefined {
|
|
177
|
+
return command === "trace" || command === "profiler" ? command : undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function getTraceOwnerGuardMessage(options: {
|
|
181
|
+
command: string | undefined;
|
|
182
|
+
sessionName: string | undefined;
|
|
183
|
+
subcommand: string | undefined;
|
|
184
|
+
traceOwners: Map<string, TraceOwner>;
|
|
185
|
+
}): string | undefined {
|
|
186
|
+
const owner = getTraceOwner(options.command);
|
|
187
|
+
if (!owner || !options.sessionName || (options.subcommand !== "start" && options.subcommand !== "stop")) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
const activeOwner = options.traceOwners.get(options.sessionName);
|
|
191
|
+
if (!activeOwner || activeOwner === owner) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
return options.subcommand === "start"
|
|
195
|
+
? `Wrapper believes ${activeOwner} tracing is active for session ${options.sessionName}; stop ${activeOwner} before starting ${owner}.`
|
|
196
|
+
: `Wrapper believes tracing for session ${options.sessionName} is owned by ${activeOwner}; run ${activeOwner} stop instead of ${owner} stop.`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function updateTraceOwnerState(options: {
|
|
200
|
+
command: string | undefined;
|
|
201
|
+
sessionName: string | undefined;
|
|
202
|
+
subcommand: string | undefined;
|
|
203
|
+
succeeded: boolean;
|
|
204
|
+
traceOwners: Map<string, TraceOwner>;
|
|
205
|
+
}): void {
|
|
206
|
+
const owner = getTraceOwner(options.command);
|
|
207
|
+
if (!owner || !options.sessionName || !options.succeeded) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (options.subcommand === "start") {
|
|
211
|
+
options.traceOwners.set(options.sessionName, owner);
|
|
212
|
+
}
|
|
213
|
+
if (options.subcommand === "stop" && options.traceOwners.get(options.sessionName) === owner) {
|
|
214
|
+
options.traceOwners.delete(options.sessionName);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function extractStringResultField(data: unknown, fieldName: "result" | "title" | "url" | "value"): string | undefined {
|
|
219
|
+
if (typeof data === "string") {
|
|
220
|
+
if (fieldName === "value") return data;
|
|
221
|
+
const text = data.trim();
|
|
222
|
+
return text.length > 0 ? text : undefined;
|
|
223
|
+
}
|
|
224
|
+
if (!isRecord(data) || typeof data[fieldName] !== "string") {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
if (fieldName === "value") return data[fieldName];
|
|
228
|
+
const text = data[fieldName].trim();
|
|
229
|
+
return text.length > 0 ? text : undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function extractNavigationSummaryFromData(data: unknown): NavigationSummary | undefined {
|
|
233
|
+
const result = isRecord(data) && isRecord(data.result) ? data.result : data;
|
|
234
|
+
const title = extractStringResultField(result, "title");
|
|
235
|
+
const url = extractStringResultField(result, "url");
|
|
236
|
+
return title || url ? { title, url } : undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function shouldCaptureNavigationSummary(command: string | undefined, data: unknown): boolean {
|
|
240
|
+
return (
|
|
241
|
+
command !== undefined &&
|
|
242
|
+
NAVIGATION_SUMMARY_COMMANDS.has(command) &&
|
|
243
|
+
(!isRecord(data) || (typeof data.title !== "string" && typeof data.url !== "string"))
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function mergeNavigationSummaryIntoData(data: unknown, navigationSummary: NavigationSummary): unknown {
|
|
248
|
+
if (isRecord(data)) {
|
|
249
|
+
return { ...data, navigationSummary };
|
|
250
|
+
}
|
|
251
|
+
return { navigationSummary, result: data };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function buildAboutBlankRecoveryHint(): string {
|
|
255
|
+
return "agent_browser detected that the active tab became about:blank while this session still had a prior intended tab. Run tab list for this session and re-select the intended tab, or retry with sessionMode:fresh if the tab is gone.".replace("sessionMode:fresh", "sessionMode=fresh");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function buildAboutBlankWarning(mismatch: AboutBlankSessionMismatch): string {
|
|
259
|
+
return `Warning: agent_browser detected that this session returned about:blank while the prior intended tab was ${mismatch.targetUrl}. ${mismatch.recoveryApplied ? "The wrapper re-selected the intended tab for the session." : "No matching tab could be re-selected; run tab list for the same session or retry with sessionMode=fresh."}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function extractBatchResultCommand(item: Record<string, unknown>): string[] {
|
|
263
|
+
return Array.isArray(item.command) ? item.command.filter((token): token is string => typeof token === "string") : [];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function validateUserBatchStep(
|
|
267
|
+
step: unknown,
|
|
268
|
+
index: number,
|
|
269
|
+
):
|
|
270
|
+
| { ok: true; step: BatchCommandStep }
|
|
271
|
+
| { ok: false; error: string } {
|
|
272
|
+
if (!Array.isArray(step)) {
|
|
273
|
+
return {
|
|
274
|
+
ok: false,
|
|
275
|
+
error: `agent_browser batch stdin step ${index} must be a non-empty array of string command tokens.`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (step.length === 0) {
|
|
279
|
+
return {
|
|
280
|
+
ok: false,
|
|
281
|
+
error: `agent_browser batch stdin step ${index} must not be empty.`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const invalidTokenIndex = step.findIndex((token) => typeof token !== "string");
|
|
285
|
+
if (invalidTokenIndex !== -1) {
|
|
286
|
+
return {
|
|
287
|
+
ok: false,
|
|
288
|
+
error: `agent_browser batch stdin step ${index} token ${invalidTokenIndex} must be a string.`,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return { ok: true, step: step as BatchCommandStep };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function parseUserBatchStdin(stdin: string | undefined): { error?: string; steps?: BatchCommandStep[] } {
|
|
295
|
+
if (stdin === undefined) {
|
|
296
|
+
return { steps: [] };
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const parsed = JSON.parse(stdin) as unknown;
|
|
300
|
+
if (!Array.isArray(parsed)) {
|
|
301
|
+
return { error: "agent_browser batch stdin must be a JSON array of command steps." };
|
|
302
|
+
}
|
|
303
|
+
const steps: BatchCommandStep[] = [];
|
|
304
|
+
for (const [index, rawStep] of parsed.entries()) {
|
|
305
|
+
const validated = validateUserBatchStep(rawStep, index);
|
|
306
|
+
if (!validated.ok) {
|
|
307
|
+
return { error: validated.error };
|
|
308
|
+
}
|
|
309
|
+
steps.push(validated.step);
|
|
310
|
+
}
|
|
311
|
+
return { steps };
|
|
312
|
+
} catch (error) {
|
|
313
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
314
|
+
return { error: `agent_browser batch stdin could not be parsed as JSON: ${message}` };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function getStaleRefArgs(commandTokens: string[], stdin?: string): string[] {
|
|
319
|
+
if (commandTokens[0] !== "batch" || stdin === undefined) {
|
|
320
|
+
return commandTokens;
|
|
321
|
+
}
|
|
322
|
+
const parsed = parseUserBatchStdin(stdin);
|
|
323
|
+
if (parsed.error || parsed.steps === undefined) {
|
|
324
|
+
return commandTokens;
|
|
325
|
+
}
|
|
326
|
+
return parsed.steps.flatMap((step) => step);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function collectRefsFromTokens(tokens: string[]): string[] {
|
|
330
|
+
return tokens.filter((token) => /^@e\d+\b/.test(token)).map((token) => token.slice(1));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function getGuardedRefUsage(commandTokens: string[], stdin?: string, options: { includeRefsAfterBatchSnapshot?: boolean } = {}): string[] {
|
|
334
|
+
const collectFromStep = (step: string[]) => REF_GUARDED_COMMANDS.has(step[0] ?? "") ? collectRefsFromTokens(step) : [];
|
|
335
|
+
if (commandTokens[0] !== "batch" || stdin === undefined) {
|
|
336
|
+
return collectFromStep(commandTokens);
|
|
337
|
+
}
|
|
338
|
+
const parsed = parseUserBatchStdin(stdin);
|
|
339
|
+
if (parsed.error || parsed.steps === undefined) {
|
|
340
|
+
return collectFromStep(commandTokens);
|
|
341
|
+
}
|
|
342
|
+
const refsBeforeInBatchSnapshot: string[] = [];
|
|
343
|
+
for (const step of parsed.steps) {
|
|
344
|
+
if (!options.includeRefsAfterBatchSnapshot && (step[0] ?? "") === "snapshot") break;
|
|
345
|
+
refsBeforeInBatchSnapshot.push(...collectFromStep(step));
|
|
346
|
+
}
|
|
347
|
+
return refsBeforeInBatchSnapshot;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function getBatchRefInvalidationMessage(commandTokens: string[], stdin?: string): string | undefined {
|
|
351
|
+
if (commandTokens[0] !== "batch" || stdin === undefined) return undefined;
|
|
352
|
+
const parsed = parseUserBatchStdin(stdin);
|
|
353
|
+
if (parsed.error || parsed.steps === undefined) return undefined;
|
|
354
|
+
let priorStepInvalidatesRefs = false;
|
|
355
|
+
for (const step of parsed.steps) {
|
|
356
|
+
if ((step[0] ?? "") === "snapshot") {
|
|
357
|
+
priorStepInvalidatesRefs = false;
|
|
358
|
+
}
|
|
359
|
+
const refIds = collectRefsFromTokens(step);
|
|
360
|
+
if (refIds.length > 0 && REF_GUARDED_COMMANDS.has(step[0] ?? "") && priorStepInvalidatesRefs) {
|
|
361
|
+
return `Batch step ${step[0]} uses page-scoped ref ${refIds.map((refId) => `@${refId}`).join(", ")} after an earlier batch step can navigate or mutate the page. Split the batch, run snapshot -i after the page-changing step, then retry with current refs.`;
|
|
362
|
+
}
|
|
363
|
+
if (REF_INVALIDATING_BATCH_COMMANDS.has(step[0] ?? "")) {
|
|
364
|
+
priorStepInvalidatesRefs = true;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function buildStaleRefPreflight(options: {
|
|
371
|
+
commandTokens: string[];
|
|
372
|
+
currentTarget?: SessionTabTarget;
|
|
373
|
+
refSnapshot?: SessionRefSnapshot;
|
|
374
|
+
refSnapshotInvalidation?: SessionRefSnapshotInvalidation;
|
|
375
|
+
stdin?: string;
|
|
376
|
+
}): StaleRefPreflight | undefined {
|
|
377
|
+
const guardedRefIds = [...new Set(getGuardedRefUsage(options.commandTokens, options.stdin))];
|
|
378
|
+
const usedRefIds = options.refSnapshotInvalidation
|
|
379
|
+
? [...new Set(getGuardedRefUsage(options.commandTokens, options.stdin, { includeRefsAfterBatchSnapshot: true }))]
|
|
380
|
+
: guardedRefIds;
|
|
381
|
+
const batchInvalidationMessage = getBatchRefInvalidationMessage(options.commandTokens, options.stdin);
|
|
382
|
+
if (batchInvalidationMessage && guardedRefIds.length > 0) {
|
|
383
|
+
return {
|
|
384
|
+
message: batchInvalidationMessage,
|
|
385
|
+
refIds: guardedRefIds,
|
|
386
|
+
snapshot: options.refSnapshot,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
if (usedRefIds.length === 0) return undefined;
|
|
390
|
+
if (options.refSnapshotInvalidation) {
|
|
391
|
+
return {
|
|
392
|
+
message: `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} cannot be used because the latest snapshot for this session reported No active page. Run snapshot -i successfully before using page-scoped refs.`,
|
|
393
|
+
refIds: usedRefIds,
|
|
394
|
+
snapshotInvalidation: options.refSnapshotInvalidation,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
if (!options.refSnapshot) return undefined;
|
|
398
|
+
if (!targetsMatch(options.refSnapshot.target, options.currentTarget)) {
|
|
399
|
+
return {
|
|
400
|
+
message: `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} came from a snapshot for ${options.refSnapshot.target?.url ?? "a prior page"}, but the current session target is ${options.currentTarget?.url ?? "unknown"}. Run snapshot -i again before using page-scoped refs.`,
|
|
401
|
+
refIds: usedRefIds,
|
|
402
|
+
snapshot: options.refSnapshot,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
const knownRefs = new Set(options.refSnapshot.refIds);
|
|
406
|
+
const missingRefs = usedRefIds.filter((refId) => !knownRefs.has(refId));
|
|
407
|
+
if (missingRefs.length > 0) {
|
|
408
|
+
return {
|
|
409
|
+
message: `Ref ${missingRefs.map((refId) => `@${refId}`).join(", ")} was not present in the latest snapshot for this session. Run snapshot -i again before using page-scoped refs.`,
|
|
410
|
+
refIds: missingRefs,
|
|
411
|
+
snapshot: options.refSnapshot,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
return undefined;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function supportsPinnedStdinCommand(options: { command?: string; commandTokens: string[]; stdin?: string }): boolean {
|
|
418
|
+
if (options.command === "batch") {
|
|
419
|
+
return options.stdin !== undefined;
|
|
420
|
+
}
|
|
421
|
+
if (options.stdin === undefined) {
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
if (options.command === "eval") {
|
|
425
|
+
return options.commandTokens.includes("--stdin");
|
|
426
|
+
}
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function shouldPinSessionTabForCommand(options: {
|
|
431
|
+
command?: string;
|
|
432
|
+
commandTokens: string[];
|
|
433
|
+
pinningRequired?: boolean;
|
|
434
|
+
sessionName?: string;
|
|
435
|
+
stdin?: string;
|
|
436
|
+
}): boolean {
|
|
437
|
+
return (
|
|
438
|
+
options.pinningRequired === true &&
|
|
439
|
+
options.sessionName !== undefined &&
|
|
440
|
+
options.command !== undefined &&
|
|
441
|
+
!SESSION_TAB_PINNING_EXCLUDED_COMMANDS.has(options.command) &&
|
|
442
|
+
supportsPinnedStdinCommand(options)
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function buildPinnedBatchPlan(options: {
|
|
447
|
+
command?: string;
|
|
448
|
+
commandTokens: string[];
|
|
449
|
+
selectedTab: string;
|
|
450
|
+
stdin?: string;
|
|
451
|
+
}): PinnedBatchPlan | { error: string } | undefined {
|
|
452
|
+
if (options.command === "batch") {
|
|
453
|
+
const parsed = parseUserBatchStdin(options.stdin);
|
|
454
|
+
if (parsed.error) {
|
|
455
|
+
return { error: parsed.error };
|
|
456
|
+
}
|
|
457
|
+
const tabSelectionStep: BatchCommandStep = ["tab", options.selectedTab];
|
|
458
|
+
return {
|
|
459
|
+
includeNavigationSummary: false,
|
|
460
|
+
steps: [tabSelectionStep, ...(parsed.steps ?? [])],
|
|
461
|
+
unwrapMode: "user-batch",
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
if (options.commandTokens.length === 0) {
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
const includeNavigationSummary = options.command !== undefined && NAVIGATION_SUMMARY_COMMANDS.has(options.command);
|
|
468
|
+
const tabSelectionStep: BatchCommandStep = ["tab", options.selectedTab];
|
|
469
|
+
const commandStep = options.commandTokens as BatchCommandStep;
|
|
470
|
+
const navigationSummarySteps: BatchCommandStep[] = includeNavigationSummary ? [["eval", NAVIGATION_SUMMARY_EVAL]] : [];
|
|
471
|
+
return {
|
|
472
|
+
includeNavigationSummary,
|
|
473
|
+
steps: [tabSelectionStep, commandStep, ...navigationSummarySteps],
|
|
474
|
+
unwrapMode: "single-command",
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function shouldCorrectSessionTabAfterCommand(options: { command?: string; pinningRequired?: boolean; sessionName?: string }): boolean {
|
|
479
|
+
return (
|
|
480
|
+
options.pinningRequired === true &&
|
|
481
|
+
options.sessionName !== undefined &&
|
|
482
|
+
options.command !== undefined &&
|
|
483
|
+
!SESSION_TAB_POST_COMMAND_CORRECTION_EXCLUDED_COMMANDS.has(options.command)
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function selectSessionTargetTab(options: {
|
|
488
|
+
tabs: Array<{ active?: boolean; index?: number; label?: string; tabId?: string; title?: string; url?: string }>;
|
|
489
|
+
target: SessionTabTarget;
|
|
490
|
+
}): OpenResultTabCorrection | undefined {
|
|
491
|
+
return chooseOpenResultTabCorrection({
|
|
492
|
+
tabs: options.tabs,
|
|
493
|
+
targetTitle: options.target.title,
|
|
494
|
+
targetUrl: options.target.url,
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function unwrapPinnedSessionBatchEnvelope(options: {
|
|
499
|
+
envelope?: AgentBrowserEnvelope;
|
|
500
|
+
includeNavigationSummary: boolean;
|
|
501
|
+
mode?: PinnedBatchUnwrapMode;
|
|
502
|
+
}): { envelope?: AgentBrowserEnvelope; navigationSummary?: NavigationSummary; parseError?: string } {
|
|
503
|
+
if (!options.envelope) {
|
|
504
|
+
return {};
|
|
505
|
+
}
|
|
506
|
+
if (!Array.isArray(options.envelope.data)) {
|
|
507
|
+
return {
|
|
508
|
+
parseError: "agent-browser returned an unexpected response while applying the wrapper's tab-pinning batch.",
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const steps = options.envelope.data.filter(isRecord) as AgentBrowserBatchResult[];
|
|
513
|
+
const tabSelectionStep = steps[0];
|
|
514
|
+
const commandStep = steps[1];
|
|
515
|
+
if (tabSelectionStep?.success === false) {
|
|
516
|
+
return {
|
|
517
|
+
envelope: {
|
|
518
|
+
success: false,
|
|
519
|
+
error: tabSelectionStep.error ?? "agent-browser could not re-select the intended tab before running the command.",
|
|
520
|
+
},
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (options.mode === "user-batch") {
|
|
524
|
+
const userSteps = steps.slice(1);
|
|
525
|
+
return {
|
|
526
|
+
envelope: {
|
|
527
|
+
success: userSteps.every((step) => step.success !== false),
|
|
528
|
+
data: userSteps,
|
|
529
|
+
error: userSteps.find((step) => step.success === false)?.error,
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
if (!commandStep) {
|
|
534
|
+
return {
|
|
535
|
+
envelope: {
|
|
536
|
+
success: false,
|
|
537
|
+
error: "agent-browser did not return the corrected command result.",
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const navigationSummaryStep = options.includeNavigationSummary ? steps[2] : undefined;
|
|
543
|
+
const navigationSummary = normalizeSessionTabTarget(extractNavigationSummaryFromData(navigationSummaryStep?.result));
|
|
544
|
+
return {
|
|
545
|
+
envelope: {
|
|
546
|
+
success: commandStep.success !== false,
|
|
547
|
+
data: commandStep.result,
|
|
548
|
+
error: commandStep.success === false ? commandStep.error : undefined,
|
|
549
|
+
},
|
|
550
|
+
navigationSummary,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export async function runSessionCommandData(options: {
|
|
555
|
+
args: string[];
|
|
556
|
+
cwd: string;
|
|
557
|
+
sessionName?: string;
|
|
558
|
+
signal?: AbortSignal;
|
|
559
|
+
stdin?: string;
|
|
560
|
+
timeoutMs?: number;
|
|
561
|
+
}): Promise<unknown | undefined> {
|
|
562
|
+
const { args, cwd, sessionName, signal, stdin, timeoutMs } = options;
|
|
563
|
+
if (!sessionName) return undefined;
|
|
564
|
+
|
|
565
|
+
const processResult = await runAgentBrowserProcess({
|
|
566
|
+
args: ["--json", "--session", sessionName, ...args],
|
|
567
|
+
cwd,
|
|
568
|
+
signal,
|
|
569
|
+
stdin,
|
|
570
|
+
timeoutMs,
|
|
571
|
+
});
|
|
572
|
+
try {
|
|
573
|
+
if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
|
|
574
|
+
return undefined;
|
|
575
|
+
}
|
|
576
|
+
const parsed = await parseAgentBrowserEnvelope({
|
|
577
|
+
stdout: processResult.stdout,
|
|
578
|
+
stdoutPath: processResult.stdoutSpillPath,
|
|
579
|
+
});
|
|
580
|
+
if (parsed.parseError || parsed.envelope?.success === false) {
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
return parsed.envelope?.data;
|
|
584
|
+
} finally {
|
|
585
|
+
if (processResult.stdoutSpillPath) {
|
|
586
|
+
await rm(processResult.stdoutSpillPath, { force: true }).catch(() => undefined);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export async function collectOpenResultTabCorrection(options: {
|
|
592
|
+
cwd: string;
|
|
593
|
+
sessionName?: string;
|
|
594
|
+
signal?: AbortSignal;
|
|
595
|
+
targetTitle?: string;
|
|
596
|
+
targetUrl?: string;
|
|
597
|
+
}): Promise<OpenResultTabCorrection | undefined> {
|
|
598
|
+
const { cwd, sessionName, signal, targetTitle, targetUrl } = options;
|
|
599
|
+
const tabData = await runSessionCommandData({ args: ["tab", "list"], cwd, sessionName, signal });
|
|
600
|
+
if (!isRecord(tabData) || !Array.isArray(tabData.tabs)) {
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
603
|
+
const tabs = tabData.tabs.filter(isRecord).map((tab, index) => ({
|
|
604
|
+
active: tab.active === true,
|
|
605
|
+
index: typeof tab.index === "number" ? tab.index : index,
|
|
606
|
+
label: typeof tab.label === "string" ? tab.label : undefined,
|
|
607
|
+
tabId: typeof tab.tabId === "string" ? tab.tabId : undefined,
|
|
608
|
+
title: typeof tab.title === "string" ? tab.title : undefined,
|
|
609
|
+
url: typeof tab.url === "string" ? tab.url : undefined,
|
|
610
|
+
}));
|
|
611
|
+
return chooseOpenResultTabCorrection({ tabs, targetTitle, targetUrl });
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export async function collectSessionTabSelection(options: {
|
|
615
|
+
cwd: string;
|
|
616
|
+
sessionName?: string;
|
|
617
|
+
signal?: AbortSignal;
|
|
618
|
+
target: SessionTabTarget;
|
|
619
|
+
}): Promise<OpenResultTabCorrection | undefined> {
|
|
620
|
+
const { cwd, sessionName, signal, target } = options;
|
|
621
|
+
const tabData = await runSessionCommandData({ args: ["tab", "list"], cwd, sessionName, signal });
|
|
622
|
+
if (!isRecord(tabData) || !Array.isArray(tabData.tabs)) {
|
|
623
|
+
return undefined;
|
|
624
|
+
}
|
|
625
|
+
const tabs = tabData.tabs.filter(isRecord).map((tab, index) => ({
|
|
626
|
+
active: tab.active === true,
|
|
627
|
+
index: typeof tab.index === "number" ? tab.index : index,
|
|
628
|
+
label: typeof tab.label === "string" ? tab.label : undefined,
|
|
629
|
+
tabId: typeof tab.tabId === "string" ? tab.tabId : undefined,
|
|
630
|
+
title: typeof tab.title === "string" ? tab.title : undefined,
|
|
631
|
+
url: typeof tab.url === "string" ? tab.url : undefined,
|
|
632
|
+
}));
|
|
633
|
+
return selectSessionTargetTab({ tabs, target });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export async function applyOpenResultTabCorrection(options: {
|
|
637
|
+
correction: OpenResultTabCorrection;
|
|
638
|
+
cwd: string;
|
|
639
|
+
sessionName?: string;
|
|
640
|
+
signal?: AbortSignal;
|
|
641
|
+
}): Promise<OpenResultTabCorrection | undefined> {
|
|
642
|
+
const { correction, cwd, sessionName, signal } = options;
|
|
643
|
+
const result = await runSessionCommandData({
|
|
644
|
+
args: ["tab", correction.selectedTab],
|
|
645
|
+
cwd,
|
|
646
|
+
sessionName,
|
|
647
|
+
signal,
|
|
648
|
+
});
|
|
649
|
+
return result === undefined ? undefined : correction;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
export function isLiveElectronRendererTarget(target: ElectronCdpTarget): boolean {
|
|
653
|
+
const normalizedUrl = normalizeComparableUrl(target.url);
|
|
654
|
+
if (!normalizedUrl || normalizedUrl === "about:blank" || normalizedUrl.startsWith("devtools://")) return false;
|
|
655
|
+
return target.type === undefined || target.type === "page" || target.type === "webview";
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function getLiveElectronRendererTargets(targets: ElectronCdpTarget[]): ElectronCdpTarget[] {
|
|
659
|
+
return targets.filter(isLiveElectronRendererTarget);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export function electronTargetLabel(target: ElectronCdpTarget | undefined): string {
|
|
663
|
+
if (!target) return "unknown target";
|
|
664
|
+
return [target.title, target.url, target.id].find((value) => typeof value === "string" && value.trim().length > 0) ?? "unknown target";
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function getActiveElectronRecords(records: Map<string, ElectronLaunchRecord>): ElectronLaunchRecord[] {
|
|
668
|
+
return [...records.values()].filter((record) => record.cleanupState === "active" || record.cleanupState === "dead" || record.cleanupState === "partial" || record.cleanupState === "failed");
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
export function findElectronLaunchRecordForSession(sessionName: string | undefined, records: Map<string, ElectronLaunchRecord>): ElectronLaunchRecord | undefined {
|
|
672
|
+
if (!sessionName) return undefined;
|
|
673
|
+
return getActiveElectronRecords(records).find((record) => record.sessionName === sessionName);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function buildElectronReattachNextAction(record: ElectronLaunchRecord, liveTarget?: ElectronCdpTarget): AgentBrowserNextAction {
|
|
677
|
+
const endpoint = liveTarget?.webSocketDebuggerUrl ?? record.webSocketDebuggerUrl ?? String(record.port);
|
|
678
|
+
return {
|
|
679
|
+
id: "reattach-electron-launch",
|
|
680
|
+
params: { args: ["connect", endpoint], sessionMode: "fresh" },
|
|
681
|
+
reason: "Attach a fresh managed session to the same wrapper-tracked Electron debug endpoint when the current session no longer matches the live renderer.",
|
|
682
|
+
safety: "Creates a new managed browser session; it does not mutate the Electron app. Keep the launchId for later status and cleanup.",
|
|
683
|
+
tool: "agent_browser",
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export function buildElectronMismatchNextActions(record: ElectronLaunchRecord, liveTarget?: ElectronCdpTarget): AgentBrowserNextAction[] {
|
|
688
|
+
const baseActions = buildAgentBrowserNextActions({
|
|
689
|
+
electron: { launchId: record.launchId, sessionName: record.sessionName, status: record.cleanupState },
|
|
690
|
+
resultCategory: "success",
|
|
691
|
+
successCategory: "completed",
|
|
692
|
+
}) ?? [];
|
|
693
|
+
const reattachAction = buildElectronReattachNextAction(record, liveTarget);
|
|
694
|
+
const actions: AgentBrowserNextAction[] = [];
|
|
695
|
+
for (const action of baseActions) {
|
|
696
|
+
actions.push(action);
|
|
697
|
+
if (action.id === "probe-electron-launch") actions.push(reattachAction);
|
|
698
|
+
}
|
|
699
|
+
if (!actions.some((action) => action.id === reattachAction.id)) actions.push(reattachAction);
|
|
700
|
+
return actions;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export function buildElectronSessionMismatch(options: {
|
|
704
|
+
managedSession: ElectronManagedSessionTarget;
|
|
705
|
+
record: ElectronLaunchRecord;
|
|
706
|
+
statusTargets: ElectronCdpTarget[];
|
|
707
|
+
}): ElectronSessionMismatch | undefined {
|
|
708
|
+
const liveTargets = getLiveElectronRendererTargets(options.statusTargets);
|
|
709
|
+
if (liveTargets.length === 0) return undefined;
|
|
710
|
+
const managedUrl = normalizeComparableUrl(options.managedSession.url);
|
|
711
|
+
const matchingLiveTarget = managedUrl
|
|
712
|
+
? liveTargets.find((target) => normalizeComparableUrl(target.url) === managedUrl)
|
|
713
|
+
: undefined;
|
|
714
|
+
if (matchingLiveTarget) return undefined;
|
|
715
|
+
|
|
716
|
+
const liveTarget = liveTargets[0];
|
|
717
|
+
let reason: ElectronSessionMismatchReason | undefined;
|
|
718
|
+
if (isAboutBlankUrl(options.managedSession.url)) {
|
|
719
|
+
reason = "managed-session-about-blank-while-launch-target-live";
|
|
720
|
+
} else if (options.record.sessionName && options.record.sessionName !== options.managedSession.sessionName) {
|
|
721
|
+
reason = "launch-session-not-current";
|
|
722
|
+
} else if (managedUrl) {
|
|
723
|
+
reason = "managed-session-target-not-in-launch-status";
|
|
724
|
+
}
|
|
725
|
+
if (!reason) return undefined;
|
|
726
|
+
|
|
727
|
+
const managedDescription = options.managedSession.url ?? options.managedSession.title ?? options.managedSession.sessionName;
|
|
728
|
+
const liveDescription = electronTargetLabel(liveTarget);
|
|
729
|
+
const summary = reason === "launch-session-not-current"
|
|
730
|
+
? `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}.`
|
|
731
|
+
: `Electron session mismatch: managed session ${options.managedSession.sessionName} is on ${managedDescription}, but launch ${options.record.launchId} still has live target ${liveDescription}.`;
|
|
732
|
+
const nextActions = buildElectronMismatchNextActions(options.record, liveTarget);
|
|
733
|
+
return {
|
|
734
|
+
launchId: options.record.launchId,
|
|
735
|
+
liveTarget,
|
|
736
|
+
managedSession: options.managedSession,
|
|
737
|
+
nextActionIds: nextActions.map((action) => action.id),
|
|
738
|
+
reason,
|
|
739
|
+
sessionName: options.record.sessionName,
|
|
740
|
+
statusTargets: options.statusTargets,
|
|
741
|
+
summary,
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export function formatElectronSessionMismatchText(mismatch: ElectronSessionMismatch): string {
|
|
746
|
+
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.`;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export function shouldInspectElectronPostCommandHealth(command: string | undefined): boolean {
|
|
750
|
+
return command !== undefined && ELECTRON_POST_COMMAND_HEALTH_COMMANDS.has(command);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export function buildElectronLifecycleNextActions(record: ElectronLaunchRecord): AgentBrowserNextAction[] {
|
|
754
|
+
return buildAgentBrowserNextActions({
|
|
755
|
+
electron: { launchId: record.launchId, sessionName: record.sessionName, status: record.cleanupState },
|
|
756
|
+
resultCategory: "success",
|
|
757
|
+
successCategory: "completed",
|
|
758
|
+
}) ?? [];
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
export function buildElectronPostCommandHealthDiagnostic(options: {
|
|
762
|
+
command?: string;
|
|
763
|
+
record: ElectronLaunchRecord;
|
|
764
|
+
status: ElectronLaunchStatus;
|
|
765
|
+
target?: SessionTabTarget;
|
|
766
|
+
}): ElectronPostCommandHealthDiagnostic | undefined {
|
|
767
|
+
let reason: ElectronPostCommandHealthReason | undefined;
|
|
768
|
+
if (options.status.pidAlive === false) reason = "process-dead";
|
|
769
|
+
else if (!options.status.portAlive) reason = "debug-port-dead";
|
|
770
|
+
else if (isAboutBlankUrl(options.target?.url) && getLiveElectronRendererTargets(options.status.targets).length === 0) reason = "about-blank-no-live-target";
|
|
771
|
+
if (!reason) return undefined;
|
|
772
|
+
const nextActions = buildElectronLifecycleNextActions(options.record);
|
|
773
|
+
const commandText = options.command ? `${options.command} command` : "command";
|
|
774
|
+
const statusText = `${options.status.portAlive ? "debug port alive" : "debug port dead"}${options.status.pidAlive === undefined ? "" : options.status.pidAlive ? ", pid alive" : ", pid dead"}`;
|
|
775
|
+
const summary = `Electron lifecycle warning: ${commandText} completed, but launch ${options.record.launchId} is no longer healthy (${statusText}).`;
|
|
776
|
+
return {
|
|
777
|
+
appName: options.record.appName,
|
|
778
|
+
command: options.command,
|
|
779
|
+
launchId: options.record.launchId,
|
|
780
|
+
nextActionIds: nextActions.map((action) => action.id),
|
|
781
|
+
reason,
|
|
782
|
+
sessionName: options.record.sessionName,
|
|
783
|
+
status: options.status,
|
|
784
|
+
summary,
|
|
785
|
+
target: options.target,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
export function formatElectronPostCommandHealthText(diagnostic: ElectronPostCommandHealthDiagnostic | undefined): string | undefined {
|
|
790
|
+
if (!diagnostic) return undefined;
|
|
791
|
+
const lines = [diagnostic.summary];
|
|
792
|
+
if (diagnostic.target?.url) lines.push(`Current browser session target: ${diagnostic.target.url}.`);
|
|
793
|
+
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).`);
|
|
794
|
+
lines.push(`Next: run electron.status/electron.probe with launchId ${diagnostic.launchId}, cleanup the wrapper-owned launch if dead, or relaunch the app.`);
|
|
795
|
+
return lines.join("\n");
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
export function buildElectronIdentifiers(record: ElectronLaunchRecord): { appName: string; launchId: string; sessionName?: string } {
|
|
799
|
+
return { appName: record.appName, launchId: record.launchId, sessionName: record.sessionName };
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export function buildElectronRefFreshnessNextActions(sessionName: string | undefined): AgentBrowserNextAction[] {
|
|
803
|
+
return [{
|
|
804
|
+
id: "refresh-electron-refs-after-rerender",
|
|
805
|
+
params: { args: sessionName ? ["--session", sessionName, "snapshot", "-i"] : ["snapshot", "-i"] },
|
|
806
|
+
reason: "Electron UIs often rerender without changing URL; refresh refs before using old @e handles again.",
|
|
807
|
+
safety: "Read-only snapshot; avoids stale same-URL refs after quick-pick, modal, theme, or editor rerenders.",
|
|
808
|
+
tool: "agent_browser",
|
|
809
|
+
}];
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
export function buildElectronRefFreshnessDiagnostic(options: {
|
|
813
|
+
command?: string;
|
|
814
|
+
commandTokens: string[];
|
|
815
|
+
record?: ElectronLaunchRecord;
|
|
816
|
+
sessionName?: string;
|
|
817
|
+
stdin?: string;
|
|
818
|
+
}): ElectronRefFreshnessDiagnostic | undefined {
|
|
819
|
+
if (!options.record || !shouldInspectElectronPostCommandHealth(options.command)) return undefined;
|
|
820
|
+
if (getGuardedRefUsage(options.commandTokens, options.stdin).length === 0) return undefined;
|
|
821
|
+
const nextActions = buildElectronRefFreshnessNextActions(options.sessionName);
|
|
822
|
+
return {
|
|
823
|
+
command: options.command,
|
|
824
|
+
launchId: options.record.launchId,
|
|
825
|
+
nextActionIds: nextActions.map((action) => action.id),
|
|
826
|
+
sessionName: options.sessionName,
|
|
827
|
+
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.`,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
export function formatElectronRefFreshnessText(diagnostic: ElectronRefFreshnessDiagnostic | undefined): string | undefined {
|
|
832
|
+
return diagnostic?.summary;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export async function closeManagedSession(options: { cwd: string; sessionName: string; timeoutMs: number }): Promise<string | undefined> {
|
|
836
|
+
const controller = new AbortController();
|
|
837
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
838
|
+
let stdoutSpillPath: string | undefined;
|
|
839
|
+
const closeArgs = ["--session", options.sessionName, "close"];
|
|
840
|
+
try {
|
|
841
|
+
const processResult = await runAgentBrowserProcess({
|
|
842
|
+
args: closeArgs,
|
|
843
|
+
cwd: options.cwd,
|
|
844
|
+
signal: controller.signal,
|
|
845
|
+
});
|
|
846
|
+
stdoutSpillPath = processResult.stdoutSpillPath;
|
|
847
|
+
return getAgentBrowserErrorText({
|
|
848
|
+
aborted: processResult.aborted,
|
|
849
|
+
command: "close",
|
|
850
|
+
effectiveArgs: redactInvocationArgs(closeArgs),
|
|
851
|
+
exitCode: processResult.exitCode,
|
|
852
|
+
plainTextInspection: false,
|
|
853
|
+
spawnError: processResult.spawnError,
|
|
854
|
+
stderr: processResult.stderr,
|
|
855
|
+
timedOut: processResult.timedOut,
|
|
856
|
+
timeoutMs: processResult.timeoutMs,
|
|
857
|
+
});
|
|
858
|
+
} catch (error) {
|
|
859
|
+
return error instanceof Error ? error.message : String(error);
|
|
860
|
+
} finally {
|
|
861
|
+
clearTimeout(timer);
|
|
862
|
+
if (stdoutSpillPath) {
|
|
863
|
+
await rm(stdoutSpillPath, { force: true }).catch(() => undefined);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
export { extractBatchResultCommand };
|