pi-agent-browser-native 0.2.22 → 0.2.24

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.
@@ -78,7 +78,7 @@ Examples:
78
78
 
79
79
  - type: `string`
80
80
  - optional
81
- - raw stdin for `eval --stdin` and `batch`
81
+ - raw stdin for `eval --stdin`, `batch`, and `auth save --password-stdin`
82
82
  - rejected before launch for any other command/stdin combination, including commands such as `click`, `snapshot`, or `open`
83
83
 
84
84
  Examples:
@@ -91,6 +91,10 @@ Examples:
91
91
  { "args": ["batch"], "stdin": "[[\"open\",\"https://example.com\"],[\"snapshot\",\"-i\"]]" }
92
92
  ```
93
93
 
94
+ ```json
95
+ { "args": ["auth", "save", "my-login", "--password-stdin"], "stdin": "password from the user-approved secret source" }
96
+ ```
97
+
94
98
  ### `sessionMode`
95
99
 
96
100
  - type: `"auto" | "fresh"`
@@ -190,9 +194,12 @@ For oversized snapshots and other oversized tool outputs, details should switch
190
194
 
191
195
  "Rendering" here means how results appear inside `pi`, not embedding a browser UI.
192
196
 
197
+ The TUI renderer is user-facing only. It may compact or colorize what the human sees in the Pi transcript, but it must not further truncate, summarize, or remove the model-facing `content` returned by the tool. Use the existing `details.fullOutputPath` / spill-file contracts for content that is too large for the model.
198
+
193
199
  Worth doing in v1:
194
200
  - screenshots → saved-path summary, visible artifact metadata, `details.artifacts` metadata, and inline image attachment when safe; screenshot paths that upstream would treat ambiguously, such as `.dogfood/run/foo.png`, are normalized to absolute paths before launch and repaired from upstream temp output when possible
195
201
  - file artifacts such as PDFs, downloads, `wait --download` files, traces, CPU profiles, completed WebM recordings, and path-bearing HAR captures → concise saved-path summaries plus metadata in `details.artifacts` and bounded recent metadata in `details.artifactManifest`; `record start` reports recording lifecycle state and the future output path without adding a missing manifest entry; direct saved-file workflows also expose `details.savedFilePath` / `details.savedFile`; large or binary artifacts are not inlined into model context; the recent manifest cap can age out explicit-file metadata but does not remove explicit saved files from disk
202
+ - TUI display → custom `agent_browser` call/result rendering with colorized command/output text and a built-in-style collapsed view for long visible output; `ctrl+o` expansion reveals the full rendered tool result without changing the model-facing content
196
203
  - snapshots → origin + ref count + main-content-first compact preview, with the raw snapshot spill path printed directly in content and kept in `details.fullOutputPath` plus `details.artifactManifest` when the inline result would otherwise be too large
197
204
  - oversized generic outputs such as large `eval --stdin` payloads → compact preview plus the actual spill file path instead of dumping the whole payload into model context
198
205
  - extraction-style commands like `eval --stdin` and `get title` → scalar-first text with lightweight origin context when available
@@ -222,7 +229,7 @@ If `agent-browser` is not on `PATH`, fail with a message that:
222
229
  - reconstruct the current extension-managed session and latest `artifactManifest` from persisted tool details on resume/reload so later default calls keep following the active managed browser and can continue reporting artifact retention state
223
230
  - when an unnamed `sessionMode: "fresh"` launch succeeds, make it the new extension-managed session so later default calls keep using it
224
231
  - if that unnamed fresh launch replaced an already-active managed session, best-effort close the old managed session after the switch succeeds
225
- - treat explicit caller-provided `--session` choices as user-managed
232
+ - treat explicit caller-provided `--session` choices as user-managed; `--session` isolates a live browser session but is not a persisted tab/auth restore mechanism after `close`, so use `--profile`, `--session-name`, or `--state` when persisted auth/tab state is required
226
233
  - pass explicit `--profile` straight through to upstream `agent-browser`; no profile-cloning or isolation layer is added in v1
227
234
  <!-- agent-browser-playbook:start wrapper-tab-recovery -->
228
235
  <!-- Generated from extensions/agent-browser/lib/playbook.ts. Run `npm run docs -- playbook write` to update. -->
@@ -10,7 +10,15 @@ import { copyFile, mkdir, readFile, rm, stat } from "node:fs/promises";
10
10
  import { dirname, extname, isAbsolute, join, resolve } from "node:path";
11
11
 
12
12
  import { StringEnum } from "@earendil-works/pi-ai";
13
- import { isToolCallEventType, type AgentToolResult, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
+ import {
14
+ highlightCode,
15
+ isToolCallEventType,
16
+ keyHint,
17
+ type AgentToolResult,
18
+ type ExtensionAPI,
19
+ type Theme,
20
+ } from "@earendil-works/pi-coding-agent";
21
+ import { Text } from "@earendil-works/pi-tui";
14
22
  import { Type } from "typebox";
15
23
 
16
24
  import {
@@ -73,7 +81,7 @@ const AGENT_BROWSER_PARAMS = Type.Object({
73
81
  description: "Exact agent-browser CLI arguments, excluding the binary name and any shell operators.",
74
82
  minItems: 1,
75
83
  }),
76
- stdin: Type.Optional(Type.String({ description: "Optional raw stdin content; only supported for batch and eval --stdin." })),
84
+ stdin: Type.Optional(Type.String({ description: "Optional raw stdin content; only supported for batch, eval --stdin, and auth save --password-stdin." })),
77
85
  sessionMode: Type.Optional(
78
86
  StringEnum(["auto", "fresh"] as const, {
79
87
  description:
@@ -97,6 +105,154 @@ function buildInvocationPreview(effectiveArgs: string[]): string {
97
105
  return preview.length > 120 ? `${preview.slice(0, 117)}...` : preview;
98
106
  }
99
107
 
108
+ const TUI_COLLAPSED_OUTPUT_MAX_LINES = 10;
109
+ const TUI_INVOCATION_PREVIEW_MAX_CHARS = 120;
110
+ const ANSI_CONTROL_SEQUENCE_PATTERN = /\x1B(?:\][^\x07\x1B]*(?:\x07|\x1B\\)|\[[0-?]*[ -/]*[@-~]|P[^\x1B]*(?:\x1B\\)|_[^\x1B]*(?:\x1B\\)|\^[^\x1B]*(?:\x1B\\)|[@-Z\\-_])/g;
111
+ const UNSAFE_DISPLAY_CONTROL_PATTERN = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x80-\x9F]/g;
112
+
113
+ function sanitizeDisplayText(text: string): string {
114
+ return text
115
+ .replace(ANSI_CONTROL_SEQUENCE_PATTERN, "")
116
+ .replace(/\r/g, "")
117
+ .replace(UNSAFE_DISPLAY_CONTROL_PATTERN, "�");
118
+ }
119
+
120
+ function replaceTabsForDisplay(text: string): string {
121
+ return text.replaceAll("\t", " ");
122
+ }
123
+
124
+ function trimTrailingBlankLines(lines: string[]): string[] {
125
+ let end = lines.length;
126
+ while (end > 0 && lines[end - 1].trim().length === 0) {
127
+ end -= 1;
128
+ }
129
+ return lines.slice(0, end);
130
+ }
131
+
132
+ function isJsonDocumentText(text: string): boolean {
133
+ const trimmed = text.trim();
134
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
135
+ return false;
136
+ }
137
+ try {
138
+ JSON.parse(trimmed);
139
+ return true;
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ function getPrimaryTextContent(result: AgentToolResult<unknown>): string {
146
+ const textContent = result.content.find((item) => item.type === "text");
147
+ return textContent?.type === "text" ? textContent.text : "";
148
+ }
149
+
150
+ function colorizeToolOutputLines(text: string, theme: Theme, isError: boolean): string[] {
151
+ const normalizedLines = trimTrailingBlankLines(replaceTabsForDisplay(sanitizeDisplayText(text)).split("\n"));
152
+ const normalizedText = normalizedLines.join("\n");
153
+ if (normalizedText.length === 0) {
154
+ return [];
155
+ }
156
+ if (isJsonDocumentText(normalizedText)) {
157
+ return highlightCode(normalizedText, "json");
158
+ }
159
+ return normalizedLines.map((line) => {
160
+ if (line.length === 0) {
161
+ return "";
162
+ }
163
+ return isError ? theme.fg("error", line) : theme.fg("toolOutput", line);
164
+ });
165
+ }
166
+
167
+ function formatExpandHint(theme: Theme): string {
168
+ try {
169
+ return keyHint("app.tools.expand", "to expand");
170
+ } catch {
171
+ return `${theme.fg("dim", "ctrl+o")} ${theme.fg("muted", "to expand")}`;
172
+ }
173
+ }
174
+
175
+ function formatVisualTruncationNotice(remainingLines: number, totalLines: number, theme: Theme): string {
176
+ return `${theme.fg("muted", `... (${remainingLines} more lines, ${totalLines} total, `)}${formatExpandHint(theme)}${theme.fg("muted", ")")}`;
177
+ }
178
+
179
+ function formatAgentBrowserRenderCall(args: unknown, theme: Theme): string {
180
+ const input = isRecord(args) ? args : {};
181
+ const rawArgs = Array.isArray(input.args) ? input.args.filter((value): value is string => typeof value === "string") : [];
182
+ const redactedArgs = redactInvocationArgs(rawArgs);
183
+ const invocation = sanitizeDisplayText(redactedArgs.join(" ")).replace(/\s+/g, " ").trim();
184
+ const invocationPreview =
185
+ invocation.length > TUI_INVOCATION_PREVIEW_MAX_CHARS
186
+ ? `${invocation.slice(0, TUI_INVOCATION_PREVIEW_MAX_CHARS - 3)}...`
187
+ : invocation;
188
+ let text = theme.fg("toolTitle", theme.bold("agent_browser"));
189
+ if (invocationPreview.length > 0) {
190
+ text += ` ${theme.fg("accent", invocationPreview)}`;
191
+ }
192
+ if (input.sessionMode === "fresh") {
193
+ text += theme.fg("dim", " sessionMode=fresh");
194
+ }
195
+ if (typeof input.stdin === "string") {
196
+ text += theme.fg("dim", " + stdin");
197
+ }
198
+ return text;
199
+ }
200
+
201
+ function formatAgentBrowserRenderResult(
202
+ result: AgentToolResult<unknown>,
203
+ options: { expanded: boolean; isPartial: boolean },
204
+ theme: Theme,
205
+ isError: boolean,
206
+ ): string {
207
+ if (options.isPartial) {
208
+ return theme.fg("warning", "Running agent-browser...");
209
+ }
210
+
211
+ const outputText = getPrimaryTextContent(result);
212
+ const outputLines = colorizeToolOutputLines(outputText, theme, isError);
213
+ if (outputLines.length === 0) {
214
+ const details = isRecord(result.details) ? result.details : undefined;
215
+ const rawSummary = typeof details?.summary === "string" ? details.summary : isError ? "agent-browser failed" : "Done";
216
+ const sanitizedSummary = sanitizeDisplayText(rawSummary).trim();
217
+ const summary = sanitizedSummary.length > 0 ? sanitizedSummary : isError ? "agent-browser failed" : "Done";
218
+ return isError ? theme.fg("error", summary) : theme.fg("success", summary);
219
+ }
220
+
221
+ return `\n${outputLines.join("\n")}`;
222
+ }
223
+
224
+ class AgentBrowserResultComponent {
225
+ private expanded = false;
226
+ private theme: Theme | undefined;
227
+ private readonly text = new Text("", 0, 0);
228
+
229
+ setState(value: string, expanded: boolean, theme: Theme): void {
230
+ this.text.setText(value);
231
+ this.expanded = expanded;
232
+ this.theme = theme;
233
+ }
234
+
235
+ render(width: number): string[] {
236
+ const lines = this.text.render(width);
237
+ if (this.expanded || lines.length <= TUI_COLLAPSED_OUTPUT_MAX_LINES) {
238
+ return lines;
239
+ }
240
+ const theme = this.theme;
241
+ if (!theme) {
242
+ return lines.slice(0, TUI_COLLAPSED_OUTPUT_MAX_LINES);
243
+ }
244
+ const hiddenLineCount = lines.length - TUI_COLLAPSED_OUTPUT_MAX_LINES;
245
+ return [
246
+ ...lines.slice(0, TUI_COLLAPSED_OUTPUT_MAX_LINES),
247
+ formatVisualTruncationNotice(hiddenLineCount, lines.length, theme),
248
+ ];
249
+ }
250
+
251
+ invalidate(): void {
252
+ this.text.invalidate();
253
+ }
254
+ }
255
+
100
256
  function buildWrapperRecoveryHint(options: {
101
257
  pinnedBatchUnwrapMode?: PinnedBatchUnwrapMode;
102
258
  sessionTabCorrection?: OpenResultTabCorrection;
@@ -936,6 +1092,45 @@ function restoreArtifactManifestFromBranch(branch: unknown[]): SessionArtifactMa
936
1092
  return restoredManifest;
937
1093
  }
938
1094
 
1095
+ function isPasswordStdinAuthSave(options: { command?: string; commandTokens: string[] }): boolean {
1096
+ return options.command === "auth" && options.commandTokens[1] === "save" && options.commandTokens.includes("--password-stdin");
1097
+ }
1098
+
1099
+ function getExactSensitiveStdinValues(options: { command?: string; commandTokens: string[]; stdin?: string }): string[] {
1100
+ if (options.stdin === undefined || !isPasswordStdinAuthSave(options)) {
1101
+ return [];
1102
+ }
1103
+ return [...new Set([options.stdin, options.stdin.trimEnd(), options.stdin.trim()].filter((value) => value.length > 0))];
1104
+ }
1105
+
1106
+ function redactExactSensitiveText(text: string, sensitiveValues: string[]): string {
1107
+ let redacted = text;
1108
+ for (const value of sensitiveValues) {
1109
+ redacted = redacted.split(value).join("[REDACTED]");
1110
+ }
1111
+ return redacted;
1112
+ }
1113
+
1114
+ function redactExactSensitiveValue(value: unknown, sensitiveValues: string[]): unknown {
1115
+ if (sensitiveValues.length === 0) {
1116
+ return value;
1117
+ }
1118
+ if (typeof value === "string") {
1119
+ return redactExactSensitiveText(value, sensitiveValues);
1120
+ }
1121
+ if (Array.isArray(value)) {
1122
+ return value.map((item) => redactExactSensitiveValue(item, sensitiveValues));
1123
+ }
1124
+ if (!isRecord(value)) {
1125
+ return value;
1126
+ }
1127
+ return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, redactExactSensitiveValue(entryValue, sensitiveValues)]));
1128
+ }
1129
+
1130
+ function redactToolDetails(details: Record<string, unknown>, sensitiveValues: string[]): Record<string, unknown> {
1131
+ return redactSensitiveValue(redactExactSensitiveValue(details, sensitiveValues)) as Record<string, unknown>;
1132
+ }
1133
+
939
1134
  function validateStdinCommandContract(options: { command?: string; commandTokens: string[]; stdin?: string }): string | undefined {
940
1135
  if (options.stdin === undefined) {
941
1136
  return undefined;
@@ -946,8 +1141,11 @@ function validateStdinCommandContract(options: { command?: string; commandTokens
946
1141
  if (options.command === "eval" && options.commandTokens.includes("--stdin")) {
947
1142
  return undefined;
948
1143
  }
1144
+ if (isPasswordStdinAuthSave(options)) {
1145
+ return undefined;
1146
+ }
949
1147
  const commandLabel = options.command ? `\`${options.command}\`` : "the requested command";
