pi-agent-browser-native 0.2.26 → 0.2.28
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 +22 -0
- package/README.md +18 -4
- package/docs/COMMAND_REFERENCE.md +13 -9
- package/docs/RELEASE.md +25 -2
- package/docs/SUPPORT_MATRIX.md +18 -10
- package/docs/TOOL_CONTRACT.md +14 -9
- package/extensions/agent-browser/index.ts +528 -22
- package/extensions/agent-browser/lib/playbook.ts +26 -6
- package/extensions/agent-browser/lib/results/presentation.ts +17 -2
- package/extensions/agent-browser/lib/results.ts +1 -0
- package/extensions/agent-browser/lib/runtime.ts +2 -2
- package/package.json +1 -1
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* Invariants/Assumptions: agent-browser is installed separately on PATH, the wrapper targets the current locally installed upstream version only, and no backward-compatibility shims are provided.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { constants as fsConstants } from "node:fs";
|
|
10
|
+
import { access, copyFile, mkdir, readFile, readdir, rm, stat } from "node:fs/promises";
|
|
11
|
+
import { delimiter, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
11
13
|
|
|
12
14
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
13
15
|
import {
|
|
@@ -30,6 +32,7 @@ import {
|
|
|
30
32
|
buildAgentBrowserNextActions,
|
|
31
33
|
buildAgentBrowserResultCategoryDetails,
|
|
32
34
|
buildToolPresentation,
|
|
35
|
+
compareRefIds,
|
|
33
36
|
getAgentBrowserErrorText,
|
|
34
37
|
parseAgentBrowserEnvelope,
|
|
35
38
|
type AgentBrowserBatchResult,
|
|
@@ -84,6 +87,7 @@ const PACKAGE_NAME = "pi-agent-browser-native";
|
|
|
84
87
|
const AGENT_BROWSER_SEMANTIC_ACTIONS = ["check", "click", "fill", "select", "uncheck"] as const;
|
|
85
88
|
const AGENT_BROWSER_SEMANTIC_LOCATORS = ["alt", "label", "placeholder", "role", "testid", "text", "title"] as const;
|
|
86
89
|
const AGENT_BROWSER_JOB_STEP_ACTIONS = ["open", "click", "fill", "wait", "assertText", "assertUrl", "waitForDownload", "screenshot"] as const;
|
|
90
|
+
const AGENT_BROWSER_QA_LOAD_STATES = ["domcontentloaded", "load", "networkidle"] as const;
|
|
87
91
|
const SOURCE_LOOKUP_WORKSPACE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
88
92
|
const SOURCE_LOOKUP_IGNORED_DIRECTORIES = new Set([".git", "node_modules", "dist", "build", "coverage", ".next", "out", "tmp", "temp"]);
|
|
89
93
|
const SOURCE_LOOKUP_DEFAULT_MAX_WORKSPACE_FILES = 2_000;
|
|
@@ -92,6 +96,7 @@ const SOURCE_LOOKUP_MAX_WORKSPACE_FILES = 5_000;
|
|
|
92
96
|
type AgentBrowserSemanticActionName = (typeof AGENT_BROWSER_SEMANTIC_ACTIONS)[number];
|
|
93
97
|
type AgentBrowserSemanticLocator = (typeof AGENT_BROWSER_SEMANTIC_LOCATORS)[number];
|
|
94
98
|
type AgentBrowserJobStepAction = (typeof AGENT_BROWSER_JOB_STEP_ACTIONS)[number];
|
|
99
|
+
type AgentBrowserQaLoadState = (typeof AGENT_BROWSER_QA_LOAD_STATES)[number];
|
|
95
100
|
type AgentBrowserSourceLookupStatus = "candidates-found" | "no-candidates" | "unsupported";
|
|
96
101
|
type AgentBrowserNetworkSourceLookupStatus = "failed-requests-found" | "no-failed-requests" | "no-candidates";
|
|
97
102
|
|
|
@@ -111,6 +116,48 @@ interface CompiledAgentBrowserSemanticAction {
|
|
|
111
116
|
args: string[];
|
|
112
117
|
}
|
|
113
118
|
|
|
119
|
+
interface ScrollPositionSnapshot {
|
|
120
|
+
containerCount: number;
|
|
121
|
+
containers: Array<{ id: string; scrollLeft: number; scrollTop: number }>;
|
|
122
|
+
innerHeight: number;
|
|
123
|
+
innerWidth: number;
|
|
124
|
+
scrollHeight: number;
|
|
125
|
+
scrollWidth: number;
|
|
126
|
+
scrollX: number;
|
|
127
|
+
scrollY: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface ScrollNoopDiagnostic {
|
|
131
|
+
after: ScrollPositionSnapshot;
|
|
132
|
+
before: ScrollPositionSnapshot;
|
|
133
|
+
message: string;
|
|
134
|
+
reason: "no-observed-scroll-position-change";
|
|
135
|
+
recommendations: string[];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface ComboboxFocusDiagnostic {
|
|
139
|
+
activeElement: {
|
|
140
|
+
expanded?: string;
|
|
141
|
+
hasPopup?: string;
|
|
142
|
+
name?: string;
|
|
143
|
+
role?: string;
|
|
144
|
+
tagName?: string;
|
|
145
|
+
};
|
|
146
|
+
message: string;
|
|
147
|
+
reason: "focused-combobox-without-visible-options";
|
|
148
|
+
recommendations: string[];
|
|
149
|
+
visibleListboxCount: number;
|
|
150
|
+
visibleOptionCount: number;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface RecordingDependencyWarning {
|
|
154
|
+
command: "record start" | "record restart";
|
|
155
|
+
dependency: "ffmpeg";
|
|
156
|
+
message: string;
|
|
157
|
+
reason: "ffmpeg-missing-for-recording";
|
|
158
|
+
recommendations: string[];
|
|
159
|
+
}
|
|
160
|
+
|
|
114
161
|
interface CompiledAgentBrowserJobStep {
|
|
115
162
|
action: AgentBrowserJobStepAction;
|
|
116
163
|
args: string[];
|
|
@@ -127,6 +174,7 @@ interface CompiledAgentBrowserQaPreset extends CompiledAgentBrowserJob {
|
|
|
127
174
|
checkConsole: boolean;
|
|
128
175
|
checkErrors: boolean;
|
|
129
176
|
checkNetwork: boolean;
|
|
177
|
+
loadState: AgentBrowserQaLoadState;
|
|
130
178
|
expectedText: string[];
|
|
131
179
|
expectedSelector?: string;
|
|
132
180
|
screenshotPath?: string;
|
|
@@ -238,6 +286,7 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
238
286
|
checkConsole: Type.Optional(Type.Boolean({ description: "Whether to fail on console error messages. Defaults to true." })),
|
|
239
287
|
checkErrors: Type.Optional(Type.Boolean({ description: "Whether to fail on page errors. Defaults to true." })),
|
|
240
288
|
checkNetwork: Type.Optional(Type.Boolean({ description: "Whether to inspect network requests and fail on actionable request failures; benign icon misses warn. Defaults to true." })),
|
|
289
|
+
loadState: Type.Optional(StringEnum(AGENT_BROWSER_QA_LOAD_STATES, { description: "Page readiness state for the QA preset before assertions and diagnostics. Defaults to domcontentloaded; use networkidle only for pages without long-lived background requests." })),
|
|
241
290
|
}),
|
|
242
291
|
),
|
|
243
292
|
sourceLookup: Type.Optional(
|
|
@@ -450,16 +499,21 @@ function compileAgentBrowserQaPreset(input: unknown): { compiled?: CompiledAgent
|
|
|
450
499
|
return { error: `qa.${field} must be a boolean when provided.` };
|
|
451
500
|
}
|
|
452
501
|
}
|
|
502
|
+
const rawLoadState = input.loadState;
|
|
503
|
+
if (rawLoadState !== undefined && (typeof rawLoadState !== "string" || !AGENT_BROWSER_QA_LOAD_STATES.includes(rawLoadState as AgentBrowserQaLoadState))) {
|
|
504
|
+
return { error: `qa.loadState must be one of: ${AGENT_BROWSER_QA_LOAD_STATES.join(", ")}.` };
|
|
505
|
+
}
|
|
453
506
|
const checkConsole = input.checkConsole !== false;
|
|
454
507
|
const checkErrors = input.checkErrors !== false;
|
|
455
508
|
const checkNetwork = input.checkNetwork !== false;
|
|
509
|
+
const loadState = (rawLoadState as AgentBrowserQaLoadState | undefined) ?? "domcontentloaded";
|
|
456
510
|
const steps: CompiledAgentBrowserJobStep[] = [];
|
|
457
511
|
if (checkNetwork) steps.push({ action: "wait", args: ["network", "requests", "--clear"] });
|
|
458
512
|
if (checkConsole) steps.push({ action: "wait", args: ["console", "--clear"] });
|
|
459
513
|
if (checkErrors) steps.push({ action: "wait", args: ["errors", "--clear"] });
|
|
460
514
|
steps.push(
|
|
461
515
|
{ action: "open", args: ["open", url] },
|
|
462
|
-
{ action: "wait", args: ["wait", "--load",
|
|
516
|
+
{ action: "wait", args: ["wait", "--load", loadState] },
|
|
463
517
|
);
|
|
464
518
|
for (const text of expectedText) {
|
|
465
519
|
steps.push({ action: "assertText", args: ["wait", "--text", text] });
|
|
@@ -474,7 +528,7 @@ function compileAgentBrowserQaPreset(input: unknown): { compiled?: CompiledAgent
|
|
|
474
528
|
return {
|
|
475
529
|
compiled: {
|
|
476
530
|
args: ["batch"],
|
|
477
|
-
checks: { checkConsole, checkErrors, checkNetwork, expectedSelector, expectedText, screenshotPath, url },
|
|
531
|
+
checks: { checkConsole, checkErrors, checkNetwork, expectedSelector, expectedText, loadState, screenshotPath, url },
|
|
478
532
|
stdin: JSON.stringify(steps.map((step) => step.args)),
|
|
479
533
|
steps,
|
|
480
534
|
},
|
|
@@ -969,6 +1023,61 @@ function buildSemanticActionCandidateActions(compiled: CompiledAgentBrowserSeman
|
|
|
969
1023
|
return [];
|
|
970
1024
|
}
|
|
971
1025
|
|
|
1026
|
+
function normalizeSemanticActionAccessibleName(name: string): string {
|
|
1027
|
+
return name.replace(/\s+/g, " ").trim().toLowerCase();
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function semanticActionNameMatches(candidateName: string, targetName: string): boolean {
|
|
1031
|
+
const normalizedCandidate = normalizeSemanticActionAccessibleName(candidateName);
|
|
1032
|
+
const normalizedTarget = normalizeSemanticActionAccessibleName(targetName);
|
|
1033
|
+
return normalizedCandidate === normalizedTarget || normalizedCandidate.startsWith(`${normalizedTarget} `);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function getCompiledSemanticActionRoleTarget(compiled: CompiledAgentBrowserSemanticAction): { role: string; targetName: string } | undefined {
|
|
1037
|
+
if (compiled.locator !== "role" || !["check", "click", "uncheck"].includes(compiled.action)) return undefined;
|
|
1038
|
+
const findIndex = compiled.args.indexOf("find");
|
|
1039
|
+
if (findIndex < 0 || compiled.args[findIndex + 1] !== "role") return undefined;
|
|
1040
|
+
const role = compiled.args[findIndex + 2];
|
|
1041
|
+
const nameFlagIndex = compiled.args.indexOf("--name");
|
|
1042
|
+
const targetName = nameFlagIndex >= 0 ? compiled.args[nameFlagIndex + 1] : undefined;
|
|
1043
|
+
if (!role || !targetName) return undefined;
|
|
1044
|
+
return { role, targetName };
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function findSemanticActionRefInSnapshot(compiled: CompiledAgentBrowserSemanticAction, snapshotData: unknown): string | undefined {
|
|
1048
|
+
const target = getCompiledSemanticActionRoleTarget(compiled);
|
|
1049
|
+
const refs = getSnapshotRefRecord(snapshotData);
|
|
1050
|
+
if (!target || !refs) return undefined;
|
|
1051
|
+
const candidates = Object.entries(refs).flatMap(([ref, entry]) => {
|
|
1052
|
+
if (!/^e\d+$/.test(ref) || !isRecord(entry)) return [];
|
|
1053
|
+
const role = typeof entry.role === "string" ? entry.role : undefined;
|
|
1054
|
+
const name = typeof entry.name === "string" ? entry.name : undefined;
|
|
1055
|
+
if (!role || !name || role.toLowerCase() !== target.role.toLowerCase() || !semanticActionNameMatches(name, target.targetName)) return [];
|
|
1056
|
+
return [{ exact: normalizeSemanticActionAccessibleName(name) === normalizeSemanticActionAccessibleName(target.targetName), name, ref }];
|
|
1057
|
+
});
|
|
1058
|
+
candidates.sort((left, right) => Number(right.exact) - Number(left.exact) || left.name.length - right.name.length || compareRefIds(left.ref, right.ref));
|
|
1059
|
+
return candidates[0]?.ref;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
interface SemanticActionVisibleRefResolution {
|
|
1063
|
+
args: string[];
|
|
1064
|
+
snapshot: SessionRefSnapshot;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
async function resolveSemanticActionVisibleRefArgs(options: {
|
|
1068
|
+
compiled: CompiledAgentBrowserSemanticAction | undefined;
|
|
1069
|
+
cwd: string;
|
|
1070
|
+
sessionName?: string;
|
|
1071
|
+
signal?: AbortSignal;
|
|
1072
|
+
}): Promise<SemanticActionVisibleRefResolution | undefined> {
|
|
1073
|
+
if (!options.compiled || !options.sessionName || !getCompiledSemanticActionRoleTarget(options.compiled)) return undefined;
|
|
1074
|
+
const snapshotData = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
1075
|
+
const ref = findSemanticActionRefInSnapshot(options.compiled, snapshotData);
|
|
1076
|
+
const snapshot = extractRefSnapshotFromData(snapshotData);
|
|
1077
|
+
if (!ref || !snapshot) return undefined;
|
|
1078
|
+
return { args: [...getCompiledSemanticActionSessionPrefix(options.compiled), options.compiled.action, `@${ref}`], snapshot };
|
|
1079
|
+
}
|
|
1080
|
+
|
|
972
1081
|
function compileAgentBrowserSemanticAction(input: unknown): { compiled?: CompiledAgentBrowserSemanticAction; error?: string } {
|
|
973
1082
|
if (!isRecord(input)) {
|
|
974
1083
|
return { error: "semanticAction must be an object." };
|
|
@@ -2615,6 +2724,140 @@ async function collectNavigationSummary(options: {
|
|
|
2615
2724
|
return { title, url };
|
|
2616
2725
|
}
|
|
2617
2726
|
|
|
2727
|
+
function extractScrollPositionSnapshot(data: unknown): ScrollPositionSnapshot | undefined {
|
|
2728
|
+
const result = isRecord(data) && isRecord(data.result) ? data.result : data;
|
|
2729
|
+
if (!isRecord(result)) return undefined;
|
|
2730
|
+
const scrollX = typeof result.scrollX === "number" ? result.scrollX : undefined;
|
|
2731
|
+
const scrollY = typeof result.scrollY === "number" ? result.scrollY : undefined;
|
|
2732
|
+
const innerHeight = typeof result.innerHeight === "number" ? result.innerHeight : undefined;
|
|
2733
|
+
const innerWidth = typeof result.innerWidth === "number" ? result.innerWidth : undefined;
|
|
2734
|
+
const scrollHeight = typeof result.scrollHeight === "number" ? result.scrollHeight : undefined;
|
|
2735
|
+
const scrollWidth = typeof result.scrollWidth === "number" ? result.scrollWidth : undefined;
|
|
2736
|
+
if (scrollX === undefined || scrollY === undefined || innerHeight === undefined || innerWidth === undefined || scrollHeight === undefined || scrollWidth === undefined) return undefined;
|
|
2737
|
+
const containers = Array.isArray(result.containers)
|
|
2738
|
+
? result.containers.flatMap((entry, index): ScrollPositionSnapshot["containers"] => {
|
|
2739
|
+
if (!isRecord(entry)) return [];
|
|
2740
|
+
const rawId = typeof entry.id === "string" ? entry.id : undefined;
|
|
2741
|
+
const id = rawId && /^\d+:[a-z][a-z0-9-]*(?:\[role=[a-z-]+\])?$/i.test(rawId) ? rawId : `sample-${index}`;
|
|
2742
|
+
const scrollTop = typeof entry.scrollTop === "number" ? entry.scrollTop : undefined;
|
|
2743
|
+
const scrollLeft = typeof entry.scrollLeft === "number" ? entry.scrollLeft : undefined;
|
|
2744
|
+
return scrollTop !== undefined && scrollLeft !== undefined ? [{ id, scrollLeft, scrollTop }] : [];
|
|
2745
|
+
})
|
|
2746
|
+
: [];
|
|
2747
|
+
return {
|
|
2748
|
+
containerCount: typeof result.containerCount === "number" ? result.containerCount : containers.length,
|
|
2749
|
+
containers,
|
|
2750
|
+
innerHeight,
|
|
2751
|
+
innerWidth,
|
|
2752
|
+
scrollHeight,
|
|
2753
|
+
scrollWidth,
|
|
2754
|
+
scrollX,
|
|
2755
|
+
scrollY,
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
const SCROLL_POSITION_EVAL = `(() => {
|
|
2760
|
+
const viewport = {
|
|
2761
|
+
scrollX: window.scrollX,
|
|
2762
|
+
scrollY: window.scrollY,
|
|
2763
|
+
innerHeight: window.innerHeight,
|
|
2764
|
+
innerWidth: window.innerWidth,
|
|
2765
|
+
scrollHeight: Math.max(document.documentElement?.scrollHeight || 0, document.body?.scrollHeight || 0),
|
|
2766
|
+
scrollWidth: Math.max(document.documentElement?.scrollWidth || 0, document.body?.scrollWidth || 0),
|
|
2767
|
+
};
|
|
2768
|
+
const describe = (element, index) => {
|
|
2769
|
+
const role = element.getAttribute("role") || "";
|
|
2770
|
+
const id = element.tagName.toLowerCase();
|
|
2771
|
+
return {
|
|
2772
|
+
id: String(index) + ":" + id + (role ? "[role=" + role + "]" : ""),
|
|
2773
|
+
scrollTop: element.scrollTop,
|
|
2774
|
+
scrollLeft: element.scrollLeft,
|
|
2775
|
+
area: element.clientWidth * element.clientHeight,
|
|
2776
|
+
};
|
|
2777
|
+
};
|
|
2778
|
+
const containers = Array.from(document.querySelectorAll("body *"))
|
|
2779
|
+
.filter((element) => element instanceof HTMLElement && (element.scrollHeight > element.clientHeight + 1 || element.scrollWidth > element.clientWidth + 1))
|
|
2780
|
+
.map(describe)
|
|
2781
|
+
.sort((left, right) => right.area - left.area)
|
|
2782
|
+
.slice(0, 10)
|
|
2783
|
+
.map(({ area, ...entry }) => entry);
|
|
2784
|
+
return { ...viewport, containerCount: containers.length, containers };
|
|
2785
|
+
})()`;
|
|
2786
|
+
|
|
2787
|
+
async function collectScrollPositionSnapshot(options: {
|
|
2788
|
+
cwd: string;
|
|
2789
|
+
sessionName?: string;
|
|
2790
|
+
signal?: AbortSignal;
|
|
2791
|
+
}): Promise<ScrollPositionSnapshot | undefined> {
|
|
2792
|
+
return extractScrollPositionSnapshot(await runSessionCommandData({
|
|
2793
|
+
args: ["eval", "--stdin"],
|
|
2794
|
+
cwd: options.cwd,
|
|
2795
|
+
sessionName: options.sessionName,
|
|
2796
|
+
signal: options.signal,
|
|
2797
|
+
stdin: SCROLL_POSITION_EVAL,
|
|
2798
|
+
}));
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
function sameScrollPositionSnapshot(left: ScrollPositionSnapshot, right: ScrollPositionSnapshot): boolean {
|
|
2802
|
+
if (
|
|
2803
|
+
left.scrollX !== right.scrollX ||
|
|
2804
|
+
left.scrollY !== right.scrollY ||
|
|
2805
|
+
left.scrollHeight !== right.scrollHeight ||
|
|
2806
|
+
left.scrollWidth !== right.scrollWidth ||
|
|
2807
|
+
left.containers.length !== right.containers.length
|
|
2808
|
+
) {
|
|
2809
|
+
return false;
|
|
2810
|
+
}
|
|
2811
|
+
return left.containers.every((container, index) => {
|
|
2812
|
+
const other = right.containers[index];
|
|
2813
|
+
return other?.id === container.id && other.scrollTop === container.scrollTop && other.scrollLeft === container.scrollLeft;
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
function buildScrollNoopDiagnostic(before: ScrollPositionSnapshot | undefined, after: ScrollPositionSnapshot | undefined): ScrollNoopDiagnostic | undefined {
|
|
2818
|
+
if (!before || !after || !sameScrollPositionSnapshot(before, after)) return undefined;
|
|
2819
|
+
return {
|
|
2820
|
+
after,
|
|
2821
|
+
before,
|
|
2822
|
+
message: "Scroll reported success, but the viewport and sampled scrollable containers did not change position.",
|
|
2823
|
+
reason: "no-observed-scroll-position-change",
|
|
2824
|
+
recommendations: [
|
|
2825
|
+
"Run snapshot -i or screenshot to confirm what is visible before choosing the next action.",
|
|
2826
|
+
"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.",
|
|
2827
|
+
],
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2831
|
+
function buildScrollNoopNextActions(sessionName: string | undefined): AgentBrowserNextAction[] {
|
|
2832
|
+
const withSession = (args: string[]): string[] => sessionName ? ["--session", sessionName, ...args] : args;
|
|
2833
|
+
return [
|
|
2834
|
+
{
|
|
2835
|
+
id: "inspect-after-noop-scroll",
|
|
2836
|
+
params: { args: withSession(["snapshot", "-i"]) },
|
|
2837
|
+
reason: "Refresh interactive refs and inspect whether the intended target is inside a nested scroll container.",
|
|
2838
|
+
safety: "Do not assume repeated page scrolls will move dashboard panels or nested panes.",
|
|
2839
|
+
tool: "agent_browser",
|
|
2840
|
+
},
|
|
2841
|
+
{
|
|
2842
|
+
id: "verify-noop-scroll-visually",
|
|
2843
|
+
params: { args: withSession(["screenshot"]) },
|
|
2844
|
+
reason: "Capture the current viewport to verify whether the scroll actually changed visible content.",
|
|
2845
|
+
safety: "Use screenshot evidence before concluding a dense dashboard did or did not move.",
|
|
2846
|
+
tool: "agent_browser",
|
|
2847
|
+
},
|
|
2848
|
+
];
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
function formatScrollNoopDiagnosticText(diagnostic: ScrollNoopDiagnostic | undefined): string | undefined {
|
|
2852
|
+
if (!diagnostic) return undefined;
|
|
2853
|
+
return [
|
|
2854
|
+
"Scroll diagnostic: no observed scroll movement.",
|
|
2855
|
+
`Reason: ${diagnostic.message}`,
|
|
2856
|
+
`Sampled scrollable containers: ${diagnostic.after.containers.length}/${diagnostic.after.containerCount}.`,
|
|
2857
|
+
...diagnostic.recommendations.map((recommendation) => `- ${recommendation}`),
|
|
2858
|
+
].join("\n");
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2618
2861
|
function mergeNavigationSummaryIntoData(data: unknown, navigationSummary: NavigationSummary): unknown {
|
|
2619
2862
|
if (isRecord(data)) {
|
|
2620
2863
|
return { ...data, navigationSummary };
|
|
@@ -2622,12 +2865,187 @@ function mergeNavigationSummaryIntoData(data: unknown, navigationSummary: Naviga
|
|
|
2622
2865
|
return { navigationSummary, result: data };
|
|
2623
2866
|
}
|
|
2624
2867
|
|
|
2868
|
+
const COMBOBOX_FOCUS_EVAL = `(() => {
|
|
2869
|
+
const isVisible = (element) => {
|
|
2870
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
2871
|
+
const style = window.getComputedStyle(element);
|
|
2872
|
+
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return false;
|
|
2873
|
+
return element.getClientRects().length > 0;
|
|
2874
|
+
};
|
|
2875
|
+
const active = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
2876
|
+
const role = active?.getAttribute("role") || undefined;
|
|
2877
|
+
const hasPopup = active?.getAttribute("aria-haspopup") || undefined;
|
|
2878
|
+
const expanded = active?.getAttribute("aria-expanded") || undefined;
|
|
2879
|
+
const tagName = active?.tagName.toLowerCase();
|
|
2880
|
+
const name = (active?.getAttribute("aria-label") || active?.getAttribute("placeholder") || active?.getAttribute("title") || active?.textContent || "").trim().slice(0, 80) || undefined;
|
|
2881
|
+
const visibleListboxCount = Array.from(document.querySelectorAll('[role="listbox"], [role="menu"]')).filter(isVisible).length;
|
|
2882
|
+
const visibleOptionCount = Array.from(document.querySelectorAll('[role="option"], option, [role="menuitem"]')).filter(isVisible).length;
|
|
2883
|
+
const comboboxLike = role === "combobox" || hasPopup === "listbox" || hasPopup === "menu" || tagName === "select" || active?.getAttribute("aria-autocomplete") !== null;
|
|
2884
|
+
return { activeElement: active ? { expanded, hasPopup, name, role, tagName } : undefined, comboboxLike, visibleListboxCount, visibleOptionCount };
|
|
2885
|
+
})()`;
|
|
2886
|
+
|
|
2887
|
+
function extractComboboxFocusDiagnostic(data: unknown): ComboboxFocusDiagnostic | undefined {
|
|
2888
|
+
const result = isRecord(data) && isRecord(data.result) ? data.result : data;
|
|
2889
|
+
if (!isRecord(result) || result.comboboxLike !== true || !isRecord(result.activeElement)) return undefined;
|
|
2890
|
+
const visibleListboxCount = typeof result.visibleListboxCount === "number" ? result.visibleListboxCount : 0;
|
|
2891
|
+
const visibleOptionCount = typeof result.visibleOptionCount === "number" ? result.visibleOptionCount : 0;
|
|
2892
|
+
const expanded = typeof result.activeElement.expanded === "string" ? result.activeElement.expanded : undefined;
|
|
2893
|
+
if ((expanded !== "false" && expanded !== "true") || visibleListboxCount > 0 || visibleOptionCount > 0) return undefined;
|
|
2894
|
+
return {
|
|
2895
|
+
activeElement: {
|
|
2896
|
+
expanded,
|
|
2897
|
+
hasPopup: typeof result.activeElement.hasPopup === "string" ? result.activeElement.hasPopup : undefined,
|
|
2898
|
+
name: typeof result.activeElement.name === "string" ? redactSensitiveText(result.activeElement.name) : undefined,
|
|
2899
|
+
role: typeof result.activeElement.role === "string" ? result.activeElement.role : undefined,
|
|
2900
|
+
tagName: typeof result.activeElement.tagName === "string" ? result.activeElement.tagName : undefined,
|
|
2901
|
+
},
|
|
2902
|
+
message: "A combobox-like control is focused, but no listbox or option elements are visibly open.",
|
|
2903
|
+
reason: "focused-combobox-without-visible-options",
|
|
2904
|
+
recommendations: [
|
|
2905
|
+
"Run snapshot -i to inspect whether options appeared under a different role or portal.",
|
|
2906
|
+
"Try ArrowDown or Enter to open the option list before selecting, or use select/visible option refs when available.",
|
|
2907
|
+
],
|
|
2908
|
+
visibleListboxCount,
|
|
2909
|
+
visibleOptionCount,
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
function isComboboxFocusDiagnosticCommand(command: string | undefined, commandTokens: string[]): boolean {
|
|
2914
|
+
const explicitlyTargetsCombobox = commandTokens.some((token) => /^(?:combobox|listbox)$/i.test(token));
|
|
2915
|
+
if (!explicitlyTargetsCombobox) return false;
|
|
2916
|
+
if (command === "click" || command === "fill") return true;
|
|
2917
|
+
return command === "find" && commandTokens.some((token) => ["click", "fill", "select"].includes(token));
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
function getCompiledSemanticActionRoleValue(compiled: CompiledAgentBrowserSemanticAction): string | undefined {
|
|
2921
|
+
if (compiled.locator !== "role") return undefined;
|
|
2922
|
+
const findIndex = compiled.args.indexOf("find");
|
|
2923
|
+
if (findIndex < 0 || compiled.args[findIndex + 1] !== "role") return undefined;
|
|
2924
|
+
return compiled.args[findIndex + 2];
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
function isComboboxFocusDiagnosticSemanticAction(compiled: CompiledAgentBrowserSemanticAction | undefined): boolean {
|
|
2928
|
+
if (!compiled || !["click", "fill", "select"].includes(compiled.action)) return false;
|
|
2929
|
+
return /^(?:combobox|listbox)$/i.test(getCompiledSemanticActionRoleValue(compiled) ?? "");
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
async function collectComboboxFocusDiagnostic(options: {
|
|
2933
|
+
command?: string;
|
|
2934
|
+
commandTokens: string[];
|
|
2935
|
+
cwd: string;
|
|
2936
|
+
semanticAction?: CompiledAgentBrowserSemanticAction;
|
|
2937
|
+
sessionName?: string;
|
|
2938
|
+
signal?: AbortSignal;
|
|
2939
|
+
}): Promise<ComboboxFocusDiagnostic | undefined> {
|
|
2940
|
+
if (!isComboboxFocusDiagnosticCommand(options.command, options.commandTokens) && !isComboboxFocusDiagnosticSemanticAction(options.semanticAction)) return undefined;
|
|
2941
|
+
return extractComboboxFocusDiagnostic(await runSessionCommandData({
|
|
2942
|
+
args: ["eval", "--stdin"],
|
|
2943
|
+
cwd: options.cwd,
|
|
2944
|
+
sessionName: options.sessionName,
|
|
2945
|
+
signal: options.signal,
|
|
2946
|
+
stdin: COMBOBOX_FOCUS_EVAL,
|
|
2947
|
+
}));
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
function buildComboboxFocusNextActions(sessionName: string | undefined): AgentBrowserNextAction[] {
|
|
2951
|
+
const withSession = (args: string[]): string[] => sessionName ? ["--session", sessionName, ...args] : args;
|
|
2952
|
+
return [
|
|
2953
|
+
{
|
|
2954
|
+
id: "inspect-focused-combobox",
|
|
2955
|
+
params: { args: withSession(["snapshot", "-i"]) },
|
|
2956
|
+
reason: "Inspect the focused combobox and any portal/listbox refs before choosing an option.",
|
|
2957
|
+
safety: "Prefer visible option refs or select when a native/selectable option list is exposed.",
|
|
2958
|
+
tool: "agent_browser",
|
|
2959
|
+
},
|
|
2960
|
+
{
|
|
2961
|
+
id: "try-open-combobox-with-arrow",
|
|
2962
|
+
params: { args: withSession(["press", "ArrowDown"]) },
|
|
2963
|
+
reason: "Many searchable comboboxes open their option list with ArrowDown after focus.",
|
|
2964
|
+
safety: "Use only when the focused combobox is still the intended control, then re-snapshot before selecting.",
|
|
2965
|
+
tool: "agent_browser",
|
|
2966
|
+
},
|
|
2967
|
+
{
|
|
2968
|
+
id: "try-open-combobox-with-enter",
|
|
2969
|
+
params: { args: withSession(["press", "Enter"]) },
|
|
2970
|
+
reason: "Some comboboxes open or confirm their option list with Enter after focus.",
|
|
2971
|
+
safety: "Enter may select a highlighted/default option; prefer ArrowDown first unless Enter is the app's expected opener.",
|
|
2972
|
+
tool: "agent_browser",
|
|
2973
|
+
},
|
|
2974
|
+
];
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
function formatComboboxFocusDiagnosticText(diagnostic: ComboboxFocusDiagnostic | undefined): string | undefined {
|
|
2978
|
+
if (!diagnostic) return undefined;
|
|
2979
|
+
const label = diagnostic.activeElement.name ? ` (${diagnostic.activeElement.name})` : "";
|
|
2980
|
+
return [
|
|
2981
|
+
`Combobox diagnostic: focused combobox did not expose visible options${label}.`,
|
|
2982
|
+
`Reason: ${diagnostic.message}`,
|
|
2983
|
+
...diagnostic.recommendations.map((recommendation) => `- ${recommendation}`),
|
|
2984
|
+
].join("\n");
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
function getRecordStartLikeCommand(command: string | undefined, commandTokens: string[]): RecordingDependencyWarning["command"] | undefined {
|
|
2988
|
+
if (command !== "record") return undefined;
|
|
2989
|
+
const subcommand = commandTokens[1]?.toLowerCase();
|
|
2990
|
+
if (subcommand === "start") return "record start";
|
|
2991
|
+
if (subcommand === "restart") return "record restart";
|
|
2992
|
+
return undefined;
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
async function executableExistsOnPath(command: string): Promise<boolean> {
|
|
2996
|
+
const pathValue = process.env.PATH ?? "";
|
|
2997
|
+
const extensions = process.platform === "win32"
|
|
2998
|
+
? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean)
|
|
2999
|
+
: [""];
|
|
3000
|
+
for (const directory of pathValue.split(delimiter).filter(Boolean)) {
|
|
3001
|
+
for (const extension of extensions) {
|
|
3002
|
+
try {
|
|
3003
|
+
const candidate = join(directory, `${command}${extension}`);
|
|
3004
|
+
await access(candidate, fsConstants.X_OK);
|
|
3005
|
+
if ((await stat(candidate)).isFile()) return true;
|
|
3006
|
+
} catch {
|
|
3007
|
+
// Try the next candidate.
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
return false;
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
async function collectRecordingDependencyWarning(options: {
|
|
3015
|
+
command: string | undefined;
|
|
3016
|
+
commandTokens: string[];
|
|
3017
|
+
succeeded: boolean;
|
|
3018
|
+
}): Promise<RecordingDependencyWarning | undefined> {
|
|
3019
|
+
if (!options.succeeded) return undefined;
|
|
3020
|
+
const recordCommand = getRecordStartLikeCommand(options.command, options.commandTokens);
|
|
3021
|
+
if (!recordCommand) return undefined;
|
|
3022
|
+
if (await executableExistsOnPath("ffmpeg")) return undefined;
|
|
3023
|
+
return {
|
|
3024
|
+
command: recordCommand,
|
|
3025
|
+
dependency: "ffmpeg",
|
|
3026
|
+
message: `${recordCommand} can begin recording, but record stop needs ffmpeg on PATH to encode the WebM output.`,
|
|
3027
|
+
reason: "ffmpeg-missing-for-recording",
|
|
3028
|
+
recommendations: [
|
|
3029
|
+
"Install ffmpeg before relying on this recording workflow; on macOS with Homebrew, brew install ffmpeg or brew install ffmpeg-full.",
|
|
3030
|
+
"If ffmpeg was just installed, restart pi or ensure the PATH visible to pi includes the ffmpeg binary before running record stop.",
|
|
3031
|
+
],
|
|
3032
|
+
};
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
function formatRecordingDependencyWarningText(warning: RecordingDependencyWarning | undefined): string | undefined {
|
|
3036
|
+
if (!warning) return undefined;
|
|
3037
|
+
return [
|
|
3038
|
+
"Recording dependency warning: ffmpeg not found on PATH.",
|
|
3039
|
+
`Reason: ${warning.message}`,
|
|
3040
|
+
...warning.recommendations.map((recommendation) => `- ${recommendation}`),
|
|
3041
|
+
].join("\n");
|
|
3042
|
+
}
|
|
3043
|
+
|
|
2625
3044
|
function getSnapshotRefRecord(data: unknown): Record<string, unknown> | undefined {
|
|
2626
3045
|
return isRecord(data) && isRecord(data.refs) ? data.refs : undefined;
|
|
2627
3046
|
}
|
|
2628
3047
|
|
|
2629
3048
|
const OVERLAY_CLOSE_NAME_PATTERN = /(?:\b(?:close|dismiss|no thanks|not now|maybe later|hide|skip|continue without|x)\b|^\s*×\s*$)/i;
|
|
2630
|
-
const OVERLAY_CONTEXT_NAME_PATTERN = /\b(?:banner|modal|dialog|popup|pop-up|overlay|donat(?:e|ion)|subscribe|sign in|login|cookie|privacy|consent)\b/i;
|
|
2631
3049
|
const OVERLAY_CONTEXT_ROLES = new Set(["alertdialog", "dialog"]);
|
|
2632
3050
|
const OVERLAY_ACTION_ROLES = new Set(["button", "link", "menuitem"]);
|
|
2633
3051
|
const OVERLAY_BLOCKER_CANDIDATE_LIMIT = 3;
|
|
@@ -2638,8 +3056,7 @@ function getOverlayBlockerCandidates(snapshotData: unknown): OverlayBlockerCandi
|
|
|
2638
3056
|
const hasOverlayContext = Object.values(refs).some((entry) => {
|
|
2639
3057
|
if (!isRecord(entry)) return false;
|
|
2640
3058
|
const role = typeof entry.role === "string" ? entry.role : "";
|
|
2641
|
-
|
|
2642
|
-
return OVERLAY_CONTEXT_ROLES.has(role.toLowerCase()) || OVERLAY_CONTEXT_NAME_PATTERN.test(name);
|
|
3059
|
+
return OVERLAY_CONTEXT_ROLES.has(role.toLowerCase());
|
|
2643
3060
|
});
|
|
2644
3061
|
if (!hasOverlayContext) return [];
|
|
2645
3062
|
const candidates: OverlayBlockerCandidate[] = [];
|
|
@@ -2810,16 +3227,27 @@ function formatEvalStdinHintText(hint: EvalStdinHint | undefined): string | unde
|
|
|
2810
3227
|
return hint ? `Eval stdin hint: ${hint.reason} ${hint.suggestion}` : undefined;
|
|
2811
3228
|
}
|
|
2812
3229
|
|
|
2813
|
-
function getArtifactCleanupGuidance(options: { command?: string; manifest?: SessionArtifactManifest; succeeded: boolean }): ArtifactCleanupGuidance | undefined {
|
|
3230
|
+
async function getArtifactCleanupGuidance(options: { command?: string; cwd: string; manifest?: SessionArtifactManifest; succeeded: boolean }): Promise<ArtifactCleanupGuidance | undefined> {
|
|
2814
3231
|
if (!options.succeeded || options.command !== "close" || !options.manifest || options.manifest.entries.length === 0) return undefined;
|
|
2815
|
-
const
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
.
|
|
3232
|
+
const explicitEntries = options.manifest.entries.filter((entry) => entry.storageScope === "explicit-path");
|
|
3233
|
+
const explicitArtifactPaths: string[] = [];
|
|
3234
|
+
const seenPaths = new Set<string>();
|
|
3235
|
+
for (const entry of explicitEntries) {
|
|
3236
|
+
if (explicitArtifactPaths.length >= 10) break;
|
|
3237
|
+
const displayPath = entry.path;
|
|
3238
|
+
if (seenPaths.has(displayPath)) continue;
|
|
3239
|
+
const absolutePath = entry.absolutePath ?? (isAbsolute(entry.path) ? entry.path : resolve(options.cwd, entry.path));
|
|
3240
|
+
try {
|
|
3241
|
+
await stat(absolutePath);
|
|
3242
|
+
} catch {
|
|
3243
|
+
continue;
|
|
3244
|
+
}
|
|
3245
|
+
seenPaths.add(displayPath);
|
|
3246
|
+
explicitArtifactPaths.push(displayPath);
|
|
3247
|
+
}
|
|
2820
3248
|
return {
|
|
2821
3249
|
explicitArtifactPaths,
|
|
2822
|
-
note: "Closing the browser session does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; clean
|
|
3250
|
+
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.",
|
|
2823
3251
|
owner: "host-file-tools",
|
|
2824
3252
|
summary: formatSessionArtifactRetentionSummary(options.manifest),
|
|
2825
3253
|
};
|
|
@@ -3283,10 +3711,19 @@ async function closeManagedSession(options: { cwd: string; sessionName: string;
|
|
|
3283
3711
|
}
|
|
3284
3712
|
}
|
|
3285
3713
|
|
|
3714
|
+
function getInstalledDocsPaths(): { readmePath: string; commandReferencePath: string; toolContractPath: string } {
|
|
3715
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
3716
|
+
return {
|
|
3717
|
+
readmePath: join(packageRoot, "README.md"),
|
|
3718
|
+
commandReferencePath: join(packageRoot, "docs", "COMMAND_REFERENCE.md"),
|
|
3719
|
+
toolContractPath: join(packageRoot, "docs", "TOOL_CONTRACT.md"),
|
|
3720
|
+
};
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3286
3723
|
export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
3287
3724
|
const ephemeralSessionSeed = createEphemeralSessionSeed();
|
|
3288
3725
|
const hasBraveApiKey = hasUsableBraveApiKey();
|
|
3289
|
-
const toolPromptGuidelines = buildToolPromptGuidelines({ includeBraveSearch: hasBraveApiKey });
|
|
3726
|
+
const toolPromptGuidelines = buildToolPromptGuidelines({ includeBraveSearch: hasBraveApiKey, docs: getInstalledDocsPaths() });
|
|
3290
3727
|
const implicitSessionIdleTimeoutMs = String(getImplicitSessionIdleTimeoutMs());
|
|
3291
3728
|
const implicitSessionCloseTimeoutMs = getImplicitSessionCloseTimeoutMs();
|
|
3292
3729
|
let managedSessionActive = false;
|
|
@@ -3457,12 +3894,29 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3457
3894
|
const runTool = async (): Promise<AgentBrowserToolResult> => {
|
|
3458
3895
|
const sessionMode = params.sessionMode ?? DEFAULT_SESSION_MODE;
|
|
3459
3896
|
const freshSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal + 1);
|
|
3460
|
-
|
|
3897
|
+
let executionPlan = buildExecutionPlan(preparedArgs.args, {
|
|
3461
3898
|
freshSessionName,
|
|
3462
3899
|
managedSessionActive,
|
|
3463
3900
|
managedSessionName,
|
|
3464
3901
|
sessionMode,
|
|
3465
3902
|
});
|
|
3903
|
+
let semanticActionVisibleRefResolution: SemanticActionVisibleRefResolution | undefined;
|
|
3904
|
+
if (!executionPlan.validationError && executionPlan.managedSessionName !== freshSessionName) {
|
|
3905
|
+
semanticActionVisibleRefResolution = await resolveSemanticActionVisibleRefArgs({
|
|
3906
|
+
compiled: compiledSemanticAction,
|
|
3907
|
+
cwd: ctx.cwd,
|
|
3908
|
+
sessionName: executionPlan.sessionName,
|
|
3909
|
+
signal,
|
|
3910
|
+
});
|
|
3911
|
+
if (semanticActionVisibleRefResolution) {
|
|
3912
|
+
executionPlan = buildExecutionPlan(semanticActionVisibleRefResolution.args, {
|
|
3913
|
+
freshSessionName,
|
|
3914
|
+
managedSessionActive,
|
|
3915
|
+
managedSessionName,
|
|
3916
|
+
sessionMode,
|
|
3917
|
+
});
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3466
3920
|
const redactedEffectiveArgs = redactInvocationArgs(executionPlan.effectiveArgs);
|
|
3467
3921
|
const redactedRecoveryHint = redactRecoveryHint(executionPlan.recoveryHint);
|
|
3468
3922
|
const compatibilityWorkaround: CompatibilityWorkaround | undefined = executionPlan.compatibilityWorkaround;
|
|
@@ -3490,7 +3944,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3490
3944
|
};
|
|
3491
3945
|
}
|
|
3492
3946
|
|
|
3493
|
-
const commandTokens = extractCommandTokens(preparedArgs.args);
|
|
3947
|
+
const commandTokens = semanticActionVisibleRefResolution ? extractCommandTokens(semanticActionVisibleRefResolution.args) : extractCommandTokens(preparedArgs.args);
|
|
3494
3948
|
const exactSensitiveValues = getExactSensitiveStdinValues({
|
|
3495
3949
|
command: executionPlan.commandInfo.command,
|
|
3496
3950
|
commandTokens,
|
|
@@ -3560,10 +4014,13 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3560
4014
|
const priorSessionTabTargetState = executionPlan.sessionName ? sessionTabTargets.get(executionPlan.sessionName) : undefined;
|
|
3561
4015
|
const priorSessionTabTarget = priorSessionTabTargetState?.target;
|
|
3562
4016
|
const priorRefSnapshotState = executionPlan.sessionName ? sessionRefSnapshots.get(executionPlan.sessionName) : undefined;
|
|
4017
|
+
const resolvedSemanticActionRefSnapshot = semanticActionVisibleRefResolution?.snapshot
|
|
4018
|
+
? { ...semanticActionVisibleRefResolution.snapshot, target: semanticActionVisibleRefResolution.snapshot.target ?? priorSessionTabTarget }
|
|
4019
|
+
: undefined;
|
|
3563
4020
|
const staleRefPreflight = buildStaleRefPreflight({
|
|
3564
4021
|
commandTokens,
|
|
3565
4022
|
currentTarget: priorSessionTabTarget,
|
|
3566
|
-
refSnapshot: priorRefSnapshotState,
|
|
4023
|
+
refSnapshot: resolvedSemanticActionRefSnapshot ?? priorRefSnapshotState,
|
|
3567
4024
|
stdin: toolStdin,
|
|
3568
4025
|
});
|
|
3569
4026
|
if (staleRefPreflight) {
|
|
@@ -3668,6 +4125,14 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3668
4125
|
}
|
|
3669
4126
|
}
|
|
3670
4127
|
const redactedProcessArgs = redactInvocationArgs(processArgs);
|
|
4128
|
+
const shouldProbeScrollNoop = executionPlan.commandInfo.command === "scroll" && executionPlan.startupScopedFlags.length === 0;
|
|
4129
|
+
const scrollPositionBefore = shouldProbeScrollNoop
|
|
4130
|
+
? await collectScrollPositionSnapshot({
|
|
4131
|
+
cwd: ctx.cwd,
|
|
4132
|
+
sessionName: executionPlan.sessionName,
|
|
4133
|
+
signal,
|
|
4134
|
+
})
|
|
4135
|
+
: undefined;
|
|
3671
4136
|
|
|
3672
4137
|
onUpdate?.({
|
|
3673
4138
|
content: [{ type: "text", text: `Running agent-browser ${buildInvocationPreview(redactedProcessArgs)}` }],
|
|
@@ -3921,6 +4386,31 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3921
4386
|
signal,
|
|
3922
4387
|
});
|
|
3923
4388
|
}
|
|
4389
|
+
const comboboxFocusDiagnostic = succeeded
|
|
4390
|
+
? await collectComboboxFocusDiagnostic({
|
|
4391
|
+
command: executionPlan.commandInfo.command,
|
|
4392
|
+
commandTokens,
|
|
4393
|
+
cwd: ctx.cwd,
|
|
4394
|
+
semanticAction: compiledSemanticAction,
|
|
4395
|
+
sessionName: executionPlan.sessionName,
|
|
4396
|
+
signal,
|
|
4397
|
+
})
|
|
4398
|
+
: undefined;
|
|
4399
|
+
const recordingDependencyWarning = await collectRecordingDependencyWarning({
|
|
4400
|
+
command: executionPlan.commandInfo.command,
|
|
4401
|
+
commandTokens,
|
|
4402
|
+
succeeded,
|
|
4403
|
+
});
|
|
4404
|
+
const scrollNoopDiagnostic = succeeded && shouldProbeScrollNoop
|
|
4405
|
+
? buildScrollNoopDiagnostic(
|
|
4406
|
+
scrollPositionBefore,
|
|
4407
|
+
await collectScrollPositionSnapshot({
|
|
4408
|
+
cwd: ctx.cwd,
|
|
4409
|
+
sessionName: executionPlan.sessionName,
|
|
4410
|
+
signal,
|
|
4411
|
+
}),
|
|
4412
|
+
)
|
|
4413
|
+
: undefined;
|
|
3924
4414
|
let currentRefSnapshot: SessionRefSnapshot | undefined;
|
|
3925
4415
|
if (executionPlan.sessionName) {
|
|
3926
4416
|
const activeSessionTabTargetState = sessionTabTargets.get(executionPlan.sessionName);
|
|
@@ -3937,7 +4427,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3937
4427
|
? extractRefSnapshotFromData(presentationEnvelope?.data)
|
|
3938
4428
|
: executionPlan.commandInfo.command === "batch"
|
|
3939
4429
|
? extractRefSnapshotFromBatchResults(presentationEnvelope?.data)
|
|
3940
|
-
: overlayBlockerDiagnostic?.snapshot
|
|
4430
|
+
: resolvedSemanticActionRefSnapshot ?? overlayBlockerDiagnostic?.snapshot
|
|
3941
4431
|
: undefined;
|
|
3942
4432
|
if (refSnapshot && shouldApplySessionTabTargetUpdate({ current: sessionRefSnapshots.get(executionPlan.sessionName), updateOrder: tabTargetUpdateOrder })) {
|
|
3943
4433
|
currentRefSnapshot = { ...refSnapshot, target: refSnapshot.target ?? currentSessionTabTarget };
|
|
@@ -4082,8 +4572,9 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4082
4572
|
stdin: toolStdin,
|
|
4083
4573
|
});
|
|
4084
4574
|
const resultArtifactManifest = presentation.artifactManifest ?? artifactManifest;
|
|
4085
|
-
const artifactCleanup = getArtifactCleanupGuidance({
|
|
4575
|
+
const artifactCleanup = await getArtifactCleanupGuidance({
|
|
4086
4576
|
command: executionPlan.commandInfo.command,
|
|
4577
|
+
cwd: ctx.cwd,
|
|
4087
4578
|
manifest: resultArtifactManifest,
|
|
4088
4579
|
succeeded,
|
|
4089
4580
|
});
|
|
@@ -4147,6 +4638,12 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4147
4638
|
if (selectorTextVisibilityDiagnostics.length > 0) {
|
|
4148
4639
|
(nextActions ??= []).push(...buildSelectorTextVisibilityNextActions({ diagnostics: selectorTextVisibilityDiagnostics, sessionName: executionPlan.sessionName }));
|
|
4149
4640
|
}
|
|
4641
|
+
if (scrollNoopDiagnostic) {
|
|
4642
|
+
(nextActions ??= []).push(...buildScrollNoopNextActions(executionPlan.sessionName));
|
|
4643
|
+
}
|
|
4644
|
+
if (comboboxFocusDiagnostic) {
|
|
4645
|
+
(nextActions ??= []).push(...buildComboboxFocusNextActions(executionPlan.sessionName));
|
|
4646
|
+
}
|
|
4150
4647
|
if (categoryDetails.failureCategory === "stale-ref" && redactedCompiledSemanticAction) {
|
|
4151
4648
|
(nextActions ??= []).push({
|
|
4152
4649
|
id: "retry-semantic-action-after-stale-ref",
|
|
@@ -4156,6 +4653,9 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4156
4653
|
tool: "agent_browser" as const,
|
|
4157
4654
|
});
|
|
4158
4655
|
}
|
|
4656
|
+
const pageChangeSummary = (scrollNoopDiagnostic || comboboxFocusDiagnostic) && presentation.pageChangeSummary
|
|
4657
|
+
? { ...presentation.pageChangeSummary, nextActionIds: nextActions?.map((action) => action.id) }
|
|
4658
|
+
: presentation.pageChangeSummary;
|
|
4159
4659
|
const details = {
|
|
4160
4660
|
args: redactedArgs,
|
|
4161
4661
|
compiledJob: redactedCompiledJob,
|
|
@@ -4189,8 +4689,11 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4189
4689
|
imagePath: presentation.imagePath,
|
|
4190
4690
|
imagePaths: presentation.imagePaths,
|
|
4191
4691
|
nextActions,
|
|
4192
|
-
pageChangeSummary
|
|
4692
|
+
pageChangeSummary,
|
|
4193
4693
|
overlayBlockers: overlayBlockerDiagnostic,
|
|
4694
|
+
comboboxFocus: comboboxFocusDiagnostic,
|
|
4695
|
+
recordingDependencyWarning,
|
|
4696
|
+
scrollNoop: scrollNoopDiagnostic,
|
|
4194
4697
|
qaPreset,
|
|
4195
4698
|
selectorTextVisibility: selectorTextVisibilityDiagnostics[0],
|
|
4196
4699
|
selectorTextVisibilityAll: selectorTextVisibilityDiagnostics.length > 1 ? selectorTextVisibilityDiagnostics : undefined,
|
|
@@ -4218,11 +4721,14 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4218
4721
|
const semanticActionCandidateText = nextActions ? formatSemanticActionCandidateText(nextActions) : undefined;
|
|
4219
4722
|
const overlayBlockerText = overlayBlockerDiagnostic ? formatOverlayBlockerText(overlayBlockerDiagnostic) : undefined;
|
|
4220
4723
|
const selectorTextVisibilityText = formatSelectorTextVisibilityText(selectorTextVisibilityDiagnostics);
|
|
4724
|
+
const scrollNoopDiagnosticText = formatScrollNoopDiagnosticText(scrollNoopDiagnostic);
|
|
4725
|
+
const comboboxFocusDiagnosticText = formatComboboxFocusDiagnosticText(comboboxFocusDiagnostic);
|
|
4726
|
+
const recordingDependencyWarningText = formatRecordingDependencyWarningText(recordingDependencyWarning);
|
|
4221
4727
|
const evalStdinHintText = formatEvalStdinHintText(evalStdinHint);
|
|
4222
4728
|
const artifactCleanupText = formatArtifactCleanupGuidanceText(artifactCleanup);
|
|
4223
4729
|
const timeoutPartialProgressText = timeoutPartialProgress ? formatTimeoutPartialProgressText(timeoutPartialProgress) : undefined;
|
|
4224
4730
|
const managedSessionOutcomeText = formatManagedSessionOutcomeText(managedSessionOutcome);
|
|
4225
|
-
const rawAppendedDiagnosticText = [semanticActionCandidateText, overlayBlockerText, selectorTextVisibilityText, evalStdinHintText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText].filter((item): item is string => item !== undefined).join("\n\n");
|
|
4731
|
+
const rawAppendedDiagnosticText = [semanticActionCandidateText, overlayBlockerText, selectorTextVisibilityText, scrollNoopDiagnosticText, comboboxFocusDiagnosticText, recordingDependencyWarningText, evalStdinHintText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText].filter((item): item is string => item !== undefined).join("\n\n");
|
|
4226
4732
|
const appendedDiagnosticText = redactSensitiveText(redactExactSensitiveText(rawAppendedDiagnosticText, exactSensitiveValues));
|
|
4227
4733
|
const shouldAppendDiagnosticText = appendedDiagnosticText.length > 0 && (!userRequestedJson || plainTextInspection);
|
|
4228
4734
|
const content = shouldAppendDiagnosticText && redactedContent[0]?.type === "text"
|