pi-agent-browser-native 0.2.23 → 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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.2.24 - 2026-05-11
6
+
7
+ ### Added
8
+ - added custom `agent_browser` TUI rendering with colorized call/output text and built-in-style visual truncation for long visible output while preserving model-facing tool content
9
+
5
10
  ## 0.2.23 - 2026-05-10
6
11
 
7
12
  ### Fixed
package/README.md CHANGED
@@ -26,6 +26,7 @@ The result is optimized for agent work:
26
26
  - screenshots and downloaded files surfaced as Pi artifacts
27
27
  - structured details for titles, URLs, saved files, sessions, and errors
28
28
  - spill files for oversized raw output instead of dumping pages into context
29
+ - compact, colorized Pi TUI rows that can be expanded without changing what the agent receives
29
30
  - recovery hints when a tab, selector, stale `@ref`, or launch mode needs a different next step
30
31
 
31
32
  ## Who this is for
@@ -194,9 +194,12 @@ For oversized snapshots and other oversized tool outputs, details should switch
194
194
 
195
195
  "Rendering" here means how results appear inside `pi`, not embedding a browser UI.
196
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
+
197
199
  Worth doing in v1:
198
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
199
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
200
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
201
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
202
205
  - extraction-style commands like `eval --stdin` and `get title` → scalar-first text with lightweight origin context when available
@@ -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 {
@@ -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;
@@ -1554,6 +1710,18 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
1554
1710
  "Browse websites, read live docs, click and fill pages, extract browser content, take screenshots, and automate real web workflows.",
1555
1711
  promptGuidelines: toolPromptGuidelines,
1556
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
+ },
1557
1725
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
1558
1726
  const redactedArgs = redactInvocationArgs(params.args);
1559
1727
  const validationError = validateToolArgs(params.args) ?? getBatchAnnotateValidationError(params.args, params.stdin);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-browser-native",
3
- "version": "0.2.23",
3
+ "version": "0.2.24",
4
4
  "description": "pi extension that exposes agent-browser as a native tool for browser automation",
5
5
  "type": "module",
6
6
  "author": "Mitch Fultz (https://github.com/fitchmultz)",