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 +5 -0
- package/README.md +1 -0
- package/docs/TOOL_CONTRACT.md +3 -0
- package/extensions/agent-browser/index.ts +169 -1
- package/package.json +1 -1
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
|
package/docs/TOOL_CONTRACT.md
CHANGED
|
@@ -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 {
|
|
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