pi-agent-browser-native 0.2.48 → 0.2.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +16 -6
- package/dist/extensions/agent-browser/index.js +785 -0
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +71 -0
- package/dist/extensions/agent-browser/lib/argv-grammar.js +121 -0
- package/dist/extensions/agent-browser/lib/bash-guard.js +190 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +85 -0
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +302 -0
- package/dist/extensions/agent-browser/lib/config-policy.js +686 -0
- package/dist/extensions/agent-browser/lib/config.js +122 -0
- package/dist/extensions/agent-browser/lib/electron/cdp.js +51 -0
- package/dist/extensions/agent-browser/lib/electron/cleanup.js +212 -0
- package/dist/extensions/agent-browser/lib/electron/discovery.js +633 -0
- package/dist/extensions/agent-browser/lib/electron/launch.js +351 -0
- package/{extensions/agent-browser/lib/electron/text.ts → dist/extensions/agent-browser/lib/electron/text.js} +5 -5
- package/dist/extensions/agent-browser/lib/executable-path.js +20 -0
- package/dist/extensions/agent-browser/lib/fs-utils.js +18 -0
- package/dist/extensions/agent-browser/lib/input-modes/electron.js +165 -0
- package/dist/extensions/agent-browser/lib/input-modes/job.js +519 -0
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +440 -0
- package/dist/extensions/agent-browser/lib/input-modes/params.js +164 -0
- package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +119 -0
- package/dist/extensions/agent-browser/lib/input-modes/shared.js +42 -0
- package/dist/extensions/agent-browser/lib/input-modes/types.js +21 -0
- package/dist/extensions/agent-browser/lib/input-modes.js +10 -0
- package/dist/extensions/agent-browser/lib/json-schema.js +58 -0
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +59 -0
- package/dist/extensions/agent-browser/lib/navigation-policy.js +83 -0
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +62 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +39 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +276 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +909 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +443 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +47 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +141 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +108 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +112 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +158 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +54 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +762 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +491 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.js +40 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +5 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +731 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/types.js +1 -0
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +718 -0
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +247 -0
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +68 -0
- package/{extensions/agent-browser/lib/parsing.ts → dist/extensions/agent-browser/lib/parsing.js} +12 -11
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +241 -0
- package/dist/extensions/agent-browser/lib/playbook.js +121 -0
- package/dist/extensions/agent-browser/lib/process.js +448 -0
- package/dist/extensions/agent-browser/lib/prompt-policy.js +91 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +220 -0
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +111 -0
- package/{extensions/agent-browser/lib/results/artifact-state.ts → dist/extensions/agent-browser/lib/results/artifact-state.js} +4 -8
- package/dist/extensions/agent-browser/lib/results/categories.js +76 -0
- package/dist/extensions/agent-browser/lib/results/confirmation.js +63 -0
- package/dist/extensions/agent-browser/lib/results/contracts.js +8 -0
- package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +74 -0
- package/dist/extensions/agent-browser/lib/results/envelope.js +166 -0
- package/dist/extensions/agent-browser/lib/results/network-routes.js +92 -0
- package/dist/extensions/agent-browser/lib/results/network.js +73 -0
- package/dist/extensions/agent-browser/lib/results/next-actions.js +72 -0
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +515 -0
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +397 -0
- package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +55 -0
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +46 -0
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +24 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +960 -0
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +205 -0
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +134 -0
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +159 -0
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +216 -0
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +104 -0
- package/dist/extensions/agent-browser/lib/results/presentation/skills.js +152 -0
- package/dist/extensions/agent-browser/lib/results/presentation.js +177 -0
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +107 -0
- package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +50 -0
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +225 -0
- package/{extensions/agent-browser/lib/results/shared.ts → dist/extensions/agent-browser/lib/results/shared.js} +0 -1
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +208 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +78 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +331 -0
- package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +40 -0
- package/dist/extensions/agent-browser/lib/results/snapshot.js +264 -0
- package/dist/extensions/agent-browser/lib/results/text.js +40 -0
- package/{extensions/agent-browser/lib/results.ts → dist/extensions/agent-browser/lib/results.js} +2 -32
- package/dist/extensions/agent-browser/lib/runtime.js +816 -0
- package/dist/extensions/agent-browser/lib/session-page-state.js +411 -0
- package/dist/extensions/agent-browser/lib/string-enum-schema.js +13 -0
- package/dist/extensions/agent-browser/lib/temp.js +498 -0
- package/dist/extensions/agent-browser/lib/web-search.js +562 -0
- package/docs/RELEASE.md +22 -11
- package/docs/SUPPORT_MATRIX.md +4 -3
- package/package.json +9 -5
- package/scripts/config.mjs +8 -2
- package/scripts/doctor.mjs +8 -7
- package/extensions/agent-browser/index.ts +0 -961
- package/extensions/agent-browser/lib/argv-descriptor.ts +0 -90
- package/extensions/agent-browser/lib/argv-grammar.ts +0 -128
- package/extensions/agent-browser/lib/bash-guard.ts +0 -205
- package/extensions/agent-browser/lib/command-policy.ts +0 -71
- package/extensions/agent-browser/lib/command-taxonomy.ts +0 -336
- package/extensions/agent-browser/lib/config-policy.js +0 -690
- package/extensions/agent-browser/lib/config.ts +0 -211
- package/extensions/agent-browser/lib/electron/cdp.ts +0 -69
- package/extensions/agent-browser/lib/electron/cleanup.ts +0 -235
- package/extensions/agent-browser/lib/electron/discovery.ts +0 -710
- package/extensions/agent-browser/lib/electron/launch.ts +0 -499
- package/extensions/agent-browser/lib/executable-path.ts +0 -19
- package/extensions/agent-browser/lib/fs-utils.ts +0 -18
- package/extensions/agent-browser/lib/input-modes/electron.ts +0 -170
- package/extensions/agent-browser/lib/input-modes/job.ts +0 -527
- package/extensions/agent-browser/lib/input-modes/lookups.ts +0 -447
- package/extensions/agent-browser/lib/input-modes/params.ts +0 -205
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +0 -127
- package/extensions/agent-browser/lib/input-modes/shared.ts +0 -46
- package/extensions/agent-browser/lib/input-modes/types.ts +0 -225
- package/extensions/agent-browser/lib/input-modes.ts +0 -45
- package/extensions/agent-browser/lib/json-schema.ts +0 -73
- package/extensions/agent-browser/lib/launch-scoped-flags.ts +0 -67
- package/extensions/agent-browser/lib/navigation-policy.ts +0 -95
- package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +0 -65
- package/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.ts +0 -44
- package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +0 -280
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +0 -914
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +0 -521
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +0 -53
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.ts +0 -158
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.ts +0 -116
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.ts +0 -147
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.ts +0 -183
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.ts +0 -58
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +0 -847
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +0 -559
- package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +0 -47
- package/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.ts +0 -8
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +0 -868
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +0 -565
- package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +0 -855
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +0 -375
- package/extensions/agent-browser/lib/orchestration/output-file.ts +0 -86
- package/extensions/agent-browser/lib/pi-tool-rendering.ts +0 -267
- package/extensions/agent-browser/lib/playbook.ts +0 -142
- package/extensions/agent-browser/lib/process.ts +0 -516
- package/extensions/agent-browser/lib/prompt-policy.ts +0 -105
- package/extensions/agent-browser/lib/results/action-recommendations.ts +0 -264
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +0 -111
- package/extensions/agent-browser/lib/results/categories.ts +0 -106
- package/extensions/agent-browser/lib/results/confirmation.ts +0 -76
- package/extensions/agent-browser/lib/results/contracts.ts +0 -241
- package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +0 -72
- package/extensions/agent-browser/lib/results/envelope.ts +0 -195
- package/extensions/agent-browser/lib/results/network-routes.ts +0 -83
- package/extensions/agent-browser/lib/results/network.ts +0 -78
- package/extensions/agent-browser/lib/results/next-actions.ts +0 -117
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +0 -588
- package/extensions/agent-browser/lib/results/presentation/batch.ts +0 -450
- package/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.ts +0 -67
- package/extensions/agent-browser/lib/results/presentation/common.ts +0 -53
- package/extensions/agent-browser/lib/results/presentation/content.ts +0 -36
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +0 -923
- package/extensions/agent-browser/lib/results/presentation/errors.ts +0 -227
- package/extensions/agent-browser/lib/results/presentation/large-output.ts +0 -182
- package/extensions/agent-browser/lib/results/presentation/navigation.ts +0 -184
- package/extensions/agent-browser/lib/results/presentation/registry.ts +0 -242
- package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +0 -131
- package/extensions/agent-browser/lib/results/presentation/skills.ts +0 -143
- package/extensions/agent-browser/lib/results/presentation.ts +0 -257
- package/extensions/agent-browser/lib/results/recovery-actions.ts +0 -139
- package/extensions/agent-browser/lib/results/recovery-next-actions.ts +0 -71
- package/extensions/agent-browser/lib/results/selector-recovery.ts +0 -320
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +0 -273
- package/extensions/agent-browser/lib/results/snapshot-refs.ts +0 -100
- package/extensions/agent-browser/lib/results/snapshot-segments.ts +0 -366
- package/extensions/agent-browser/lib/results/snapshot-spill.ts +0 -63
- package/extensions/agent-browser/lib/results/snapshot.ts +0 -329
- package/extensions/agent-browser/lib/results/text.ts +0 -40
- package/extensions/agent-browser/lib/runtime.ts +0 -988
- package/extensions/agent-browser/lib/session-page-state.ts +0 -512
- package/extensions/agent-browser/lib/string-enum-schema.ts +0 -20
- package/extensions/agent-browser/lib/temp.ts +0 -577
- package/extensions/agent-browser/lib/web-search.ts +0 -728
- /package/{extensions/agent-browser/lib/orchestration/browser-run.ts → dist/extensions/agent-browser/lib/orchestration/browser-run.js} +0 -0
|
@@ -1,914 +0,0 @@
|
|
|
1
|
-
import { stat } from "node:fs/promises";
|
|
2
|
-
import { isAbsolute, resolve } from "node:path";
|
|
3
|
-
|
|
4
|
-
import { isCloseCommand, isOpenNavigationCommand } from "../../command-taxonomy.js";
|
|
5
|
-
import type { ElectronLaunchRecord } from "../../electron/launch.js";
|
|
6
|
-
import { boundElectronProbeString } from "../../electron/text.js";
|
|
7
|
-
import { executableExistsOnPath } from "../../executable-path.js";
|
|
8
|
-
import type { AgentBrowserSourceLookupAnalysis, CompiledAgentBrowserJob, CompiledAgentBrowserSemanticAction } from "../../input-modes.js";
|
|
9
|
-
import { isHttpOrHttpsUrl } from "../../input-modes/job.js";
|
|
10
|
-
import type { AgentBrowserNextAction } from "../../results.js";
|
|
11
|
-
import { formatSessionArtifactRetentionSummary } from "../../results/artifact-manifest.js";
|
|
12
|
-
import { buildNextToolAction, withOptionalSessionArgs } from "../../results/next-actions.js";
|
|
13
|
-
import { buildVisibleRefFallbackDiagnosticFromSnapshot, getVisibleRefFallbackTarget, type VisibleRefFallbackDiagnostic } from "../../results/selector-recovery.js";
|
|
14
|
-
import { extractRefSnapshotFromData, normalizeComparableUrl, type SessionRefSnapshot, type SessionTabTarget } from "../../session-page-state.js";
|
|
15
|
-
import { redactInvocationArgs, redactSensitiveText, type CommandInfo } from "../../runtime.js";
|
|
16
|
-
import { isRecord } from "../../parsing.js";
|
|
17
|
-
import {
|
|
18
|
-
extractBatchResultCommand,
|
|
19
|
-
extractNavigationSummaryFromData,
|
|
20
|
-
extractStringResultField,
|
|
21
|
-
findElectronLaunchRecordForSession,
|
|
22
|
-
getGuardedRefUsage,
|
|
23
|
-
runSessionCommandData,
|
|
24
|
-
} from "./session-state.js";
|
|
25
|
-
import { parseValidBatchStepEntries } from "../batch-stdin.js";
|
|
26
|
-
import { getScreenshotPathTokenIndex } from "./artifact-paths.js";
|
|
27
|
-
import type {
|
|
28
|
-
ArtifactCleanupGuidance,
|
|
29
|
-
ComboboxFocusDiagnostic,
|
|
30
|
-
ElectronBroadGetTextScopeDiagnostic,
|
|
31
|
-
ElectronHandoffSummary,
|
|
32
|
-
ElectronManagedSessionTarget,
|
|
33
|
-
FillVerificationDiagnostic,
|
|
34
|
-
NavigationSummary,
|
|
35
|
-
OverlayBlockerCandidate,
|
|
36
|
-
OverlayBlockerDiagnostic,
|
|
37
|
-
QaAttachedPreconditionFailure,
|
|
38
|
-
QaAttachedTarget,
|
|
39
|
-
RecordingDependencyWarning,
|
|
40
|
-
ScrollNoopDiagnostic,
|
|
41
|
-
ScrollPositionSnapshot,
|
|
42
|
-
SelectorTextVisibilityDiagnostic,
|
|
43
|
-
TimeoutArtifactEvidence,
|
|
44
|
-
TimeoutPartialProgress,
|
|
45
|
-
TimeoutProgressStep,
|
|
46
|
-
} from "./types.js";
|
|
47
|
-
import type { SessionArtifactManifest } from "../../results/contracts.js";
|
|
48
|
-
|
|
49
|
-
const ELECTRON_FILL_VERIFICATION_TIMEOUT_MS = 2_000;
|
|
50
|
-
|
|
51
|
-
export function sleepMs(ms: number): Promise<void> {
|
|
52
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function collectNavigationSummary(options: {
|
|
56
|
-
cwd: string;
|
|
57
|
-
sessionName?: string;
|
|
58
|
-
signal?: AbortSignal;
|
|
59
|
-
}): Promise<NavigationSummary | undefined> {
|
|
60
|
-
return extractNavigationSummaryFromData(await runSessionCommandData({
|
|
61
|
-
args: ["eval", "--stdin"],
|
|
62
|
-
cwd: options.cwd,
|
|
63
|
-
sessionName: options.sessionName,
|
|
64
|
-
signal: options.signal,
|
|
65
|
-
stdin: `({ title: document.title, url: location.href })`,
|
|
66
|
-
}));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function extractScrollPositionSnapshot(data: unknown): ScrollPositionSnapshot | undefined {
|
|
70
|
-
const result = isRecord(data) && isRecord(data.result) ? data.result : data;
|
|
71
|
-
if (!isRecord(result)) return undefined;
|
|
72
|
-
const scrollX = typeof result.scrollX === "number" ? result.scrollX : undefined;
|
|
73
|
-
const scrollY = typeof result.scrollY === "number" ? result.scrollY : undefined;
|
|
74
|
-
const innerHeight = typeof result.innerHeight === "number" ? result.innerHeight : undefined;
|
|
75
|
-
const innerWidth = typeof result.innerWidth === "number" ? result.innerWidth : undefined;
|
|
76
|
-
const scrollHeight = typeof result.scrollHeight === "number" ? result.scrollHeight : undefined;
|
|
77
|
-
const scrollWidth = typeof result.scrollWidth === "number" ? result.scrollWidth : undefined;
|
|
78
|
-
if (scrollX === undefined || scrollY === undefined || innerHeight === undefined || innerWidth === undefined || scrollHeight === undefined || scrollWidth === undefined) return undefined;
|
|
79
|
-
const containers = Array.isArray(result.containers)
|
|
80
|
-
? result.containers.flatMap((entry, index): ScrollPositionSnapshot["containers"] => {
|
|
81
|
-
if (!isRecord(entry)) return [];
|
|
82
|
-
const rawId = typeof entry.id === "string" ? entry.id : undefined;
|
|
83
|
-
const id = rawId && /^\d+:[a-z][a-z0-9-]*(?:\[role=[a-z-]+\])?$/i.test(rawId) ? rawId : `sample-${index}`;
|
|
84
|
-
const scrollTop = typeof entry.scrollTop === "number" ? entry.scrollTop : undefined;
|
|
85
|
-
const scrollLeft = typeof entry.scrollLeft === "number" ? entry.scrollLeft : undefined;
|
|
86
|
-
return scrollTop !== undefined && scrollLeft !== undefined ? [{ id, scrollLeft, scrollTop }] : [];
|
|
87
|
-
})
|
|
88
|
-
: [];
|
|
89
|
-
return { containerCount: typeof result.containerCount === "number" ? result.containerCount : containers.length, containers, innerHeight, innerWidth, scrollHeight, scrollWidth, scrollX, scrollY };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const SCROLL_POSITION_EVAL = `(() => {
|
|
93
|
-
const viewport = {
|
|
94
|
-
scrollX: window.scrollX,
|
|
95
|
-
scrollY: window.scrollY,
|
|
96
|
-
innerHeight: window.innerHeight,
|
|
97
|
-
innerWidth: window.innerWidth,
|
|
98
|
-
scrollHeight: Math.max(document.documentElement?.scrollHeight || 0, document.body?.scrollHeight || 0),
|
|
99
|
-
scrollWidth: Math.max(document.documentElement?.scrollWidth || 0, document.body?.scrollWidth || 0),
|
|
100
|
-
};
|
|
101
|
-
const describe = (element, index) => {
|
|
102
|
-
const role = element.getAttribute("role") || "";
|
|
103
|
-
const id = element.tagName.toLowerCase();
|
|
104
|
-
return { id: String(index) + ":" + id + (role ? "[role=" + role + "]" : ""), scrollTop: element.scrollTop, scrollLeft: element.scrollLeft, area: element.clientWidth * element.clientHeight };
|
|
105
|
-
};
|
|
106
|
-
const containers = Array.from(document.querySelectorAll("body *"))
|
|
107
|
-
.filter((element) => element instanceof HTMLElement && (element.scrollHeight > element.clientHeight + 1 || element.scrollWidth > element.clientWidth + 1))
|
|
108
|
-
.map(describe)
|
|
109
|
-
.sort((left, right) => right.area - left.area)
|
|
110
|
-
.slice(0, 10)
|
|
111
|
-
.map(({ area, ...entry }) => entry);
|
|
112
|
-
return { ...viewport, containerCount: containers.length, containers };
|
|
113
|
-
})()`;
|
|
114
|
-
|
|
115
|
-
export async function collectScrollPositionSnapshot(options: { cwd: string; sessionName?: string; signal?: AbortSignal }): Promise<ScrollPositionSnapshot | undefined> {
|
|
116
|
-
return extractScrollPositionSnapshot(await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, stdin: SCROLL_POSITION_EVAL }));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function sameScrollPositionSnapshot(left: ScrollPositionSnapshot, right: ScrollPositionSnapshot): boolean {
|
|
120
|
-
return left.scrollX === right.scrollX && left.scrollY === right.scrollY && left.scrollHeight === right.scrollHeight && left.scrollWidth === right.scrollWidth && left.containers.length === right.containers.length && left.containers.every((container, index) => {
|
|
121
|
-
const other = right.containers[index];
|
|
122
|
-
return other?.id === container.id && other.scrollTop === container.scrollTop && other.scrollLeft === container.scrollLeft;
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function buildScrollNoopDiagnostic(before: ScrollPositionSnapshot | undefined, after: ScrollPositionSnapshot | undefined): ScrollNoopDiagnostic | undefined {
|
|
127
|
-
if (!before || !after || !sameScrollPositionSnapshot(before, after)) return undefined;
|
|
128
|
-
return {
|
|
129
|
-
after,
|
|
130
|
-
before,
|
|
131
|
-
message: "Scroll reported success, but the viewport and sampled scrollable containers did not change position.",
|
|
132
|
-
reason: "no-observed-scroll-position-change",
|
|
133
|
-
recommendations: [
|
|
134
|
-
"Run snapshot -i or screenshot to confirm what is visible before choosing the next action.",
|
|
135
|
-
"On dashboards and panes with nested scrolling, use scrollintoview <@ref> for a visible target or target the actual scrollable region instead of repeating page scrolls.",
|
|
136
|
-
],
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function buildScrollNoopNextActions(sessionName: string | undefined): AgentBrowserNextAction[] {
|
|
141
|
-
return [
|
|
142
|
-
{ id: "inspect-after-noop-scroll", params: { args: withOptionalSessionArgs(sessionName, ["snapshot", "-i"]) }, reason: "Refresh interactive refs and inspect whether the intended target is inside a nested scroll container.", safety: "Do not assume repeated page scrolls will move dashboard panels or nested panes.", tool: "agent_browser" },
|
|
143
|
-
{ id: "verify-noop-scroll-visually", params: { args: withOptionalSessionArgs(sessionName, ["screenshot"]) }, reason: "Capture the current viewport to verify whether the scroll actually changed visible content.", safety: "Use screenshot evidence before concluding a dense dashboard did or did not move.", tool: "agent_browser" },
|
|
144
|
-
];
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function formatScrollNoopDiagnosticText(diagnostic: ScrollNoopDiagnostic | undefined): string | undefined {
|
|
148
|
-
if (!diagnostic) return undefined;
|
|
149
|
-
return ["Scroll diagnostic: no observed scroll movement.", `Reason: ${diagnostic.message}`, `Sampled scrollable containers: ${diagnostic.after.containers.length}/${diagnostic.after.containerCount}.`, ...diagnostic.recommendations.map((recommendation) => `- ${recommendation}`)].join("\n");
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const COMBOBOX_FOCUS_EVAL = `(() => {
|
|
153
|
-
const isVisible = (element) => {
|
|
154
|
-
if (!(element instanceof HTMLElement)) return false;
|
|
155
|
-
const style = window.getComputedStyle(element);
|
|
156
|
-
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return false;
|
|
157
|
-
return element.getClientRects().length > 0;
|
|
158
|
-
};
|
|
159
|
-
const active = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
160
|
-
const role = active?.getAttribute("role") || undefined;
|
|
161
|
-
const hasPopup = active?.getAttribute("aria-haspopup") || undefined;
|
|
162
|
-
const expanded = active?.getAttribute("aria-expanded") || undefined;
|
|
163
|
-
const tagName = active?.tagName.toLowerCase();
|
|
164
|
-
const name = (active?.getAttribute("aria-label") || active?.getAttribute("placeholder") || active?.getAttribute("title") || active?.textContent || "").trim().slice(0, 80) || undefined;
|
|
165
|
-
const visibleListboxCount = Array.from(document.querySelectorAll('[role="listbox"], [role="menu"]')).filter(isVisible).length;
|
|
166
|
-
const visibleOptionCount = Array.from(document.querySelectorAll('[role="option"], option, [role="menuitem"]')).filter(isVisible).length;
|
|
167
|
-
const comboboxLike = role === "combobox" || hasPopup === "listbox" || hasPopup === "menu" || tagName === "select" || active?.getAttribute("aria-autocomplete") !== null;
|
|
168
|
-
return { activeElement: active ? { expanded, hasPopup, name, role, tagName } : undefined, comboboxLike, visibleListboxCount, visibleOptionCount };
|
|
169
|
-
})()`;
|
|
170
|
-
|
|
171
|
-
function extractComboboxFocusDiagnostic(data: unknown): ComboboxFocusDiagnostic | undefined {
|
|
172
|
-
const result = isRecord(data) && isRecord(data.result) ? data.result : data;
|
|
173
|
-
if (!isRecord(result) || result.comboboxLike !== true || !isRecord(result.activeElement)) return undefined;
|
|
174
|
-
const visibleListboxCount = typeof result.visibleListboxCount === "number" ? result.visibleListboxCount : 0;
|
|
175
|
-
const visibleOptionCount = typeof result.visibleOptionCount === "number" ? result.visibleOptionCount : 0;
|
|
176
|
-
const expanded = typeof result.activeElement.expanded === "string" ? result.activeElement.expanded : undefined;
|
|
177
|
-
if ((expanded !== "false" && expanded !== "true") || visibleListboxCount > 0 || visibleOptionCount > 0) return undefined;
|
|
178
|
-
return {
|
|
179
|
-
activeElement: {
|
|
180
|
-
expanded,
|
|
181
|
-
hasPopup: typeof result.activeElement.hasPopup === "string" ? result.activeElement.hasPopup : undefined,
|
|
182
|
-
name: typeof result.activeElement.name === "string" ? redactSensitiveText(result.activeElement.name) : undefined,
|
|
183
|
-
role: typeof result.activeElement.role === "string" ? result.activeElement.role : undefined,
|
|
184
|
-
tagName: typeof result.activeElement.tagName === "string" ? result.activeElement.tagName : undefined,
|
|
185
|
-
},
|
|
186
|
-
message: "A combobox-like control is focused, but no listbox or option elements are visibly open.",
|
|
187
|
-
reason: "focused-combobox-without-visible-options",
|
|
188
|
-
recommendations: ["Run snapshot -i to inspect whether options appeared under a different role or portal.", "Try ArrowDown or Enter to open the option list before selecting, or use select/visible option refs when available."],
|
|
189
|
-
visibleListboxCount,
|
|
190
|
-
visibleOptionCount,
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function isComboboxFocusDiagnosticCommand(command: string | undefined, commandTokens: string[]): boolean {
|
|
195
|
-
const explicitlyTargetsCombobox = commandTokens.some((token) => /^(?:combobox|listbox)$/i.test(token));
|
|
196
|
-
if (!explicitlyTargetsCombobox) return false;
|
|
197
|
-
if (command === "click" || command === "fill") return true;
|
|
198
|
-
return command === "find" && commandTokens.some((token) => ["click", "fill"].includes(token));
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function getCompiledSemanticActionRoleValue(compiled: CompiledAgentBrowserSemanticAction): string | undefined {
|
|
202
|
-
if (compiled.locator !== "role") return undefined;
|
|
203
|
-
const findIndex = compiled.args.indexOf("find");
|
|
204
|
-
if (findIndex < 0 || compiled.args[findIndex + 1] !== "role") return undefined;
|
|
205
|
-
return compiled.args[findIndex + 2];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function isComboboxFocusDiagnosticSemanticAction(compiled: CompiledAgentBrowserSemanticAction | undefined): boolean {
|
|
209
|
-
if (!compiled || !["click", "fill"].includes(compiled.action)) return false;
|
|
210
|
-
return /^(?:combobox|listbox)$/i.test(getCompiledSemanticActionRoleValue(compiled) ?? "");
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export async function collectComboboxFocusDiagnostic(options: { command?: string; commandTokens: string[]; cwd: string; semanticAction?: CompiledAgentBrowserSemanticAction; sessionName?: string; signal?: AbortSignal }): Promise<ComboboxFocusDiagnostic | undefined> {
|
|
214
|
-
if (!isComboboxFocusDiagnosticCommand(options.command, options.commandTokens) && !isComboboxFocusDiagnosticSemanticAction(options.semanticAction)) return undefined;
|
|
215
|
-
return extractComboboxFocusDiagnostic(await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, stdin: COMBOBOX_FOCUS_EVAL }));
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export function buildComboboxFocusNextActions(sessionName: string | undefined): AgentBrowserNextAction[] {
|
|
219
|
-
return [
|
|
220
|
-
{ id: "inspect-focused-combobox", params: { args: withOptionalSessionArgs(sessionName, ["snapshot", "-i"]) }, reason: "Inspect the focused combobox and any portal/listbox refs before choosing an option.", safety: "Prefer visible option refs or select when a native/selectable option list is exposed.", tool: "agent_browser" },
|
|
221
|
-
{ id: "try-open-combobox-with-arrow", params: { args: withOptionalSessionArgs(sessionName, ["press", "ArrowDown"]) }, reason: "Many searchable comboboxes open their option list with ArrowDown after focus.", safety: "Use only when the focused combobox is still the intended control, then re-snapshot before selecting.", tool: "agent_browser" },
|
|
222
|
-
{ id: "try-open-combobox-with-enter", params: { args: withOptionalSessionArgs(sessionName, ["press", "Enter"]) }, reason: "Some comboboxes open or confirm their option list with Enter after focus.", safety: "Enter may select a highlighted/default option; prefer ArrowDown first unless Enter is the app's expected opener.", tool: "agent_browser" },
|
|
223
|
-
];
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export function formatComboboxFocusDiagnosticText(diagnostic: ComboboxFocusDiagnostic | undefined): string | undefined {
|
|
227
|
-
if (!diagnostic) return undefined;
|
|
228
|
-
const label = diagnostic.activeElement.name ? ` (${diagnostic.activeElement.name})` : "";
|
|
229
|
-
return [`Combobox diagnostic: focused combobox did not expose visible options${label}.`, `Reason: ${diagnostic.message}`, ...diagnostic.recommendations.map((recommendation) => `- ${recommendation}`)].join("\n");
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function getRecordStartLikeCommand(command: string | undefined, commandTokens: string[]): RecordingDependencyWarning["command"] | undefined {
|
|
233
|
-
if (command !== "record") return undefined;
|
|
234
|
-
const subcommand = commandTokens[1]?.toLowerCase();
|
|
235
|
-
if (subcommand === "start") return "record start";
|
|
236
|
-
if (subcommand === "restart") return "record restart";
|
|
237
|
-
return undefined;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export async function collectRecordingDependencyWarning(options: { command: string | undefined; commandTokens: string[]; succeeded: boolean }): Promise<RecordingDependencyWarning | undefined> {
|
|
241
|
-
if (!options.succeeded) return undefined;
|
|
242
|
-
const recordCommand = getRecordStartLikeCommand(options.command, options.commandTokens);
|
|
243
|
-
if (!recordCommand) return undefined;
|
|
244
|
-
if (await executableExistsOnPath("ffmpeg")) return undefined;
|
|
245
|
-
return { command: recordCommand, dependency: "ffmpeg", message: `${recordCommand} can begin recording, but record stop needs ffmpeg on PATH to encode the WebM output.`, reason: "ffmpeg-missing-for-recording", recommendations: ["Install ffmpeg before relying on this recording workflow; on macOS with Homebrew, brew install ffmpeg or brew install ffmpeg-full.", "If ffmpeg was just installed, restart pi or ensure the PATH visible to pi includes the ffmpeg binary before running record stop."] };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export function formatRecordingDependencyWarningText(warning: RecordingDependencyWarning | undefined): string | undefined {
|
|
249
|
-
if (!warning) return undefined;
|
|
250
|
-
return ["Recording dependency warning: ffmpeg not found on PATH.", `Reason: ${warning.message}`, ...warning.recommendations.map((recommendation) => `- ${recommendation}`)].join("\n");
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function getSnapshotRefRecord(data: unknown): Record<string, unknown> | undefined {
|
|
254
|
-
return isRecord(data) && isRecord(data.refs) ? data.refs : undefined;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const OVERLAY_CLOSE_NAME_PATTERN = /(?:\b(?:close|dismiss|no thanks|not now|maybe later|hide|skip|continue without|x)\b|^\s*×\s*$)/i;
|
|
258
|
-
const OVERLAY_CONTEXT_ROLES = new Set(["alertdialog", "dialog"]);
|
|
259
|
-
const OVERLAY_ACTION_ROLES = new Set(["button", "link", "menuitem"]);
|
|
260
|
-
const OVERLAY_BLOCKER_CANDIDATE_LIMIT = 3;
|
|
261
|
-
|
|
262
|
-
function getOverlayBlockerCandidates(snapshotData: unknown): OverlayBlockerCandidate[] {
|
|
263
|
-
const refs = getSnapshotRefRecord(snapshotData);
|
|
264
|
-
if (!refs) return [];
|
|
265
|
-
const hasOverlayContext = Object.values(refs).some((entry) => isRecord(entry) && OVERLAY_CONTEXT_ROLES.has((typeof entry.role === "string" ? entry.role : "").toLowerCase()));
|
|
266
|
-
if (!hasOverlayContext) return [];
|
|
267
|
-
const candidates: OverlayBlockerCandidate[] = [];
|
|
268
|
-
for (const [ref, entry] of Object.entries(refs)) {
|
|
269
|
-
if (!/^e\d+$/.test(ref) || !isRecord(entry)) continue;
|
|
270
|
-
const role = typeof entry.role === "string" ? entry.role : undefined;
|
|
271
|
-
const name = typeof entry.name === "string" ? entry.name : undefined;
|
|
272
|
-
if (!role || !OVERLAY_ACTION_ROLES.has(role.toLowerCase()) || !name || !OVERLAY_CLOSE_NAME_PATTERN.test(name)) continue;
|
|
273
|
-
candidates.push({ args: ["click", `@${ref}`], name, reason: `Visible ${role} ${JSON.stringify(name)} appears in a snapshot that also contains overlay/banner/dialog context.`, ref: `@${ref}`, role });
|
|
274
|
-
if (candidates.length >= OVERLAY_BLOCKER_CANDIDATE_LIMIT) break;
|
|
275
|
-
}
|
|
276
|
-
return candidates;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
export function formatOverlayBlockerText(diagnostic: OverlayBlockerDiagnostic): string {
|
|
280
|
-
return ["Possible overlay blockers:", ...diagnostic.candidates.map((candidate) => `- ${candidate.ref}${candidate.role ? ` ${candidate.role}` : ""}${candidate.name ? ` ${JSON.stringify(candidate.name)}` : ""}: ${candidate.reason}`)].join("\n");
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export function buildOverlayBlockerNextActions(options: { diagnostic: OverlayBlockerDiagnostic; sessionName?: string }): AgentBrowserNextAction[] {
|
|
284
|
-
return [{ id: "inspect-overlay-state", params: { args: withOptionalSessionArgs(options.sessionName, ["snapshot", "-i"]) }, reason: "Refresh interactive refs and inspect whether an overlay, banner, modal, or dialog is blocking the intended click.", safety: "Read-only inspection; use current refs from this snapshot before interacting.", tool: "agent_browser" }, ...options.diagnostic.candidates.map((candidate, index) => ({ id: `try-overlay-blocker-candidate-${index + 1}`, params: { args: withOptionalSessionArgs(options.sessionName, candidate.args) }, reason: candidate.reason, safety: "Only click this if the candidate is clearly a close/dismiss control for an overlay that blocks the intended workflow.", tool: "agent_browser" as const }))];
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
export function collectSnapshotOverlayBlockerDiagnostic(data: unknown): OverlayBlockerDiagnostic | undefined {
|
|
288
|
-
const candidates = getOverlayBlockerCandidates(data);
|
|
289
|
-
const snapshot = extractRefSnapshotFromData(data);
|
|
290
|
-
if (candidates.length === 0 || !snapshot) return undefined;
|
|
291
|
-
return { candidates, snapshot, summary: "Snapshot contains dialog/modal context plus likely close or dismiss controls; treat covered controls as potentially obstructed until the overlay state is resolved." };
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export async function collectOverlayBlockerDiagnostic(options: { command?: string; cwd: string; data: unknown; navigationSummary?: NavigationSummary; priorTarget?: SessionTabTarget; sessionName?: string; signal?: AbortSignal }): Promise<OverlayBlockerDiagnostic | undefined> {
|
|
295
|
-
if (options.command !== "click" || !isRecord(options.data) || typeof options.data.clicked !== "string") return undefined;
|
|
296
|
-
const priorUrl = normalizeComparableUrl(options.priorTarget?.url);
|
|
297
|
-
const currentUrl = normalizeComparableUrl(options.navigationSummary?.url);
|
|
298
|
-
if (!priorUrl || !currentUrl || priorUrl !== currentUrl) return undefined;
|
|
299
|
-
const snapshotData = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
300
|
-
const diagnostic = collectSnapshotOverlayBlockerDiagnostic(snapshotData);
|
|
301
|
-
if (!diagnostic) return undefined;
|
|
302
|
-
return { ...diagnostic, summary: `Click completed but the page stayed on ${currentUrl}; a fresh snapshot contains likely overlay close/dismiss controls.` };
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const SELECTOR_TEXT_VISIBILITY_CANDIDATE_LIMIT = 8;
|
|
306
|
-
|
|
307
|
-
function buildVisibleTextProbeScript(selector: string): string {
|
|
308
|
-
return `(() => {\n const selector = ${JSON.stringify(selector)};\n const isVisible = (element) => {\n const style = window.getComputedStyle(element);\n if (!style || style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse' || Number(style.opacity) === 0) return false;\n return Array.from(element.getClientRects()).some((rect) => rect.width > 0 && rect.height > 0);\n };\n let matches = [];\n try {\n matches = Array.from(document.querySelectorAll(selector));\n } catch (error) {\n return JSON.stringify({ selector, error: error instanceof Error ? error.message : String(error) });\n }\n const visible = matches.filter(isVisible);\n const trim = (value) => typeof value === 'string' ? value.trim().replace(/\\s+/g, ' ').slice(0, 200) : undefined;\n const describeCandidate = (element) => {\n const index = matches.indexOf(element);\n const role = element.getAttribute('role');\n const candidate = { index, tagName: element.tagName.toLowerCase(), textPreview: trim(element.textContent) };\n if (role) candidate.role = role;\n return candidate;\n };\n const visibleCandidates = visible.slice(0, ${SELECTOR_TEXT_VISIBILITY_CANDIDATE_LIMIT}).map(describeCandidate);\n return JSON.stringify({ selector, matchCount: matches.length, visibleCount: visible.length, firstMatchVisible: matches[0] ? isVisible(matches[0]) : undefined, firstTextPreview: trim(matches[0]?.textContent), firstVisibleTextPreview: trim(visible[0]?.textContent), visibleCandidates });\n})()`;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function parseSelectorTextVisibilityCandidates(value: unknown): SelectorTextVisibilityDiagnostic["visibleCandidates"] {
|
|
312
|
-
if (!Array.isArray(value)) return undefined;
|
|
313
|
-
const candidates = value.flatMap((entry): NonNullable<SelectorTextVisibilityDiagnostic["visibleCandidates"]> => {
|
|
314
|
-
if (!isRecord(entry) || typeof entry.index !== "number" || typeof entry.tagName !== "string") return [];
|
|
315
|
-
const role = typeof entry.role === "string" && entry.role.length > 0 ? entry.role : undefined;
|
|
316
|
-
const textPreview = typeof entry.textPreview === "string" && entry.textPreview.length > 0 ? redactSensitiveText(entry.textPreview) : undefined;
|
|
317
|
-
return [{ index: entry.index, tagName: entry.tagName, ...(role ? { role } : {}), ...(textPreview ? { textPreview } : {}) }];
|
|
318
|
-
});
|
|
319
|
-
return candidates.length > 0 ? candidates : undefined;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function parseSelectorTextVisibilityProbe(data: unknown, selector: string): Omit<SelectorTextVisibilityDiagnostic, "summary"> | undefined {
|
|
323
|
-
const result = extractStringResultField(data, "result");
|
|
324
|
-
if (!result) return undefined;
|
|
325
|
-
let parsed: unknown;
|
|
326
|
-
try { parsed = JSON.parse(result); } catch { return undefined; }
|
|
327
|
-
if (!isRecord(parsed) || typeof parsed.error === "string") return undefined;
|
|
328
|
-
const matchCount = typeof parsed.matchCount === "number" ? parsed.matchCount : undefined;
|
|
329
|
-
const visibleCount = typeof parsed.visibleCount === "number" ? parsed.visibleCount : undefined;
|
|
330
|
-
if (matchCount === undefined || visibleCount === undefined) return undefined;
|
|
331
|
-
return {
|
|
332
|
-
firstMatchVisible: typeof parsed.firstMatchVisible === "boolean" ? parsed.firstMatchVisible : undefined,
|
|
333
|
-
firstVisibleTextPreview: typeof parsed.firstVisibleTextPreview === "string" && parsed.firstVisibleTextPreview.length > 0 ? redactSensitiveText(parsed.firstVisibleTextPreview) : undefined,
|
|
334
|
-
matchCount,
|
|
335
|
-
selector,
|
|
336
|
-
visibleCandidates: parseSelectorTextVisibilityCandidates(parsed.visibleCandidates),
|
|
337
|
-
visibleCount,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function selectorMayExposeSensitiveLiteral(selector: string): boolean {
|
|
342
|
-
return redactSensitiveText(selector) !== selector || /\[[^\]]*[~|^$*]?=\s*(?:"[^"]*"|'[^']*'|[^\]\s]+)\s*(?:[is]\s*)?\]/.test(selector);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async function collectSelectorTextVisibilityDiagnosticForSelector(options: { cwd: string; selector: string | undefined; sessionName?: string; signal?: AbortSignal }): Promise<SelectorTextVisibilityDiagnostic | undefined> {
|
|
346
|
-
const { selector } = options;
|
|
347
|
-
if (!selector || /^@e\d+$/.test(selector) || selectorMayExposeSensitiveLiteral(selector)) return undefined;
|
|
348
|
-
const probe = await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, stdin: buildVisibleTextProbeScript(selector) });
|
|
349
|
-
const parsed = parseSelectorTextVisibilityProbe(probe, selector);
|
|
350
|
-
if (!parsed || parsed.matchCount <= 1 && parsed.firstMatchVisible !== false) return undefined;
|
|
351
|
-
if (parsed.visibleCount === 0) return undefined;
|
|
352
|
-
const visibleMatchNoun = `visible match${parsed.visibleCount === 1 ? "" : "es"}`;
|
|
353
|
-
const visibleMatchVerb = parsed.visibleCount === 1 ? "exists" : "exist";
|
|
354
|
-
const summary = parsed.firstMatchVisible === false
|
|
355
|
-
? `Selector ${JSON.stringify(selector)} matched ${parsed.matchCount} elements; the first match is hidden while ${parsed.visibleCount} ${visibleMatchNoun} ${visibleMatchVerb}.`
|
|
356
|
-
: `Selector ${JSON.stringify(selector)} matched ${parsed.matchCount} elements; get text reads the first upstream match, which may not be the intended visible tab/panel.`;
|
|
357
|
-
return { ...parsed, summary };
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function getBatchGetTextSelectors(data: unknown): string[] {
|
|
361
|
-
if (!Array.isArray(data)) return [];
|
|
362
|
-
return data.flatMap((item) => {
|
|
363
|
-
if (!isRecord(item) || item.success === false) return [];
|
|
364
|
-
const [command, subcommand, selector] = extractBatchResultCommand(item);
|
|
365
|
-
return command === "get" && subcommand === "text" && selector ? [selector] : [];
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function getSuccessfulGetTextSelectors(options: { commandInfo: CommandInfo; commandTokens: string[]; data: unknown }): string[] {
|
|
370
|
-
return options.commandInfo.command === "get" && options.commandInfo.subcommand === "text"
|
|
371
|
-
? [options.commandTokens[2]].filter((selector): selector is string => typeof selector === "string" && selector.length > 0)
|
|
372
|
-
: options.commandInfo.command === "batch" ? getBatchGetTextSelectors(options.data) : [];
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export async function collectSelectorTextVisibilityDiagnostics(options: { commandInfo: CommandInfo; commandTokens: string[]; cwd: string; data: unknown; sessionName?: string; signal?: AbortSignal }): Promise<SelectorTextVisibilityDiagnostic[]> {
|
|
376
|
-
const selectors = getSuccessfulGetTextSelectors(options);
|
|
377
|
-
const diagnostics: SelectorTextVisibilityDiagnostic[] = [];
|
|
378
|
-
for (const selector of selectors) {
|
|
379
|
-
const diagnostic = await collectSelectorTextVisibilityDiagnosticForSelector({ cwd: options.cwd, selector, sessionName: options.sessionName, signal: options.signal });
|
|
380
|
-
if (diagnostic) diagnostics.push(diagnostic);
|
|
381
|
-
}
|
|
382
|
-
return diagnostics.sort((left, right) => Number(right.firstMatchVisible === false) - Number(left.firstMatchVisible === false));
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
export function formatSelectorTextVisibilityText(diagnostics: SelectorTextVisibilityDiagnostic[]): string | undefined {
|
|
386
|
-
if (diagnostics.length === 0) return undefined;
|
|
387
|
-
return diagnostics.flatMap((diagnostic, index) => {
|
|
388
|
-
const actionId = index === 0 ? "inspect-visible-text-candidates" : `inspect-visible-text-candidates-${index + 1}`;
|
|
389
|
-
const lines = [`Selector text visibility warning: ${diagnostic.summary}`];
|
|
390
|
-
if (diagnostic.firstVisibleTextPreview) lines.push(`First visible text preview: ${JSON.stringify(diagnostic.firstVisibleTextPreview)}`);
|
|
391
|
-
if (diagnostic.visibleCandidates && diagnostic.visibleCandidates.length > 0) {
|
|
392
|
-
lines.push(`Visible candidates (${diagnostic.visibleCandidates.length} shown, querySelectorAll index):`);
|
|
393
|
-
for (const candidate of diagnostic.visibleCandidates) {
|
|
394
|
-
const rolePart = candidate.role ? ` role=${candidate.role}` : "";
|
|
395
|
-
const previewPart = candidate.textPreview ? `: ${JSON.stringify(candidate.textPreview)}` : "";
|
|
396
|
-
lines.push(`- [${candidate.index}] ${candidate.tagName}${rolePart}${previewPart}`);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
lines.push(`Next action: use details.nextActions ${actionId} before trusting this selector text.`);
|
|
400
|
-
return lines;
|
|
401
|
-
}).join("\n");
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
export function buildSelectorTextVisibilityNextActions(options: { diagnostics: SelectorTextVisibilityDiagnostic[]; sessionName?: string }): AgentBrowserNextAction[] {
|
|
405
|
-
return options.diagnostics.map((diagnostic, index) => ({ id: index === 0 ? "inspect-visible-text-candidates" : `inspect-visible-text-candidates-${index + 1}`, params: { args: withOptionalSessionArgs(options.sessionName, ["eval", "--stdin"]), stdin: buildVisibleTextProbeScript(diagnostic.selector) }, reason: "Inspect selector match count and visible text before trusting get text on tabbed or hidden DOM content.", safety: "Read-only DOM inspection; use a more specific visible selector or current @ref before acting on hidden-tab text.", tool: "agent_browser" as const }));
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function normalizeSelectorForScopeHeuristic(selector: string): string {
|
|
409
|
-
return selector.trim().replace(/\s+/g, " ").toLowerCase();
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function isBroadGetTextSelector(selector: string | undefined): selector is string {
|
|
413
|
-
if (!selector || /^@e\d+$/.test(selector) || selectorMayExposeSensitiveLiteral(selector)) return false;
|
|
414
|
-
const normalized = normalizeSelectorForScopeHeuristic(selector);
|
|
415
|
-
return normalized === "body" || normalized === "html" || normalized === ":root" || normalized === "*" || normalized === "main" || normalized === "div" || normalized === "section" || normalized === "article" || /^\[role=(?:"application"|'application'|application)\]$/i.test(normalized);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function getElectronTextScopeContext(options: { currentTarget?: SessionTabTarget; electronLaunchRecords: Map<string, ElectronLaunchRecord>; priorTarget?: SessionTabTarget; sessionName?: string }): ElectronBroadGetTextScopeDiagnostic["electronContext"] | undefined {
|
|
419
|
-
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords);
|
|
420
|
-
if (!record) return undefined;
|
|
421
|
-
const url = options.currentTarget?.url ?? options.priorTarget?.url;
|
|
422
|
-
return { launchId: record.launchId, sessionName: record.sessionName ?? options.sessionName, url };
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
export function getSourceLookupElectronContext(options: { currentTarget?: SessionTabTarget; electronLaunchRecords: Map<string, ElectronLaunchRecord>; priorTarget?: SessionTabTarget; sessionName?: string }): AgentBrowserSourceLookupAnalysis["electronContext"] | undefined {
|
|
426
|
-
const record = findElectronLaunchRecordForSession(options.sessionName, options.electronLaunchRecords);
|
|
427
|
-
if (!record) return undefined;
|
|
428
|
-
const url = options.currentTarget?.url ?? options.priorTarget?.url;
|
|
429
|
-
return { appName: record.appName, appPath: record.appPath, executablePath: record.executablePath, launchId: record.launchId, sessionName: record.sessionName ?? options.sessionName, url };
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
export function buildSourceLookupElectronNextActions(sourceLookup: AgentBrowserSourceLookupAnalysis | undefined): AgentBrowserNextAction[] {
|
|
433
|
-
if (sourceLookup?.status !== "no-candidates" || !sourceLookup.electronContext) return [];
|
|
434
|
-
const actions: AgentBrowserNextAction[] = [];
|
|
435
|
-
const { launchId, sessionName } = sourceLookup.electronContext;
|
|
436
|
-
if (sessionName) actions.push({ id: "snapshot-electron-session", params: { args: withOptionalSessionArgs(sessionName, ["snapshot", "-i"]) }, reason: "Refresh interactive refs in the attached Electron session before retrying source lookup with a narrower target.", safety: "Read-only snapshot; no app mutation.", tool: "agent_browser" });
|
|
437
|
-
if (launchId) actions.push({ id: "probe-electron-launch", params: { electron: { action: "probe", launchId } }, reason: "Collect bounded wrapper/session context for the packaged Electron launch after sourceLookup found no candidates.", safety: "Read-only probe of title, URL, focus, tabs, and compact snapshot metadata.", tool: "agent_browser" });
|
|
438
|
-
if (sessionName) actions.push({ id: "list-electron-tabs", params: { args: withOptionalSessionArgs(sessionName, ["tab", "list"]) }, reason: "Check current Electron tabs/targets before choosing a narrower selector or @ref.", safety: "Read-only tab listing.", tool: "agent_browser" });
|
|
439
|
-
return actions;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
export function collectElectronBroadGetTextScopeDiagnostics(options: { commandInfo: CommandInfo; commandTokens: string[]; currentTarget?: SessionTabTarget; data: unknown; electronLaunchRecords: Map<string, ElectronLaunchRecord>; priorTarget?: SessionTabTarget; sessionName?: string }): ElectronBroadGetTextScopeDiagnostic[] {
|
|
443
|
-
const electronContext = getElectronTextScopeContext(options);
|
|
444
|
-
if (!electronContext) return [];
|
|
445
|
-
return getSuccessfulGetTextSelectors(options).filter(isBroadGetTextSelector).map((selector) => ({ electronContext, selector, summary: `Broad Electron get text selector warning: selector ${JSON.stringify(selector)} may read the entire app shell; prefer snapshot -i and a current @ref or a narrower panel selector.` }));
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
export function formatElectronBroadGetTextScopeText(diagnostics: ElectronBroadGetTextScopeDiagnostic[]): string | undefined {
|
|
449
|
-
return diagnostics.length > 0 ? diagnostics.map((diagnostic) => diagnostic.summary).join("\n") : undefined;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
export function buildElectronBroadGetTextScopeNextActions(options: { diagnostics: ElectronBroadGetTextScopeDiagnostic[]; sessionName?: string }): AgentBrowserNextAction[] {
|
|
453
|
-
return options.diagnostics.map((diagnostic, index) => ({ id: index === 0 ? "snapshot-for-electron-text-scope" : `snapshot-for-electron-text-scope-${index + 1}`, params: { args: withOptionalSessionArgs(options.sessionName, ["snapshot", "-i"]) }, reason: `Refresh Electron refs before trusting broad get text selector ${JSON.stringify(diagnostic.selector)}.`, safety: "Read-only snapshot; prefer a current @ref or narrower selector before extracting app-shell text.", tool: "agent_browser" as const }));
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function looksLikeFunctionEvalStdin(stdin: string | undefined): boolean {
|
|
457
|
-
const trimmed = stdin?.trim();
|
|
458
|
-
if (!trimmed) return false;
|
|
459
|
-
return /^(?:async\s+)?function\b/.test(trimmed) || /^(?:async\s*)?\([^)]*\)\s*=>/.test(trimmed) || /^(?:async\s+)?[A-Za-z_$][\w$]*\s*=>/.test(trimmed);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function isPlainEmptyObject(value: unknown): boolean {
|
|
463
|
-
if (!isRecord(value) || Array.isArray(value)) return false;
|
|
464
|
-
const prototype = Object.getPrototypeOf(value);
|
|
465
|
-
return (prototype === Object.prototype || prototype === null) && Object.keys(value).length === 0;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
export function getEvalStdinHint(options: { command?: string; data: unknown; stdin?: string }) {
|
|
469
|
-
if (options.command !== "eval" || !looksLikeFunctionEvalStdin(options.stdin) || !isRecord(options.data)) return undefined;
|
|
470
|
-
const result = options.data.result;
|
|
471
|
-
if (!isPlainEmptyObject(result)) return undefined;
|
|
472
|
-
return { reason: "eval --stdin received a function-shaped snippet and the upstream JSON result was an empty object, which often means the function itself was returned or serialized instead of invoked.", suggestion: "Pass a plain expression such as `({ title: document.title })`, or invoke the function explicitly, for example `(() => ({ title: document.title }))()`." };
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
export function formatEvalStdinHintText(hint: ReturnType<typeof getEvalStdinHint>): string | undefined {
|
|
476
|
-
return hint ? `Eval stdin hint: ${hint.reason} ${hint.suggestion}` : undefined;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
export function getEvalResultWarning(options: { command?: string; data: unknown; navigationSummary?: { url?: string }; pageUrl?: string; stdin?: string }) {
|
|
480
|
-
if (options.command !== "eval" || !options.stdin?.trim() || !isRecord(options.data) || options.data.result !== null) return undefined;
|
|
481
|
-
const pageUrl = options.pageUrl?.trim() ?? options.navigationSummary?.url?.trim() ?? extractNavigationSummaryFromData(options.data)?.url;
|
|
482
|
-
if (!pageUrl || !/^file:/i.test(pageUrl)) return undefined;
|
|
483
|
-
const trimmed = options.stdin.trim();
|
|
484
|
-
if (/^(?:null|undefined)$/i.test(trimmed)) return undefined;
|
|
485
|
-
return {
|
|
486
|
-
reason: "eval --stdin returned null on a file:// page; upstream may not expose full DOM semantics for local fixtures.",
|
|
487
|
-
suggestion: "Treat this as inconclusive verification. Use snapshot -i, get text on current @refs, screenshot evidence, or a reachable http(s) fixture before concluding DOM state.",
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
export function formatEvalResultWarningText(warning: ReturnType<typeof getEvalResultWarning>): string | undefined {
|
|
492
|
-
return warning ? `Eval result warning: ${warning.reason} ${warning.suggestion}` : undefined;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
export async function getArtifactCleanupGuidance(options: { command?: string; cwd: string; manifest?: SessionArtifactManifest; succeeded: boolean }): Promise<ArtifactCleanupGuidance | undefined> {
|
|
496
|
-
if (!options.succeeded || !isCloseCommand(options.command) || !options.manifest || options.manifest.entries.length === 0) return undefined;
|
|
497
|
-
const explicitEntries = options.manifest.entries.filter((entry) => entry.storageScope === "explicit-path");
|
|
498
|
-
const explicitArtifactPaths: string[] = [];
|
|
499
|
-
const seenPaths = new Set<string>();
|
|
500
|
-
for (const entry of explicitEntries) {
|
|
501
|
-
if (explicitArtifactPaths.length >= 10) break;
|
|
502
|
-
const displayPath = entry.path;
|
|
503
|
-
if (seenPaths.has(displayPath)) continue;
|
|
504
|
-
const absolutePath = entry.absolutePath ?? (isAbsolute(entry.path) ? entry.path : resolve(options.cwd, entry.path));
|
|
505
|
-
try { await stat(absolutePath); } catch { continue; }
|
|
506
|
-
seenPaths.add(displayPath);
|
|
507
|
-
explicitArtifactPaths.push(displayPath);
|
|
508
|
-
}
|
|
509
|
-
return { explicitArtifactPaths, note: "Closing the browser session does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; clean existing paths with host file tools when no longer needed.", owner: "host-file-tools", summary: formatSessionArtifactRetentionSummary(options.manifest) };
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
export function formatArtifactCleanupGuidanceText(guidance: ArtifactCleanupGuidance | undefined): string | undefined {
|
|
513
|
-
if (!guidance) return undefined;
|
|
514
|
-
const explicitCount = guidance.explicitArtifactPaths.length;
|
|
515
|
-
const explicitSummary = explicitCount === 0
|
|
516
|
-
? "No existing explicit artifact paths were found in the recent manifest."
|
|
517
|
-
: `${explicitCount} explicit artifact${explicitCount === 1 ? "" : "s"} remain${explicitCount === 1 ? "s" : ""}; expand or inspect details.artifactCleanup.explicitArtifactPaths for paths.`;
|
|
518
|
-
return `Artifact lifecycle: ${explicitSummary} Browser close does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; use host file tools for cleanup.`;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
async function collectManagedSessionCommandData(options: { args: string[]; cwd: string; sessionName: string; signal?: AbortSignal; timeoutMs?: number }): Promise<{ data?: unknown; error?: string }> {
|
|
522
|
-
try { return { data: await runSessionCommandData(options) }; } catch (error) { return { error: error instanceof Error ? error.message : String(error) }; }
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
async function collectElectronManagedSessionUrl(options: { cwd: string; sessionName: string; signal?: AbortSignal; timeoutMs?: number }): Promise<{ error?: string; url?: string }> {
|
|
526
|
-
const urlResult = await collectManagedSessionCommandData({
|
|
527
|
-
args: ["get", "url"],
|
|
528
|
-
cwd: options.cwd,
|
|
529
|
-
sessionName: options.sessionName,
|
|
530
|
-
signal: options.signal,
|
|
531
|
-
timeoutMs: options.timeoutMs,
|
|
532
|
-
});
|
|
533
|
-
const url = boundElectronProbeString(extractStringResultField(urlResult.data, "result") ?? extractStringResultField(urlResult.data, "url"), 300);
|
|
534
|
-
return urlResult.error ? { error: urlResult.error } : { url };
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
export async function collectElectronManagedSessionTarget(options: { cwd: string; sessionName?: string; signal?: AbortSignal; timeoutMs?: number }): Promise<ElectronManagedSessionTarget | undefined> {
|
|
538
|
-
if (!options.sessionName) return undefined;
|
|
539
|
-
const [titleResult, urlResult] = await Promise.all([
|
|
540
|
-
collectManagedSessionCommandData({ args: ["get", "title"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, timeoutMs: options.timeoutMs }),
|
|
541
|
-
collectManagedSessionCommandData({ args: ["get", "url"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, timeoutMs: options.timeoutMs }),
|
|
542
|
-
]);
|
|
543
|
-
const title = boundElectronProbeString(extractStringResultField(titleResult.data, "result") ?? extractStringResultField(titleResult.data, "title"), 160);
|
|
544
|
-
const url = boundElectronProbeString(extractStringResultField(urlResult.data, "result") ?? extractStringResultField(urlResult.data, "url"), 300);
|
|
545
|
-
const errors = [titleResult.error, urlResult.error].filter((value): value is string => value !== undefined);
|
|
546
|
-
return { sessionName: options.sessionName, title, url, ...(errors.length > 0 ? { error: errors.join("; ") } : {}) };
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
export async function collectQaAttachedTarget(options: { currentTarget?: SessionTabTarget; cwd: string; sessionName?: string; signal?: AbortSignal }): Promise<QaAttachedTarget | undefined> {
|
|
550
|
-
if (!options.sessionName) return undefined;
|
|
551
|
-
if (options.currentTarget?.title || options.currentTarget?.url) return { sessionName: options.sessionName, title: options.currentTarget.title, url: options.currentTarget.url };
|
|
552
|
-
return collectElectronManagedSessionTarget({ cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
export function formatQaAttachedTargetText(target: QaAttachedTarget | undefined): string | undefined {
|
|
556
|
-
if (!target) return undefined;
|
|
557
|
-
return ["QA attached target:", target.sessionName, target.title, target.url].filter((part): part is string => typeof part === "string" && part.length > 0).join(" — ");
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
export function buildQaAttachedRecoveryNextActions(sessionName: string | undefined): AgentBrowserNextAction[] {
|
|
561
|
-
const sessionArgs = (args: string[]) => withOptionalSessionArgs(sessionName, args);
|
|
562
|
-
return [
|
|
563
|
-
buildNextToolAction({
|
|
564
|
-
args: sessionArgs(["tab", "list"]),
|
|
565
|
-
id: "list-tabs-before-qa-attached",
|
|
566
|
-
reason: "Inspect the connected session tabs before retrying qa.attached.",
|
|
567
|
-
safety: "Read-only tab listing for the attached session.",
|
|
568
|
-
}),
|
|
569
|
-
buildNextToolAction({
|
|
570
|
-
args: sessionArgs(["snapshot", "-i"]),
|
|
571
|
-
id: "snapshot-before-qa-attached",
|
|
572
|
-
reason: "Capture interactive refs on the active http(s) page before retrying qa.attached.",
|
|
573
|
-
safety: "Read-only snapshot; confirms a renderable page is selected.",
|
|
574
|
-
}),
|
|
575
|
-
];
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
export async function validateQaAttachedPrecondition(options: {
|
|
579
|
-
cwd: string;
|
|
580
|
-
sessionName?: string;
|
|
581
|
-
signal?: AbortSignal;
|
|
582
|
-
}): Promise<QaAttachedPreconditionFailure | undefined> {
|
|
583
|
-
if (!options.sessionName) {
|
|
584
|
-
return {
|
|
585
|
-
error: "qa.attached requires an active attached session with a resolvable session name.",
|
|
586
|
-
nextActions: buildQaAttachedRecoveryNextActions(options.sessionName),
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
const urlProbe = await collectElectronManagedSessionUrl({ cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
590
|
-
if (urlProbe.error) {
|
|
591
|
-
return {
|
|
592
|
-
error: `qa.attached could not read the attached session URL: ${urlProbe.error}. Run tab list or snapshot -i before retrying qa.attached.`,
|
|
593
|
-
nextActions: buildQaAttachedRecoveryNextActions(options.sessionName),
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
const url = urlProbe.url?.trim();
|
|
597
|
-
if (!url) {
|
|
598
|
-
return {
|
|
599
|
-
error: "qa.attached requires an attached session with a readable http(s) page URL. Run tab list, select a stable tab, then snapshot -i before retrying.",
|
|
600
|
-
nextActions: buildQaAttachedRecoveryNextActions(options.sessionName),
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
if (!isHttpOrHttpsUrl(url)) {
|
|
604
|
-
return {
|
|
605
|
-
error: `qa.attached requires an http(s) page URL; the current attached URL is "${url}". Use tab list and snapshot -i to recover a web surface before retrying.`,
|
|
606
|
-
nextActions: buildQaAttachedRecoveryNextActions(options.sessionName),
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
return undefined;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
function getTopLevelFillInvocation(commandTokens: string[]): { expected: string; refId?: string; selector: string } | undefined {
|
|
613
|
-
if (commandTokens[0] !== "fill" || commandTokens.length < 3) return undefined;
|
|
614
|
-
const selector = commandTokens[1];
|
|
615
|
-
const expected = commandTokens.slice(2).join(" ");
|
|
616
|
-
const refId = selector?.match(/^@?(e\d+)$/)?.[1];
|
|
617
|
-
return selector && expected.length > 0 ? { expected, ...(refId ? { refId } : {}), selector } : undefined;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function shouldVerifyContenteditableFill(fill: { refId?: string } | undefined, refSnapshot?: SessionRefSnapshot): boolean {
|
|
621
|
-
if (!fill?.refId) return false;
|
|
622
|
-
const ref = refSnapshot?.refs?.[fill.refId];
|
|
623
|
-
if (!ref) return false;
|
|
624
|
-
return ref.isContentEditable === true && (ref.role === "generic" || ref.role === "unknown" || ref.role === "textbox");
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
export function buildFillVerificationNextActions(diagnostic: FillVerificationDiagnostic, sessionName: string | undefined): AgentBrowserNextAction[] {
|
|
628
|
-
return [
|
|
629
|
-
{ id: "inspect-after-fill-verification", params: { args: withOptionalSessionArgs(sessionName, ["snapshot", "-i"]) }, reason: "Refresh the UI after a fill that reported success but did not appear to update the target.", safety: "Read-only snapshot; use current refs before retrying.", tool: "agent_browser" },
|
|
630
|
-
{ id: "verify-filled-value", params: { args: withOptionalSessionArgs(sessionName, ["get", diagnostic.method, diagnostic.selector]) }, reason: `Check the target ${diagnostic.method} directly before submitting or creating files.`, safety: "Read-only check; selector may still be stale if the UI rerendered.", tool: "agent_browser" },
|
|
631
|
-
];
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function extractFillVerificationValue(data: unknown): string | undefined {
|
|
635
|
-
if (typeof data === "string") return data;
|
|
636
|
-
if (!isRecord(data)) return undefined;
|
|
637
|
-
if (typeof data.value === "string") return data.value;
|
|
638
|
-
if (typeof data.result === "string") return data.result;
|
|
639
|
-
return undefined;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
export async function collectFillVerificationDiagnostic(options: { commandTokens: string[]; cwd: string; forceValueVerification?: boolean; refSnapshot?: SessionRefSnapshot; sessionName?: string; signal?: AbortSignal }): Promise<FillVerificationDiagnostic | undefined> {
|
|
643
|
-
const fill = getTopLevelFillInvocation(options.commandTokens);
|
|
644
|
-
if (!fill || !options.sessionName) return undefined;
|
|
645
|
-
const contenteditable = shouldVerifyContenteditableFill(fill, options.refSnapshot);
|
|
646
|
-
if (!contenteditable && !options.forceValueVerification) return undefined;
|
|
647
|
-
const method = contenteditable ? "text" : "value";
|
|
648
|
-
let valueData: unknown | undefined;
|
|
649
|
-
try { valueData = await runSessionCommandData({ args: ["get", method, fill.selector], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, timeoutMs: ELECTRON_FILL_VERIFICATION_TIMEOUT_MS }); } catch { return undefined; }
|
|
650
|
-
const actual = extractFillVerificationValue(valueData);
|
|
651
|
-
if (actual === undefined || actual === fill.expected) return undefined;
|
|
652
|
-
const reason = contenteditable ? "contenteditable-fill-mismatch" : "value-fill-mismatch";
|
|
653
|
-
const actualPreview = actual.length > 0 ? `"${boundElectronProbeString(actual, 80)}"` : `an empty ${method}`;
|
|
654
|
-
const diagnostic: FillVerificationDiagnostic = { actual: actual.length > 0 ? boundElectronProbeString(actual, 160) : "", expected: boundElectronProbeString(fill.expected, 160) ?? fill.expected, method, nextActionIds: [], reason, selector: fill.selector, status: "mismatch", summary: `Fill verification warning: fill ${fill.selector} reported success, but get ${method} returned ${actualPreview}.` };
|
|
655
|
-
diagnostic.nextActionIds = buildFillVerificationNextActions(diagnostic, options.sessionName).map((action) => action.id);
|
|
656
|
-
return diagnostic;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
export function formatFillVerificationText(diagnostic: FillVerificationDiagnostic | undefined): string | undefined {
|
|
660
|
-
if (!diagnostic) return undefined;
|
|
661
|
-
const actual = diagnostic.actual !== undefined ? `actual "${diagnostic.actual}"` : `actual ${diagnostic.method} unavailable`;
|
|
662
|
-
const recovery = diagnostic.reason === "contenteditable-fill-mismatch"
|
|
663
|
-
? "Contenteditable fill may append or prepend instead of replacing. Re-run snapshot -i, then prefer focus/click plus keyboard shortcut selection or direct keyboard insertion only after verifying the editor state."
|
|
664
|
-
: "Re-run snapshot -i, then prefer click/focus plus keyboard type for custom quick-input controls before submitting.";
|
|
665
|
-
return `${diagnostic.summary}\nExpected: "${diagnostic.expected}"; ${actual}.\nNext: ${recovery}`;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
export async function collectVisibleRefFallbackDiagnostic(options: { commandTokens: string[]; compiledSemanticAction?: CompiledAgentBrowserSemanticAction; cwd: string; sessionName?: string; signal?: AbortSignal }): Promise<VisibleRefFallbackDiagnostic | undefined> {
|
|
669
|
-
if (!options.sessionName) return undefined;
|
|
670
|
-
const target = getVisibleRefFallbackTarget({ commandTokens: options.commandTokens, compiledSemanticAction: options.compiledSemanticAction });
|
|
671
|
-
if (!target) return undefined;
|
|
672
|
-
const snapshotData = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
673
|
-
return buildVisibleRefFallbackDiagnosticFromSnapshot({ snapshotData, target });
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
export async function collectElectronHandoff(options: { cwd: string; handoff: "connect" | "snapshot" | "tabs"; sessionName?: string; signal?: AbortSignal }): Promise<ElectronHandoffSummary> {
|
|
677
|
-
if (options.handoff === "connect") return { handoff: "connect" };
|
|
678
|
-
const tabs = await runSessionCommandData({ args: ["tab", "list"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
679
|
-
if (options.handoff === "tabs") return { handoff: "tabs", tabs };
|
|
680
|
-
let snapshot = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
681
|
-
let refSnapshot = extractRefSnapshotFromData(snapshot);
|
|
682
|
-
let snapshotRetryCount = 0;
|
|
683
|
-
while ((!refSnapshot || refSnapshot.refIds.length === 0) && snapshotRetryCount < 2) {
|
|
684
|
-
snapshotRetryCount += 1;
|
|
685
|
-
await sleepMs(250);
|
|
686
|
-
snapshot = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
687
|
-
refSnapshot = extractRefSnapshotFromData(snapshot);
|
|
688
|
-
}
|
|
689
|
-
return { handoff: "snapshot", refSnapshot, snapshot, ...(snapshotRetryCount > 0 ? { snapshotRetryCount } : {}), tabs };
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
function getTimeoutProgressSteps(compiledJob: CompiledAgentBrowserJob | undefined, command: string | undefined, stdin: string | undefined): Array<{ args: string[]; generatedFrom?: string; index: number }> {
|
|
693
|
-
if (compiledJob) return compiledJob.steps.map((step, index) => ({ args: step.args, generatedFrom: step.generatedFrom, index: index + 1 }));
|
|
694
|
-
if (command !== "batch" || !stdin) return [];
|
|
695
|
-
return parseValidBatchStepEntries(stdin).map(({ index, step }) => ({ args: step, index: index + 1 }));
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function getLastPositionalToken(args: string[], startIndex = 1): string | undefined {
|
|
699
|
-
for (let index = args.length - 1; index >= startIndex; index -= 1) {
|
|
700
|
-
const token = args[index];
|
|
701
|
-
if (token && !token.startsWith("-")) return token;
|
|
702
|
-
}
|
|
703
|
-
return undefined;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function getTimeoutStepArtifactPath(args: string[]): string | undefined {
|
|
707
|
-
const [command] = args;
|
|
708
|
-
if (command === "screenshot") {
|
|
709
|
-
const index = getScreenshotPathTokenIndex(args);
|
|
710
|
-
return index === undefined ? undefined : args[index];
|
|
711
|
-
}
|
|
712
|
-
if (command === "pdf") return getLastPositionalToken(args);
|
|
713
|
-
if (command === "download") return getLastPositionalToken(args, 2);
|
|
714
|
-
if (command === "wait") {
|
|
715
|
-
const inlineDownload = args.find((token) => token.startsWith("--download="));
|
|
716
|
-
if (inlineDownload) return inlineDownload.slice("--download=".length) || undefined;
|
|
717
|
-
const downloadIndex = args.indexOf("--download");
|
|
718
|
-
const downloadPath = downloadIndex >= 0 ? args[downloadIndex + 1] : undefined;
|
|
719
|
-
if (downloadPath && !downloadPath.startsWith("-")) return downloadPath;
|
|
720
|
-
}
|
|
721
|
-
return undefined;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
async function statTimeoutArtifactPath(absolutePath: string): Promise<{ exists: false } | { exists: true; sizeBytes: number }> {
|
|
725
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
726
|
-
try {
|
|
727
|
-
const stats = await stat(absolutePath);
|
|
728
|
-
return { exists: true, sizeBytes: stats.size };
|
|
729
|
-
} catch {
|
|
730
|
-
if (attempt < 2) await sleepMs(25);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
return { exists: false };
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
async function collectTimeoutArtifactEvidence(cwd: string, steps: Array<{ args: string[]; index: number }>): Promise<TimeoutArtifactEvidence[]> {
|
|
737
|
-
const evidence: TimeoutArtifactEvidence[] = [];
|
|
738
|
-
for (const step of steps) {
|
|
739
|
-
const path = getTimeoutStepArtifactPath(step.args);
|
|
740
|
-
if (!path) continue;
|
|
741
|
-
const absolutePath = isAbsolute(path) ? path : resolve(cwd, path);
|
|
742
|
-
const artifact = await statTimeoutArtifactPath(absolutePath);
|
|
743
|
-
evidence.push(artifact.exists
|
|
744
|
-
? { absolutePath, exists: true, path, sizeBytes: artifact.sizeBytes, state: "verified", stepIndex: step.index }
|
|
745
|
-
: { absolutePath, exists: false, path, state: "missing", stepIndex: step.index });
|
|
746
|
-
}
|
|
747
|
-
return evidence;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
function getPlannedCurrentPageUrl(steps: Array<{ args: string[]; index: number }>): string | undefined {
|
|
751
|
-
for (let index = steps.length - 1; index >= 0; index -= 1) {
|
|
752
|
-
const args = steps[index]?.args ?? [];
|
|
753
|
-
if (isOpenNavigationCommand(args[0]) || args[0] === "pushstate") return getLastPositionalToken(args);
|
|
754
|
-
}
|
|
755
|
-
return undefined;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
const TIMEOUT_RETRYABLE_COMMANDS = new Set([
|
|
759
|
-
"console",
|
|
760
|
-
"diff",
|
|
761
|
-
"errors",
|
|
762
|
-
"get",
|
|
763
|
-
"goto",
|
|
764
|
-
"navigate",
|
|
765
|
-
"network",
|
|
766
|
-
"open",
|
|
767
|
-
"pdf",
|
|
768
|
-
"pushstate",
|
|
769
|
-
"screenshot",
|
|
770
|
-
"snapshot",
|
|
771
|
-
"tab",
|
|
772
|
-
"vitals",
|
|
773
|
-
"wait",
|
|
774
|
-
]);
|
|
775
|
-
|
|
776
|
-
function getTimeoutStepRetry(step: { args: string[] }): { args: string[] } | undefined {
|
|
777
|
-
const command = step.args[0];
|
|
778
|
-
return command && TIMEOUT_RETRYABLE_COMMANDS.has(command) ? { args: step.args } : undefined;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function normalizeUrlForTimeoutComparison(url: string | undefined): URL | undefined {
|
|
782
|
-
if (!url) return undefined;
|
|
783
|
-
try {
|
|
784
|
-
return new URL(url);
|
|
785
|
-
} catch {
|
|
786
|
-
return undefined;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
function currentUrlMatchesNavigationStep(currentUrl: string | undefined, plannedUrl: string | undefined): boolean {
|
|
791
|
-
if (!currentUrl || !plannedUrl) return false;
|
|
792
|
-
if (currentUrl === plannedUrl) return true;
|
|
793
|
-
const current = normalizeUrlForTimeoutComparison(currentUrl);
|
|
794
|
-
const planned = normalizeUrlForTimeoutComparison(plannedUrl);
|
|
795
|
-
if (!current || !planned || current.origin !== planned.origin) return false;
|
|
796
|
-
const plannedPath = planned.pathname.endsWith("/") ? planned.pathname : `${planned.pathname}/`;
|
|
797
|
-
const currentPath = current.pathname.endsWith("/") ? current.pathname : `${current.pathname}/`;
|
|
798
|
-
return planned.pathname === "/" || currentPath.startsWith(plannedPath);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
function buildTimeoutProgressSteps(options: {
|
|
802
|
-
artifacts: TimeoutArtifactEvidence[];
|
|
803
|
-
currentPageSource?: "live" | "planned";
|
|
804
|
-
currentPageUrl?: string;
|
|
805
|
-
steps: Array<{ args: string[]; generatedFrom?: string; index: number }>;
|
|
806
|
-
}): { openedButPostOpenTimedOut?: boolean; retryStep?: TimeoutProgressStep; steps: TimeoutProgressStep[] } {
|
|
807
|
-
let retryStep: TimeoutProgressStep | undefined;
|
|
808
|
-
let lastCompletedNavigationIndex: number | undefined;
|
|
809
|
-
const progressSteps = options.steps.map((step): TimeoutProgressStep => {
|
|
810
|
-
const stepArtifacts = options.artifacts.filter((artifact) => artifact.stepIndex === step.index);
|
|
811
|
-
const command = step.args[0];
|
|
812
|
-
const navigationUrl = isOpenNavigationCommand(command) || command === "pushstate" ? getLastPositionalToken(step.args) : undefined;
|
|
813
|
-
if (stepArtifacts.some((artifact) => artifact.exists)) {
|
|
814
|
-
return { ...step, reason: "Declared artifact exists on disk after timeout.", status: "completed" };
|
|
815
|
-
}
|
|
816
|
-
if (options.currentPageSource === "live" && currentUrlMatchesNavigationStep(options.currentPageUrl, navigationUrl)) {
|
|
817
|
-
lastCompletedNavigationIndex = step.index;
|
|
818
|
-
return { ...step, reason: "Live page URL was recovered after timeout.", status: "completed" };
|
|
819
|
-
}
|
|
820
|
-
return { ...step, reason: stepArtifacts.length > 0 ? "Declared artifact was not present when the watchdog fired." : undefined, status: "unknown" };
|
|
821
|
-
});
|
|
822
|
-
const highestCompletedIndex = Math.max(0, ...progressSteps.filter((step) => step.status === "completed").map((step) => step.index));
|
|
823
|
-
for (const step of progressSteps) {
|
|
824
|
-
if (step.status === "unknown" && step.index < highestCompletedIndex) {
|
|
825
|
-
step.status = "completed";
|
|
826
|
-
step.reason = "Later step completion evidence indicates the batch advanced past this step before timeout.";
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
for (const step of progressSteps) {
|
|
830
|
-
const command = step.args[0];
|
|
831
|
-
if (step.status === "completed" && (isOpenNavigationCommand(command) || command === "pushstate")) {
|
|
832
|
-
lastCompletedNavigationIndex = Math.max(lastCompletedNavigationIndex ?? 0, step.index);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
for (const step of progressSteps) {
|
|
836
|
-
if (step.status === "completed") continue;
|
|
837
|
-
if (!retryStep) {
|
|
838
|
-
const retry = getTimeoutStepRetry(step);
|
|
839
|
-
retryStep = {
|
|
840
|
-
...step,
|
|
841
|
-
reason: step.reason ?? (retry ? "Likely active when the wrapper watchdog fired." : "Likely active when the wrapper watchdog fired; executable retry omitted because this step may have already mutated page state."),
|
|
842
|
-
retry,
|
|
843
|
-
status: "failed",
|
|
844
|
-
};
|
|
845
|
-
Object.assign(step, retryStep);
|
|
846
|
-
continue;
|
|
847
|
-
}
|
|
848
|
-
step.status = "pending";
|
|
849
|
-
step.reason = step.reason ?? `Pending behind timed-out step ${retryStep.index}.`;
|
|
850
|
-
}
|
|
851
|
-
return {
|
|
852
|
-
openedButPostOpenTimedOut: lastCompletedNavigationIndex !== undefined && retryStep !== undefined && retryStep.index > lastCompletedNavigationIndex,
|
|
853
|
-
retryStep,
|
|
854
|
-
steps: progressSteps,
|
|
855
|
-
};
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
export async function collectTimeoutPartialProgress(options: { command?: string; compiledJob?: CompiledAgentBrowserJob; cwd: string; sessionName?: string; stdin?: string }): Promise<TimeoutPartialProgress | undefined> {
|
|
859
|
-
const rawSteps = getTimeoutProgressSteps(options.compiledJob, options.command, options.stdin);
|
|
860
|
-
const artifacts = await collectTimeoutArtifactEvidence(options.cwd, rawSteps);
|
|
861
|
-
const [urlData, titleData] = await Promise.all([runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, sessionName: options.sessionName }), runSessionCommandData({ args: ["get", "title"], cwd: options.cwd, sessionName: options.sessionName })]);
|
|
862
|
-
const recoveredUrl = extractStringResultField(urlData, "result") ?? extractStringResultField(urlData, "url");
|
|
863
|
-
const title = extractStringResultField(titleData, "result") ?? extractStringResultField(titleData, "title");
|
|
864
|
-
const plannedUrl = recoveredUrl ? undefined : getPlannedCurrentPageUrl(rawSteps);
|
|
865
|
-
const url = recoveredUrl ?? plannedUrl;
|
|
866
|
-
const currentPageSource = recoveredUrl ? "live" as const : plannedUrl ? "planned" as const : title ? "live" as const : undefined;
|
|
867
|
-
const stepProgress = buildTimeoutProgressSteps({ artifacts, currentPageSource: recoveredUrl ? "live" : undefined, currentPageUrl: recoveredUrl, steps: rawSteps });
|
|
868
|
-
if (rawSteps.length === 0 && artifacts.length === 0 && !url && !title) return undefined;
|
|
869
|
-
const foundArtifacts = artifacts.filter((artifact) => artifact.exists).length;
|
|
870
|
-
const completedSteps = stepProgress.steps.filter((step) => step.status === "completed").length;
|
|
871
|
-
const pageStateSummary = recoveredUrl || title ? " and current page state" : plannedUrl ? " and planned page URL" : "";
|
|
872
|
-
const retrySummary = stepProgress.retryStep ? ` Retry step ${stepProgress.retryStep.index} is the first incomplete step.` : "";
|
|
873
|
-
return { artifacts, currentPage: url || title ? { source: currentPageSource, title, url } : undefined, liveUrlRecovered: recoveredUrl !== undefined, openedButPostOpenTimedOut: stepProgress.openedButPostOpenTimedOut, retryStep: stepProgress.retryStep, steps: stepProgress.steps.length > 0 ? stepProgress.steps : undefined, summary: `Timed out before upstream returned final results; recovered ${completedSteps}/${rawSteps.length} planned step state${rawSteps.length === 1 ? "" : "s"} and ${foundArtifacts}/${artifacts.length} declared artifact path${artifacts.length === 1 ? "" : "s"}${pageStateSummary}.${retrySummary}` };
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
function redactSensitivePathSegmentsForDiagnostic(path: string): string {
|
|
877
|
-
return path.split(/([/\\]+)/).map((segment) => segment === "/" || segment === "\\" || /^[/\\]+$/.test(segment) ? segment : redactSensitiveText(segment) !== segment || /(?:secret|token|password|passwd|credential|auth|api[-_]?key|bearer)/i.test(segment) ? "[REDACTED]" : segment).join("");
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
function sanitizeCurrentPageUrlForTimeoutDiagnostic(url: string): string {
|
|
881
|
-
try {
|
|
882
|
-
const parsedUrl = new URL(url);
|
|
883
|
-
parsedUrl.pathname = parsedUrl.pathname.split("/").map((segment) => redactSensitivePathSegmentsForDiagnostic(segment)).join("/");
|
|
884
|
-
for (const [key, value] of parsedUrl.searchParams.entries()) {
|
|
885
|
-
if (redactSensitiveText(key) !== key || redactSensitiveText(value) !== value || /(?:secret|token|password|passwd|credential|auth|api[-_]?key|bearer)/i.test(`${key} ${value}`)) parsedUrl.searchParams.set(key, "[REDACTED]");
|
|
886
|
-
}
|
|
887
|
-
if (parsedUrl.hash) parsedUrl.hash = redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(parsedUrl.hash));
|
|
888
|
-
return redactSensitiveText(parsedUrl.toString());
|
|
889
|
-
} catch {
|
|
890
|
-
return redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(url));
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
export function formatTimeoutPartialProgressText(progress: TimeoutPartialProgress): string {
|
|
895
|
-
const lines = [`Timeout partial progress: ${progress.summary}`];
|
|
896
|
-
const currentPageTitle = progress.currentPage?.title ? redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(progress.currentPage.title)) : undefined;
|
|
897
|
-
const currentPageUrl = progress.currentPage?.url ? sanitizeCurrentPageUrlForTimeoutDiagnostic(progress.currentPage.url) : undefined;
|
|
898
|
-
if (currentPageTitle || currentPageUrl) lines.push(`Current page: ${[currentPageTitle, currentPageUrl].filter(Boolean).join(" — ")}`);
|
|
899
|
-
if (progress.steps && progress.steps.length > 0) {
|
|
900
|
-
const shownSteps = progress.steps.slice(0, 6);
|
|
901
|
-
lines.push("Planned steps:");
|
|
902
|
-
for (const step of shownSteps) {
|
|
903
|
-
const commandText = redactSensitivePathSegmentsForDiagnostic(redactInvocationArgs(step.args).join(" "));
|
|
904
|
-
const generatedFrom = step.generatedFrom ? `, generated from ${step.generatedFrom}` : "";
|
|
905
|
-
lines.push(`- Step ${step.index} [${step.status}${generatedFrom}]: ${commandText}${step.reason ? ` — ${redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(step.reason))}` : ""}`);
|
|
906
|
-
}
|
|
907
|
-
if (progress.steps.length > shownSteps.length) lines.push(`- ... ${progress.steps.length - shownSteps.length} more step${progress.steps.length - shownSteps.length === 1 ? "" : "s"} omitted`);
|
|
908
|
-
}
|
|
909
|
-
if (progress.retryStep?.retry?.args) {
|
|
910
|
-
lines.push(`Retry failed step: ${JSON.stringify({ args: redactInvocationArgs(progress.retryStep.retry.args) })}`);
|
|
911
|
-
}
|
|
912
|
-
for (const artifact of progress.artifacts) lines.push(`Artifact from step ${artifact.stepIndex}: ${redactSensitivePathSegmentsForDiagnostic(artifact.path)} (${artifact.exists ? `exists${typeof artifact.sizeBytes === "number" ? `, ${artifact.sizeBytes} bytes` : ""}` : "missing"})`);
|
|
913
|
-
return lines.join("\n");
|
|
914
|
-
}
|