@ridit/lens 0.3.7 → 0.3.9

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 (96) hide show
  1. package/dist/index.mjs +105368 -274002
  2. package/package.json +13 -19
  3. package/src/colors.ts +15 -15
  4. package/src/commands/chat.tsx +32 -23
  5. package/src/commands/provider.tsx +11 -238
  6. package/src/commands/repo.tsx +66 -120
  7. package/src/commands/timeline.tsx +11 -22
  8. package/src/components/ChatView.tsx +238 -0
  9. package/src/components/Message.tsx +46 -0
  10. package/src/components/ToolCall.tsx +67 -0
  11. package/src/components/chat/ChatView.tsx +550 -0
  12. package/src/components/chat/Message.tsx +152 -0
  13. package/src/components/chat/StatusBar.tsx +214 -0
  14. package/src/components/chat/TextArea.tsx +173 -176
  15. package/src/components/provider/ApiKeyStep.tsx +207 -199
  16. package/src/components/provider/ModelStep.tsx +90 -88
  17. package/src/components/provider/ProviderSetup.tsx +331 -0
  18. package/src/components/provider/ProviderTypeStep.tsx +53 -61
  19. package/src/components/repo/StepRow.tsx +68 -69
  20. package/src/components/timeline/TimelineView.tsx +840 -0
  21. package/src/components/toolcall-utils.ts +103 -0
  22. package/src/components/watch/RunView.tsx +497 -0
  23. package/src/hooks/useChatInput.ts +49 -0
  24. package/src/hooks/useCommandHandler.ts +117 -0
  25. package/src/index.tsx +386 -139
  26. package/src/utils/git.ts +149 -155
  27. package/src/utils/repo.ts +62 -69
  28. package/src/utils/thinking.tsx +64 -0
  29. package/src/utils/watch.ts +165 -307
  30. package/tests/message.test.ts +38 -0
  31. package/tests/toolcall-utils.test.ts +111 -0
  32. package/tsconfig.json +8 -24
  33. package/CLAUDE.md +0 -50
  34. package/LENS.md +0 -48
  35. package/LICENSE +0 -21
  36. package/README.md +0 -93
  37. package/addons/README.md +0 -55
  38. package/addons/clean-cache.js +0 -48
  39. package/addons/generate-readme.js +0 -67
  40. package/addons/git-stats.js +0 -29
  41. package/addons/run-tests.js +0 -127
  42. package/src/commands/commit.tsx +0 -668
  43. package/src/commands/review.tsx +0 -294
  44. package/src/commands/run.tsx +0 -56
  45. package/src/commands/task.tsx +0 -36
  46. package/src/components/chat/ChatMessage.tsx +0 -195
  47. package/src/components/chat/ChatOverlays.tsx +0 -399
  48. package/src/components/chat/ChatRunner.tsx +0 -517
  49. package/src/components/chat/hooks/useChat.ts +0 -631
  50. package/src/components/chat/hooks/useChatInput.ts +0 -79
  51. package/src/components/chat/hooks/useCommandHandlers.ts +0 -327
  52. package/src/components/provider/ProviderPicker.tsx +0 -76
  53. package/src/components/provider/RemoveProviderStep.tsx +0 -82
  54. package/src/components/repo/DiffViewer.tsx +0 -175
  55. package/src/components/repo/FileReviewer.tsx +0 -70
  56. package/src/components/repo/FileViewer.tsx +0 -60
  57. package/src/components/repo/IssueFixer.tsx +0 -666
  58. package/src/components/repo/LensFileMenu.tsx +0 -115
  59. package/src/components/repo/NoProviderPrompt.tsx +0 -28
  60. package/src/components/repo/PreviewRunner.tsx +0 -217
  61. package/src/components/repo/RepoAnalysis.tsx +0 -534
  62. package/src/components/task/TaskRunner.tsx +0 -396
  63. package/src/components/timeline/CommitDetail.tsx +0 -272
  64. package/src/components/timeline/CommitList.tsx +0 -162
  65. package/src/components/timeline/TimelineChat.tsx +0 -166
  66. package/src/components/timeline/TimelineRunner.tsx +0 -1285
  67. package/src/components/watch/RunRunner.tsx +0 -929
  68. package/src/prompts/fewshot.ts +0 -252
  69. package/src/prompts/index.ts +0 -2
  70. package/src/prompts/system.ts +0 -285
  71. package/src/tools/chart.ts +0 -202
  72. package/src/tools/convert-image.ts +0 -312
  73. package/src/tools/files.ts +0 -253
  74. package/src/tools/git.ts +0 -603
  75. package/src/tools/index.ts +0 -17
  76. package/src/tools/pdf.ts +0 -164
  77. package/src/tools/shell.ts +0 -96
  78. package/src/tools/view-image.ts +0 -335
  79. package/src/tools/web.ts +0 -212
  80. package/src/types/chat.ts +0 -86
  81. package/src/types/config.ts +0 -20
  82. package/src/types/repo.ts +0 -54
  83. package/src/utils/addons/loadAddons.ts +0 -34
  84. package/src/utils/ai.ts +0 -321
  85. package/src/utils/chat.ts +0 -326
  86. package/src/utils/chatHistory.ts +0 -121
  87. package/src/utils/config.ts +0 -61
  88. package/src/utils/files.ts +0 -105
  89. package/src/utils/intentClassifier.ts +0 -58
  90. package/src/utils/lensfile.ts +0 -142
  91. package/src/utils/llm.ts +0 -81
  92. package/src/utils/memory.ts +0 -209
  93. package/src/utils/preview.ts +0 -119
  94. package/src/utils/stats.ts +0 -174
  95. package/src/utils/tools/builtins.ts +0 -377
  96. package/src/utils/tools/registry.ts +0 -105
