chenbhao Claude Opus 4.6 commited on
Commit
e7b07b3
·
1 Parent(s): 02a4f30

feat: register ImageShowTool as a proper buildTool with prompt + UI

Browse files

Convert ImageShowTool from standalone utilities to a full Tool definition
using buildTool(), add prompt.ts for LLM-facing description, and refactor
UI.tsx into tool render functions. Standalone executable moved to
standalone.tsx to avoid side effects on import.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

src/tools/ImageShowTool/ImageShowTool.ts CHANGED
@@ -1,5 +1,17 @@
 
1
  import { Jimp } from "jimp";
 
2
  import { loadImageFromUrl } from "../../ink-picture/utils/jimpURL.ts";
 
 
 
 
 
 
 
 
 
 
3
 
4
  export const CELL_WIDTH = 8;
5
  export const CELL_HEIGHT = 16;
@@ -46,3 +58,125 @@ export function calculateDimensions(
46
  pixelHeight: finalH_pixels,
47
  };
48
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { z } from 'zod/v4'
2
  import { Jimp } from "jimp";
3
+ import { buildTool, type ToolDef } from '../../Tool.js'
4
  import { loadImageFromUrl } from "../../ink-picture/utils/jimpURL.ts";
5
+ import { lazySchema } from '../../utils/lazySchema.js'
6
+ import type { PermissionDecision } from '../../utils/permissions/PermissionResult.js'
7
+ import { IMAGE_SHOW_TOOL_NAME, DESCRIPTION } from './prompt.js'
8
+ import {
9
+ getToolUseSummary,
10
+ renderToolResultMessage,
11
+ renderToolUseMessage,
12
+ } from './UI.js'
13
+
14
+ // ── Utility functions (kept for standalone UI and external use) ──
15
 
16
  export const CELL_WIDTH = 8;
17
  export const CELL_HEIGHT = 16;
 
58
  pixelHeight: finalH_pixels,
59
  };
60
  }
