pi-agent-browser-native 0.3.0 → 0.6.5

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.
Files changed (89) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +130 -54
  3. package/dist/extensions/agent-browser/index.js +781 -169
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +50 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +5 -8
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +53 -12
  9. package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
  10. package/dist/extensions/agent-browser/lib/config.js +1 -1
  11. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -13
  12. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  13. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -24
  14. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  15. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  16. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +26 -4
  17. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +6 -139
  18. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -116
  19. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  20. package/dist/extensions/agent-browser/lib/managed-session-storage.js +54 -25
  21. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +54 -48
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +71 -5
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -7
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
  30. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +152 -64
  31. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +244 -102
  32. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +63 -37
  33. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +20 -21
  34. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  35. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
  36. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  37. package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
  38. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  39. package/dist/extensions/agent-browser/lib/playbook.js +29 -25
  40. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  41. package/dist/extensions/agent-browser/lib/process-identity.js +5 -12
  42. package/dist/extensions/agent-browser/lib/process.js +130 -104
  43. package/dist/extensions/agent-browser/lib/recording-reservations.js +116 -0
  44. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -6
  45. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  46. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  47. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  48. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  49. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +86 -18
  50. package/dist/extensions/agent-browser/lib/results/presentation/common.js +38 -2
  51. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +18 -17
  52. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  53. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +38 -20
  54. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +60 -15
  55. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  56. package/dist/extensions/agent-browser/lib/results/presentation.js +36 -6
  57. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +3 -1
  58. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
  59. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +54 -11
  60. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  61. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
  62. package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
  63. package/dist/extensions/agent-browser/lib/runtime.js +186 -108
  64. package/dist/extensions/agent-browser/lib/session-page-state.js +71 -10
  65. package/dist/extensions/agent-browser/lib/temp.js +1 -2
  66. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  67. package/dist/extensions/agent-browser/lib/web-search.js +108 -24
  68. package/dist/extensions/agent-browser/script-worker.js +169 -0
  69. package/dist/scripts/agent-browser-target.mjs +21 -0
  70. package/docs/ARCHITECTURE.md +57 -34
  71. package/docs/COMMAND_REFERENCE.md +255 -68
  72. package/docs/ELECTRON.md +2 -2
  73. package/docs/RELEASE.md +12 -10
  74. package/docs/REQUIREMENTS.md +11 -8
  75. package/docs/SUPPORT_MATRIX.md +36 -24
  76. package/docs/TOOL_CONTRACT.md +169 -95
  77. package/package.json +3 -1
  78. package/platform-smoke.config.mjs +2 -2
  79. package/scripts/agent-browser-capability-baseline.mjs +87 -9
  80. package/scripts/agent-browser-target.mjs +21 -0
  81. package/scripts/build.mjs +41 -0
  82. package/scripts/config.mjs +1 -0
  83. package/scripts/doctor.mjs +16 -9
  84. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  85. package/scripts/platform-smoke/targets.mjs +12 -6
  86. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
  87. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -583
  88. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
  89. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
@@ -1,24 +1,54 @@
1
1
  import { isRecord } from "../../parsing.js";
2
2
  import { detectConfirmationRequired } from "../confirmation.js";
3
3
  import { formatRawSnapshotText, formatSnapshotSummary } from "../snapshot.js";
4
- import { redactModelFacingText, stringifyModelFacing } from "./common.js";
4
+ import { getPageSummary, omitUpstreamLifecycle, redactModelFacingText, stringifyModelFacing } from "./common.js";
5
5
  import { formatDiagnosticSummary, formatDiagnosticText, formatProfilesText, getStreamSummary, getTabSummary } from "./diagnostics.js";
6
6
  import { getScreenshotSummary } from "./artifacts.js";
7
7
  import { formatSkillsText } from "./skills.js";
8
8
  import { formatExtractionSummary, formatExtractionText, formatNavigationActionResult, formatNavigationSummary, getNavigationSummary, isNavigationObservableCommand, } from "./navigation.js";
9
9
  import { formatSemanticActionPresentationSummary, formatSemanticActionPresentationText, resolvePresentationCommandInfo, } from "./semantic-action.js";
10
- function getPageSummary(data) {
11
- const title = typeof data.title === "string" ? data.title : undefined;
12
- const url = typeof data.url === "string" ? data.url : undefined;
13
- if (!title && !url)
14
- return undefined;
15
- if (title && url)
16
- return `${title}\n${url}`;
17
- return title ?? url;
18
- }
19
10
  function formatConfirmationRequiredSummary(confirmation) {
20
11
  return `Confirmation required: ${confirmation.id}`;
21
12
  }
