@luan.sh/pi-view-image 0.3.7 → 0.3.8

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/README.md CHANGED
@@ -1,6 +1,4 @@
1
- # @luan.sh/pi-view-image
2
-
3
- [Pi gallery](https://pi.dev/packages/@luan.sh/pi-view-image)
1
+ # @luan.sh/pi-view-image&nbsp;[<img src="https://pi.luan.sh/icons/pi.svg" width="14" alt="Pi gallery">](https://pi.dev/packages/@luan.sh/pi-view-image)&nbsp;[<img src="https://pi.luan.sh/icons/npm.svg" width="14" alt="npm">](https://www.npmjs.com/package/@luan.sh/pi-view-image)
4
2
 
5
3
  `@luan.sh/pi-view-image` adds a Codex-compatible `view_image` tool to Pi. A
6
4
  native Rust binary reads and validates a local PNG, JPEG, GIF, or WebP file and
@@ -11,9 +9,9 @@ before they reach the provider.
11
9
 
12
10
  ## Preview
13
11
 
14
- ![@luan.sh/pi-view-image in Bootty](https://github.com/luan/agents/releases/download/v0.3.2/pi-view-image.png)
12
+ ![@luan.sh/pi-view-image in Bootty](https://pi.luan.sh/media/previews/pi-view-image-3b76f3119bb5.png)
15
13
 
16
- [Watch the demo](https://github.com/luan/agents/releases/download/v0.3.2/pi-view-image.mp4).
14
+ [Watch the demo](https://pi.luan.sh/media/previews/pi-view-image-62df21208d2a.mp4).
17
15
 
18
16
  ## Install
19
17
 
@@ -21,7 +19,7 @@ before they reach the provider.
21
19
  pi install npm:@luan.sh/pi-view-image
22
20
  ```
23
21
 
24
- Requires a Rust toolchain (https://rustup.rs). The `view_image` binary builds
22
+ Requires a Rust toolchain (https://rustup.rs). The `view-image` binary builds
25
23
  itself on first use under Pi's agent directory (`native/view-image/<version>/`).
26
24
  Set `PI_VIEW_IMAGE_BIN` to use a prebuilt binary; it must point to an
27
25
  executable file. Pi shows an info notification while the first build runs.
@@ -48,7 +46,8 @@ Example call:
48
46
 
49
47
  Behaviour:
50
48
 
51
- - `high` resizes images larger than 2048 pixels on either axis. Resized
49
+ - `high` resizes images larger than 2000 pixels on either axis (Anthropic's
50
+ limit for requests carrying more than 20 images). Resized
52
51
  images and GIF input are re-encoded as PNG; PNG, JPEG, and WebP files that
53
52
  are not resized keep their original bytes and MIME type.
54
53
  - `original` keeps the source dimensions and bytes.
@@ -89,7 +88,7 @@ paste a file path on Command-V. Ordinary text and non-image paths are left
89
88
  alone.
90
89
 
91
90
  When the prompt is submitted, each pending pill is loaded through the native
92
- binary at `original` detail, attached as an image, and its text becomes a
91
+ binary at `high` detail, attached as an image, and its text becomes a
93
92
  `<file name="..."></file>` tag. If a file cannot be loaded, the pill reverts to
94
93
  the plain path and Pi shows a warning `Could not attach <path>: <message>`.
95
94
  Pending pills are cleared on session start and shutdown.
@@ -112,6 +111,15 @@ of image tags equals the count of image blocks; otherwise the message is left
112
111
  unchanged. This applies to Pi's own `@image` attachments and to pastes handled
113
112
  by this package, and is provider-neutral.
114
113
 
114
+ The same hook then clamps every image block in the outgoing context, in user
115
+ and tool-result messages alike, to 2000 pixels on either axis using Pi's
116
+ `resizeImage`. Anthropic rejects larger images once a request carries more
117
+ than 20 of them, and attach-time resizing cannot fix images already in the
118
+ session history. Each distinct image is decoded once per session.
119
+ Shrinking an image that Anthropic has already seen changes the request prefix,
120
+ so the next response reports dropped thinking blocks once; pi-thinking-binding
121
+ then strips those blocks for the rest of the session.
122
+
115
123
  ## Configuration
116
124
 
117
125
  The package has no settings. The only configuration is the
@@ -1,6 +1,4 @@
1
- # @luan.sh/pi-code-mode
2
-
3
- [Pi gallery](https://pi.dev/packages/@luan.sh/pi-code-mode)
1
+ # @luan.sh/pi-code-mode&nbsp;[<img src="https://pi.luan.sh/icons/pi.svg" width="14" alt="Pi gallery">](https://pi.dev/packages/@luan.sh/pi-code-mode)&nbsp;[<img src="https://pi.luan.sh/icons/npm.svg" width="14" alt="npm">](https://www.npmjs.com/package/@luan.sh/pi-code-mode)
4
2
 
5
3
  Code Mode adds two tools to Pi:
6
4
 
@@ -14,9 +12,9 @@ available inside `exec` through the SDK described below.
14
12
 
15
13
  ## Preview
16
14
 
17
- ![@luan.sh/pi-code-mode in Bootty](https://github.com/luan/agents/releases/download/v0.3.3/pi-code-mode.png)
15
+ ![@luan.sh/pi-code-mode in Bootty](https://pi.luan.sh/media/previews/pi-code-mode-1fb9bf32cd5a.png)
18
16
 
19
- [Watch the demo](https://github.com/luan/agents/releases/download/v0.3.3/pi-code-mode.mp4).
17
+ [Watch the demo](https://pi.luan.sh/media/previews/pi-code-mode-90738a72b416.mp4).
20
18
 
21
19
  ## Install
22
20
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luan.sh/pi-code-mode",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,8 +21,8 @@
21
21
  "./src/extension.ts",
22
22
  "./node_modules/@luan.sh/pi-libtui/src/extension.ts"
23
23
  ],
24
- "image": "https://github.com/luan/agents/releases/download/v0.3.3/pi-code-mode.png",
25
- "video": "https://github.com/luan/agents/releases/download/v0.3.3/pi-code-mode.mp4"
24
+ "image": "https://pi.luan.sh/media/previews/pi-code-mode-1fb9bf32cd5a.png",
25
+ "video": "https://pi.luan.sh/media/previews/pi-code-mode-90738a72b416.mp4"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "@earendil-works/pi-coding-agent": "*",
@@ -1,6 +1,4 @@
1
- # @luan.sh/pi-libtui
2
-
3
- [Pi gallery](https://pi.dev/packages/@luan.sh/pi-libtui)
1
+ # @luan.sh/pi-libtui&nbsp;[<img src="https://pi.luan.sh/icons/pi.svg" width="14" alt="Pi gallery">](https://pi.dev/packages/@luan.sh/pi-libtui)&nbsp;[<img src="https://pi.luan.sh/icons/npm.svg" width="14" alt="npm">](https://www.npmjs.com/package/@luan.sh/pi-libtui)
4
2
 
5
3
  Shared terminal UI for Pi extensions: layouts, split panes, dialogs, pickers,
6
4
  selection actions, semantic colors, icons, cursors, syntax highlighting,
@@ -20,9 +18,9 @@ registers no model-facing tools, keybindings, or feature-specific UI.
20
18
 
21
19
  The native palette diagnostic and shared picker components in Xsettings.
22
20
 
23
- ![@luan.sh/pi-libtui in Bootty](https://github.com/luan/agents/releases/download/v0.3.2/pi-libtui.png)
21
+ ![@luan.sh/pi-libtui in Bootty](https://pi.luan.sh/media/previews/pi-libtui-1820a03ca387.png)
24
22
 
25
- [Watch the demo](https://github.com/luan/agents/releases/download/v0.3.2/pi-libtui.mp4).
23
+ [Watch the demo](https://pi.luan.sh/media/previews/pi-libtui-e7ff85f140d3.mp4).
26
24
 
27
25
  ## Install
28
26
 
@@ -49,6 +47,59 @@ neither a generated 256-color palette nor an ANSI base-16 palette, the host
49
47
  switches to Pi's built-in theme for the detected light or dark scheme (dark if
50
48
  the measurement fails).
51
49
 
50
+ ## Components
51
+
52
+ Captures of the shared components inside the extensions that use them. The
53
+ images are served from the documentation site.
54
+
55
+ `ActionPanel` with a `DialogButtonBar` footer in a `DialogOverlay`: the
56
+ pi-copy-mode reaction picker.
57
+
58
+ ![ActionPanel and DialogButtonBar in pi-copy-mode](https://pi.luan.sh/media/libtui/action-panel.png)
59
+
60
+ `MultiSelect` with ordered checkboxes, a filter `SemanticInput`, and a
61
+ `DialogButtonBar`: the pi-xsettings segment editor.
62
+
63
+ ![MultiSelect in pi-xsettings](https://pi.luan.sh/media/libtui/multi-select.png)
64
+
65
+ `mountSplitPane` with side-panel tabs from `registerSidePanelProvider`:
66
+ pi-panels hosting a pi-side conversation.
67
+
68
+ ![Split pane and side-panel tabs in pi-panels](https://pi.luan.sh/media/libtui/split-panes.png)
69
+
70
+ `UnifiedDiffView` from `pi-libtui/diff`: the pi-fileops `apply_patch` result.
71
+
72
+ ![UnifiedDiffView in pi-fileops](https://pi.luan.sh/media/libtui/unified-diff.png)
73
+
74
+ `PtyPane` over `TerminalProjection` from `pi-libtui/terminal`: the
75
+ pi-exec-command Process Hub attached to a running server.
76
+
77
+ ![PtyPane in pi-exec-command](https://pi.luan.sh/media/libtui/terminal.png)
78
+
79
+ `renderDetailCard` as an anchored overlay: a pi-copy-mode annotation.
80
+
81
+ ![Detail card in pi-copy-mode](https://pi.luan.sh/media/libtui/detail-card.png)
82
+
83
+ `ToolActivity` and `ToolTranscript` from `pi-libtui/tool`: pi-tool-search
84
+ and pi-exec-command rows collapsed into an `Explored` group.
85
+
86
+ ![ToolActivity rows in pi-tool-search](https://pi.luan.sh/media/libtui/tool-activity.png)
87
+
88
+ `ProgressBar`, `renderPill`, and `renderEditorTokenPills`: the
89
+ pi-custom-editor status row and file tokens.
90
+
91
+ ![Status row and editor pills in pi-custom-editor](https://pi.luan.sh/media/libtui/status-row.png)
92
+
93
+ `TransientPill`: the pi-tuicr confirmation after a review comment is added.
94
+
95
+ ![TransientPill in pi-tuicr](https://pi.luan.sh/media/libtui/transient-pill.png)
96
+
97
+ Components without a capture in a shipped extension: `PickerPanel`
98
+ (pi-prompt-storage), `SearchableSelect` and `SelectBox` (pi-xsettings
99
+ editors), `SelectionActionBar` (pi-copy-mode), `FramedEditorOverlay`,
100
+ `mountHoverTooltip`, `FloatingOverlay`, `ActivityIndicator` styles, and
101
+ `applyScrollbar`. Add them here when a gallery recording shows them in use.
102
+
52
103
  ## Public modules
53
104
 
54
105
  Every entry point is a side-effect-free import. "Host required" means the
@@ -76,9 +127,9 @@ guarded Pi 0.84–0.85 adapter; unsupported hosts keep their native transcript.
76
127
 
77
128
  ## Native binaries
78
129
 
79
- Feature packages shell out to Rust binaries such as `terminal_bridge` (the
130
+ Feature packages shell out to Rust binaries such as `terminal-bridge` (the
80
131
  exported `TERMINAL_BRIDGE` descriptor). Requires a Rust toolchain
81
- (https://rustup.rs). The `terminal_bridge` binary builds itself on first use
132
+ (https://rustup.rs). The `terminal-bridge` binary builds itself on first use
82
133
  under Pi's agent directory (`native/terminal-bridge/<revision>/`), where
83
134
  `<revision>` is the source commit recorded in the packaged `native-revision` file. Set `PI_TERMINAL_BRIDGE_BINARY` to use
84
135
  a prebuilt binary; it must point at an executable file.
@@ -1 +1 @@
1
- 6ed73340be70e909918ec92adc31ba920bfc8cd4
1
+ eace67b49e52950cb408c08f290f1ca9c409d68a
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luan.sh/pi-libtui",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,8 +30,8 @@
30
30
  "themes": [
31
31
  "./themes"
32
32
  ],
33
- "image": "https://github.com/luan/agents/releases/download/v0.3.2/pi-libtui.png",
34
- "video": "https://github.com/luan/agents/releases/download/v0.3.2/pi-libtui.mp4"
33
+ "image": "https://pi.luan.sh/media/previews/pi-libtui-1820a03ca387.png",
34
+ "video": "https://pi.luan.sh/media/previews/pi-libtui-e7ff85f140d3.mp4"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@earendil-works/pi-coding-agent": "*",
@@ -35,7 +35,7 @@ export type NativeBinaryLocation =
35
35
 
36
36
  export const TERMINAL_BRIDGE: NativeBinary = {
37
37
  crate: "terminal-bridge",
38
- binaryName: "terminal_bridge",
38
+ binaryName: "terminal-bridge",
39
39
  env: "PI_TERMINAL_BRIDGE_BINARY",
40
40
  };
41
41
 
@@ -10,7 +10,7 @@ const options = (executables: readonly string[], environment: Record<string, str
10
10
  rev: "v1.2.3",
11
11
  isExecutable: (path: string) => executables.includes(path),
12
12
  });
13
- const managed = join("/agent/native", "terminal-bridge", "v1.2.3", "bin", "terminal_bridge");
13
+ const managed = join("/agent/native", "terminal-bridge", "v1.2.3", "bin", "terminal-bridge");
14
14
 
15
15
  test("env override wins and must be executable", () => {
16
16
  expect(locateNativeBinary(TERMINAL_BRIDGE, options(["/x/bridge"], { [ENV]: "/x/bridge" }))).toEqual({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luan.sh/pi-view-image",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,8 +20,8 @@
20
20
  "./src/extension.ts",
21
21
  "./node_modules/@luan.sh/pi-libtui/src/extension.ts"
22
22
  ],
23
- "image": "https://github.com/luan/agents/releases/download/v0.3.2/pi-view-image.png",
24
- "video": "https://github.com/luan/agents/releases/download/v0.3.2/pi-view-image.mp4"
23
+ "image": "https://pi.luan.sh/media/previews/pi-view-image-3b76f3119bb5.png",
24
+ "video": "https://pi.luan.sh/media/previews/pi-view-image-62df21208d2a.mp4"
25
25
  },
26
26
  "dependencies": {
27
27
  "@luan.sh/pi-code-mode": "file:../pi-code-mode",
package/src/extension.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import { type ExtensionAPI, resizeImage } from "@earendil-works/pi-coding-agent";
2
2
  import { registerViewImageCodeModeAdapter } from "./code-mode-adapter.ts";
3
3
  import { ImageAttachmentStore } from "./core/attachments.ts";
4
+ import { createImageClamp, MAX_IMAGE_DIMENSION } from "./image-limits.ts";
4
5
  import { resolveViewImageBinary } from "./native/binary.ts";
5
6
  import { labelNativeImageAttachments } from "./native-attachments.ts";
6
7
  import { runViewImageBinary } from "./native/view-image.ts";
@@ -11,6 +12,13 @@ import { configureViewImageToolForModel, createViewImageTool } from "./tools/vie
11
12
  export default function viewImageExtension(pi: ExtensionAPI): void {
12
13
  const tool = createViewImageTool();
13
14
  const attachments = new ImageAttachmentStore();
15
+ const clampImages = createImageClamp(async (image) => {
16
+ const resized = await resizeImage(Buffer.from(image.data, "base64"), image.mimeType, {
17
+ maxWidth: MAX_IMAGE_DIMENSION,
18
+ maxHeight: MAX_IMAGE_DIMENSION,
19
+ });
20
+ return resized?.wasResized ? { type: "image", data: resized.data, mimeType: resized.mimeType } : null;
21
+ });
14
22
  let removeImagePasteSession: (() => void) | undefined;
15
23
  pi.registerTool(tool);
16
24
  pi.on("session_start", (_event, context) => {
@@ -26,7 +34,8 @@ export default function viewImageExtension(pi: ExtensionAPI): void {
26
34
  pi.on("input", async (event, context) => {
27
35
  const transformed = await transformPendingImageAttachments(event, attachments, (path) =>
28
36
  resolveViewImageBinary({ onBuild: (message) => context.ui.notify(message, "info") }).then((binary) =>
29
- runViewImageBinary(binary, { path, detail: "original" }, context.cwd),
37
+ // Pasted screenshots are attached at `high` so oversized captures are resized within provider limits.
38
+ runViewImageBinary(binary, { path, detail: "high" }, context.cwd),
30
39
  ),
31
40
  );
32
41
  if (!transformed) return { action: "continue" };
@@ -35,8 +44,10 @@ export default function viewImageExtension(pi: ExtensionAPI): void {
35
44
  }
36
45
  return { action: "transform", text: transformed.text, images: transformed.images };
37
46
  });
38
- pi.on("context", (event) => {
39
- const messages = labelNativeImageAttachments(event.messages);
47
+ pi.on("context", async (event) => {
48
+ const labelled = labelNativeImageAttachments(event.messages);
49
+ const clamped = await clampImages(labelled ?? event.messages);
50
+ const messages = clamped ?? labelled;
40
51
  return messages ? { messages } : undefined;
41
52
  });
42
53
  const disposeCodeModeAdapter = registerViewImageCodeModeAdapter(tool);
@@ -0,0 +1,57 @@
1
+ import type { ContextEvent } from "@earendil-works/pi-coding-agent";
2
+
3
+ type AgentMessage = ContextEvent["messages"][number];
4
+ export type ImageBlock = { type: "image"; data: string; mimeType: string };
5
+
6
+ // Anthropic rejects images over 2000 px on either side once a request carries more than 20 images.
7
+ // Pi's resizeImage defaults to the same cap; keep them aligned if this changes.
8
+ export const MAX_IMAGE_DIMENSION = 2000;
9
+
10
+ /** Returns a smaller block, or null when the image already fits. */
11
+ export type ResizeImage = (image: ImageBlock) => Promise<ImageBlock | null>;
12
+
13
+ const isImageBlock = (block: unknown): block is ImageBlock =>
14
+ typeof block === "object" &&
15
+ block !== null &&
16
+ Reflect.get(block, "type") === "image" &&
17
+ typeof Reflect.get(block, "data") === "string" &&
18
+ typeof Reflect.get(block, "mimeType") === "string";
19
+
20
+ /**
21
+ * Clamps every image block in the context before a request leaves. Attach-time resizing cannot fix
22
+ * images already in history (pasted before the fix, produced by other extensions, or by Pi itself),
23
+ * so this runs at the model boundary. Each distinct image is decoded once and memoized for the session.
24
+ */
25
+ export function createImageClamp(
26
+ resize: ResizeImage,
27
+ ): (messages: readonly AgentMessage[]) => Promise<AgentMessage[] | undefined> {
28
+ const cache = new Map<string, Promise<ImageBlock | null>>();
29
+ const clamp = (image: ImageBlock): Promise<ImageBlock | null> => {
30
+ let pending = cache.get(image.data);
31
+ if (!pending) {
32
+ pending = resize(image).catch(() => null);
33
+ cache.set(image.data, pending);
34
+ }
35
+ return pending;
36
+ };
37
+
38
+ return async (messages) => {
39
+ let changed = false;
40
+ const next = await Promise.all(
41
+ messages.map(async (message) => {
42
+ if (!("content" in message) || !Array.isArray(message.content)) return message;
43
+ const content = await Promise.all(
44
+ message.content.map(async (block: unknown) => {
45
+ if (!isImageBlock(block)) return block;
46
+ const smaller = await clamp(block);
47
+ if (!smaller) return block;
48
+ changed = true;
49
+ return { ...block, data: smaller.data, mimeType: smaller.mimeType };
50
+ }),
51
+ );
52
+ return { ...message, content } as AgentMessage;
53
+ }),
54
+ );
55
+ return changed ? next : undefined;
56
+ };
57
+ }
@@ -1,6 +1,6 @@
1
1
  import { type EnsureNativeBinaryHooks, ensureNativeBinary, type NativeBinary } from "@luan.sh/pi-libtui";
2
2
 
3
- const VIEW_IMAGE: NativeBinary = { crate: "view-image", binaryName: "view_image", env: "PI_VIEW_IMAGE_BIN" };
3
+ const VIEW_IMAGE: NativeBinary = { crate: "view-image", binaryName: "view-image", env: "PI_VIEW_IMAGE_BIN" };
4
4
 
5
5
  export function resolveViewImageBinary(hooks?: EnsureNativeBinaryHooks): Promise<string> {
6
6
  return ensureNativeBinary(VIEW_IMAGE, hooks);
@@ -0,0 +1,50 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createImageClamp, type ImageBlock } from "../src/image-limits.ts";
3
+
4
+ const image = (data: string): ImageBlock => ({ type: "image", data, mimeType: "image/png" });
5
+ const smaller = (block: ImageBlock): ImageBlock => ({
6
+ type: "image",
7
+ data: `small:${block.data}`,
8
+ mimeType: "image/jpeg",
9
+ });
10
+
11
+ describe("image clamp", () => {
12
+ test("replaces oversized images in user and tool-result messages, once per distinct image", async () => {
13
+ const seen: string[] = [];
14
+ const clamp = createImageClamp(async (block) => {
15
+ seen.push(block.data);
16
+ return block.data.startsWith("big") ? smaller(block) : null;
17
+ });
18
+ const messages = [
19
+ { role: "user", content: [{ type: "text", text: "look" }, image("big-1"), image("fits")], timestamp: 1 },
20
+ {
21
+ role: "toolResult",
22
+ toolCallId: "t",
23
+ toolName: "read",
24
+ content: [image("big-1"), image("big-2")],
25
+ isError: false,
26
+ timestamp: 2,
27
+ },
28
+ ] as never[];
29
+
30
+ const first = await clamp(messages);
31
+ expect(first?.map((message) => Reflect.get(message, "content"))).toEqual([
32
+ [{ type: "text", text: "look" }, { type: "image", data: "small:big-1", mimeType: "image/jpeg" }, image("fits")],
33
+ [
34
+ { type: "image", data: "small:big-1", mimeType: "image/jpeg" },
35
+ { type: "image", data: "small:big-2", mimeType: "image/jpeg" },
36
+ ],
37
+ ]);
38
+
39
+ await clamp(messages);
40
+ expect(seen.sort()).toEqual(["big-1", "big-2", "fits"]);
41
+ });
42
+
43
+ test("returns undefined when nothing needs resizing and tolerates resize failures", async () => {
44
+ const clamp = createImageClamp(async () => {
45
+ throw new Error("decoder unavailable");
46
+ });
47
+ const messages = [{ role: "user", content: [image("any")], timestamp: 1 }] as never[];
48
+ expect(await clamp(messages)).toBeUndefined();
49
+ });
50
+ });