pi-agent-browser-native 0.2.25 → 0.2.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -1
- package/README.md +19 -8
- package/docs/ARCHITECTURE.md +7 -1
- package/docs/COMMAND_REFERENCE.md +22 -5
- package/docs/RELEASE.md +5 -1
- package/docs/REQUIREMENTS.md +2 -1
- package/docs/SUPPORT_MATRIX.md +20 -0
- package/docs/TOOL_CONTRACT.md +44 -13
- package/extensions/agent-browser/index.ts +1003 -19
- package/extensions/agent-browser/lib/playbook.ts +6 -5
- package/extensions/agent-browser/lib/results/presentation.ts +83 -5
- package/extensions/agent-browser/lib/results/shared.ts +72 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +69 -9
- package/extensions/agent-browser/lib/runtime.ts +7 -2
- package/package.json +1 -1
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
parseAgentBrowserEnvelope,
|
|
35
35
|
type AgentBrowserBatchResult,
|
|
36
36
|
type AgentBrowserEnvelope,
|
|
37
|
+
type AgentBrowserNextAction,
|
|
37
38
|
} from "./lib/results.js";
|
|
38
39
|
import {
|
|
39
40
|
buildExecutionPlan,
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
resolveManagedSessionState,
|
|
57
58
|
shouldAppendBrowserSystemPrompt,
|
|
58
59
|
validateToolArgs,
|
|
60
|
+
type CommandInfo,
|
|
59
61
|
type CompatibilityWorkaround,
|
|
60
62
|
type OpenResultTabCorrection,
|
|
61
63
|
} from "./lib/runtime.js";
|
|
@@ -72,6 +74,7 @@ import {
|
|
|
72
74
|
formatSessionArtifactRetentionSummary,
|
|
73
75
|
isSessionArtifactManifest,
|
|
74
76
|
mergeSessionArtifactManifest,
|
|
77
|
+
summarizeNetworkFailures,
|
|
75
78
|
} from "./lib/results/shared.js";
|
|
76
79
|
|
|
77
80
|
const DEFAULT_SESSION_MODE = "auto" as const;
|
|
@@ -99,6 +102,7 @@ interface AgentBrowserSemanticActionInput {
|
|
|
99
102
|
text?: string;
|
|
100
103
|
role?: string;
|
|
101
104
|
name?: string;
|
|
105
|
+
session?: string;
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
interface CompiledAgentBrowserSemanticAction {
|
|
@@ -222,6 +226,7 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
222
226
|
text: Type.Optional(Type.String({ description: "Text/value argument for fill or select actions." })),
|
|
223
227
|
role: Type.Optional(Type.String({ description: "Role locator value; when set it must match value for locator=role." })),
|
|
224
228
|
name: Type.Optional(Type.String({ description: "Accessible name filter for locator=role; compiles to --name <name>." })),
|
|
229
|
+
session: Type.Optional(Type.String({ description: "Optional upstream session name; prepends --session <name> before the compiled find command." })),
|
|
225
230
|
}),
|
|
226
231
|
),
|
|
227
232
|
qa: Type.Optional(
|
|
@@ -232,7 +237,7 @@ const AGENT_BROWSER_PARAMS = Type.Object({
|
|
|
232
237
|
screenshotPath: Type.Optional(Type.String({ description: "Optional evidence screenshot path captured at the end of the QA preset." })),
|
|
233
238
|
checkConsole: Type.Optional(Type.Boolean({ description: "Whether to fail on console error messages. Defaults to true." })),
|
|
234
239
|
checkErrors: Type.Optional(Type.Boolean({ description: "Whether to fail on page errors. Defaults to true." })),
|
|
235
|
-
checkNetwork: Type.Optional(Type.Boolean({ description: "Whether to fail on
|
|
240
|
+
checkNetwork: Type.Optional(Type.Boolean({ description: "Whether to inspect network requests and fail on actionable request failures; benign icon misses warn. Defaults to true." })),
|
|
236
241
|
}),
|
|
237
242
|
),
|
|
238
243
|
sourceLookup: Type.Optional(
|
|
@@ -366,6 +371,7 @@ interface AgentBrowserQaPresetAnalysis {
|
|
|
366
371
|
failedChecks: string[];
|
|
367
372
|
passed: boolean;
|
|
368
373
|
summary: string;
|
|
374
|
+
warnings: string[];
|
|
369
375
|
}
|
|
370
376
|
|
|
371
377
|
function getBatchResultItems(data: unknown): Array<Record<string, unknown>> {
|
|
@@ -381,6 +387,7 @@ function analyzeQaPresetResults(data: unknown): AgentBrowserQaPresetAnalysis | u
|
|
|
381
387
|
const items = getBatchResultItems(data);
|
|
382
388
|
if (items.length === 0) return undefined;
|
|
383
389
|
const failedChecks: string[] = [];
|
|
390
|
+
const warnings: string[] = [];
|
|
384
391
|
for (const item of items) {
|
|
385
392
|
if (item.success === false) {
|
|
386
393
|
failedChecks.push(`${getCommandNameFromBatchItem(item) ?? "step"} failed`);
|
|
@@ -395,15 +402,20 @@ function analyzeQaPresetResults(data: unknown): AgentBrowserQaPresetAnalysis | u
|
|
|
395
402
|
if (errorCount > 0) failedChecks.push(`${errorCount} console error message(s)`);
|
|
396
403
|
}
|
|
397
404
|
if (commandName === "network" && Array.isArray(result?.requests)) {
|
|
398
|
-
const
|
|
399
|
-
if (
|
|
405
|
+
const networkFailures = summarizeNetworkFailures(result.requests);
|
|
406
|
+
if (networkFailures.actionableCount > 0) failedChecks.push(`${networkFailures.actionableCount} actionable failed network request(s)`);
|
|
407
|
+
if (networkFailures.benignCount > 0) warnings.push(`${networkFailures.benignCount} benign network request failure(s) ignored`);
|
|
400
408
|
}
|
|
401
409
|
}
|
|
402
410
|
const uniqueFailures = [...new Set(failedChecks)];
|
|
411
|
+
const uniqueWarnings = [...new Set(warnings)];
|
|
403
412
|
return {
|
|
404
413
|
failedChecks: uniqueFailures,
|
|
405
414
|
passed: uniqueFailures.length === 0,
|
|
406
|
-
summary: uniqueFailures.length === 0
|
|
415
|
+
summary: uniqueFailures.length === 0
|
|
416
|
+
? uniqueWarnings.length === 0 ? "QA preset passed." : `QA preset passed with warnings: ${uniqueWarnings.join("; ")}.`
|
|
417
|
+
: `QA preset failed: ${uniqueFailures.join("; ")}.`,
|
|
418
|
+
warnings: uniqueWarnings,
|
|
407
419
|
};
|
|
408
420
|
}
|
|
409
421
|
|
|
@@ -878,6 +890,85 @@ async function analyzeNetworkSourceLookupResults(data: unknown, compiled: Compil
|
|
|
878
890
|
return { candidates, failedRequests, limitations, status, summary: failedRequests.length === 0 ? "Network source lookup found no failed requests." : candidates.length > 0 ? `Network source lookup found ${failedRequests.length} failed request(s) and ${candidates.length} candidate source hint(s).` : `Network source lookup found ${failedRequests.length} failed request(s) but no source candidates.` };
|
|
879
891
|
}
|
|
880
892
|
|
|
893
|
+
function appendSemanticActionTextArg(args: string[], action: string, text: string | undefined): void {
|
|
894
|
+
if ((action === "fill" || action === "select") && text) {
|
|
895
|
+
args.push(text);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function getCompiledSemanticActionCommandIndex(compiled: CompiledAgentBrowserSemanticAction): number {
|
|
900
|
+
return compiled.args[0] === "--session" ? 2 : 0;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function getCompiledSemanticActionTextArg(compiled: CompiledAgentBrowserSemanticAction): string | undefined {
|
|
904
|
+
if (compiled.action !== "fill" && compiled.action !== "select") return undefined;
|
|
905
|
+
const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
|
|
906
|
+
if (commandIndex < 0) return undefined;
|
|
907
|
+
const markerIndex = compiled.args.indexOf("--name");
|
|
908
|
+
return markerIndex >= 0 ? compiled.args[markerIndex - 1] : compiled.args[commandIndex + 4];
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function getCompiledSemanticActionSessionPrefix(compiled: CompiledAgentBrowserSemanticAction): string[] {
|
|
912
|
+
const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
|
|
913
|
+
return commandIndex > 0 ? compiled.args.slice(0, commandIndex) : [];
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const SEMANTIC_ACTION_CANDIDATE_ACTION_IDS = new Set([
|
|
917
|
+
"try-searchbox-name-candidate",
|
|
918
|
+
"try-textbox-name-candidate",
|
|
919
|
+
"try-button-name-candidate",
|
|
920
|
+
"try-link-name-candidate",
|
|
921
|
+
"try-labeled-textbox-candidate",
|
|
922
|
+
]);
|
|
923
|
+
|
|
924
|
+
function formatSemanticActionCandidateText(actions: AgentBrowserNextAction[]): string | undefined {
|
|
925
|
+
const candidateActions = actions.filter((action) => SEMANTIC_ACTION_CANDIDATE_ACTION_IDS.has(action.id) && action.params?.args);
|
|
926
|
+
if (candidateActions.length === 0) return undefined;
|
|
927
|
+
return [
|
|
928
|
+
"Agent-browser candidate fallbacks:",
|
|
929
|
+
...candidateActions.map((action) => `- ${action.id}: agent_browser ${JSON.stringify({ args: action.params?.args })} — ${action.reason}`),
|
|
930
|
+
].join("\n");
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function buildSemanticActionCandidateActions(compiled: CompiledAgentBrowserSemanticAction): AgentBrowserNextAction[] {
|
|
934
|
+
const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
|
|
935
|
+
if (commandIndex < 0) return [];
|
|
936
|
+
const locator = compiled.args[commandIndex + 1];
|
|
937
|
+
const value = compiled.args[commandIndex + 2];
|
|
938
|
+
if (!locator || !value) return [];
|
|
939
|
+
const text = getCompiledSemanticActionTextArg(compiled);
|
|
940
|
+
const sessionPrefix = getCompiledSemanticActionSessionPrefix(compiled);
|
|
941
|
+
const buildRoleCandidate = (role: string, id: string, reason: string): AgentBrowserNextAction => {
|
|
942
|
+
const args = [...sessionPrefix, "find", "role", role, compiled.action];
|
|
943
|
+
appendSemanticActionTextArg(args, compiled.action, text);
|
|
944
|
+
args.push("--name", value);
|
|
945
|
+
return {
|
|
946
|
+
id,
|
|
947
|
+
params: { args: redactInvocationArgs(args) },
|
|
948
|
+
reason,
|
|
949
|
+
safety: "Candidate locator fallback only; inspect the page if multiple elements could match the same accessible name.",
|
|
950
|
+
tool: "agent_browser" as const,
|
|
951
|
+
};
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
if (locator === "placeholder" && compiled.action === "fill") {
|
|
955
|
+
return [
|
|
956
|
+
buildRoleCandidate("searchbox", "try-searchbox-name-candidate", "Retry against a searchbox with the same accessible name; many search inputs expose names instead of placeholders."),
|
|
957
|
+
buildRoleCandidate("textbox", "try-textbox-name-candidate", "Retry against a textbox with the same accessible name when placeholder lookup misses."),
|
|
958
|
+
];
|
|
959
|
+
}
|
|
960
|
+
if (locator === "text" && compiled.action === "click") {
|
|
961
|
+
return [
|
|
962
|
+
buildRoleCandidate("button", "try-button-name-candidate", "Retry against a button with the same accessible name when text lookup misses."),
|
|
963
|
+
buildRoleCandidate("link", "try-link-name-candidate", "Retry against a link with the same accessible name when text lookup misses."),
|
|
964
|
+
];
|
|
965
|
+
}
|
|
966
|
+
if (locator === "label" && compiled.action === "fill") {
|
|
967
|
+
return [buildRoleCandidate("textbox", "try-labeled-textbox-candidate", "Retry against a textbox with the same accessible name when label lookup misses.")];
|
|
968
|
+
}
|
|
969
|
+
return [];
|
|
970
|
+
}
|
|
971
|
+
|
|
881
972
|
function compileAgentBrowserSemanticAction(input: unknown): { compiled?: CompiledAgentBrowserSemanticAction; error?: string } {
|
|
882
973
|
if (!isRecord(input)) {
|
|
883
974
|
return { error: "semanticAction must be an object." };
|
|
@@ -888,6 +979,7 @@ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: Compile
|
|
|
888
979
|
const text = input.text;
|
|
889
980
|
const role = input.role;
|
|
890
981
|
const name = input.name;
|
|
982
|
+
const session = input.session;
|
|
891
983
|
if (typeof action !== "string" || !AGENT_BROWSER_SEMANTIC_ACTIONS.includes(action as AgentBrowserSemanticActionName)) {
|
|
892
984
|
return { error: `semanticAction.action must be one of: ${AGENT_BROWSER_SEMANTIC_ACTIONS.join(", ")}.` };
|
|
893
985
|
}
|
|
@@ -912,7 +1004,10 @@ function compileAgentBrowserSemanticAction(input: unknown): { compiled?: Compile
|
|
|
912
1004
|
if (name !== undefined && (locator !== "role" || typeof name !== "string" || name.length === 0)) {
|
|
913
1005
|
return { error: "semanticAction.name is only supported as a non-empty string for locator=role." };
|
|
914
1006
|
}
|
|
915
|
-
|
|
1007
|
+
if (session !== undefined && (typeof session !== "string" || session.trim().length === 0)) {
|
|
1008
|
+
return { error: "semanticAction.session must be a non-empty string when provided." };
|
|
1009
|
+
}
|
|
1010
|
+
const args = typeof session === "string" ? ["--session", session, "find", locator, value, action] : ["find", locator, value, action];
|
|
916
1011
|
if (action === "fill" || action === "select") {
|
|
917
1012
|
args.push(text as string);
|
|
918
1013
|
}
|
|
@@ -1300,6 +1395,72 @@ interface NavigationSummary {
|
|
|
1300
1395
|
url?: string;
|
|
1301
1396
|
}
|
|
1302
1397
|
|
|
1398
|
+
interface OverlayBlockerCandidate {
|
|
1399
|
+
args: string[];
|
|
1400
|
+
name?: string;
|
|
1401
|
+
reason: string;
|
|
1402
|
+
ref: string;
|
|
1403
|
+
role?: string;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
interface OverlayBlockerDiagnostic {
|
|
1407
|
+
candidates: OverlayBlockerCandidate[];
|
|
1408
|
+
snapshot: SessionRefSnapshot;
|
|
1409
|
+
summary: string;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
interface SelectorTextVisibilityDiagnostic {
|
|
1413
|
+
firstMatchVisible?: boolean;
|
|
1414
|
+
firstVisibleTextPreview?: string;
|
|
1415
|
+
matchCount: number;
|
|
1416
|
+
selector: string;
|
|
1417
|
+
summary: string;
|
|
1418
|
+
visibleCount: number;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
interface TimeoutArtifactEvidence {
|
|
1422
|
+
absolutePath: string;
|
|
1423
|
+
exists: boolean;
|
|
1424
|
+
path: string;
|
|
1425
|
+
sizeBytes?: number;
|
|
1426
|
+
stepIndex: number;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
interface TimeoutPartialProgress {
|
|
1430
|
+
artifacts: TimeoutArtifactEvidence[];
|
|
1431
|
+
currentPage?: {
|
|
1432
|
+
title?: string;
|
|
1433
|
+
url?: string;
|
|
1434
|
+
};
|
|
1435
|
+
steps?: Array<{ args: string[]; index: number }>;
|
|
1436
|
+
summary: string;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
interface EvalStdinHint {
|
|
1440
|
+
reason: string;
|
|
1441
|
+
suggestion: string;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
interface ArtifactCleanupGuidance {
|
|
1445
|
+
explicitArtifactPaths: string[];
|
|
1446
|
+
note: string;
|
|
1447
|
+
owner: "host-file-tools";
|
|
1448
|
+
summary: string;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
interface ManagedSessionOutcome {
|
|
1452
|
+
activeAfter: boolean;
|
|
1453
|
+
activeBefore: boolean;
|
|
1454
|
+
attemptedSessionName?: string;
|
|
1455
|
+
currentSessionName: string;
|
|
1456
|
+
previousSessionName: string;
|
|
1457
|
+
replacedSessionName?: string;
|
|
1458
|
+
sessionMode: "auto" | "fresh";
|
|
1459
|
+
status: "abandoned" | "closed" | "created" | "preserved" | "replaced" | "unchanged";
|
|
1460
|
+
succeeded: boolean;
|
|
1461
|
+
summary: string;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1303
1464
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
1304
1465
|
return typeof value === "object" && value !== null;
|
|
1305
1466
|
}
|
|
@@ -1703,7 +1864,7 @@ function shouldCaptureNavigationSummary(command: string | undefined, data: unkno
|
|
|
1703
1864
|
);
|
|
1704
1865
|
}
|
|
1705
1866
|
|
|
1706
|
-
function extractStringResultField(data: unknown, fieldName: "title" | "url"): string | undefined {
|
|
1867
|
+
function extractStringResultField(data: unknown, fieldName: "result" | "title" | "url"): string | undefined {
|
|
1707
1868
|
if (typeof data === "string") {
|
|
1708
1869
|
const text = data.trim();
|
|
1709
1870
|
return text.length > 0 ? text : undefined;
|
|
@@ -1740,6 +1901,21 @@ interface OrderedSessionTabTarget {
|
|
|
1740
1901
|
target: SessionTabTarget;
|
|
1741
1902
|
}
|
|
1742
1903
|
|
|
1904
|
+
interface SessionRefSnapshot {
|
|
1905
|
+
refIds: string[];
|
|
1906
|
+
target?: SessionTabTarget;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
interface OrderedSessionRefSnapshot extends SessionRefSnapshot {
|
|
1910
|
+
order: number;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
interface StaleRefPreflight {
|
|
1914
|
+
message: string;
|
|
1915
|
+
refIds: string[];
|
|
1916
|
+
snapshot?: SessionRefSnapshot;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1743
1919
|
interface AboutBlankSessionMismatch {
|
|
1744
1920
|
activeUrl: "about:blank";
|
|
1745
1921
|
recoveryApplied: boolean;
|
|
@@ -1748,7 +1924,7 @@ interface AboutBlankSessionMismatch {
|
|
|
1748
1924
|
targetUrl: string;
|
|
1749
1925
|
}
|
|
1750
1926
|
|
|
1751
|
-
function getLatestSessionTabTargetOrder(targets: Map<string,
|
|
1927
|
+
function getLatestSessionTabTargetOrder(targets: Map<string, { order: number }>): number {
|
|
1752
1928
|
let latestOrder = 0;
|
|
1753
1929
|
for (const target of targets.values()) {
|
|
1754
1930
|
latestOrder = Math.max(latestOrder, target.order);
|
|
@@ -1757,7 +1933,7 @@ function getLatestSessionTabTargetOrder(targets: Map<string, OrderedSessionTabTa
|
|
|
1757
1933
|
}
|
|
1758
1934
|
|
|
1759
1935
|
function shouldApplySessionTabTargetUpdate(options: {
|
|
1760
|
-
current?:
|
|
1936
|
+
current?: { order: number };
|
|
1761
1937
|
updateOrder: number;
|
|
1762
1938
|
}): boolean {
|
|
1763
1939
|
return !options.current || options.updateOrder >= options.current.order;
|
|
@@ -1903,6 +2079,66 @@ function restoreSessionTabTargetsFromBranch(branch: unknown[]): Map<string, Orde
|
|
|
1903
2079
|
return restoredTargets;
|
|
1904
2080
|
}
|
|
1905
2081
|
|
|
2082
|
+
function extractRefSnapshotFromData(data: unknown): SessionRefSnapshot | undefined {
|
|
2083
|
+
if (!isRecord(data)) return undefined;
|
|
2084
|
+
const refIds = isRecord(data.refs) ? Object.keys(data.refs).filter((refId) => /^e\d+$/.test(refId)) : [];
|
|
2085
|
+
if (refIds.length === 0) return undefined;
|
|
2086
|
+
return {
|
|
2087
|
+
refIds,
|
|
2088
|
+
target: extractSessionTabTargetFromData(data),
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
function extractRefSnapshotFromBatchResults(data: unknown): SessionRefSnapshot | undefined {
|
|
2093
|
+
if (!Array.isArray(data)) return undefined;
|
|
2094
|
+
let latestSnapshot: SessionRefSnapshot | undefined;
|
|
2095
|
+
for (const item of data) {
|
|
2096
|
+
if (!isRecord(item) || item.success === false) continue;
|
|
2097
|
+
const [name] = extractBatchResultCommand(item);
|
|
2098
|
+
if (name !== "snapshot") continue;
|
|
2099
|
+
latestSnapshot = extractRefSnapshotFromData(item.result) ?? latestSnapshot;
|
|
2100
|
+
}
|
|
2101
|
+
return latestSnapshot;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
function restoreSessionRefSnapshotsFromBranch(branch: unknown[]): Map<string, OrderedSessionRefSnapshot> {
|
|
2105
|
+
const restoredSnapshots = new Map<string, OrderedSessionRefSnapshot>();
|
|
2106
|
+
let restoredOrder = 0;
|
|
2107
|
+
for (const entry of branch) {
|
|
2108
|
+
if (!isRecord(entry) || entry.type !== "message") continue;
|
|
2109
|
+
const message = isRecord(entry.message) ? entry.message : undefined;
|
|
2110
|
+
if (!message || message.toolName !== "agent_browser") continue;
|
|
2111
|
+
const details = isRecord(message.details) ? message.details : undefined;
|
|
2112
|
+
if (!details) continue;
|
|
2113
|
+
const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
|
|
2114
|
+
if (!sessionName) continue;
|
|
2115
|
+
const command = typeof details.command === "string" ? details.command : undefined;
|
|
2116
|
+
if (command === "close" && message.isError !== true) {
|
|
2117
|
+
restoredOrder += 1;
|
|
2118
|
+
restoredSnapshots.delete(sessionName);
|
|
2119
|
+
continue;
|
|
2120
|
+
}
|
|
2121
|
+
const refSnapshot = isRecord(details.refSnapshot)
|
|
2122
|
+
? {
|
|
2123
|
+
refIds: Array.isArray(details.refSnapshot.refIds)
|
|
2124
|
+
? details.refSnapshot.refIds.filter((refId): refId is string => typeof refId === "string" && /^e\d+$/.test(refId))
|
|
2125
|
+
: [],
|
|
2126
|
+
target: isRecord(details.refSnapshot.target)
|
|
2127
|
+
? normalizeSessionTabTarget({
|
|
2128
|
+
title: typeof details.refSnapshot.target.title === "string" ? details.refSnapshot.target.title : undefined,
|
|
2129
|
+
url: typeof details.refSnapshot.target.url === "string" ? details.refSnapshot.target.url : undefined,
|
|
2130
|
+
})
|
|
2131
|
+
: undefined,
|
|
2132
|
+
}
|
|
2133
|
+
: undefined;
|
|
2134
|
+
if (refSnapshot && refSnapshot.refIds.length > 0) {
|
|
2135
|
+
restoredOrder += 1;
|
|
2136
|
+
restoredSnapshots.set(sessionName, { ...refSnapshot, order: restoredOrder });
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
return restoredSnapshots;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
1906
2142
|
function restoreArtifactManifestFromBranch(branch: unknown[]): SessionArtifactManifest | undefined {
|
|
1907
2143
|
let restoredManifest: SessionArtifactManifest | undefined;
|
|
1908
2144
|
for (const entry of branch) {
|
|
@@ -2052,6 +2288,46 @@ function parseUserBatchStdin(stdin: string | undefined): { error?: string; steps
|
|
|
2052
2288
|
}
|
|
2053
2289
|
}
|
|
2054
2290
|
|
|
2291
|
+
const REF_INVALIDATING_BATCH_COMMANDS = new Set([
|
|
2292
|
+
"back",
|
|
2293
|
+
"check",
|
|
2294
|
+
"click",
|
|
2295
|
+
"dblclick",
|
|
2296
|
+
"drag",
|
|
2297
|
+
"fill",
|
|
2298
|
+
"forward",
|
|
2299
|
+
"goto",
|
|
2300
|
+
"keyboard",
|
|
2301
|
+
"mouse",
|
|
2302
|
+
"navigate",
|
|
2303
|
+
"open",
|
|
2304
|
+
"press",
|
|
2305
|
+
"reload",
|
|
2306
|
+
"select",
|
|
2307
|
+
"type",
|
|
2308
|
+
"uncheck",
|
|
2309
|
+
"upload",
|
|
2310
|
+
]);
|
|
2311
|
+
|
|
2312
|
+
const REF_GUARDED_COMMANDS = new Set([
|
|
2313
|
+
"check",
|
|
2314
|
+
"click",
|
|
2315
|
+
"dblclick",
|
|
2316
|
+
"download",
|
|
2317
|
+
"drag",
|
|
2318
|
+
"fill",
|
|
2319
|
+
"focus",
|
|
2320
|
+
"hover",
|
|
2321
|
+
"keyboard",
|
|
2322
|
+
"mouse",
|
|
2323
|
+
"press",
|
|
2324
|
+
"scrollintoview",
|
|
2325
|
+
"select",
|
|
2326
|
+
"type",
|
|
2327
|
+
"uncheck",
|
|
2328
|
+
"upload",
|
|
2329
|
+
]);
|
|
2330
|
+
|
|
2055
2331
|
function getStaleRefArgs(commandTokens: string[], stdin?: string): string[] {
|
|
2056
2332
|
if (commandTokens[0] !== "batch" || stdin === undefined) {
|
|
2057
2333
|
return commandTokens;
|
|
@@ -2063,6 +2339,101 @@ function getStaleRefArgs(commandTokens: string[], stdin?: string): string[] {
|
|
|
2063
2339
|
return parsed.steps.flatMap((step) => step);
|
|
2064
2340
|
}
|
|
2065
2341
|
|
|
2342
|
+
function collectRefsFromTokens(tokens: string[]): string[] {
|
|
2343
|
+
return tokens.filter((token) => /^@e\d+\b/.test(token)).map((token) => token.slice(1));
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
function getGuardedRefUsage(commandTokens: string[], stdin?: string): string[] {
|
|
2347
|
+
const collectFromStep = (step: string[]) => REF_GUARDED_COMMANDS.has(step[0] ?? "") ? collectRefsFromTokens(step) : [];
|
|
2348
|
+
if (commandTokens[0] !== "batch" || stdin === undefined) {
|
|
2349
|
+
return collectFromStep(commandTokens);
|
|
2350
|
+
}
|
|
2351
|
+
const parsed = parseUserBatchStdin(stdin);
|
|
2352
|
+
if (parsed.error || parsed.steps === undefined) {
|
|
2353
|
+
return collectFromStep(commandTokens);
|
|
2354
|
+
}
|
|
2355
|
+
const refsBeforeInBatchSnapshot: string[] = [];
|
|
2356
|
+
for (const step of parsed.steps) {
|
|
2357
|
+
if ((step[0] ?? "") === "snapshot") break;
|
|
2358
|
+
refsBeforeInBatchSnapshot.push(...collectFromStep(step));
|
|
2359
|
+
}
|
|
2360
|
+
return refsBeforeInBatchSnapshot;
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
function targetsMatch(left: SessionTabTarget | undefined, right: SessionTabTarget | undefined): boolean {
|
|
2364
|
+
if (!left || !right) return true;
|
|
2365
|
+
return normalizeComparableUrl(left.url) === normalizeComparableUrl(right.url);
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
function getBatchRefInvalidationMessage(commandTokens: string[], stdin?: string): string | undefined {
|
|
2369
|
+
if (commandTokens[0] !== "batch" || stdin === undefined) return undefined;
|
|
2370
|
+
const parsed = parseUserBatchStdin(stdin);
|
|
2371
|
+
if (parsed.error || parsed.steps === undefined) return undefined;
|
|
2372
|
+
let priorStepInvalidatesRefs = false;
|
|
2373
|
+
for (const step of parsed.steps) {
|
|
2374
|
+
if ((step[0] ?? "") === "snapshot") {
|
|
2375
|
+
priorStepInvalidatesRefs = false;
|
|
2376
|
+
}
|
|
2377
|
+
const refIds = collectRefsFromTokens(step);
|
|
2378
|
+
if (refIds.length > 0 && REF_GUARDED_COMMANDS.has(step[0] ?? "") && priorStepInvalidatesRefs) {
|
|
2379
|
+
return `Batch step ${step[0]} uses page-scoped ref ${refIds.map((refId) => `@${refId}`).join(", ")} after an earlier batch step can navigate or mutate the page. Split the batch, run snapshot -i after the page-changing step, then retry with current refs.`;
|
|
2380
|
+
}
|
|
2381
|
+
if (REF_INVALIDATING_BATCH_COMMANDS.has(step[0] ?? "")) {
|
|
2382
|
+
priorStepInvalidatesRefs = true;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
return undefined;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
function buildStaleRefPreflight(options: {
|
|
2389
|
+
commandTokens: string[];
|
|
2390
|
+
currentTarget?: SessionTabTarget;
|
|
2391
|
+
refSnapshot?: SessionRefSnapshot;
|
|
2392
|
+
stdin?: string;
|
|
2393
|
+
}): StaleRefPreflight | undefined {
|
|
2394
|
+
const usedRefIds = [...new Set(getGuardedRefUsage(options.commandTokens, options.stdin))];
|
|
2395
|
+
const batchInvalidationMessage = getBatchRefInvalidationMessage(options.commandTokens, options.stdin);
|
|
2396
|
+
if (batchInvalidationMessage && usedRefIds.length > 0) {
|
|
2397
|
+
return {
|
|
2398
|
+
message: batchInvalidationMessage,
|
|
2399
|
+
refIds: usedRefIds,
|
|
2400
|
+
snapshot: options.refSnapshot,
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
if (usedRefIds.length === 0 || !options.refSnapshot) return undefined;
|
|
2404
|
+
if (!targetsMatch(options.refSnapshot.target, options.currentTarget)) {
|
|
2405
|
+
return {
|
|
2406
|
+
message: `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} came from a snapshot for ${options.refSnapshot.target?.url ?? "a prior page"}, but the current session target is ${options.currentTarget?.url ?? "unknown"}. Run snapshot -i again before using page-scoped refs.`,
|
|
2407
|
+
refIds: usedRefIds,
|
|
2408
|
+
snapshot: options.refSnapshot,
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
const knownRefs = new Set(options.refSnapshot.refIds);
|
|
2412
|
+
const missingRefs = usedRefIds.filter((refId) => !knownRefs.has(refId));
|
|
2413
|
+
if (missingRefs.length > 0) {
|
|
2414
|
+
return {
|
|
2415
|
+
message: `Ref ${missingRefs.map((refId) => `@${refId}`).join(", ")} was not present in the latest snapshot for this session. Run snapshot -i again before using page-scoped refs.`,
|
|
2416
|
+
refIds: missingRefs,
|
|
2417
|
+
snapshot: options.refSnapshot,
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
return undefined;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
function sessionPrefixArgs(sessionName: string | undefined, args: string[]): string[] {
|
|
2424
|
+
return sessionName && args[0] !== "--session" ? ["--session", sessionName, ...args] : args;
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
function sessionAwareStaleRefNextActions(sessionName: string | undefined): AgentBrowserNextAction[] {
|
|
2428
|
+
return (buildAgentBrowserNextActions({ failureCategory: "stale-ref", resultCategory: "failure" }) ?? []).map((action) => {
|
|
2429
|
+
const actionArgs = action.params?.args;
|
|
2430
|
+
return {
|
|
2431
|
+
...action,
|
|
2432
|
+
params: action.params && actionArgs ? { ...action.params, args: sessionPrefixArgs(sessionName, actionArgs) } : action.params,
|
|
2433
|
+
};
|
|
2434
|
+
});
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2066
2437
|
function buildPinnedBatchPlan(options: {
|
|
2067
2438
|
command?: string;
|
|
2068
2439
|
commandTokens: string[];
|
|
@@ -2196,14 +2567,16 @@ async function runSessionCommandData(options: {
|
|
|
2196
2567
|
cwd: string;
|
|
2197
2568
|
sessionName?: string;
|
|
2198
2569
|
signal?: AbortSignal;
|
|
2570
|
+
stdin?: string;
|
|
2199
2571
|
}): Promise<unknown | undefined> {
|
|
2200
|
-
const { args, cwd, sessionName, signal } = options;
|
|
2572
|
+
const { args, cwd, sessionName, signal, stdin } = options;
|
|
2201
2573
|
if (!sessionName) return undefined;
|
|
2202
2574
|
|
|
2203
2575
|
const processResult = await runAgentBrowserProcess({
|
|
2204
2576
|
args: ["--json", "--session", sessionName, ...args],
|
|
2205
2577
|
cwd,
|
|
2206
2578
|
signal,
|
|
2579
|
+
stdin,
|
|
2207
2580
|
});
|
|
2208
2581
|
try {
|
|
2209
2582
|
if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
|
|
@@ -2249,6 +2622,401 @@ function mergeNavigationSummaryIntoData(data: unknown, navigationSummary: Naviga
|
|
|
2249
2622
|
return { navigationSummary, result: data };
|
|
2250
2623
|
}
|
|
2251
2624
|
|
|
2625
|
+
function getSnapshotRefRecord(data: unknown): Record<string, unknown> | undefined {
|
|
2626
|
+
return isRecord(data) && isRecord(data.refs) ? data.refs : undefined;
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
const OVERLAY_CLOSE_NAME_PATTERN = /(?:\b(?:close|dismiss|no thanks|not now|maybe later|hide|skip|continue without|x)\b|^\s*×\s*$)/i;
|
|
2630
|
+
const OVERLAY_CONTEXT_NAME_PATTERN = /\b(?:banner|modal|dialog|popup|pop-up|overlay|donat(?:e|ion)|subscribe|sign in|login|cookie|privacy|consent)\b/i;
|
|
2631
|
+
const OVERLAY_CONTEXT_ROLES = new Set(["alertdialog", "dialog"]);
|
|
2632
|
+
const OVERLAY_ACTION_ROLES = new Set(["button", "link", "menuitem"]);
|
|
2633
|
+
const OVERLAY_BLOCKER_CANDIDATE_LIMIT = 3;
|
|
2634
|
+
|
|
2635
|
+
function getOverlayBlockerCandidates(snapshotData: unknown): OverlayBlockerCandidate[] {
|
|
2636
|
+
const refs = getSnapshotRefRecord(snapshotData);
|
|
2637
|
+
if (!refs) return [];
|
|
2638
|
+
const hasOverlayContext = Object.values(refs).some((entry) => {
|
|
2639
|
+
if (!isRecord(entry)) return false;
|
|
2640
|
+
const role = typeof entry.role === "string" ? entry.role : "";
|
|
2641
|
+
const name = typeof entry.name === "string" ? entry.name : "";
|
|
2642
|
+
return OVERLAY_CONTEXT_ROLES.has(role.toLowerCase()) || OVERLAY_CONTEXT_NAME_PATTERN.test(name);
|
|
2643
|
+
});
|
|
2644
|
+
if (!hasOverlayContext) return [];
|
|
2645
|
+
const candidates: OverlayBlockerCandidate[] = [];
|
|
2646
|
+
for (const [ref, entry] of Object.entries(refs)) {
|
|
2647
|
+
if (!/^e\d+$/.test(ref) || !isRecord(entry)) continue;
|
|
2648
|
+
const role = typeof entry.role === "string" ? entry.role : undefined;
|
|
2649
|
+
const name = typeof entry.name === "string" ? entry.name : undefined;
|
|
2650
|
+
if (!role || !OVERLAY_ACTION_ROLES.has(role.toLowerCase()) || !name || !OVERLAY_CLOSE_NAME_PATTERN.test(name)) continue;
|
|
2651
|
+
candidates.push({
|
|
2652
|
+
args: ["click", `@${ref}`],
|
|
2653
|
+
name,
|
|
2654
|
+
reason: `Visible ${role} ${JSON.stringify(name)} appears in a snapshot that also contains overlay/banner/dialog context.`,
|
|
2655
|
+
ref: `@${ref}`,
|
|
2656
|
+
role,
|
|
2657
|
+
});
|
|
2658
|
+
if (candidates.length >= OVERLAY_BLOCKER_CANDIDATE_LIMIT) break;
|
|
2659
|
+
}
|
|
2660
|
+
return candidates;
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
function formatOverlayBlockerText(diagnostic: OverlayBlockerDiagnostic): string {
|
|
2664
|
+
return [
|
|
2665
|
+
"Possible overlay blockers:",
|
|
2666
|
+
...diagnostic.candidates.map((candidate) => `- ${candidate.ref}${candidate.role ? ` ${candidate.role}` : ""}${candidate.name ? ` ${JSON.stringify(candidate.name)}` : ""}: ${candidate.reason}`),
|
|
2667
|
+
].join("\n");
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
function buildOverlayBlockerNextActions(options: { diagnostic: OverlayBlockerDiagnostic; sessionName?: string }): AgentBrowserNextAction[] {
|
|
2671
|
+
return [
|
|
2672
|
+
{
|
|
2673
|
+
id: "inspect-overlay-state",
|
|
2674
|
+
params: { args: sessionPrefixArgs(options.sessionName, ["snapshot", "-i"]) },
|
|
2675
|
+
reason: "Refresh interactive refs and inspect whether an overlay, banner, modal, or dialog is blocking the intended click.",
|
|
2676
|
+
safety: "Read-only inspection; use current refs from this snapshot before interacting.",
|
|
2677
|
+
tool: "agent_browser" as const,
|
|
2678
|
+
},
|
|
2679
|
+
...options.diagnostic.candidates.map((candidate, index) => ({
|
|
2680
|
+
id: `try-overlay-blocker-candidate-${index + 1}`,
|
|
2681
|
+
params: { args: sessionPrefixArgs(options.sessionName, candidate.args) },
|
|
2682
|
+
reason: candidate.reason,
|
|
2683
|
+
safety: "Only click this if the candidate is clearly a close/dismiss control for an overlay that blocks the intended workflow.",
|
|
2684
|
+
tool: "agent_browser" as const,
|
|
2685
|
+
})),
|
|
2686
|
+
];
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
function buildVisibleTextProbeScript(selector: string): string {
|
|
2690
|
+
return `(() => {\n const selector = ${JSON.stringify(selector)};\n const isVisible = (element) => {\n const style = window.getComputedStyle(element);\n if (!style || style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse' || Number(style.opacity) === 0) return false;\n return Array.from(element.getClientRects()).some((rect) => rect.width > 0 && rect.height > 0);\n };\n let matches = [];\n try {\n matches = Array.from(document.querySelectorAll(selector));\n } catch (error) {\n return JSON.stringify({ selector, error: error instanceof Error ? error.message : String(error) });\n }\n const visible = matches.filter(isVisible);\n const trim = (value) => typeof value === 'string' ? value.trim().replace(/\\s+/g, ' ').slice(0, 200) : undefined;\n return JSON.stringify({\n selector,\n matchCount: matches.length,\n visibleCount: visible.length,\n firstMatchVisible: matches[0] ? isVisible(matches[0]) : undefined,\n firstTextPreview: trim(matches[0]?.textContent),\n firstVisibleTextPreview: trim(visible[0]?.textContent),\n });\n})()`;
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
function parseSelectorTextVisibilityProbe(data: unknown, selector: string): Omit<SelectorTextVisibilityDiagnostic, "summary"> | undefined {
|
|
2694
|
+
const result = extractStringResultField(data, "result");
|
|
2695
|
+
if (!result) return undefined;
|
|
2696
|
+
let parsed: unknown;
|
|
2697
|
+
try {
|
|
2698
|
+
parsed = JSON.parse(result);
|
|
2699
|
+
} catch {
|
|
2700
|
+
return undefined;
|
|
2701
|
+
}
|
|
2702
|
+
if (!isRecord(parsed) || typeof parsed.error === "string") return undefined;
|
|
2703
|
+
const matchCount = typeof parsed.matchCount === "number" ? parsed.matchCount : undefined;
|
|
2704
|
+
const visibleCount = typeof parsed.visibleCount === "number" ? parsed.visibleCount : undefined;
|
|
2705
|
+
if (matchCount === undefined || visibleCount === undefined) return undefined;
|
|
2706
|
+
return {
|
|
2707
|
+
firstMatchVisible: typeof parsed.firstMatchVisible === "boolean" ? parsed.firstMatchVisible : undefined,
|
|
2708
|
+
firstVisibleTextPreview: typeof parsed.firstVisibleTextPreview === "string" && parsed.firstVisibleTextPreview.length > 0 ? redactSensitiveText(parsed.firstVisibleTextPreview) : undefined,
|
|
2709
|
+
matchCount,
|
|
2710
|
+
selector,
|
|
2711
|
+
visibleCount,
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
function selectorMayExposeSensitiveLiteral(selector: string): boolean {
|
|
2716
|
+
return redactSensitiveText(selector) !== selector || /\[[^\]]*[~|^$*]?=\s*(?:"[^"]*"|'[^']*'|[^\]\s]+)\s*(?:[is]\s*)?\]/.test(selector);
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
async function collectSelectorTextVisibilityDiagnosticForSelector(options: {
|
|
2720
|
+
cwd: string;
|
|
2721
|
+
selector: string | undefined;
|
|
2722
|
+
sessionName?: string;
|
|
2723
|
+
signal?: AbortSignal;
|
|
2724
|
+
}): Promise<SelectorTextVisibilityDiagnostic | undefined> {
|
|
2725
|
+
const { selector } = options;
|
|
2726
|
+
if (!selector || /^@e\d+$/.test(selector) || selectorMayExposeSensitiveLiteral(selector)) return undefined;
|
|
2727
|
+
const probe = await runSessionCommandData({
|
|
2728
|
+
args: ["eval", "--stdin"],
|
|
2729
|
+
cwd: options.cwd,
|
|
2730
|
+
sessionName: options.sessionName,
|
|
2731
|
+
signal: options.signal,
|
|
2732
|
+
stdin: buildVisibleTextProbeScript(selector),
|
|
2733
|
+
});
|
|
2734
|
+
const parsed = parseSelectorTextVisibilityProbe(probe, selector);
|
|
2735
|
+
if (!parsed || parsed.matchCount <= 1 && parsed.firstMatchVisible !== false) return undefined;
|
|
2736
|
+
if (parsed.visibleCount === 0) return undefined;
|
|
2737
|
+
const visibleMatchNoun = `visible match${parsed.visibleCount === 1 ? "" : "es"}`;
|
|
2738
|
+
const visibleMatchVerb = parsed.visibleCount === 1 ? "exists" : "exist";
|
|
2739
|
+
const summary = parsed.firstMatchVisible === false
|
|
2740
|
+
? `Selector ${JSON.stringify(selector)} matched ${parsed.matchCount} elements; the first match is hidden while ${parsed.visibleCount} ${visibleMatchNoun} ${visibleMatchVerb}.`
|
|
2741
|
+
: `Selector ${JSON.stringify(selector)} matched ${parsed.matchCount} elements; get text reads the first upstream match, which may not be the intended visible tab/panel.`;
|
|
2742
|
+
return { ...parsed, summary };
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
function getBatchGetTextSelectors(data: unknown): string[] {
|
|
2746
|
+
if (!Array.isArray(data)) return [];
|
|
2747
|
+
return data.flatMap((item) => {
|
|
2748
|
+
if (!isRecord(item) || item.success === false) return [];
|
|
2749
|
+
const [command, subcommand, selector] = extractBatchResultCommand(item);
|
|
2750
|
+
return command === "get" && subcommand === "text" && selector ? [selector] : [];
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
async function collectSelectorTextVisibilityDiagnostics(options: {
|
|
2755
|
+
commandInfo: CommandInfo;
|
|
2756
|
+
commandTokens: string[];
|
|
2757
|
+
cwd: string;
|
|
2758
|
+
data: unknown;
|
|
2759
|
+
sessionName?: string;
|
|
2760
|
+
signal?: AbortSignal;
|
|
2761
|
+
}): Promise<SelectorTextVisibilityDiagnostic[]> {
|
|
2762
|
+
const selectors = options.commandInfo.command === "get" && options.commandInfo.subcommand === "text"
|
|
2763
|
+
? [options.commandTokens[2]]
|
|
2764
|
+
: options.commandInfo.command === "batch"
|
|
2765
|
+
? getBatchGetTextSelectors(options.data)
|
|
2766
|
+
: [];
|
|
2767
|
+
const diagnostics: SelectorTextVisibilityDiagnostic[] = [];
|
|
2768
|
+
for (const selector of selectors) {
|
|
2769
|
+
const diagnostic = await collectSelectorTextVisibilityDiagnosticForSelector({
|
|
2770
|
+
cwd: options.cwd,
|
|
2771
|
+
selector,
|
|
2772
|
+
sessionName: options.sessionName,
|
|
2773
|
+
signal: options.signal,
|
|
2774
|
+
});
|
|
2775
|
+
if (diagnostic) diagnostics.push(diagnostic);
|
|
2776
|
+
}
|
|
2777
|
+
return diagnostics.sort((left, right) => Number(right.firstMatchVisible === false) - Number(left.firstMatchVisible === false));
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
function formatSelectorTextVisibilityText(diagnostics: SelectorTextVisibilityDiagnostic[]): string | undefined {
|
|
2781
|
+
if (diagnostics.length === 0) return undefined;
|
|
2782
|
+
return diagnostics.flatMap((diagnostic) => {
|
|
2783
|
+
const lines = [`Selector text visibility warning: ${diagnostic.summary}`];
|
|
2784
|
+
if (diagnostic.firstVisibleTextPreview) lines.push(`First visible text preview: ${JSON.stringify(diagnostic.firstVisibleTextPreview)}`);
|
|
2785
|
+
return lines;
|
|
2786
|
+
}).join("\n");
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
function looksLikeFunctionEvalStdin(stdin: string | undefined): boolean {
|
|
2790
|
+
const trimmed = stdin?.trim();
|
|
2791
|
+
if (!trimmed) return false;
|
|
2792
|
+
return /^(?:async\s+)?function\b/.test(trimmed) || /^(?:async\s*)?\([^)]*\)\s*=>/.test(trimmed) || /^(?:async\s+)?[A-Za-z_$][\w$]*\s*=>/.test(trimmed);
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
function isEmptyRecord(value: unknown): boolean {
|
|
2796
|
+
return isRecord(value) && Object.keys(value).length === 0;
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
function getEvalStdinHint(options: { command?: string; data: unknown; stdin?: string }): EvalStdinHint | undefined {
|
|
2800
|
+
if (options.command !== "eval" || !looksLikeFunctionEvalStdin(options.stdin) || !isRecord(options.data)) return undefined;
|
|
2801
|
+
const result = options.data.result;
|
|
2802
|
+
if (!isEmptyRecord(result)) return undefined;
|
|
2803
|
+
return {
|
|
2804
|
+
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.",
|
|
2805
|
+
suggestion: "Pass a plain expression such as `({ title: document.title })`, or invoke the function explicitly, for example `(() => ({ title: document.title }))()`.",
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
function formatEvalStdinHintText(hint: EvalStdinHint | undefined): string | undefined {
|
|
2810
|
+
return hint ? `Eval stdin hint: ${hint.reason} ${hint.suggestion}` : undefined;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
function getArtifactCleanupGuidance(options: { command?: string; manifest?: SessionArtifactManifest; succeeded: boolean }): ArtifactCleanupGuidance | undefined {
|
|
2814
|
+
if (!options.succeeded || options.command !== "close" || !options.manifest || options.manifest.entries.length === 0) return undefined;
|
|
2815
|
+
const explicitArtifactPaths = options.manifest.entries
|
|
2816
|
+
.filter((entry) => entry.storageScope === "explicit-path")
|
|
2817
|
+
.map((entry) => entry.path)
|
|
2818
|
+
.filter((path, index, paths) => paths.indexOf(path) === index)
|
|
2819
|
+
.slice(0, 10);
|
|
2820
|
+
return {
|
|
2821
|
+
explicitArtifactPaths,
|
|
2822
|
+
note: "Closing the browser session does not delete explicit screenshots, downloads, PDFs, traces, HAR files, or recordings; clean those paths with host file tools when no longer needed.",
|
|
2823
|
+
owner: "host-file-tools",
|
|
2824
|
+
summary: formatSessionArtifactRetentionSummary(options.manifest),
|
|
2825
|
+
};
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
function formatArtifactCleanupGuidanceText(guidance: ArtifactCleanupGuidance | undefined): string | undefined {
|
|
2829
|
+
if (!guidance) return undefined;
|
|
2830
|
+
const lines = [
|
|
2831
|
+
"Artifact lifecycle:",
|
|
2832
|
+
`- ${guidance.summary}`,
|
|
2833
|
+
`- ${guidance.note}`,
|
|
2834
|
+
];
|
|
2835
|
+
if (guidance.explicitArtifactPaths.length > 0) {
|
|
2836
|
+
lines.push(`- Explicit artifact paths to review: ${guidance.explicitArtifactPaths.join(", ")}`);
|
|
2837
|
+
}
|
|
2838
|
+
return lines.join("\n");
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
function buildSelectorTextVisibilityNextActions(options: { diagnostics: SelectorTextVisibilityDiagnostic[]; sessionName?: string }): AgentBrowserNextAction[] {
|
|
2842
|
+
return options.diagnostics.map((diagnostic, index) => ({
|
|
2843
|
+
id: index === 0 ? "inspect-visible-text-candidates" : `inspect-visible-text-candidates-${index + 1}`,
|
|
2844
|
+
params: {
|
|
2845
|
+
args: sessionPrefixArgs(options.sessionName, ["eval", "--stdin"]),
|
|
2846
|
+
stdin: buildVisibleTextProbeScript(diagnostic.selector),
|
|
2847
|
+
},
|
|
2848
|
+
reason: "Inspect selector match count and visible text before trusting get text on tabbed or hidden DOM content.",
|
|
2849
|
+
safety: "Read-only DOM inspection; use a more specific visible selector or current @ref before acting on hidden-tab text.",
|
|
2850
|
+
tool: "agent_browser" as const,
|
|
2851
|
+
}));
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
function getTimeoutProgressSteps(compiledJob: CompiledAgentBrowserJob | undefined, command: string | undefined, stdin: string | undefined): Array<{ args: string[]; index: number }> {
|
|
2855
|
+
if (compiledJob) return compiledJob.steps.map((step, index) => ({ args: step.args, index: index + 1 }));
|
|
2856
|
+
if (command !== "batch" || !stdin) return [];
|
|
2857
|
+
try {
|
|
2858
|
+
const parsed = JSON.parse(stdin) as unknown;
|
|
2859
|
+
if (!Array.isArray(parsed)) return [];
|
|
2860
|
+
return parsed.flatMap((step, index) => Array.isArray(step) && step.every((token) => typeof token === "string") ? [{ args: step as string[], index: index + 1 }] : []);
|
|
2861
|
+
} catch {
|
|
2862
|
+
return [];
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
function getLastPositionalToken(args: string[], startIndex = 1): string | undefined {
|
|
2867
|
+
for (let index = args.length - 1; index >= startIndex; index -= 1) {
|
|
2868
|
+
const token = args[index];
|
|
2869
|
+
if (token && !token.startsWith("-")) return token;
|
|
2870
|
+
}
|
|
2871
|
+
return undefined;
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
function getTimeoutStepArtifactPath(args: string[]): string | undefined {
|
|
2875
|
+
const [command] = args;
|
|
2876
|
+
if (command === "screenshot") {
|
|
2877
|
+
const index = getScreenshotPathTokenIndex(args);
|
|
2878
|
+
return index === undefined ? undefined : args[index];
|
|
2879
|
+
}
|
|
2880
|
+
if (command === "pdf") return getLastPositionalToken(args);
|
|
2881
|
+
if (command === "download") return getLastPositionalToken(args, 2);
|
|
2882
|
+
if (command === "wait") {
|
|
2883
|
+
const inlineDownload = args.find((token) => token.startsWith("--download="));
|
|
2884
|
+
if (inlineDownload) return inlineDownload.slice("--download=".length) || undefined;
|
|
2885
|
+
const downloadIndex = args.indexOf("--download");
|
|
2886
|
+
const downloadPath = downloadIndex >= 0 ? args[downloadIndex + 1] : undefined;
|
|
2887
|
+
if (downloadPath && !downloadPath.startsWith("-")) return downloadPath;
|
|
2888
|
+
}
|
|
2889
|
+
return undefined;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
async function collectTimeoutArtifactEvidence(cwd: string, steps: Array<{ args: string[]; index: number }>): Promise<TimeoutArtifactEvidence[]> {
|
|
2893
|
+
const evidence: TimeoutArtifactEvidence[] = [];
|
|
2894
|
+
for (const step of steps) {
|
|
2895
|
+
const path = getTimeoutStepArtifactPath(step.args);
|
|
2896
|
+
if (!path) continue;
|
|
2897
|
+
const absolutePath = isAbsolute(path) ? path : resolve(cwd, path);
|
|
2898
|
+
try {
|
|
2899
|
+
const stats = await stat(absolutePath);
|
|
2900
|
+
evidence.push({ absolutePath, exists: true, path, sizeBytes: stats.size, stepIndex: step.index });
|
|
2901
|
+
} catch {
|
|
2902
|
+
evidence.push({ absolutePath, exists: false, path, stepIndex: step.index });
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
return evidence;
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
function getPlannedCurrentPageUrl(steps: Array<{ args: string[]; index: number }>): string | undefined {
|
|
2909
|
+
for (let index = steps.length - 1; index >= 0; index -= 1) {
|
|
2910
|
+
const args = steps[index]?.args ?? [];
|
|
2911
|
+
if (args[0] === "open" || args[0] === "navigate" || args[0] === "pushstate") {
|
|
2912
|
+
return getLastPositionalToken(args);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
return undefined;
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
async function collectTimeoutPartialProgress(options: {
|
|
2919
|
+
command?: string;
|
|
2920
|
+
compiledJob?: CompiledAgentBrowserJob;
|
|
2921
|
+
cwd: string;
|
|
2922
|
+
sessionName?: string;
|
|
2923
|
+
stdin?: string;
|
|
2924
|
+
}): Promise<TimeoutPartialProgress | undefined> {
|
|
2925
|
+
const steps = getTimeoutProgressSteps(options.compiledJob, options.command, options.stdin);
|
|
2926
|
+
const artifacts = await collectTimeoutArtifactEvidence(options.cwd, steps);
|
|
2927
|
+
const [urlData, titleData] = await Promise.all([
|
|
2928
|
+
runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, sessionName: options.sessionName }),
|
|
2929
|
+
runSessionCommandData({ args: ["get", "title"], cwd: options.cwd, sessionName: options.sessionName }),
|
|
2930
|
+
]);
|
|
2931
|
+
const recoveredUrl = extractStringResultField(urlData, "result") ?? extractStringResultField(urlData, "url");
|
|
2932
|
+
const title = extractStringResultField(titleData, "result") ?? extractStringResultField(titleData, "title");
|
|
2933
|
+
const plannedUrl = recoveredUrl ? undefined : getPlannedCurrentPageUrl(steps);
|
|
2934
|
+
const url = recoveredUrl ?? plannedUrl;
|
|
2935
|
+
if (steps.length === 0 && artifacts.length === 0 && !url && !title) return undefined;
|
|
2936
|
+
const foundArtifacts = artifacts.filter((artifact) => artifact.exists).length;
|
|
2937
|
+
const pageStateSummary = recoveredUrl || title ? " and current page state" : plannedUrl ? " and planned page URL" : "";
|
|
2938
|
+
return {
|
|
2939
|
+
artifacts,
|
|
2940
|
+
currentPage: url || title ? { title, url } : undefined,
|
|
2941
|
+
steps: steps.length > 0 ? steps : undefined,
|
|
2942
|
+
summary: `Timed out before upstream returned final results; recovered ${foundArtifacts}/${artifacts.length} declared artifact path${artifacts.length === 1 ? "" : "s"}${pageStateSummary}.`,
|
|
2943
|
+
};
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
function redactSensitivePathSegmentsForDiagnostic(path: string): string {
|
|
2947
|
+
return path.split(/([/\\]+)/).map((segment) => {
|
|
2948
|
+
if (segment === "/" || segment === "\\" || /^[/\\]+$/.test(segment)) return segment;
|
|
2949
|
+
return redactSensitiveText(segment) !== segment || /(?:secret|token|password|passwd|credential|auth|api[-_]?key|bearer)/i.test(segment) ? "[REDACTED]" : segment;
|
|
2950
|
+
}).join("");
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
function sanitizeCurrentPageUrlForTimeoutDiagnostic(url: string): string {
|
|
2954
|
+
try {
|
|
2955
|
+
const parsedUrl = new URL(url);
|
|
2956
|
+
parsedUrl.pathname = parsedUrl.pathname.split("/").map((segment) => redactSensitivePathSegmentsForDiagnostic(segment)).join("/");
|
|
2957
|
+
for (const [key, value] of parsedUrl.searchParams.entries()) {
|
|
2958
|
+
if (redactSensitiveText(key) !== key || redactSensitiveText(value) !== value || /(?:secret|token|password|passwd|credential|auth|api[-_]?key|bearer)/i.test(`${key} ${value}`)) {
|
|
2959
|
+
parsedUrl.searchParams.set(key, "[REDACTED]");
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
if (parsedUrl.hash) {
|
|
2963
|
+
parsedUrl.hash = redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(parsedUrl.hash));
|
|
2964
|
+
}
|
|
2965
|
+
return redactSensitiveText(parsedUrl.toString());
|
|
2966
|
+
} catch {
|
|
2967
|
+
return redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(url));
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
function formatTimeoutPartialProgressText(progress: TimeoutPartialProgress): string {
|
|
2972
|
+
const lines = [`Timeout partial progress: ${progress.summary}`];
|
|
2973
|
+
const currentPageTitle = progress.currentPage?.title ? redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(progress.currentPage.title)) : undefined;
|
|
2974
|
+
const currentPageUrl = progress.currentPage?.url ? sanitizeCurrentPageUrlForTimeoutDiagnostic(progress.currentPage.url) : undefined;
|
|
2975
|
+
if (currentPageTitle || currentPageUrl) {
|
|
2976
|
+
lines.push(`Current page: ${[currentPageTitle, currentPageUrl].filter(Boolean).join(" — ")}`);
|
|
2977
|
+
}
|
|
2978
|
+
if (progress.steps && progress.steps.length > 0) {
|
|
2979
|
+
const shownSteps = progress.steps.slice(0, 6);
|
|
2980
|
+
lines.push("Planned steps:");
|
|
2981
|
+
for (const step of shownSteps) {
|
|
2982
|
+
const command = redactSensitivePathSegmentsForDiagnostic(redactInvocationArgs(step.args).join(" "));
|
|
2983
|
+
lines.push(`- Step ${step.index}: ${command}`);
|
|
2984
|
+
}
|
|
2985
|
+
if (progress.steps.length > shownSteps.length) {
|
|
2986
|
+
lines.push(`- ... ${progress.steps.length - shownSteps.length} more step${progress.steps.length - shownSteps.length === 1 ? "" : "s"} omitted`);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
for (const artifact of progress.artifacts) {
|
|
2990
|
+
const path = redactSensitivePathSegmentsForDiagnostic(artifact.path);
|
|
2991
|
+
lines.push(`Artifact from step ${artifact.stepIndex}: ${path} (${artifact.exists ? `exists${typeof artifact.sizeBytes === "number" ? `, ${artifact.sizeBytes} bytes` : ""}` : "missing"})`);
|
|
2992
|
+
}
|
|
2993
|
+
return lines.join("\n");
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
async function collectOverlayBlockerDiagnostic(options: {
|
|
2997
|
+
command?: string;
|
|
2998
|
+
cwd: string;
|
|
2999
|
+
data: unknown;
|
|
3000
|
+
navigationSummary?: NavigationSummary;
|
|
3001
|
+
priorTarget?: SessionTabTarget;
|
|
3002
|
+
sessionName?: string;
|
|
3003
|
+
signal?: AbortSignal;
|
|
3004
|
+
}): Promise<OverlayBlockerDiagnostic | undefined> {
|
|
3005
|
+
if (options.command !== "click" || !isRecord(options.data) || typeof options.data.clicked !== "string") return undefined;
|
|
3006
|
+
const priorUrl = normalizeComparableUrl(options.priorTarget?.url);
|
|
3007
|
+
const currentUrl = normalizeComparableUrl(options.navigationSummary?.url);
|
|
3008
|
+
if (!priorUrl || !currentUrl || priorUrl !== currentUrl) return undefined;
|
|
3009
|
+
const snapshotData = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, sessionName: options.sessionName, signal: options.signal });
|
|
3010
|
+
const candidates = getOverlayBlockerCandidates(snapshotData);
|
|
3011
|
+
const snapshot = extractRefSnapshotFromData(snapshotData);
|
|
3012
|
+
if (candidates.length === 0 || !snapshot) return undefined;
|
|
3013
|
+
return {
|
|
3014
|
+
candidates,
|
|
3015
|
+
snapshot,
|
|
3016
|
+
summary: `Click completed but the page stayed on ${currentUrl}; a fresh snapshot contains likely overlay close/dismiss controls.`,
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
|
|
2252
3020
|
async function collectOpenResultTabCorrection(options: {
|
|
2253
3021
|
cwd: string;
|
|
2254
3022
|
sessionName?: string;
|
|
@@ -2314,6 +3082,68 @@ function buildSessionDetailFields(sessionName: string | undefined, usedImplicitS
|
|
|
2314
3082
|
return sessionName ? { sessionName, usedImplicitSession } : {};
|
|
2315
3083
|
}
|
|
2316
3084
|
|
|
3085
|
+
function buildManagedSessionOutcome(options: {
|
|
3086
|
+
activeAfter: boolean;
|
|
3087
|
+
activeBefore: boolean;
|
|
3088
|
+
attemptedSessionName?: string;
|
|
3089
|
+
command?: string;
|
|
3090
|
+
currentSessionName: string;
|
|
3091
|
+
previousSessionName: string;
|
|
3092
|
+
replacedSessionName?: string;
|
|
3093
|
+
sessionMode: "auto" | "fresh";
|
|
3094
|
+
succeeded: boolean;
|
|
3095
|
+
}): ManagedSessionOutcome | undefined {
|
|
3096
|
+
const { activeAfter, activeBefore, attemptedSessionName, command, currentSessionName, previousSessionName, replacedSessionName, sessionMode, succeeded } = options;
|
|
3097
|
+
if (!attemptedSessionName) return undefined;
|
|
3098
|
+
let status: ManagedSessionOutcome["status"];
|
|
3099
|
+
let summary: string;
|
|
3100
|
+
if (command === "close") {
|
|
3101
|
+
status = succeeded ? "closed" : activeBefore ? "preserved" : "abandoned";
|
|
3102
|
+
summary = succeeded
|
|
3103
|
+
? `Managed session ${attemptedSessionName} was closed.`
|
|
3104
|
+
: activeBefore
|
|
3105
|
+
? `Managed session close failed; previous managed session ${previousSessionName} remains current.`
|
|
3106
|
+
: `Managed session close failed; no managed session is active.`;
|
|
3107
|
+
} else if (succeeded) {
|
|
3108
|
+
if (replacedSessionName) {
|
|
3109
|
+
status = "replaced";
|
|
3110
|
+
summary = `Managed session ${replacedSessionName} was replaced by ${currentSessionName}.`;
|
|
3111
|
+
} else if (!activeBefore && activeAfter) {
|
|
3112
|
+
status = "created";
|
|
3113
|
+
summary = `Managed session ${currentSessionName} is now current.`;
|
|
3114
|
+
} else {
|
|
3115
|
+
status = "unchanged";
|
|
3116
|
+
summary = `Managed session ${currentSessionName} remains current.`;
|
|
3117
|
+
}
|
|
3118
|
+
} else if (activeBefore) {
|
|
3119
|
+
status = "preserved";
|
|
3120
|
+
summary = sessionMode === "fresh" && attemptedSessionName !== previousSessionName
|
|
3121
|
+
? `Fresh managed session ${attemptedSessionName} failed before becoming current; previous managed session ${previousSessionName} was preserved.`
|
|
3122
|
+
: `Managed session call failed; previous managed session ${previousSessionName} was preserved.`;
|
|
3123
|
+
} else {
|
|
3124
|
+
status = "abandoned";
|
|
3125
|
+
summary = sessionMode === "fresh"
|
|
3126
|
+
? `Fresh managed session ${attemptedSessionName} failed before becoming current; no previous managed session was active, so no managed session is current.`
|
|
3127
|
+
: `Managed session call failed before any managed session became current.`;
|
|
3128
|
+
}
|
|
3129
|
+
return {
|
|
3130
|
+
activeAfter,
|
|
3131
|
+
activeBefore,
|
|
3132
|
+
attemptedSessionName,
|
|
3133
|
+
currentSessionName,
|
|
3134
|
+
previousSessionName,
|
|
3135
|
+
replacedSessionName,
|
|
3136
|
+
sessionMode,
|
|
3137
|
+
status,
|
|
3138
|
+
succeeded,
|
|
3139
|
+
summary,
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
function formatManagedSessionOutcomeText(outcome: ManagedSessionOutcome | undefined): string | undefined {
|
|
3144
|
+
return outcome && !outcome.succeeded && outcome.sessionMode === "fresh" ? `Managed session outcome: ${outcome.summary}` : undefined;
|
|
3145
|
+
}
|
|
3146
|
+
|
|
2317
3147
|
function getPersistentSessionArtifactStore(ctx: {
|
|
2318
3148
|
sessionManager: {
|
|
2319
3149
|
getSessionDir?: () => string;
|
|
@@ -2465,6 +3295,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2465
3295
|
let managedSessionCwd = process.cwd();
|
|
2466
3296
|
let freshSessionOrdinal = 0;
|
|
2467
3297
|
let sessionTabTargets = new Map<string, OrderedSessionTabTarget>();
|
|
3298
|
+
let sessionRefSnapshots = new Map<string, OrderedSessionRefSnapshot>();
|
|
2468
3299
|
let sessionTabTargetUpdateOrder = 0;
|
|
2469
3300
|
let traceOwners = new Map<string, TraceOwner>();
|
|
2470
3301
|
let artifactManifest: SessionArtifactManifest | undefined;
|
|
@@ -2478,7 +3309,8 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2478
3309
|
managedSessionCwd = ctx.cwd;
|
|
2479
3310
|
freshSessionOrdinal = restoredState.freshSessionOrdinal;
|
|
2480
3311
|
sessionTabTargets = restoreSessionTabTargetsFromBranch(ctx.sessionManager.getBranch());
|
|
2481
|
-
|
|
3312
|
+
sessionRefSnapshots = restoreSessionRefSnapshotsFromBranch(ctx.sessionManager.getBranch());
|
|
3313
|
+
sessionTabTargetUpdateOrder = Math.max(getLatestSessionTabTargetOrder(sessionTabTargets), getLatestSessionTabTargetOrder(sessionRefSnapshots));
|
|
2482
3314
|
artifactManifest = restoreArtifactManifestFromBranch(ctx.sessionManager.getBranch());
|
|
2483
3315
|
});
|
|
2484
3316
|
|
|
@@ -2495,6 +3327,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2495
3327
|
}
|
|
2496
3328
|
managedSessionActive = false;
|
|
2497
3329
|
sessionTabTargets = new Map<string, OrderedSessionTabTarget>();
|
|
3330
|
+
sessionRefSnapshots = new Map<string, OrderedSessionRefSnapshot>();
|
|
2498
3331
|
sessionTabTargetUpdateOrder = 0;
|
|
2499
3332
|
traceOwners = new Map<string, TraceOwner>();
|
|
2500
3333
|
artifactManifest = undefined;
|
|
@@ -2726,6 +3559,31 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2726
3559
|
|
|
2727
3560
|
const priorSessionTabTargetState = executionPlan.sessionName ? sessionTabTargets.get(executionPlan.sessionName) : undefined;
|
|
2728
3561
|
const priorSessionTabTarget = priorSessionTabTargetState?.target;
|
|
3562
|
+
const priorRefSnapshotState = executionPlan.sessionName ? sessionRefSnapshots.get(executionPlan.sessionName) : undefined;
|
|
3563
|
+
const staleRefPreflight = buildStaleRefPreflight({
|
|
3564
|
+
commandTokens,
|
|
3565
|
+
currentTarget: priorSessionTabTarget,
|
|
3566
|
+
refSnapshot: priorRefSnapshotState,
|
|
3567
|
+
stdin: toolStdin,
|
|
3568
|
+
});
|
|
3569
|
+
if (staleRefPreflight) {
|
|
3570
|
+
return {
|
|
3571
|
+
content: [{ type: "text", text: staleRefPreflight.message }],
|
|
3572
|
+
details: {
|
|
3573
|
+
args: redactedArgs,
|
|
3574
|
+
command: executionPlan.commandInfo.command,
|
|
3575
|
+
compatibilityWorkaround,
|
|
3576
|
+
effectiveArgs: redactedEffectiveArgs,
|
|
3577
|
+
nextActions: sessionAwareStaleRefNextActions(executionPlan.sessionName),
|
|
3578
|
+
refIds: staleRefPreflight.refIds,
|
|
3579
|
+
refSnapshot: staleRefPreflight.snapshot,
|
|
3580
|
+
sessionMode,
|
|
3581
|
+
...buildAgentBrowserResultCategoryDetails({ args: redactedEffectiveArgs, command: executionPlan.commandInfo.command, errorText: staleRefPreflight.message, failureCategory: "stale-ref", succeeded: false }),
|
|
3582
|
+
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
3583
|
+
},
|
|
3584
|
+
isError: true,
|
|
3585
|
+
};
|
|
3586
|
+
}
|
|
2729
3587
|
let pinnedBatchUnwrapMode: PinnedBatchUnwrapMode | undefined;
|
|
2730
3588
|
let includePinnedNavigationSummary = false;
|
|
2731
3589
|
let sessionTabCorrection: OpenResultTabCorrection | undefined;
|
|
@@ -2832,12 +3690,24 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2832
3690
|
|
|
2833
3691
|
if (processResult.spawnError?.message.includes("ENOENT")) {
|
|
2834
3692
|
const errorText = buildMissingBinaryMessage();
|
|
3693
|
+
const managedSessionOutcome = buildManagedSessionOutcome({
|
|
3694
|
+
activeAfter: managedSessionActive,
|
|
3695
|
+
activeBefore: managedSessionActive,
|
|
3696
|
+
attemptedSessionName: executionPlan.managedSessionName,
|
|
3697
|
+
command: executionPlan.commandInfo.command,
|
|
3698
|
+
currentSessionName: managedSessionName,
|
|
3699
|
+
previousSessionName: managedSessionName,
|
|
3700
|
+
sessionMode,
|
|
3701
|
+
succeeded: false,
|
|
3702
|
+
});
|
|
3703
|
+
const managedSessionOutcomeText = formatManagedSessionOutcomeText(managedSessionOutcome);
|
|
2835
3704
|
return {
|
|
2836
|
-
content: [{ type: "text", text: errorText }],
|
|
3705
|
+
content: [{ type: "text", text: managedSessionOutcomeText ? `${errorText}\n\n${managedSessionOutcomeText}` : errorText }],
|
|
2837
3706
|
details: {
|
|
2838
3707
|
args: redactedArgs,
|
|
2839
3708
|
compatibilityWorkaround,
|
|
2840
3709
|
effectiveArgs: redactedProcessArgs,
|
|
3710
|
+
managedSessionOutcome,
|
|
2841
3711
|
sessionMode,
|
|
2842
3712
|
sessionTabCorrection,
|
|
2843
3713
|
...buildAgentBrowserResultCategoryDetails({ args: redactedProcessArgs, command: executionPlan.commandInfo.command, errorText, failureCategory: "missing-binary", spawnError: processResult.spawnError.message, succeeded: false }),
|
|
@@ -2918,6 +3788,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
2918
3788
|
data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary),
|
|
2919
3789
|
};
|
|
2920
3790
|
}
|
|
3791
|
+
let overlayBlockerDiagnostic: OverlayBlockerDiagnostic | undefined;
|
|
2921
3792
|
|
|
2922
3793
|
let openResultTabCorrection: OpenResultTabCorrection | undefined;
|
|
2923
3794
|
if (
|
|
@@ -3021,33 +3892,91 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3021
3892
|
}
|
|
3022
3893
|
}
|
|
3023
3894
|
}
|
|
3895
|
+
let selectorTextVisibilityDiagnostics: SelectorTextVisibilityDiagnostic[] = [];
|
|
3896
|
+
const timeoutPartialProgress = processResult.timedOut ? await collectTimeoutPartialProgress({
|
|
3897
|
+
command: executionPlan.commandInfo.command,
|
|
3898
|
+
compiledJob,
|
|
3899
|
+
cwd: ctx.cwd,
|
|
3900
|
+
sessionName: executionPlan.sessionName,
|
|
3901
|
+
stdin: toolStdin,
|
|
3902
|
+
}) : undefined;
|
|
3903
|
+
if (succeeded && !sessionTabCorrection && !aboutBlankSessionMismatch) {
|
|
3904
|
+
overlayBlockerDiagnostic = await collectOverlayBlockerDiagnostic({
|
|
3905
|
+
command: executionPlan.commandInfo.command,
|
|
3906
|
+
cwd: ctx.cwd,
|
|
3907
|
+
data: presentationEnvelope?.data,
|
|
3908
|
+
navigationSummary,
|
|
3909
|
+
priorTarget: priorSessionTabTarget,
|
|
3910
|
+
sessionName: executionPlan.sessionName,
|
|
3911
|
+
signal,
|
|
3912
|
+
});
|
|
3913
|
+
}
|
|
3914
|
+
if (succeeded) {
|
|
3915
|
+
selectorTextVisibilityDiagnostics = await collectSelectorTextVisibilityDiagnostics({
|
|
3916
|
+
commandInfo: executionPlan.commandInfo,
|
|
3917
|
+
commandTokens,
|
|
3918
|
+
cwd: ctx.cwd,
|
|
3919
|
+
data: presentationEnvelope?.data,
|
|
3920
|
+
sessionName: executionPlan.sessionName,
|
|
3921
|
+
signal,
|
|
3922
|
+
});
|
|
3923
|
+
}
|
|
3924
|
+
let currentRefSnapshot: SessionRefSnapshot | undefined;
|
|
3024
3925
|
if (executionPlan.sessionName) {
|
|
3025
3926
|
const activeSessionTabTargetState = sessionTabTargets.get(executionPlan.sessionName);
|
|
3026
3927
|
if (shouldApplySessionTabTargetUpdate({ current: activeSessionTabTargetState, updateOrder: tabTargetUpdateOrder })) {
|
|
3027
3928
|
if (executionPlan.commandInfo.command === "close" && succeeded) {
|
|
3028
3929
|
sessionTabTargets.delete(executionPlan.sessionName);
|
|
3930
|
+
sessionRefSnapshots.delete(executionPlan.sessionName);
|
|
3029
3931
|
} else if (currentSessionTabTarget) {
|
|
3030
3932
|
sessionTabTargets.set(executionPlan.sessionName, { order: tabTargetUpdateOrder, target: currentSessionTabTarget });
|
|
3031
3933
|
}
|
|
3032
3934
|
}
|
|
3935
|
+
const refSnapshot = succeeded
|
|
3936
|
+
? executionPlan.commandInfo.command === "snapshot"
|
|
3937
|
+
? extractRefSnapshotFromData(presentationEnvelope?.data)
|
|
3938
|
+
: executionPlan.commandInfo.command === "batch"
|
|
3939
|
+
? extractRefSnapshotFromBatchResults(presentationEnvelope?.data)
|
|
3940
|
+
: overlayBlockerDiagnostic?.snapshot
|
|
3941
|
+
: undefined;
|
|
3942
|
+
if (refSnapshot && shouldApplySessionTabTargetUpdate({ current: sessionRefSnapshots.get(executionPlan.sessionName), updateOrder: tabTargetUpdateOrder })) {
|
|
3943
|
+
currentRefSnapshot = { ...refSnapshot, target: refSnapshot.target ?? currentSessionTabTarget };
|
|
3944
|
+
sessionRefSnapshots.set(executionPlan.sessionName, { ...currentRefSnapshot, order: tabTargetUpdateOrder });
|
|
3945
|
+
} else {
|
|
3946
|
+
currentRefSnapshot = sessionRefSnapshots.get(executionPlan.sessionName);
|
|
3947
|
+
}
|
|
3033
3948
|
}
|
|
3034
3949
|
|
|
3950
|
+
const priorManagedSessionActive = managedSessionActive;
|
|
3035
3951
|
const priorManagedSessionCwd = managedSessionCwd;
|
|
3952
|
+
const priorManagedSessionName = managedSessionName;
|
|
3036
3953
|
const managedSessionState = resolveManagedSessionState({
|
|
3037
3954
|
command: executionPlan.commandInfo.command,
|
|
3038
3955
|
managedSessionName: executionPlan.managedSessionName,
|
|
3039
|
-
priorActive:
|
|
3040
|
-
priorSessionName:
|
|
3956
|
+
priorActive: priorManagedSessionActive,
|
|
3957
|
+
priorSessionName: priorManagedSessionName,
|
|
3041
3958
|
succeeded,
|
|
3042
3959
|
});
|
|
3043
3960
|
const replacedManagedSessionName = managedSessionState.replacedSessionName;
|
|
3044
3961
|
managedSessionActive = managedSessionState.active;
|
|
3045
3962
|
managedSessionName = managedSessionState.sessionName;
|
|
3963
|
+
let managedSessionOutcome = buildManagedSessionOutcome({
|
|
3964
|
+
activeAfter: managedSessionActive,
|
|
3965
|
+
activeBefore: priorManagedSessionActive,
|
|
3966
|
+
attemptedSessionName: executionPlan.managedSessionName,
|
|
3967
|
+
command: executionPlan.commandInfo.command,
|
|
3968
|
+
currentSessionName: managedSessionName,
|
|
3969
|
+
previousSessionName: priorManagedSessionName,
|
|
3970
|
+
replacedSessionName: replacedManagedSessionName,
|
|
3971
|
+
sessionMode,
|
|
3972
|
+
succeeded,
|
|
3973
|
+
});
|
|
3046
3974
|
if (executionPlan.managedSessionName && succeeded) {
|
|
3047
3975
|
managedSessionCwd = ctx.cwd;
|
|
3048
3976
|
}
|
|
3049
3977
|
if (replacedManagedSessionName) {
|
|
3050
3978
|
sessionTabTargets.delete(replacedManagedSessionName);
|
|
3979
|
+
sessionRefSnapshots.delete(replacedManagedSessionName);
|
|
3051
3980
|
await closeManagedSession({
|
|
3052
3981
|
cwd: priorManagedSessionCwd,
|
|
3053
3982
|
sessionName: replacedManagedSessionName,
|
|
@@ -3132,9 +4061,11 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3132
4061
|
} else if (sourceLookup) {
|
|
3133
4062
|
presentation.content.unshift({ type: "text", text: sourceLookup.summary });
|
|
3134
4063
|
}
|
|
3135
|
-
if (qaPreset && !qaPreset.passed) {
|
|
3136
|
-
|
|
3137
|
-
|
|
4064
|
+
if (qaPreset && (!qaPreset.passed || qaPreset.warnings.length > 0)) {
|
|
4065
|
+
if (!qaPreset.passed) {
|
|
4066
|
+
succeeded = false;
|
|
4067
|
+
presentation.failureCategory = "qa-failure";
|
|
4068
|
+
}
|
|
3138
4069
|
presentation.summary = qaPreset.summary;
|
|
3139
4070
|
if (presentation.content[0]?.type === "text") {
|
|
3140
4071
|
presentation.content[0] = { ...presentation.content[0], text: `${qaPreset.summary}\n\n${presentation.content[0].text}` };
|
|
@@ -3142,6 +4073,20 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3142
4073
|
presentation.content.unshift({ type: "text", text: qaPreset.summary });
|
|
3143
4074
|
}
|
|
3144
4075
|
}
|
|
4076
|
+
if (managedSessionOutcome && managedSessionOutcome.succeeded !== succeeded) {
|
|
4077
|
+
managedSessionOutcome = { ...managedSessionOutcome, succeeded };
|
|
4078
|
+
}
|
|
4079
|
+
const evalStdinHint = getEvalStdinHint({
|
|
4080
|
+
command: executionPlan.commandInfo.command,
|
|
4081
|
+
data: presentationEnvelope?.data,
|
|
4082
|
+
stdin: toolStdin,
|
|
4083
|
+
});
|
|
4084
|
+
const resultArtifactManifest = presentation.artifactManifest ?? artifactManifest;
|
|
4085
|
+
const artifactCleanup = getArtifactCleanupGuidance({
|
|
4086
|
+
command: executionPlan.commandInfo.command,
|
|
4087
|
+
manifest: resultArtifactManifest,
|
|
4088
|
+
succeeded,
|
|
4089
|
+
});
|
|
3145
4090
|
const warningText = aboutBlankSessionMismatch ? buildAboutBlankWarning(aboutBlankSessionMismatch) : undefined;
|
|
3146
4091
|
const contentWithSessionWarnings = userRequestedJson && !plainTextInspection
|
|
3147
4092
|
? buildJsonVisibleContent({
|
|
@@ -3187,6 +4132,21 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3187
4132
|
validationError: undefined,
|
|
3188
4133
|
});
|
|
3189
4134
|
let nextActions = presentation.nextActions ? [...presentation.nextActions] : undefined;
|
|
4135
|
+
if (categoryDetails.failureCategory === "stale-ref") {
|
|
4136
|
+
nextActions = sessionAwareStaleRefNextActions(executionPlan.sessionName);
|
|
4137
|
+
}
|
|
4138
|
+
if (categoryDetails.failureCategory === "selector-not-found" && redactedCompiledSemanticAction) {
|
|
4139
|
+
const candidateActions = buildSemanticActionCandidateActions(redactedCompiledSemanticAction);
|
|
4140
|
+
if (candidateActions.length > 0) {
|
|
4141
|
+
(nextActions ??= []).push(...candidateActions);
|
|
4142
|
+
}
|
|
4143
|
+
}
|
|
4144
|
+
if (overlayBlockerDiagnostic) {
|
|
4145
|
+
(nextActions ??= []).push(...buildOverlayBlockerNextActions({ diagnostic: overlayBlockerDiagnostic, sessionName: executionPlan.sessionName }));
|
|
4146
|
+
}
|
|
4147
|
+
if (selectorTextVisibilityDiagnostics.length > 0) {
|
|
4148
|
+
(nextActions ??= []).push(...buildSelectorTextVisibilityNextActions({ diagnostics: selectorTextVisibilityDiagnostics, sessionName: executionPlan.sessionName }));
|
|
4149
|
+
}
|
|
3190
4150
|
if (categoryDetails.failureCategory === "stale-ref" && redactedCompiledSemanticAction) {
|
|
3191
4151
|
(nextActions ??= []).push({
|
|
3192
4152
|
id: "retry-semantic-action-after-stale-ref",
|
|
@@ -3202,8 +4162,9 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3202
4162
|
compiledQaPreset: redactedCompiledQaPreset,
|
|
3203
4163
|
compiledSourceLookup: redactedCompiledSourceLookup,
|
|
3204
4164
|
compiledNetworkSourceLookup: redactedCompiledNetworkSourceLookup,
|
|
3205
|
-
artifactManifest:
|
|
3206
|
-
artifactRetentionSummary: presentation.artifactRetentionSummary,
|
|
4165
|
+
artifactManifest: resultArtifactManifest,
|
|
4166
|
+
artifactRetentionSummary: presentation.artifactRetentionSummary ?? (resultArtifactManifest ? formatSessionArtifactRetentionSummary(resultArtifactManifest) : undefined),
|
|
4167
|
+
artifactCleanup,
|
|
3207
4168
|
artifactVerification: presentation.artifactVerification,
|
|
3208
4169
|
artifacts: presentation.artifacts,
|
|
3209
4170
|
batchFailure: presentation.batchFailure,
|
|
@@ -3224,11 +4185,17 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3224
4185
|
fullOutputPath: parseFailureOutput.fullOutputPath ?? presentation.fullOutputPath,
|
|
3225
4186
|
fullOutputPaths: presentation.fullOutputPaths,
|
|
3226
4187
|
fullOutputUnavailable: parseFailureOutput.fullOutputUnavailable,
|
|
4188
|
+
managedSessionOutcome,
|
|
3227
4189
|
imagePath: presentation.imagePath,
|
|
3228
4190
|
imagePaths: presentation.imagePaths,
|
|
3229
4191
|
nextActions,
|
|
3230
4192
|
pageChangeSummary: presentation.pageChangeSummary,
|
|
4193
|
+
overlayBlockers: overlayBlockerDiagnostic,
|
|
3231
4194
|
qaPreset,
|
|
4195
|
+
selectorTextVisibility: selectorTextVisibilityDiagnostics[0],
|
|
4196
|
+
selectorTextVisibilityAll: selectorTextVisibilityDiagnostics.length > 1 ? selectorTextVisibilityDiagnostics : undefined,
|
|
4197
|
+
evalStdinHint,
|
|
4198
|
+
timeoutPartialProgress,
|
|
3232
4199
|
parseError: plainTextInspection ? undefined : parseError,
|
|
3233
4200
|
savedFile: presentation.savedFile,
|
|
3234
4201
|
savedFilePath: presentation.savedFilePath,
|
|
@@ -3237,6 +4204,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3237
4204
|
sessionMode,
|
|
3238
4205
|
sessionTabCorrection,
|
|
3239
4206
|
sessionTabTarget: currentSessionTabTarget,
|
|
4207
|
+
refSnapshot: currentRefSnapshot,
|
|
3240
4208
|
...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
|
|
3241
4209
|
sessionRecoveryHint: redactedRecoveryHint,
|
|
3242
4210
|
startupScopedFlags: executionPlan.startupScopedFlags,
|
|
@@ -3247,8 +4215,24 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
|
|
|
3247
4215
|
timeoutMs: processResult.timeoutMs,
|
|
3248
4216
|
};
|
|
3249
4217
|
|
|
4218
|
+
const semanticActionCandidateText = nextActions ? formatSemanticActionCandidateText(nextActions) : undefined;
|
|
4219
|
+
const overlayBlockerText = overlayBlockerDiagnostic ? formatOverlayBlockerText(overlayBlockerDiagnostic) : undefined;
|
|
4220
|
+
const selectorTextVisibilityText = formatSelectorTextVisibilityText(selectorTextVisibilityDiagnostics);
|
|
4221
|
+
const evalStdinHintText = formatEvalStdinHintText(evalStdinHint);
|
|
4222
|
+
const artifactCleanupText = formatArtifactCleanupGuidanceText(artifactCleanup);
|
|
4223
|
+
const timeoutPartialProgressText = timeoutPartialProgress ? formatTimeoutPartialProgressText(timeoutPartialProgress) : undefined;
|
|
4224
|
+
const managedSessionOutcomeText = formatManagedSessionOutcomeText(managedSessionOutcome);
|
|
4225
|
+
const rawAppendedDiagnosticText = [semanticActionCandidateText, overlayBlockerText, selectorTextVisibilityText, evalStdinHintText, artifactCleanupText, timeoutPartialProgressText, managedSessionOutcomeText].filter((item): item is string => item !== undefined).join("\n\n");
|
|
4226
|
+
const appendedDiagnosticText = redactSensitiveText(redactExactSensitiveText(rawAppendedDiagnosticText, exactSensitiveValues));
|
|
4227
|
+
const shouldAppendDiagnosticText = appendedDiagnosticText.length > 0 && (!userRequestedJson || plainTextInspection);
|
|
4228
|
+
const content = shouldAppendDiagnosticText && redactedContent[0]?.type === "text"
|
|
4229
|
+
? [
|
|
4230
|
+
{ ...redactedContent[0], text: `${redactedContent[0].text}\n\n${appendedDiagnosticText}` },
|
|
4231
|
+
...redactedContent.slice(1),
|
|
4232
|
+
]
|
|
4233
|
+
: redactedContent;
|
|
3250
4234
|
const result = {
|
|
3251
|
-
content
|
|
4235
|
+
content,
|
|
3252
4236
|
details: redactToolDetails(details, exactSensitiveValues),
|
|
3253
4237
|
isError: !succeeded,
|
|
3254
4238
|
};
|