@clementprevot/pi-show-image 0.0.0-stage → 0.1.0

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -2
  3. package/index.ts +113 -0
  4. package/package.json +49 -5
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Clément Prévot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,63 @@
1
- # Temporary Holding Version
1
+ # @clementprevot/pi-show-image
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A [Pi](https://pi.dev) extension that lets the agent show you a local image inline in the terminal, for example a screenshot it took during QA. Only you see the image: the agent gets back one line of text, so showing an image costs no image tokens.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pi install npm:@clementprevot/pi-show-image
9
+ ```
10
+
11
+ Updates ship with `pi update --extensions`. The extension applies to your next session (quit and relaunch, or run `/reload`).
12
+
13
+ ## Usage
14
+
15
+ The agent calls the `show_image` tool:
16
+
17
+ ```json
18
+ { "path": "/tmp/login-page.png", "caption": "Login page after the fix" }
19
+ ```
20
+
21
+ - `path`: a local PNG file, absolute, relative to the working directory, or starting with `~/`.
22
+ - `caption` (optional): one line shown above the image.
23
+
24
+ You can show an image yourself with `/show-image <path>`.
25
+
26
+ To have your agent use it, add a line like this one to your instructions: "To show me an image, call `show_image` with its path."
27
+
28
+ ## How it works
29
+
30
+ 1. The tool resolves the path and checks that the file exists and is a PNG.
31
+ 2. It records a custom session entry with the path and the caption, and nothing else: no image data lands in the session file.
32
+ 3. An entry renderer reads the file and draws it with pi's `Image` component, at 60 cells wide. Pi picks the terminal image protocol (Kitty graphics or iTerm2) and falls back to a text placeholder on terminals without image support.
33
+ 4. Custom entries never enter the model context, so the agent only sees the tool's text result.
34
+
35
+ Because the entry holds a path, a session reopened after the file was deleted shows a notice instead of the image.
36
+
37
+ ## Limits
38
+
39
+ - PNG only. Kitty-protocol terminals (Kitty, Ghostty, WezTerm) only accept PNG, and pi converts other formats only for its own tool results. Convert other formats first, for example `sips -s format png photo.jpg --out photo.png` on macOS or `magick photo.jpg photo.png`.
40
+ - Interactive sessions only. In print or RPC mode the tool reports an error, since there is no terminal to draw in.
41
+ - Inside tmux or other multiplexers, images depend on pi's terminal detection and on the multiplexer forwarding the image protocol.
42
+
43
+ ## Privacy and data
44
+
45
+ The extension reads the files you or the agent point it at and writes their paths to the session file. It makes no network calls and collects no data.
46
+
47
+ ## Local development
48
+
49
+ ```bash
50
+ npm install
51
+ npm test
52
+ npm run typecheck
53
+ ```
54
+
55
+ To try the extension in a live session without installing it:
56
+
57
+ ```bash
58
+ pi -e /path/to/this/repo
59
+ ```
60
+
61
+ ## License
62
+
63
+ [MIT](LICENSE)
package/index.ts ADDED
@@ -0,0 +1,113 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, resolve } from "node:path";
4
+ import { detectSupportedImageMimeTypeFromFile, type ExtensionAPI, type Theme } from "@earendil-works/pi-coding-agent";
5
+ import { Container, Image, Text } from "@earendil-works/pi-tui";
6
+ import { Type } from "typebox";
7
+
8
+ export const ENTRY_TYPE = "show-image";
9
+ const WIDTH_CELLS = 60;
10
+
11
+ export type ShownImage = {
12
+ path: string;
13
+ caption?: string;
14
+ };
15
+
16
+ export function resolveImagePath(input: string, cwd: string, home = homedir()): string {
17
+ const path = input.trim();
18
+ if (path === "~") return home;
19
+ if (path.startsWith("~/")) return resolve(home, path.slice(2));
20
+ return resolve(cwd, path);
21
+ }
22
+
23
+ /** Throws errors whose message is shown as-is to the model or the user. */
24
+ export async function showImage(
25
+ pi: Pick<ExtensionAPI, "appendEntry">,
26
+ input: ShownImage,
27
+ cwd: string,
28
+ hasUI: boolean,
29
+ ): Promise<string> {
30
+ if (!input.path.trim()) throw new Error("Give the path of the image to show.");
31
+ const path = resolveImagePath(input.path, cwd);
32
+ let mimeType: string | null;
33
+ try {
34
+ if (!statSync(path).isFile()) throw new Error(`${path} is not a file.`);
35
+ mimeType = await detectSupportedImageMimeTypeFromFile(path);
36
+ } catch (error) {
37
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") throw new Error(`No file at ${path}.`);
38
+ throw error;
39
+ }
40
+ // Kitty-protocol terminals only accept PNG, and pi converts other formats only for its own tool results.
41
+ if (mimeType !== "image/png") {
42
+ throw new Error(
43
+ `${path} is not a still PNG image. Convert it first, for example with \`sips -s format png <file> --out <file>.png\` on macOS or \`magick <file> <file>.png\`.`,
44
+ );
45
+ }
46
+ if (!hasUI) throw new Error("No interactive terminal in this session: the image cannot be shown.");
47
+ const caption = input.caption?.trim();
48
+ pi.appendEntry<ShownImage>(ENTRY_TYPE, caption ? { path, caption } : { path });
49
+ return `Showed ${path} to the user. The image was not sent to you.`;
50
+ }
51
+
52
+ export function renderShownImage(data: ShownImage | undefined, theme: Theme): Container {
53
+ const container = new Container();
54
+ if (!data?.path) {
55
+ container.addChild(new Text(theme.fg("warning", "show-image: entry without a path"), 1, 0));
56
+ return container;
57
+ }
58
+ const caption = data.caption ? `${data.caption} ` : "";
59
+ container.addChild(new Text(`${theme.fg("accent", caption)}${theme.fg("muted", data.path)}`, 1, 0));
60
+ let base64: string;
61
+ try {
62
+ base64 = readFileSync(data.path).toString("base64");
63
+ } catch {
64
+ container.addChild(new Text(theme.fg("warning", "The image file is no longer available."), 1, 0));
65
+ return container;
66
+ }
67
+ container.addChild(
68
+ new Image(
69
+ base64,
70
+ "image/png",
71
+ { fallbackColor: (text) => theme.fg("muted", text) },
72
+ { maxWidthCells: WIDTH_CELLS, filename: basename(data.path) },
73
+ ),
74
+ );
75
+ return container;
76
+ }
77
+
78
+ export default function showImageExtension(pi: ExtensionAPI): void {
79
+ pi.registerEntryRenderer<ShownImage>(ENTRY_TYPE, (entry, _options, theme) => renderShownImage(entry.data, theme));
80
+
81
+ pi.registerTool({
82
+ name: "show_image",
83
+ label: "Show image",
84
+ description:
85
+ "Show a local PNG image to the user, inline in the terminal. The image is not sent to you: use read when you need to look at it yourself.",
86
+ promptSnippet: "Show a local PNG image (screenshot, chart) to the user inline in the terminal",
87
+ promptGuidelines: [
88
+ "Use show_image when the user should see an image, for example a screenshot taken during QA.",
89
+ "show_image does not send the image to you; use read when you need to inspect it.",
90
+ ],
91
+ parameters: Type.Object({
92
+ path: Type.String({
93
+ description: "Path to a local PNG file: absolute, relative to the working directory, or starting with ~/",
94
+ }),
95
+ caption: Type.Optional(Type.String({ description: "One line telling the user what the image shows" })),
96
+ }),
97
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
98
+ const text = await showImage(pi, params, ctx.cwd, ctx.hasUI);
99
+ return { content: [{ type: "text", text }], details: undefined };
100
+ },
101
+ });
102
+
103
+ pi.registerCommand("show-image", {
104
+ description: "Show a local PNG image inline: /show-image <path>",
105
+ handler: async (args, ctx) => {
106
+ try {
107
+ await showImage(pi, { path: args }, ctx.cwd, ctx.hasUI);
108
+ } catch (error) {
109
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
110
+ }
111
+ },
112
+ });
113
+ }
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
- "name": "@clementprevot/pi-show-image",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@clementprevot/pi-show-image",
3
+ "version": "0.1.0",
4
+ "description": "Pi extension that lets the agent show you a local image inline in the terminal, without sending the image to the model",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "image",
9
+ "screenshot",
10
+ "kitty",
11
+ "iterm2",
12
+ "tui"
13
+ ],
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/clementprevot/pi-show-image.git"
19
+ },
20
+ "engines": {
21
+ "node": ">=22.6"
22
+ },
23
+ "files": [
24
+ "index.ts"
25
+ ],
26
+ "pi": {
27
+ "extensions": [
28
+ "./index.ts"
29
+ ]
30
+ },
31
+ "peerDependencies": {
32
+ "@earendil-works/pi-coding-agent": "*",
33
+ "@earendil-works/pi-tui": "*",
34
+ "typebox": "*"
35
+ },
36
+ "devDependencies": {
37
+ "@earendil-works/pi-coding-agent": "*",
38
+ "@earendil-works/pi-tui": "*",
39
+ "@types/node": "^24.0.0",
40
+ "typebox": "*",
41
+ "typescript": "^5.9.0"
42
+ },
43
+ "scripts": {
44
+ "test": "node --test test/*.test.ts",
45
+ "typecheck": "tsc --noEmit"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
50
+ }