package/src/tools/pdf.ts DELETED
@@ -1,164 +0,0 @@
1
- // Generates PDFs from markdown content — useful for exporting codebase reports and documentation.
2
-
3
- import path from "path";
4
- import { existsSync, mkdirSync, writeFileSync } from "fs";
5
-
6
- export async function generatePdf(
7
- filePath: string,
8
- content: string,
9
- repoPath: string,
10
- ): Promise<string> {
11
- const fullPath = path.isAbsolute(filePath)
12
- ? filePath
13
- : path.join(repoPath, filePath);
14
-
15
- try {
16
- const dir = path.dirname(fullPath);
17
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
18
-
19
- const PDFDocument = require("pdfkit");
20
- const doc = new PDFDocument({ margin: 72 });
21
- const chunks: Buffer[] = [];
22
-
23
- doc.on("data", (chunk: Buffer) => chunks.push(chunk));
24
-
25
- return new Promise((resolve) => {
26
- doc.on("end", () => {
27
- try {
28
- writeFileSync(fullPath, Buffer.concat(chunks));
29
- resolve(`PDF generated: ${fullPath}`);
30
- } catch (err) {
31
- resolve(
32
- `Error writing PDF: ${err instanceof Error ? err.message : String(err)}`,
33
- );
34
- }
35
- });
36
-
37
- doc.on("error", (err: Error) => {
38
- resolve(`Error generating PDF: ${err.message}`);
39
- });
40
-
41
- const lines = content.split("\n");
42
- let firstElement = true;
43
-
44
- const addSpacingBefore = () => {
45
- if (!firstElement) doc.moveDown(0.5);
46
- firstElement = false;
47
- };
48
-
49
- const bullets: string[] = [];
50
-
51
- const flushBullets = () => {
52
- if (bullets.length === 0) return;
53
- addSpacingBefore();
54
- for (const bullet of bullets) {
55
- doc
56
- .fontSize(11)
57
- .fillColor("#333333")
58
- .text(`• ${bullet}`, { indent: 20, lineGap: 3 });
59
- }
60
- bullets.length = 0;
61
- };
62
-
63
- for (const raw of lines) {
64
- const line = raw.trimEnd();
65
-
66
- if (line.startsWith("# ")) {
67
- flushBullets();
68
- addSpacingBefore();
69
- doc
70
- .fontSize(22)
71
- .fillColor("#000000")
72
- .font("Helvetica-Bold")
73
- .text(line.slice(2));
74
- doc.moveDown(0.2);
75
- doc
76
- .moveTo(72, doc.y)
77
- .lineTo(doc.page.width - 72, doc.y)
78
- .strokeColor("#000000")
79
- .lineWidth(1)
80
- .stroke();
81
- doc.moveDown(0.4);
82
- doc.font("Helvetica");
83
- } else if (line.startsWith("## ")) {
84
- flushBullets();
85
- addSpacingBefore();
86
- doc
87
- .fontSize(16)
88
- .fillColor("#111111")
89
- .font("Helvetica-Bold")
90
- .text(line.slice(3));
91
- doc.moveDown(0.2);
92
- doc
93
- .moveTo(72, doc.y)
94
- .lineTo(doc.page.width - 72, doc.y)
95
- .strokeColor("#999999")
96
- .lineWidth(0.5)
97
- .stroke();
98
- doc.moveDown(0.3);
99
- doc.font("Helvetica");
100
- } else if (line.startsWith("### ")) {
101
- flushBullets();
102
- addSpacingBefore();
103
- doc
104
- .fontSize(13)
105
- .fillColor("#222222")
106
- .font("Helvetica-Bold")
107
- .text(line.slice(4));
108
- doc.font("Helvetica");
109
- } else if (line.startsWith("- ") || line.startsWith("* ")) {
110
- bullets.push(line.slice(2));
111
- } else if (line.startsWith("---")) {
112
- flushBullets();
113
- doc.moveDown(0.5);
114
- doc
115
- .moveTo(72, doc.y)
116
- .lineTo(doc.page.width - 72, doc.y)
117
- .strokeColor("#cccccc")
118
- .lineWidth(0.5)
119
- .stroke();
120
- doc.moveDown(0.5);
121
- } else if (line === "") {
122
- flushBullets();
123
- } else {
124
- flushBullets();
125
- addSpacingBefore();
126
- // handle inline bold (**text**) and inline code (`text`)
127
- const segments = line.split(/(\*\*[^*]+\*\*|`[^`]+`)/g);
128
- let isFirst = true;
129
- for (const seg of segments) {
130
- if (seg.startsWith("**") && seg.endsWith("**")) {
131
- doc
132
- .fontSize(11)
133
- .fillColor("#333333")
134
- .font("Helvetica-Bold")
135
- .text(seg.slice(2, -2), { continued: true, lineGap: 3 });
136
- doc.font("Helvetica");
137
- } else if (seg.startsWith("`") && seg.endsWith("`")) {
138
- doc
139
- .fontSize(10)
140
- .fillColor("#c0392b")
141
- .font("Courier")
142
- .text(seg.slice(1, -1), { continued: true, lineGap: 3 });
143
- doc.font("Helvetica").fontSize(11).fillColor("#333333");
144
- } else if (seg.length > 0) {
145
- doc
146
- .fontSize(11)
147
- .fillColor("#333333")
148
- .font("Helvetica")
149
- .text(seg, { continued: true, lineGap: 3 });
150
- }
151
- isFirst = false;
152
- }
153
- // end the continued text
154
- doc.text("", { continued: false });
155
- }
156
- }
157
-
158
- flushBullets();
159
- doc.end();
160
- });
161
- } catch (err) {
162
- return `Error generating PDF: ${err instanceof Error ? err.message : String(err)}`;
163
- }
164
- }
@@ -1,96 +0,0 @@
1
- import { execSync } from "child_process";
2
-
3
- export async function runShell(command: string, cwd: string): Promise<string> {
4
- return new Promise((resolve) => {
5
- const { spawn } =
6
- require("child_process") as typeof import("child_process");
7
- const isWin = process.platform === "win32";
8
- const shell = isWin ? "cmd.exe" : "/bin/sh";
9
- const shellFlag = isWin ? "/c" : "-c";
10
-
11
- const proc = spawn(shell, [shellFlag, command], {
12
- cwd,
13
- env: process.env,
14
- stdio: ["ignore", "pipe", "pipe"],
15
- });
16
-
17
- const chunks: Buffer[] = [];
18
- const errChunks: Buffer[] = [];
19
-
20
- proc.stdout.on("data", (d: Buffer) => chunks.push(d));
21
- proc.stderr.on("data", (d: Buffer) => errChunks.push(d));
22
-
23
- const killTimer = setTimeout(
24
- () => {
25
- proc.kill();
26
- resolve("(command timed out after 5 minutes)");
27
- },
28
- 5 * 60 * 1000,
29
- );
30
-
31
- proc.on("close", (code: number | null) => {
32
- clearTimeout(killTimer);
33
- const stdout = Buffer.concat(chunks).toString("utf-8").trim();
34
- const stderr = Buffer.concat(errChunks).toString("utf-8").trim();
35
- const combined = [stdout, stderr].filter(Boolean).join("\n");
36
- resolve(combined || (code === 0 ? "(no output)" : `exit code ${code}`));
37
- });
38
-
39
- proc.on("error", (err: Error) => {
40
- clearTimeout(killTimer);
41
- resolve(`Error: ${err.message}`);
42
- });
43
- });
44
- }
45
-
46
- export function readClipboard(): string {
47
- try {
48
- const platform = process.platform;
49
- if (platform === "win32") {
50
- return execSync("powershell -noprofile -command Get-Clipboard", {
51
- encoding: "utf-8",
52
- timeout: 2000,
53
- })
54
- .replace(/\r\n/g, "\n")
55
- .trimEnd();
56
- }
57
- if (platform === "darwin") {
58
- return execSync("pbpaste", {
59
- encoding: "utf-8",
60
- timeout: 2000,
61
- }).trimEnd();
62
- }
63
- for (const cmd of [
64
- "xclip -selection clipboard -o",
65
- "xsel --clipboard --output",
66
- "wl-paste",
67
- ]) {
68
- try {
69
- return execSync(cmd, { encoding: "utf-8", timeout: 2000 }).trimEnd();
70
- } catch {
71
- continue;
72
- }
73
- }
74
- return "";
75
- } catch {
76
- return "";
77
- }
78
- }
79
-
80
- export function openUrl(url: string): string {
81
- try {
82
- const { execSync: exec } =
83
- require("child_process") as typeof import("child_process");
84
- const platform = process.platform;
85
- if (platform === "win32") {
86
- exec(`start "" "${url}"`, { stdio: "ignore" });
87
- } else if (platform === "darwin") {
88
- exec(`open "${url}"`, { stdio: "ignore" });
89
- } else {
90
- exec(`xdg-open "${url}"`, { stdio: "ignore" });
91
- }
92
- return `Opened: ${url}`;
93
- } catch (err) {
94
- return `Error opening URL: ${err instanceof Error ? err.message : String(err)}`;
95
- }
96
- }
@@ -1,335 +0,0 @@
1
- // ── tools/view-image.ts ───────────────────────────────────────────────────────
2
- //
3
- // Display images in the terminal using the best available protocol:
4
- //
5
- // 1. iTerm2 inline image protocol (OSC 1337)
6
- // Supported by: iTerm2, WezTerm, VSCode integrated terminal,
7
- // Windows Terminal (via some builds), Tabby, Hyper
8
- //
9
- // 2. Kitty graphics protocol
10
- // Supported by: Kitty, WezTerm
11
- //
12
- // 3. Sixel
13
- // Supported by: Windows Terminal 1.22+, xterm, mlterm, WezTerm
14
- // Requires: chafa or img2sixel on PATH (auto-detected)
15
- //
16
- // 4. Half-block fallback via terminal-image
17
- // Works everywhere that supports 256 colors.
18
- // Quality is limited to block characters (▄▀) — better than nothing.
19
- //
20
- // Protocol is auto-detected from environment variables and terminal responses.
21
- // Can be overridden by passing "protocol" in the JSON input.
22
-
23
- import path from "path";
24
- import { existsSync, readFileSync } from "fs";
25
- import type { Tool } from "@ridit/lens-sdk";
26
- import { execSync } from "child_process";
27
-
28
- // ── input ─────────────────────────────────────────────────────────────────────
29
-
30
- type Protocol = "iterm2" | "kitty" | "sixel" | "halfblock" | "auto";
31
-
32
- interface ViewImageInput {
33
- /** File path (repo-relative or absolute) or URL */
34
- src: string;
35
- /** Width: percentage "50%" or column count 40. Default "80%" */
36
- width?: string | number;
37
- /** Force a specific protocol instead of auto-detecting */
38
- protocol?: Protocol;
39
- }
40
-
41
- function parseViewImageInput(body: string): ViewImageInput | null {
42
- const trimmed = body.trim();
43
- if (!trimmed) return null;
44
-
45
- if (trimmed.startsWith("{")) {
46
- try {
47
- const p = JSON.parse(trimmed) as {
48
- src?: string;
49
- path?: string;
50
- url?: string;
51
- width?: string | number;
52
- protocol?: Protocol;
53
- };
54
- const src = p.src ?? p.path ?? p.url ?? "";
55
- if (!src) return null;
56
- return { src, width: p.width ?? "80%", protocol: p.protocol ?? "auto" };
57
- } catch {
58
- return null;
59
- }
60
- }
61
-
62
- return { src: trimmed, width: "80%", protocol: "auto" };
63
- }
64
-
65
- // ── fetch image bytes ─────────────────────────────────────────────────────────
66
-
67
- async function fetchBytes(src: string, repoPath: string): Promise<Buffer> {
68
- if (src.startsWith("http://") || src.startsWith("https://")) {
69
- const res = await fetch(src, { signal: AbortSignal.timeout(20_000) });
70
- if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
71
- return Buffer.from(await res.arrayBuffer());
72
- }
73
- const resolved = path.isAbsolute(src) ? src : path.join(repoPath, src);
74
- if (!existsSync(resolved)) throw new Error(`File not found: ${resolved}`);
75
- return readFileSync(resolved);
76
- }
77
-
78
- // ── protocol detection ────────────────────────────────────────────────────────
79
-
80
- function detectProtocol(): Exclude<Protocol, "auto"> {
81
- const term = (process.env["TERM"] ?? "").toLowerCase();
82
- const termProgram = (process.env["TERM_PROGRAM"] ?? "").toLowerCase();
83
- const termEmulator = (process.env["TERM_EMULATOR"] ?? "").toLowerCase();
84
-
85
- // Kitty
86
- if (term === "xterm-kitty" || process.env["KITTY_WINDOW_ID"]) {
87
- return "kitty";
88
- }
89
-
90
- // iTerm2
91
- if (
92
- termProgram === "iterm.app" ||
93
- termProgram === "iterm2" ||
94
- process.env["ITERM_SESSION_ID"]
95
- ) {
96
- return "iterm2";
97
- }
98
-
99
- // WezTerm — supports both kitty and iterm2; prefer kitty
100
- if (termProgram === "wezterm" || process.env["WEZTERM_EXECUTABLE"]) {
101
- return "kitty";
102
- }
103
-
104
- // VSCode integrated terminal — supports iTerm2 protocol
105
- if (
106
- process.env["TERM_PROGRAM"] === "vscode" ||
107
- process.env["VSCODE_INJECTION"] ||
108
- process.env["VSCODE_GIT_ASKPASS_NODE"]
109
- ) {
110
- return "iterm2";
111
- }
112
-
113
- // Windows Terminal 1.22+ supports Sixel
114
- // WT_SESSION is set in Windows Terminal
115
- if (process.env["WT_SESSION"]) {
116
- // Check if chafa or img2sixel is available for sixel encoding
117
- if (commandExists("chafa") || commandExists("img2sixel")) {
118
- return "sixel";
119
- }
120
- // Fall through to halfblock — Windows Terminal also does colors fine
121
- }
122
-
123
- // Tabby / Hyper typically support iTerm2
124
- if (termEmulator.includes("tabby") || termEmulator.includes("hyper")) {
125
- return "iterm2";
126
- }
127
-
128
- // xterm with sixel support
129
- if (term.includes("xterm") && commandExists("img2sixel")) {
130
- return "sixel";
131
- }
132
-
133
- return "halfblock";
134
- }
135
-
136
- function commandExists(cmd: string): boolean {
137
- try {
138
- execSync(process.platform === "win32" ? `where ${cmd}` : `which ${cmd}`, {
139
- stdio: "pipe",
140
- timeout: 2000,
141
- });
142
- return true;
143
- } catch {
144
- return false;
145
- }
146
- }
147
-
148
- // ── renderers ─────────────────────────────────────────────────────────────────
149
-
150
- // iTerm2 inline image protocol (OSC 1337)
151
- function renderITerm2(buf: Buffer, width: string | number): string {
152
- const b64 = buf.toString("base64");
153
- const widthParam =
154
- typeof width === "number" ? `width=${width}` : `width=${width}`;
155
- // ESC ] 1337 ; File=inline=1;width=...: <base64> BEL
156
- return `\x1b]1337;File=inline=1;${widthParam};preserveAspectRatio=1:${b64}\x07\n`;
157
- }
158
-
159
- // Kitty graphics protocol (APC, chunked base64)
160
- function renderKitty(buf: Buffer, width: string | number): string {
161
- const b64 = buf.toString("base64");
162
- const chunkSize = 4096;
163
- const chunks: string[] = [];
164
- for (let i = 0; i < b64.length; i += chunkSize) {
165
- chunks.push(b64.slice(i, i + chunkSize));
166
- }
167
-
168
- const cols =
169
- typeof width === "number"
170
- ? width
171
- : width.endsWith("%")
172
- ? Math.floor((process.stdout.columns ?? 80) * (parseInt(width) / 100))
173
- : parseInt(width);
174
-
175
- let result = "";
176
- chunks.forEach((chunk, idx) => {
177
- const isLast = idx === chunks.length - 1;
178
- const more = isLast ? 0 : 1;
179
- if (idx === 0) {
180
- // First chunk: f=100 (PNG), a=T (transmit+display), c=columns
181
- result += `\x1b_Ga=T,f=100,m=${more},c=${cols};${chunk}\x1b\\`;
182
- } else {
183
- result += `\x1b_Gm=${more};${chunk}\x1b\\`;
184
- }
185
- });
186
- return result + "\n";
187
- }
188
-
189
- // Sixel via chafa (preferred, auto-detects format) or img2sixel
190
- function renderSixel(
191
- buf: Buffer,
192
- src: string,
193
- repoPath: string,
194
- width: string | number,
195
- ): string {
196
- const widthCols =
197
- typeof width === "number"
198
- ? width
199
- : width.endsWith("%")
200
- ? Math.floor((process.stdout.columns ?? 80) * (parseInt(width) / 100))
201
- : parseInt(width);
202
-
203
- // Write buf to a temp file so we can pass it to CLI tools
204
- const tmpPath = path.join(
205
- process.env["TEMP"] ?? process.env["TMPDIR"] ?? "/tmp",
206
- `lens_img_${Date.now()}.bin`,
207
- );
208
- const { writeFileSync, unlinkSync } = require("fs") as typeof import("fs");
209
- writeFileSync(tmpPath, buf);
210
-
211
- try {
212
- if (commandExists("chafa")) {
213
- const result = execSync(
214
- `chafa --format sixel --size ${widthCols}x40 "${tmpPath}"`,
215
- { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15_000 },
216
- );
217
- return result + "\n";
218
- }
219
- if (commandExists("img2sixel")) {
220
- const result = execSync(`img2sixel -w ${widthCols * 8} "${tmpPath}"`, {
221
- encoding: "utf-8",
222
- stdio: ["pipe", "pipe", "pipe"],
223
- timeout: 15_000,
224
- });
225
- return result + "\n";
226
- }
227
- throw new Error("no sixel encoder found");
228
- } finally {
229
- try {
230
- unlinkSync(tmpPath);
231
- } catch {
232
- /* ignore */
233
- }
234
- }
235
- }
236
-
237
- // Half-block fallback via terminal-image
238
- async function renderHalfBlock(
239
- buf: Buffer,
240
- width: string | number,
241
- ): Promise<string> {
242
- let terminalImage: any;
243
- try {
244
- terminalImage = await import("terminal-image");
245
- terminalImage = terminalImage.default ?? terminalImage;
246
- } catch {
247
- return (
248
- "Error: terminal-image not installed (npm install terminal-image).\n" +
249
- "For better image display, install chafa: https://hpjansson.org/chafa/\n" +
250
- " Windows: winget install hpjansson.chafa\n" +
251
- " macOS: brew install chafa\n" +
252
- " Linux: sudo apt install chafa"
253
- );
254
- }
255
- return await terminalImage.buffer(buf, {
256
- width,
257
- preserveAspectRatio: true,
258
- });
259
- }
260
-
261
- // ── main render ───────────────────────────────────────────────────────────────
262
-
263
- async function renderImage(
264
- input: ViewImageInput,
265
- repoPath: string,
266
- ): Promise<string> {
267
- const buf = await fetchBytes(input.src, repoPath);
268
- const width = input.width ?? "80%";
269
- const protocol =
270
- input.protocol === "auto" || !input.protocol
271
- ? detectProtocol()
272
- : input.protocol;
273
-
274
- switch (protocol) {
275
- case "iterm2":
276
- return renderITerm2(buf, width);
277
-
278
- case "kitty":
279
- return renderKitty(buf, width);
280
-
281
- case "sixel":
282
- try {
283
- return renderSixel(buf, input.src, repoPath, width);
284
- } catch (e: any) {
285
- // Sixel encoder missing — fall through to halfblock
286
- return (
287
- `(sixel encoder not found — install chafa for better quality)\n` +
288
- (await renderHalfBlock(buf, width))
289
- );
290
- }
291
-
292
- case "halfblock":
293
- default:
294
- return await renderHalfBlock(buf, width);
295
- }
296
- }
297
-
298
- // ── tool ──────────────────────────────────────────────────────────────────────
299
-
300
- export const viewImageTool: Tool<ViewImageInput> = {
301
- name: "view-image",
302
- description:
303
- "display an image in the terminal (auto-detects iTerm2/Kitty/Sixel/half-block)",
304
- safe: true,
305
- permissionLabel: "view image",
306
-
307
- systemPromptEntry: (i) =>
308
- [
309
- `### ${i}. view-image — display an image in the terminal`,
310
- `<view-image>screenshot.png</view-image>`,
311
- `<view-image>https://example.com/banner.jpg</view-image>`,
312
- `<view-image>{"src": "assets/logo.png", "width": "60%"}</view-image>`,
313
- `<view-image>{"src": "photo.jpg", "width": "80%", "protocol": "iterm2"}</view-image>`,
314
- `Protocols: auto (default), iterm2, kitty, sixel, halfblock`,
315
- `Width: percentage "50%" or column count 40. Default "80%"`,
316
- `Supported formats: PNG, JPG, GIF, WebP, BMP, TIFF`,
317
- ].join("\n"),
318
-
319
- parseInput: parseViewImageInput,
320
-
321
- summariseInput: ({ src, width }) =>
322
- `${path.basename(src)} (${width ?? "80%"})`,
323
-
324
- execute: async (input, ctx) => {
325
- try {
326
- const ansi = await renderImage(input, ctx.repoPath);
327
- return { kind: "image" as any, value: ansi };
328
- } catch (err) {
329
- return {
330
- kind: "text",
331
- value: `Error: ${err instanceof Error ? err.message : String(err)}`,
332
- };
333
- }
334
- },
335
- };