pi-agent-browser-native 0.9.1 → 0.9.3

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 (42) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +35 -8
  3. package/dist/extensions/agent-browser/index.js +39 -16
  4. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +6 -3
  5. package/dist/extensions/agent-browser/lib/browser-transcript.js +2 -1
  6. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +4 -0
  7. package/dist/extensions/agent-browser/lib/native-output.js +2 -1
  8. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +2 -8
  9. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +12 -2
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +62 -10
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +18 -12
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +33 -24
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +111 -53
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/screenshot-observation.js +6 -2
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +32 -20
  16. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +37 -32
  17. package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +30 -11
  18. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +3 -1
  19. package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -3
  20. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +2 -1
  21. package/dist/extensions/agent-browser/lib/playbook.js +7 -6
  22. package/dist/extensions/agent-browser/lib/process-environment.js +12 -3
  23. package/dist/extensions/agent-browser/lib/process.js +6 -6
  24. package/dist/extensions/agent-browser/lib/read-confirmation.js +56 -16
  25. package/dist/extensions/agent-browser/lib/results/confirmation.js +3 -0
  26. package/dist/extensions/agent-browser/lib/results/envelope.js +5 -1
  27. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +10 -1
  28. package/dist/extensions/agent-browser/lib/results/presentation/common.js +1 -0
  29. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +34 -1
  30. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +5 -1
  31. package/dist/extensions/agent-browser/lib/results/presentation.js +15 -2
  32. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +5 -5
  33. package/dist/extensions/agent-browser/lib/runtime.js +5 -1
  34. package/dist/extensions/agent-browser/lib/session-page-state.js +35 -16
  35. package/docs/ARCHITECTURE.md +10 -9
  36. package/docs/COMMAND_REFERENCE.md +37 -13
  37. package/docs/RELEASE.md +6 -5
  38. package/docs/REQUIREMENTS.md +3 -1
  39. package/docs/SESSION_STORAGE.md +1 -1
  40. package/docs/SUPPORT_MATRIX.md +12 -3
  41. package/docs/TOOL_CONTRACT.md +29 -20
  42. package/package.json +4 -4
@@ -2,21 +2,40 @@ import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
2
2
  import { extractExplicitSessionName, getAgentBrowserSessionIdentityKey, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences } from "./argv-grammar.js";
3
3
  import { getExplicitReadUrl } from "./command-policy.js";
4
4
  import { isCloseCommand } from "./command-taxonomy.js";
5
+ import { getExplicitNavigationTarget } from "./page-target-validation.js";
5
6
  import { isRecord } from "./parsing.js";
7
+ import { getUpstreamEffectiveBatchSteps } from "./orchestration/batch-stdin.js";
6
8
  export function parseReadConfirmation(value) {
7
- if (!isRecord(value) || value.source !== "native-explicit-url-read" || (value.state !== "pending" && value.state !== "cleared"))
9
+ if (!isRecord(value) || (value.source !== "native-explicit-url-read" && value.source !== "native-guarded-action") || (value.state !== "pending" && value.state !== "cleared"))
8
10
  return undefined;
9
11
  if (typeof value.id !== "string" || !value.id || typeof value.sessionName !== "string" || !value.sessionName || (value.namespace !== undefined && typeof value.namespace !== "string"))
10
12
  return undefined;
11
- return { ...(isRecord(value.capabilities) && value.capabilities.readRequiresConfirmation === true ? { capabilities: { readRequiresConfirmation: true } } : {}), id: value.id, sessionName: value.sessionName, namespace: value.namespace, source: "native-explicit-url-read", state: value.state };
13
+ if (value.source === "native-guarded-action" && (typeof value.command !== "string" || !value.command || typeof value.action !== "string" || !value.action))
14
+ return undefined;
15
+ return { ...(value.source === "native-explicit-url-read" && isRecord(value.capabilities) && value.capabilities.readRequiresConfirmation === true ? { capabilities: { readRequiresConfirmation: true } } : {}), id: value.id, sessionName: value.sessionName, namespace: value.namespace, source: value.source, state: value.state,
16
+ ...(value.source === "native-guarded-action" ? { command: typeof value.command === "string" ? value.command : undefined, action: typeof value.action === "string" ? value.action : undefined } : {}),
17
+ ...(value.source === "native-guarded-action" && value.state === "cleared" && value.command === "snapshot" && value.action === "snapshot" && value.refSnapshotFresh === true ? { refSnapshotFresh: true } : {}) };
18
+ }
19
+ export function isBrowserIndependentConfirmation(value) {
20
+ return value?.source === "native-explicit-url-read" && value.capabilities?.readRequiresConfirmation === true;
21
+ }
22
+ export function suppressConfirmationPageHelpers(value) {
23
+ return value?.state === "pending" && (value.source === "native-guarded-action" || isBrowserIndependentConfirmation(value));
12
24
  }
