pi-agent-browser-native 0.2.43 → 0.2.45
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 +37 -0
- package/README.md +26 -16
- package/docs/ARCHITECTURE.md +12 -10
- package/docs/COMMAND_REFERENCE.md +49 -27
- package/docs/ELECTRON.md +1 -1
- package/docs/RELEASE.md +16 -9
- package/docs/REQUIREMENTS.md +6 -3
- package/docs/SUPPORT_MATRIX.md +18 -14
- package/docs/TOOL_CONTRACT.md +87 -46
- package/docs/platform-smoke.md +15 -9
- package/extensions/agent-browser/index.ts +29 -445
- package/extensions/agent-browser/lib/bash-guard.ts +205 -0
- package/extensions/agent-browser/lib/electron/cdp.ts +69 -0
- package/extensions/agent-browser/lib/electron/cleanup.ts +5 -58
- package/extensions/agent-browser/lib/electron/discovery.ts +2 -9
- package/extensions/agent-browser/lib/electron/launch.ts +11 -65
- package/extensions/agent-browser/lib/electron/text.ts +13 -0
- package/extensions/agent-browser/lib/fs-utils.ts +18 -0
- package/extensions/agent-browser/lib/input-modes/job.ts +207 -21
- package/extensions/agent-browser/lib/input-modes/params.ts +17 -7
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +22 -2
- package/extensions/agent-browser/lib/input-modes/types.ts +5 -1
- package/extensions/agent-browser/lib/input-modes.ts +1 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +82 -11
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +153 -30
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +53 -2
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +1 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +751 -32
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +38 -7
- package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +0 -46
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +10 -1
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +28 -1
- package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +1 -6
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +15 -3
- package/extensions/agent-browser/lib/orchestration/output-file.ts +86 -0
- package/extensions/agent-browser/lib/pi-tool-rendering.ts +231 -0
- package/extensions/agent-browser/lib/playbook.ts +26 -26
- package/extensions/agent-browser/lib/process.ts +1 -1
- package/extensions/agent-browser/lib/prompt-policy.ts +1 -18
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +1 -4
- package/extensions/agent-browser/lib/results/artifact-state.ts +7 -3
- package/extensions/agent-browser/lib/results/contracts.ts +6 -2
- package/extensions/agent-browser/lib/results/envelope.ts +11 -2
- package/extensions/agent-browser/lib/results/network-routes.ts +7 -4
- package/extensions/agent-browser/lib/results/network.ts +7 -1
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +88 -20
- package/extensions/agent-browser/lib/results/presentation/batch.ts +84 -12
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +81 -26
- package/extensions/agent-browser/lib/results/presentation/errors.ts +13 -0
- package/extensions/agent-browser/lib/results/presentation/registry.ts +60 -0
- package/extensions/agent-browser/lib/results/presentation.ts +10 -1
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +16 -5
- package/extensions/agent-browser/lib/results/snapshot.ts +2 -0
- package/extensions/agent-browser/lib/runtime.ts +10 -1
- package/extensions/agent-browser/lib/session-page-state.ts +15 -6
- package/extensions/agent-browser/lib/web-search.ts +1 -1
- package/package.json +5 -5
- package/platform-smoke.config.mjs +15 -3
- package/scripts/doctor.mjs +70 -1
- package/scripts/platform-smoke/build-ubuntu-image.mjs +25 -0
- package/scripts/platform-smoke/crabbox-runner.mjs +62 -30
- package/scripts/platform-smoke/doctor.mjs +28 -11
- package/scripts/platform-smoke/linux-image/Dockerfile +3 -5
- package/scripts/platform-smoke/targets.mjs +60 -22
- package/scripts/platform-smoke.mjs +1 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/browser-action-model.ts +0 -154
|
@@ -7,6 +7,7 @@ import { getAllowedDomainsViolation, parseAllowedDomainsPolicyFromArgs } from ".
|
|
|
7
7
|
import {
|
|
8
8
|
analyzeNetworkSourceLookupResults,
|
|
9
9
|
analyzeQaPresetResults,
|
|
10
|
+
analyzeQaPresetTimeout,
|
|
10
11
|
analyzeSourceLookupResults,
|
|
11
12
|
buildQaCompactPassText,
|
|
12
13
|
extractQaPageContext,
|
|
@@ -78,6 +79,7 @@ import {
|
|
|
78
79
|
collectNavigationSummary,
|
|
79
80
|
collectOverlayBlockerDiagnostic,
|
|
80
81
|
collectQaAttachedTarget,
|
|
82
|
+
collectSnapshotOverlayBlockerDiagnostic,
|
|
81
83
|
collectRecordingDependencyWarning,
|
|
82
84
|
collectScrollPositionSnapshot,
|
|
83
85
|
collectSelectorTextVisibilityDiagnostics,
|
|
@@ -159,6 +161,15 @@ function isStreamEnableAlreadyEnabledNoop(options: { command: string | undefined
|
|
|
159
161
|
return message === "streaming is already enabled for this session" || message === "streaming is already enabled" || message === "stream already enabled";
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
function batchStartedManagedBrowser(data: unknown): boolean {
|
|
165
|
+
if (!Array.isArray(data)) return false;
|
|
166
|
+
return data.some((entry) => {
|
|
167
|
+
if (!isRecord(entry) || entry.success !== true || !Array.isArray(entry.command)) return false;
|
|
168
|
+
const command = typeof entry.command[0] === "string" ? entry.command[0] : undefined;
|
|
169
|
+
return command === "connect" || command === "goto" || command === "navigate" || isOpenNavigationCommand(command);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
162
173
|
function setNetworkRouteState(options: { routes?: NetworkRouteRecord[]; routesBySession: Map<string, NetworkRouteRecord[]>; sessionName: string | undefined }): Map<string, NetworkRouteRecord[]> {
|
|
163
174
|
if (!options.sessionName) return options.routesBySession;
|
|
164
175
|
const previousRoutes = options.routesBySession.get(options.sessionName);
|
|
@@ -351,11 +362,17 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
|
|
|
351
362
|
let selectorTextVisibilityDiagnostics: Awaited<ReturnType<typeof collectSelectorTextVisibilityDiagnostics>> = [];
|
|
352
363
|
let electronBroadGetTextScopeDiagnostics: ReturnType<typeof collectElectronBroadGetTextScopeDiagnostics> = [];
|
|
353
364
|
const timeoutPartialProgress = processResult.timedOut ? await collectTimeoutPartialProgress({ command: prepared.executionPlan.commandInfo.command, compiledJob: prepared.compiledJob, cwd, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin }) : undefined;
|
|
365
|
+
if (succeeded) {
|
|
366
|
+
const fillRefSnapshot = prepared.resolvedSemanticActionRefSnapshot ?? prepared.priorRefSnapshotState;
|
|
367
|
+
fillVerificationDiagnostic = await collectFillVerificationDiagnostic({ commandTokens: prepared.commandTokens, cwd, forceValueVerification: electronRecordForCommand !== undefined, refSnapshot: fillRefSnapshot, sessionName: prepared.executionPlan.sessionName, signal });
|
|
368
|
+
}
|
|
354
369
|
if (succeeded && electronRecordForCommand) {
|
|
355
|
-
fillVerificationDiagnostic = await collectFillVerificationDiagnostic({ commandTokens: prepared.commandTokens, cwd, sessionName: prepared.executionPlan.sessionName, signal });
|
|
356
370
|
electronRefFreshnessDiagnostic = buildElectronRefFreshnessDiagnostic({ command: prepared.executionPlan.commandInfo.command, commandTokens: prepared.commandTokens, record: electronRecordForCommand, sessionName: prepared.executionPlan.sessionName, stdin: prepared.runtimeToolStdin });
|
|
357
371
|
}
|
|
358
|
-
if (succeeded &&
|
|
372
|
+
if (succeeded && prepared.executionPlan.commandInfo.command === "snapshot") {
|
|
373
|
+
overlayBlockerDiagnostic = collectSnapshotOverlayBlockerDiagnostic(presentationEnvelope?.data);
|
|
374
|
+
}
|
|
375
|
+
if (succeeded && !overlayBlockerDiagnostic && !sessionTabCorrection && !aboutBlankSessionMismatch && !electronRecordForCommand && !clickDispatchDiagnostic) overlayBlockerDiagnostic = await collectOverlayBlockerDiagnostic({ command: prepared.executionPlan.commandInfo.command, cwd, data: presentationEnvelope?.data, navigationSummary, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName, signal });
|
|
359
376
|
if (succeeded) {
|
|
360
377
|
selectorTextVisibilityDiagnostics = await collectSelectorTextVisibilityDiagnostics({ commandInfo: prepared.executionPlan.commandInfo, commandTokens: prepared.commandTokens, cwd, data: presentationEnvelope?.data, sessionName: prepared.executionPlan.sessionName, signal });
|
|
361
378
|
if (electronRecordForCommand) electronBroadGetTextScopeDiagnostics = collectElectronBroadGetTextScopeDiagnostics({ commandInfo: prepared.executionPlan.commandInfo, commandTokens: prepared.commandTokens, currentTarget: currentSessionTabTarget, data: presentationEnvelope?.data, electronLaunchRecords, priorTarget: prepared.priorSessionTabTarget, sessionName: prepared.executionPlan.sessionName });
|
|
@@ -403,7 +420,9 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
|
|
|
403
420
|
? prepared.executionPlan.sessionName
|
|
404
421
|
: prepared.executionPlan.managedSessionName;
|
|
405
422
|
const policyBlockedFreshManagedSession = allowedDomainsViolation !== undefined && prepared.sessionMode === "fresh" && prepared.executionPlan.managedSessionName === prepared.executionPlan.sessionName;
|
|
406
|
-
const
|
|
423
|
+
const postLaunchBatchFailure = !succeeded && processSucceeded && parseSucceeded && prepared.sessionMode === "fresh" && prepared.executionPlan.commandInfo.command === "batch" && batchStartedManagedBrowser(presentationEnvelope?.data);
|
|
424
|
+
const postLaunchTimeoutWithPage = !succeeded && processResult.timedOut && prepared.sessionMode === "fresh" && prepared.executionPlan.commandInfo.command === "batch" && timeoutPartialProgress?.liveUrlRecovered === true;
|
|
425
|
+
const managedTransitionSucceeded = succeeded || policyBlockedFreshManagedSession || postLaunchBatchFailure || postLaunchTimeoutWithPage;
|
|
407
426
|
const managedSessionState = resolveManagedSessionState({ command: prepared.executionPlan.commandInfo.command, managedSessionName: managedCloseSessionName, priorActive: priorManagedSessionActive, priorSessionName: priorManagedSessionName, succeeded: managedTransitionSucceeded });
|
|
408
427
|
const replacedManagedSessionName = managedSessionState.replacedSessionName;
|
|
409
428
|
managedSessionActive = managedSessionState.active;
|
|
@@ -462,10 +481,16 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
|
|
|
462
481
|
}
|
|
463
482
|
const presentation = plainTextInspection ? { artifacts: undefined, batchFailure: undefined, batchSteps: undefined, content: [{ type: "text" as const, 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, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd, envelope: presentationEnvelope, errorText, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, sessionName: prepared.executionPlan.sessionName });
|
|
464
483
|
networkRoutesBySession = applyBatchNetworkRouteState({ data: presentationEnvelope?.data, routesBySession: networkRoutesBySession, sessionName: prepared.executionPlan.sessionName, succeeded });
|
|
465
|
-
if (presentation.
|
|
484
|
+
if (presentation.resultCategory === "failure" && succeeded) {
|
|
466
485
|
succeeded = false;
|
|
467
486
|
presentationEnvelope = { ...(presentationEnvelope ?? {}), error: presentation.summary, success: false };
|
|
468
487
|
}
|
|
488
|
+
if (scrollNoopDiagnostic) {
|
|
489
|
+
presentation.summary = "Scroll completed with no observed movement.";
|
|
490
|
+
if (isRecord(presentation.data)) presentation.data = { ...presentation.data, noMovement: true, scrolled: false };
|
|
491
|
+
if (presentation.content[0]?.type === "text") presentation.content[0] = { ...presentation.content[0], text: `Scroll completed with no observed movement.\n\n${presentation.content[0].text}` };
|
|
492
|
+
else presentation.content.unshift({ type: "text", text: "Scroll completed with no observed movement." });
|
|
493
|
+
}
|
|
469
494
|
if (parseFailureOutput.artifactManifest) { presentation.artifactManifest = parseFailureOutput.artifactManifest; presentation.artifactRetentionSummary = parseFailureOutput.artifactRetentionSummary; }
|
|
470
495
|
if (parseFailureOutput.fullOutputPath || parseFailureOutput.fullOutputUnavailable) {
|
|
471
496
|
const existingText = presentation.content[0]?.type === "text" ? presentation.content[0].text : "";
|
|
@@ -474,7 +499,9 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
|
|
|
474
499
|
presentation.content[0] = { type: "text", text: existingText.length > 0 ? `${existingText}\n\n${notice}` : notice };
|
|
475
500
|
}
|
|
476
501
|
if (presentation.artifactManifest) artifactManifest = presentation.artifactManifest;
|
|
477
|
-
const qaPreset = prepared.compiledQaPreset
|
|
502
|
+
const qaPreset = prepared.compiledQaPreset
|
|
503
|
+
? (processResult.timedOut ? analyzeQaPresetTimeout(prepared.compiledQaPreset) ?? analyzeQaPresetResults(presentationEnvelope?.data, prepared.compiledQaPreset) : analyzeQaPresetResults(presentationEnvelope?.data, prepared.compiledQaPreset))
|
|
504
|
+
: undefined;
|
|
478
505
|
let qaAttachedTarget = prepared.compiledQaPreset?.checks.attached
|
|
479
506
|
? await collectQaAttachedTarget({ currentTarget: currentSessionTabTarget ?? prepared.priorSessionTabTarget, cwd, sessionName: prepared.executionPlan.sessionName, signal })
|
|
480
507
|
: undefined;
|
|
@@ -508,9 +535,13 @@ export async function processBrowserOutput(input: ProcessBrowserOutputInput): Pr
|
|
|
508
535
|
presentation.content = [{ type: "text", text: compactText }, ...nonTextContent];
|
|
509
536
|
}
|
|
510
537
|
const qaAttachedTargetText = formatQaAttachedTargetText(qaAttachedTarget);
|
|
538
|
+
const qaAttachedDiagnosticsText = prepared.compiledQaPreset?.checks.attached && prepared.compiledQaPreset.checks.diagnosticsResetAtStart === false && (prepared.compiledQaPreset.checks.checkNetwork || prepared.compiledQaPreset.checks.checkConsole || prepared.compiledQaPreset.checks.checkErrors)
|
|
539
|
+
? "Attached diagnostics: existing upstream session console/network/error buffers were preserved; rows may include events from before qa.attached started."
|
|
540
|
+
: undefined;
|
|
541
|
+
const qaAttachedBannerText = [qaAttachedTargetText, qaAttachedDiagnosticsText].filter((part): part is string => typeof part === "string" && part.length > 0).join("\n");
|
|
511
542
|
const skipAttachedTargetBanner = qaPreset?.passed && prepared.compiledQaPreset?.checks.attached;
|
|
512
|
-
if (!skipAttachedTargetBanner &&
|
|
513
|
-
else if (!skipAttachedTargetBanner &&
|
|
543
|
+
if (!skipAttachedTargetBanner && qaAttachedBannerText && presentation.content[0]?.type === "text") presentation.content[0] = { ...presentation.content[0], text: `${qaAttachedBannerText}\n\n${presentation.content[0].text}` };
|
|
544
|
+
else if (!skipAttachedTargetBanner && qaAttachedBannerText) presentation.content.unshift({ type: "text", text: qaAttachedBannerText });
|
|
514
545
|
if (managedSessionOutcome && managedSessionOutcome.succeeded !== succeeded) managedSessionOutcome = { ...managedSessionOutcome, succeeded };
|
|
515
546
|
const evalNavigationSummary = navigationSummary ?? extractNavigationSummaryFromData(presentationEnvelope?.data);
|
|
516
547
|
const evalSessionTabUrl = prepared.executionPlan.sessionName ? sessionPageState.get(prepared.executionPlan.sessionName).tabTarget?.url : undefined;
|
|
@@ -4,17 +4,6 @@ import { isCloseCommand } from "../../command-taxonomy.js";
|
|
|
4
4
|
import { executableExistsOnPath } from "../../executable-path.js";
|
|
5
5
|
import type { SessionArtifactManifest } from "../../results/contracts.js";
|
|
6
6
|
import type { PromptPolicy, PromptRequestedArtifact } from "../../prompt-policy.js";
|
|
7
|
-
import type { SessionRefSnapshot } from "../../session-page-state.js";
|
|
8
|
-
import { findBlockedFinalizingAction, STOP_BOUNDARY_GUARD_SCOPE, type BrowserFinalizingAction } from "./browser-action-model.js";
|
|
9
|
-
|
|
10
|
-
export interface StopBoundaryViolation {
|
|
11
|
-
action: BrowserFinalizingAction;
|
|
12
|
-
command: string[];
|
|
13
|
-
message: string;
|
|
14
|
-
reason: "explicit-user-stop-boundary";
|
|
15
|
-
stepIndex?: number;
|
|
16
|
-
target?: string;
|
|
17
|
-
}
|
|
18
7
|
|
|
19
8
|
export interface RequestedArtifactCloseViolation {
|
|
20
9
|
message: string;
|
|
@@ -22,41 +11,6 @@ export interface RequestedArtifactCloseViolation {
|
|
|
22
11
|
reason: "requested-artifacts-missing-before-close";
|
|
23
12
|
}
|
|
24
13
|
|
|
25
|
-
function formatStopBoundaryActionPhrase(action: BrowserFinalizingAction): string {
|
|
26
|
-
if (action.kind === "keyboard-submit") return "keyboard submit (Enter/Return)";
|
|
27
|
-
return "click-like action";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function findStopBoundaryViolation(options: { commandTokens: string[]; promptPolicy: PromptPolicy; refSnapshot?: SessionRefSnapshot; stdin?: string }): StopBoundaryViolation | undefined {
|
|
31
|
-
if (!options.promptPolicy.stopBoundary) return undefined;
|
|
32
|
-
const blocked = findBlockedFinalizingAction({
|
|
33
|
-
commandTokens: options.commandTokens,
|
|
34
|
-
refSnapshot: options.refSnapshot,
|
|
35
|
-
stdin: options.stdin,
|
|
36
|
-
});
|
|
37
|
-
if (!blocked) return undefined;
|
|
38
|
-
const target = blocked.targetLabel;
|
|
39
|
-
const actionPhrase = formatStopBoundaryActionPhrase(blocked);
|
|
40
|
-
const scopeNote = `Best-effort guard scope covers ${STOP_BOUNDARY_GUARD_SCOPE.covered.join(", ")}; it does not block ${STOP_BOUNDARY_GUARD_SCOPE.excluded.join(", ")}.`;
|
|
41
|
-
if (blocked.stepIndex === undefined) {
|
|
42
|
-
return {
|
|
43
|
-
action: blocked,
|
|
44
|
-
command: blocked.command,
|
|
45
|
-
message: `Blocked likely final submit/order ${actionPhrase} (${target}) because the latest user prompt set an explicit stop boundary. Gather evidence on the current page instead of activating the final action. ${scopeNote}`,
|
|
46
|
-
reason: "explicit-user-stop-boundary",
|
|
47
|
-
target,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
action: blocked,
|
|
52
|
-
command: blocked.command,
|
|
53
|
-
message: `Blocked likely final submit/order ${actionPhrase} in batch step ${blocked.stepIndex + 1} (${target}) because the latest user prompt set an explicit stop boundary. Gather evidence on the current page instead of activating the final action. ${scopeNote}`,
|
|
54
|
-
reason: "explicit-user-stop-boundary",
|
|
55
|
-
stepIndex: blocked.stepIndex,
|
|
56
|
-
target,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
14
|
function resolveArtifactPath(cwd: string, path: string): string {
|
|
61
15
|
return isAbsolute(path) ? path : resolve(cwd, path);
|
|
62
16
|
}
|
|
@@ -157,7 +157,15 @@ function formatManagedSessionOutcomeRecoveryGuidance(outcome: ManagedSessionOutc
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
export function formatManagedSessionOutcomeText(outcome: ManagedSessionOutcome | undefined): string | undefined {
|
|
160
|
-
if (!outcome
|
|
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;
|
|
161
169
|
return [formatManagedSessionOutcomeHeadline(outcome), formatManagedSessionOutcomeRecoveryGuidance(outcome)].join("\n");
|
|
162
170
|
}
|
|
163
171
|
|
|
@@ -334,6 +342,7 @@ function isSafeSameSnapshotFormBatchStep(step: string[], refSnapshot: SessionRef
|
|
|
334
342
|
const roles = refIds.map((refId) => getSnapshotRefRole(refSnapshot, refId));
|
|
335
343
|
if (roles.some((role) => role === undefined)) return false;
|
|
336
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");
|
|
337
346
|
if (command === "select") return roles.every((role) => role === "combobox");
|
|
338
347
|
return false;
|
|
339
348
|
}
|
|
@@ -150,9 +150,17 @@ export interface ClickDispatchProbe {
|
|
|
150
150
|
target: ClickDispatchProbeTarget;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
export interface ClickDispatchScrollContainerDiagnostic {
|
|
154
|
+
selector?: string;
|
|
155
|
+
summary: string;
|
|
156
|
+
targetOutsideContainer?: boolean;
|
|
157
|
+
targetOutsideViewport?: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
153
160
|
export interface ClickDispatchDiagnostic {
|
|
154
161
|
nativeEventCount: number;
|
|
155
162
|
reason: "native-click-produced-no-target-dom-event";
|
|
163
|
+
scrollContainer?: ClickDispatchScrollContainerDiagnostic;
|
|
156
164
|
status: "no-native-event-observed";
|
|
157
165
|
summary: string;
|
|
158
166
|
target: ClickDispatchProbeTarget;
|
|
@@ -202,16 +210,32 @@ export interface TimeoutArtifactEvidence {
|
|
|
202
210
|
exists: boolean;
|
|
203
211
|
path: string;
|
|
204
212
|
sizeBytes?: number;
|
|
213
|
+
state: "missing" | "verified";
|
|
205
214
|
stepIndex: number;
|
|
206
215
|
}
|
|
207
216
|
|
|
217
|
+
export type TimeoutProgressStepStatus = "completed" | "failed" | "pending" | "unknown";
|
|
218
|
+
|
|
219
|
+
export interface TimeoutProgressStep {
|
|
220
|
+
args: string[];
|
|
221
|
+
generatedFrom?: string;
|
|
222
|
+
index: number;
|
|
223
|
+
reason?: string;
|
|
224
|
+
retry?: { args: string[] };
|
|
225
|
+
status: TimeoutProgressStepStatus;
|
|
226
|
+
}
|
|
227
|
+
|
|
208
228
|
export interface TimeoutPartialProgress {
|
|
209
229
|
artifacts: TimeoutArtifactEvidence[];
|
|
210
230
|
currentPage?: {
|
|
231
|
+
source?: "live" | "planned";
|
|
211
232
|
title?: string;
|
|
212
233
|
url?: string;
|
|
213
234
|
};
|
|
214
|
-
|
|
235
|
+
liveUrlRecovered?: boolean;
|
|
236
|
+
openedButPostOpenTimedOut?: boolean;
|
|
237
|
+
retryStep?: TimeoutProgressStep;
|
|
238
|
+
steps?: TimeoutProgressStep[];
|
|
215
239
|
summary: string;
|
|
216
240
|
}
|
|
217
241
|
|
|
@@ -374,7 +398,9 @@ export interface ElectronPostCommandHealthDiagnostic {
|
|
|
374
398
|
export interface FillVerificationDiagnostic {
|
|
375
399
|
actual?: string;
|
|
376
400
|
expected: string;
|
|
401
|
+
method: "text" | "value";
|
|
377
402
|
nextActionIds: string[];
|
|
403
|
+
reason: "contenteditable-fill-mismatch" | "value-fill-mismatch";
|
|
378
404
|
selector: string;
|
|
379
405
|
status: "mismatch";
|
|
380
406
|
summary: string;
|
|
@@ -411,6 +437,7 @@ export interface PreparedBrowserRun {
|
|
|
411
437
|
priorSessionTabTarget?: SessionTabTarget;
|
|
412
438
|
processArgs: string[];
|
|
413
439
|
processStdin?: string;
|
|
440
|
+
processTimeoutMs?: number;
|
|
414
441
|
redactedArgs: string[];
|
|
415
442
|
redactedCompiledElectron?: CompiledAgentBrowserElectron;
|
|
416
443
|
redactedCompiledJob?: CompiledAgentBrowserJob;
|
|
@@ -9,6 +9,7 @@ import type { ChildProcess } from "node:child_process";
|
|
|
9
9
|
import { cleanupElectronLaunchResources, inspectElectronLaunchStatus, type ElectronCleanupResult, type ElectronLaunchStatus } from "../../electron/cleanup.js";
|
|
10
10
|
import { discoverElectronApps, type ElectronDiscoveryResult } from "../../electron/discovery.js";
|
|
11
11
|
import type { ElectronCdpTarget, ElectronLaunchRecord } from "../../electron/launch.js";
|
|
12
|
+
import { boundElectronProbeString } from "../../electron/text.js";
|
|
12
13
|
import type { CompiledAgentBrowserElectron } from "../../input-modes.js";
|
|
13
14
|
import { isRecord } from "../../parsing.js";
|
|
14
15
|
import { buildAgentBrowserNextActions, buildAgentBrowserResultCategoryDetails } from "../../results.js";
|
|
@@ -368,12 +369,6 @@ const ELECTRON_FOCUSED_ELEMENT_EVAL = `(() => {
|
|
|
368
369
|
return { focusedElement: describeElement(document.activeElement) };
|
|
369
370
|
})()`;
|
|
370
371
|
|
|
371
|
-
function boundElectronProbeString(value: string | undefined, maxLength = 240): string | undefined {
|
|
372
|
-
const trimmed = value?.trim();
|
|
373
|
-
if (!trimmed) return undefined;
|
|
374
|
-
return trimmed.length > maxLength ? `${trimmed.slice(0, Math.max(0, maxLength - 3))}...` : trimmed;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
372
|
function getTrimmedString(value: unknown): string | undefined {
|
|
378
373
|
return typeof value === "string" ? boundElectronProbeString(value) : undefined;
|
|
379
374
|
}
|
|
@@ -23,11 +23,13 @@ export interface AgentBrowserExecuteParams {
|
|
|
23
23
|
electron?: unknown;
|
|
24
24
|
job?: unknown;
|
|
25
25
|
networkSourceLookup?: unknown;
|
|
26
|
+
outputPath?: string;
|
|
26
27
|
qa?: unknown;
|
|
27
28
|
semanticAction?: unknown;
|
|
28
29
|
sessionMode?: "auto" | "fresh";
|
|
29
30
|
sourceLookup?: unknown;
|
|
30
31
|
stdin?: string;
|
|
32
|
+
timeoutMs?: number;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export type ResolvedAgentBrowserInputKind = "args" | "electron" | "job" | "networkSourceLookup" | "qa" | "semanticAction" | "sourceLookup";
|
|
@@ -175,11 +177,11 @@ function normalizeExplicitEvalStdinArgs(args: string[], stdin: string | undefine
|
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
export function resolveAgentBrowserInput(options: {
|
|
178
|
-
|
|
180
|
+
getBatchPreflightValidationError: (args: string[], stdin: string | undefined) => string | undefined;
|
|
179
181
|
managedSessionActive: boolean;
|
|
180
182
|
params: AgentBrowserExecuteParams;
|
|
181
183
|
}): ResolvedAgentBrowserInput {
|
|
182
|
-
const {
|
|
184
|
+
const { getBatchPreflightValidationError, managedSessionActive, params } = options;
|
|
183
185
|
const semanticActionResult = params.semanticAction === undefined ? {} : compileAgentBrowserSemanticAction(params.semanticAction);
|
|
184
186
|
const jobResult = params.job === undefined ? {} : compileAgentBrowserJob(params.job);
|
|
185
187
|
const qaResult = params.qa === undefined ? {} : compileAgentBrowserQaPreset(params.qa);
|
|
@@ -219,6 +221,14 @@ export function resolveAgentBrowserInput(options: {
|
|
|
219
221
|
? "Do not provide stdin with electron; electron mode is host-only or manages its own input."
|
|
220
222
|
: undefined
|
|
221
223
|
: undefined;
|
|
224
|
+
const outputPathError = params.outputPath !== undefined && (typeof params.outputPath !== "string" || params.outputPath.trim().length === 0)
|
|
225
|
+
? "outputPath must be a non-empty string when provided."
|
|
226
|
+
: undefined;
|
|
227
|
+
const timeoutMsError = params.timeoutMs !== undefined && (typeof params.timeoutMs !== "number" || !Number.isSafeInteger(params.timeoutMs) || params.timeoutMs <= 0)
|
|
228
|
+
? "timeoutMs must be a positive integer when provided."
|
|
229
|
+
: compiledElectron && params.timeoutMs !== undefined
|
|
230
|
+
? "Use electron.timeoutMs for electron actions; top-level timeoutMs applies only to browser CLI subprocess calls."
|
|
231
|
+
: undefined;
|
|
222
232
|
const attachedQaSessionError = compiledQaPreset?.checks.attached
|
|
223
233
|
? params.sessionMode === "fresh"
|
|
224
234
|
? "qa.attached cannot be used with sessionMode=fresh; attach or launch a session first, then run qa.attached with the current session."
|
|
@@ -234,8 +244,10 @@ export function resolveAgentBrowserInput(options: {
|
|
|
234
244
|
?? electronResult.error
|
|
235
245
|
?? inputModeError
|
|
236
246
|
?? generatedStdinError
|
|
247
|
+
?? outputPathError
|
|
248
|
+
?? timeoutMsError
|
|
237
249
|
?? attachedQaSessionError
|
|
238
|
-
?? (compiledElectron ? undefined : validateToolArgs(toolArgs) ??
|
|
250
|
+
?? (compiledElectron ? undefined : validateToolArgs(toolArgs) ?? getBatchPreflightValidationError(toolArgs, toolStdin));
|
|
239
251
|
const redactedCompiledJob = redactCompiledJob(compiledJob);
|
|
240
252
|
const redactedCompiledSemanticAction = compiledSemanticAction
|
|
241
253
|
? { ...compiledSemanticAction, args: redactInvocationArgs(compiledSemanticAction.args) }
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { isRecord } from "../parsing.js";
|
|
5
|
+
import type { AgentBrowserToolResult } from "./browser-run/types.js";
|
|
6
|
+
|
|
7
|
+
export interface AgentBrowserOutputFileDetails {
|
|
8
|
+
absolutePath: string;
|
|
9
|
+
bytes?: number;
|
|
10
|
+
error?: string;
|
|
11
|
+
path: string;
|
|
12
|
+
source: "content.text" | "details.data";
|
|
13
|
+
status: "failed" | "saved";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeRequestedOutputPath(path: string): string {
|
|
17
|
+
return path.startsWith("@") ? path.slice(1) : path;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getTextContent(result: AgentBrowserToolResult): string {
|
|
21
|
+
return result.content
|
|
22
|
+
?.filter((item): item is { text: string; type: "text" } => item.type === "text")
|
|
23
|
+
.map((item) => item.text)
|
|
24
|
+
.join("\n\n") ?? "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getOutputPayload(result: AgentBrowserToolResult): { source: AgentBrowserOutputFileDetails["source"]; value: unknown } {
|
|
28
|
+
const details = isRecord(result.details) ? result.details : undefined;
|
|
29
|
+
if (details && details.data !== undefined) return { source: "details.data", value: details.data };
|
|
30
|
+
return { source: "content.text", value: getTextContent(result) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function serializeOutputPayload(value: unknown): string {
|
|
34
|
+
return typeof value === "string" ? value : `${JSON.stringify(value, null, 2)}\n`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function appendOutputFileNotice(result: AgentBrowserToolResult, message: string): AgentBrowserToolResult["content"] {
|
|
38
|
+
const content = [...(result.content ?? [])] as AgentBrowserToolResult["content"];
|
|
39
|
+
if (content[0]?.type === "text") {
|
|
40
|
+
content[0] = { ...content[0], text: `${content[0].text}\n\n${message}` };
|
|
41
|
+
return content;
|
|
42
|
+
}
|
|
43
|
+
return [{ type: "text", text: message }, ...content];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function applyAgentBrowserOutputPath(options: {
|
|
47
|
+
cwd: string;
|
|
48
|
+
outputPath?: string;
|
|
49
|
+
preserveTextContent?: boolean;
|
|
50
|
+
result: AgentBrowserToolResult;
|
|
51
|
+
}): Promise<AgentBrowserToolResult> {
|
|
52
|
+
if (!options.outputPath) return options.result;
|
|
53
|
+
if (options.result.isError || (isRecord(options.result.details) && options.result.details.resultCategory === "failure")) return options.result;
|
|
54
|
+
const requestedPath = normalizeRequestedOutputPath(options.outputPath);
|
|
55
|
+
const absolutePath = isAbsolute(requestedPath) ? requestedPath : resolve(options.cwd, requestedPath);
|
|
56
|
+
const payload = getOutputPayload(options.result);
|
|
57
|
+
try {
|
|
58
|
+
const serialized = serializeOutputPayload(payload.value);
|
|
59
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
60
|
+
await writeFile(absolutePath, serialized, "utf8");
|
|
61
|
+
const bytes = Buffer.byteLength(serialized, "utf8");
|
|
62
|
+
const outputFile: AgentBrowserOutputFileDetails = { absolutePath, bytes, path: requestedPath, source: payload.source, status: "saved" };
|
|
63
|
+
const details = isRecord(options.result.details) ? { ...options.result.details, outputFile } : { outputFile };
|
|
64
|
+
return {
|
|
65
|
+
...options.result,
|
|
66
|
+
content: options.preserveTextContent ? options.result.content : appendOutputFileNotice(options.result, `Output file: ${requestedPath} (${bytes} bytes from ${payload.source}).`),
|
|
67
|
+
details,
|
|
68
|
+
};
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
71
|
+
const outputFile: AgentBrowserOutputFileDetails = { absolutePath, error: message, path: requestedPath, source: payload.source, status: "failed" };
|
|
72
|
+
const details = isRecord(options.result.details)
|
|
73
|
+
? (() => {
|
|
74
|
+
const rest = { ...options.result.details };
|
|
75
|
+
delete rest.successCategory;
|
|
76
|
+
return { ...rest, failureCategory: rest.failureCategory ?? "upstream-error", outputFile, resultCategory: "failure" };
|
|
77
|
+
})()
|
|
78
|
+
: { failureCategory: "upstream-error", outputFile, resultCategory: "failure" };
|
|
79
|
+
return {
|
|
80
|
+
...options.result,
|
|
81
|
+
content: options.preserveTextContent ? options.result.content : appendOutputFileNotice(options.result, `Output file failed: ${requestedPath} (${message}).`),
|
|
82
|
+
details,
|
|
83
|
+
isError: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { AgentToolResult, Theme, ToolResultEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { highlightCode, keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
compileAgentBrowserElectron,
|
|
7
|
+
compileAgentBrowserJob,
|
|
8
|
+
compileAgentBrowserNetworkSourceLookup,
|
|
9
|
+
compileAgentBrowserQaPreset,
|
|
10
|
+
compileAgentBrowserSemanticAction,
|
|
11
|
+
compileAgentBrowserSourceLookup,
|
|
12
|
+
} from "./input-modes.js";
|
|
13
|
+
import { isRecord } from "./parsing.js";
|
|
14
|
+
import { redactInvocationArgs } from "./runtime.js";
|
|
15
|
+
|
|
16
|
+
const TUI_INVOCATION_PREVIEW_MAX_CHARS = 160;
|
|
17
|
+
const TUI_COLLAPSED_OUTPUT_MAX_LINES = 12;
|
|
18
|
+
const ANSI_CONTROL_SEQUENCE_PATTERN = /\x1B(?:\][^\x07\x1B]*(?:\x07|\x1B\\)|\[[0-?]*[ -/]*[@-~]|P[^\x1B]*(?:\x1B\\)|_[^\x1B]*(?:\x1B\\)|\^[^\x1B]*(?:\x1B\\)|[@-Z\\-_])/g;
|
|
19
|
+
const UNSAFE_DISPLAY_CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/g;
|
|
20
|
+
|
|
21
|
+
function sanitizeDisplayText(value: string): string {
|
|
22
|
+
return value
|
|
23
|
+
.replace(ANSI_CONTROL_SEQUENCE_PATTERN, "")
|
|
24
|
+
.replace(/\r/g, "")
|
|
25
|
+
.replace(UNSAFE_DISPLAY_CONTROL_PATTERN, "�");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function replaceTabsForDisplay(value: string): string {
|
|
29
|
+
return value.replaceAll("\t", " ");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function trimTrailingBlankLines(lines: string[]): string[] {
|
|
33
|
+
let end = lines.length;
|
|
34
|
+
while (end > 0 && lines[end - 1].trim().length === 0) {
|
|
35
|
+
end -= 1;
|
|
36
|
+
}
|
|
37
|
+
return lines.slice(0, end);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isJsonDocumentText(value: string): boolean {
|
|
41
|
+
const trimmed = value.trim();
|
|
42
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return false;
|
|
43
|
+
try {
|
|
44
|
+
JSON.parse(trimmed);
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getPrimaryTextContent(result: AgentToolResult<unknown>): string {
|
|
52
|
+
const textContent = result.content.find((item) => item.type === "text");
|
|
53
|
+
return textContent?.type === "text" ? textContent.text : "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function colorizeToolOutputLines(outputText: string, theme: Theme, isError: boolean): string[] {
|
|
57
|
+
const normalizedLines = trimTrailingBlankLines(replaceTabsForDisplay(sanitizeDisplayText(outputText)).split("\n"));
|
|
58
|
+
const normalizedText = normalizedLines.join("\n");
|
|
59
|
+
if (normalizedText.length === 0) return [];
|
|
60
|
+
if (isJsonDocumentText(normalizedText)) {
|
|
61
|
+
return highlightCode(normalizedText, "json");
|
|
62
|
+
}
|
|
63
|
+
return normalizedLines.map((line) => {
|
|
64
|
+
if (line.length === 0) {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
return isError ? theme.fg("error", line) : theme.fg("toolOutput", line);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatExpandHint(theme: Theme): string {
|
|
72
|
+
try {
|
|
73
|
+
return keyHint("app.tools.expand", "to expand");
|
|
74
|
+
} catch {
|
|
75
|
+
return `${theme.fg("dim", "ctrl+o")} ${theme.fg("muted", "to expand")}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatVisualTruncationNotice(remainingLines: number, totalLines: number, theme: Theme, width: number): string {
|
|
80
|
+
const notice = `${theme.fg("muted", `... (${remainingLines} more lines, ${totalLines} total, `)}${formatExpandHint(theme)}${theme.fg("muted", ")")}`;
|
|
81
|
+
return truncateToWidth(notice, Math.max(0, width));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function formatAgentBrowserRenderCall(args: unknown, theme: Theme): string {
|
|
85
|
+
const input = isRecord(args) ? args : {};
|
|
86
|
+
const semanticAction = compileAgentBrowserSemanticAction(input.semanticAction);
|
|
87
|
+
const job = compileAgentBrowserJob(input.job);
|
|
88
|
+
const qa = compileAgentBrowserQaPreset(input.qa);
|
|
89
|
+
const sourceLookup = compileAgentBrowserSourceLookup(input.sourceLookup);
|
|
90
|
+
const networkSourceLookup = compileAgentBrowserNetworkSourceLookup(input.networkSourceLookup);
|
|
91
|
+
const electron = compileAgentBrowserElectron(input.electron);
|
|
92
|
+
const generatedBatch = networkSourceLookup.compiled ?? sourceLookup.compiled ?? job.compiled ?? qa.compiled;
|
|
93
|
+
const rawArgs = Array.isArray(input.args)
|
|
94
|
+
? input.args.filter((value): value is string => typeof value === "string")
|
|
95
|
+
: electron.compiled
|
|
96
|
+
? ["electron", electron.compiled.action]
|
|
97
|
+
: (semanticAction.compiled?.args ?? generatedBatch?.args ?? []);
|
|
98
|
+
const redactedArgs = redactInvocationArgs(rawArgs);
|
|
99
|
+
const invocation = sanitizeDisplayText(redactedArgs.join(" ")).replace(/\s+/g, " ").trim();
|
|
100
|
+
const invocationPreview =
|
|
101
|
+
invocation.length > TUI_INVOCATION_PREVIEW_MAX_CHARS
|
|
102
|
+
? `${invocation.slice(0, TUI_INVOCATION_PREVIEW_MAX_CHARS - 3)}...`
|
|
103
|
+
: invocation;
|
|
104
|
+
let text = theme.fg("toolTitle", theme.bold("agent_browser"));
|
|
105
|
+
if (invocationPreview.length > 0) {
|
|
106
|
+
text += ` ${theme.fg("accent", invocationPreview)}`;
|
|
107
|
+
}
|
|
108
|
+
if (input.sessionMode === "fresh") {
|
|
109
|
+
text += theme.fg("dim", " sessionMode=fresh");
|
|
110
|
+
}
|
|
111
|
+
if (typeof input.stdin === "string") {
|
|
112
|
+
text += theme.fg("dim", " + stdin");
|
|
113
|
+
}
|
|
114
|
+
return text;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function formatAgentBrowserRenderResult(
|
|
118
|
+
result: AgentToolResult<unknown>,
|
|
119
|
+
options: { expanded: boolean; isPartial: boolean },
|
|
120
|
+
theme: Theme,
|
|
121
|
+
isError: boolean,
|
|
122
|
+
): string {
|
|
123
|
+
if (options.isPartial) {
|
|
124
|
+
return theme.fg("warning", "Running agent-browser...");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const outputText = getPrimaryTextContent(result);
|
|
128
|
+
const outputLines = colorizeToolOutputLines(outputText, theme, isError);
|
|
129
|
+
if (outputLines.length === 0) {
|
|
130
|
+
const details = isRecord(result.details) ? result.details : undefined;
|
|
131
|
+
const rawSummary = typeof details?.summary === "string" ? details.summary : isError ? "agent-browser failed" : "Done";
|
|
132
|
+
const sanitizedSummary = sanitizeDisplayText(rawSummary).trim();
|
|
133
|
+
const summary = sanitizedSummary.length > 0 ? sanitizedSummary : isError ? "agent-browser failed" : "Done";
|
|
134
|
+
return isError ? theme.fg("error", summary) : theme.fg("success", summary);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return `\n${outputLines.join("\n")}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatModelVisibleFailureCategoryNotice(details: unknown): string | undefined {
|
|
141
|
+
if (!isRecord(details) || details.resultCategory !== "failure") return undefined;
|
|
142
|
+
const failureCategory = typeof details.failureCategory === "string" && details.failureCategory.length > 0
|
|
143
|
+
? details.failureCategory
|
|
144
|
+
: undefined;
|
|
145
|
+
return `Result category: failure${failureCategory ? `; failureCategory: ${failureCategory}` : ""}; Pi tool isError: true.`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
type AgentBrowserToolContent = AgentToolResult<unknown>["content"];
|
|
149
|
+
type AgentBrowserToolContentItem = AgentBrowserToolContent[number];
|
|
150
|
+
|
|
151
|
+
export type AgentBrowserToolResultPatch = {
|
|
152
|
+
content?: AgentBrowserToolContent;
|
|
153
|
+
isError?: boolean;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
function agentBrowserToolResultRequestedJson(event: ToolResultEvent): boolean {
|
|
157
|
+
const details = isRecord(event.details) ? event.details : undefined;
|
|
158
|
+
const detailArgs = Array.isArray(details?.args) ? details.args : undefined;
|
|
159
|
+
const inputArgs = isRecord(event.input) && Array.isArray(event.input.args) ? event.input.args : undefined;
|
|
160
|
+
return detailArgs?.includes("--json") === true || inputArgs?.includes("--json") === true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function agentBrowserToolResultHasParseableJsonContent(content: AgentBrowserToolContent): boolean {
|
|
164
|
+
return content.some((item) => {
|
|
165
|
+
if (item.type !== "text" || typeof item.text !== "string") return false;
|
|
166
|
+
const text = item.text.trim();
|
|
167
|
+
if (text.length === 0) return false;
|
|
168
|
+
try {
|
|
169
|
+
JSON.parse(text);
|
|
170
|
+
return true;
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function appendModelVisibleFailureCategoryNotice(content: AgentBrowserToolContent, notice: string): AgentBrowserToolContent | undefined {
|
|
178
|
+
const noticeContent: AgentBrowserToolContentItem = { type: "text", text: notice };
|
|
179
|
+
const textIndex = content.findIndex((item) => item.type === "text" && typeof item.text === "string");
|
|
180
|
+
if (textIndex === -1) return [noticeContent, ...content];
|
|
181
|
+
const textItem = content[textIndex];
|
|
182
|
+
if (textItem.type !== "text" || typeof textItem.text !== "string" || textItem.text.includes(notice)) return undefined;
|
|
183
|
+
return content.map((item, index) => index === textIndex
|
|
184
|
+
? { ...item, text: `${textItem.text}\n\n${notice}` }
|
|
185
|
+
: item);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function buildAgentBrowserToolResultPatch(event: ToolResultEvent): AgentBrowserToolResultPatch | undefined {
|
|
189
|
+
if (event.toolName !== "agent_browser") return undefined;
|
|
190
|
+
const preservesParseableJson = agentBrowserToolResultRequestedJson(event) && agentBrowserToolResultHasParseableJsonContent(event.content);
|
|
191
|
+
const notice = preservesParseableJson ? undefined : formatModelVisibleFailureCategoryNotice(event.details);
|
|
192
|
+
const content = notice ? appendModelVisibleFailureCategoryNotice(event.content, notice) : undefined;
|
|
193
|
+
const shouldMarkError = isRecord(event.details) && event.details.resultCategory === "failure" && event.isError !== true;
|
|
194
|
+
if (!shouldMarkError && !content) return undefined;
|
|
195
|
+
return {
|
|
196
|
+
...(content ? { content } : {}),
|
|
197
|
+
...(shouldMarkError ? { isError: true } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export class AgentBrowserResultComponent {
|
|
202
|
+
private expanded = false;
|
|
203
|
+
private theme: Theme | undefined;
|
|
204
|
+
private readonly text = new Text("", 0, 0);
|
|
205
|
+
|
|
206
|
+
setState(value: string, expanded: boolean, theme: Theme): void {
|
|
207
|
+
this.text.setText(value);
|
|
208
|
+
this.expanded = expanded;
|
|
209
|
+
this.theme = theme;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
render(width: number): string[] {
|
|
213
|
+
const lines = this.text.render(width);
|
|
214
|
+
if (this.expanded || lines.length <= TUI_COLLAPSED_OUTPUT_MAX_LINES) {
|
|
215
|
+
return lines;
|
|
216
|
+
}
|
|
217
|
+
const theme = this.theme;
|
|
218
|
+
if (!theme) {
|
|
219
|
+
return lines.slice(0, TUI_COLLAPSED_OUTPUT_MAX_LINES);
|
|
220
|
+
}
|
|
221
|
+
const hiddenLineCount = lines.length - TUI_COLLAPSED_OUTPUT_MAX_LINES;
|
|
222
|
+
return [
|
|
223
|
+
...lines.slice(0, TUI_COLLAPSED_OUTPUT_MAX_LINES),
|
|
224
|
+
formatVisualTruncationNotice(hiddenLineCount, lines.length, theme, width),
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
invalidate(): void {
|
|
229
|
+
this.text.invalidate();
|
|
230
|
+
}
|
|
231
|
+
}
|