pi-agent-browser-native 0.2.47 → 0.2.48
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 +46 -19
- package/README.md +38 -15
- package/docs/ARCHITECTURE.md +10 -10
- package/docs/COMMAND_REFERENCE.md +35 -21
- package/docs/ELECTRON.md +3 -3
- package/docs/RELEASE.md +28 -19
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/SUPPORT_MATRIX.md +34 -106
- package/docs/TOOL_CONTRACT.md +23 -21
- package/extensions/agent-browser/index.ts +13 -4
- package/extensions/agent-browser/lib/config.ts +2 -0
- package/extensions/agent-browser/lib/input-modes/job.ts +138 -62
- package/extensions/agent-browser/lib/input-modes/params.ts +2 -2
- package/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.ts +44 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +42 -19
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +6 -4
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +18 -9
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.ts +158 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.ts +116 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.ts +147 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.ts +183 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.ts +58 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +19 -653
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +1 -6
- package/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.ts +8 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +1 -0
- package/extensions/agent-browser/lib/pi-tool-rendering.ts +34 -19
- package/extensions/agent-browser/lib/playbook.ts +4 -4
- package/extensions/agent-browser/lib/results/action-recommendations.ts +3 -3
- package/extensions/agent-browser/lib/web-search.ts +11 -4
- package/package.json +4 -4
- package/scripts/agent-browser-capability-baseline.mjs +6 -3
- package/scripts/doctor.mjs +11 -10
- package/scripts/platform-smoke.mjs +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { isRecord } from "../../../parsing.js";
|
|
2
|
+
import { buildAgentBrowserResultCategoryDetails } from "../../../results.js";
|
|
3
|
+
import type { CompatibilityWorkaround } from "../../../runtime.js";
|
|
4
|
+
import { buildScrollNoopNextActions } from "../diagnostics.js";
|
|
5
|
+
import { buildSessionDetailFields, runSessionCommandData } from "../session-state.js";
|
|
6
|
+
import type { AgentBrowserToolResult } from "../types.js";
|
|
7
|
+
|
|
8
|
+
const SCROLL_CONTAINER_DIRECTIONS = new Set(["down", "left", "right", "up"]);
|
|
9
|
+
|
|
10
|
+
function getContainerScrollRequest(commandTokens: string[]): { amount?: string; direction: string; selector: string } | undefined {
|
|
11
|
+
if (commandTokens[0] !== "scroll" || commandTokens.length < 3) return undefined;
|
|
12
|
+
const selector = commandTokens[1];
|
|
13
|
+
const direction = commandTokens[2]?.toLowerCase();
|
|
14
|
+
if (!selector || selector.startsWith("-") || selector.startsWith("@") || SCROLL_CONTAINER_DIRECTIONS.has(selector.toLowerCase())) return undefined;
|
|
15
|
+
if (!SCROLL_CONTAINER_DIRECTIONS.has(direction)) return undefined;
|
|
16
|
+
return { amount: commandTokens[3], direction, selector };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildContainerScrollScript(request: { amount?: string; direction: string; selector: string }): string {
|
|
20
|
+
return `(() => {
|
|
21
|
+
const selector = ${JSON.stringify(request.selector)};
|
|
22
|
+
const direction = ${JSON.stringify(request.direction)};
|
|
23
|
+
const amountToken = ${JSON.stringify(request.amount ?? "")};
|
|
24
|
+
let element;
|
|
25
|
+
try { element = document.querySelector(selector); } catch (error) { return { status: "invalid-selector", selector, error: String(error && error.message || error) }; }
|
|
26
|
+
if (!(element instanceof HTMLElement)) return { status: "not-found", selector };
|
|
27
|
+
const axis = direction === "left" || direction === "right" ? "x" : "y";
|
|
28
|
+
const before = { scrollLeft: element.scrollLeft, scrollTop: element.scrollTop, scrollHeight: element.scrollHeight, scrollWidth: element.scrollWidth, clientHeight: element.clientHeight, clientWidth: element.clientWidth };
|
|
29
|
+
const parseAmount = () => {
|
|
30
|
+
const token = String(amountToken || "").trim().toLowerCase();
|
|
31
|
+
const extent = axis === "x" ? element.clientWidth : element.clientHeight;
|
|
32
|
+
if (!token) return Math.max(1, Math.floor(extent * 0.8));
|
|
33
|
+
if (token.endsWith("%")) {
|
|
34
|
+
const value = Number(token.slice(0, -1));
|
|
35
|
+
return Number.isFinite(value) ? Math.max(1, Math.floor(extent * value / 100)) : Math.max(1, Math.floor(extent * 0.8));
|
|
36
|
+
}
|
|
37
|
+
const pixels = Number(token.replace(/px$/, ""));
|
|
38
|
+
return Number.isFinite(pixels) && pixels > 0 ? Math.floor(pixels) : Math.max(1, Math.floor(extent * 0.8));
|
|
39
|
+
};
|
|
40
|
+
const delta = parseAmount() * (direction === "up" || direction === "left" ? -1 : 1);
|
|
41
|
+
if (axis === "x") element.scrollLeft += delta;
|
|
42
|
+
else element.scrollTop += delta;
|
|
43
|
+
const after = { scrollLeft: element.scrollLeft, scrollTop: element.scrollTop, scrollHeight: element.scrollHeight, scrollWidth: element.scrollWidth, clientHeight: element.clientHeight, clientWidth: element.clientWidth };
|
|
44
|
+
const moved = before.scrollLeft !== after.scrollLeft || before.scrollTop !== after.scrollTop;
|
|
45
|
+
return { status: moved ? "scrolled" : "no-movement", selector, direction, amount: amountToken || undefined, before, after };
|
|
46
|
+
})()`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildScrollResult(options: {
|
|
50
|
+
command: "scroll";
|
|
51
|
+
compatibilityWorkaround?: CompatibilityWorkaround;
|
|
52
|
+
effectiveArgs: string[];
|
|
53
|
+
message: string;
|
|
54
|
+
redactedArgs: string[];
|
|
55
|
+
result: Record<string, unknown>;
|
|
56
|
+
scrollField: "scrollContainer" | "scrollPage";
|
|
57
|
+
scrollValue: unknown;
|
|
58
|
+
sessionMode: "auto" | "fresh";
|
|
59
|
+
sessionName?: string;
|
|
60
|
+
succeeded: boolean;
|
|
61
|
+
usedImplicitSession: boolean;
|
|
62
|
+
}): AgentBrowserToolResult {
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: "text", text: options.message }],
|
|
65
|
+
details: {
|
|
66
|
+
args: options.redactedArgs,
|
|
67
|
+
command: options.command,
|
|
68
|
+
compatibilityWorkaround: options.compatibilityWorkaround,
|
|
69
|
+
data: options.result,
|
|
70
|
+
effectiveArgs: options.effectiveArgs,
|
|
71
|
+
nextActions: options.succeeded ? undefined : buildScrollNoopNextActions(options.sessionName),
|
|
72
|
+
[options.scrollField]: options.scrollValue,
|
|
73
|
+
sessionMode: options.sessionMode,
|
|
74
|
+
...buildAgentBrowserResultCategoryDetails({ args: options.effectiveArgs, command: options.command, errorText: options.succeeded ? undefined : options.message, succeeded: options.succeeded, validationError: options.succeeded ? undefined : options.message }),
|
|
75
|
+
...buildSessionDetailFields(options.sessionName, options.usedImplicitSession),
|
|
76
|
+
summary: options.message,
|
|
77
|
+
validationError: options.succeeded ? undefined : options.message,
|
|
78
|
+
},
|
|
79
|
+
isError: !options.succeeded,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function tryContainerScroll(options: {
|
|
84
|
+
commandTokens: string[];
|
|
85
|
+
compatibilityWorkaround?: CompatibilityWorkaround;
|
|
86
|
+
cwd: string;
|
|
87
|
+
effectiveArgs: string[];
|
|
88
|
+
redactedArgs: string[];
|
|
89
|
+
sessionMode: "auto" | "fresh";
|
|
90
|
+
sessionName?: string;
|
|
91
|
+
signal?: AbortSignal;
|
|
92
|
+
usedImplicitSession: boolean;
|
|
93
|
+
}): Promise<AgentBrowserToolResult | undefined> {
|
|
94
|
+
const request = getContainerScrollRequest(options.commandTokens);
|
|
95
|
+
if (!request || !options.sessionName) return undefined;
|
|
96
|
+
const data = await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, stdin: buildContainerScrollScript(request) });
|
|
97
|
+
const result = isRecord(data) && isRecord(data.result) ? data.result : data;
|
|
98
|
+
if (!isRecord(result) || typeof result.status !== "string") return undefined;
|
|
99
|
+
const succeeded = result.status === "scrolled";
|
|
100
|
+
const message = succeeded
|
|
101
|
+
? `Scrolled container ${request.selector} ${request.direction}${request.amount ? ` by ${request.amount}` : ""}.`
|
|
102
|
+
: `Scroll container ${request.selector} did not move (${result.status}).`;
|
|
103
|
+
return buildScrollResult({ ...options, command: "scroll", message, result, scrollField: "scrollContainer", scrollValue: { request, result }, succeeded });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getPageScrollToRequest(commandTokens: string[]): { target: "end" | "top" } | undefined {
|
|
107
|
+
if (commandTokens[0] !== "scroll" || commandTokens[1]?.toLowerCase() !== "to") return undefined;
|
|
108
|
+
const target = commandTokens[2]?.toLowerCase();
|
|
109
|
+
return target === "end" || target === "top" ? { target } : undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildPageScrollToScript(request: { target: "end" | "top" }): string {
|
|
113
|
+
return `(() => {
|
|
114
|
+
const target = ${JSON.stringify(request.target)};
|
|
115
|
+
const scroller = document.scrollingElement || document.documentElement || document.body;
|
|
116
|
+
if (!scroller) return { status: "no-scroller", target };
|
|
117
|
+
const before = { scrollLeft: scroller.scrollLeft, scrollTop: scroller.scrollTop, scrollHeight: scroller.scrollHeight, scrollWidth: scroller.scrollWidth, clientHeight: scroller.clientHeight, clientWidth: scroller.clientWidth };
|
|
118
|
+
const nextTop = target === "top" ? 0 : Math.max(0, scroller.scrollHeight - scroller.clientHeight);
|
|
119
|
+
const nextLeft = scroller.scrollLeft;
|
|
120
|
+
scroller.scrollTop = nextTop;
|
|
121
|
+
window.scrollTo(nextLeft, nextTop);
|
|
122
|
+
const after = { scrollLeft: scroller.scrollLeft, scrollTop: scroller.scrollTop, scrollHeight: scroller.scrollHeight, scrollWidth: scroller.scrollWidth, clientHeight: scroller.clientHeight, clientWidth: scroller.clientWidth };
|
|
123
|
+
const moved = before.scrollLeft !== after.scrollLeft || before.scrollTop !== after.scrollTop;
|
|
124
|
+
return { status: moved ? "scrolled" : "no-movement", target, before, after };
|
|
125
|
+
})()`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function tryPageScrollTo(options: {
|
|
129
|
+
commandTokens: string[];
|
|
130
|
+
compatibilityWorkaround?: CompatibilityWorkaround;
|
|
131
|
+
cwd: string;
|
|
132
|
+
effectiveArgs: string[];
|
|
133
|
+
redactedArgs: string[];
|
|
134
|
+
sessionMode: "auto" | "fresh";
|
|
135
|
+
sessionName?: string;
|
|
136
|
+
signal?: AbortSignal;
|
|
137
|
+
usedImplicitSession: boolean;
|
|
138
|
+
}): Promise<AgentBrowserToolResult | undefined> {
|
|
139
|
+
const request = getPageScrollToRequest(options.commandTokens);
|
|
140
|
+
if (!request || !options.sessionName) return undefined;
|
|
141
|
+
const data = await runSessionCommandData({ args: ["eval", "--stdin"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal, stdin: buildPageScrollToScript(request) });
|
|
142
|
+
const result = isRecord(data) && isRecord(data.result) ? data.result : data;
|
|
143
|
+
if (!isRecord(result) || typeof result.status !== "string") return undefined;
|
|
144
|
+
const succeeded = result.status === "scrolled";
|
|
145
|
+
const message = succeeded ? `Scrolled page to ${request.target}.` : `Scroll to ${request.target} completed with no observed movement (${result.status}).`;
|
|
146
|
+
return buildScrollResult({ ...options, command: "scroll", message, result, scrollField: "scrollPage", scrollValue: { request, result }, succeeded });
|
|
147
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { isRecord } from "../../../parsing.js";
|
|
2
|
+
import { buildAgentBrowserResultCategoryDetails } from "../../../results.js";
|
|
3
|
+
import { buildSnapshotPresentation } from "../../../results/snapshot.js";
|
|
4
|
+
import { extractRefSnapshotFromData, type SessionRefSnapshot } from "../../../session-page-state.js";
|
|
5
|
+
import type { CompatibilityWorkaround } from "../../../runtime.js";
|
|
6
|
+
import { collectScrollPositionSnapshot } from "../diagnostics.js";
|
|
7
|
+
import { buildSessionDetailFields, runSessionCommandData } from "../session-state.js";
|
|
8
|
+
import type { SessionArtifactManifest } from "../../../results/contracts.js";
|
|
9
|
+
import type { PersistentSessionArtifactStore } from "../../../temp.js";
|
|
10
|
+
import type { AgentBrowserToolResult, BrowserRunOptions } from "../types.js";
|
|
11
|
+
|
|
12
|
+
export interface SnapshotFilterResult {
|
|
13
|
+
artifactManifest?: SessionArtifactManifest;
|
|
14
|
+
result: AgentBrowserToolResult;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SnapshotFilterRequest {
|
|
18
|
+
cleanArgs: string[];
|
|
19
|
+
diff?: boolean;
|
|
20
|
+
role?: string;
|
|
21
|
+
search?: string;
|
|
22
|
+
viewport?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseSnapshotFilterRequest(commandTokens: string[]): SnapshotFilterRequest | undefined {
|
|
26
|
+
if (commandTokens[0] !== "snapshot") return undefined;
|
|
27
|
+
const cleanArgs: string[] = [];
|
|
28
|
+
let role: string | undefined;
|
|
29
|
+
let search: string | undefined;
|
|
30
|
+
for (let index = 0; index < commandTokens.length; index += 1) {
|
|
31
|
+
const token = commandTokens[index];
|
|
32
|
+
if (token === "--viewport") continue;
|
|
33
|
+
if (token === "--diff") continue;
|
|
34
|
+
if (token === "--search") {
|
|
35
|
+
const value = commandTokens[index + 1];
|
|
36
|
+
if (typeof value === "string" && !value.startsWith("-")) {
|
|
37
|
+
search = value;
|
|
38
|
+
index += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (token === "--filter") {
|
|
43
|
+
const value = commandTokens[index + 1];
|
|
44
|
+
if (typeof value === "string" && !value.startsWith("-")) {
|
|
45
|
+
const roleMatch = /^role=(.+)$/i.exec(value.trim());
|
|
46
|
+
if (roleMatch?.[1]) role = roleMatch[1].trim().toLowerCase();
|
|
47
|
+
index += 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
cleanArgs.push(token);
|
|
52
|
+
}
|
|
53
|
+
const viewport = commandTokens.includes("--viewport");
|
|
54
|
+
const diff = commandTokens.includes("--diff");
|
|
55
|
+
if (!search && !role && !viewport && !diff) return undefined;
|
|
56
|
+
return { cleanArgs, diff, role, search, viewport };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface SnapshotDiffSummary {
|
|
60
|
+
addedRefs: string[];
|
|
61
|
+
changedRefs: string[];
|
|
62
|
+
removedRefs: string[];
|
|
63
|
+
summary: string;
|
|
64
|
+
unchangedRefs: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildSnapshotDiff(previous: SessionRefSnapshot | undefined, current: SessionRefSnapshot | undefined): SnapshotDiffSummary | undefined {
|
|
68
|
+
if (!current) return undefined;
|
|
69
|
+
const currentRefs = current.refs ?? {};
|
|
70
|
+
const previousRefs = previous?.refs ?? {};
|
|
71
|
+
if (!previous) return { addedRefs: Object.keys(currentRefs), changedRefs: [], removedRefs: [], summary: `Snapshot diff: no previous snapshot; ${Object.keys(currentRefs).length} current refs recorded.`, unchangedRefs: 0 };
|
|
72
|
+
const addedRefs: string[] = [];
|
|
73
|
+
const removedRefs: string[] = [];
|
|
74
|
+
const changedRefs: string[] = [];
|
|
75
|
+
let unchangedRefs = 0;
|
|
76
|
+
for (const refId of Object.keys(currentRefs)) {
|
|
77
|
+
const currentRef = currentRefs[refId];
|
|
78
|
+
const previousRef = previousRefs[refId];
|
|
79
|
+
if (!previousRef) {
|
|
80
|
+
addedRefs.push(refId);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (previousRef.role !== currentRef.role || previousRef.name !== currentRef.name) changedRefs.push(refId);
|
|
84
|
+
else unchangedRefs += 1;
|
|
85
|
+
}
|
|
86
|
+
for (const refId of Object.keys(previousRefs)) if (!currentRefs[refId]) removedRefs.push(refId);
|
|
87
|
+
return { addedRefs, changedRefs, removedRefs, summary: `Snapshot diff: +${addedRefs.length} / -${removedRefs.length} / Δ${changedRefs.length} refs versus previous snapshot.`, unchangedRefs };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function filterSnapshotData(data: unknown, request: SnapshotFilterRequest): { data: Record<string, unknown>; matchedRefs: number; totalRefs: number; totalLines: number; visibleLines: number } | undefined {
|
|
91
|
+
if (!isRecord(data)) return undefined;
|
|
92
|
+
const refs = isRecord(data.refs) ? data.refs : {};
|
|
93
|
+
const snapshot = typeof data.snapshot === "string" ? data.snapshot : "";
|
|
94
|
+
const normalizedSearch = request.search?.trim().toLowerCase();
|
|
95
|
+
const matchingRefIds = new Set<string>();
|
|
96
|
+
for (const [refId, refValue] of Object.entries(refs)) {
|
|
97
|
+
if (!isRecord(refValue)) continue;
|
|
98
|
+
const role = typeof refValue.role === "string" ? refValue.role.toLowerCase() : "";
|
|
99
|
+
const name = typeof refValue.name === "string" ? refValue.name : "";
|
|
100
|
+
const roleMatches = request.role ? role === request.role : true;
|
|
101
|
+
const searchMatches = normalizedSearch ? `${role} ${name}`.toLowerCase().includes(normalizedSearch) : true;
|
|
102
|
+
if (roleMatches && searchMatches) matchingRefIds.add(refId);
|
|
103
|
+
}
|
|
104
|
+
const lines = snapshot.split(/\r?\n/);
|
|
105
|
+
const visibleLines = lines.filter((line) => {
|
|
106
|
+
const normalizedLine = line.toLowerCase();
|
|
107
|
+
if (normalizedSearch && normalizedLine.includes(normalizedSearch)) return true;
|
|
108
|
+
return [...matchingRefIds].some((refId) => line.includes(`[ref=${refId}]`) || line.includes(`ref=${refId}`));
|
|
109
|
+
});
|
|
110
|
+
const filteredRefs = Object.fromEntries(Object.entries(refs).filter(([refId]) => matchingRefIds.has(refId)));
|
|
111
|
+
const description = [request.role ? `role=${request.role}` : undefined, request.search ? `search=${JSON.stringify(request.search)}` : undefined].filter((part): part is string => part !== undefined).join(", ");
|
|
112
|
+
const filteredSnapshot = visibleLines.length > 0 ? visibleLines.join("\n") : `(no snapshot lines matched ${description})`;
|
|
113
|
+
return {
|
|
114
|
+
data: { ...data, refs: filteredRefs, snapshot: filteredSnapshot },
|
|
115
|
+
matchedRefs: Object.keys(filteredRefs).length,
|
|
116
|
+
totalRefs: Object.keys(refs).length,
|
|
117
|
+
totalLines: lines.filter((line) => line.length > 0).length,
|
|
118
|
+
visibleLines: visibleLines.length,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function trySnapshotFilter(options: {
|
|
123
|
+
artifactManifest?: SessionArtifactManifest;
|
|
124
|
+
commandTokens: string[];
|
|
125
|
+
compatibilityWorkaround?: CompatibilityWorkaround;
|
|
126
|
+
cwd: string;
|
|
127
|
+
effectiveArgs: string[];
|
|
128
|
+
persistentArtifactStore?: PersistentSessionArtifactStore;
|
|
129
|
+
redactedArgs: string[];
|
|
130
|
+
previousRefSnapshot?: SessionRefSnapshot;
|
|
131
|
+
sessionMode: "auto" | "fresh";
|
|
132
|
+
sessionName?: string;
|
|
133
|
+
sessionPageState: BrowserRunOptions["state"]["sessionPageState"];
|
|
134
|
+
sessionPageStateUpdate: ReturnType<BrowserRunOptions["state"]["sessionPageState"]["beginUpdate"]>;
|
|
135
|
+
signal?: AbortSignal;
|
|
136
|
+
usedImplicitSession: boolean;
|
|
137
|
+
}): Promise<SnapshotFilterResult | undefined> {
|
|
138
|
+
const request = parseSnapshotFilterRequest(options.commandTokens);
|
|
139
|
+
if (!request || !options.sessionName) return undefined;
|
|
140
|
+
const snapshotData = await runSessionCommandData({ args: request.cleanArgs, cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
141
|
+
const filtered = request.role || request.search ? filterSnapshotData(snapshotData, request) : isRecord(snapshotData) ? { data: snapshotData, matchedRefs: isRecord(snapshotData.refs) ? Object.keys(snapshotData.refs).length : 0, totalLines: typeof snapshotData.snapshot === "string" ? snapshotData.snapshot.split(/\r?\n/).filter((line) => line.length > 0).length : 0, totalRefs: isRecord(snapshotData.refs) ? Object.keys(snapshotData.refs).length : 0, visibleLines: typeof snapshotData.snapshot === "string" ? snapshotData.snapshot.split(/\r?\n/).filter((line) => line.length > 0).length : 0 } : undefined;
|
|
142
|
+
if (!filtered) return undefined;
|
|
143
|
+
const viewport = request.viewport ? await collectScrollPositionSnapshot({ cwd: options.cwd, sessionName: options.sessionName, signal: options.signal }) : undefined;
|
|
144
|
+
const fullSnapshot = extractRefSnapshotFromData(snapshotData);
|
|
145
|
+
const diff = request.diff ? buildSnapshotDiff(options.previousRefSnapshot, fullSnapshot) : undefined;
|
|
146
|
+
if (fullSnapshot) options.sessionPageState.applyRefSnapshot({ sessionName: options.sessionName, snapshot: fullSnapshot, update: options.sessionPageStateUpdate });
|
|
147
|
+
const presentation = await buildSnapshotPresentation(filtered.data, options.persistentArtifactStore, options.artifactManifest);
|
|
148
|
+
const summary = request.role || request.search
|
|
149
|
+
? `Snapshot filter: ${filtered.matchedRefs}/${filtered.totalRefs} direct refs matched${request.role ? ` role=${request.role}` : ""}${request.search ? ` search ${JSON.stringify(request.search)}` : ""}; ${filtered.visibleLines} surrounding snapshot line${filtered.visibleLines === 1 ? "" : "s"} shown.`
|
|
150
|
+
: request.diff
|
|
151
|
+
? diff?.summary ?? "Snapshot diff unavailable."
|
|
152
|
+
: "Snapshot viewport metadata collected.";
|
|
153
|
+
const viewportText = viewport ? `Viewport: ${viewport.innerWidth}×${viewport.innerHeight}, scroll ${viewport.scrollX},${viewport.scrollY}, document ${viewport.scrollWidth}×${viewport.scrollHeight}, sampled scroll containers ${viewport.containers.length}/${viewport.containerCount}.` : undefined;
|
|
154
|
+
const diffText = diff && (request.role || request.search) ? diff.summary : undefined;
|
|
155
|
+
const prefix = [summary, diffText, viewportText].filter((line): line is string => line !== undefined).join("\n");
|
|
156
|
+
if (presentation.content[0]?.type === "text") presentation.content[0] = { ...presentation.content[0], text: `${prefix}\n\n${presentation.content[0].text}` };
|
|
157
|
+
return {
|
|
158
|
+
artifactManifest: presentation.artifactManifest,
|
|
159
|
+
result: {
|
|
160
|
+
content: presentation.content,
|
|
161
|
+
details: {
|
|
162
|
+
args: options.redactedArgs,
|
|
163
|
+
artifactManifest: presentation.artifactManifest,
|
|
164
|
+
artifactRetentionSummary: presentation.artifactRetentionSummary,
|
|
165
|
+
command: "snapshot",
|
|
166
|
+
compatibilityWorkaround: options.compatibilityWorkaround,
|
|
167
|
+
data: presentation.data,
|
|
168
|
+
effectiveArgs: options.effectiveArgs,
|
|
169
|
+
fullOutputPath: presentation.fullOutputPath,
|
|
170
|
+
fullOutputPaths: presentation.fullOutputPaths,
|
|
171
|
+
refSnapshot: fullSnapshot,
|
|
172
|
+
sessionMode: options.sessionMode,
|
|
173
|
+
snapshotDiff: diff,
|
|
174
|
+
snapshotFilter: request.role || request.search ? { cleanArgs: request.cleanArgs, matchedRefs: filtered.matchedRefs, role: request.role, search: request.search, totalLines: filtered.totalLines, totalRefs: filtered.totalRefs, visibleLines: filtered.visibleLines } : undefined,
|
|
175
|
+
snapshotViewport: viewport,
|
|
176
|
+
...buildAgentBrowserResultCategoryDetails({ args: options.effectiveArgs, command: "snapshot", succeeded: true }),
|
|
177
|
+
...buildSessionDetailFields(options.sessionName, options.usedImplicitSession),
|
|
178
|
+
summary,
|
|
179
|
+
},
|
|
180
|
+
isError: false,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getAgentBrowserProcessTimeoutMs } from "../../../process.js";
|
|
2
|
+
import { parseValidBatchStepEntries } from "../../batch-stdin.js";
|
|
3
|
+
|
|
4
|
+
const WAIT_PROCESS_TIMEOUT_GRACE_MS = 5_000;
|
|
5
|
+
|
|
6
|
+
function parseMillisecondsToken(token: string | undefined): number | undefined {
|
|
7
|
+
if (token === undefined || !/^\d+$/.test(token)) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
const parsed = Number(token);
|
|
11
|
+
return Number.isSafeInteger(parsed) ? parsed : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function findWaitTimeoutMs(commandTokens: string[]): number | undefined {
|
|
15
|
+
if (commandTokens[0] !== "wait") {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
for (let index = 1; index < commandTokens.length; index += 1) {
|
|
19
|
+
const token = commandTokens[index];
|
|
20
|
+
if (token === "--timeout") {
|
|
21
|
+
return parseMillisecondsToken(commandTokens[index + 1]);
|
|
22
|
+
}
|
|
23
|
+
if (token.startsWith("--timeout=")) {
|
|
24
|
+
return parseMillisecondsToken(token.slice("--timeout=".length));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const firstWaitArgument = commandTokens[1];
|
|
28
|
+
if (firstWaitArgument && !firstWaitArgument.startsWith("-")) {
|
|
29
|
+
return parseMillisecondsToken(firstWaitArgument);
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function findWaitTimeoutBudgetMs(commandTokens: string[], stdin: string | undefined): number | undefined {
|
|
35
|
+
const directWaitTimeout = findWaitTimeoutMs(commandTokens);
|
|
36
|
+
if (directWaitTimeout !== undefined) {
|
|
37
|
+
return directWaitTimeout;
|
|
38
|
+
}
|
|
39
|
+
if (commandTokens[0] !== "batch" || stdin === undefined) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
let batchWaitTimeoutTotal = 0;
|
|
43
|
+
for (const { step } of parseValidBatchStepEntries(stdin)) {
|
|
44
|
+
const waitTimeout = findWaitTimeoutMs(step);
|
|
45
|
+
if (waitTimeout !== undefined) {
|
|
46
|
+
batchWaitTimeoutTotal += waitTimeout;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return batchWaitTimeoutTotal === 0 ? undefined : batchWaitTimeoutTotal;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getWaitAwareProcessTimeoutMs(commandTokens: string[], stdin: string | undefined): number | undefined {
|
|
53
|
+
const waitTimeoutBudgetMs = findWaitTimeoutBudgetMs(commandTokens, stdin);
|
|
54
|
+
if (waitTimeoutBudgetMs === undefined) return undefined;
|
|
55
|
+
const neededTimeoutMs = waitTimeoutBudgetMs + WAIT_PROCESS_TIMEOUT_GRACE_MS;
|
|
56
|
+
const defaultProcessTimeoutMs = getAgentBrowserProcessTimeoutMs();
|
|
57
|
+
return neededTimeoutMs > defaultProcessTimeoutMs ? neededTimeoutMs : undefined;
|
|
58
|
+
}
|