pi-agent-browser-native 0.2.47 → 0.2.49
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 +63 -19
- package/README.md +52 -19
- package/dist/extensions/agent-browser/index.js +785 -0
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +71 -0
- package/dist/extensions/agent-browser/lib/argv-grammar.js +121 -0
- package/dist/extensions/agent-browser/lib/bash-guard.js +190 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +85 -0
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +302 -0
- package/dist/extensions/agent-browser/lib/config-policy.js +686 -0
- package/dist/extensions/agent-browser/lib/config.js +122 -0
- package/dist/extensions/agent-browser/lib/electron/cdp.js +51 -0
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +212 -0
- package/dist/extensions/agent-browser/lib/electron/discovery.js +633 -0
- package/dist/extensions/agent-browser/lib/electron/launch.js +351 -0
- package/{extensions/agent-browser/lib/electron/text.ts → dist/extensions/agent-browser/lib/electron/text.js} +5 -5
- package/dist/extensions/agent-browser/lib/executable-path.js +20 -0
- package/dist/extensions/agent-browser/lib/fs-utils.js +18 -0
- package/dist/extensions/agent-browser/lib/input-modes/electron.js +165 -0
- package/dist/extensions/agent-browser/lib/input-modes/job.js +519 -0
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +440 -0
- package/dist/extensions/agent-browser/lib/input-modes/params.js +164 -0
- package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +119 -0
- package/dist/extensions/agent-browser/lib/input-modes/shared.js +42 -0
- package/dist/extensions/agent-browser/lib/input-modes/types.js +21 -0
- package/dist/extensions/agent-browser/lib/input-modes.js +10 -0
- package/dist/extensions/agent-browser/lib/json-schema.js +58 -0
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +59 -0
- package/dist/extensions/agent-browser/lib/navigation-policy.js +83 -0
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +62 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +39 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +276 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +909 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +443 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +47 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +141 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +108 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +112 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +158 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +54 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +762 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +491 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.js +40 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +5 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +731 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/types.js +1 -0
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +718 -0
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +247 -0
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +68 -0
- package/{extensions/agent-browser/lib/parsing.ts → dist/extensions/agent-browser/lib/parsing.js} +12 -11
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +241 -0
- package/dist/extensions/agent-browser/lib/playbook.js +121 -0
- package/dist/extensions/agent-browser/lib/process.js +448 -0
- package/dist/extensions/agent-browser/lib/prompt-policy.js +91 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +220 -0
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +111 -0
- package/{extensions/agent-browser/lib/results/artifact-state.ts → dist/extensions/agent-browser/lib/results/artifact-state.js} +4 -8
- package/dist/extensions/agent-browser/lib/results/categories.js +76 -0
- package/dist/extensions/agent-browser/lib/results/confirmation.js +63 -0
- package/dist/extensions/agent-browser/lib/results/contracts.js +8 -0
- package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +74 -0
- package/dist/extensions/agent-browser/lib/results/envelope.js +166 -0
- package/dist/extensions/agent-browser/lib/results/network-routes.js +92 -0
- package/dist/extensions/agent-browser/lib/results/network.js +73 -0
- package/dist/extensions/agent-browser/lib/results/next-actions.js +72 -0
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +515 -0
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +397 -0
- package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +55 -0
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +46 -0
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +24 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +960 -0
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +205 -0
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +134 -0
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +159 -0
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +216 -0
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +104 -0
- package/dist/extensions/agent-browser/lib/results/presentation/skills.js +152 -0
- package/dist/extensions/agent-browser/lib/results/presentation.js +177 -0
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +107 -0
- package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +50 -0
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +225 -0
- package/{extensions/agent-browser/lib/results/shared.ts → dist/extensions/agent-browser/lib/results/shared.js} +0 -1
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +208 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +78 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +331 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +40 -0
- package/dist/extensions/agent-browser/lib/results/snapshot.js +264 -0
- package/dist/extensions/agent-browser/lib/results/text.js +40 -0
- package/{extensions/agent-browser/lib/results.ts → dist/extensions/agent-browser/lib/results.js} +2 -32
- package/dist/extensions/agent-browser/lib/runtime.js +816 -0
- package/dist/extensions/agent-browser/lib/session-page-state.js +411 -0
- package/dist/extensions/agent-browser/lib/string-enum-schema.js +13 -0
- package/dist/extensions/agent-browser/lib/temp.js +498 -0
- package/dist/extensions/agent-browser/lib/web-search.js +562 -0
- package/docs/ARCHITECTURE.md +10 -10
- package/docs/COMMAND_REFERENCE.md +35 -21
- package/docs/ELECTRON.md +3 -3
- package/docs/RELEASE.md +46 -26
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/SUPPORT_MATRIX.md +35 -106
- package/docs/TOOL_CONTRACT.md +23 -21
- package/package.json +12 -8
- package/scripts/agent-browser-capability-baseline.mjs +6 -3
- package/scripts/config.mjs +8 -2
- package/scripts/doctor.mjs +19 -17
- package/scripts/platform-smoke.mjs +1 -1
- package/extensions/agent-browser/index.ts +0 -952
- package/extensions/agent-browser/lib/argv-descriptor.ts +0 -90
- package/extensions/agent-browser/lib/argv-grammar.ts +0 -128
- package/extensions/agent-browser/lib/bash-guard.ts +0 -205
- package/extensions/agent-browser/lib/command-policy.ts +0 -71
- package/extensions/agent-browser/lib/command-taxonomy.ts +0 -336
- package/extensions/agent-browser/lib/config-policy.js +0 -690
- package/extensions/agent-browser/lib/config.ts +0 -209
- package/extensions/agent-browser/lib/electron/cdp.ts +0 -69
- package/extensions/agent-browser/lib/electron/cleanup.ts +0 -235
- package/extensions/agent-browser/lib/electron/discovery.ts +0 -710
- package/extensions/agent-browser/lib/electron/launch.ts +0 -499
- package/extensions/agent-browser/lib/executable-path.ts +0 -19
- package/extensions/agent-browser/lib/fs-utils.ts +0 -18
- package/extensions/agent-browser/lib/input-modes/electron.ts +0 -170
- package/extensions/agent-browser/lib/input-modes/job.ts +0 -451
- package/extensions/agent-browser/lib/input-modes/lookups.ts +0 -447
- package/extensions/agent-browser/lib/input-modes/params.ts +0 -205
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +0 -127
- package/extensions/agent-browser/lib/input-modes/shared.ts +0 -46
- package/extensions/agent-browser/lib/input-modes/types.ts +0 -225
- package/extensions/agent-browser/lib/input-modes.ts +0 -45
- package/extensions/agent-browser/lib/json-schema.ts +0 -73
- package/extensions/agent-browser/lib/launch-scoped-flags.ts +0 -67
- package/extensions/agent-browser/lib/navigation-policy.ts +0 -95
- package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +0 -65
- package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +0 -257
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +0 -912
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +0 -512
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +0 -53
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +0 -1481
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +0 -564
- package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +0 -47
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +0 -868
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +0 -564
- package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +0 -855
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +0 -375
- package/extensions/agent-browser/lib/orchestration/output-file.ts +0 -86
- package/extensions/agent-browser/lib/pi-tool-rendering.ts +0 -252
- package/extensions/agent-browser/lib/playbook.ts +0 -142
- package/extensions/agent-browser/lib/process.ts +0 -516
- package/extensions/agent-browser/lib/prompt-policy.ts +0 -105
- package/extensions/agent-browser/lib/results/action-recommendations.ts +0 -264
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +0 -111
- package/extensions/agent-browser/lib/results/categories.ts +0 -106
- package/extensions/agent-browser/lib/results/confirmation.ts +0 -76
- package/extensions/agent-browser/lib/results/contracts.ts +0 -241
- package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +0 -72
- package/extensions/agent-browser/lib/results/envelope.ts +0 -195
- package/extensions/agent-browser/lib/results/network-routes.ts +0 -83
- package/extensions/agent-browser/lib/results/network.ts +0 -78
- package/extensions/agent-browser/lib/results/next-actions.ts +0 -117
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +0 -588
- package/extensions/agent-browser/lib/results/presentation/batch.ts +0 -450
- package/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.ts +0 -67
- package/extensions/agent-browser/lib/results/presentation/common.ts +0 -53
- package/extensions/agent-browser/lib/results/presentation/content.ts +0 -36
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +0 -923
- package/extensions/agent-browser/lib/results/presentation/errors.ts +0 -227
- package/extensions/agent-browser/lib/results/presentation/large-output.ts +0 -182
- package/extensions/agent-browser/lib/results/presentation/navigation.ts +0 -184
- package/extensions/agent-browser/lib/results/presentation/registry.ts +0 -242
- package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +0 -131
- package/extensions/agent-browser/lib/results/presentation/skills.ts +0 -143
- package/extensions/agent-browser/lib/results/presentation.ts +0 -257
- package/extensions/agent-browser/lib/results/recovery-actions.ts +0 -139
- package/extensions/agent-browser/lib/results/recovery-next-actions.ts +0 -71
- package/extensions/agent-browser/lib/results/selector-recovery.ts +0 -320
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +0 -273
- package/extensions/agent-browser/lib/results/snapshot-refs.ts +0 -100
- package/extensions/agent-browser/lib/results/snapshot-segments.ts +0 -366
- package/extensions/agent-browser/lib/results/snapshot-spill.ts +0 -63
- package/extensions/agent-browser/lib/results/snapshot.ts +0 -329
- package/extensions/agent-browser/lib/results/text.ts +0 -40
- package/extensions/agent-browser/lib/runtime.ts +0 -988
- package/extensions/agent-browser/lib/session-page-state.ts +0 -512
- package/extensions/agent-browser/lib/string-enum-schema.ts +0 -20
- package/extensions/agent-browser/lib/temp.ts +0 -577
- package/extensions/agent-browser/lib/web-search.ts +0 -721
- /package/{extensions/agent-browser/lib/orchestration/browser-run.ts → dist/extensions/agent-browser/lib/orchestration/browser-run.js} +0 -0
|
@@ -1,868 +0,0 @@
|
|
|
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 { buildNextToolAction, withOptionalSessionArgs } from "../../results/next-actions.js";
|
|
8
|
-
import {
|
|
9
|
-
extractRefSnapshotFromData,
|
|
10
|
-
isAboutBlankUrl,
|
|
11
|
-
normalizeComparableUrl,
|
|
12
|
-
normalizeSessionTabTarget,
|
|
13
|
-
targetsMatch,
|
|
14
|
-
type SessionRefSnapshot,
|
|
15
|
-
type SessionRefSnapshotInvalidation,
|
|
16
|
-
type SessionTabTarget,
|
|
17
|
-
} from "../../session-page-state.js";
|
|
18
|
-
import {
|
|
19
|
-
isCloseCommand,
|
|
20
|
-
isElectronPostCommandHealthCommand,
|
|
21
|
-
isNavigationObservableCommandName,
|
|
22
|
-
isRefGuardedCommand,
|
|
23
|
-
isRefInvalidatingBatchCommand,
|
|
24
|
-
isSessionTabPinningExcludedCommand,
|
|
25
|
-
isSessionTabPostCommandCorrectionExcludedCommand,
|
|
26
|
-
} from "../../command-taxonomy.js";
|
|
27
|
-
import { chooseOpenResultTabCorrection, redactInvocationArgs, type OpenResultTabCorrection } from "../../runtime.js";
|
|
28
|
-
import { isRecord } from "../../parsing.js";
|
|
29
|
-
import { parseUserBatchStdin } from "../batch-stdin.js";
|
|
30
|
-
import type {
|
|
31
|
-
AboutBlankSessionMismatch,
|
|
32
|
-
BatchCommandStep,
|
|
33
|
-
BrowserRunState,
|
|
34
|
-
BrowserRunStatePatch,
|
|
35
|
-
ElectronManagedSessionTarget,
|
|
36
|
-
ElectronPostCommandHealthDiagnostic,
|
|
37
|
-
ElectronPostCommandHealthReason,
|
|
38
|
-
ElectronRefFreshnessDiagnostic,
|
|
39
|
-
ElectronSessionMismatch,
|
|
40
|
-
ElectronSessionMismatchReason,
|
|
41
|
-
ManagedSessionOutcome,
|
|
42
|
-
NavigationSummary,
|
|
43
|
-
PinnedBatchPlan,
|
|
44
|
-
PinnedBatchUnwrapMode,
|
|
45
|
-
StaleRefPreflight,
|
|
46
|
-
TraceOwner,
|
|
47
|
-
} from "./types.js";
|
|
48
|
-
|
|
49
|
-
export const NAVIGATION_SUMMARY_EVAL = `({ title: document.title, url: location.href })`;
|
|
50
|
-
|
|
51
|
-
export function applyBrowserRunStatePatch(state: BrowserRunState, patch: BrowserRunStatePatch | undefined): void {
|
|
52
|
-
if (!patch) return;
|
|
53
|
-
if (patch.allowedDomainsBySession) state.allowedDomainsBySession = patch.allowedDomainsBySession;
|
|
54
|
-
if ("artifactManifest" in patch) state.artifactManifest = patch.artifactManifest;
|
|
55
|
-
if (patch.freshSessionOrdinal !== undefined) state.freshSessionOrdinal = patch.freshSessionOrdinal;
|
|
56
|
-
if (patch.managedSessionActive !== undefined) state.managedSessionActive = patch.managedSessionActive;
|
|
57
|
-
if (patch.managedSessionCwd !== undefined) state.managedSessionCwd = patch.managedSessionCwd;
|
|
58
|
-
if (patch.managedSessionName !== undefined) state.managedSessionName = patch.managedSessionName;
|
|
59
|
-
if (patch.networkRoutesBySession) state.networkRoutesBySession = patch.networkRoutesBySession;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function buildSessionDetailFields(sessionName: string | undefined, usedImplicitSession: boolean): Record<string, unknown> {
|
|
63
|
-
return sessionName ? { sessionName, usedImplicitSession } : {};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function buildManagedSessionOutcome(options: {
|
|
67
|
-
activeAfter: boolean;
|
|
68
|
-
activeBefore: boolean;
|
|
69
|
-
attemptedSessionName?: string;
|
|
70
|
-
command?: string;
|
|
71
|
-
currentSessionName: string;
|
|
72
|
-
previousSessionName: string;
|
|
73
|
-
replacedSessionName?: string;
|
|
74
|
-
sessionMode: "auto" | "fresh";
|
|
75
|
-
succeeded: boolean;
|
|
76
|
-
}): ManagedSessionOutcome | undefined {
|
|
77
|
-
const { activeAfter, activeBefore, attemptedSessionName, command, currentSessionName, previousSessionName, replacedSessionName, sessionMode, succeeded } = options;
|
|
78
|
-
if (!attemptedSessionName) return undefined;
|
|
79
|
-
let status: ManagedSessionOutcome["status"];
|
|
80
|
-
let summary: string;
|
|
81
|
-
if (isCloseCommand(command)) {
|
|
82
|
-
status = succeeded ? "closed" : activeBefore ? "preserved" : "abandoned";
|
|
83
|
-
summary = succeeded
|
|
84
|
-
? `Managed session ${attemptedSessionName} was closed.`
|
|
85
|
-
: activeBefore
|
|
86
|
-
? `Managed session close failed; previous managed session ${previousSessionName} remains current.`
|
|
87
|
-
: `Managed session close failed; no managed session is active.`;
|
|
88
|
-
} else if (succeeded) {
|
|
89
|
-
if (replacedSessionName) {
|
|
90
|
-
status = "replaced";
|
|
91
|
-
summary = `Managed session ${replacedSessionName} was replaced by ${currentSessionName}.`;
|
|
92
|
-
} else if (!activeBefore && activeAfter) {
|
|
93
|
-
status = "created";
|
|
94
|
-
summary = `Managed session ${currentSessionName} is now current.`;
|
|
95
|
-
} else {
|
|
96
|
-
status = "unchanged";
|
|
97
|
-
summary = `Managed session ${currentSessionName} remains current.`;
|
|
98
|
-
}
|
|
99
|
-
} else if (activeBefore) {
|
|
100
|
-
status = "preserved";
|
|
101
|
-
summary = sessionMode === "fresh" && attemptedSessionName !== previousSessionName
|
|
102
|
-
? `Fresh managed session ${attemptedSessionName} failed before becoming current; previous managed session ${previousSessionName} was preserved.`
|
|
103
|
-
: `Managed session call failed; previous managed session ${previousSessionName} was preserved.`;
|
|
104
|
-
} else {
|
|
105
|
-
status = "abandoned";
|
|
106
|
-
summary = sessionMode === "fresh"
|
|
107
|
-
? `Fresh managed session ${attemptedSessionName} failed before becoming current; no previous managed session was active, so no managed session is current.`
|
|
108
|
-
: `Managed session call failed before any managed session became current.`;
|
|
109
|
-
}
|
|
110
|
-
return {
|
|
111
|
-
activeAfter,
|
|
112
|
-
activeBefore,
|
|
113
|
-
attemptedSessionName,
|
|
114
|
-
currentSessionName,
|
|
115
|
-
previousSessionName,
|
|
116
|
-
replacedSessionName,
|
|
117
|
-
sessionMode,
|
|
118
|
-
status,
|
|
119
|
-
succeeded,
|
|
120
|
-
summary,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function isFreshPostLaunchFailure(outcome: ManagedSessionOutcome): boolean {
|
|
125
|
-
return !outcome.succeeded && outcome.sessionMode === "fresh" && outcome.activeAfter && !!outcome.currentSessionName && (outcome.status === "created" || outcome.status === "replaced" || outcome.status === "unchanged");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function formatManagedSessionOutcomeHeadline(outcome: ManagedSessionOutcome): string {
|
|
129
|
-
if (outcome.status === "preserved") {
|
|
130
|
-
return "Managed session outcome: Fresh launch failed; your previous browser session is still active.";
|
|
131
|
-
}
|
|
132
|
-
if (outcome.status === "abandoned") {
|
|
133
|
-
return "Managed session outcome: Fresh launch failed; no managed browser session is current.";
|
|
134
|
-
}
|
|
135
|
-
if (isFreshPostLaunchFailure(outcome)) {
|
|
136
|
-
return "Managed session outcome: Fresh launch became current, but this tool call failed after launch.";
|
|
137
|
-
}
|
|
138
|
-
return `Managed session outcome: ${outcome.summary}`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function formatManagedSessionOutcomeRecoveryGuidance(outcome: ManagedSessionOutcome): string {
|
|
142
|
-
const lines = ["Recovery:"];
|
|
143
|
-
if (outcome.status === "preserved") {
|
|
144
|
-
lines.push('- Continue with sessionMode "auto" on the current session, or retry the intended launch with sessionMode "fresh".');
|
|
145
|
-
lines.push("- Run doctor to verify agent-browser install and environment when failures persist.");
|
|
146
|
-
} else if (outcome.status === "abandoned") {
|
|
147
|
-
lines.push('- Retry with sessionMode "fresh" (for example args: ["open", "<url>"]) after verifying agent-browser is on PATH.');
|
|
148
|
-
lines.push("- Run doctor when install or environment issues are suspected.");
|
|
149
|
-
} else if (isFreshPostLaunchFailure(outcome)) {
|
|
150
|
-
lines.push('- Continue with sessionMode "auto" on the current session, or inspect failureCategory / qaPreset to fix the post-launch failure.');
|
|
151
|
-
lines.push("- Run doctor only if later browser commands also fail.");
|
|
152
|
-
} else {
|
|
153
|
-
lines.push('- Retry with sessionMode "fresh" when launch-scoped flags must apply, or run doctor to verify the environment.');
|
|
154
|
-
}
|
|
155
|
-
lines.push("- Full session names and transition details remain in details.managedSessionOutcome.");
|
|
156
|
-
return lines.join("\n");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function formatManagedSessionOutcomeText(outcome: ManagedSessionOutcome | undefined): string | undefined {
|
|
160
|
-
if (!outcome) return undefined;
|
|
161
|
-
if (outcome.status === "closed" && outcome.succeeded) {
|
|
162
|
-
return [
|
|
163
|
-
"Managed session outcome: The current wrapper-managed browser session was closed.",
|
|
164
|
-
"Next sessionMode auto call will start or attach a managed session as needed. If upstream session list still shows rows, they are separate saved/upstream sessions; use close --all only when full cleanup is intended.",
|
|
165
|
-
"Full session names and transition details remain in details.managedSessionOutcome.",
|
|
166
|
-
].join("\n");
|
|
167
|
-
}
|
|
168
|
-
if (outcome.succeeded || outcome.sessionMode !== "fresh") return undefined;
|
|
169
|
-
return [formatManagedSessionOutcomeHeadline(outcome), formatManagedSessionOutcomeRecoveryGuidance(outcome)].join("\n");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export function buildManagedSessionFreshFailureNextActions(outcome: ManagedSessionOutcome | undefined): AgentBrowserNextAction[] {
|
|
173
|
-
if (!outcome || outcome.succeeded || outcome.sessionMode !== "fresh") return [];
|
|
174
|
-
const actions: AgentBrowserNextAction[] = [];
|
|
175
|
-
if (!isFreshPostLaunchFailure(outcome)) {
|
|
176
|
-
actions.push(buildNextToolAction({
|
|
177
|
-
args: ["doctor"],
|
|
178
|
-
id: "run-agent-browser-doctor",
|
|
179
|
-
reason: "Verify agent-browser install, PATH, and environment after a failed fresh launch.",
|
|
180
|
-
safety: "Read-only local diagnostics; does not mutate browser state.",
|
|
181
|
-
}));
|
|
182
|
-
}
|
|
183
|
-
if ((outcome.status === "preserved" || isFreshPostLaunchFailure(outcome)) && outcome.activeAfter && outcome.currentSessionName) {
|
|
184
|
-
const sessionLabel = isFreshPostLaunchFailure(outcome) ? "current managed session" : "preserved managed session";
|
|
185
|
-
actions.push(
|
|
186
|
-
buildNextToolAction({
|
|
187
|
-
args: withOptionalSessionArgs(outcome.currentSessionName, ["get", "url"]),
|
|
188
|
-
id: "verify-current-managed-session",
|
|
189
|
-
reason: `Confirm the ${sessionLabel} before continuing with sessionMode auto.`,
|
|
190
|
-
safety: `Read-only URL check on the ${sessionLabel}.`,
|
|
191
|
-
}),
|
|
192
|
-
buildNextToolAction({
|
|
193
|
-
args: withOptionalSessionArgs(outcome.currentSessionName, ["snapshot", "-i"]),
|
|
194
|
-
id: "snapshot-current-managed-session",
|
|
195
|
-
reason: `Refresh interactive refs on the ${sessionLabel} before retrying the workflow.`,
|
|
196
|
-
safety: "Read-only snapshot; no navigation.",
|
|
197
|
-
}),
|
|
198
|
-
);
|
|
199
|
-
} else {
|
|
200
|
-
actions.push(
|
|
201
|
-
buildNextToolAction({
|
|
202
|
-
args: ["open", "about:blank"],
|
|
203
|
-
id: "retry-fresh-managed-session",
|
|
204
|
-
reason: "Start a new managed browser session after the failed fresh launch.",
|
|
205
|
-
safety: "Replace about:blank with the intended URL from your workflow.",
|
|
206
|
-
sessionMode: "fresh",
|
|
207
|
-
}),
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
return actions;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function getTraceOwner(command: string | undefined): TraceOwner | undefined {
|
|
214
|
-
return command === "trace" || command === "profiler" ? command : undefined;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export function getTraceOwnerGuardMessage(options: {
|
|
218
|
-
command: string | undefined;
|
|
219
|
-
sessionName: string | undefined;
|
|
220
|
-
subcommand: string | undefined;
|
|
221
|
-
traceOwners: Map<string, TraceOwner>;
|
|
222
|
-
}): string | undefined {
|
|
223
|
-
const owner = getTraceOwner(options.command);
|
|
224
|
-
if (!owner || !options.sessionName || (options.subcommand !== "start" && options.subcommand !== "stop")) {
|
|
225
|
-
return undefined;
|
|
226
|
-
}
|
|
227
|
-
const activeOwner = options.traceOwners.get(options.sessionName);
|
|
228
|
-
if (!activeOwner || activeOwner === owner) {
|
|
229
|
-
return undefined;
|
|
230
|
-
}
|
|
231
|
-
return options.subcommand === "start"
|
|
232
|
-
? `Wrapper believes ${activeOwner} tracing is active for session ${options.sessionName}; stop ${activeOwner} before starting ${owner}.`
|
|
233
|
-
: `Wrapper believes tracing for session ${options.sessionName} is owned by ${activeOwner}; run ${activeOwner} stop instead of ${owner} stop.`;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export function updateTraceOwnerState(options: {
|
|
237
|
-
command: string | undefined;
|
|
238
|
-
sessionName: string | undefined;
|
|
239
|
-
subcommand: string | undefined;
|
|
240
|
-
succeeded: boolean;
|
|
241
|
-
traceOwners: Map<string, TraceOwner>;
|
|
242
|
-
}): void {
|
|
243
|
-
const owner = getTraceOwner(options.command);
|
|
244
|
-
if (!owner || !options.sessionName || !options.succeeded) {
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
if (options.subcommand === "start") {
|
|
248
|
-
options.traceOwners.set(options.sessionName, owner);
|
|
249
|
-
}
|
|
250
|
-
if (options.subcommand === "stop" && options.traceOwners.get(options.sessionName) === owner) {
|
|
251
|
-
options.traceOwners.delete(options.sessionName);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export function extractStringResultField(data: unknown, fieldName: "result" | "title" | "url" | "value"): string | undefined {
|
|
256
|
-
if (typeof data === "string") {
|
|
257
|
-
if (fieldName === "value") return data;
|
|
258
|
-
const text = data.trim();
|
|
259
|
-
return text.length > 0 ? text : undefined;
|
|
260
|
-
}
|
|
261
|
-
if (!isRecord(data) || typeof data[fieldName] !== "string") {
|
|
262
|
-
return undefined;
|
|
263
|
-
}
|
|
264
|
-
if (fieldName === "value") return data[fieldName];
|
|
265
|
-
const text = data[fieldName].trim();
|
|
266
|
-
return text.length > 0 ? text : undefined;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export function extractNavigationSummaryFromData(data: unknown): NavigationSummary | undefined {
|
|
270
|
-
const result = isRecord(data) && isRecord(data.result) ? data.result : data;
|
|
271
|
-
const title = extractStringResultField(result, "title");
|
|
272
|
-
const url = extractStringResultField(result, "url");
|
|
273
|
-
return title || url ? { title, url } : undefined;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
export function shouldCaptureNavigationSummary(command: string | undefined, data: unknown): boolean {
|
|
277
|
-
return (
|
|
278
|
-
isNavigationObservableCommandName(command) &&
|
|
279
|
-
(!isRecord(data) || (typeof data.title !== "string" && typeof data.url !== "string"))
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export function mergeNavigationSummaryIntoData(data: unknown, navigationSummary: NavigationSummary): unknown {
|
|
284
|
-
if (isRecord(data)) {
|
|
285
|
-
return { ...data, navigationSummary };
|
|
286
|
-
}
|
|
287
|
-
return { navigationSummary, result: data };
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export function buildAboutBlankRecoveryHint(): string {
|
|
291
|
-
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");
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export function buildAboutBlankWarning(mismatch: AboutBlankSessionMismatch): string {
|
|
295
|
-
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."}`;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function extractBatchResultCommand(item: Record<string, unknown>): string[] {
|
|
299
|
-
return Array.isArray(item.command) ? item.command.filter((token): token is string => typeof token === "string") : [];
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export function getStaleRefArgs(commandTokens: string[], stdin?: string): string[] {
|
|
303
|
-
if (commandTokens[0] !== "batch" || stdin === undefined) {
|
|
304
|
-
return commandTokens;
|
|
305
|
-
}
|
|
306
|
-
const parsed = parseUserBatchStdin(stdin);
|
|
307
|
-
if (parsed.error || parsed.steps === undefined) {
|
|
308
|
-
return commandTokens;
|
|
309
|
-
}
|
|
310
|
-
return parsed.steps.flatMap((step) => step);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function collectRefsFromTokens(tokens: string[]): string[] {
|
|
314
|
-
return tokens.filter((token) => /^@e\d+\b/.test(token)).map((token) => token.slice(1));
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export function getGuardedRefUsage(commandTokens: string[], stdin?: string, options: { includeRefsAfterBatchSnapshot?: boolean } = {}): string[] {
|
|
318
|
-
const collectFromStep = (step: string[]) => isRefGuardedCommand(step[0]) ? collectRefsFromTokens(step) : [];
|
|
319
|
-
if (commandTokens[0] !== "batch" || stdin === undefined) {
|
|
320
|
-
return collectFromStep(commandTokens);
|
|
321
|
-
}
|
|
322
|
-
const parsed = parseUserBatchStdin(stdin);
|
|
323
|
-
if (parsed.error || parsed.steps === undefined) {
|
|
324
|
-
return collectFromStep(commandTokens);
|
|
325
|
-
}
|
|
326
|
-
const refsBeforeInBatchSnapshot: string[] = [];
|
|
327
|
-
for (const step of parsed.steps) {
|
|
328
|
-
if (!options.includeRefsAfterBatchSnapshot && (step[0] ?? "") === "snapshot") break;
|
|
329
|
-
refsBeforeInBatchSnapshot.push(...collectFromStep(step));
|
|
330
|
-
}
|
|
331
|
-
return refsBeforeInBatchSnapshot;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function getSnapshotRefRole(refSnapshot: SessionRefSnapshot | undefined, refId: string): string | undefined {
|
|
335
|
-
return refSnapshot?.refs?.[refId]?.role?.toLowerCase();
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function isSafeSameSnapshotFormBatchStep(step: string[], refSnapshot: SessionRefSnapshot | undefined): boolean {
|
|
339
|
-
const command = step[0];
|
|
340
|
-
const refIds = collectRefsFromTokens(step);
|
|
341
|
-
if (refIds.length === 0 || !refSnapshot) return false;
|
|
342
|
-
const roles = refIds.map((refId) => getSnapshotRefRole(refSnapshot, refId));
|
|
343
|
-
if (roles.some((role) => role === undefined)) return false;
|
|
344
|
-
if (command === "check" || command === "uncheck") return roles.every((role) => role === "checkbox" || role === "radio");
|
|
345
|
-
if (command === "click" || command === "tap") return roles.every((role) => role === "checkbox" || role === "radio");
|
|
346
|
-
if (command === "select") return roles.every((role) => role === "combobox");
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function getBatchRefInvalidationMessage(commandTokens: string[], stdin?: string, refSnapshot?: SessionRefSnapshot): 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 && isRefGuardedCommand(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 (isRefInvalidatingBatchCommand(step[0]) && !isSafeSameSnapshotFormBatchStep(step, refSnapshot)) {
|
|
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, options.refSnapshot);
|
|
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
|
-
!isSessionTabPinningExcludedCommand(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 = isNavigationObservableCommandName(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
|
-
!isSessionTabPostCommandCorrectionExcludedCommand(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 isElectronPostCommandHealthCommand(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 };
|