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.
@@ -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: AgentBrowserSemanticLocator;
106
- value: string;
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: AgentBrowserSemanticLocator;
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, such as visible text, label text, placeholder text, test id, title, alt text, or role." }),
274
- text: Type.Optional(Type.String({ description: "Text/value argument for fill or select actions." })),
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 find command." })),
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/get-like steps." })),
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
- return { compiled: { args: ["batch"], query: { filter, maxWorkspaceFiles: maxWorkspaceFiles.value as number, requestId, url }, stdin: JSON.stringify(steps.map((step) => step.args)), steps } };
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 ((action === "fill" || action === "select") && text) {
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" && compiled.action !== "select") return undefined;
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 ((action === "fill" || action === "select") && (typeof text !== "string" || text.length === 0)) {
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" && action !== "select" && text !== undefined) {
1108
- return { error: `semanticAction.text is only supported for fill and select actions.` };
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" || action === "select") {
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 = isRecord(details.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 ? [["get", "title"], ["get", "url"]] : [];
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
- extractSessionTabTargetFromData(options.data) ??
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 titleStep = options.includeNavigationSummary ? steps[2] : undefined;
2659
- const urlStep = options.includeNavigationSummary ? steps[3] : undefined;
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
- const { cwd, sessionName, signal } = options;
2715
- const title = extractStringResultField(
2716
- await runSessionCommandData({ args: ["get", "title"], cwd, sessionName, signal }),
2717
- "title",
2718
- );
2719
- const url = extractStringResultField(
2720
- await runSessionCommandData({ args: ["get", "url"], cwd, sessionName, signal }),
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", "select"].includes(token));
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", "select"].includes(compiled.action)) return false;
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 isEmptyRecord(value: unknown): boolean {
3213
- return isRecord(value) && Object.keys(value).length === 0;
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 (!isEmptyRecord(result)) return undefined;
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
- extractSessionTabTargetFromData(presentationEnvelope?.data);
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"