13
+ const SIMPLE_ACTION_RESULTS = {
14
+ check: { field: "checked", label: "Checked" },
15
+ click: { field: "clicked", label: "Clicked" },
16
+ fill: { field: "filled", label: "Filled" },
17
+ focus: { field: "focused", label: "Focused" },
18
+ hover: { field: "hovered", label: "Hovered" },
19
+ press: { field: "pressed", label: "Pressed" },
20
+ select: { field: "selected", label: "Selected" },
21
+ type: { field: "typed", label: "Typed" },
22
+ uncheck: { field: "unchecked", label: "Unchecked" },
23
+ };
24
+ function formatSimpleActionResult(command, data) {
25
+ if (!isRecord(data))
26
+ return undefined;
27
+ if (command === "click" && getNavigationSummary(data))
28
+ return undefined;
29
+ const definition = SIMPLE_ACTION_RESULTS[command];
30
+ if (!definition)
31
+ return undefined;
32
+ const value = data[definition.field];
33
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
34
+ return undefined;
35
+ return `${definition.label}: ${redactModelFacingText(String(value))}`;
36
+ }
37
+ function formatWaitResult(data) {
38
+ if (!isRecord(data))
39
+ return undefined;
40
+ if (data.waited === "timeout")
41
+ return "Fixed wait elapsed; no page condition was verified.";
42
+ for (const field of ["selector", "text", "url", "state", "waited"]) {
43
+ const value = data[field];
44
+ if (typeof value === "string" && value.length > 0)
45
+ return `Wait completed: ${redactModelFacingText(value)}`;
46
+ }
47
+ return undefined;
48
+ }
49
+ function formatCloseResult(data) {
50
+ return isRecord(data) && data.closed === true ? "Browser session closed." : undefined;
51
+ }
22
52
  const VITALS_METRICS = ["lcp", "fcp", "ttfb", "inp", "cls"];