950
- return `agent_browser stdin is only supported for \`batch\` and \`eval --stdin\`; remove stdin from ${commandLabel} or use one of those command forms.`;
1148
+ return `agent_browser stdin is only supported for \`batch\`, \`eval --stdin\`, and \`auth save --password-stdin\`; remove stdin from ${commandLabel} or use one of those command forms.`;
951
1149
  }
952
1150
 
953
1151
  function supportsPinnedStdinCommand(options: { command?: string; commandTokens: string[]; stdin?: string }): boolean {
@@ -1029,6 +1227,17 @@ function parseUserBatchStdin(stdin: string | undefined): { error?: string; steps
1029
1227
  }
1030
1228
  }
1031
1229
 
1230
+ function getStaleRefArgs(commandTokens: string[], stdin?: string): string[] {
1231
+ if (commandTokens[0] !== "batch" || stdin === undefined) {
1232
+ return commandTokens;
1233
+ }
1234
+ const parsed = parseUserBatchStdin(stdin);
1235
+ if (parsed.error || parsed.steps === undefined) {
1236
+ return commandTokens;
1237
+ }
1238
+ return parsed.steps.flatMap((step) => step);
1239
+ }
1240
+
1032
1241
  function buildPinnedBatchPlan(options: {
1033
1242
  command?: string;
1034
1243
  commandTokens: string[];
@@ -1293,6 +1502,7 @@ function getPersistentSessionArtifactStore(ctx: {
1293
1502
 
1294
1503
  async function preserveParseFailureOutput(options: {
1295
1504
  artifactManifest?: SessionArtifactManifest;
1505
+ exactSensitiveValues?: string[];
1296
1506
  persistentArtifactStore?: PersistentSessionArtifactStore;
1297
1507
  stdoutSpillPath?: string;
1298
1508
  }): Promise<{
@@ -1306,7 +1516,7 @@ async function preserveParseFailureOutput(options: {
1306
1516
  }
1307
1517
 
1308
1518
  try {
1309
- const rawOutput = await readFile(options.stdoutSpillPath);
1519
+ const rawOutput = redactExactSensitiveText(await readFile(options.stdoutSpillPath, "utf8"), options.exactSensitiveValues ?? []);
1310
1520
  const nowMs = Date.now();
1311
1521
  let evictedArtifacts: PersistentSessionArtifactEviction[] = [];
1312
1522
  let fullOutputPath: string;
@@ -1500,6 +1710,18 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1500
1710
  "Browse websites, read live docs, click and fill pages, extract browser content, take screenshots, and automate real web workflows.",
1501
1711
  promptGuidelines: toolPromptGuidelines,
1502
1712
  parameters: AGENT_BROWSER_PARAMS,
1713
+ renderCall(args, theme, context) {
1714
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
1715
+ text.setText(formatAgentBrowserRenderCall(args, theme));
1716
+ return text;
1717
+ },
1718
+ renderResult(result, options, theme, context) {
1719
+ const component = context.lastComponent instanceof AgentBrowserResultComponent
1720
+ ? context.lastComponent
1721
+ : new AgentBrowserResultComponent();
1722
+ component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
1723
+ return component;
1724
+ },
1503
1725
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
1504
1726
  const redactedArgs = redactInvocationArgs(params.args);
1505
1727
  const validationError = validateToolArgs(params.args) ?? getBatchAnnotateValidationError(params.args, params.stdin);
@@ -1546,6 +1768,11 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1546
1768
  }
1547
1769
 
1548
1770
  const commandTokens = extractCommandTokens(preparedArgs.args);
1771
+ const exactSensitiveValues = getExactSensitiveStdinValues({
1772
+ command: executionPlan.commandInfo.command,
1773
+ commandTokens,
1774
+ stdin: params.stdin,
1775
+ });
1549
1776
  const traceOwnerGuardMessage = getTraceOwnerGuardMessage({
1550
1777
  command: executionPlan.commandInfo.command,
1551
1778
  sessionName: executionPlan.sessionName,
@@ -1755,9 +1982,13 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1755
1982
  presentationEnvelope = repairedBatchScreenshots.envelope;
1756
1983
  const screenshotArtifactRequest = repairedScreenshot.request;
1757
1984
  const batchScreenshotArtifactRequests = repairedBatchScreenshots.requests;
1985
+ if (presentationEnvelope && exactSensitiveValues.length > 0) {
1986
+ presentationEnvelope = redactExactSensitiveValue(presentationEnvelope, exactSensitiveValues) as AgentBrowserEnvelope;
1987
+ }
1758
1988
  const parseFailureOutput = parseError
1759
1989
  ? await preserveParseFailureOutput({
1760
1990
  artifactManifest,
1991
+ exactSensitiveValues,
1761
1992
  persistentArtifactStore,
1762
1993
  stdoutSpillPath: processResult.stdoutSpillPath,
1763
1994
  })
@@ -1934,6 +2165,7 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1934
2165
  exitCode: processResult.exitCode,
1935
2166
  parseError,
1936
2167
  plainTextInspection,
2168
+ staleRefArgs: getStaleRefArgs(commandTokens, params.stdin),
1937
2169
  spawnError: processResult.spawnError,
1938
2170
  stderr: processResult.stderr,
1939
2171
  timedOut: processResult.timedOut,
@@ -2009,54 +2241,55 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
2009
2241
  contentWithSessionWarnings.unshift({ type: "text", text: warningText });
2010
2242
  }
2011
2243
  }
2012
- const redactedContent = contentWithSessionWarnings.map((item) =>
2013
- item.type === "text" && !(userRequestedJson && !plainTextInspection) ? { ...item, text: redactSensitiveText(item.text) } : item,
2014
- );
2244
+ const redactedContent = contentWithSessionWarnings.map((item) => {
2245
+ if (item.type !== "text") return item;
2246
+ const exactRedactedText = redactExactSensitiveText(item.text, exactSensitiveValues);
2247
+ return userRequestedJson && !plainTextInspection
2248
+ ? { ...item, text: exactRedactedText }
2249
+ : { ...item, text: redactSensitiveText(exactRedactedText) };
2250
+ });
2251
+ const details = {
2252
+ args: redactedArgs,
2253
+ artifactManifest: presentation.artifactManifest,
2254
+ artifactRetentionSummary: presentation.artifactRetentionSummary,
2255
+ artifacts: presentation.artifacts,
2256
+ batchFailure: presentation.batchFailure,
2257
+ batchSteps: presentation.batchSteps,
2258
+ command: executionPlan.commandInfo.command,
2259
+ compatibilityWorkaround,
2260
+ subcommand: executionPlan.commandInfo.subcommand,
2261
+ data: presentation.data,
2262
+ error: plainTextInspection ? undefined : presentationEnvelope?.error,
2263
+ inspection: plainTextInspection || undefined,
2264
+ navigationSummary,
2265
+ aboutBlankSessionMismatch,
2266
+ openResultTabCorrection,
2267
+ effectiveArgs: redactedProcessArgs,
2268
+ exitCode: processResult.exitCode,
2269
+ fullOutputPath: parseFailureOutput.fullOutputPath ?? presentation.fullOutputPath,
2270
+ fullOutputPaths: presentation.fullOutputPaths,
2271
+ fullOutputUnavailable: parseFailureOutput.fullOutputUnavailable,
2272
+ imagePath: presentation.imagePath,
2273
+ imagePaths: presentation.imagePaths,
2274
+ parseError: plainTextInspection ? undefined : parseError,
2275
+ savedFile: presentation.savedFile,
2276
+ savedFilePath: presentation.savedFilePath,
2277
+ sessionMode,
2278
+ sessionTabCorrection,
2279
+ sessionTabTarget: currentSessionTabTarget,
2280
+ ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
2281
+ sessionRecoveryHint: redactedRecoveryHint,
2282
+ startupScopedFlags: executionPlan.startupScopedFlags,
2283
+ stderr: processResult.stderr,
2284
+ stdout: plainTextInspection ? inspectionText ?? "" : parseSucceeded ? undefined : processResult.stdout,
2285
+ summary: presentation.summary,
2286
+ timedOut: processResult.timedOut || undefined,
2287
+ timeoutMs: processResult.timeoutMs,
2288
+ };
2015
2289
 
2016
2290
  return {
2017
2291
  content: redactedContent,
2018
- details: {
2019
- args: redactedArgs,
2020
- artifactManifest: redactSensitiveValue(presentation.artifactManifest),
2021
- artifactRetentionSummary: presentation.artifactRetentionSummary,
2022
- artifacts: redactSensitiveValue(presentation.artifacts),
2023
- batchFailure: redactSensitiveValue(presentation.batchFailure),
2024
- batchSteps: redactSensitiveValue(presentation.batchSteps),
2025
- command: executionPlan.commandInfo.command,
2026
- compatibilityWorkaround,
2027
- subcommand: executionPlan.commandInfo.subcommand,
2028
- data: redactSensitiveValue(presentation.data),
2029
- error: plainTextInspection ? undefined : redactSensitiveValue(presentationEnvelope?.error),
2030
- inspection: plainTextInspection || undefined,
2031
- navigationSummary: redactSensitiveValue(navigationSummary),
2032
- aboutBlankSessionMismatch: redactSensitiveValue(aboutBlankSessionMismatch),
2033
- openResultTabCorrection: redactSensitiveValue(openResultTabCorrection),
2034
- effectiveArgs: redactedProcessArgs,
2035
- exitCode: processResult.exitCode,
2036
- fullOutputPath: parseFailureOutput.fullOutputPath ?? presentation.fullOutputPath,
2037
- fullOutputPaths: presentation.fullOutputPaths,
2038
- fullOutputUnavailable: parseFailureOutput.fullOutputUnavailable,
2039
- imagePath: presentation.imagePath,
2040
- imagePaths: presentation.imagePaths,
2041
- parseError: plainTextInspection ? undefined : parseError,
2042
- savedFile: redactSensitiveValue(presentation.savedFile),
2043
- savedFilePath: presentation.savedFilePath ? redactSensitiveText(presentation.savedFilePath) : undefined,
2044
- sessionMode,
2045
- sessionTabCorrection: redactSensitiveValue(sessionTabCorrection),
2046
- sessionTabTarget: redactSensitiveValue(currentSessionTabTarget),
2047
- ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
2048
- sessionRecoveryHint: redactedRecoveryHint,
2049
- startupScopedFlags: executionPlan.startupScopedFlags,
2050
- stderr: processResult.stderr ? redactSensitiveText(processResult.stderr) : undefined,
2051
- stdout: plainTextInspection
2052
- ? redactSensitiveText(inspectionText ?? "")
2053
- : parseSucceeded
2054
- ? undefined
2055
- : redactSensitiveText(processResult.stdout),
2056
- summary: redactSensitiveText(presentation.summary),
2057
- timedOut: processResult.timedOut || undefined,
2058
- timeoutMs: processResult.timeoutMs,
2059
- },
2292
+ details: redactToolDetails(details, exactSensitiveValues),
2060
2293
  isError: !succeeded,
2061
2294
  };
2062
2295
  } finally {
@@ -3,7 +3,7 @@
3
3
  * Responsibilities: Define stable guidance bullets, native tool-call examples, and wrapper-behavior notes without importing runtime/browser process code.
4
4
  * Scope: Agent-facing documentation and prompt-guidance text only; command execution and wrapper state behavior live in runtime modules.
5
5
  * Usage: Imported by the extension entrypoint for promptGuidelines and by the documentation drift-check script for generated Markdown blocks.
6
- * Invariants/Assumptions: The native pi tool receives args after the agent-browser binary, stdin is only for batch/eval --stdin, and wrapper behavior documented here must match implemented behavior.
6
+ * Invariants/Assumptions: The native pi tool receives args after the agent-browser binary, stdin is only for batch/eval --stdin/auth save --password-stdin, and wrapper behavior documented here must match implemented behavior.
7
7
  */
8
8
 
9
9
  export const PROJECT_RULE_PROMPT =
@@ -14,9 +14,9 @@ export const TOOL_PROMPT_GUIDELINES_PREFIX = [
14
14
  ] as const;
15
15
 
16
16
  export const QUICK_START_GUIDELINES = [
17
- "Quick start mental model: args are the exact agent-browser CLI args after the binary; stdin is only for batch and eval --stdin, and other command/stdin combinations are rejected before launch; sessionMode=fresh switches the extension-managed pi-scoped session to a fresh upstream launch when you need new --profile, --session-name, --cdp, --state, --auto-connect, --init-script, or --enable state.",
17
+ "Quick start mental model: args are the exact agent-browser CLI args after the binary; stdin is only for batch, eval --stdin, and auth save --password-stdin, and other command/stdin combinations are rejected before launch; sessionMode=fresh switches the extension-managed pi-scoped session to a fresh upstream launch when you need new --profile, --session-name, --cdp, --state, --auto-connect, --init-script, or --enable state.",
18
18
  "Common first calls: { args: [\"open\", \"https://example.com\"] } then { args: [\"snapshot\", \"-i\"] }; after navigation, use { args: [\"click\", \"@e2\"] } then { args: [\"snapshot\", \"-i\"] }.",
19
- "Common advanced calls: { args: [\"batch\"], stdin: \"[[\\\"open\\\",\\\"https://example.com\\\"],[\\\"snapshot\\\",\\\"-i\\\"]]\" }, { args: [\"eval\", \"--stdin\"], stdin: \"document.title\" }, { args: [\"--profile\", \"Default\", \"open\", \"https://example.com/account\"], sessionMode: \"fresh\" }, and { args: [\"open\", \"--enable\", \"react-devtools\", \"https://example.com\"], sessionMode: \"fresh\" }.",
19
+ "Common advanced calls: { args: [\"batch\"], stdin: \"[[\\\"open\\\",\\\"https://example.com\\\"],[\\\"snapshot\\\",\\\"-i\\\"]]\" }, { args: [\"eval\", \"--stdin\"], stdin: \"document.title\" }, { args: [\"auth\", \"save\", \"name\", \"--password-stdin\"], stdin: \"<password from user-approved secret source>\" }, { args: [\"--profile\", \"Default\", \"open\", \"https://example.com/account\"], sessionMode: \"fresh\" }, and { args: [\"open\", \"--enable\", \"react-devtools\", \"https://example.com\"], sessionMode: \"fresh\" }.",
20
20
  "High-value command reference: download <selector> <path> saves a file triggered by a click; get title/url/text/html/value/attr/count reads page state; screenshot [path] captures an image; pdf <path> saves a PDF; tab list and tab <tab-id-or-label> inspect or recover the active tab; react tree/inspect/renders/suspense introspect React after --enable react-devtools; vitals [url] measures Core Web Vitals; pushstate <url> performs SPA navigation.",
21
21
  "For artifact-producing commands, read the visible artifact block for requested path, absolute path, existence, size, type, cwd, and session; details.artifacts contains the same machine-readable metadata. For annotated screenshots inside batch, put --annotate in top-level args (for example { args: [\"--annotate\", \"batch\"], stdin: \"[[\\\"screenshot\\\",\\\"/tmp/page.png\\\"]]\" }) rather than inside the screenshot step.",
22
22
  ] as const;
@@ -47,7 +47,7 @@ export const TOOL_PROMPT_GUIDELINES_SUFFIX = [
47
47
  "Prefer agent_browser over bash for opening sites, reading docs on the web, clicking, filling, screenshots, eval, and batch workflows.",
48
48
  "Do not fall back to osascript, AppleScript, or generic browser-driving bash commands when agent_browser can do the job.",
49
49
  "Pass exact agent-browser CLI arguments in args, excluding the binary name.",
50
- "Use stdin only for eval --stdin and batch instead of shell heredocs; other command/stdin combinations are rejected before launch.",
50
+ "Use stdin only for eval --stdin, batch, and auth save --password-stdin instead of shell heredocs or password args; other command/stdin combinations are rejected before launch.",
51
51
  "Let the extension-managed session handle the common path unless you explicitly need a fresh launch for upstream flags like --profile, --session-name, --cdp, --state, --auto-connect, --init-script, or --enable.",
52
52
  "Use sessionMode=fresh when switching from an existing implicit session to a new profile/debug/init-script launch without inventing a fixed explicit session name; later auto calls will follow that new session.",
53
53
  ] as const;
@@ -135,6 +135,17 @@ function buildUpstreamIpcReadTimeoutMessage(): string {
135
135
  ].join(" ");
136
136
  }
137
137
 
138
+ function maybeAppendStaleRefHint(message: string, args?: string[]): string {
139
+ const usedRef = args?.some((arg) => /^@e\d+\b/.test(arg)) ?? false;
140
+ if (!usedRef || !/could not locate element|element not found|no element/i.test(message)) {
141
+ return message;
142
+ }
143
+ return [
144
+ message,
145
+ "This @ref may be stale after navigation, scrolling, or a DOM update. Run `agent_browser` with `{ \"args\": [\"snapshot\", \"-i\"] }` again and retry with a current ref, or use a stable `find` locator.",
146
+ ].join("\n");
147
+ }
148
+
138
149
  export function getAgentBrowserErrorText(options: {
139
150
  aborted: boolean;
140
151
  command?: string;
@@ -144,6 +155,7 @@ export function getAgentBrowserErrorText(options: {
144
155
  parseError?: string;
145
156
  plainTextInspection: boolean;
146
157
  spawnError?: Error;
158
+ staleRefArgs?: string[];
147
159
  stderr: string;
148
160
  timedOut?: boolean;
149
161
  timeoutMs?: number;
@@ -163,7 +175,8 @@ export function getAgentBrowserErrorText(options: {
163
175
  if (envelopeErrorText && isUpstreamIpcReadTimeoutMessage(envelopeErrorText)) {
164
176
  return buildUpstreamIpcReadTimeoutMessage();
165
177
  }
166
- return envelopeErrorText ?? (stderr.trim() || buildFailureFallback(options));
178
+ const fallback = envelopeErrorText ?? (stderr.trim() || buildFailureFallback(options));
179
+ return maybeAppendStaleRefHint(fallback, options.staleRefArgs ?? options.effectiveArgs);
167
180
  }
168
181
  if (exitCode !== 0) {
169
182
  return stderr.trim() || buildExitCodeFallback(options);
@@ -349,6 +349,9 @@ function splitShellWords(input: string): string[] | undefined {
349
349
  current += input[index];
350
350
  continue;
351
351
  }
352
+ if (char === "#" && current.length === 0) {
353
+ break;
354
+ }
352
355
  if (/\s/.test(char)) {
353
356
  if (current.length > 0) {
354
357
  words.push(current);
@@ -384,7 +387,7 @@ function formatNativeSkillContent(content: string): string {
384
387
  const heredocMatch = /^(.*?)\s+(<<-?)['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?\s*$/.exec(rawArgsText);
385
388
  const argsText = heredocMatch?.[1] ?? rawArgsText;
386
389
  const args = splitShellWords(argsText);
387
- if (!args) {
390
+ if (!args || args.length === 0) {
388
391
  output.push(line);
389
392
  continue;
390
393
  }
@@ -419,7 +422,7 @@ function formatSkillsText(commandInfo: CommandInfo, data: unknown): string | und
419
422
  if (content) {
420
423
  const note = [
421
424
  "Pi native-tool note: upstream skill text was adapted for this native tool.",
422
- "Use args for CLI tokens and stdin only for batch or eval --stdin; do not pipe heredocs through bash unless the user explicitly asks for a bash workflow.",
425
+ "Use args for CLI tokens and stdin only for batch, eval --stdin, or auth save --password-stdin; do not pipe heredocs through bash unless the user explicitly asks for a bash workflow.",
423
426
  ].join("\n");
424
427
  return `${note}\n\n${redactModelFacingText(formatNativeSkillContent(content))}`;
425
428
  }