61
+
62
+ // ── Tool definition ──
63
+
64
+ const inputSchema = lazySchema(() =>
65
+ z.strictObject({
66
+ src: z.string().describe('Image source — local file path or HTTPS URL'),
67
+ }),
68
+ )
69
+ type InputSchema = ReturnType<typeof inputSchema>
70
+
71
+ const outputSchema = lazySchema(() =>
72
+ z.object({
73
+ src: z.string().describe('The image source that was displayed'),
74
+ success: z.boolean().describe('Whether the image was displayed successfully'),
75
+ }),
76
+ )
77
+ type OutputSchema = ReturnType<typeof outputSchema>
78
+
79
+ export type Output = z.infer<OutputSchema>
80
+
81
+ export const ImageShowTool = buildTool({
82
+ name: IMAGE_SHOW_TOOL_NAME,
83
+ searchHint: 'display an image in the terminal',
84
+ maxResultSizeChars: 10_000,
85
+ shouldDefer: true,
86
+ async description(input) {
87
+ const { src } = input as { src: string }
88
+ try {
89
+ const url = new URL(src)
90
+ return `Codev wants to display image from ${url.hostname}`
91
+ } catch {
92
+ return `Codev wants to display image: ${src}`
93
+ }
94
+ },
95
+ userFacingName() {
96
+ return 'Image'
97
+ },
98
+ getToolUseSummary,
99
+ getActivityDescription(input) {
100
+ const { src } = input as { src: string }
101
+ try {
102
+ const url = new URL(src)
103
+ return `Showing image from ${url.hostname}`
104
+ } catch {
105
+ return `Showing image: ${src}`
106
+ }
107
+ },
108
+ get inputSchema(): InputSchema {
109
+ return inputSchema()
110
+ },
111
+ get outputSchema(): OutputSchema {
112
+ return outputSchema()
113
+ },
114
+ isConcurrencySafe() {
115
+ return true
116
+ },
117
+ isReadOnly() {
118
+ return true
119
+ },
120
+ async checkPermissions(_input, _context): Promise<PermissionDecision> {
121
+ return {
122
+ behavior: 'allow',
123
+ updatedInput: _input,
124
+ decisionReason: { type: 'other', reason: 'ImageShowTool is read-only' },
125
+ }
126
+ },
127
+ async prompt(_options) {
128
+ return DESCRIPTION
129
+ },
130
+ async validateInput(input) {
131
+ const { src } = input
132
+ if (!src || src.trim().length === 0) {
133
+ return {
134
+ result: false,
135
+ message: 'Error: "src" is required and cannot be empty.',
136
+ meta: { reason: 'missing_src' },
137
+ errorCode: 1,
138
+ }
139
+ }
140
+ return { result: true }
141
+ },
142
+ renderToolUseMessage,
143
+ renderToolResultMessage,
144
+ async call({ src }) {
145
+ try {
146
+ const image = await loadImage(src)
147
+ const dims = calculateDimensions(
148
+ image.bitmap.width,
149
+ image.bitmap.height,
150
+ 80,
151
+ )
152
+ void dims
153
+ return {
154
+ data: {
155
+ src,
156
+ success: true,
157
+ } satisfies Output,
158
+ }
159
+ } catch (error) {
160
+ return {
161
+ data: {
162
+ src,
163
+ success: false,
164
+ } satisfies Output,
165
+ }
166
+ }
167
+ },
168
+ mapToolResultToToolResultBlockParam(output, toolUseID) {
169
+ return {
170
+ tool_use_id: toolUseID,
171
+ type: 'tool_result',
172
+ content: [
173
+ {
174
+ type: 'text',
175
+ text: output.success
176
+ ? `Image displayed: ${output.src}`
177
+ : `Failed to display image: ${output.src}`,
178
+ },
179
+ ],
180
+ }
181
+ },
182
+ } satisfies ToolDef<InputSchema, Output>)
src/tools/ImageShowTool/UI.tsx CHANGED
@@ -1,76 +1,57 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Visual test: display a local image or URL using ink-picture + Ink.
4
- */
5
-
6
- import { useEffect, useState } from "react";
7
- import { render, Box, Text, useApp } from "ink";
8
- import Image, { InkPictureProvider } from "../../ink-picture/index.ts";
9
- import {
10
- getImagePath,
11
- isUrl,
12
- loadImage,
13
- calculateDimensions,
14
- type ImageDimensions,
15
- } from "./ImageShowTool.ts";
16
-
17
- const args = process.argv.slice(2);
18
- const IMAGE_PATH = getImagePath(args);
19
-
20
- function App() {
21
- const { exit } = useApp();
22
- const [dimensions, setDimensions] = useState<ImageDimensions | null>(null);
23
- const [err, setErr] = useState(false);
24
-
25
- useEffect(() => {
26
- (async () => {
27
- try {
28
- const image = await loadImage(IMAGE_PATH);
29
- const dims = calculateDimensions(
30
- image.bitmap.width,
31
- image.bitmap.height,
32
- process.stdout.columns ?? 80
33
- );
34
- setDimensions(dims);
35
- } catch (e) {
36
- console.error(e);
37
- setErr(true);
38
- exit();
39
- }
40
- })();
41
- }, [exit]);
42
 
43
- useEffect(() => {
44
- const handleSigint = () => exit();
45
- process.on("SIGINT", handleSigint);
46
- return () => {
47
- process.off("SIGINT", handleSigint);
48
- };
49
- }, [exit]);
50
 
51
- if (err) {
52
- return <Text color="red">Failed to fetch: {IMAGE_PATH}</Text>;
 
 
 
 
 
 
 
 
 
53
  }
54
-
55
- if (!dimensions) {
56
- return <Text>Loading...</Text>;
 
 
 
 
 
 
 
57
  }
58
-
59
  return (
60
- <Box flexDirection="column">
61
- <InkPictureProvider>
62
- <Image
63
- src={IMAGE_PATH}
64
- width={dimensions.width}
65
- height={dimensions.height}
66
- pixelWidth={dimensions.pixelWidth}
67
- pixelHeight={dimensions.pixelHeight}
68
- alt={isUrl(IMAGE_PATH) ? "url-image" : "local-image"}
69
- />
70
- </InkPictureProvider>
71
- </Box>
72
- );
73
  }
74
 
75
- const { waitUntilExit } = render(<App />);
76
- await waitUntilExit();
 
 
 
