pi-agent-browser-native 0.2.29 → 0.2.31
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 +28 -0
- package/README.md +26 -16
- package/docs/ARCHITECTURE.md +6 -6
- package/docs/COMMAND_REFERENCE.md +25 -12
- package/docs/RELEASE.md +46 -5
- package/docs/REQUIREMENTS.md +4 -3
- package/docs/SUPPORT_MATRIX.md +30 -14
- package/docs/TOOL_CONTRACT.md +46 -33
- package/extensions/agent-browser/index.ts +356 -60
- package/extensions/agent-browser/lib/playbook.ts +19 -18
- package/extensions/agent-browser/lib/results/presentation.ts +154 -2
- package/extensions/agent-browser/lib/results/shared.ts +7 -1
- package/package.json +1 -1
|
@@ -86,7 +86,7 @@ const PACKAGE_NAME = "pi-agent-browser-native";
|
|
|
86
86
|
|
|
87
87
|
const AGENT_BROWSER_SEMANTIC_ACTIONS = ["check", "click", "fill", "select", "uncheck"] as const;
|
|
88
88
|
const AGENT_BROWSER_SEMANTIC_LOCATORS = ["alt", "label", "placeholder", "role", "testid", "text", "title"] as const;
|
|
89
|
-
const AGENT_BROWSER_JOB_STEP_ACTIONS = ["open", "click", "fill", "wait", "assertText", "assertUrl", "waitForDownload", "screenshot"] as const;
|
|
89
|
+
const AGENT_BROWSER_JOB_STEP_ACTIONS = ["open", "click", "fill", "select", "wait", "assertText", "assertUrl", "waitForDownload", "screenshot"] as const;
|
|
90
90
|
const AGENT_BROWSER_QA_LOAD_STATES = ["domcontentloaded", "load", "networkidle"] as const;
|
|
91
91
|
const SOURCE_LOOKUP_WORKSPACE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
92
92
|
const SOURCE_LOOKUP_IGNORED_DIRECTORIES = new Set([".git", "node_modules", "dist", "build", "coverage", ".next", "out", "tmp", "temp"]);
|
|
@@ -102,8 +102,10 @@ type AgentBrowserNetworkSourceLookupStatus = "failed-requests-found" | "no-faile
|
|
|
102
102
|
|
|
103
103
|
interface AgentBrowserSemanticActionInput {
|
|
104
104
|
action: AgentBrowserSemanticActionName;
|
|
105
|
-
locator
|
|
106
|
-
value
|
|
105
|
+
locator?: AgentBrowserSemanticLocator;
|
|
106
|
+
value?: string;
|
|
107
|
+
values?: string[];
|
|
108
|
+
selector?: string;
|
|
107
109
|
text?: string;
|
|
108
110
|
role?: string;
|
|
109
111
|
name?: string;
|
|
@@ -112,7 +114,9 @@ interface AgentBrowserSemanticActionInput {
|
|
|
112
114
|
|
|
113
115
|
interface CompiledAgentBrowserSemanticAction {
|
|
114
116
|
action: AgentBrowserSemanticActionName;
|
|
115
|
-
locator
|
|
117
|
+
locator?: AgentBrowserSemanticLocator;
|
|
118
|
+
selector?: string;
|
|
119
|
+
values?: string[];
|
|
116
120
|
args: string[];
|
|
117
121
|
}
|
|
118
122
|
|
|
@@ -225,6 +229,7 @@ interface CompiledAgentBrowserNetworkSourceLookup {
|
|
|
225
229
|
filter?: string;
|
|
226
230
|
maxWorkspaceFiles: number;
|
|
227
231
|
requestId?: string;
|
|
232
|
+
session?: string;
|
|
228
233
|
url?: string;
|
|
229
234
|
};
|
|
230
235
|
}
|
|
@@ -265,16 +270,18 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
265
270
|
semanticAction: Type.Optional(
|
|
266
271
|
Type.Object({
|
|
267
272
|
action: StringEnum(AGENT_BROWSER_SEMANTIC_ACTIONS, {
|
|
268
|
-
description: "Intent action to compile to an existing agent-browser find command.",
|
|
273
|
+
description: "Intent action to compile to an existing agent-browser find command, or to upstream select when action=select.",
|
|
269
274
|
}),
|
|
270
|
-
locator: StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, {
|
|
271
|
-
description: "Upstream find locator family to use.",
|
|
272
|
-
}),
|
|
273
|
-
value: Type.String({ description: "Locator value
|
|
274
|
-
|
|
275
|
+
locator: Type.Optional(StringEnum(AGENT_BROWSER_SEMANTIC_LOCATORS, {
|
|
276
|
+
description: "Upstream find locator family to use for check/click/fill/uncheck actions.",
|
|
277
|
+
})),
|
|
278
|
+
value: Type.Optional(Type.String({ description: "Locator value for find actions, or a single option value for select actions." })),
|
|
279
|
+
values: Type.Optional(Type.Array(Type.String({ description: "Option value for select actions." }), { description: "One or more option values for select actions.", minItems: 1 })),
|
|
280
|
+
selector: Type.Optional(Type.String({ description: "Selector or @ref for select actions; compiled to select <selector> <value...>." })),
|
|
281
|
+
text: Type.Optional(Type.String({ description: "Text/value argument for fill actions." })),
|
|
275
282
|
role: Type.Optional(Type.String({ description: "Role locator value; when set it must match value for locator=role." })),
|
|
276
283
|
name: Type.Optional(Type.String({ description: "Accessible name filter for locator=role; compiles to --name <name>." })),
|
|
277
|
-
session: Type.Optional(Type.String({ description: "Optional upstream session name; prepends --session <name> before the compiled
|
|
284
|
+
session: Type.Optional(Type.String({ description: "Optional upstream session name; prepends --session <name> before the compiled command." })),
|
|
278
285
|
}),
|
|
279
286
|
),
|
|
280
287
|
qa: Type.Optional(
|
|
@@ -302,6 +309,7 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
302
309
|
Type.Object({
|
|
303
310
|
filter: Type.Optional(Type.String({ description: "Optional upstream network requests filter pattern." })),
|
|
304
311
|
requestId: Type.Optional(Type.String({ description: "Optional network request id to inspect with network request <id>." })),
|
|
312
|
+
session: Type.Optional(Type.String({ description: "Optional upstream session name; prepends --session <name> before the generated batch." })),
|
|
305
313
|
url: Type.Optional(Type.String({ description: "Optional failed request URL or URL fragment to correlate with local source." })),
|
|
306
314
|
maxWorkspaceFiles: Type.Optional(Type.Number({ description: "Maximum local source files to scan for URL literals. Defaults to 2000 and cannot exceed 5000.", minimum: 1, maximum: SOURCE_LOOKUP_MAX_WORKSPACE_FILES })),
|
|
307
315
|
}),
|
|
@@ -314,8 +322,10 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
314
322
|
description: "Constrained one-call job step compiled to existing upstream batch commands.",
|
|
315
323
|
}),
|
|
316
324
|
url: Type.Optional(Type.String({ description: "URL for open steps, or URL pattern for assertUrl steps." })),
|
|
317
|
-
selector: Type.Optional(Type.String({ description: "Selector or @ref for click/fill/
|
|
325
|
+
selector: Type.Optional(Type.String({ description: "Selector or @ref for click/fill/select-like steps." })),
|
|
318
326
|
text: Type.Optional(Type.String({ description: "Text for fill steps or visible text for assertText steps." })),
|
|
327
|
+
value: Type.Optional(Type.String({ description: "Single option value for select steps." })),
|
|
328
|
+
values: Type.Optional(Type.Array(Type.String({ description: "Option value for select steps." }), { description: "One or more option values for select steps.", minItems: 1 })),
|
|
319
329
|
path: Type.Optional(Type.String({ description: "Artifact/download path for waitForDownload or screenshot steps." })),
|
|
320
330
|
milliseconds: Type.Optional(Type.Number({ description: "Milliseconds for wait steps." })),
|
|
321
331
|
}),
|
|
@@ -355,6 +365,24 @@ function getRequiredJobString(step: Record<string, unknown>, field: "path" | "se
|
|
|
355
365
|
return { value };
|
|
356
366
|
}
|
|
357
367
|
|
|
368
|
+
function getSelectValues(input: Record<string, unknown>, context: string): { values?: string[]; error?: string } {
|
|
369
|
+
const rawValue = input.value;
|
|
370
|
+
const rawValues = input.values;
|
|
371
|
+
if (rawValue !== undefined && rawValues !== undefined) {
|
|
372
|
+
return { error: `${context}.value and ${context}.values cannot both be provided for select.` };
|
|
373
|
+
}
|
|
374
|
+
if (rawValues !== undefined) {
|
|
375
|
+
if (!Array.isArray(rawValues) || rawValues.length === 0 || rawValues.some((value) => typeof value !== "string" || value.trim().length === 0)) {
|
|
376
|
+
return { error: `${context}.values must be a non-empty array of non-empty strings for select.` };
|
|
377
|
+
}
|
|
378
|
+
return { values: rawValues };
|
|
379
|
+
}
|
|
380
|
+
if (typeof rawValue === "string" && rawValue.trim().length > 0) {
|
|
381
|
+
return { values: [rawValue] };
|
|
382
|
+
}
|
|
383
|
+
return { error: `${context}.value or ${context}.values is required for select.` };
|
|
384
|
+
}
|
|
385
|
+
|
|
358
386
|
function compileAgentBrowserJob(input: unknown): { compiled?: CompiledAgentBrowserJob; error?: string } {
|
|
359
387
|
if (!isRecord(input)) {
|
|
360
388
|
return { error: "job must be an object." };
|
|
@@ -388,6 +416,12 @@ function compileAgentBrowserJob(input: unknown): { compiled?: CompiledAgentBrows
|
|
|
388
416
|
const text = getRequiredJobString(rawStep, "text", jobAction);
|
|
389
417
|
if (text.error) return { error: `job.steps[${index}]: ${text.error}` };
|
|
390
418
|
args = ["fill", selector.value as string, text.value as string];
|
|
419
|
+
} else if (jobAction === "select") {
|
|
420
|
+
const selector = getRequiredJobString(rawStep, "selector", jobAction);
|
|
421
|
+
if (selector.error) return { error: `job.steps[${index}]: ${selector.error}` };
|
|
422
|
+
const values = getSelectValues(rawStep, `job.steps[${index}]`);
|
|
423
|
+
if (values.error) return { error: values.error };
|
|
424
|
+
args = ["select", selector.value as string, ...(values.values as string[])];
|
|
391
425
|
} else if (jobAction === "wait") {
|
|
392
426
|
const milliseconds = rawStep.milliseconds;
|
|
393
427
|
if (typeof milliseconds !== "number" || !Number.isInteger(milliseconds) || milliseconds <= 0) {
|
|
@@ -781,9 +815,11 @@ function compileAgentBrowserNetworkSourceLookup(input: unknown): { compiled?: Co
|
|
|
781
815
|
if (!isRecord(input)) return { error: "networkSourceLookup must be an object." };
|
|
782
816
|
const filter = input.filter;
|
|
783
817
|
const requestId = input.requestId;
|
|
818
|
+
const session = input.session;
|
|
784
819
|
const url = input.url;
|
|
785
820
|
if (filter !== undefined && (typeof filter !== "string" || filter.trim().length === 0)) return { error: "networkSourceLookup.filter must be a non-empty string when provided." };
|
|
786
821
|
if (requestId !== undefined && (typeof requestId !== "string" || requestId.trim().length === 0)) return { error: "networkSourceLookup.requestId must be a non-empty string when provided." };
|
|
822
|
+
if (session !== undefined && (typeof session !== "string" || session.trim().length === 0)) return { error: "networkSourceLookup.session must be a non-empty string when provided." };
|
|
787
823
|
if (url !== undefined && (typeof url !== "string" || url.trim().length === 0)) return { error: "networkSourceLookup.url must be a non-empty string when provided." };
|
|
788
824
|
if (filter === undefined && requestId === undefined && url === undefined) return { error: "networkSourceLookup requires requestId, filter, or url." };
|
|
789
825
|
const maxWorkspaceFiles = validateLookupMaxWorkspaceFiles(input.maxWorkspaceFiles, "networkSourceLookup.maxWorkspaceFiles");
|
|
@@ -796,7 +832,8 @@ function compileAgentBrowserNetworkSourceLookup(input: unknown): { compiled?: Co
|
|
|
796
832
|
if (effectiveFilter) {
|
|
797
833
|
steps.push({ action: "network", args: ["network", "requests", "--filter", effectiveFilter] });
|
|
798
834
|
}
|
|
799
|
-
|
|
835
|
+
const args = typeof session === "string" ? ["--session", session, "batch"] : ["batch"];
|
|
836
|
+
return { compiled: { args, query: { filter, maxWorkspaceFiles: maxWorkspaceFiles.value as number, requestId, session, url }, stdin: JSON.stringify(steps.map((step) => step.args)), steps } };
|
|
800
837
|
}
|
|
801
838
|
|
|
802
839
|
function getResultPayload(item: Record<string, unknown>): unknown {
|
|
@@ -945,7 +982,7 @@ async function analyzeNetworkSourceLookupResults(data: unknown, compiled: Compil
|
|
|
945
982
|
}
|
|
946
983
|
|
|
947
984
|
function appendSemanticActionTextArg(args: string[], action: string, text: string | undefined): void {
|
|
948
|
-
if (
|
|
985
|
+
if (action === "fill" && text) {
|
|
949
986
|
args.push(text);
|
|
950
987
|
}
|
|
951
988
|
}
|
|
@@ -955,7 +992,7 @@ function getCompiledSemanticActionCommandIndex(compiled: CompiledAgentBrowserSem
|
|
|
955
992
|
}
|
|
956
993
|
|
|
957
994
|
function getCompiledSemanticActionTextArg(compiled: CompiledAgentBrowserSemanticAction): string | undefined {
|
|
958
|
-
if (compiled.action !== "fill"
|
|
995
|
+
if (compiled.action !== "fill") return undefined;
|
|
959
996
|
const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
|
|
960
997
|
if (commandIndex < 0) return undefined;
|
|
961
998
|
const markerIndex = compiled.args.indexOf("--name");
|
|
@@ -967,6 +1004,11 @@ function getCompiledSemanticActionSessionPrefix(compiled: CompiledAgentBrowserSe
|
|
|
967
1004
|
return commandIndex > 0 ? compiled.args.slice(0, commandIndex) : [];
|
|
968
1005
|
}
|
|
969
1006
|
|
|
1007
|
+
function isCompiledSemanticActionFindCommand(compiled: CompiledAgentBrowserSemanticAction | undefined): boolean {
|
|
1008
|
+
if (!compiled) return false;
|
|
1009
|
+
return compiled.args[getCompiledSemanticActionCommandIndex(compiled)] === "find";
|
|
1010
|
+
}
|
|
1011
|
+
|
|
970
1012
|
const SEMANTIC_ACTION_CANDIDATE_ACTION_IDS = new Set([
|
|
971
1013
|
"try-searchbox-name-candidate",
|
|
972
1014
|
"try-textbox-name-candidate",
|
|
@@ -986,7 +1028,7 @@ function formatSemanticActionCandidateText(actions: AgentBrowserNextAction[]): s
|
|
|
986
1028
|
|
|
987
1029
|
function buildSemanticActionCandidateActions(compiled: CompiledAgentBrowserSemanticAction): AgentBrowserNextAction[] {
|
|
988
1030
|
const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
|
|
989
|
-
if (commandIndex < 0) return [];
|
|
1031
|
+
if (commandIndex < 0 || compiled.args[commandIndex] !== "find") return [];
|
|
990
1032
|
const locator = compiled.args[commandIndex + 1];
|
|
991
1033
|
const value = compiled.args[commandIndex + 2];
|
|
992
1034
|
if (!locator || !value) return [];
|
|
@@ -1023,6 +1065,121 @@ function buildSemanticActionCandidateActions(compiled: CompiledAgentBrowserSeman
|
|
|
1023
1065
|
return [];
|
|
1024
1066
|
}
|
|
1025
1067
|
|
|
1068
|
+
function isAgentBrowserSemanticActionName(value: string | undefined): value is AgentBrowserSemanticActionName {
|
|
1069
|
+
return typeof value === "string" && AGENT_BROWSER_SEMANTIC_ACTIONS.includes(value as AgentBrowserSemanticActionName);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function getFindNameFlagValue(args: string[], startIndex: number): string | undefined {
|
|
1073
|
+
const nameFlagIndex = args.indexOf("--name", startIndex);
|
|
1074
|
+
const name = nameFlagIndex >= 0 ? args[nameFlagIndex + 1] : undefined;
|
|
1075
|
+
return name && !name.startsWith("-") ? name : undefined;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function getFindVisibleRefFallbackTarget(args: string[]): VisibleRefFallbackTarget | undefined {
|
|
1079
|
+
const findIndex = args[0] === "--session" ? 2 : 0;
|
|
1080
|
+
if (args[findIndex] !== "find") return undefined;
|
|
1081
|
+
const locator = args[findIndex + 1];
|
|
1082
|
+
const value = args[findIndex + 2];
|
|
1083
|
+
const action = args[findIndex + 3];
|
|
1084
|
+
if (!locator || !value || !isAgentBrowserSemanticActionName(action) || action === "select") return undefined;
|
|
1085
|
+
const text = action === "fill" ? args[findIndex + 4] : undefined;
|
|
1086
|
+
if (action === "fill" && (!text || text.startsWith("-"))) return undefined;
|
|
1087
|
+
if (locator === "role") {
|
|
1088
|
+
const targetName = getFindNameFlagValue(args, findIndex + 4);
|
|
1089
|
+
return targetName ? { action, roles: [value], targetName, text } : undefined;
|
|
1090
|
+
}
|
|
1091
|
+
if (locator === "text" && action === "click") {
|
|
1092
|
+
return { action, roles: ["button", "link"], targetName: value };
|
|
1093
|
+
}
|
|
1094
|
+
if (locator === "label" && action === "fill") {
|
|
1095
|
+
return { action, roles: ["textbox"], targetName: value, text };
|
|
1096
|
+
}
|
|
1097
|
+
if (locator === "placeholder" && action === "fill") {
|
|
1098
|
+
return { action, roles: ["searchbox", "textbox"], targetName: value, text };
|
|
1099
|
+
}
|
|
1100
|
+
return undefined;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function getVisibleRefFallbackTarget(options: {
|
|
1104
|
+
commandTokens: string[];
|
|
1105
|
+
compiledSemanticAction?: CompiledAgentBrowserSemanticAction;
|
|
1106
|
+
}): VisibleRefFallbackTarget | undefined {
|
|
1107
|
+
return getFindVisibleRefFallbackTarget(options.commandTokens) ?? (options.compiledSemanticAction ? getFindVisibleRefFallbackTarget(options.compiledSemanticAction.args) : undefined);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const VISIBLE_REF_FALLBACK_CANDIDATE_LIMIT = 3;
|
|
1111
|
+
|
|
1112
|
+
function getVisibleRefFallbackCandidates(target: VisibleRefFallbackTarget, snapshotData: unknown): VisibleRefFallbackCandidate[] {
|
|
1113
|
+
const refs = getSnapshotRefRecord(snapshotData);
|
|
1114
|
+
if (!refs) return [];
|
|
1115
|
+
const roleOrder = target.roles.map((role) => role.toLowerCase());
|
|
1116
|
+
const targetName = normalizeSemanticActionAccessibleName(target.targetName);
|
|
1117
|
+
const candidates = Object.entries(refs).flatMap(([ref, entry]): VisibleRefFallbackCandidate[] => {
|
|
1118
|
+
if (!/^e\d+$/.test(ref) || !isRecord(entry)) return [];
|
|
1119
|
+
const role = typeof entry.role === "string" ? entry.role : undefined;
|
|
1120
|
+
const name = typeof entry.name === "string" ? entry.name : undefined;
|
|
1121
|
+
if (!role || !name || !roleOrder.includes(role.toLowerCase()) || normalizeSemanticActionAccessibleName(name) !== targetName) return [];
|
|
1122
|
+
const args = [target.action, `@${ref}`];
|
|
1123
|
+
appendSemanticActionTextArg(args, target.action, target.text);
|
|
1124
|
+
return [{
|
|
1125
|
+
action: target.action,
|
|
1126
|
+
args,
|
|
1127
|
+
name,
|
|
1128
|
+
reason: `Current snapshot shows ${role} ${JSON.stringify(name)} at @${ref}, matching the failed ${target.action} locator exactly.`,
|
|
1129
|
+
ref: `@${ref}`,
|
|
1130
|
+
role,
|
|
1131
|
+
}];
|
|
1132
|
+
});
|
|
1133
|
+
candidates.sort((left, right) => roleOrder.indexOf(left.role.toLowerCase()) - roleOrder.indexOf(right.role.toLowerCase()) || compareRefIds(left.ref.slice(1), right.ref.slice(1)));
|
|
1134
|
+
return candidates.slice(0, VISIBLE_REF_FALLBACK_CANDIDATE_LIMIT);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
async function collectVisibleRefFallbackDiagnostic(options: {
|
|
1138
|
+
commandTokens: string[];
|
|
1139
|
+
compiledSemanticAction?: CompiledAgentBrowserSemanticAction;
|
|
1140
|
+
cwd: string;
|
|
1141
|
+
sessionName?: string;
|
|
1142
|
+
signal?: AbortSignal;
|
|
1143
|
+
}): Promise<VisibleRefFallbackDiagnostic | undefined> {
|
|
1144
|
+
if (!options.sessionName) return undefined;
|
|
1145
|
+
const target = getVisibleRefFallbackTarget({ commandTokens: options.commandTokens, compiledSemanticAction: options.compiledSemanticAction });
|
|
1146
|
+
if (!target) return undefined;
|
|
1147
|
+
const snapshotData = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
1148
|
+
const snapshot = extractRefSnapshotFromData(snapshotData);
|
|
1149
|
+
if (!snapshot) return undefined;
|
|
1150
|
+
const candidates = getVisibleRefFallbackCandidates(target, snapshotData);
|
|
1151
|
+
if (candidates.length === 0) return undefined;
|
|
1152
|
+
return {
|
|
1153
|
+
candidates,
|
|
1154
|
+
snapshot,
|
|
1155
|
+
summary: candidates.length === 1
|
|
1156
|
+
? `Current snapshot has one exact visible ref match for ${target.action} ${JSON.stringify(target.targetName)}.`
|
|
1157
|
+
: `Current snapshot has ${candidates.length} exact visible ref matches for ${target.action} ${JSON.stringify(target.targetName)}; choose only if the intended control is unambiguous.`,
|
|
1158
|
+
target,
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function buildVisibleRefFallbackNextActions(options: { diagnostic: VisibleRefFallbackDiagnostic; sessionName?: string }): AgentBrowserNextAction[] {
|
|
1163
|
+
const ambiguous = options.diagnostic.candidates.length > 1;
|
|
1164
|
+
return options.diagnostic.candidates.map((candidate, index) => ({
|
|
1165
|
+
id: ambiguous ? `try-current-visible-ref-${index + 1}` : "try-current-visible-ref",
|
|
1166
|
+
params: { args: sessionPrefixArgs(options.sessionName, candidate.args) },
|
|
1167
|
+
reason: candidate.reason,
|
|
1168
|
+
safety: ambiguous
|
|
1169
|
+
? "Several current refs share the same exact role/name. Inspect the snapshot and use only the ref that clearly matches the intended target."
|
|
1170
|
+
: "Use only while this current snapshot still represents the page; refresh refs first if the page changed.",
|
|
1171
|
+
tool: "agent_browser" as const,
|
|
1172
|
+
}));
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function formatVisibleRefFallbackText(diagnostic: VisibleRefFallbackDiagnostic | undefined): string | undefined {
|
|
1176
|
+
if (!diagnostic) return undefined;
|
|
1177
|
+
return [
|
|
1178
|
+
"Current snapshot ref fallback:",
|
|
1179
|
+
...diagnostic.candidates.map((candidate) => `- ${candidate.ref}${candidate.role ? ` ${candidate.role}` : ""} ${JSON.stringify(candidate.name)}: ${candidate.reason}`),
|
|
1180
|
+
].join("\n");
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1026
1183
|
function normalizeSemanticActionAccessibleName(name: string): string {
|
|
1027
1184
|
return name.replace(/\s+/g, " ").trim().toLowerCase();
|
|
1028
1185
|
}
|
|
@@ -1085,6 +1242,8 @@ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: Compile
|
|
|
1085
1242
|
const action = input.action;
|
|
1086
1243
|
const locator = input.locator;
|
|
1087
1244
|
const value = input.value;
|
|
1245
|
+
const values = input.values;
|
|
1246
|
+
const selector = input.selector;
|
|
1088
1247
|
const text = input.text;
|
|
1089
1248
|
const role = input.role;
|
|
1090
1249
|
const name = input.name;
|
|
@@ -1092,6 +1251,27 @@ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: Compile
|
|
|
1092
1251
|
if (typeof action !== "string" || !AGENT_BROWSER_SEMANTIC_ACTIONS.includes(action as AgentBrowserSemanticActionName)) {
|
|
1093
1252
|
return { error: `semanticAction.action must be one of: ${AGENT_BROWSER_SEMANTIC_ACTIONS.join(", ")}.` };
|
|
1094
1253
|
}
|
|
1254
|
+
if (session !== undefined && (typeof session !== "string" || session.trim().length === 0)) {
|
|
1255
|
+
return { error: "semanticAction.session must be a non-empty string when provided." };
|
|
1256
|
+
}
|
|
1257
|
+
if (action === "select") {
|
|
1258
|
+
if (locator !== undefined || role !== undefined || name !== undefined) {
|
|
1259
|
+
return { error: "semanticAction.locator, role, and name are not supported for select; use selector plus value or values." };
|
|
1260
|
+
}
|
|
1261
|
+
if (text !== undefined) {
|
|
1262
|
+
return { error: "semanticAction.text is not supported for select; use value or values for option values." };
|
|
1263
|
+
}
|
|
1264
|
+
if (typeof selector !== "string" || selector.trim().length === 0) {
|
|
1265
|
+
return { error: "semanticAction.selector is required for select." };
|
|
1266
|
+
}
|
|
1267
|
+
const selectedValues = getSelectValues(input, "semanticAction");
|
|
1268
|
+
if (selectedValues.error) return { error: selectedValues.error };
|
|
1269
|
+
const args = typeof session === "string" ? ["--session", session, "select", selector, ...(selectedValues.values as string[])] : ["select", selector, ...(selectedValues.values as string[])];
|
|
1270
|
+
return { compiled: { action: "select", selector, values: selectedValues.values, args } };
|
|
1271
|
+
}
|
|
1272
|
+
if (selector !== undefined || values !== undefined) {
|
|
1273
|
+
return { error: "semanticAction.selector and values are only supported for select actions." };
|
|
1274
|
+
}
|
|
1095
1275
|
if (typeof locator !== "string" || !AGENT_BROWSER_SEMANTIC_LOCATORS.includes(locator as AgentBrowserSemanticLocator)) {
|
|
1096
1276
|
return { error: `semanticAction.locator must be one of: ${AGENT_BROWSER_SEMANTIC_LOCATORS.join(", ")}.` };
|
|
1097
1277
|
}
|
|
@@ -1101,11 +1281,11 @@ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: Compile
|
|
|
1101
1281
|
if (text !== undefined && typeof text !== "string") {
|
|
1102
1282
|
return { error: "semanticAction.text must be a string when provided." };
|
|
1103
1283
|
}
|
|
1104
|
-
if (
|
|
1284
|
+
if (action === "fill" && (typeof text !== "string" || text.length === 0)) {
|
|
1105
1285
|
return { error: `semanticAction.text is required for ${action}.` };
|
|
1106
1286
|
}
|
|
1107
|
-
if (action !== "fill" &&
|
|
1108
|
-
return { error:
|
|
1287
|
+
if (action !== "fill" && text !== undefined) {
|
|
1288
|
+
return { error: "semanticAction.text is only supported for fill actions." };
|
|
1109
1289
|
}
|
|
1110
1290
|
if (role !== undefined && (locator !== "role" || role !== value)) {
|
|
1111
1291
|
return { error: "semanticAction.role is only supported for locator=role and must match value." };
|
|
@@ -1113,11 +1293,8 @@ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: Compile
|
|
|
1113
1293
|
if (name !== undefined && (locator !== "role" || typeof name !== "string" || name.length === 0)) {
|
|
1114
1294
|
return { error: "semanticAction.name is only supported as a non-empty string for locator=role." };
|
|
1115
1295
|
}
|
|
1116
|
-
if (session !== undefined && (typeof session !== "string" || session.trim().length === 0)) {
|
|
1117
|
-
return { error: "semanticAction.session must be a non-empty string when provided." };
|
|
1118
|
-
}
|
|
1119
1296
|
const args = typeof session === "string" ? ["--session", session, "find", locator, value, action] : ["find", locator, value, action];
|
|
1120
|
-
if (action === "fill"
|
|
1297
|
+
if (action === "fill") {
|
|
1121
1298
|
args.push(text as string);
|
|
1122
1299
|
}
|
|
1123
1300
|
if (locator === "role" && typeof name === "string") {
|
|
@@ -1498,6 +1675,10 @@ async function isDirectAgentBrowserBashAllowed(cwd: string): Promise<boolean> {
|
|
|
1498
1675
|
}
|
|
1499
1676
|
|
|
1500
1677
|
const NAVIGATION_SUMMARY_COMMANDS = new Set(["back", "click", "dblclick", "forward", "reload"]);
|
|
1678
|
+
const NAVIGATION_SUMMARY_EVAL = `({ title: document.title, url: location.href })`;
|
|
1679
|
+
// These commands can expose URLs for inspected resources (request URLs, cookie/storage scope, or log sources),
|
|
1680
|
+
// but they do not navigate the active tab and must not poison page-scoped ref guards.
|
|
1681
|
+
const READ_ONLY_DIAGNOSTIC_SESSION_TARGET_COMMANDS = new Set(["console", "cookies", "errors", "network", "storage"]);
|
|
1501
1682
|
|
|
1502
1683
|
interface NavigationSummary {
|
|
1503
1684
|
title?: string;
|
|
@@ -1518,6 +1699,34 @@ interface OverlayBlockerDiagnostic {
|
|
|
1518
1699
|
summary: string;
|
|
1519
1700
|
}
|
|
1520
1701
|
|
|
1702
|
+
interface VisibleRefFallbackCandidate {
|
|
1703
|
+
action: AgentBrowserSemanticActionName;
|
|
1704
|
+
args: string[];
|
|
1705
|
+
name: string;
|
|
1706
|
+
reason: string;
|
|
1707
|
+
ref: string;
|
|
1708
|
+
role: string;
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
interface VisibleRefFallbackDiagnostic {
|
|
1712
|
+
candidates: VisibleRefFallbackCandidate[];
|
|
1713
|
+
snapshot: SessionRefSnapshot;
|
|
1714
|
+
summary: string;
|
|
1715
|
+
target: {
|
|
1716
|
+
action: AgentBrowserSemanticActionName;
|
|
1717
|
+
roles: string[];
|
|
1718
|
+
text?: string;
|
|
1719
|
+
targetName: string;
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
interface VisibleRefFallbackTarget {
|
|
1724
|
+
action: AgentBrowserSemanticActionName;
|
|
1725
|
+
roles: string[];
|
|
1726
|
+
text?: string;
|
|
1727
|
+
targetName: string;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1521
1730
|
interface SelectorTextVisibilityDiagnostic {
|
|
1522
1731
|
firstMatchVisible?: boolean;
|
|
1523
1732
|
firstVisibleTextPreview?: string;
|
|
@@ -1985,6 +2194,13 @@ function extractStringResultField(data: unknown, fieldName: "result" | "title" |
|
|
|
1985
2194
|
return text.length > 0 ? text : undefined;
|
|
1986
2195
|
}
|
|
1987
2196
|
|
|
2197
|
+
function extractNavigationSummaryFromData(data: unknown): NavigationSummary | undefined {
|
|
2198
|
+
const result = isRecord(data) && isRecord(data.result) ? data.result : data;
|
|
2199
|
+
const title = extractStringResultField(result, "title");
|
|
2200
|
+
const url = extractStringResultField(result, "url");
|
|
2201
|
+
return title || url ? { title, url } : undefined;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
1988
2204
|
const SESSION_TAB_PINNING_EXCLUDED_COMMANDS = new Set(["close", "goto", "navigate", "open", "session", "tab"]);
|
|
1989
2205
|
const SESSION_TAB_POST_COMMAND_CORRECTION_EXCLUDED_COMMANDS = new Set(["batch", "close", "session", "tab"]);
|
|
1990
2206
|
|
|
@@ -2108,6 +2324,15 @@ function extractSessionTabTargetFromData(data: unknown): SessionTabTarget | unde
|
|
|
2108
2324
|
return undefined;
|
|
2109
2325
|
}
|
|
2110
2326
|
|
|
2327
|
+
function isReadOnlyDiagnosticSessionTargetCommand(command: string | undefined, _subcommand: string | undefined): boolean {
|
|
2328
|
+
return command !== undefined && READ_ONLY_DIAGNOSTIC_SESSION_TARGET_COMMANDS.has(command);
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
function extractSessionTabTargetFromCommandData(commandTokens: string[], data: unknown): SessionTabTarget | undefined {
|
|
2332
|
+
const [command, subcommand] = commandTokens;
|
|
2333
|
+
return isReadOnlyDiagnosticSessionTargetCommand(command, subcommand) ? undefined : extractSessionTabTargetFromData(data);
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2111
2336
|
function extractBatchResultCommand(item: Record<string, unknown>): string[] {
|
|
2112
2337
|
return Array.isArray(item.command) ? item.command.filter((token): token is string => typeof token === "string") : [];
|
|
2113
2338
|
}
|
|
@@ -2139,8 +2364,7 @@ function extractSessionTabTargetFromBatchResults(data: unknown): SessionTabTarge
|
|
|
2139
2364
|
pendingTitle = undefined;
|
|
2140
2365
|
continue;
|
|
2141
2366
|
}
|
|
2142
|
-
|
|
2143
|
-
const resultTarget = extractSessionTabTargetFromData(result);
|
|
2367
|
+
const resultTarget = extractSessionTabTargetFromCommandData([name, subcommand].filter((token): token is string => token !== undefined), result);
|
|
2144
2368
|
if (resultTarget) {
|
|
2145
2369
|
currentTarget = resultTarget;
|
|
2146
2370
|
}
|
|
@@ -2149,6 +2373,40 @@ function extractSessionTabTargetFromBatchResults(data: unknown): SessionTabTarge
|
|
|
2149
2373
|
return currentTarget;
|
|
2150
2374
|
}
|
|
2151
2375
|
|
|
2376
|
+
function batchContainsOnlyReadOnlyDiagnosticTargets(data: unknown): boolean {
|
|
2377
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
2378
|
+
return false;
|
|
2379
|
+
}
|
|
2380
|
+
return data.every((item) => {
|
|
2381
|
+
if (!isRecord(item)) return false;
|
|
2382
|
+
const [command, subcommand] = extractBatchResultCommand(item);
|
|
2383
|
+
return isReadOnlyDiagnosticSessionTargetCommand(command, subcommand);
|
|
2384
|
+
});
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
function getRestoredSessionTabTarget(details: Record<string, unknown>, command: string | undefined, subcommand: string | undefined): SessionTabTarget | undefined {
|
|
2388
|
+
if (isReadOnlyDiagnosticSessionTargetCommand(command, subcommand)) {
|
|
2389
|
+
return undefined;
|
|
2390
|
+
}
|
|
2391
|
+
const storedTarget = isRecord(details.sessionTabTarget)
|
|
2392
|
+
? normalizeSessionTabTarget({
|
|
2393
|
+
title: typeof details.sessionTabTarget.title === "string" ? details.sessionTabTarget.title : undefined,
|
|
2394
|
+
url: typeof details.sessionTabTarget.url === "string" ? details.sessionTabTarget.url : undefined,
|
|
2395
|
+
})
|
|
2396
|
+
: undefined;
|
|
2397
|
+
if (command !== "batch") {
|
|
2398
|
+
return storedTarget;
|
|
2399
|
+
}
|
|
2400
|
+
const batchTarget = extractSessionTabTargetFromBatchResults(details.data);
|
|
2401
|
+
if (batchTarget) {
|
|
2402
|
+
return batchTarget;
|
|
2403
|
+
}
|
|
2404
|
+
if (isRecord(details.compiledNetworkSourceLookup) || batchContainsOnlyReadOnlyDiagnosticTargets(details.data)) {
|
|
2405
|
+
return undefined;
|
|
2406
|
+
}
|
|
2407
|
+
return storedTarget;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2152
2410
|
function restoreSessionTabTargetsFromBranch(branch: unknown[]): Map<string, OrderedSessionTabTarget> {
|
|
2153
2411
|
const restoredTargets = new Map<string, OrderedSessionTabTarget>();
|
|
2154
2412
|
let restoredOrder = 0;
|
|
@@ -2169,17 +2427,13 @@ function restoreSessionTabTargetsFromBranch(branch: unknown[]): Map<string, Orde
|
|
|
2169
2427
|
continue;
|
|
2170
2428
|
}
|
|
2171
2429
|
const command = typeof details.command === "string" ? details.command : undefined;
|
|
2430
|
+
const subcommand = typeof details.subcommand === "string" ? details.subcommand : undefined;
|
|
2172
2431
|
if (command === "close" && message.isError !== true) {
|
|
2173
2432
|
restoredOrder += 1;
|
|
2174
2433
|
restoredTargets.delete(sessionName);
|
|
2175
2434
|
continue;
|
|
2176
2435
|
}
|
|
2177
|
-
const sessionTabTarget =
|
|
2178
|
-
? normalizeSessionTabTarget({
|
|
2179
|
-
title: typeof details.sessionTabTarget.title === "string" ? details.sessionTabTarget.title : undefined,
|
|
2180
|
-
url: typeof details.sessionTabTarget.url === "string" ? details.sessionTabTarget.url : undefined,
|
|
2181
|
-
})
|
|
2182
|
-
: undefined;
|
|
2436
|
+
const sessionTabTarget = getRestoredSessionTabTarget(details, command, subcommand);
|
|
2183
2437
|
if (sessionTabTarget) {
|
|
2184
2438
|
restoredOrder += 1;
|
|
2185
2439
|
restoredTargets.set(sessionName, { order: restoredOrder, target: sessionTabTarget });
|
|
@@ -2334,10 +2588,12 @@ function supportsPinnedStdinCommand(options: { command?: string; commandTokens:
|
|
|
2334
2588
|
function shouldPinSessionTabForCommand(options: {
|
|
2335
2589
|
command?: string;
|
|
2336
2590
|
commandTokens: string[];
|
|
2591
|
+
pinningRequired?: boolean;
|
|
2337
2592
|
sessionName?: string;
|
|
2338
2593
|
stdin?: string;
|
|
2339
2594
|
}): boolean {
|
|
2340
2595
|
return (
|
|
2596
|
+
options.pinningRequired === true &&
|
|
2341
2597
|
options.sessionName !== undefined &&
|
|
2342
2598
|
options.command !== undefined &&
|
|
2343
2599
|
!SESSION_TAB_PINNING_EXCLUDED_COMMANDS.has(options.command) &&
|
|
@@ -2403,7 +2659,6 @@ const REF_INVALIDATING_BATCH_COMMANDS = new Set([
|
|
|
2403
2659
|
"click",
|
|
2404
2660
|
"dblclick",
|
|
2405
2661
|
"drag",
|
|
2406
|
-
"fill",
|
|
2407
2662
|
"forward",
|
|
2408
2663
|
"goto",
|
|
2409
2664
|
"keyboard",
|
|
@@ -2567,7 +2822,7 @@ function buildPinnedBatchPlan(options: {
|
|
|
2567
2822
|
const includeNavigationSummary = options.command !== undefined && NAVIGATION_SUMMARY_COMMANDS.has(options.command);
|
|
2568
2823
|
const tabSelectionStep: BatchCommandStep = ["tab", options.selectedTab];
|
|
2569
2824
|
const commandStep = options.commandTokens as BatchCommandStep;
|
|
2570
|
-
const navigationSummarySteps: BatchCommandStep[] = includeNavigationSummary ? [["
|
|
2825
|
+
const navigationSummarySteps: BatchCommandStep[] = includeNavigationSummary ? [["eval", NAVIGATION_SUMMARY_EVAL]] : [];
|
|
2571
2826
|
return {
|
|
2572
2827
|
includeNavigationSummary,
|
|
2573
2828
|
steps: [tabSelectionStep, commandStep, ...navigationSummarySteps],
|
|
@@ -2575,8 +2830,9 @@ function buildPinnedBatchPlan(options: {
|
|
|
2575
2830
|
};
|
|
2576
2831
|
}
|
|
2577
2832
|
|
|
2578
|
-
function shouldCorrectSessionTabAfterCommand(options: { command?: string; sessionName?: string }): boolean {
|
|
2833
|
+
function shouldCorrectSessionTabAfterCommand(options: { command?: string; pinningRequired?: boolean; sessionName?: string }): boolean {
|
|
2579
2834
|
return (
|
|
2835
|
+
options.pinningRequired === true &&
|
|
2580
2836
|
options.sessionName !== undefined &&
|
|
2581
2837
|
options.command !== undefined &&
|
|
2582
2838
|
!SESSION_TAB_POST_COMMAND_CORRECTION_EXCLUDED_COMMANDS.has(options.command)
|
|
@@ -2599,14 +2855,18 @@ function deriveSessionTabTarget(options: {
|
|
|
2599
2855
|
data: unknown;
|
|
2600
2856
|
navigationSummary?: NavigationSummary;
|
|
2601
2857
|
previousTarget?: SessionTabTarget;
|
|
2858
|
+
subcommand?: string;
|
|
2602
2859
|
}): SessionTabTarget | undefined {
|
|
2603
2860
|
if (options.command === "close") {
|
|
2604
2861
|
return undefined;
|
|
2605
2862
|
}
|
|
2863
|
+
const commandDataTarget = isReadOnlyDiagnosticSessionTargetCommand(options.command, options.subcommand)
|
|
2864
|
+
? undefined
|
|
2865
|
+
: extractSessionTabTargetFromData(options.data);
|
|
2606
2866
|
return (
|
|
2607
2867
|
normalizeSessionTabTarget(options.navigationSummary) ??
|
|
2608
2868
|
extractSessionTabTargetFromBatchResults(options.data) ??
|
|
2609
|
-
|
|
2869
|
+
commandDataTarget ??
|
|
2610
2870
|
options.previousTarget
|
|
2611
2871
|
);
|
|
2612
2872
|
}
|
|
@@ -2655,12 +2915,8 @@ function unwrapPinnedSessionBatchEnvelope(options: {
|
|
|
2655
2915
|
};
|
|
2656
2916
|
}
|
|
2657
2917
|
|
|
2658
|
-
const
|
|
2659
|
-
const
|
|
2660
|
-
const navigationSummary = normalizeSessionTabTarget({
|
|
2661
|
-
title: extractStringResultField(titleStep?.result, "title"),
|
|
2662
|
-
url: extractStringResultField(urlStep?.result, "url"),
|
|
2663
|
-
});
|
|
2918
|
+
const navigationSummaryStep = options.includeNavigationSummary ? steps[2] : undefined;
|
|
2919
|
+
const navigationSummary = normalizeSessionTabTarget(extractNavigationSummaryFromData(navigationSummaryStep?.result));
|
|
2664
2920
|
return {
|
|
2665
2921
|
envelope: {
|
|
2666
2922
|
success: commandStep.success !== false,
|
|
@@ -2711,17 +2967,13 @@ async function collectNavigationSummary(options: {
|
|
|
2711
2967
|
sessionName?: string;
|
|
2712
2968
|
signal?: AbortSignal;
|
|
2713
2969
|
}): Promise<NavigationSummary | undefined> {
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
"url",
|
|
2722
|
-
);
|
|
2723
|
-
if (!title && !url) return undefined;
|
|
2724
|
-
return { title, url };
|
|
2970
|
+
return extractNavigationSummaryFromData(await runSessionCommandData({
|
|
2971
|
+
args: ["eval", "--stdin"],
|
|
2972
|
+
cwd: options.cwd,
|
|
2973
|
+
sessionName: options.sessionName,
|
|
2974
|
+
signal: options.signal,
|
|
2975
|
+
stdin: NAVIGATION_SUMMARY_EVAL,
|
|
2976
|
+
}));
|
|
2725
2977
|
}
|
|
2726
2978
|
|
|
2727
2979
|
function extractScrollPositionSnapshot(data: unknown): ScrollPositionSnapshot | undefined {
|
|
@@ -2914,7 +3166,7 @@ function isComboboxFocusDiagnosticCommand(command: string | undefined, commandTo
|
|
|
2914
3166
|
const explicitlyTargetsCombobox = commandTokens.some((token) => /^(?:combobox|listbox)$/i.test(token));
|
|
2915
3167
|
if (!explicitlyTargetsCombobox) return false;
|
|
2916
3168
|
if (command === "click" || command === "fill") return true;
|
|
2917
|
-
return command === "find" && commandTokens.some((token) => ["click", "fill"
|
|
3169
|
+
return command === "find" && commandTokens.some((token) => ["click", "fill"].includes(token));
|
|
2918
3170
|
}
|
|
2919
3171
|
|
|
2920
3172
|
function getCompiledSemanticActionRoleValue(compiled: CompiledAgentBrowserSemanticAction): string | undefined {
|
|
@@ -2925,7 +3177,7 @@ function getCompiledSemanticActionRoleValue(compiled: CompiledAgentBrowserSemant
|
|
|
2925
3177
|
}
|
|
2926
3178
|
|
|
2927
3179
|
function isComboboxFocusDiagnosticSemanticAction(compiled: CompiledAgentBrowserSemanticAction | undefined): boolean {
|
|
2928
|
-
if (!compiled || !["click", "fill"
|
|
3180
|
+
if (!compiled || !["click", "fill"].includes(compiled.action)) return false;
|
|
2929
3181
|
return /^(?:combobox|listbox)$/i.test(getCompiledSemanticActionRoleValue(compiled) ?? "");
|
|
2930
3182
|
}
|
|
2931
3183
|
|
|
@@ -3209,14 +3461,16 @@ function looksLikeFunctionEvalStdin(stdin: string | undefined): boolean {
|
|
|
3209
3461
|
return /^(?:async\s+)?function\b/.test(trimmed) || /^(?:async\s*)?\([^)]*\)\s*=>/.test(trimmed) || /^(?:async\s+)?[A-Za-z_$][\w$]*\s*=>/.test(trimmed);
|
|
3210
3462
|
}
|
|
3211
3463
|
|
|
3212
|
-
function
|
|
3213
|
-
|
|
3464
|
+
function isPlainEmptyObject(value: unknown): boolean {
|
|
3465
|
+
if (!isRecord(value) || Array.isArray(value)) return false;
|
|
3466
|
+
const prototype = Object.getPrototypeOf(value);
|
|
3467
|
+
return (prototype === Object.prototype || prototype === null) && Object.keys(value).length === 0;
|
|
3214
3468
|
}
|
|
3215
3469
|
|
|
3216
3470
|
function getEvalStdinHint(options: { command?: string; data: unknown; stdin?: string }): EvalStdinHint | undefined {
|
|
3217
3471
|
if (options.command !== "eval" || !looksLikeFunctionEvalStdin(options.stdin) || !isRecord(options.data)) return undefined;
|
|
3218
3472
|
const result = options.data.result;
|
|
3219
|
-
if (!
|
|
3473
|
+
if (!isPlainEmptyObject(result)) return undefined;
|
|
3220
3474
|
return {
|
|
3221
3475
|
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.",
|
|
3222
3476
|
suggestion: "Pass a plain expression such as `({ title: document.title })`, or invoke the function explicitly, for example `(() => ({ title: document.title }))()`.",
|
|
@@ -3733,6 +3987,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3733
3987
|
let freshSessionOrdinal = 0;
|
|
3734
3988
|
let sessionTabTargets = new Map<string, OrderedSessionTabTarget>();
|
|
3735
3989
|
let sessionRefSnapshots = new Map<string, OrderedSessionRefSnapshot>();
|
|
3990
|
+
let sessionTabPinningReasons = new Map<string, "drift" | "restore">();
|
|
3736
3991
|
let sessionTabTargetUpdateOrder = 0;
|
|
3737
3992
|
let traceOwners = new Map<string, TraceOwner>();
|
|
3738
3993
|
let artifactManifest: SessionArtifactManifest | undefined;
|
|
@@ -3747,6 +4002,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3747
4002
|
freshSessionOrdinal = restoredState.freshSessionOrdinal;
|
|
3748
4003
|
sessionTabTargets = restoreSessionTabTargetsFromBranch(ctx.sessionManager.getBranch());
|
|
3749
4004
|
sessionRefSnapshots = restoreSessionRefSnapshotsFromBranch(ctx.sessionManager.getBranch());
|
|
4005
|
+
sessionTabPinningReasons = new Map([...sessionTabTargets.keys()].map((sessionName) => [sessionName, "restore"]));
|
|
3750
4006
|
sessionTabTargetUpdateOrder = Math.max(getLatestSessionTabTargetOrder(sessionTabTargets), getLatestSessionTabTargetOrder(sessionRefSnapshots));
|
|
3751
4007
|
artifactManifest = restoreArtifactManifestFromBranch(ctx.sessionManager.getBranch());
|
|
3752
4008
|
});
|
|
@@ -3765,6 +4021,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3765
4021
|
managedSessionActive = false;
|
|
3766
4022
|
sessionTabTargets = new Map<string, OrderedSessionTabTarget>();
|
|
3767
4023
|
sessionRefSnapshots = new Map<string, OrderedSessionRefSnapshot>();
|
|
4024
|
+
sessionTabPinningReasons = new Map<string, "drift" | "restore">();
|
|
3768
4025
|
sessionTabTargetUpdateOrder = 0;
|
|
3769
4026
|
traceOwners = new Map<string, TraceOwner>();
|
|
3770
4027
|
artifactManifest = undefined;
|
|
@@ -3862,6 +4119,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3862
4119
|
const redactedCompiledNetworkSourceLookup = compiledNetworkSourceLookup && redactedCompiledNetworkSourceLookupSteps
|
|
3863
4120
|
? {
|
|
3864
4121
|
...compiledNetworkSourceLookup,
|
|
4122
|
+
args: redactNetworkSourceLookupArgs(compiledNetworkSourceLookup.args),
|
|
3865
4123
|
query: {
|
|
3866
4124
|
...compiledNetworkSourceLookup.query,
|
|
3867
4125
|
filter: redactNetworkSourceLookupUrl(compiledNetworkSourceLookup.query.filter),
|
|
@@ -4013,6 +4271,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4013
4271
|
|
|
4014
4272
|
const priorSessionTabTargetState = executionPlan.sessionName ? sessionTabTargets.get(executionPlan.sessionName) : undefined;
|
|
4015
4273
|
const priorSessionTabTarget = priorSessionTabTargetState?.target;
|
|
4274
|
+
const sessionTabPinningReason = executionPlan.sessionName ? sessionTabPinningReasons.get(executionPlan.sessionName) : undefined;
|
|
4016
4275
|
const priorRefSnapshotState = executionPlan.sessionName ? sessionRefSnapshots.get(executionPlan.sessionName) : undefined;
|
|
4017
4276
|
const resolvedSemanticActionRefSnapshot = semanticActionVisibleRefResolution?.snapshot
|
|
4018
4277
|
? { ...semanticActionVisibleRefResolution.snapshot, target: semanticActionVisibleRefResolution.snapshot.target ?? priorSessionTabTarget }
|
|
@@ -4051,6 +4310,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4051
4310
|
shouldPinSessionTabForCommand({
|
|
4052
4311
|
command: executionPlan.commandInfo.command,
|
|
4053
4312
|
commandTokens,
|
|
4313
|
+
pinningRequired: sessionTabPinningReason !== undefined,
|
|
4054
4314
|
sessionName: executionPlan.sessionName,
|
|
4055
4315
|
stdin: toolStdin,
|
|
4056
4316
|
})
|
|
@@ -4286,12 +4546,13 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4286
4546
|
const observedSessionTabTarget =
|
|
4287
4547
|
normalizeSessionTabTarget(navigationSummary) ??
|
|
4288
4548
|
extractSessionTabTargetFromBatchResults(presentationEnvelope?.data) ??
|
|
4289
|
-
|
|
4549
|
+
extractSessionTabTargetFromCommandData(commandTokens, presentationEnvelope?.data);
|
|
4290
4550
|
let currentSessionTabTarget = deriveSessionTabTarget({
|
|
4291
4551
|
command: executionPlan.commandInfo.command,
|
|
4292
4552
|
data: presentationEnvelope?.data,
|
|
4293
4553
|
navigationSummary,
|
|
4294
4554
|
previousTarget: priorSessionTabTarget,
|
|
4555
|
+
subcommand: executionPlan.commandInfo.subcommand,
|
|
4295
4556
|
});
|
|
4296
4557
|
let aboutBlankSessionMismatch: AboutBlankSessionMismatch | undefined;
|
|
4297
4558
|
const shouldTreatAboutBlankAsMismatch =
|
|
@@ -4336,6 +4597,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4336
4597
|
observedSessionTabTarget &&
|
|
4337
4598
|
shouldCorrectSessionTabAfterCommand({
|
|
4338
4599
|
command: executionPlan.commandInfo.command,
|
|
4600
|
+
pinningRequired: sessionTabPinningReason !== undefined,
|
|
4339
4601
|
sessionName: executionPlan.sessionName,
|
|
4340
4602
|
})
|
|
4341
4603
|
) {
|
|
@@ -4418,9 +4680,14 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4418
4680
|
if (executionPlan.commandInfo.command === "close" && succeeded) {
|
|
4419
4681
|
sessionTabTargets.delete(executionPlan.sessionName);
|
|
4420
4682
|
sessionRefSnapshots.delete(executionPlan.sessionName);
|
|
4683
|
+
sessionTabPinningReasons.delete(executionPlan.sessionName);
|
|
4421
4684
|
} else if (currentSessionTabTarget) {
|
|
4422
4685
|
sessionTabTargets.set(executionPlan.sessionName, { order: tabTargetUpdateOrder, target: currentSessionTabTarget });
|
|
4423
4686
|
}
|
|
4687
|
+
} else if (succeeded && currentSessionTabTarget) {
|
|
4688
|
+
// A stale overlapping command may have moved browser focus even though its older target
|
|
4689
|
+
// must not replace the newer logical target. Require tab pinning on the next call.
|
|
4690
|
+
sessionTabPinningReasons.set(executionPlan.sessionName, "drift");
|
|
4424
4691
|
}
|
|
4425
4692
|
const refSnapshot = succeeded
|
|
4426
4693
|
? executionPlan.commandInfo.command === "snapshot"
|
|
@@ -4464,9 +4731,18 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4464
4731
|
if (executionPlan.managedSessionName && succeeded) {
|
|
4465
4732
|
managedSessionCwd = ctx.cwd;
|
|
4466
4733
|
}
|
|
4734
|
+
if (executionPlan.sessionName && succeeded) {
|
|
4735
|
+
if (openResultTabCorrection || sessionTabCorrection || aboutBlankSessionMismatch?.recoveryApplied) {
|
|
4736
|
+
sessionTabPinningReasons.set(executionPlan.sessionName, "drift");
|
|
4737
|
+
} else if (sessionTabPinningReason === "restore") {
|
|
4738
|
+
sessionTabPinningReasons.delete(executionPlan.sessionName);
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
|
|
4467
4742
|
if (replacedManagedSessionName) {
|
|
4468
4743
|
sessionTabTargets.delete(replacedManagedSessionName);
|
|
4469
4744
|
sessionRefSnapshots.delete(replacedManagedSessionName);
|
|
4745
|
+
sessionTabPinningReasons.delete(replacedManagedSessionName);
|
|
4470
4746
|
await closeManagedSession({
|
|
4471
4747
|
cwd: priorManagedSessionCwd,
|
|
4472
4748
|
sessionName: replacedManagedSessionName,
|
|
@@ -4622,10 +4898,28 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4622
4898
|
timedOut: processResult.timedOut,
|
|
4623
4899
|
validationError: undefined,
|
|
4624
4900
|
});
|
|
4901
|
+
let visibleRefFallbackDiagnostic: VisibleRefFallbackDiagnostic | undefined;
|
|
4902
|
+
const visibleRefFallbackSessionName = executionPlan.sessionName ?? extractExplicitSessionName(toolArgs);
|
|
4903
|
+
if (categoryDetails.failureCategory === "selector-not-found") {
|
|
4904
|
+
visibleRefFallbackDiagnostic = await collectVisibleRefFallbackDiagnostic({
|
|
4905
|
+
commandTokens,
|
|
4906
|
+
compiledSemanticAction,
|
|
4907
|
+
cwd: ctx.cwd,
|
|
4908
|
+
sessionName: visibleRefFallbackSessionName,
|
|
4909
|
+
signal,
|
|
4910
|
+
});
|
|
4911
|
+
if (visibleRefFallbackDiagnostic && visibleRefFallbackSessionName && shouldApplySessionTabTargetUpdate({ current: sessionRefSnapshots.get(visibleRefFallbackSessionName), updateOrder: tabTargetUpdateOrder })) {
|
|
4912
|
+
currentRefSnapshot = { ...visibleRefFallbackDiagnostic.snapshot, target: visibleRefFallbackDiagnostic.snapshot.target ?? currentSessionTabTarget };
|
|
4913
|
+
sessionRefSnapshots.set(visibleRefFallbackSessionName, { ...currentRefSnapshot, order: tabTargetUpdateOrder });
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4625
4916
|
let nextActions = presentation.nextActions ? [...presentation.nextActions] : undefined;
|
|
4626
4917
|
if (categoryDetails.failureCategory === "stale-ref") {
|
|
4627
4918
|
nextActions = sessionAwareStaleRefNextActions(executionPlan.sessionName);
|
|
4628
4919
|
}
|
|
4920
|
+
if (visibleRefFallbackDiagnostic) {
|
|
4921
|
+
(nextActions ??= []).push(...buildVisibleRefFallbackNextActions({ diagnostic: visibleRefFallbackDiagnostic, sessionName: visibleRefFallbackSessionName }));
|
|
4922
|
+
}
|
|
4629
4923
|
if (categoryDetails.failureCategory === "selector-not-found" && redactedCompiledSemanticAction) {
|
|
4630
4924
|
const candidateActions = buildSemanticActionCandidateActions(redactedCompiledSemanticAction);
|
|
4631
4925
|
if (candidateActions.length > 0) {
|
|
@@ -4644,7 +4938,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4644
4938
|
if (comboboxFocusDiagnostic) {
|
|
4645
4939
|
(nextActions ??= []).push(...buildComboboxFocusNextActions(executionPlan.sessionName));
|
|
4646
4940
|
}
|
|
4647
|
-
if (categoryDetails.failureCategory === "stale-ref" && redactedCompiledSemanticAction) {
|
|
4941
|
+
if (categoryDetails.failureCategory === "stale-ref" && redactedCompiledSemanticAction && isCompiledSemanticActionFindCommand(compiledSemanticAction)) {
|
|
4648
4942
|
(nextActions ??= []).push({
|
|
4649
4943
|
id: "retry-semantic-action-after-stale-ref",
|
|
4650
4944
|
params: { args: redactedCompiledSemanticAction.args },
|
|
@@ -4691,6 +4985,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4691
4985
|
nextActions,
|
|
4692
4986
|
pageChangeSummary,
|
|
4693
4987
|
overlayBlockers: overlayBlockerDiagnostic,
|
|
4988
|
+
visibleRefFallback: visibleRefFallbackDiagnostic,
|
|
4694
4989
|
comboboxFocus: comboboxFocusDiagnostic,
|
|
4695
4990
|
recordingDependencyWarning,
|
|
4696
4991
|
scrollNoop: scrollNoopDiagnostic,
|
|
@@ -4718,6 +5013,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4718
5013
|
timeoutMs: processResult.timeoutMs,
|
|
4719
5014
|
};
|
|
4720
5015
|
|
|
5016
|
+
const visibleRefFallbackText = formatVisibleRefFallbackText(visibleRefFallbackDiagnostic);
|
|
4721
5017
|
const semanticActionCandidateText = nextActions ? formatSemanticActionCandidateText(nextActions) : undefined;
|
|
4722
5018
|
const overlayBlockerText = overlayBlockerDiagnostic ? formatOverlayBlockerText(overlayBlockerDiagnostic) : undefined;
|
|
4723
5019
|
const selectorTextVisibilityText = formatSelectorTextVisibilityText(selectorTextVisibilityDiagnostics);
|
|
@@ -4728,7 +5024,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
4728
5024
|
const artifactCleanupText = formatArtifactCleanupGuidanceText(artifactCleanup);
|
|
4729
5025
|
const timeoutPartialProgressText = timeoutPartialProgress ? formatTimeoutPartialProgressText(timeoutPartialProgress) : undefined;
|
|
4730
5026
|
const managedSessionOutcomeText = formatManagedSessionOutcomeText(managedSessionOutcome);
|
|
4731
|
-
const rawAppendedDiagnosticText = [semanticActionCandidateText, overlayBlockerText, selectorTextVisibilityText, scrollNoopDiagnosticText, comboboxFocusDiagnosticText, recordingDependencyWarningText, evalStdinHintText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText].filter((item): item is string => item !== undefined).join("\n\n");
|
|
5027
|
+
const rawAppendedDiagnosticText = [visibleRefFallbackText, semanticActionCandidateText, overlayBlockerText, selectorTextVisibilityText, scrollNoopDiagnosticText, comboboxFocusDiagnosticText, recordingDependencyWarningText, evalStdinHintText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText].filter((item): item is string => item !== undefined).join("\n\n");
|
|
4732
5028
|
const appendedDiagnosticText = redactSensitiveText(redactExactSensitiveText(rawAppendedDiagnosticText, exactSensitiveValues));
|
|
4733
5029
|
const shouldAppendDiagnosticText = appendedDiagnosticText.length > 0 && (!userRequestedJson || plainTextInspection);
|
|
4734
5030
|
const content = shouldAppendDiagnosticText && redactedContent[0]?.type === "text"
|