23
53
  function coerceVitalsMetricValue(value) {
24
54
  if (typeof value === "number" && Number.isFinite(value))
@@ -80,6 +110,14 @@ function formatConfirmationRequiredText(confirmation) {
80
110
  return lines.join("\n");
81
111
  }
82
112
  const COMMAND_PRESENTERS = {
113
+ ...Object.fromEntries(Object.keys(SIMPLE_ACTION_RESULTS).map((command) => [command, {
114
+ summary: (_commandInfo, data) => formatSimpleActionResult(command, data),
115
+ text: (_commandInfo, data) => formatSimpleActionResult(command, data),
116
+ }])),
117
+ close: { summary: (_commandInfo, data) => formatCloseResult(data), text: (_commandInfo, data) => formatCloseResult(data) },
118
+ exit: { summary: (_commandInfo, data) => formatCloseResult(data), text: (_commandInfo, data) => formatCloseResult(data) },
119
+ quit: { summary: (_commandInfo, data) => formatCloseResult(data), text: (_commandInfo, data) => formatCloseResult(data) },
120
+ wait: { summary: (_commandInfo, data) => formatWaitResult(data), text: (_commandInfo, data) => formatWaitResult(data) },
83
121
  profiles: {
84
122
  summary: (_commandInfo, data) => Array.isArray(data) ? `Chrome profiles: ${data.length}` : undefined,
85
123
  text: (_commandInfo, data) => Array.isArray(data) ? formatProfilesText(data, "Chrome profiles") : undefined,
@@ -118,8 +156,12 @@ const COMMAND_PRESENTERS = {
118
156
  text: (commandInfo, data) => isRecord(data) && commandInfo.subcommand === "status" ? getStreamSummary(data) : undefined,
119
157
  },
120
158
  tab: {
121
- summary: (_commandInfo, data) => isRecord(data) && Array.isArray(data.tabs) ? `Tabs: ${data.tabs.length}` : undefined,
122
- text: (_commandInfo, data) => isRecord(data) ? getTabSummary(data) : undefined,
159
+ summary: (_commandInfo, data) => isRecord(data) && Array.isArray(data.tabs)
160
+ ? `Tabs: ${data.tabs.length}`
161
+ : isRecord(data) && data.closed === true ? "Tab closed" : undefined,
162
+ text: (_commandInfo, data) => isRecord(data) && data.closed === true
163
+ ? `Tab closed${typeof data.tabId === "string" ? `: ${redactModelFacingText(data.tabId)}` : "."}`
164
+ : isRecord(data) ? getTabSummary(data) : undefined,
123
165
  },
124
166
  vitals: {
125
167
  summary: (_commandInfo, data) => isRecord(data) ? formatVitalsSummary(data) : undefined,
@@ -155,7 +197,7 @@ export function formatPresentationSummary(commandInfo, data, compiledSemanticAct
155
197
  return semanticSummary;
156
198
  }
157
199
  const navigationSummary = getNavigationSummary(data);
158
- if (navigationSummary && isNavigationObservableCommand(presentationCommandInfo.command)) {
200
+ if (navigationSummary && isNavigationObservableCommand(presentationCommandInfo.command, presentationCommandInfo.subcommand)) {
159
201
  const navigationText = formatNavigationSummary(navigationSummary);
160
202
  if (navigationText) {
161
203
  return `${presentationCommandInfo.command ?? "navigation"} → ${navigationText.split("\n", 1)[0] ?? navigationText}`;
@@ -200,7 +242,7 @@ export function formatPresentationContentText(commandInfo, data, compiledSemanti
200
242
  }
201
243
  const presentationCommandInfo = resolvePresentationCommandInfo(commandInfo, compiledSemanticAction);
202
244
  const navigationSummary = getNavigationSummary(data);
203
- if (navigationSummary && isNavigationObservableCommand(presentationCommandInfo.command)) {
245
+ if (navigationSummary && isNavigationObservableCommand(presentationCommandInfo.command, presentationCommandInfo.subcommand)) {
204
246
  const navigationText = formatNavigationSummary(navigationSummary);
205
247
  if (navigationText) {
206
248
  const actionText = formatNavigationActionResult(data);
@@ -216,5 +258,8 @@ export function formatPresentationContentText(commandInfo, data, compiledSemanti
216
258
  const pageSummary = getPageSummary(data);
217
259
  if (pageSummary)
218
260
  return redactModelFacingText(pageSummary);
219
- return stringifyModelFacing(data);
261
+ const { navigationSummary: _navigationSummary, ...pageData } = omitUpstreamLifecycle(data);
262
+ if (Object.keys(pageData).length > 0)
263
+ return stringifyModelFacing(pageData);
264
+ return `${presentationCommandInfo.command ?? commandInfo.command ?? "agent-browser"} completed`;
220
265
  }
@@ -1,18 +1,9 @@
1
1
  import { getCompiledSemanticActionCommandIndex, isCompiledSemanticActionFindCommand, } from "../../input-modes/semantic-action.js";
2
2
  import { isRecord } from "../../parsing.js";
3
3
  import { formatNavigationSummary, getNavigationSummary, isNavigationObservableCommand, } from "./navigation.js";
4
- import { redactModelFacingText } from "./common.js";
4
+ import { getPageSummary, redactModelFacingText } from "./common.js";
5
5
  const SEMANTIC_NAVIGATION_PROBE_ACTIONS = new Set(["check", "click"]);
6
6
  const SEMANTIC_PRESENTATION_ACTIONS = new Set(["check", "click", "fill", "select"]);
7
- function getPageSummary(data) {
8
- const title = typeof data.title === "string" ? data.title : undefined;
9
- const url = typeof data.url === "string" ? data.url : undefined;
10
- if (!title && !url)
11
- return undefined;
12
- if (title && url)
13
- return `${title}\n${url}`;
14
- return title ?? url;
15
- }
16
7
  function formatSemanticActionTarget(compiled) {
17
8
  if (compiled.action === "select") {
18
9
  const selector = compiled.selector ?? "selector";
@@ -1,13 +1,13 @@
1
1
  import { isRecord } from "../parsing.js";
2
- import { extractCommandTokens } from "../runtime.js";
2
+ import { extractUpstreamCommandTokens, parseCommandInfo, redactInvocationArgs } from "../runtime.js";
3
3
  import { buildAgentBrowserNextActions } from "./action-recommendations.js";
4
4
  import { buildAgentBrowserResultCategoryDetails } from "./categories.js";
5
5
  import { detectConfirmationRequired } from "./confirmation.js";
6
6
  import { buildSnapshotPresentation } from "./snapshot.js";
7
7
  import { parseJsonPreviewString, redactModelFacingText, stringifyModelFacing } from "./presentation/common.js";
8
8
  import { applyArtifactManifest, attachInlineImage, buildArtifactVerificationSummary, buildManifestEntriesForFileArtifacts, classifyPresentationSuccessCategory, extractFileArtifacts, extractImagePath, formatArtifactMetadataLines, formatArtifactSummary, formatMissingArtifactFailureText, getSavedFileDetails, hasMissingFileArtifact, isManifestFileArtifact, } from "./presentation/artifacts.js";
9
- import { buildBatchPresentation, isAgentBrowserBatchResultArray } from "./presentation/batch.js";
10
- import { getPresentationPaths } from "./presentation/content.js";
9
+ import { buildBatchPresentation, isAgentBrowserBatchResultArray, redactBatchStepErrorData } from "./presentation/batch.js";
10
+ import { getPresentationPaths, isStringArray } from "./presentation/content.js";
11
11
  import { buildNetworkRequestsNextActions, buildStreamNextActions, enrichStreamStatusData, formatNetworkRouteDiagnosticsText, redactPresentationData, } from "./presentation/diagnostics.js";
12
12
  import { buildErrorPresentation } from "./presentation/errors.js";
13
13
  import { compactLargePresentationOutput } from "./presentation/large-output.js";
@@ -31,16 +31,35 @@ function mergeNextActions(...groups) {
31
31
  function shouldAddAnnotatedScreenshotGuidance(commandInfo, args) {
32
32
  return commandInfo.command === "screenshot" && (args?.includes("--annotate") ?? false);
33
33
  }
34
+ function getKeyboardInsertTextWarning(commandInfo) {
35
+ if (commandInfo.command !== "keyboard" || commandInfo.subcommand !== "inserttext")
36
+ return undefined;
37
+ return "Input dispatch warning: keyboard inserttext skips key events. A DOM value change does not prove a framework-controlled editor accepted it; verify application state before saving, or use keyboard type when real key events are required.";
38
+ }
39
+ function redactBatchSpillData(data) {
40
+ return data.map((row) => {
41
+ const command = isStringArray(row.command) ? row.command : undefined;
42
+ const commandInfo = parseCommandInfo(command ?? []);
43
+ return {
44
+ ...row,
45
+ command: command ? redactInvocationArgs(command) : row.command,
46
+ error: row.error === undefined ? undefined : redactBatchStepErrorData(command, row.error),
47
+ result: row.result === undefined ? undefined : redactPresentationData(commandInfo, row.result),
48
+ };
49
+ });
50
+ }
34
51
  export async function buildToolPresentation(options) {
35
52
  const { args, artifactManifest, artifactRequest, commandInfo, compiledSemanticAction, cwd, envelope, errorText, namespace, networkRouteDiagnostics, networkRoutes, persistentArtifactStore, sessionName, } = options;
36
- const commandInfoWithTokens = commandInfo.commandTokens || !args ? commandInfo : { ...commandInfo, commandTokens: extractCommandTokens(args) };
53
+ const commandInfoWithTokens = commandInfo.commandTokens || !args ? commandInfo : { ...commandInfo, commandTokens: extractUpstreamCommandTokens(args) };
37
54
  const presentationCommandInfo = resolvePresentationCommandInfo(commandInfoWithTokens, compiledSemanticAction);
38
55
  if (errorText) {
39
56
  return buildErrorPresentation({ args, commandInfo, errorText, sessionName });
40
57
  }
41
58
  const data = enrichStreamStatusData(commandInfoWithTokens, envelope?.data);
42
- const presentationData = redactPresentationData(commandInfoWithTokens, data);
43
- const artifacts = await extractFileArtifacts({ artifactManifest, artifactRequest, commandInfo: presentationCommandInfo, cwd, data, sessionName });
59
+ const presentationData = commandInfo.command === "batch" && isAgentBrowserBatchResultArray(data)
60
+ ? redactBatchSpillData(data)
61
+ : redactPresentationData(commandInfoWithTokens, data);
62
+ const artifacts = await extractFileArtifacts({ artifactManifest, artifactMaxUpdatedAtMs: options.artifactMaxUpdatedAtMs, artifactMinUpdatedAtMs: options.artifactMinUpdatedAtMs, artifactRequest, commandInfo: presentationCommandInfo, cwd, data, namespace, sessionName });
44
63
  const artifactVerification = buildArtifactVerificationSummary(artifacts);
45
64
  const artifactSummary = formatArtifactSummary(artifacts);
46
65
  const summary = artifactSummary ?? formatPresentationSummary(commandInfoWithTokens, data, compiledSemanticAction);
@@ -49,6 +68,8 @@ export async function buildToolPresentation(options) {
49
68
  if (commandInfo.command === "batch" && isAgentBrowserBatchResultArray(data)) {
50
69
  presentation = await buildBatchPresentation({
51
70
  artifactManifest,
71
+ artifactMaxUpdatedAtMs: options.artifactMaxUpdatedAtMs,
72
+ artifactMinUpdatedAtMs: options.artifactMinUpdatedAtMs,
52
73
  artifactRequests: options.batchArtifactRequests,
53
74
  buildNestedToolPresentation: buildToolPresentation,
54
75
  cwd,
@@ -93,6 +114,10 @@ export async function buildToolPresentation(options) {
93
114
  const guidance = "Annotated screenshot note: dense pages can produce overlapping labels. If the labels are noisy, capture a scoped element screenshot, take a non-annotated screenshot, or use snapshot -i high-value refs as the machine-readable map.";
94
115
  presentation.content[0] = { ...presentation.content[0], text: `${presentation.content[0].text}\n\n${guidance}` };
95
116
  }
117
+ const keyboardInsertTextWarning = getKeyboardInsertTextWarning(commandInfoWithTokens);
118
+ if (keyboardInsertTextWarning && presentation.content[0]?.type === "text") {
119
+ presentation.content[0] = { ...presentation.content[0], text: `${presentation.content[0].text}\n\n${keyboardInsertTextWarning}` };
120
+ }
96
121
  const imagePath = artifactRequest?.absolutePath ?? extractImagePath(commandInfo, cwd, data);
97
122
  const presentationWithImage = imagePath ? await attachInlineImage(presentation, imagePath) : presentation;
98
123
  const compactedPresentation = await compactLargePresentationOutput({
@@ -156,6 +181,8 @@ export async function buildToolPresentation(options) {
156
181
  failureCategory: presentationWithManifest.failureCategory,
157
182
  resultCategory: presentationWithManifest.resultCategory ?? "success",
158
183
  savedFilePath: presentationWithManifest.savedFilePath,
184
+ sessionName,
185
+ subcommand: presentationCommandInfo.subcommand,
159
186
  successCategory: presentationWithManifest.successCategory,
160
187
  });
161
188
  const networkNextActions = commandInfoWithTokens.command === "network" && commandInfoWithTokens.subcommand === "requests" && presentationWithManifest.resultCategory === "success"
@@ -171,5 +198,8 @@ export async function buildToolPresentation(options) {
171
198
  savedFilePath: presentationWithManifest.savedFilePath,
172
199
  summary: presentationWithManifest.summary,
173
200
  });
201
+ if (presentationWithManifest.pageChangeSummary?.observed === false && presentationCommandInfo.command !== "batch" && presentationWithManifest.content[0]?.type === "text") {
202
+ presentationWithManifest.content[0] = { ...presentationWithManifest.content[0], text: `${presentationWithManifest.content[0].text}\n\nAction dispatched; application change unverified. Verify the expected URL, text, state, or external receipt before relying on it.` };
203
+ }
174
204
  return sanitizeModelFacingPresentation(presentationWithManifest);
175
205
  }
@@ -8,6 +8,8 @@ export const AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS = {
8
8
  selectIntendedTabAfterDrift: "select-intended-tab-after-drift",
9
9
  snapshotAfterTabRecovery: "snapshot-after-tab-recovery",
10
10
  tabDriftListTabs: "list-tabs-for-tab-drift-recovery",
11
+ tabGoneListTabs: "list-tabs-after-tab-gone",
12
+ tabGoneNewTab: "open-tab-after-tab-gone",
11
13
  };
12
14
  export const AGENT_BROWSER_RICH_INPUT_RECOVERY_NEXT_ACTION_IDS = {
13
15
  click: "click-current-editable-ref",
@@ -58,7 +60,7 @@ export function buildRecoveryNextActions(recovery) {
58
60
  args: sessionArgs(["get", "url"]),
59
61
  id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.connectedSessionGetUrl,
60
62
  reason: "Verify the attached page URL before any page-content inspection.",
61
- safety: "Read-only URL lookup. The wrapper keeps page inspection blocked if the active target is a local file page or the URL cannot be verified.",
63
+ safety: "Read-only URL lookup. The wrapper keeps page inspection blocked if the active target URL cannot be verified.",
62
64
  }),
63
65
  buildNextToolAction({
64
66
  args: sessionArgs(["tab", "list"]),
@@ -17,6 +17,15 @@ export function buildNoActivePageNextActions(sessionName) {
17
17
  resultCategory: "failure",
18
18
  }) ?? [];
19
19
  }
20
+ export function buildPendingWebMcpNextActions(sessionName) {
21
+ return [{
22
+ id: "verify-page-target-after-pending-webmcp",
23
+ params: { args: withOptionalSessionArgs(sessionName, ["get", "url"]) },
24
+ reason: "Verify the current page target before taking a fresh snapshot; use webmcp result or cancel to settle the detached invocation.",
25
+ safety: "Read-only URL inspection. It does not settle the pending page tool, which may still mutate or navigate later.",
26
+ tool: "agent_browser",
27
+ }];
28
+ }
20
29
  export function buildSessionTabRecoveryNextActions(options) {
21
30
  const resultCategory = options.resultCategory ?? "success";
22
31
  return buildAgentBrowserNextActions({
@@ -9,14 +9,24 @@ const SELECTOR_RECOVERY_ACTION_NAMES = new Set(["check", "click", "fill", "selec
9
9
  const VISIBLE_REF_FALLBACK_CANDIDATE_LIMIT = 3;
10
10
  const EDITABLE_CONTROL_ROLES = new Set(["combobox", "searchbox", "textbox"]);
11
11
  const RICH_INPUT_RECOVERY_EDITABLE_ROLES = new Set(["searchbox", "textbox"]);
12
- const RICH_INPUT_RECOVERY_HINT = "After the editable ref is focused, use keyboard inserttext or keyboard type with the intended text in a separate call, and do not press Enter or otherwise submit unless the user flow explicitly calls for it.";
12
+ const RICH_INPUT_RECOVERY_HINT = "After the editable ref is focused, use keyboard type when a framework-controlled editor requires real key events. keyboard inserttext is paste-like and needs separate application-state verification. Do not press Enter or otherwise submit unless the user flow explicitly calls for it.";
13
13
  function isSelectorRecoveryActionName(action) {
14
14
  return SELECTOR_RECOVERY_ACTION_NAMES.has(action);
15
15
  }
16
16
  function getFindNameFlagValue(args, startIndex) {
17
17
  const nameFlagIndex = args.indexOf("--name", startIndex);
18
18
  const name = nameFlagIndex >= 0 ? args[nameFlagIndex + 1] : undefined;
19
- return name && !name.startsWith("-") ? name : undefined;
19
+ return typeof name === "string" && name.length > 0 ? name : undefined;
20
+ }
21
+ function collectFindTrailingValues(args, startIndex) {
22
+ const values = [];
23
+ for (let index = startIndex; index < args.length; index += 1) {
24
+ const token = args[index];
25
+ if (!token || token.startsWith("-"))
26
+ break;
27
+ values.push(token);
28
+ }
29
+ return values;
20
30
  }
21
31
  function getFindVisibleRefFallbackTarget(args, options = {}) {
22
32
  const findIndex = args[0] === "--session" ? 2 : 0;
@@ -25,8 +35,21 @@ function getFindVisibleRefFallbackTarget(args, options = {}) {
25
35
  const locator = args[findIndex + 1];
26
36
  const value = args[findIndex + 2];
27
37
  const action = args[findIndex + 3];
28
- if (!locator || !value || !isSelectorRecoveryActionName(action) || action === "select")
38
+ if (!locator || !value || !isSelectorRecoveryActionName(action))
29
39
  return undefined;
40
+ if (action === "select") {
41
+ const optionValues = collectFindTrailingValues(args, findIndex + 4);
42
+ if (locator === "role") {
43
+ if (!/^(?:combobox|listbox)$/i.test(value))
44
+ return undefined;
45
+ const targetName = getFindNameFlagValue(args, findIndex + 4);
46
+ return targetName ? { action, optionValues, roles: [value.toLowerCase()], targetName } : undefined;
47
+ }
48
+ if (locator === "label") {
49
+ return { action, optionValues, roles: ["combobox", "listbox"], targetName: value };
50
+ }
51
+ return undefined;
52
+ }
30
53
  const text = action === "fill" ? args[findIndex + 4] : undefined;
31
54
  if (action === "fill" && (!text || (!options.allowLeadingDashFillText && text.startsWith("-"))))
32
55
  return undefined;
@@ -69,7 +92,13 @@ function getVisibleRefFallbackCandidates(target, snapshotData) {
69
92
  return [];
70
93
  if (target.action === "fill" && editableEvidence === false && EDITABLE_CONTROL_ROLES.has(role.toLowerCase()))
71
94
  return [];
72
- const directRefArgs = target.action === "fill" ? undefined : [target.action, `@${ref}`];
95
+ const directRefArgs = target.action === "fill"
96
+ ? undefined
97
+ : target.action === "select" && target.optionValues && target.optionValues.length > 0
98
+ ? ["select", `@${ref}`, ...target.optionValues]
99
+ : target.action === "select"
100
+ ? undefined
101
+ : [target.action, `@${ref}`];
73
102
  return [{
74
103
  action: target.action,
75
104
  ...(directRefArgs ? { args: directRefArgs } : {}),
@@ -101,19 +130,33 @@ export function buildVisibleRefFallbackDiagnosticFromSnapshot(options) {
101
130
  }
102
131
  export function resolveVisibleRefActionFromSnapshot(options) {
103
132
  const target = getFindVisibleRefFallbackTarget(options.compiledAction.args, { allowLeadingDashFillText: true });
104
- if (!target || target.action === "select")
133
+ if (!target)
105
134
  return undefined;
106
135
  const snapshot = extractRefSnapshotFromData(options.snapshotData);
107
136
  if (!snapshot)
108
137
  return undefined;
109
- const candidates = getVisibleRefFallbackCandidates(target, options.snapshotData);
110
- if (target.action === "fill") {
111
- if (!options.allowFill || candidates.length !== 1 || target.text === undefined)
138
+ const selectOptionValues = options.compiledAction.values && options.compiledAction.values.length > 0
139
+ ? options.compiledAction.values
140
+ : target.optionValues;
141
+ const effectiveTarget = target.action === "select" && selectOptionValues
142
+ ? { ...target, optionValues: selectOptionValues }
143
+ : target;
144
+ const candidates = getVisibleRefFallbackCandidates(effectiveTarget, options.snapshotData);
145
+ if (effectiveTarget.action === "fill") {
146
+ if (!options.allowFill || candidates.length !== 1 || effectiveTarget.text === undefined)
112
147
  return undefined;
113
148
  const [candidate] = candidates;
114
149
  if (!candidate || candidate.editableEvidence === false || !EDITABLE_CONTROL_ROLES.has(candidate.role.toLowerCase()))
115
150
  return undefined;
116
- return { args: ["fill", candidate.ref, target.text], snapshot };
151
+ return { args: ["fill", candidate.ref, effectiveTarget.text], snapshot };
152
+ }
153
+ if (effectiveTarget.action === "select") {
154
+ if (candidates.length !== 1 || !selectOptionValues || selectOptionValues.length === 0)
155
+ return undefined;
156
+ const [candidate] = candidates;
157
+ if (!candidate)
158
+ return undefined;
159
+ return { args: ["select", candidate.ref, ...selectOptionValues], snapshot };
117
160
  }
118
161
  const candidate = candidates.find((item) => item.args !== undefined);
119
162
  if (!candidate?.args)
@@ -182,7 +225,7 @@ export function buildRichInputRecoveryNextActions(options) {
182
225
  const clickId = getAgentBrowserRichInputRecoveryNextActionId("click", index, candidateCount);
183
226
  const safety = ambiguous
184
227
  ? `Several editable refs share the same exact name. Inspect the current snapshot and use only the ${candidate.ref} ${candidate.role} if it is clearly the intended input. No fill text or submit key is included.`
185
- : "Does not include fill text or submit the form. After focus/click succeeds, use keyboard inserttext or keyboard type with the intended text only if this is the right input.";
228
+ : "Does not include fill text or submit the form. After focus/click succeeds, use keyboard type for framework-controlled editors; use keyboard inserttext only with separate application-state verification.";
186
229
  return [
187
230
  {
188
231
  id: focusId,
@@ -208,7 +251,7 @@ export function formatRichInputRecoveryText(diagnostic) {
208
251
  "Rich input recovery:",
209
252
  ...diagnostic.candidates.map((candidate, index) => {
210
253
  const [focusId, clickId] = diagnostic.nextActionIds.slice(index * 2, index * 2 + 2);
211
- return `- ${candidate.ref} ${candidate.role} ${JSON.stringify(candidate.name)}: use ${focusId} or ${clickId}; then use keyboard inserttext/type with the intended text.`;
254
+ return `- ${candidate.ref} ${candidate.role} ${JSON.stringify(candidate.name)}: use ${focusId} or ${clickId}; then use keyboard type for framework-controlled editors, or paste-like keyboard inserttext only with separate application-state verification.`;
212
255
  }),
213
256
  `- ${diagnostic.inputMethodHint}`,
214
257
  ].join("\n");
@@ -2,6 +2,8 @@ import { compareRefIds } from "./text.js";
2
2
  const SNAPSHOT_HIGH_VALUE_EDITABLE_REF_FILL_TARGET_LINES = 4;
3
3
  const SNAPSHOT_HIGH_VALUE_SURFACE_REF_FILL_TARGET_LINES = 3;
4
4
  const SNAPSHOT_HIGH_VALUE_PRIMARY_ACTION_REF_FILL_TARGET_LINES = 3;
5
+ const SNAPSHOT_HIGH_VALUE_NAMED_LINK_REF_FILL_TARGET_LINES = 6;
6
+ const SNAPSHOT_HIGH_VALUE_LINK_NAME_MAX_LENGTH = 80;
5
7
  const SNAPSHOT_HIGH_VALUE_CONTROL_ROLES = new Set([
6
8
  "button",
7
9
  "checkbox",
@@ -33,9 +35,6 @@ const SNAPSHOT_PRIMARY_ACTION_BUTTON_NAME_PATTERNS = [
33
35
  /^(?:add|apply|ask|attach|choose|confirm|connect|continue|create|deploy|done|download|go|insert|launch|log in|new|next|ok|open|publish|refresh|retry|run|save|search|select|send|sign in|sign up|start|submit|upload)$/i,
34
36
  /^(?:add|apply|ask|confirm|connect|continue|create|launch|new|open|refresh|retry|run|save|search|send|start|submit)\b/i,
35
37
  ];
36
- const SNAPSHOT_HIGH_VALUE_LINK_NAME_PATTERNS = [
37
- /^[a-z0-9_.-]+\/[a-z0-9_.-]+$/i,
38
- ];
39
38
  function getHighValueControlRole(entry) {
40
39
  return entry.isEditable === true && (entry.role === "unknown" || entry.role === "generic") ? "textbox" : entry.role;
41
40
  }
@@ -82,22 +81,29 @@ const SNAPSHOT_HIGH_VALUE_CONTROL_CATEGORY_RULES = [
82
81
  matches: (entry) => isPrimaryActionButtonRef(entry),
83
82
  priority: 2,
84
83
  },
84
+ {
85
+ bucketKey: () => "named-link",
86
+ fillTarget: SNAPSHOT_HIGH_VALUE_NAMED_LINK_REF_FILL_TARGET_LINES,
87
+ id: "named-link",
88
+ matches: (entry) => getHighValueControlRole(entry) === "link" && isNamedActionLinkRef(entry),
89
+ priority: 3,
90
+ },
85
91
  {
86
92
  bucketKey: (_entry, role) => role,
87
93
  id: "role",
88
94
  matches: () => true,
89
- priority: 3,
95
+ priority: 4,
90
96
  },
91
97
  ];
92
- function isHighValueLinkRef(entry) {
93
- return entry.name.length > 0 && SNAPSHOT_HIGH_VALUE_LINK_NAME_PATTERNS.some((pattern) => pattern.test(entry.name));
98
+ function isNamedActionLinkRef(entry) {
99
+ return entry.name.length > 0 && entry.name.length <= SNAPSHOT_HIGH_VALUE_LINK_NAME_MAX_LENGTH;
94
100
  }
95
101
  export function isHighValueControlEntry(entry) {
96
102
  const role = getHighValueControlRole(entry);
97
103
  if (!SNAPSHOT_HIGH_VALUE_CONTROL_ROLES.has(role))
98
104
  return false;
99
105
  if (role === "link")
100
- return isHighValueLinkRef(entry);
106
+ return isNamedActionLinkRef(entry);
101
107
  if (entry.isEditable === false && (role === "searchbox" || role === "textbox" || role === "combobox"))
102
108
  return false;
103
109
  return entry.name.length > 0 || isEditableControlRef(entry);
@@ -1,9 +1,10 @@
1
+ import { redactSensitiveValue } from "../runtime.js";
1
2
  import { writePersistentSessionArtifactFile, writeSecureTempFile } from "../temp.js";
2
3
  import { buildEvictedSessionArtifactEntries, formatSessionArtifactRetentionSummary, mergeSessionArtifactManifest, } from "./artifact-manifest.js";
3
4
  const SNAPSHOT_SPILL_FILE_PREFIX = "pi-agent-browser-snapshot";
4
5
  export async function writeSnapshotSpillFile(data, persistentArtifactStore) {
5
6
  const options = {
6
- content: JSON.stringify(data, null, 2),
7
+ content: JSON.stringify(redactSensitiveValue(data), null, 2),
7
8
  prefix: SNAPSHOT_SPILL_FILE_PREFIX,
8
9
  suffix: ".json",
9
10
  };
@@ -162,12 +162,12 @@ export async function buildSnapshotPresentation(data, persistentArtifactStore =
162
162
  ...(roleCountsText ? [`Top roles: ${roleCountsText}`] : []),
163
163
  "",
164
164
  "Compact snapshot view.",
165
- "Viewport note: compact snapshots are DOM/signal-prioritized, not guaranteed to start with the currently scrolled viewport; use the full raw snapshot, a screenshot, or listed high-value refs when viewport context matters.",
165
+ "Viewport note: compact snapshots are DOM/signal-prioritized, not guaranteed to start with the currently scrolled viewport; use the full redacted snapshot, a screenshot, or listed high-value refs when viewport context matters.",
166
166
  ];
167
167
  if (fallbackPreview) {
168
168
  lines.push("", "Compact outline:", ...(fallbackPreview.lines.length > 0 ? fallbackPreview.lines : ["(no interactive elements)"]));
169
169
  if (fallbackPreview.omittedCount > 0) {
170
- lines.push(`- ... (${fallbackPreview.omittedCount} additional snapshot lines omitted; ${fullOutputPath ? `full output path: ${fullOutputPath}` : "the full raw snapshot was omitted"})`);
170
+ lines.push(`- ... (${fallbackPreview.omittedCount} additional snapshot lines omitted; ${fullOutputPath ? `full output path: ${fullOutputPath}` : "the full redacted snapshot was omitted"})`);
171
171
  }
172
172
  }
173
173
  else {
@@ -204,8 +204,8 @@ export async function buildSnapshotPresentation(data, persistentArtifactStore =
204
204
  }
205
205
  }
206
206
  lines.push("", fullOutputPath
207
- ? `Full raw snapshot path: ${fullOutputPath}`
208
- : `Full raw snapshot unavailable: ${spillErrorText ?? "temp spill file could not be created."}`);
207
+ ? `Full redacted snapshot path: ${fullOutputPath}`
208
+ : `Full redacted snapshot unavailable: ${spillErrorText ?? "temp spill file could not be created."}`);
209
209
  const manifestFields = applySnapshotArtifactManifest({
210
210
  baseManifest: artifactManifest,
211
211
  command: "snapshot",