1
+ import React from 'react'
2
+ import { MessageResponse } from '../../components/MessageResponse.js'
3
+ import { Box, Text } from '../../ink.js'
4
+
5
+ export function renderToolUseMessage(
6
+ { src }: { src?: string },
7
+ { verbose }: { theme?: string; verbose: boolean },
8
+ ): React.ReactNode {
9
+ if (!src) return null
10
+ if (verbose) return `src: "${src}"`
11
+ return src
12
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
+ export function renderToolUseProgressMessage(): React.ReactNode {
15
+ return (
16
+ <MessageResponse height={1}>
17
+ <Text dimColor>Rendering image…</Text>
18
+ </MessageResponse>
19
+ )
20
+ }
21
 
22
+ export function renderToolResultMessage(
23
+ { src, success }: { src: string; success: boolean },
24
+ _progressMessages: unknown[],
25
+ { verbose }: { verbose: boolean },
26
+ ): React.ReactNode {
27
+ if (!success) {
28
+ return (
29
+ <MessageResponse height={1}>
30
+ <Text color="red">Failed to display: {src}</Text>
31
+ </MessageResponse>
32
+ )
33
  }
34
+ if (verbose) {
35
+ return (
36
+ <Box flexDirection="column">
37
+ <MessageResponse height={1}>
38
+ <Text>
39
+ Image displayed: <Text bold>{src}</Text>
40
+ </Text>
41
+ </MessageResponse>
42
+ </Box>
43
+ )
44
  }
 
45
  return (
46
+ <MessageResponse height={1}>
47
+ <Text>
48
+ Image displayed: <Text bold>{src}</Text>
49
+ </Text>
50
+ </MessageResponse>
51
+ )
 
 
 
 
 
 
 
52
  }
53
 
54
+ export function getToolUseSummary(input: { src?: string } | undefined): string | null {
55
+ if (!input?.src) return null
56
+ return input.src.length > 80 ? input.src.slice(0, 77) + '...' : input.src
57
+ }
src/tools/ImageShowTool/prompt.ts CHANGED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const IMAGE_SHOW_TOOL_NAME = 'ImageShow'
2
+
3
+ export const DESCRIPTION = `
4
+ ImageShow — Display images directly in the terminal.
5
+
6
+ This tool renders PNG/JPEG/GIF/WebP images in the terminal using Kitty sixel protocol via ink-picture. It supports both local file paths and HTTPS URLs.
7
+
8
+ **When to use this tool:**
9
+ - User asks to display, view, or show an image
10
+ - Search results include image thumbnails that should be displayed inline
11
+ - User references a local image path or URL and wants to see it
12
+ - Any situation where visual inspection of an image is helpful
13
+
14
+ **Input:**
15
+ - \`src\`: Image source — either a local file path (e.g. \`/home/user/pic.png\`) or a HTTPS URL (e.g. \`https://example.com/image.jpg\`)
16
+
17
+ **Supported formats:** PNG, JPEG, GIF, WebP
18
+
19
+ **Usage examples:**
20
+ - Display a local image: \`src: "/home/yuki/Pictures/wallpaper.jpg"\`
21
+ - Display a remote image: \`src: "https://example.com/photo.png"\`
22
+ - Display Google Street View: \`src: "https://maps.googleapis.com/maps/api/streetview?size=800x400&location=37.7749,-122.4194&key=..."\`
23
+
24
+ **Rendering details:**
25
+ - Image width is auto-calculated as ~16.18% of terminal width (golden ratio)
26
+ - Height is derived from aspect ratio, with a minimum of 3 character cells
27
+ - Supports sixel-capable terminals (Kitty, WezTerm, etc.)
28
+
29
+ **Combining with other tools:**
30
+ - WebSearch results with images → ImageShow displays them inline automatically
31
+ - LocationTool place photos → ImageShow renders them
32
+ - WebFetchTool image URLs → ImageShow displays the fetched content
33
+ `
src/tools/ImageShowTool/standalone.tsx ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Standalone visual test: display a local image or URL using ink-picture + Ink.
4
+ *
5
+ * Usage:
6
+ * bun run src/tools/ImageShowTool/standalone.tsx [path|url]
7
+ *
8
+ * Examples:
9
+ * bun run src/tools/ImageShowTool/standalone.tsx ~/Pictures/image.png
10
+ * bun run src/tools/ImageShowTool/standalone.tsx https://example.com/image.jpg
11
+ */
12
+
13
+ import { useEffect, useState } from "react";
14
+ import { render, Box, Text, useApp } from "ink";
15
+ import Image, { InkPictureProvider } from "../../ink-picture/index.ts";
16
+ import {
17
+ getImagePath,
18
+ isUrl,
19
+ loadImage,
20
+ calculateDimensions,
21
+ type ImageDimensions,
22
+ } from "./ImageShowTool.ts";
23
+
24
+ const args = process.argv.slice(2);
25
+ const IMAGE_PATH = getImagePath(args);
26
+
27
+ function App() {
28
+ const { exit } = useApp();
29
+ const [dimensions, setDimensions] = useState<ImageDimensions | null>(null);
30
+ const [err, setErr] = useState(false);
31
+
32
+ useEffect(() => {
33
+ (async () => {
34
+ try {
35
+ const image = await loadImage(IMAGE_PATH);
36
+ const dims = calculateDimensions(
37
+ image.bitmap.width,
38
+ image.bitmap.height,
39
+ process.stdout.columns ?? 80
40
+ );
41
+ setDimensions(dims);
42
+ } catch (e) {
43
+ console.error(e);
44
+ setErr(true);
45
+ exit();
46
+ }
47
+ })();
48
+ }, [exit]);
49
+
50
+ useEffect(() => {
51
+ const handleSigint = () => exit();
52
+ process.on("SIGINT", handleSigint);
53
+ return () => {
54
+ process.off("SIGINT", handleSigint);
55
+ };
56
+ }, [exit]);
57
+
58
+ if (err) {
59
+ return <Text color="red">Failed to fetch: {IMAGE_PATH}</Text>;
60
+ }
61
+
62
+ if (!dimensions) {
63
+ return <Text>Loading...</Text>;
64
+ }
65
+
66
+ return (
67
+ <Box flexDirection="column">
68
+ <InkPictureProvider>
69
+ <Image
70
+ src={IMAGE_PATH}
71
+ width={dimensions.width}
72
+ height={dimensions.height}
73
+ pixelWidth={dimensions.pixelWidth}
74
+ pixelHeight={dimensions.pixelHeight}
75
+ alt={isUrl(IMAGE_PATH) ? "url-image" : "local-image"}
76
+ />
77
+ </InkPictureProvider>
78
+ </Box>
79
+ );
80
+ }
81
+
82
+ const { waitUntilExit } = render(<App />);
83
+ await waitUntilExit();