@underactive/pi-topping-web-tools 0.2.5 → 0.2.6
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 +12 -0
- package/README.md +2 -0
- package/package.json +4 -4
- package/src/fetch-markdown.ts +2 -8
- package/src/pdf-extract.ts +2 -8
- package/src/render-preview.ts +74 -28
- package/src/web-browser/index.ts +9 -11
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.2.6] - 2026-10-02
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Tested against pi 1.0.0. No break: the extension-facing declaration surface (`ToolDefinition`, `ExtensionAPI`, `ExtensionContext`, `ExtensionUIContext`, `defineTool`) is unchanged, `pi-ai`'s declaration surface is unchanged, and pi-tui's three changed declaration files are additive. The codemode tool description for each declared tool is now one line instead of a rendered TypeScript declaration, which shortens the prompt without changing what `promptGuidelines` contribute. `web_browser`'s `browser` status line is unaffected by the `tuiMode` default moving to `fullscreen`: the footer that renders it is mounted unconditionally and its renderer is unchanged.
|
|
10
|
+
- Bumped the pinned `@earendil-works/pi-ai`, `@earendil-works/pi-coding-agent`, and `@earendil-works/pi-tui` devDependencies from 0.99.2 to 1.0.0.
|
|
11
|
+
- `web_browser`: added a prompt guideline that image content is not visible to a codemode script, so `screenshot` should be called directly, or with `toFile=true` to get a path the script can read.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `fetch_markdown`, `pdf_extract`, `web_browser`: expanded result previews are bounded in wrapped rows at the render width instead of in characters. The 0.2.5 bound of 10 logical lines of 200 characters still rendered about 43 rows at 80 columns and 76 at 40; the body is now capped at 20 rows at any terminal width, using pi's `truncateToVisualLines`.
|
|
16
|
+
|
|
5
17
|
## [0.2.5] - 2026-09-30
|
|
6
18
|
|
|
7
19
|
### Changed
|
package/README.md
CHANGED
|
@@ -142,6 +142,8 @@ When `screenshot` returns an inline result (`toFile` omitted or `false`), `web_b
|
|
|
142
142
|
|
|
143
143
|
`toFile=true` and `/browser-screenshot` save the original PNG to disk and put only the path and dimensions in the tool result. The file is resized only if a later `read` or `@file` attachment sends it to the model.
|
|
144
144
|
|
|
145
|
+
In codemode, a tool result exposes only its text blocks, so an inline `screenshot` reaches the script as its caption with no image bytes. Call `screenshot` directly from the session, or pass `toFile=true` and read the saved path from the script.
|
|
146
|
+
|
|
145
147
|
The resize fields are:
|
|
146
148
|
|
|
147
149
|
- `maxWidth` / `maxHeight`: pixel bounds (pi defaults to 2000×2000)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@underactive/pi-topping-web-tools",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Pi extensions for the web: fetch_markdown (URL to markdown, cached, keyless), pdf_extract (PDF text extraction) and web_browser (headless Playwright automation), sharing a preapproved-host allowlist.",
|
|
5
5
|
"keywords": ["pi-package", "pi-extension"],
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,9 +53,9 @@
|
|
|
53
53
|
"typebox": "*"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@earendil-works/pi-ai": "0.
|
|
57
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
58
|
-
"@earendil-works/pi-tui": "0.
|
|
56
|
+
"@earendil-works/pi-ai": "1.0.0",
|
|
57
|
+
"@earendil-works/pi-coding-agent": "1.0.0",
|
|
58
|
+
"@earendil-works/pi-tui": "1.0.0",
|
|
59
59
|
"@types/node": "^24.0.0",
|
|
60
60
|
"@types/turndown": "^5.0.5",
|
|
61
61
|
"typebox": "^1.3.3",
|
package/src/fetch-markdown.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { combineSignals } from "./abort-utils.ts";
|
|
|
12
12
|
import { getBrowserHeaders } from "./browser-headers.ts";
|
|
13
13
|
import { isPreapprovedHost, permissionKey } from "./permissions.ts";
|
|
14
14
|
import { requestHostPermission } from "./permission-prompt.ts";
|
|
15
|
-
import {
|
|
15
|
+
import { BoundedPreview, clipField, resultText } from "./render-preview.ts";
|
|
16
16
|
import { MAX_URL_LENGTH, isLocalOrPrivateHost, upgradeHttpToHttps } from "./web-browser/permissions.ts";
|
|
17
17
|
|
|
18
18
|
const MAX_HTTP_CONTENT_LENGTH = 25 * 1024 * 1024;
|
|
@@ -572,13 +572,7 @@ To complete your request, I need to fetch content from the redirected URL. Pleas
|
|
|
572
572
|
}
|
|
573
573
|
|
|
574
574
|
if (expanded) {
|
|
575
|
-
|
|
576
|
-
for (const line of preview.lines) {
|
|
577
|
-
text += `\n${theme.fg("dim", line)}`;
|
|
578
|
-
}
|
|
579
|
-
if (preview.truncated) {
|
|
580
|
-
text += `\n${theme.fg("muted", "… (truncated)")}`;
|
|
581
|
-
}
|
|
575
|
+
return new BoundedPreview({ header: text, body: resultText(result.content), marker: "… (truncated)", theme });
|
|
582
576
|
}
|
|
583
577
|
|
|
584
578
|
return new Text(text, 0, 0);
|
package/src/pdf-extract.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { extractText, getDocumentProxy } from "unpdf";
|
|
|
11
11
|
import { combineSignals } from "./abort-utils.ts";
|
|
12
12
|
import { isPreapprovedHost, permissionKey } from "./permissions.ts";
|
|
13
13
|
import { requestHostPermission } from "./permission-prompt.ts";
|
|
14
|
-
import {
|
|
14
|
+
import { BoundedPreview, clipField, resultText } from "./render-preview.ts";
|
|
15
15
|
import { getWithPermittedRedirects, isPermittedRedirect, sliceContent, validateURL } from "./fetch-markdown.ts";
|
|
16
16
|
import { upgradeHttpToHttps } from "./web-browser/permissions.ts";
|
|
17
17
|
|
|
@@ -423,13 +423,7 @@ Usage notes:
|
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
if (expanded) {
|
|
426
|
-
|
|
427
|
-
for (const line of preview.lines) {
|
|
428
|
-
text += `\n${theme.fg("dim", line)}`;
|
|
429
|
-
}
|
|
430
|
-
if (preview.truncated) {
|
|
431
|
-
text += `\n${theme.fg("muted", "… (truncated)")}`;
|
|
432
|
-
}
|
|
426
|
+
return new BoundedPreview({ header: text, body: resultText(result.content), marker: "… (truncated)", theme });
|
|
433
427
|
}
|
|
434
428
|
|
|
435
429
|
return new Text(text, 0, 0);
|
package/src/render-preview.ts
CHANGED
|
@@ -7,31 +7,26 @@
|
|
|
7
7
|
* same bound has to be applied here. Header fields need the same bound: an HTTP
|
|
8
8
|
* reason phrase and a page `<title>` are attacker-controlled and can be megabytes.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* at
|
|
10
|
+
* `renderResult` is not handed a width, so header fields and collapsed bodies are
|
|
11
|
+
* bounded in characters. The width does reach the component it returns, through
|
|
12
|
+
* `render(width)`, so an expanded body is bounded in rows there: `BoundedPreview`
|
|
13
|
+
* wraps it with the host's `truncateToVisualLines`, which keeps its height the same
|
|
14
|
+
* at every terminal width.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { truncateToVisualLines, type Theme } from "@earendil-works/pi-coding-agent";
|
|
17
18
|
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
|
19
|
+
import { Text, type Component } from "@earendil-works/pi-tui";
|
|
18
20
|
|
|
19
|
-
/**
|
|
20
|
-
export const
|
|
21
|
+
/** Body rows in an expanded preview, counted after wrapping at the render width. */
|
|
22
|
+
export const MAX_VISUAL_PREVIEW_LINES = 20;
|
|
21
23
|
|
|
22
|
-
/**
|
|
23
|
-
export const
|
|
24
|
+
/** Body characters handed to the wrapper: fills the row cap up to 400 columns without wrapping a 100K result on every redraw. */
|
|
25
|
+
export const MAX_PREVIEW_CHARS = 8_000;
|
|
24
26
|
|
|
25
27
|
/** Characters kept from a single unbounded field: a header value, status field, or collapsed body. */
|
|
26
28
|
export const MAX_FIELD_CHARS = 200;
|
|
27
29
|
|
|
28
|
-
export interface ResultPreview {
|
|
29
|
-
/** Logical lines to display, each already clipped to {@link MAX_PREVIEW_LINE_CHARS}. */
|
|
30
|
-
lines: string[];
|
|
31
|
-
/** Whether logical lines were dropped by the {@link MAX_PREVIEW_LINES} cap. */
|
|
32
|
-
truncated: boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
30
|
/** Concatenate a tool result's text blocks; empty when the result carries only images. */
|
|
36
31
|
export function resultText(content: readonly (TextContent | ImageContent)[]): string {
|
|
37
32
|
return content
|
|
@@ -45,17 +40,68 @@ export function clipField(text: string): string {
|
|
|
45
40
|
return text.length > MAX_FIELD_CHARS ? `${text.slice(0, MAX_FIELD_CHARS)}…` : text;
|
|
46
41
|
}
|
|
47
42
|
|
|
43
|
+
export interface BoundedPreviewOptions {
|
|
44
|
+
/** Styled summary; its attacker-controlled fields are already clipped. */
|
|
45
|
+
header: string;
|
|
46
|
+
/** Raw result text, styled dim per logical line. */
|
|
47
|
+
body: string;
|
|
48
|
+
/** Raw marker shown muted when the body was cut. */
|
|
49
|
+
marker: string;
|
|
50
|
+
/** Raw line shown dim after the body whether or not it was cut. */
|
|
51
|
+
trailer?: string;
|
|
52
|
+
theme: Theme;
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
56
|
+
* Expanded tool result: header, at most {@link MAX_VISUAL_PREVIEW_LINES} body rows, then the
|
|
57
|
+
* marker and trailer. Rows depend on the width, which only `render` sees; lines are cached per
|
|
58
|
+
* width because the transcript renders every result on every frame.
|
|
52
59
|
*/
|
|
53
|
-
export
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
export class BoundedPreview implements Component {
|
|
61
|
+
private readonly header: string;
|
|
62
|
+
private readonly body: string;
|
|
63
|
+
private readonly marker: string;
|
|
64
|
+
private readonly trailer: string | undefined;
|
|
65
|
+
private readonly cut: boolean;
|
|
66
|
+
private cachedWidth: number | undefined;
|
|
67
|
+
private cachedLines: string[] | undefined;
|
|
68
|
+
|
|
69
|
+
constructor(options: BoundedPreviewOptions) {
|
|
70
|
+
const { theme } = options;
|
|
71
|
+
this.header = options.header;
|
|
72
|
+
this.cut = options.body.length > MAX_PREVIEW_CHARS;
|
|
73
|
+
// Slice the raw text before styling so the cap never cuts an escape sequence. Every logical
|
|
74
|
+
// line takes at least one row, so one line past the cap is enough for the skip to register.
|
|
75
|
+
this.body = options.body
|
|
76
|
+
.slice(0, MAX_PREVIEW_CHARS)
|
|
77
|
+
.split("\n", MAX_VISUAL_PREVIEW_LINES + 1)
|
|
78
|
+
.map((line) => theme.fg("dim", line))
|
|
79
|
+
.join("\n");
|
|
80
|
+
this.marker = theme.fg("muted", options.marker);
|
|
81
|
+
this.trailer = options.trailer === undefined ? undefined : theme.fg("dim", options.trailer);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
render(width: number): string[] {
|
|
85
|
+
if (this.cachedLines === undefined || this.cachedWidth !== width) {
|
|
86
|
+
// paddingX 0 keeps the geometry of the `new Text(text, 0, 0)` this replaces.
|
|
87
|
+
const { visualLines, skippedCount } = truncateToVisualLines(
|
|
88
|
+
this.body,
|
|
89
|
+
MAX_VISUAL_PREVIEW_LINES,
|
|
90
|
+
width,
|
|
91
|
+
0,
|
|
92
|
+
"start",
|
|
93
|
+
);
|
|
94
|
+
const lines = [...new Text(this.header, 0, 0).render(width), ...visualLines];
|
|
95
|
+
if (this.cut || skippedCount > 0) lines.push(...new Text(this.marker, 0, 0).render(width));
|
|
96
|
+
if (this.trailer !== undefined) lines.push(...new Text(this.trailer, 0, 0).render(width));
|
|
97
|
+
this.cachedLines = lines;
|
|
98
|
+
this.cachedWidth = width;
|
|
99
|
+
}
|
|
100
|
+
return this.cachedLines;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
invalidate(): void {
|
|
104
|
+
this.cachedWidth = undefined;
|
|
105
|
+
this.cachedLines = undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/web-browser/index.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
import { buildOriginChecker } from "./egress-policy.ts";
|
|
32
32
|
import { checkUrlPermission, upgradeHttpToHttps, validateURL } from "./permissions.ts";
|
|
33
33
|
import { requestHostPermission } from "../permission-prompt.ts";
|
|
34
|
-
import {
|
|
34
|
+
import { BoundedPreview, clipField, resultText } from "../render-preview.ts";
|
|
35
35
|
import { listGrants } from "../permission-store.ts";
|
|
36
36
|
import { permissionKey } from "../permissions.ts";
|
|
37
37
|
import { isInsideCwd } from "../pdf-extract.ts";
|
|
@@ -343,6 +343,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
343
343
|
"Use get_content only when DOM structure or attributes are needed; it returns raw HTML.",
|
|
344
344
|
"On SPA sites, use wait_for after navigate (selector/state or networkidle) before click/type.",
|
|
345
345
|
"Use screenshot for visual verification (requires a vision-capable model); use toFile=true to save PNG to disk instead of base64.",
|
|
346
|
+
"Image content is not visible to a codemode script: a script receives only a tool result's text blocks, so a screenshot taken from one returns its caption without the pixels. Call screenshot directly, or with toFile=true to get a file path the script can read.",
|
|
346
347
|
"Use get_console_logs when JavaScript errors or console output may explain page behavior.",
|
|
347
348
|
"Use get_network_logs when failed, egress-blocked, or 4xx/5xx requests may explain page behavior. Navigate directly to a blocked origin to request approval.",
|
|
348
349
|
"Use set_cookies to inject cookies for authenticated flows after manual login.",
|
|
@@ -1101,16 +1102,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1101
1102
|
}
|
|
1102
1103
|
|
|
1103
1104
|
if (expanded) {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
}
|
|
1111
|
-
if (details.fullOutputPath) {
|
|
1112
|
-
text += `\n${theme.fg("dim", `Full: ${details.fullOutputPath}`)}`;
|
|
1113
|
-
}
|
|
1105
|
+
return new BoundedPreview({
|
|
1106
|
+
header: text,
|
|
1107
|
+
body: resultText(result.content),
|
|
1108
|
+
marker: "…",
|
|
1109
|
+
trailer: details.fullOutputPath ? `Full: ${details.fullOutputPath}` : undefined,
|
|
1110
|
+
theme,
|
|
1111
|
+
});
|
|
1114
1112
|
}
|
|
1115
1113
|
|
|
1116
1114
|
return new Text(text, 0, 0);
|