@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 +16 -8
- package/node_modules/@luan.sh/pi-code-mode/README.md +3 -5
- package/node_modules/@luan.sh/pi-code-mode/package.json +3 -3
- package/node_modules/@luan.sh/pi-libtui/README.md +58 -7
- package/node_modules/@luan.sh/pi-libtui/native-revision +1 -1
- package/node_modules/@luan.sh/pi-libtui/package.json +3 -3
- package/node_modules/@luan.sh/pi-libtui/src/native-binary.ts +1 -1
- package/node_modules/@luan.sh/pi-libtui/test/native-binary.test.ts +1 -1
- package/package.json +3 -3
- package/src/extension.ts +15 -4
- package/src/image-limits.ts +57 -0
- package/src/native/binary.ts +1 -1
- package/test/image-limits.test.ts +50 -0
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 [<img src="https://pi.luan.sh/icons/pi.svg" width="14" alt="Pi gallery">](https://pi.dev/packages/@luan.sh/pi-view-image) [<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
|
-

|
|
15
13
|
|
|
16
|
-
[Watch the demo](https://
|
|
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 `
|
|
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
|
|
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 `
|
|
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 [<img src="https://pi.luan.sh/icons/pi.svg" width="14" alt="Pi gallery">](https://pi.dev/packages/@luan.sh/pi-code-mode) [<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
|
-

|
|
18
16
|
|
|
19
|
-
[Watch the demo](https://
|
|
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.
|
|
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://
|
|
25
|
-
"video": "https://
|
|
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 [<img src="https://pi.luan.sh/icons/pi.svg" width="14" alt="Pi gallery">](https://pi.dev/packages/@luan.sh/pi-libtui) [<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
|
-

|
|
24
22
|
|
|
25
|
-
[Watch the demo](https://
|
|
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
|
+

|
|
59
|
+
|
|
60
|
+
`MultiSelect` with ordered checkboxes, a filter `SemanticInput`, and a
|
|
61
|
+
`DialogButtonBar`: the pi-xsettings segment editor.
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
65
|
+
`mountSplitPane` with side-panel tabs from `registerSidePanelProvider`:
|
|
66
|
+
pi-panels hosting a pi-side conversation.
|
|
67
|
+
|
|
68
|
+

|
|
69
|
+
|
|
70
|
+
`UnifiedDiffView` from `pi-libtui/diff`: the pi-fileops `apply_patch` result.
|
|
71
|
+
|
|
72
|
+

|
|
73
|
+
|
|
74
|
+
`PtyPane` over `TerminalProjection` from `pi-libtui/terminal`: the
|
|
75
|
+
pi-exec-command Process Hub attached to a running server.
|
|
76
|
+
|
|
77
|
+

|
|
78
|
+
|
|
79
|
+
`renderDetailCard` as an anchored overlay: a pi-copy-mode annotation.
|
|
80
|
+
|
|
81
|
+

|
|
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
|
+

|
|
87
|
+
|
|
88
|
+
`ProgressBar`, `renderPill`, and `renderEditorTokenPills`: the
|
|
89
|
+
pi-custom-editor status row and file tokens.
|
|
90
|
+
|
|
91
|
+

|
|
92
|
+
|
|
93
|
+
`TransientPill`: the pi-tuicr confirmation after a review comment is added.
|
|
94
|
+
|
|
95
|
+

|
|
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 `
|
|
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 `
|
|
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
|
-
|
|
1
|
+
eace67b49e52950cb408c08f290f1ca9c409d68a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luan.sh/pi-libtui",
|
|
3
|
-
"version": "0.3.
|
|
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://
|
|
34
|
-
"video": "https://
|
|
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": "*",
|
|
@@ -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", "
|
|
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.
|
|
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://
|
|
24
|
-
"video": "https://
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
package/src/native/binary.ts
CHANGED
|
@@ -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: "
|
|
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
|
+
});
|