13
- export function findReadConfirmation(args, confirmations, namespace) {
14
- const tokens = extractUpstreamCommandTokens(args);
25
+ export function isSuccessfulNativeConfirmedClose(commandTokens, data) {
26
+ return commandTokens[0] === "confirm" && commandTokens.length === 2 && isRecord(data) && data.confirmed === true
27
+ && data.action === "close" && isRecord(data.result) && data.result.success === true
28
+ && isRecord(data.result.data) && data.result.data.closed === true;
29
+ }
30
+ export function findReadConfirmation(args, confirmations, namespace, stdin) {
31
+ const command = extractUpstreamCommandTokens(args);
32
+ const tokens = command[0] === "batch" ? getUpstreamEffectiveBatchSteps(command, stdin)[0] ?? [] : command;
15
33
  if (tokens.length !== 2 || !["confirm", "deny"].includes(tokens[0]))
16
34
  return undefined;
17
35
  const sessionName = extractExplicitSessionName(args);
18
36
  const effectiveNamespace = resolveAgentBrowserNamespace(args, namespace);
19
37
  const matches = [...confirmations].filter(value => value.state === "pending" && value.id === tokens[1]
38
+ && (command[0] !== "batch" || value.source === "native-guarded-action")
20
39
  && (sessionName === undefined || getAgentBrowserSessionIdentityKey(sessionName, value.namespace) === getAgentBrowserSessionIdentityKey(value.sessionName, value.namespace))
21
40
  && (effectiveNamespace === undefined || getAgentBrowserSessionIdentityKey(value.sessionName, effectiveNamespace) === getAgentBrowserSessionIdentityKey(value.sessionName, value.namespace)));
22
41
  return matches.length === 1 ? matches[0] : undefined;
@@ -28,32 +47,53 @@ export function scopeReadConfirmationArgs(args, confirmation) {
28
47
  ...args,
29
48
  ];
30
49
  }
50
+ export function getNativeTabContinuationGuidance(commandTokens, data) {
51
+ if (commandTokens[0] !== "tab" || commandTokens[1] !== "new" || !isRecord(data)
52
+ || data.confirmation_required !== true || typeof data.confirmation_id !== "string" || !data.confirmation_id || data.action !== "tab_new"
53
+ || !Object.keys(data).every(key => ["confirmation_required", "confirmation_id", "action", "capabilities", "after_confirmation", "guidance"].includes(key))
54
+ || !Array.isArray(data.after_confirmation) || data.after_confirmation.length !== 2 || data.after_confirmation[0] !== "open"
55
+ || typeof data.after_confirmation[1] !== "string" || !data.after_confirmation[1]
56
+ || data.after_confirmation[1] !== getExplicitNavigationTarget(commandTokens)
57
+ || typeof data.guidance !== "string" || !data.guidance.trim())
58
+ return undefined;
59
+ // Transport prose only: approval creates the tab; the continuation is never stored or executed.
60
+ return data.guidance;
61
+ }
31
62
  export function nextReadConfirmation(options) {
32
63
  const { commandTokens: tokens, current } = options;
33
- const settlesRead = current?.state === "pending" && tokens.length === 2 && ["confirm", "deny"].includes(tokens[0]) && tokens[1] === current.id;
34
- const confirmedResult = settlesRead && tokens[0] === "confirm" && isRecord(options.data) && options.data.confirmed === true && options.data.action === "read" && isRecord(options.data.result)
35
- ? options.data.result.data : options.data;
36
- // Native control fields only. Never parse response content, snapshot text or nested page JSON as provenance.
37
- if (isRecord(confirmedResult) && confirmedResult.confirmation_required === true && typeof confirmedResult.confirmation_id === "string" && confirmedResult.confirmation_id && !("content" in confirmedResult)) {
38
- if (confirmedResult.action === "read" && (typeof getExplicitReadUrl(tokens) === "string" || settlesRead)) {
39
- return { ...(isRecord(confirmedResult.capabilities) && confirmedResult.capabilities.readRequiresConfirmation === true ? { capabilities: { readRequiresConfirmation: true } } : {}), id: confirmedResult.confirmation_id, namespace: options.namespace, sessionName: options.sessionName, source: "native-explicit-url-read", state: "pending" };
64
+ const settles = current?.state === "pending" && tokens.length === 2 && ["confirm", "deny"].includes(tokens[0]) && tokens[1] === current.id;
65
+ const confirmed = settles && tokens[0] === "confirm" && isRecord(options.data) && options.data.confirmed === true
66
+ && options.data.action === (current.source === "native-explicit-url-read" ? "read" : current.action) && isRecord(options.data.result);
67
+ const control = confirmed && isRecord(options.data) && isRecord(options.data.result) ? options.data.result.data : options.data;
68
+ // Only native control fields and a validated tab transport continuation. Page output is never provenance.
69
+ if (options.succeeded && isRecord(control) && !Array.isArray(control) && control.confirmation_required === true
70
+ && typeof control.confirmation_id === "string" && control.confirmation_id && typeof control.action === "string" && control.action
71
+ && (Object.keys(control).every(key => ["confirmation_required", "confirmation_id", "action", "capabilities"].includes(key))
72
+ || getNativeTabContinuationGuidance(tokens, control) !== undefined)) {
73
+ const explicitRead = control.action === "read" && (typeof getExplicitReadUrl(tokens) === "string" || confirmed && current.source === "native-explicit-url-read");
74
+ if (explicitRead)
75
+ return { ...(isRecord(control.capabilities) && control.capabilities.readRequiresConfirmation === true ? { capabilities: { readRequiresConfirmation: true } } : {}), id: control.confirmation_id, namespace: options.namespace, sessionName: options.sessionName, source: "native-explicit-url-read", state: "pending" };
76
+ if (tokens[0] && !["confirm", "deny"].includes(tokens[0]) || confirmed && current.source === "native-guarded-action") {
77
+ return { id: control.confirmation_id, namespace: options.namespace, sessionName: options.sessionName, source: "native-guarded-action", command: confirmed ? current.command : tokens[0], action: control.action, state: "pending" };
40
78
  }
41
79
  if (current?.state === "pending")
42
80
  return { ...current, state: "cleared" };
43
81
  }
44
- return options.succeeded && current?.state === "pending" && (settlesRead || isCloseCommand(tokens[0])) ? { ...current, state: "cleared" } : undefined;
82
+ return options.succeeded && current?.state === "pending" && (settles || isCloseCommand(tokens[0])) ? { ...current, state: "cleared" } : undefined;
45
83
  }
46
84
  export function buildReadConfirmationNextActions(confirmation, pendingResponse) {
47
85
  if (confirmation.state === "cleared")
48
86
  return [];
49
87
  const prefix = ["--namespace", confirmation.namespace ?? "", "--session", confirmation.sessionName];
50
88
  if (!pendingResponse)
51
- return [{ id: "inspect-read-confirmation-session", tool: "agent_browser", params: { args: [...prefix, "session", "info"] }, reason: "Inspect the exact native session after the read confirmation failed; rerun the original URL read if its ID expired.", safety: "Read-only status, without browser launch or tab changes. Do not substitute a different pending confirmation ID." }];
89
+ return [{ id: "inspect-read-confirmation-session", tool: "agent_browser", params: { args: [...prefix, "session", "info"] }, reason: "Inspect the exact native session after the confirmation failed; rerun the original command if its ID expired.", safety: "Read-only status, without browser launch or tab changes. Do not substitute a different pending confirmation ID." }];
52
90
  return ["confirm", "deny"].map(command => ({
53
91
  id: command === "confirm" ? "approve-confirmation" : "deny-confirmation", tool: "agent_browser", params: { args: [...prefix, command, confirmation.id] },
54
- reason: `${command === "confirm" ? "Approve" : "Deny"} the native confirmation for this explicit URL read.`,
55
- safety: confirmation.capabilities?.readRequiresConfirmation === true
92
+ reason: `${command === "confirm" ? "Approve" : "Deny"} the native confirmation for ${confirmation.source === "native-explicit-url-read" ? "this explicit URL read" : `${confirmation.command} (${confirmation.action})`}.`,
93
+ safety: isBrowserIndependentConfirmation(confirmation)
56
94
  ? "Review the requested read first. The native capability proves ID matching; no DOM confirmation is implied."
57
- : "Native ID matching/browser independence is unproven. The exact native session is preserved, but this confirmation retains normal page checks.",
95
+ : confirmation.source === "native-guarded-action"
96
+ ? "Review the original guarded action. Exact wrapper observation preserves its session and avoids overwriting helpers; native ID validation is not implied."
97
+ : "Native ID matching/browser independence is unproven. The exact native session is preserved, but this confirmation retains normal page checks.",
58
98
  }));
59
99
  }
@@ -34,6 +34,9 @@ function getNestedConfirmationRecord(data) {
34
34
  return undefined;
35
35
  }
36
36
  export function detectConfirmationRequired(data) {
37
+ // Native confirm can acknowledge one action while its original compound command remains pending.
38
+ if (isRecord(data) && data.confirmed === true && isRecord(data.result) && data.result.success === true)
39
+ data = data.result.data;
37
40
  if (!isRecord(data)) {
38
41
  return undefined;
39
42
  }
@@ -47,6 +47,10 @@ export async function parseAgentBrowserEnvelope(options) {
47
47
  catch (error) {
48
48
  return { parseError: error instanceof Error ? error.message : String(error) };
49
49
  }
50
+ // ponytail: native text has no machine receipts; use JSON mode for structured evidence.
51
+ // JSON-looking page text must never become an envelope, confirmation, or batch row.
52
+ if (typeof options !== "string" && options.textOutput)
53
+ return { envelope: { success: true, data: stdout } };
50
54
  const trimmed = stdout.trim();
51
55
  const plainText = typeof options !== "string" && options.plainText === true;
52
56
  if (trimmed.length === 0 && !plainText) {
@@ -150,7 +154,7 @@ export function getAgentBrowserErrorText(options) {
150
154
  if (spawnError)
151
155
  return spawnError.message;
152
156
  if (parseError)
153
- return parseError;
157
+ return exitCode !== 0 && stderr.trim() ? stderr.trim() : parseError;
154
158
  if (envelope?.success === false) {
155
159
  const explicitErrorText = extractEnvelopeErrorText(envelope.error);
156
160
  if ((hasStructuredBatchStepFailure(envelope.data) || detectConfirmationRequired(envelope.data)) && explicitErrorText === undefined) {
@@ -6,6 +6,7 @@ import { getExplicitArtifactDestination } from "../../orchestration/browser-run/
6
6
  import { isRecord, parsePositiveInteger } from "../../parsing.js";
7
7
  import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingArtifact, isPendingRecordingCommand, mergeSessionArtifactManifest, } from "../artifact-manifest.js";
8
8
  import { classifyAgentBrowserSuccessCategory } from "../categories.js";
9
+ import { LIGHTPANDA_IMAGE_REASON } from "./common.js";
9
10
  import { formatRecordingReceipt, getRecordingReceipt } from "../recording.js";
10
11
  const PNG_HEADER = Buffer.from("89504e470d0a1a0a0000000d49484452", "hex");
11
12
  const INLINE_IMAGE_MAX_BYTES_ENV = "PI_AGENT_BROWSER_INLINE_IMAGE_MAX_BYTES";
@@ -220,6 +221,9 @@ async function buildPreviousRestartRecordingArtifact(options) {
220
221
  const recording = getRecordingReceipt(options.data.previousRecording);
221
222
  return recording ? buildFileArtifactMetadata({ ...options, commandInfo: { command: "record", subcommand: "restart-previous" }, path: recording.path, recording }) : undefined;
222
223
  }
224
+ // A rejected or pending restart has not acknowledged replacement of the active take.
225
+ if (!isRecord(options.data) || options.data.restarted !== true)
226
+ return undefined;
223
227
  const sessionKey = options.sessionName ? getAgentBrowserSessionIdentityKey(options.sessionName, options.namespace) : undefined;
224
228
  const previousRecording = options.artifactManifest?.entries.find((entry) => (entry.command === "record" &&
225
229
  (entry.subcommand === "start" || entry.subcommand === "restart") &&
@@ -587,11 +591,16 @@ export async function attachInlineImage(presentation, imagePath, modelVisible =
587
591
  return presentation;
588
592
  const dimensions = getImageDimensions(header.toString("base64"), mimeType);
589
593
  presentation.imagePath = imagePath;
594
+ const lifecycle = isRecord(presentation.data) && isRecord(presentation.data.lifecycle) ? presentation.data.lifecycle : undefined;
595
+ const textRendered = isRecord(lifecycle?.effectiveLaunch) && lifecycle.effectiveLaunch.engine === "lightpanda";
590
596
  presentation.imageObservations = [{ path: imagePath, mimeType,
597
+ ...(textRendered ? { rendering: "text" } : {}),
591
598
  pixels: dimensions ? { width: dimensions.widthPx, height: dimensions.heightPx } : undefined,
592
- capture: "unknown", geometry: { status: "unknown", reason: dimensions ? "Capture geometry was not observed." : "Image dimensions could not be read from the bounded header." } }];
599
+ capture: "unknown", geometry: { status: "unknown", reason: textRendered ? LIGHTPANDA_IMAGE_REASON : dimensions ? "Capture geometry was not observed." : "Image dimensions could not be read from the bounded header." } }];
593
600
  if (!modelVisible)
594
601
  return presentation;
602
+ if (textRendered)
603
+ appendPresentationNotice(presentation, LIGHTPANDA_IMAGE_REASON);
595
604
  const inlineImageMaxBytes = getInlineImageMaxBytes();
596
605
  if (fileStats.size > inlineImageMaxBytes) {
597
606
  appendPresentationNotice(presentation, `Image attachment skipped: ${formatByteCount(fileStats.size)} exceeds the inline limit of ${formatByteCount(inlineImageMaxBytes)}.`);
@@ -2,6 +2,7 @@ import { isRecord } from "../../parsing.js";
2
2
  import { redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
3
3
  import { stringifyUnknown, truncateText } from "../text.js";
4
4
  const UNTITLED_PAGE_SUMMARY = "(untitled page)";
5
+ export const LIGHTPANDA_IMAGE_REASON = "Lightpanda screenshot is a text-rendered page representation, not a graphical capture; CSS/mouse coordinate mapping is unknown.";
5
6
  export function stringifyModelFacing(value) {
6
7
  return stringifyUnknown(redactSensitiveValue(value));
7
8
  }
@@ -1,4 +1,5 @@
1
1
  import { isRecord } from "../../parsing.js";
2
+ import { getUpstreamEffectiveBatchSteps } from "../../orchestration/batch-stdin.js";
2
3
  import { redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
3
4
  import { classifyNetworkRequestFailure, isApiLikeNetworkRequest, isNetworkArtifactNoiseRequest, summarizeNetworkFailures } from "../network.js";
4
5
  import { withOptionalSessionArgs } from "../next-actions.js";
@@ -866,6 +867,8 @@ function redactStorageData(value) {
866
867
  if (!isRecord(value))
867
868
  return redactSensitiveValue(value);
868
869
  const entries = Object.fromEntries(Object.entries(value).map(([key, entryValue]) => {
870
+ if (key === "data" && isRecord(entryValue))
871
+ return [key, Object.fromEntries(Object.entries(entryValue).map(([storageKey, storageValue]) => [storageKey, shouldRevealStorageValue(storageKey, storageValue) ? redactSensitiveValue(storageValue) : "[REDACTED]"]))];
869
872
  if ((key === "entries" || key === "items") && Array.isArray(entryValue))
870
873
  return [key, entryValue.map((item) => isRecord(item) ? redactStorageEntryValue(item) : redactSensitiveValue(item))];
871
874
  if (key === "value") {
@@ -885,6 +888,9 @@ function redactStorageData(value) {
885
888
  }
886
889
  function formatStorageText(data) {
887
890
  const type = getStringField(data, "type") ?? getStringField(data, "storage") ?? "storage";
891
+ if (isRecord(data.data)) {
892
+ return Object.entries(data.data).map(([key, value]) => `${redactModelFacingText(key)}: ${formatStorageValue(key, value)}`).join("\n") || `${type}: no entries.`;
893
+ }
888
894
  const entries = getArrayField(data, "entries") ?? getArrayField(data, "items");
889
895
  if (entries) {
890
896
  if (entries.length === 0)
@@ -974,7 +980,34 @@ function redactStatefulValues(value, sensitiveKeys) {
974
980
  sensitiveKeys.has(key.toLowerCase()) ? "[REDACTED]" : redactStatefulValues(entryValue, sensitiveKeys),
975
981
  ]));
976
982
  }
977
- export function redactPresentationData(commandInfo, data) {
983
+ function redactNativeContextText(commandInfo, text, stdin) {
984
+ if (commandInfo.command === "batch") {
985
+ const steps = getUpstreamEffectiveBatchSteps(commandInfo.commandTokens ?? ["batch"], stdin);
986
+ if (steps.some(step => step[0] === "cookies"))
987
+ text = redactNativeContextText({ command: "cookies" }, text);
988
+ if (steps.some(step => step[0] === "storage"))
989
+ text = redactNativeContextText({ command: "storage" }, text);
990
+ return text;
991
+ }
992
+ if (commandInfo.command === "cookies")
993
+ return text.replace(/^([^=\r\n]*)=([^\r\n]*)/gm, "$1=[REDACTED]");
994
+ if (commandInfo.command !== "storage")
995
+ return text;
996
+ const tokens = commandInfo.commandTokens;
997
+ const operation = tokens?.[2];
998
+ const key = operation === "get" ? tokens?.[3] : operation === "set" || operation === "clear" ? undefined : operation;
999
+ if (key !== undefined && text.startsWith(`${key}: `)) {
1000
+ const suffix = text.endsWith("\n") ? "\n" : "";
1001
+ const value = text.slice(key.length + 2, suffix ? -1 : undefined);
1002
+ return `${key}: ${formatStorageValue(key, value)}${suffix}`;
1003
+ }
1004
+ // ponytail: native all-entry/raw-batch text does not escape multiline values.
1005
+ // Format heuristics cannot separate lookalike entries; use structured JSON for sensitive storage.
1006
+ return text.replace(/^([^\r\n]*?): ([^\r\n]*)/gm, (_line, key, value) => `${key}: ${formatStorageValue(key, value)}`);
1007
+ }
1008
+ export function redactPresentationData(commandInfo, data, stdin) {
1009
+ if (typeof data === "string")
1010
+ return redactSensitiveText(redactNativeContextText(commandInfo, data, stdin));
978
1011
  if (commandInfo.command === "cookies")
979
1012
  return redactStatefulValues(data, new Set(["value"]));
980
1013
  if (commandInfo.command === "storage")
@@ -1,4 +1,5 @@
1
1
  import { isRecord } from "../../parsing.js";
2
+ import { getNativeTabContinuationGuidance } from "../../read-confirmation.js";
2
3
  import { detectConfirmationRequired } from "../confirmation.js";
3
4
  import { formatRawSnapshotText, formatSnapshotSummary } from "../snapshot.js";
4
5
  import { getPageSummary, omitUpstreamLifecycle, redactModelFacingText, stringifyModelFacing } from "./common.js";
@@ -225,7 +226,10 @@ export function formatPresentationSummary(commandInfo, data, compiledSemanticAct
225
226
  export function formatPresentationContentText(commandInfo, data, compiledSemanticAction) {
226
227
  const confirmationRequired = detectConfirmationRequired(data);
227
228
  if (confirmationRequired)
228
- return formatConfirmationRequiredText(confirmationRequired);
229
+ return [
230
+ formatConfirmationRequiredText(confirmationRequired),
231
+ getNativeTabContinuationGuidance(commandInfo.commandTokens ?? [], data),
232
+ ].filter(Boolean).join("\n\n");
229
233
  const presenterText = commandInfo.command ? COMMAND_PRESENTERS[commandInfo.command]?.text?.(commandInfo, data) : undefined;
230
234
  if (presenterText)
231
235
  return presenterText;
@@ -53,6 +53,19 @@ export async function buildToolPresentation(options) {
53
53
  const { args, artifactManifest, artifactRequest, commandInfo, compiledSemanticAction, cwd, envelope, errorText, namespace, networkRouteDiagnostics, networkRoutes, persistentArtifactStore, sessionName, } = options;
54
54
  const commandInfoWithTokens = commandInfo.commandTokens || !args ? commandInfo : { ...commandInfo, commandTokens: extractUpstreamCommandTokens(args) };
55
55
  const presentationCommandInfo = resolvePresentationCommandInfo(commandInfoWithTokens, compiledSemanticAction);
56
+ if (options.textOutput) {
57
+ const text = String(redactPresentationData(commandInfoWithTokens, typeof envelope?.data === "string" ? envelope.data : "", options.stdin));
58
+ const failure = errorText ? buildErrorPresentation({ args, commandInfo, errorText, sessionName }) : undefined;
59
+ const presentation = {
60
+ ...failure,
61
+ content: options.modelVisible === false ? [] : [{ type: "text", text: [errorText, text].filter(Boolean).join("\n\n") }],
62
+ data: text,
63
+ summary: failure?.summary ?? (text.split("\n", 1)[0] || "Native text command completed."),
64
+ };
65
+ return sanitizeModelFacingPresentation(options.modelVisible === false ? presentation : await compactLargePresentationOutput({
66
+ artifactManifest, commandInfo, data: presentation.data, persistentArtifactStore, presentation,
67
+ }));
68
+ }
56
69
  const recordingCommand = commandInfo.command === "record";
57
70
  const recordingBatch = commandInfo.command === "batch" && isAgentBrowserBatchResultArray(envelope?.data)
58
71
  && envelope.data.some((row) => row.command?.[0] === "record");
@@ -66,6 +79,7 @@ export async function buildToolPresentation(options) {
66
79
  data = { ...data, piCleanupOwnership: options.piCleanupOwnership ?? "unknown" };
67
80
  }
68
81
  const readConfirmation = nextReadConfirmation({ commandTokens: commandInfoWithTokens.commandTokens ?? [], data, namespace, sessionName: sessionName ?? "default", succeeded: envelope?.success !== false });
82
+ const confirmationRequired = detectConfirmationRequired(data);
69
83
  const presentationData = commandInfo.command === "batch" && isAgentBrowserBatchResultArray(data)
70
84
  ? redactBatchSpillData(data)
71
85
  : redactPresentationData(commandInfoWithTokens, data);
@@ -93,7 +107,7 @@ export async function buildToolPresentation(options) {
93
107
  summary,
94
108
  });
95
109
  }
96
- else if (options.modelVisible !== false && commandInfo.command === "snapshot" && isRecord(data)) {
110
+ else if (options.modelVisible !== false && commandInfo.command === "snapshot" && isRecord(data) && !confirmationRequired) {
97
111
  presentation = await buildSnapshotPresentation(data, persistentArtifactStore, artifactManifest);
98
112
  }
99
113
  else {
@@ -160,7 +174,6 @@ export async function buildToolPresentation(options) {
160
174
  secondaryPaths: presentationWithManifest.fullOutputPaths,
161
175
  }));
162
176
  presentationWithManifest.artifactVerification = buildArtifactVerificationSummary(artifacts, presentationWithManifest.artifactManifest, currentSpillPaths) ?? presentationWithManifest.artifactVerification;
163
- const confirmationRequired = detectConfirmationRequired(data);
164
177
  const missingArtifactFailureText = formatMissingArtifactFailureText(presentationWithManifest.artifacts);
165
178
  if (!errorText && missingArtifactFailureText && hasMissingFileArtifact(presentationWithManifest.artifacts)) {
166
179
  presentationWithManifest.resultCategory = "failure";
@@ -74,8 +74,8 @@ function getFindVisibleRefFallbackTarget(args, options = {}) {
74
74
  export function getVisibleRefFallbackTarget(options) {
75
75
  return getFindVisibleRefFallbackTarget(options.commandTokens, { allowLeadingDashFillText: true }) ?? (options.compiledSemanticAction ? getFindVisibleRefFallbackTarget(options.compiledSemanticAction.args, { allowLeadingDashFillText: true }) : undefined);
76
76
  }
77
- function getVisibleRefFallbackCandidates(target, snapshotData) {
78
- const refs = getSnapshotRefRecord(snapshotData);
77
+ function getVisibleRefFallbackCandidates(target, snapshotData, refSnapshot) {
78
+ const refs = refSnapshot?.refs ?? getSnapshotRefRecord(snapshotData);
79
79
  if (!refs)
80
80
  return [];
81
81
  const snapshotLineByRef = getSnapshotLineTextByRef(snapshotData);
@@ -85,7 +85,7 @@ function getVisibleRefFallbackCandidates(target, snapshotData) {
85
85
  if (!/^e\d+$/.test(ref) || !isRecord(entry))
86
86
  return [];
87
87
  const snapshotLine = snapshotLineByRef.get(ref);
88
- const editableEvidence = getEditableRefEvidence({ ref: entry, text: snapshotLine });
88
+ const editableEvidence = refSnapshot ? refSnapshot.refs?.[ref]?.isEditable : getEditableRefEvidence({ ref: entry, text: snapshotLine });
89
89
  const role = getSnapshotRefRole(entry, editableEvidence);
90
90
  const name = typeof entry.name === "string" ? entry.name : undefined;
91
91
  if (!role || !name || !roleOrder.includes(role.toLowerCase()) || normalizeSemanticActionAccessibleName(name) !== targetName)
@@ -132,7 +132,7 @@ export function resolveVisibleRefActionFromSnapshot(options) {
132
132
  const target = getFindVisibleRefFallbackTarget(options.compiledAction.args, { allowLeadingDashFillText: true });
133
133
  if (!target)
134
134
  return undefined;
135
- const snapshot = extractRefSnapshotFromData(options.snapshotData);
135
+ const snapshot = options.refSnapshot ?? extractRefSnapshotFromData(options.snapshotData);
136
136
  if (!snapshot)
137
137
  return undefined;
138
138
  const selectOptionValues = options.compiledAction.values && options.compiledAction.values.length > 0
@@ -141,7 +141,7 @@ export function resolveVisibleRefActionFromSnapshot(options) {
141
141
  const effectiveTarget = target.action === "select" && selectOptionValues
142
142
  ? { ...target, optionValues: selectOptionValues }
143
143
  : target;
144
- const candidates = getVisibleRefFallbackCandidates(effectiveTarget, options.snapshotData);
144
+ const candidates = getVisibleRefFallbackCandidates(effectiveTarget, options.snapshotData, options.refSnapshot);
145
145
  if (effectiveTarget.action === "fill") {
146
146
  if (!options.allowFill || candidates.length !== 1 || effectiveTarget.text === undefined)
147
147
  return undefined;
@@ -157,6 +157,10 @@ function createBalancedJsonEndFinder(text) {
157
157
  };
158
158
  }
159
159
  function redactSerializedJson(text) {
160
+ if (text === "[REDACTED]")
161
+ return undefined;
162
+ if (!/^\s*[\[{"\d\-tfn]/.test(text))
163
+ return undefined;
160
164
  // Validate grammar only; rebuilding parsed values loses duplicates and numeric spelling.
161
165
  try {
162
166
  JSON.parse(text);
@@ -878,7 +882,7 @@ export function buildExecutionPlan(args, options) {
878
882
  const argvDescriptor = parseArgvDescriptor(args);
879
883
  const commandInfo = argvDescriptor.commandInfo;
880
884
  const commandNeedsManagedSession = !plainTextInspection && !options.browserIndependentReadConfirmation && needsManagedSession(argvDescriptor, options.stdin);
881
- const effectiveArgs = plainTextInspection ? [...args] : args.includes("--json") ? [] : ["--json"];
885
+ const effectiveArgs = plainTextInspection ? [...args] : getBooleanFlagValue(args, "--json") !== undefined ? [] : ["--json"];
882
886
  let namespace = explicitNamespacePresent ? explicitNamespace ?? "" : undefined;
883
887
  if (plainTextInspection) {
884
888
  return {
@@ -4,7 +4,8 @@ import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNa
4
4
  import { getBrowserRecord, snapshotFromDefinition } from "./browser-transcript.js";
5
5
  import { isCloseCommand, isOpenNavigationCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWebMcpPageMutationCommand, isWindowOrDiffPageTransitionCommand } from "./command-taxonomy.js";
6
6
  import { isRecord } from "./parsing.js";
7
- import { findReadConfirmation as findPendingReadConfirmation, parseReadConfirmation } from "./read-confirmation.js";
7
+ import { detectConfirmationRequired } from "./results/confirmation.js";
8
+ import { findReadConfirmation as findPendingReadConfirmation, isSuccessfulNativeConfirmedClose, parseReadConfirmation } from "./read-confirmation.js";
8
9
  import { getEditableRefEvidence } from "./results/editable-ref-evidence.js";
9
10
  import { enrichSnapshotRefEntries, getFullSnapshotData, getSnapshotRefEntries } from "./results/snapshot-refs.js";
10
11
  import { parseSnapshotLines } from "./results/snapshot-segments.js";
@@ -81,6 +82,10 @@ function extractBatchResultCommand(item) {
81
82
  }
82
83
  export function extractSessionTabTargetFromCommandData(commandTokens, data) {
83
84
  const [command, subcommand] = commandTokens;
85
+ if (command === "confirm" && isRecord(data) && data.confirmed === true && ["navigate", "url"].includes(String(data.action))
86
+ && isRecord(data.result) && data.result.success === true && !detectConfirmationRequired(data)) {
87
+ return extractSessionTabTargetFromData(data.result.data);
88
+ }
84
89
  if (command === "get" && subcommand === "url") {
85
90
  return normalizeSessionTabTarget({ url: extractStringResultField(data, "url") ?? extractStringResultField(data, "result") });
86
91
  }
@@ -93,7 +98,7 @@ export function extractSessionTabTargetFromBatchResults(data) {
93
98
  let currentTarget;
94
99
  let pendingTitle;
95
100
  for (const item of data) {
96
- if (!isRecord(item))
101
+ if (!isRecord(item) || detectConfirmationRequired(item.result))
97
102
  continue;
98
103
  // Only this row's native identity can describe the active tab after it ran.
99
104
  if (currentTarget?.targetId)
@@ -108,7 +113,7 @@ export function extractSessionTabTargetFromBatchResults(data) {
108
113
  if (item.success === false)
109
114
  continue;
110
115
  const result = item.result;
111
- if (isCloseCommand(name)) {
116
+ if (isCloseCommand(name) || isSuccessfulNativeConfirmedClose(commandTokens, result)) {
112
117
  currentTarget = undefined;
113
118
  pendingTitle = undefined;
114
119
  continue;
@@ -138,9 +143,7 @@ export function deriveSessionTabTarget(options) {
138
143
  if (isCloseCommand(options.command)) {
139
144
  return undefined;
140
145
  }
141
- const commandDataTarget = isReadOnlyDiagnosticSessionTargetCommand(options.command, options.subcommand)
142
- ? undefined
143
- : extractSessionTabTargetFromData(options.data);
146
+ const commandDataTarget = extractSessionTabTargetFromCommandData([options.command, options.subcommand].filter((token) => token !== undefined), options.data);
144
147
  const observedTarget = normalizeSessionTabTarget(options.navigationSummary)
145
148
  ?? extractSessionTabTargetFromBatchResults(options.data)
146
149
  ?? commandDataTarget;
@@ -163,6 +166,8 @@ function extractRefSnapshotRefs(data) {
163
166
  return Object.keys(refs).length > 0 ? refs : undefined;
164
167
  }
165
168
  export function extractRefSnapshotFromData(value) {
169
+ if (detectConfirmationRequired(value))
170
+ return undefined;
166
171
  const data = getFullSnapshotData(value);
167
172
  if (!data)
168
173
  return undefined;
@@ -214,7 +219,7 @@ export function extractLatestRefSnapshotStateFromBatchResults(data) {
214
219
  continue;
215
220
  const commandTokens = extractUpstreamCommandTokens(extractBatchResultCommand(item));
216
221
  const [name] = commandTokens;
217
- if (item.success !== false && isCloseCommand(name)) {
222
+ if (item.success !== false && !detectConfirmationRequired(item.result) && (isCloseCommand(name) || isSuccessfulNativeConfirmedClose(commandTokens, item.result))) {
218
223
  latestState = undefined;
219
224
  continue;
220
225
  }
@@ -258,6 +263,7 @@ export function getSessionPageStateKey(sessionName, namespace) {
258
263
  return sessionName ? getAgentBrowserSessionIdentityKey(sessionName, namespace) : undefined;
259
264
  }
260
265
  export class SessionPageState {
266
+ confirmActions = new Map();
261
267
  readConfirmations = new Map();
262
268
  refSnapshotInvalidations = new Map();
263
269
  refSnapshots = new Map();
@@ -281,6 +287,7 @@ export class SessionPageState {
281
287
  /** A command operates on its admitted state while the committed view is unavailable. */
282
288
  fork() {
283
289
  const state = new SessionPageState();
290
+ state.confirmActions = new Map(this.confirmActions);
284
291
  state.readConfirmations = new Map(this.readConfirmations);
285
292
  state.refSnapshotInvalidations = new Map(this.refSnapshotInvalidations);
286
293
  state.refSnapshots = new Map(this.refSnapshots);
@@ -293,7 +300,7 @@ export class SessionPageState {
293
300
  return state;
294
301
  }
295
302
  views() {
296
- return new Map([...new Set([...this.tabTargets.keys(), ...this.tabTargetUnknownOrders.keys(), ...this.refSnapshots.keys(), ...this.refSnapshotInvalidations.keys()])].map(key => [key, this.get(key)]));
303
+ return new Map([...new Set([...this.confirmActions.keys(), ...this.tabTargets.keys(), ...this.tabTargetUnknownOrders.keys(), ...this.refSnapshots.keys(), ...this.refSnapshotInvalidations.keys()])].map(key => [key, this.get(key)]));
297
304
  }
298
305
  /** The same reducer commits live observations and replays selected journal envelopes. */
299
306
  applyBrowserRecord({ event, snapshot }) {
@@ -301,12 +308,6 @@ export class SessionPageState {
301
308
  const confirmation = parseReadConfirmation(event.state.readConfirmation);
302
309
  for (const page of event.pages ?? []) {
303
310
  const key = page.key;
304
- if (event.phase === "begin") {
305
- this.pending.set(key, { operationId: event.operationId, snapshot: this.refSnapshots.get(key) ?? this.pending.get(key)?.snapshot });
306
- this.markTabTargetUnknown({ sessionName: key, update });
307
- this.applyRefSnapshotInvalidation({ sessionName: key, update, invalidation: buildPageTransitionRefSnapshotInvalidation("This browser operation has no persisted finish. Inspect the current URL and take a fresh snapshot before using refs; changes may already have happened.") });
308
- continue;
309
- }
310
311
  if (page.clear) {
311
312
  this.clearSession(key);
312
313
  continue;
@@ -314,6 +315,14 @@ export class SessionPageState {
314
315
  const pending = this.pending.get(key);
315
316
  if (event.phase === "finish" && pending && pending.operationId !== event.operationId)
316
317
  continue;
318
+ if (page.confirmActions !== undefined)
319
+ this.setConfirmActions(key, page.confirmActions ?? undefined);
320
+ if (event.phase === "begin") {
321
+ this.pending.set(key, { operationId: event.operationId, snapshot: this.refSnapshots.get(key) ?? this.pending.get(key)?.snapshot });
322
+ this.markTabTargetUnknown({ sessionName: key, update });
323
+ this.applyRefSnapshotInvalidation({ sessionName: key, update, invalidation: buildPageTransitionRefSnapshotInvalidation("This browser operation has no persisted finish. Inspect the current URL and take a fresh snapshot before using refs; changes may already have happened.") });
324
+ continue;
325
+ }
317
326
  if (page.unknown || page.refs.kind === "unknown") {
318
327
  this.markTabTargetUnknown({ sessionName: key, update });
319
328
  this.applyRefSnapshotInvalidation({ sessionName: key, update, invalidation: page.refs.kind === "unknown" && page.refs.invalidation
@@ -361,6 +370,7 @@ export class SessionPageState {
361
370
  return this.updateOrder;
362
371
  }
363
372
  reset() {
373
+ this.confirmActions.clear();
364
374
  this.readConfirmations.clear();
365
375
  this.pending.clear();
366
376
  this.nativeGenerations.clear();
@@ -375,6 +385,7 @@ export class SessionPageState {
375
385
  if (!sessionName)
376
386
  return {};
377
387
  return {
388
+ ...(this.confirmActions.has(sessionName) ? { confirmActions: this.confirmActions.get(sessionName) } : {}),
378
389
  pinningReason: this.tabPinningReasons.get(sessionName),
379
390
  ...(this.tabTargets.get(sessionName)?.reopenPending !== undefined ? { tabReopenPending: this.tabTargets.get(sessionName)?.reopenPending } : {}),
380
391
  refSnapshot: stripRefSnapshotOrder(this.refSnapshots.get(sessionName)),
@@ -383,8 +394,14 @@ export class SessionPageState {
383
394
  tabTarget: this.tabTargets.get(sessionName)?.target,
384
395
  };
385
396
  }
386
- findReadConfirmation(args, namespace) {
387
- return findPendingReadConfirmation(args, [...this.readConfirmations.values()].map(entry => entry.value), namespace);
397
+ findReadConfirmation(args, namespace, stdin) {
398
+ return findPendingReadConfirmation(args, [...this.readConfirmations.values()].map(entry => entry.value), namespace, stdin);
399
+ }
400
+ setConfirmActions(sessionName, value) {
401
+ if (value !== undefined)
402
+ this.confirmActions.set(sessionName, value);
403
+ else
404
+ this.confirmActions.delete(sessionName);
388
405
  }
389
406
  getReadConfirmation(sessionKey) {
390
407
  return this.readConfirmations.get(sessionKey)?.value;
@@ -455,6 +472,7 @@ export class SessionPageState {
455
472
  return { ...this.get(options.sessionName), applied: true };
456
473
  }
457
474
  clearSession(sessionName) {
475
+ this.confirmActions.delete(sessionName);
458
476
  this.pending.delete(sessionName);
459
477
  this.nativeGenerations.delete(sessionName);
460
478
  this.readConfirmations.delete(sessionName);
@@ -466,6 +484,7 @@ export class SessionPageState {
466
484
  }
467
485
  clearNamespace(namespace) {
468
486
  const sessionKeys = new Set([
487
+ ...this.confirmActions.keys(),
469
488
  ...this.readConfirmations.keys(),
470
489
  ...this.refSnapshotInvalidations.keys(),
471
490
  ...this.refSnapshots.keys(),