pi-agent-browser-native 0.6.6 → 0.6.8
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 +40 -1
- package/README.md +21 -6
- package/dist/extensions/agent-browser/index.js +138 -75
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +6 -5
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +10 -1
- package/dist/extensions/agent-browser/lib/input-modes/params.js +20 -7
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +0 -1
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +13 -12
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +6 -25
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +2 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +10 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +3 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +104 -117
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +64 -36
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +100 -135
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -1
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +3 -1
- package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -10
- package/dist/extensions/agent-browser/lib/parsing.js +7 -0
- package/dist/extensions/agent-browser/lib/playbook.js +6 -9
- package/dist/extensions/agent-browser/lib/process.js +28 -113
- package/dist/extensions/agent-browser/lib/recording-reservations.js +3 -1
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +5 -2
- package/dist/extensions/agent-browser/lib/results/envelope.js +9 -4
- package/dist/extensions/agent-browser/lib/results/next-actions.js +8 -0
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +45 -43
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +2 -1
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +10 -2
- package/dist/extensions/agent-browser/lib/results/presentation.js +9 -2
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +4 -4
- package/dist/extensions/agent-browser/lib/runtime.js +18 -2
- package/dist/extensions/agent-browser/lib/session-page-state.js +29 -10
- package/docs/ARCHITECTURE.md +8 -5
- package/docs/COMMAND_REFERENCE.md +25 -16
- package/docs/ELECTRON.md +6 -6
- package/docs/RELEASE.md +14 -5
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/SUPPORT_MATRIX.md +18 -5
- package/docs/TOOL_CONTRACT.md +38 -25
- package/package.json +5 -1
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
|
+
import { parseArgvDescriptor } from "../../argv-descriptor.js";
|
|
3
|
+
import { needsManagedSession } from "../../command-policy.js";
|
|
2
4
|
import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../../argv-grammar.js";
|
|
3
5
|
import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "../../batch-lifecycle.js";
|
|
4
|
-
import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand, isUnverifiedPageTransitionCommand } from "../../command-taxonomy.js";
|
|
6
|
+
import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWindowOrDiffPageTransitionCommand } from "../../command-taxonomy.js";
|
|
5
7
|
import { OPEN_RESULT_TAB_CORRECTION_FLAGS } from "../../launch-scoped-flags.js";
|
|
6
8
|
import { cleanupElectronLaunchResources, inspectElectronLaunchStatus } from "../../electron/cleanup.js";
|
|
7
9
|
import { getResultingPageTargetState, commandRequiresLivePageVerification } from "../../page-target-validation.js";
|
|
@@ -9,7 +11,8 @@ import { analyzeNetworkSourceLookupResults, analyzeSourceLookupResults, redactNe
|
|
|
9
11
|
import { analyzeQaPresetResults, analyzeQaPresetTimeout, buildQaCompactFailureText, buildQaCompactPassText, extractQaPageContext } from "../../input-modes/job.js";
|
|
10
12
|
import { applyNetworkRouteRecords, buildNetworkRouteDiagnostics } from "../../results/network-routes.js";
|
|
11
13
|
import { buildToolPresentation } from "../../results/presentation.js";
|
|
12
|
-
import {
|
|
14
|
+
import { compactLargePresentationOutput } from "../../results/presentation/large-output.js";
|
|
15
|
+
import { extractEnvelopeErrorText, getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
|
|
13
16
|
import { omitUpstreamLifecycle } from "../../results/presentation/common.js";
|
|
14
17
|
import { getClipboardWritePayloadCandidates, redactClipboardPermissionEcho, redactClipboardPermissionErrorValue } from "../../results/presentation/errors.js";
|
|
15
18
|
import { shouldCaptureSemanticActionNavigationSummary } from "../../results/presentation/semantic-action.js";
|
|
@@ -20,7 +23,7 @@ import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
|
|
|
20
23
|
import { createFreshSessionName, extractUpstreamCommandTokens, resolveManagedSessionState } from "../../runtime.js";
|
|
21
24
|
import { getUpstreamEffectiveBatchSteps } from "../batch-stdin.js";
|
|
22
25
|
import { closeManagedSession, inspectManagedSessionDaemon } from "./managed-session-daemon-policy.js";
|
|
23
|
-
import { applyOpenResultTabCorrection, buildAboutBlankRecoveryHint, buildAboutBlankWarning, buildElectronPostCommandHealthDiagnostic, buildElectronRefFreshnessDiagnostic, buildElectronSessionMismatch, buildManagedSessionOutcome, collectOpenResultTabCorrection, collectSessionTabSelection, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, formatElectronPostCommandHealthText, formatElectronSessionMismatchText, getSessionContextKey, getStaleRefArgs, mergeNavigationSummaryIntoData, shouldCaptureNavigationSummary, shouldCorrectSessionTabAfterCommand, shouldInspectElectronPostCommandHealth,
|
|
26
|
+
import { applyOpenResultTabCorrection, buildAboutBlankRecoveryHint, buildAboutBlankWarning, buildElectronPostCommandHealthDiagnostic, buildElectronRefFreshnessDiagnostic, buildElectronSessionMismatch, buildManagedSessionOutcome, collectOpenResultTabCorrection, collectSessionTabSelection, commandChoosesSessionTabTarget, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, formatElectronPostCommandHealthText, formatElectronSessionMismatchText, getSessionContextKey, getStaleRefArgs, mergeNavigationSummaryIntoData, shouldCaptureNavigationSummary, shouldCorrectSessionTabAfterCommand, shouldInspectElectronPostCommandHealth, updateTraceOwnerState, } from "./session-state.js";
|
|
24
27
|
import { collectClickDispatchDiagnostic } from "./click-dispatch.js";
|
|
25
28
|
import { buildScrollNoopDiagnostic, collectComboboxFocusDiagnostic, collectElectronBroadGetTextScopeDiagnostics, collectElectronHandoff, collectFillVerificationDiagnostic, collectNavigationSummary, collectOverlayBlockerDiagnostic, collectQaAttachedTarget, collectSnapshotOverlayBlockerDiagnostic, collectRecordingDependencyWarning, collectScrollPositionSnapshot, collectSelectorTextVisibilityDiagnostics, collectTimeoutPartialProgress, sleepMs, formatQaAttachedTargetText, getArtifactCleanupGuidance, getEvalResultWarning, getEvalStdinHint, getSourceLookupElectronContext, } from "./diagnostics.js";
|
|
26
29
|
import { repairScreenshotData } from "./prepare.js";
|
|
@@ -160,17 +163,13 @@ export async function processBrowserOutput(input) {
|
|
|
160
163
|
let networkRoutesBySession = state.networkRoutesBySession;
|
|
161
164
|
try {
|
|
162
165
|
const persistentArtifactStore = getPersistentSessionArtifactStore(ctx);
|
|
163
|
-
|
|
166
|
+
// Native upgrade prints text even with --json; all other command shapes keep strict JSON parsing.
|
|
167
|
+
const plainTextUpgrade = !prepared.executionPlan.plainTextInspection && prepared.executionPlan.commandInfo.command === "upgrade" && !needsManagedSession(parseArgvDescriptor(prepared.runtimeToolArgs));
|
|
168
|
+
const parsed = await parseAgentBrowserEnvelope({ stdout: processResult.stdout, stdoutPath: processResult.stdoutSpillPath, plainText: plainTextUpgrade });
|
|
164
169
|
let parseError = parsed.parseError;
|
|
165
170
|
let presentationEnvelope = parsed.envelope;
|
|
166
171
|
let navigationSummary = undefined;
|
|
167
172
|
let failedTransitionReverification = false;
|
|
168
|
-
if (prepared.pinnedBatchUnwrapMode) {
|
|
169
|
-
const pinnedBatchResult = unwrapPinnedSessionBatchEnvelope({ envelope: parsed.envelope, includeNavigationSummary: prepared.includePinnedNavigationSummary, mode: prepared.pinnedBatchUnwrapMode });
|
|
170
|
-
parseError = pinnedBatchResult.parseError ?? parseError;
|
|
171
|
-
presentationEnvelope = pinnedBatchResult.envelope ?? presentationEnvelope;
|
|
172
|
-
navigationSummary = pinnedBatchResult.navigationSummary;
|
|
173
|
-
}
|
|
174
173
|
const repairedScreenshot = await repairScreenshotArtifact({ cwd, envelope: presentationEnvelope, request: prepared.preparedArgs.screenshotPathRequest });
|
|
175
174
|
presentationEnvelope = repairedScreenshot.envelope;
|
|
176
175
|
const repairedBatchScreenshots = await repairBatchScreenshotArtifacts({ cwd, envelope: presentationEnvelope, requests: prepared.preparedArgs.batchScreenshotPathRequests });
|
|
@@ -180,6 +179,15 @@ export async function processBrowserOutput(input) {
|
|
|
180
179
|
const batchCommandSteps = prepared.executionPlan.commandInfo.command === "batch"
|
|
181
180
|
? getUpstreamEffectiveBatchSteps(prepared.commandTokens, prepared.runtimeToolStdin)
|
|
182
181
|
: [];
|
|
182
|
+
const dispatchedCommands = prepared.executionPlan.commandInfo.command !== "batch"
|
|
183
|
+
? [prepared.commandTokens]
|
|
184
|
+
: Array.isArray(presentationEnvelope?.data)
|
|
185
|
+
? presentationEnvelope.data.flatMap((row, index) => isRecord(row) ? [Array.isArray(row.command) && row.command.every((token) => typeof token === "string") ? row.command : batchCommandSteps[index] ?? []] : [])
|
|
186
|
+
: batchCommandSteps;
|
|
187
|
+
const destinationTransition = dispatchedCommands.some((step) => {
|
|
188
|
+
const [command, subcommand] = extractUpstreamCommandTokens(step);
|
|
189
|
+
return isWindowOrDiffPageTransitionCommand(command, subcommand);
|
|
190
|
+
});
|
|
183
191
|
const nestedBatchClose = prepared.executionPlan.commandInfo.command === "batch"
|
|
184
192
|
? getSuccessfulBatchCloseLifecycle(presentationEnvelope?.data, batchCommandSteps)
|
|
185
193
|
: undefined;
|
|
@@ -198,7 +206,7 @@ export async function processBrowserOutput(input) {
|
|
|
198
206
|
const parseFailureOutput = parseError && processResult.stdoutSpillPath
|
|
199
207
|
? { fullOutputUnavailable: "Malformed upstream output was discarded because it may contain sensitive browser data." }
|
|
200
208
|
: {};
|
|
201
|
-
const processSucceeded = !processResult.aborted && !processResult.spawnError && processResult.exitCode === 0;
|
|
209
|
+
const processSucceeded = !processResult.timedOut && !processResult.aborted && !processResult.spawnError && processResult.exitCode === 0;
|
|
202
210
|
const plainTextInspection = prepared.executionPlan.plainTextInspection && processSucceeded;
|
|
203
211
|
const parseSucceeded = plainTextInspection || parseError === undefined;
|
|
204
212
|
if (isStreamEnableAlreadyEnabledNoop({ command: prepared.executionPlan.commandInfo.command, envelope: presentationEnvelope, processSucceeded, subcommand: prepared.executionPlan.commandInfo.subcommand })) {
|
|
@@ -209,6 +217,10 @@ export async function processBrowserOutput(input) {
|
|
|
209
217
|
const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
|
|
210
218
|
const sessionStateKey = getSessionContextKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace);
|
|
211
219
|
const closeAllApplied = nestedBatchClosesAll || (directCloseAllRequested && succeeded);
|
|
220
|
+
if (sessionStateKey && processResult.agentBrowserStarted && sessionPageState.get(sessionStateKey).tabReopenPending === true) {
|
|
221
|
+
if (dispatchedCommands.some(commandChoosesSessionTabTarget))
|
|
222
|
+
sessionPageState.setTabReopenPending({ pending: false, sessionName: sessionStateKey, update: sessionPageStateUpdate });
|
|
223
|
+
}
|
|
212
224
|
if (closeAllApplied) {
|
|
213
225
|
networkRoutesBySession = withoutNamespaceEntries(networkRoutesBySession, prepared.executionPlan.namespace);
|
|
214
226
|
deleteNamespaceEntries(state.attachedSessionKeys, prepared.executionPlan.namespace);
|
|
@@ -252,36 +264,32 @@ export async function processBrowserOutput(input) {
|
|
|
252
264
|
}
|
|
253
265
|
}
|
|
254
266
|
const tabTransition = prepared.executionPlan.commandInfo.command === "tab" && prepared.executionPlan.commandInfo.subcommand !== undefined && !["list", "new"].includes(prepared.executionPlan.commandInfo.subcommand);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
currentPageUrl: prepared.priorSessionTabTarget?.url,
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
267
|
+
// Non-page rows (including a failed prefix) cannot retire a cold target for a navigation that never ran.
|
|
268
|
+
const resultingPageState = sessionPageState.get(sessionStateKey).tabReopenPending === true
|
|
269
|
+
? { currentPageUrl: prepared.priorSessionTabTarget?.url, pageTargetMayHaveChanged: false, pageUrlUnknown: prepared.priorSessionTabTargetUnknown === true }
|
|
270
|
+
: getResultingPageTargetState({
|
|
271
|
+
args: prepared.executionPlan.effectiveArgs,
|
|
272
|
+
executedBatchSteps: dispatchedCommands,
|
|
273
|
+
currentPageUrl: prepared.priorSessionTabTarget?.url,
|
|
274
|
+
pageUrlUnknown: prepared.priorSessionTabTargetUnknown === true,
|
|
275
|
+
});
|
|
262
276
|
if (succeeded &&
|
|
263
|
-
!navigationSummary &&
|
|
264
277
|
(shouldCaptureNavigationSummary(prepared.executionPlan.commandInfo.command, presentationEnvelope?.data, prepared.executionPlan.commandInfo.subcommand) ||
|
|
265
278
|
shouldCaptureSemanticActionNavigationSummary(prepared.compiledSemanticAction, presentationEnvelope?.data) ||
|
|
266
279
|
commandRequiresLivePageVerification(prepared.executionPlan.effectiveArgs, prepared.runtimeToolStdin) ||
|
|
267
|
-
tabTransition)) {
|
|
280
|
+
(destinationTransition && !nestedBatchClosed) || tabTransition)) {
|
|
268
281
|
navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, reusePriorTitle: !tabTransition, sessionName: prepared.executionPlan.sessionName, signal });
|
|
269
282
|
}
|
|
270
|
-
//
|
|
271
|
-
// agent through a manual get url round trip. Probe the live URL ourselves instead: an observed page
|
|
272
|
-
// stays verified, and a failed
|
|
273
|
-
// probe preserves the existing unknown-target behavior.
|
|
283
|
+
// Failed transitions may already have changed the page; keep only a live observed URL.
|
|
274
284
|
if (succeeded === false &&
|
|
275
|
-
!navigationSummary &&
|
|
276
285
|
processResult.agentBrowserStarted &&
|
|
277
286
|
!processResult.aborted &&
|
|
278
287
|
!processResult.timedOut &&
|
|
279
|
-
|
|
280
|
-
|
|
288
|
+
!nestedBatchClosed &&
|
|
289
|
+
(destinationTransition || (prepared.executionPlan.commandInfo.command !== "batch" &&
|
|
290
|
+
isUnverifiedPageTransitionCommand(prepared.executionPlan.commandInfo.command, prepared.executionPlan.commandInfo.subcommand)))) {
|
|
281
291
|
navigationSummary = await collectNavigationSummary({ cwd, namespace: prepared.executionPlan.namespace, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName, signal });
|
|
282
|
-
//
|
|
283
|
-
// keeping the verified URL must not keep the prior snapshot refs (markTabTargetUnknown dropped
|
|
284
|
-
// them before this probe existed); invalidate so the next ref use requires a fresh snapshot.
|
|
292
|
+
// Re-verifying a failed transition's URL does not make its prior refs valid.
|
|
285
293
|
failedTransitionReverification = navigationSummary !== undefined;
|
|
286
294
|
}
|
|
287
295
|
if (navigationSummary && presentationEnvelope && prepared.executionPlan.commandInfo.command !== "eval" && !Array.isArray(presentationEnvelope.data))
|
|
@@ -309,8 +317,9 @@ export async function processBrowserOutput(input) {
|
|
|
309
317
|
const safeObservedSessionTabTarget = observedSessionTabTarget;
|
|
310
318
|
let currentSessionTabTarget = safeObservedSessionTabTarget;
|
|
311
319
|
if (!currentSessionTabTarget && nestedBatchClose === undefined) {
|
|
320
|
+
// Window/diff responses do not report the final URL; URL2 is intent, not redirect evidence.
|
|
312
321
|
currentSessionTabTarget = resultingPageState.pageTargetMayHaveChanged
|
|
313
|
-
? succeeded ? normalizeSessionTabTarget({ url: resultingPageState.currentPageUrl }) : undefined
|
|
322
|
+
? succeeded && !destinationTransition ? normalizeSessionTabTarget({ url: resultingPageState.currentPageUrl }) : undefined
|
|
314
323
|
: deriveSessionTabTarget({ command: prepared.executionPlan.commandInfo.command, data: presentationEnvelope?.data, navigationSummary, previousTarget: prepared.priorSessionTabTarget, subcommand: prepared.executionPlan.commandInfo.subcommand });
|
|
315
324
|
}
|
|
316
325
|
let aboutBlankSessionMismatch;
|
|
@@ -318,7 +327,8 @@ export async function processBrowserOutput(input) {
|
|
|
318
327
|
let electronRefFreshnessDiagnostic;
|
|
319
328
|
let electronSessionMismatch;
|
|
320
329
|
let electronStatusAfterCommand;
|
|
321
|
-
const
|
|
330
|
+
const explicitlyTargetsAboutBlank = dispatchedCommands.some((step) => commandExplicitlyTargetsAboutBlank(extractUpstreamCommandTokens(step)));
|
|
331
|
+
const shouldTreatAboutBlankAsMismatch = succeeded && !tabTransition && !destinationTransition && !explicitlyTargetsAboutBlank && nestedBatchClose === undefined && prepared.priorSessionTabTarget !== undefined && !isAboutBlankSessionTabTarget(prepared.priorSessionTabTarget) && isAboutBlankSessionTabTarget(observedSessionTabTarget ?? currentSessionTabTarget);
|
|
322
332
|
let sessionTabCorrection = prepared.sessionTabCorrection;
|
|
323
333
|
if (shouldTreatAboutBlankAsMismatch && prepared.priorSessionTabTarget) {
|
|
324
334
|
const aboutBlankObservedTarget = observedSessionTabTarget ?? currentSessionTabTarget;
|
|
@@ -536,7 +546,7 @@ export async function processBrowserOutput(input) {
|
|
|
536
546
|
if (sessionStateKey && succeeded) {
|
|
537
547
|
if (openResultTabCorrection || sessionTabCorrection || aboutBlankSessionMismatch?.recoveryApplied)
|
|
538
548
|
sessionPageState.markPinning(sessionStateKey, "drift");
|
|
539
|
-
else if (prepared.sessionTabPinningReason === "restore")
|
|
549
|
+
else if (prepared.sessionTabPinningReason === "restore" && observedSessionTabTarget)
|
|
540
550
|
sessionPageState.clearRestorePinning(sessionStateKey);
|
|
541
551
|
}
|
|
542
552
|
if (replacedManagedSessionName) {
|
|
@@ -626,14 +636,25 @@ export async function processBrowserOutput(input) {
|
|
|
626
636
|
electronLaunchRecord = electronFailedConnectCleanup.record;
|
|
627
637
|
}
|
|
628
638
|
}
|
|
629
|
-
let errorText = getAgentBrowserErrorText({ aborted: processResult.aborted, command: prepared.executionPlan.commandInfo.command, effectiveArgs: prepared.redactedProcessArgs, envelope: presentationEnvelope, exitCode: processResult.exitCode, parseError, plainTextInspection, staleRefArgs: getStaleRefArgs(prepared.commandTokens, prepared.runtimeToolStdin), spawnError: processResult.spawnError, stderr: processResult.stderr, timedOut: processResult.timedOut, timeoutMs: processResult.timeoutMs, wrapperRecoveryHint: buildWrapperRecoveryHint({
|
|
639
|
+
let errorText = getAgentBrowserErrorText({ aborted: processResult.aborted, command: prepared.executionPlan.commandInfo.command, effectiveArgs: prepared.redactedProcessArgs, envelope: presentationEnvelope, exitCode: processResult.exitCode, parseError, plainTextInspection, staleRefArgs: getStaleRefArgs(prepared.commandTokens, prepared.runtimeToolStdin), spawnError: processResult.spawnError, stderr: processResult.stderr, timedOut: processResult.timedOut, timeoutMs: processResult.timeoutMs, wrapperRecoveryHint: buildWrapperRecoveryHint({ sessionTabCorrection }) });
|
|
640
|
+
if (errorText && presentationEnvelope?.success === false && extractEnvelopeErrorText(presentationEnvelope.error) === undefined)
|
|
641
|
+
presentationEnvelope = { ...presentationEnvelope, error: errorText };
|
|
630
642
|
if (errorText) {
|
|
631
643
|
const clipboardWritePayloadCandidates = getClipboardWritePayloadCandidates(prepared.commandTokens);
|
|
632
644
|
errorText = redactClipboardPermissionEcho(prepared.executionPlan.commandInfo, errorText);
|
|
633
645
|
if (presentationEnvelope?.error !== undefined)
|
|
634
646
|
presentationEnvelope = { ...presentationEnvelope, error: redactClipboardPermissionErrorValue(prepared.executionPlan.commandInfo, presentationEnvelope.error, clipboardWritePayloadCandidates) };
|
|
635
647
|
}
|
|
636
|
-
|
|
648
|
+
if (plainTextUpgrade && errorText)
|
|
649
|
+
presentationEnvelope = { ...presentationEnvelope, success: false, error: errorText };
|
|
650
|
+
let presentation = plainTextInspection ? { artifacts: undefined, batchFailure: undefined, batchSteps: undefined, content: [{ type: "text", text: inspectionText ?? "" }], data: undefined, fullOutputPath: undefined, fullOutputPaths: undefined, imagePath: undefined, imagePaths: undefined, savedFile: undefined, savedFilePath: undefined, summary: `${prepared.redactedArgs.join(" ")} completed` } : await buildToolPresentation({ args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, sessionName: prepared.executionPlan.sessionName });
|
|
651
|
+
if (plainTextUpgrade && !succeeded && typeof presentationEnvelope?.data === "string") {
|
|
652
|
+
presentation.data = presentationEnvelope.data;
|
|
653
|
+
const errorContent = presentation.content[0];
|
|
654
|
+
if (errorContent?.type === "text" && presentationEnvelope.data)
|
|
655
|
+
errorContent.text += `\n\n${presentationEnvelope.data}`;
|
|
656
|
+
presentation = await compactLargePresentationOutput({ artifactManifest, commandInfo: prepared.executionPlan.commandInfo, data: presentation.data, persistentArtifactStore, presentation });
|
|
657
|
+
}
|
|
637
658
|
if (electronHandoff?.error && electronHandoff.failureCategory)
|
|
638
659
|
presentation.failureCategory = electronHandoff.failureCategory;
|
|
639
660
|
networkRoutesBySession = applyBatchNetworkRouteState({ data: presentationEnvelope?.data, routesBySession: networkRoutesBySession, sessionName: sessionStateKey, succeeded });
|
|
@@ -738,7 +759,14 @@ export async function processBrowserOutput(input) {
|
|
|
738
759
|
const evalResultWarning = getEvalResultWarning({ command: prepared.executionPlan.commandInfo.command, data: presentationEnvelope?.data, navigationSummary: evalNavigationSummary, pageUrl: evalPageUrl, stdin: prepared.runtimeToolStdin });
|
|
739
760
|
const resultArtifactManifest = presentation.artifactManifest ?? artifactManifest;
|
|
740
761
|
const artifactCleanup = await getArtifactCleanupGuidance({ command: prepared.executionPlan.commandInfo.command, cwd, manifest: resultArtifactManifest, succeeded });
|
|
741
|
-
const
|
|
762
|
+
const recordingTransitionReached = prepared.executionPlan.commandInfo.command === "batch"
|
|
763
|
+
? presentation.batchSteps?.some((step) => isRecordPageTransitionCommand(extractUpstreamCommandTokens(step.command ?? [])))
|
|
764
|
+
: isRecordPageTransitionCommand(prepared.commandTokens);
|
|
765
|
+
const recordingPageWarning = processResult.agentBrowserStarted && !prepared.executionPlan.plainTextInspection && recordingTransitionReached
|
|
766
|
+
? "Page state: this recording command can replace or navigate the active page, even on failure; prior in-page DOM and JavaScript state may not carry over. Take a fresh snapshot before continuing with page-scoped refs."
|
|
767
|
+
: undefined;
|
|
768
|
+
const sessionWarning = electronPostCommandHealth ? formatElectronPostCommandHealthText(electronPostCommandHealth) : electronSessionMismatch ? formatElectronSessionMismatchText(electronSessionMismatch) : aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
|
|
769
|
+
const warningText = [sessionWarning, recordingPageWarning].filter(Boolean).join("\n\n") || undefined;
|
|
742
770
|
const redactedContent = buildRedactedPresentationContent({ exactSensitiveValues: prepared.exactSensitiveValues, plainTextInspection, presentation, presentationEnvelope, succeeded, userRequestedJson: prepared.userRequestedJson, warningText });
|
|
743
771
|
const finalRecoveryState = await prepareFinalResultRecoveryState({ aboutBlankSessionMismatch, batchRefSnapshotState, commandTokens: prepared.commandTokens, compiledSemanticAction: prepared.compiledSemanticAction, currentRefSnapshot, currentRefSnapshotInvalidation, currentSessionTabTarget, cwd, electronPostCommandHealth, errorText, executionPlan: prepared.executionPlan, parseError, plainTextInspection, presentation, processResult, redactedProcessArgs: prepared.redactedProcessArgs, runtimeToolArgs: prepared.runtimeToolArgs, sessionPageState, sessionPageStateUpdate, sessionTabCorrection, signal, succeeded });
|
|
744
772
|
currentRefSnapshot = finalRecoveryState.currentRefSnapshot;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
|
-
import { getAgentBrowserSessionIdentityKey
|
|
2
|
+
import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
|
|
3
|
+
import { parseArgvDescriptor } from "../../argv-descriptor.js";
|
|
4
|
+
import { needsManagedSession } from "../../command-policy.js";
|
|
3
5
|
import { runAgentBrowserProcess } from "../../process.js";
|
|
4
6
|
import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
|
|
5
7
|
import { parseAgentBrowserEnvelope } from "../../results/envelope.js";
|
|
6
8
|
import { buildNextToolAction, withOptionalNamespaceArgs, withOptionalSessionArgs } from "../../results/next-actions.js";
|
|
7
|
-
import { getSessionPageStateKey, isAboutBlankUrl, normalizeComparableUrl,
|
|
8
|
-
import { isCloseCommand, isElectronPostCommandHealthCommand, isNavigationObservableCommandName, isRefGuardedCommand, isRefInvalidatingBatchCommand, isSessionTabPinningExcludedCommand, isSessionTabPostCommandCorrectionExcludedCommand, } from "../../command-taxonomy.js";
|
|
9
|
+
import { getSessionPageStateKey, isAboutBlankUrl, normalizeComparableUrl, targetsMatch, } from "../../session-page-state.js";
|
|
10
|
+
import { isCloseCommand, isElectronPostCommandHealthCommand, isNavigationObservableCommandName, isOpenNavigationCommand, isRefGuardedCommand, isRefInvalidatingBatchCommand, isRecordPageTransitionCommand, isSessionTabPinningExcludedCommand, isSessionTabPostCommandCorrectionExcludedCommand, isWindowOrDiffPageTransitionCommand, } from "../../command-taxonomy.js";
|
|
9
11
|
import { chooseOpenResultTabCorrection } from "../../runtime.js";
|
|
10
|
-
import { isRecord } from "../../parsing.js";
|
|
11
|
-
import { getUpstreamEffectiveBatchSteps
|
|
12
|
-
|
|
12
|
+
import { isRecord, parseRefId } from "../../parsing.js";
|
|
13
|
+
import { getUpstreamEffectiveBatchSteps } from "../batch-stdin.js";
|
|
14
|
+
import { findFirstPositionalArgument } from "./prepare/wait-timeouts.js";
|
|
13
15
|
export function applyBrowserRunStatePatch(state, patch) {
|
|
14
16
|
if (!patch)
|
|
15
17
|
return;
|
|
@@ -278,39 +280,57 @@ export function getStaleRefArgs(commandTokens, stdin) {
|
|
|
278
280
|
const steps = getUpstreamEffectiveBatchSteps(commandTokens, stdin);
|
|
279
281
|
return steps.length > 0 ? steps.flatMap((step) => step) : commandTokens;
|
|
280
282
|
}
|
|
281
|
-
//
|
|
282
|
-
// (--text, --baseline, --name, and similar) are literal text or paths that may merely look like refs,
|
|
283
|
-
// per upstream parsing. Boolean flags such as --new-tab or --full do not consume the following token,
|
|
284
|
-
// so a ref after them is a genuine positional selector and must stay guarded.
|
|
285
|
-
const SELECTOR_FLAG_TOKENS = new Set(["--selector", "-s"]);
|
|
286
|
-
const SHORT_VALUE_FLAG_TOKENS = new Set(["-b", "-o", "-t"]);
|
|
287
|
-
function isNonSelectorValueFlagToken(token) {
|
|
288
|
-
if (token === undefined || SELECTOR_FLAG_TOKENS.has(token))
|
|
289
|
-
return false;
|
|
290
|
-
return VALUE_FLAGS.has(token) || SHORT_VALUE_FLAG_TOKENS.has(token);
|
|
291
|
-
}
|
|
283
|
+
// Inspect only upstream's selector slots: fill text, file paths and key data are not refs.
|
|
292
284
|
function collectRefsFromTokens(tokens) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
285
|
+
if (!isRefGuardedCommand(tokens[0]) || (tokens[0] === "diff" && tokens[1] !== "screenshot"))
|
|
286
|
+
return [];
|
|
287
|
+
let selectors;
|
|
288
|
+
switch (tokens[0]) {
|
|
289
|
+
case "click":
|
|
290
|
+
selectors = [tokens.slice(1).find((token) => token !== "--new-tab")];
|
|
291
|
+
break;
|
|
292
|
+
case "drag":
|
|
293
|
+
selectors = tokens.slice(1, 3);
|
|
294
|
+
break;
|
|
295
|
+
case "get":
|
|
296
|
+
selectors = [!["url", "title", "cdp-url", "count"].includes(tokens[1]) ? tokens[2] : undefined];
|
|
297
|
+
break;
|
|
298
|
+
case "is":
|
|
299
|
+
selectors = [tokens[2]];
|
|
300
|
+
break;
|
|
301
|
+
case "screenshot":
|
|
302
|
+
selectors = [tokens.slice(1).find((token) => !["--full", "-f"].includes(token))];
|
|
303
|
+
break;
|
|
304
|
+
case "diff":
|
|
305
|
+
case "scroll": {
|
|
306
|
+
let selector;
|
|
307
|
+
for (let index = tokens[0] === "diff" ? 2 : 1; index < tokens.length; index += 1) {
|
|
308
|
+
const token = tokens[index];
|
|
309
|
+
if (token === "--selector" || token === "-s")
|
|
310
|
+
selector = tokens[++index];
|
|
311
|
+
else if (tokens[0] === "diff" && ["--baseline", "-b", "--output", "-o", "--threshold", "-t", "--depth", "-d"].includes(token))
|
|
312
|
+
index += 1;
|
|
313
|
+
}
|
|
314
|
+
selectors = [selector];
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
default: selectors = [tokens[1]];
|
|
300
318
|
}
|
|
301
|
-
return
|
|
319
|
+
return selectors.flatMap((selector) => {
|
|
320
|
+
const ref = selector === undefined ? undefined : parseRefId(selector);
|
|
321
|
+
return ref === undefined ? [] : [ref];
|
|
322
|
+
});
|
|
302
323
|
}
|
|
303
324
|
export function getGuardedRefUsage(commandTokens, stdin, options = {}) {
|
|
304
|
-
const collectFromStep = (step) => isRefGuardedCommand(step[0]) ? collectRefsFromTokens(step) : [];
|
|
305
325
|
if (commandTokens[0] !== "batch") {
|
|
306
|
-
return
|
|
326
|
+
return collectRefsFromTokens(commandTokens);
|
|
307
327
|
}
|
|
308
328
|
const steps = getUpstreamEffectiveBatchSteps(commandTokens, stdin);
|
|
309
329
|
const refsBeforeInBatchSnapshot = [];
|
|
310
330
|
for (const step of steps) {
|
|
311
331
|
if (!options.includeRefsAfterBatchSnapshot && (step[0] ?? "") === "snapshot")
|
|
312
332
|
break;
|
|
313
|
-
refsBeforeInBatchSnapshot.push(...
|
|
333
|
+
refsBeforeInBatchSnapshot.push(...collectRefsFromTokens(step));
|
|
314
334
|
}
|
|
315
335
|
return refsBeforeInBatchSnapshot;
|
|
316
336
|
}
|
|
@@ -395,61 +415,41 @@ export function buildStaleRefPreflight(options) {
|
|
|
395
415
|
}
|
|
396
416
|
return undefined;
|
|
397
417
|
}
|
|
398
|
-
function
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return false;
|
|
418
|
+
export function commandChoosesSessionTabTarget(args) {
|
|
419
|
+
const tokens = parseArgvDescriptor(args).upstreamCommandTokens;
|
|
420
|
+
const [command, subcommand] = tokens;
|
|
421
|
+
return isOpenNavigationCommand(command) || isCloseCommand(command) || command === "connect"
|
|
422
|
+
|| (command === "state" && subcommand === "load")
|
|
423
|
+
|| (command === "tab" && subcommand !== undefined && subcommand !== "list")
|
|
424
|
+
|| isWindowOrDiffPageTransitionCommand(command, subcommand)
|
|
425
|
+
|| (command === "a11y" && findFirstPositionalArgument(tokens) !== undefined)
|
|
426
|
+
|| (["vitals", "web-vitals"].includes(command) && tokens.slice(1).some((token) => !token.startsWith("--")))
|
|
427
|
+
|| (isRecordPageTransitionCommand(tokens) && tokens[3] !== undefined);
|
|
409
428
|
}
|
|
410
429
|
export function shouldPinSessionTabForCommand(options) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (parsed.error) {
|
|
433
|
-
return { error: parsed.error };
|
|
434
|
-
}
|
|
435
|
-
return {
|
|
436
|
-
includeNavigationSummary: false,
|
|
437
|
-
steps: [tabSelectionStep, ...(parsed.steps ?? [])],
|
|
438
|
-
unwrapMode: "user-batch",
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
if (options.commandTokens.length === 0) {
|
|
442
|
-
return undefined;
|
|
430
|
+
if (!options.pinningRequired || !options.sessionName || !options.command)
|
|
431
|
+
return false;
|
|
432
|
+
const steps = options.command === "batch" ? getUpstreamEffectiveBatchSteps(options.commandTokens, options.stdin) : [options.commandTokens];
|
|
433
|
+
for (const step of steps) {
|
|
434
|
+
const descriptor = parseArgvDescriptor(step);
|
|
435
|
+
const tokens = descriptor.upstreamCommandTokens;
|
|
436
|
+
const [command, subcommand] = tokens;
|
|
437
|
+
// A later page action uses the explicit destination, not the remembered tab.
|
|
438
|
+
if (commandChoosesSessionTabTarget(tokens))
|
|
439
|
+
return false;
|
|
440
|
+
if (!needsManagedSession(descriptor) || isSessionTabPinningExcludedCommand(command))
|
|
441
|
+
continue;
|
|
442
|
+
if (command === "get" && subcommand === "url" && !options.reopenPending)
|
|
443
|
+
continue;
|
|
444
|
+
if (command === "read" && findFirstPositionalArgument(tokens) !== undefined)
|
|
445
|
+
continue;
|
|
446
|
+
if (["console", "errors"].includes(command))
|
|
447
|
+
continue;
|
|
448
|
+
if (command === "network" && !(subcommand === "requests" && tokens.some((token) => ["--current-page", "--current-origin", "--current-url"].includes(token))))
|
|
449
|
+
continue;
|
|
450
|
+
return true;
|
|
443
451
|
}
|
|
444
|
-
|
|
445
|
-
const tabSelectionStep = ["tab", options.selectedTab];
|
|
446
|
-
const commandStep = options.commandTokens;
|
|
447
|
-
const navigationSummarySteps = includeNavigationSummary ? [["eval", NAVIGATION_SUMMARY_EVAL]] : [];
|
|
448
|
-
return {
|
|
449
|
-
includeNavigationSummary,
|
|
450
|
-
steps: [tabSelectionStep, commandStep, ...navigationSummarySteps],
|
|
451
|
-
unwrapMode: "single-command",
|
|
452
|
-
};
|
|
452
|
+
return false;
|
|
453
453
|
}
|
|
454
454
|
export function shouldCorrectSessionTabAfterCommand(options) {
|
|
455
455
|
return (options.pinningRequired === true &&
|
|
@@ -477,59 +477,11 @@ function selectAnySessionTargetTab(options) {
|
|
|
477
477
|
return undefined;
|
|
478
478
|
const matchingTabs = options.tabs.filter((tab) => normalizeComparableUrl(tab.url ?? "") === targetUrl);
|
|
479
479
|
const targetTitle = options.target.title?.trim() ?? "";
|
|
480
|
-
const
|
|
480
|
+
const titledTabs = targetTitle ? matchingTabs.filter((tab) => tab.title?.trim() === targetTitle) : [];
|
|
481
|
+
const selectedTab = titledTabs.find((tab) => tab.active) ?? titledTabs[0] ?? matchingTabs.find((tab) => tab.active) ?? matchingTabs[0];
|
|
481
482
|
const selection = selectedTab ? getTabSelection(selectedTab) : undefined;
|
|
482
483
|
return selection ? { ...selection, ...(targetTitle ? { targetTitle } : {}), targetUrl } : undefined;
|
|
483
484
|
}
|
|
484
|
-
export function unwrapPinnedSessionBatchEnvelope(options) {
|
|
485
|
-
if (!options.envelope) {
|
|
486
|
-
return {};
|
|
487
|
-
}
|
|
488
|
-
if (!Array.isArray(options.envelope.data)) {
|
|
489
|
-
return {
|
|
490
|
-
parseError: "agent-browser returned an unexpected response while applying the wrapper's tab-pinning batch.",
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
const steps = options.envelope.data.filter(isRecord);
|
|
494
|
-
const tabSelectionStep = steps[0];
|
|
495
|
-
const commandStep = steps[1];
|
|
496
|
-
if (tabSelectionStep?.success === false) {
|
|
497
|
-
return {
|
|
498
|
-
envelope: {
|
|
499
|
-
success: false,
|
|
500
|
-
error: tabSelectionStep.error ?? "agent-browser could not re-select the intended tab before running the command.",
|
|
501
|
-
},
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
if (options.mode === "user-batch") {
|
|
505
|
-
const userSteps = steps.slice(1);
|
|
506
|
-
return {
|
|
507
|
-
envelope: {
|
|
508
|
-
success: userSteps.every((step) => step.success !== false),
|
|
509
|
-
data: userSteps,
|
|
510
|
-
error: userSteps.find((step) => step.success === false)?.error,
|
|
511
|
-
},
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
if (!commandStep) {
|
|
515
|
-
return {
|
|
516
|
-
envelope: {
|
|
517
|
-
success: false,
|
|
518
|
-
error: "agent-browser did not return the corrected command result.",
|
|
519
|
-
},
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
const navigationSummaryStep = options.includeNavigationSummary ? steps[2] : undefined;
|
|
523
|
-
const navigationSummary = normalizeSessionTabTarget(extractNavigationSummaryFromData(navigationSummaryStep?.result));
|
|
524
|
-
return {
|
|
525
|
-
envelope: {
|
|
526
|
-
success: commandStep.success !== false,
|
|
527
|
-
data: commandStep.result,
|
|
528
|
-
error: commandStep.success === false ? commandStep.error : undefined,
|
|
529
|
-
},
|
|
530
|
-
navigationSummary,
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
485
|
export async function runSessionCommandData(options) {
|
|
534
486
|
const { args, cwd, namespace, pinNamespace, sessionName, signal, stdin, throwOnFailure, timeoutMs } = options;
|
|
535
487
|
if (!sessionName)
|
|
@@ -542,6 +494,7 @@ export async function runSessionCommandData(options) {
|
|
|
542
494
|
timeoutMs,
|
|
543
495
|
});
|
|
544
496
|
try {
|
|
497
|
+
options.onProcessResult?.(processResult);
|
|
545
498
|
if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
|
|
546
499
|
if (throwOnFailure) {
|
|
547
500
|
const reason = processResult.aborted
|
|
@@ -604,11 +557,23 @@ export async function collectSessionTabSelection(options) {
|
|
|
604
557
|
const tabs = mapTabData(tabData);
|
|
605
558
|
return tabs ? selectSessionTargetTab({ tabs, target }) : undefined;
|
|
606
559
|
}
|
|
607
|
-
export async function
|
|
608
|
-
const {
|
|
609
|
-
const
|
|
610
|
-
const
|
|
611
|
-
|
|
560
|
+
export async function ensureSessionTabTarget(options) {
|
|
561
|
+
const readTabs = async () => mapTabData(await runSessionCommandData({ ...options, args: ["tab", "list"] }));
|
|
562
|
+
const tabs = await readTabs();
|
|
563
|
+
const active = tabs?.find((tab) => tab.active);
|
|
564
|
+
const correction = tabs && selectAnySessionTargetTab({ tabs, target: options.target });
|
|
565
|
+
const error = "agent-browser could not re-select and verify the intended tab before running the command. Run tab list and select the intended tab, then snapshot -i before retrying.";
|
|
566
|
+
if (!correction)
|
|
567
|
+
return { error };
|
|
568
|
+
// Native tab selection clears refs and frame scope even when selecting the current tab.
|
|
569
|
+
if (active && getTabSelection(active)?.selectedTab === correction.selectedTab)
|
|
570
|
+
return {};
|
|
571
|
+
if (!await applyOpenResultTabCorrection({ ...options, correction }))
|
|
572
|
+
return { correction, error };
|
|
573
|
+
const selected = (await readTabs())?.find((tab) => tab.active);
|
|
574
|
+
return selected && getTabSelection(selected)?.selectedTab === correction.selectedTab && normalizeComparableUrl(selected.url ?? "") === normalizeComparableUrl(options.target.url)
|
|
575
|
+
? { correction }
|
|
576
|
+
: { correction, error };
|
|
612
577
|
}
|
|
613
578
|
export async function applyOpenResultTabCorrection(options) {
|
|
614
579
|
const { correction, cwd, namespace, sessionName, signal } = options;
|
|
@@ -174,7 +174,9 @@ function formatElectronStatusVisibleText(statuses, records, mismatches = [], man
|
|
|
174
174
|
const sessionName = record?.sessionName;
|
|
175
175
|
const appName = record?.appName ?? "Electron launch";
|
|
176
176
|
const sessionText = sessionName ? `, sessionName ${sessionName}` : "";
|
|
177
|
-
|
|
177
|
+
const historyText = status.cleanupState === "cleaned" ? "; historical cleaned launch record" : "";
|
|
178
|
+
lines.push(`- ${status.launchId}: ${appName}${sessionText}${historyText}; ${status.portAlive ? "debug port alive" : "debug port dead"}${status.pidAlive === undefined ? "" : status.pidAlive ? ", pid alive" : ", pid dead"} (port ${status.port})`);
|
|
179
|
+
lines.push(` Tracked profile path: ${status.userDataDirState}.`);
|
|
178
180
|
lines.push(` Identifiers: launchId ${status.launchId}; sessionName ${sessionName ?? "not attached"}.`);
|
|
179
181
|
for (const targetLine of formatElectronTargetLines(status.targets, 4))
|
|
180
182
|
lines.push(` ${targetLine}`);
|
|
@@ -114,7 +114,9 @@ export function resolveAgentBrowserInput(options) {
|
|
|
114
114
|
const timeoutMsError = params.timeoutMs !== undefined && (typeof params.timeoutMs !== "number" || !Number.isSafeInteger(params.timeoutMs) || params.timeoutMs <= 0)
|
|
115
115
|
? "timeoutMs must be a positive integer when provided."
|
|
116
116
|
: compiledElectron && params.timeoutMs !== undefined
|
|
117
|
-
?
|
|
117
|
+
? compiledElectron.action === "list"
|
|
118
|
+
? "electron.list has no configurable timeout; remove top-level timeoutMs."
|
|
119
|
+
: "Use electron.timeoutMs for this action; top-level timeoutMs applies only to browser CLI subprocess calls."
|
|
118
120
|
: compiledScript && params.timeoutMs !== undefined && params.timeoutMs > AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS
|
|
119
121
|
? `script timeoutMs must be ${AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS} or less.`
|
|
120
122
|
: undefined;
|
|
@@ -38,6 +38,10 @@ function getExplicitNavigationTarget(args) {
|
|
|
38
38
|
return positionals[0];
|
|
39
39
|
if (descriptor.commandInfo.command === "tab" && descriptor.commandInfo.subcommand === "new")
|
|
40
40
|
return positionals[1];
|
|
41
|
+
if (descriptor.commandInfo.command === "window" && descriptor.commandInfo.subcommand === "new")
|
|
42
|
+
return "about:blank";
|
|
43
|
+
if (descriptor.commandInfo.command === "diff" && descriptor.commandInfo.subcommand === "url")
|
|
44
|
+
return descriptor.upstreamCommandTokens[3];
|
|
41
45
|
return undefined;
|
|
42
46
|
}
|
|
43
47
|
function getResultingExplicitNavigationTarget(args, currentPageUrl) {
|
|
@@ -149,17 +153,11 @@ export function getResultingPageTargetState(options) {
|
|
|
149
153
|
|| isUnverifiedPageTransitionCommand(descriptor.commandInfo.command, descriptor.commandInfo.subcommand);
|
|
150
154
|
return { ...getResultingPageState({ ...state, args: options.args, trustedBatchTabSelection: false }), pageTargetMayHaveChanged };
|
|
151
155
|
}
|
|
152
|
-
const
|
|
153
|
-
if (batch.error)
|
|
154
|
-
return { pageTargetMayHaveChanged: true, pageUrlUnknown: true };
|
|
155
|
-
for (let index = 0; index < batch.steps.length; index += 1) {
|
|
156
|
-
const step = batch.steps[index];
|
|
157
|
-
const trustedBatchTabSelection = options.trustedFirstBatchTabSelection === true && index === 0;
|
|
156
|
+
for (const step of options.executedBatchSteps) {
|
|
158
157
|
const stepDescriptor = parseArgvDescriptor(step);
|
|
159
158
|
pageTargetMayHaveChanged ||= getExplicitNavigationTarget(step) !== undefined
|
|
160
|
-
||
|
|
161
|
-
|
|
162
|
-
state = getResultingPageState({ ...state, args: step, trustedBatchTabSelection });
|
|
159
|
+
|| isUnverifiedPageTransitionCommand(stepDescriptor.commandInfo.command, stepDescriptor.commandInfo.subcommand);
|
|
160
|
+
state = getResultingPageState({ ...state, args: step, trustedBatchTabSelection: false });
|
|
163
161
|
}
|
|
164
162
|
return { ...state, pageTargetMayHaveChanged };
|
|
165
163
|
}
|
|
@@ -189,6 +187,9 @@ export function getPageTargetValidationError(options) {
|
|
|
189
187
|
if (["close", "exit", "quit"].includes(command ?? ""))
|
|
190
188
|
return undefined;
|
|
191
189
|
if (command === "batch") {
|
|
190
|
+
if (descriptor.upstreamCommandTokens.slice(1).some((token) => token.startsWith("--bail="))) {
|
|
191
|
+
return "Use exact batch --bail for fail-fast, or omit it to continue after errors. --bail=<value> is a raw command upstream; stdin is ignored when raw batch arguments are present.";
|
|
192
|
+
}
|
|
192
193
|
const batch = getBatchCommandSteps(options.args, options.stdin);
|
|
193
194
|
if (batch.error)
|
|
194
195
|
return batch.error.startsWith("agent_browser batch stdin") || batch.error === NESTED_BATCH_ARGUMENT_MESSAGE
|
|
@@ -262,7 +263,6 @@ export function getExplicitSessionPageVerificationRequirement(options) {
|
|
|
262
263
|
pageUrlUnknown: true,
|
|
263
264
|
stdin: options.stdin,
|
|
264
265
|
allowUnverifiedPageTransitions: true,
|
|
265
|
-
trustedFirstBatchTabSelection: options.trustedFirstBatchTabSelection,
|
|
266
266
|
});
|
|
267
267
|
return validationError === UNVERIFIED_PAGE_MESSAGE || validationError === BATCH_UNVERIFIED_PAGE_MESSAGE || validationError === NON_BAIL_BATCH_NAVIGATION_MESSAGE
|
|
268
268
|
? UNVERIFIED_PAGE_MESSAGE
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export function isRecord(value) {
|
|
2
2
|
return typeof value === "object" && value !== null;
|
|
3
3
|
}
|
|
4
|
+
/** Upstream native element::parse_ref accepts @eN, ref=eN and bare eN. */
|
|
5
|
+
export function parseRefId(selector) {
|
|
6
|
+
const trimmed = selector.trim();
|
|
7
|
+
const prefixed = trimmed.startsWith("@") || trimmed.startsWith("ref=");
|
|
8
|
+
const candidate = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
|
|
9
|
+
return (prefixed ? /^e\d*$/ : /^e\d+$/).test(candidate) ? candidate : undefined;
|
|
10
|
+
}
|
|
4
11
|
export function parsePositiveInteger(rawValue) {
|
|
5
12
|
if (typeof rawValue !== "string")
|
|
6
13
|
return undefined;
|