@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
@@ -1,202 +0,0 @@
1
- import type { Tool } from "@ridit/lens-sdk";
2
-
3
- type ChartType = "bar" | "line" | "sparkline";
4
-
5
- interface ChartInput {
6
- type: ChartType;
7
- title?: string;
8
- labels?: string[];
9
- values: number[];
10
- height?: number;
11
- fill?: string;
12
- }
13
-
14
- function parseChartInput(body: string): ChartInput | null {
15
- const trimmed = body.trim();
16
- if (!trimmed) return null;
17
- try {
18
- const parsed = JSON.parse(trimmed) as Partial<ChartInput> & {
19
- data?: number[];
20
- series?: number[];
21
- };
22
- const values = parsed.values ?? parsed.data ?? parsed.series ?? [];
23
- if (!Array.isArray(values) || values.length === 0) return null;
24
- return {
25
- type: parsed.type ?? "bar",
26
- title: parsed.title,
27
- labels: parsed.labels,
28
- values: values.map(Number),
29
- height: parsed.height ?? 10,
30
- fill: parsed.fill ?? "█",
31
- };
32
- } catch {
33
- return null;
34
- }
35
- }
36
-
37
- const C = {
38
- reset: "\x1b[0m",
39
- dim: "\x1b[2m",
40
- bold: "\x1b[1m",
41
- orange: "\x1b[38;2;218;119;88m",
42
- cyan: "\x1b[36m",
43
- white: "\x1b[37m",
44
- gray: "\x1b[90m",
45
- green: "\x1b[32m",
46
- yellow: "\x1b[33m",
47
- };
48
-
49
- const PALETTE = [C.orange, C.cyan, C.green, C.yellow];
50
- function color(i: number) {
51
- return PALETTE[i % PALETTE.length]!;
52
- }
53
-
54
- function renderBar(input: ChartInput): string {
55
- const { values, labels, title, fill = "█" } = input;
56
- const max = Math.max(...values, 1);
57
- const termW = process.stdout.columns ?? 80;
58
- const maxLabelLen = labels
59
- ? Math.max(...labels.map((l) => l.length), 0)
60
- : String(values.length).length + 1;
61
- const barMaxW = Math.max(20, termW - maxLabelLen - 12);
62
-
63
- const lines: string[] = [];
64
- if (title) lines.push(`${C.bold}${C.white}${title}${C.reset}\n`);
65
-
66
- values.forEach((v, i) => {
67
- const label = labels?.[i] ?? String(i + 1);
68
- const barLen = Math.round((v / max) * barMaxW);
69
- const bar = fill.repeat(barLen);
70
- const valueStr = String(v);
71
- lines.push(
72
- `${C.gray}${label.padStart(maxLabelLen)}${C.reset} ` +
73
- `${color(i)}${bar}${C.reset} ` +
74
- `${C.dim}${valueStr}${C.reset}`,
75
- );
76
- });
77
-
78
- lines.push(
79
- `${" ".repeat(maxLabelLen + 1)}${C.gray}${"─".repeat(barMaxW)}${C.reset}`,
80
- );
81
- lines.push(
82
- `${" ".repeat(maxLabelLen + 1)}${C.gray}0${" ".repeat(barMaxW - String(max).length)}${max}${C.reset}`,
83
- );
84
-
85
- return lines.join("\n");
86
- }
87
-
88
- const SPARK_CHARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
89
-
90
- function renderSparkline(input: ChartInput): string {
91
- const { values, title } = input;
92
- const min = Math.min(...values);
93
- const max = Math.max(...values, min + 1);
94
- const range = max - min;
95
- const spark = values
96
- .map((v) => {
97
- const idx = Math.floor(((v - min) / range) * (SPARK_CHARS.length - 1));
98
- return `${color(0)}${SPARK_CHARS[idx] ?? "▁"}${C.reset}`;
99
- })
100
- .join("");
101
-
102
- const lines: string[] = [];
103
- if (title) lines.push(`${C.bold}${C.white}${title}${C.reset}`);
104
- lines.push(spark);
105
- lines.push(`${C.gray}min ${min} max ${max} n=${values.length}${C.reset}`);
106
- return lines.join("\n");
107
- }
108
-
109
- async function renderLine(input: ChartInput): Promise<string> {
110
- let asciichart: any;
111
- try {
112
- asciichart = await import("asciichart");
113
- asciichart = asciichart.default ?? asciichart;
114
- } catch {
115
- return (
116
- `${C.yellow}asciichart not installed (npm install asciichart). ` +
117
- `Falling back to sparkline:${C.reset}\n` +
118
- renderSparkline(input)
119
- );
120
- }
121
-
122
- const termW = process.stdout.columns ?? 80;
123
- const height = input.height ?? 10;
124
-
125
- const lines: string[] = [];
126
- if (input.title) {
127
- lines.push(`${C.bold}${C.white}${input.title}${C.reset}\n`);
128
- }
129
-
130
- const chart = asciichart.plot(input.values, {
131
- height,
132
- width: Math.min(input.values.length, termW - 14),
133
- colors: [asciichart.cyan],
134
- });
135
-
136
- lines.push(chart);
137
-
138
- if (input.labels && input.labels.length === input.values.length) {
139
- const step = Math.max(
140
- 1,
141
- Math.floor(input.labels.length / Math.min(input.labels.length, 10)),
142
- );
143
- const labelRow = input.labels
144
- .filter((_, i) => i % step === 0)
145
- .map((l) => l.slice(0, 6).padEnd(6))
146
- .join(" ");
147
- lines.push(`${C.gray}${" ".repeat(8)}${labelRow}${C.reset}`);
148
- }
149
-
150
- return lines.join("\n");
151
- }
152
-
153
- async function renderChart(input: ChartInput): Promise<string> {
154
- switch (input.type) {
155
- case "bar":
156
- return renderBar(input);
157
- case "sparkline":
158
- return renderSparkline(input);
159
- case "line":
160
- return await renderLine(input);
161
- default:
162
- return renderBar(input);
163
- }
164
- }
165
-
166
- export const chartDataTool: Tool<ChartInput> = {
167
- name: "chart-data",
168
- description:
169
- "render a bar, line, or sparkline chart in the terminal from given data",
170
- safe: true,
171
- permissionLabel: "chart",
172
-
173
- systemPromptEntry: (i) =>
174
- `### ${i}. chart-data — render a chart in the terminal\n` +
175
- `Types: "bar" (default), "line", "sparkline"\n` +
176
- `<chart-data>\n` +
177
- `{"type": "bar", "title": "Commits per month", "labels": ["Jan","Feb","Mar"], "values": [12, 34, 21]}\n` +
178
- `</chart-data>\n` +
179
- `<chart-data>\n` +
180
- `{"type": "line", "title": "Stars over time", "values": [1,3,6,10,15,21,28], "height": 8}\n` +
181
- `</chart-data>\n` +
182
- `<chart-data>\n` +
183
- `{"type": "sparkline", "title": "Daily commits", "values": [2,5,1,8,3,7,4]}\n` +
184
- `</chart-data>`,
185
-
186
- parseInput: parseChartInput,
187
-
188
- summariseInput: ({ type, title }) =>
189
- title ? `${type} chart — ${title}` : `${type} chart`,
190
-
191
- execute: async (input, _ctx) => {
192
- try {
193
- const rendered = await renderChart(input);
194
- return { kind: "image" as any, value: rendered };
195
- } catch (err) {
196
- return {
197
- kind: "text",
198
- value: `Error rendering chart: ${err instanceof Error ? err.message : String(err)}`,
199
- };
200
- }
201
- },
202
- };
@@ -1,312 +0,0 @@
1
- // this tool will be used by view-image tool for conversion of unsupported image formats
2
-
3
- import path from "path";
4
- import { existsSync, mkdirSync } from "fs";
5
- import { execSync } from "child_process";
6
- import type { Tool } from "@ridit/lens-sdk";
7
-
8
- interface ConvertImageInput {
9
- input: string | string[];
10
-
11
- output: string;
12
-
13
- resize?: string;
14
-
15
- crop?: string;
16
-
17
- rotate?: 90 | 180 | 270;
18
-
19
- flip?: "h" | "v" | "both";
20
-
21
- quality?: number;
22
-
23
- grayscale?: boolean;
24
-
25
- blur?: number;
26
-
27
- sharpen?: number;
28
-
29
- strip?: boolean;
30
-
31
- watermark?: string;
32
-
33
- frames?: number;
34
-
35
- gifDelay?: number;
36
-
37
- gifLoop?: number;
38
- }
39
-
40
- function parseInput(body: string): ConvertImageInput | null {
41
- const trimmed = body.trim();
42
- if (!trimmed) return null;
43
- try {
44
- const parsed = JSON.parse(trimmed) as ConvertImageInput;
45
- if (!parsed.input || !parsed.output) return null;
46
- return parsed;
47
- } catch {
48
- return null;
49
- }
50
- }
51
-
52
- function ffmpegAvailable(): boolean {
53
- try {
54
- execSync("ffmpeg -version", { stdio: "pipe", timeout: 5000 });
55
- return true;
56
- } catch {
57
- return false;
58
- }
59
- }
60
-
61
- function resolve(p: string, repoPath: string): string {
62
- return path.isAbsolute(p) ? p : path.join(repoPath, p);
63
- }
64
-
65
- function ensureDir(filePath: string): void {
66
- const dir = path.dirname(filePath);
67
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
68
- }
69
-
70
- function buildVfFilters(input: ConvertImageInput): string[] {
71
- const filters: string[] = [];
72
-
73
- if (input.resize) {
74
- const [w, h] = input.resize.split("x");
75
- const fw = w === "-1" ? "-2" : (w ?? "-2");
76
- const fh = h === "-1" ? "-2" : (h ?? "-2");
77
- filters.push(`scale=${fw}:${fh}`);
78
- }
79
-
80
- if (input.crop) {
81
- const parts = input.crop.split(/[x:]/);
82
- const [cw, ch, cx = "0", cy = "0"] = parts;
83
- filters.push(`crop=${cw}:${ch}:${cx}:${cy}`);
84
- }
85
-
86
- if (input.rotate) {
87
- const transposeMap: Record<number, string> = {
88
- 90: "transpose=1",
89
- 180: "transpose=2,transpose=2",
90
- 270: "transpose=2",
91
- };
92
- if (transposeMap[input.rotate]) filters.push(transposeMap[input.rotate]!);
93
- }
94
-
95
- if (input.flip) {
96
- if (input.flip === "h" || input.flip === "both") filters.push("hflip");
97
- if (input.flip === "v" || input.flip === "both") filters.push("vflip");
98
- }
99
-
100
- if (input.grayscale) {
101
- filters.push("hue=s=0");
102
- }
103
-
104
- if (input.blur && input.blur > 0) {
105
- filters.push(`gblur=sigma=${input.blur}`);
106
- }
107
-
108
- if (input.sharpen && input.sharpen > 0) {
109
- filters.push(`unsharp=5:5:${input.sharpen}:5:5:0`);
110
- }
111
-
112
- return filters;
113
- }
114
-
115
- function buildArgs(
116
- input: ConvertImageInput,
117
- resolvedInput: string,
118
- resolvedOutput: string,
119
- repoPath: string,
120
- ): string[] {
121
- const args: string[] = ["-y"];
122
-
123
- if (Array.isArray(input.input)) {
124
- const delay = input.gifDelay ?? 10;
125
- const loop = input.gifLoop ?? 0;
126
-
127
- for (const src of input.input) {
128
- args.push("-i", resolve(src, repoPath));
129
- }
130
- args.push(
131
- "-filter_complex",
132
- `concat=n=${input.input.length}:v=1:a=0,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse`,
133
- "-loop",
134
- String(loop),
135
- resolvedOutput,
136
- );
137
- return args;
138
- }
139
-
140
- if (input.frames) {
141
- args.push("-i", resolvedInput);
142
- args.push("-vframes", String(input.frames));
143
-
144
- args.push(resolvedOutput);
145
- return args;
146
- }
147
-
148
- if (input.watermark) {
149
- const wmPath = resolve(input.watermark, repoPath);
150
- args.push("-i", resolvedInput, "-i", wmPath);
151
- args.push(
152
- "-filter_complex",
153
- "[1:v]scale=iw/4:-1[wm];[0:v][wm]overlay=W-w-10:H-h-10",
154
- );
155
- const vf = buildVfFilters(input);
156
- if (vf.length) args.push("-vf", vf.join(","));
157
- } else {
158
- args.push("-i", resolvedInput);
159
- const vf = buildVfFilters(input);
160
- if (vf.length) args.push("-vf", vf.join(","));
161
- }
162
-
163
- const ext = path.extname(resolvedOutput).toLowerCase();
164
- if (input.quality !== undefined) {
165
- if (ext === ".jpg" || ext === ".jpeg") {
166
- const q = Math.round(2 + ((100 - input.quality) / 100) * 29);
167
- args.push("-q:v", String(q));
168
- } else if (ext === ".webp") {
169
- args.push("-quality", String(input.quality));
170
- }
171
- }
172
-
173
- if (input.strip) {
174
- args.push("-map_metadata", "-1");
175
- }
176
-
177
- const imageExts = [
178
- ".png",
179
- ".jpg",
180
- ".jpeg",
181
- ".webp",
182
- ".avif",
183
- ".bmp",
184
- ".tiff",
185
- ".gif",
186
- ];
187
- if (imageExts.includes(ext) && !input.frames) {
188
- args.push("-frames:v", "1");
189
- }
190
-
191
- args.push(resolvedOutput);
192
- return args;
193
- }
194
-
195
- function runConvert(input: ConvertImageInput, repoPath: string): string {
196
- if (!ffmpegAvailable()) {
197
- return (
198
- "Error: ffmpeg is not installed or not on PATH.\n" +
199
- "Install it from https://ffmpeg.org/download.html or:\n" +
200
- " Windows: winget install ffmpeg\n" +
201
- " macOS: brew install ffmpeg\n" +
202
- " Linux: sudo apt install ffmpeg"
203
- );
204
- }
205
-
206
- const resolvedOutput = resolve(input.output, repoPath);
207
- ensureDir(resolvedOutput);
208
-
209
- const resolvedInput = Array.isArray(input.input)
210
- ? resolve(input.input[0]!, repoPath)
211
- : resolve(input.input, repoPath);
212
-
213
- if (!Array.isArray(input.input) && !input.input.startsWith("http")) {
214
- if (!existsSync(resolvedInput)) {
215
- return `Error: input file not found — ${resolvedInput}`;
216
- }
217
- }
218
-
219
- const args = buildArgs(input, resolvedInput, resolvedOutput, repoPath);
220
- const cmd = `ffmpeg ${args.map((a) => `"${a}"`).join(" ")}`;
221
-
222
- try {
223
- const stderr = execSync(cmd, {
224
- stdio: ["pipe", "pipe", "pipe"],
225
- timeout: 120_000,
226
- encoding: "utf-8",
227
- });
228
-
229
- const lines = (stderr || "")
230
- .split("\n")
231
- .filter((l) => l.includes("video:") || l.includes("frame="))
232
- .slice(-3)
233
- .join("\n");
234
-
235
- const inputLabel = Array.isArray(input.input)
236
- ? `${input.input.length} files`
237
- : path.basename(input.input as string);
238
-
239
- return (
240
- `✓ converted ${inputLabel} → ${input.output}\n` +
241
- (lines ? `\n${lines}` : "")
242
- ).trim();
243
- } catch (e: any) {
244
- const errOut = (e.stderr ?? e.stdout ?? e.message ?? String(e))
245
- .split("\n")
246
- .filter((l: string) => l.trim() && !l.startsWith("ffmpeg version"))
247
- .slice(-6)
248
- .join("\n");
249
- return `Error: ${errOut}`;
250
- }
251
- }
252
-
253
- export const convertImageTool: Tool<ConvertImageInput> = {
254
- name: "convert-image",
255
- description:
256
- "convert, resize, crop, rotate, compress, or reformat images using ffmpeg",
257
- safe: false,
258
- permissionLabel: "convert image",
259
-
260
- systemPromptEntry: (i) =>
261
- [
262
- `### ${i}. convert-image — image conversion and manipulation via ffmpeg`,
263
- `<convert-image>`,
264
- `{"input": "photo.png", "output": "photo.webp", "quality": 85, "resize": "1280x-1"}`,
265
- `</convert-image>`,
266
- `Fields (all optional except input/output):`,
267
- ` input — source path or array of paths (for gif assembly)`,
268
- ` output — destination path; extension sets format (png/jpg/webp/avif/gif/bmp/tiff)`,
269
- ` resize — "WxH" or "Wx-1" (keep aspect) e.g. "800x-1" or "400x300"`,
270
- ` crop — "WxH:X:Y" e.g. "400x300:100:50"`,
271
- ` rotate — 90 | 180 | 270`,
272
- ` flip — "h" | "v" | "both"`,
273
- ` quality — 1–100 (jpg/webp)`,
274
- ` grayscale — true`,
275
- ` blur — gaussian blur radius e.g. 5`,
276
- ` sharpen — unsharp strength e.g. 1.5`,
277
- ` strip — true to remove EXIF metadata`,
278
- ` watermark — path to overlay image (bottom-right, 25% size)`,
279
- ` frames — extract N frames from video/gif (output must contain %04d e.g. frame%04d.png)`,
280
- ` gifDelay — frame delay in centiseconds for gif assembly (default 10)`,
281
- ` gifLoop — gif loop count, 0 = infinite (default 0)`,
282
- `Examples:`,
283
- ` Convert format: {"input":"a.png","output":"a.jpg","quality":90}`,
284
- ` Resize: {"input":"big.jpg","output":"thumb.jpg","resize":"400x-1"}`,
285
- ` Rotate + grayscale:{"input":"photo.jpg","output":"out.jpg","rotate":90,"grayscale":true}`,
286
- ` Strip EXIF: {"input":"photo.jpg","output":"clean.jpg","strip":true}`,
287
- ` Assemble gif: {"input":["f1.png","f2.png","f3.png"],"output":"anim.gif","gifDelay":15}`,
288
- ` Extract frames: {"input":"clip.gif","output":"frame%04d.png","frames":10}`,
289
- ` Watermark: {"input":"photo.jpg","output":"marked.jpg","watermark":"logo.png"}`,
290
- ].join("\n"),
291
-
292
- parseInput,
293
-
294
- summariseInput: ({ input, output }) => {
295
- const src = Array.isArray(input)
296
- ? `${input.length} files`
297
- : path.basename(input as string);
298
- return `${src} → ${path.basename(output)}`;
299
- },
300
-
301
- execute: async (input, ctx) => {
302
- try {
303
- const result = runConvert(input, ctx.repoPath);
304
- return { kind: "text", value: result };
305
- } catch (err) {
306
- return {
307
- kind: "text",
308
- value: `Error: ${err instanceof Error ? err.message : String(err)}`,
309
- };
310
- }
311
- },
312
- };