@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.
- package/dist/index.mjs +105368 -274002
- package/package.json +13 -19
- package/src/colors.ts +15 -15
- package/src/commands/chat.tsx +32 -23
- package/src/commands/provider.tsx +11 -238
- package/src/commands/repo.tsx +66 -120
- package/src/commands/timeline.tsx +11 -22
- package/src/components/ChatView.tsx +238 -0
- package/src/components/Message.tsx +46 -0
- package/src/components/ToolCall.tsx +67 -0
- package/src/components/chat/ChatView.tsx +550 -0
- package/src/components/chat/Message.tsx +152 -0
- package/src/components/chat/StatusBar.tsx +214 -0
- package/src/components/chat/TextArea.tsx +173 -176
- package/src/components/provider/ApiKeyStep.tsx +207 -199
- package/src/components/provider/ModelStep.tsx +90 -88
- package/src/components/provider/ProviderSetup.tsx +331 -0
- package/src/components/provider/ProviderTypeStep.tsx +53 -61
- package/src/components/repo/StepRow.tsx +68 -69
- package/src/components/timeline/TimelineView.tsx +840 -0
- package/src/components/toolcall-utils.ts +103 -0
- package/src/components/watch/RunView.tsx +497 -0
- package/src/hooks/useChatInput.ts +49 -0
- package/src/hooks/useCommandHandler.ts +117 -0
- package/src/index.tsx +386 -139
- package/src/utils/git.ts +149 -155
- package/src/utils/repo.ts +62 -69
- package/src/utils/thinking.tsx +64 -0
- package/src/utils/watch.ts +165 -307
- package/tests/message.test.ts +38 -0
- package/tests/toolcall-utils.test.ts +111 -0
- package/tsconfig.json +8 -24
- package/CLAUDE.md +0 -50
- package/LENS.md +0 -48
- package/LICENSE +0 -21
- package/README.md +0 -93
- package/addons/README.md +0 -55
- package/addons/clean-cache.js +0 -48
- package/addons/generate-readme.js +0 -67
- package/addons/git-stats.js +0 -29
- package/addons/run-tests.js +0 -127
- package/src/commands/commit.tsx +0 -668
- package/src/commands/review.tsx +0 -294
- package/src/commands/run.tsx +0 -56
- package/src/commands/task.tsx +0 -36
- package/src/components/chat/ChatMessage.tsx +0 -195
- package/src/components/chat/ChatOverlays.tsx +0 -399
- package/src/components/chat/ChatRunner.tsx +0 -517
- package/src/components/chat/hooks/useChat.ts +0 -631
- package/src/components/chat/hooks/useChatInput.ts +0 -79
- package/src/components/chat/hooks/useCommandHandlers.ts +0 -327
- package/src/components/provider/ProviderPicker.tsx +0 -76
- package/src/components/provider/RemoveProviderStep.tsx +0 -82
- package/src/components/repo/DiffViewer.tsx +0 -175
- package/src/components/repo/FileReviewer.tsx +0 -70
- package/src/components/repo/FileViewer.tsx +0 -60
- package/src/components/repo/IssueFixer.tsx +0 -666
- package/src/components/repo/LensFileMenu.tsx +0 -115
- package/src/components/repo/NoProviderPrompt.tsx +0 -28
- package/src/components/repo/PreviewRunner.tsx +0 -217
- package/src/components/repo/RepoAnalysis.tsx +0 -534
- package/src/components/task/TaskRunner.tsx +0 -396
- package/src/components/timeline/CommitDetail.tsx +0 -272
- package/src/components/timeline/CommitList.tsx +0 -162
- package/src/components/timeline/TimelineChat.tsx +0 -166
- package/src/components/timeline/TimelineRunner.tsx +0 -1285
- package/src/components/watch/RunRunner.tsx +0 -929
- package/src/prompts/fewshot.ts +0 -252
- package/src/prompts/index.ts +0 -2
- package/src/prompts/system.ts +0 -285
- package/src/tools/chart.ts +0 -202
- package/src/tools/convert-image.ts +0 -312
- package/src/tools/files.ts +0 -253
- package/src/tools/git.ts +0 -603
- package/src/tools/index.ts +0 -17
- package/src/tools/pdf.ts +0 -164
- package/src/tools/shell.ts +0 -96
- package/src/tools/view-image.ts +0 -335
- package/src/tools/web.ts +0 -212
- package/src/types/chat.ts +0 -86
- package/src/types/config.ts +0 -20
- package/src/types/repo.ts +0 -54
- package/src/utils/addons/loadAddons.ts +0 -34
- package/src/utils/ai.ts +0 -321
- package/src/utils/chat.ts +0 -326
- package/src/utils/chatHistory.ts +0 -121
- package/src/utils/config.ts +0 -61
- package/src/utils/files.ts +0 -105
- package/src/utils/intentClassifier.ts +0 -58
- package/src/utils/lensfile.ts +0 -142
- package/src/utils/llm.ts +0 -81
- package/src/utils/memory.ts +0 -209
- package/src/utils/preview.ts +0 -119
- package/src/utils/stats.ts +0 -174
- package/src/utils/tools/builtins.ts +0 -377
- 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
|
-
}
|
package/src/tools/shell.ts
DELETED
|
@@ -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
|
-
}
|
package/src/tools/view-image.ts
DELETED
|
@@ -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
|
-
};
|