@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,294 +0,0 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
3
- import figures from "figures";
4
- import { useEffect, useState } from "react";
5
- import path from "path";
6
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
7
- import { fetchFileTree, readImportantFiles } from "../utils/files";
8
- import { computeStats, formatNumber, topLanguages } from "../utils/stats";
9
- import { RepoAnalysis } from "../components/repo/RepoAnalysis";
10
- import { LensFileMenu } from "../components/repo/LensFileMenu";
11
- import {
12
- lensFileExists,
13
- readLensFile,
14
- lensFileToAnalysisResult,
15
- } from "../utils/lensfile";
16
- import type { ImportantFile } from "../types/repo";
17
- import type { CodeStats } from "../utils/stats";
18
- import type { LensMenuChoice } from "../components/repo/LensFileMenu";
19
-
20
- type ReviewStage =
21
- | { type: "scanning" }
22
- | {
23
- type: "lens-menu";
24
- fileTree: string[];
25
- files: ImportantFile[];
26
- stats: CodeStats;
27
- }
28
- | {
29
- type: "stats";
30
- stats: CodeStats;
31
- files: ImportantFile[];
32
- fileTree: string[];
33
- }
34
- | { type: "error"; message: string };
35
-
36
- function StatRow({ label, value }: { label: string; value: string }) {
37
- const PAD = 20;
38
- return (
39
- <Box>
40
- <Text color="gray">{label.padEnd(PAD, " ")}</Text>
41
- <Text color="white" bold>
42
- {value}
43
- </Text>
44
- </Box>
45
- );
46
- }
47
-
48
- function Divider() {
49
- return <Text color="gray">{"─".repeat(36)}</Text>;
50
- }
51
-
52
- const SKIP_DIRS = new Set([
53
- "node_modules",
54
- ".git",
55
- "dist",
56
- "build",
57
- ".next",
58
- "out",
59
- "coverage",
60
- "__pycache__",
61
- ".venv",
62
- "venv",
63
- ]);
64
-
65
- function parseGitignore(dir: string): string[] {
66
- const p = path.join(dir, ".gitignore");
67
- if (!existsSync(p)) return [];
68
- try {
69
- return readFileSync(p, "utf-8")
70
- .split("\n")
71
- .map((l) => l.trim())
72
- .filter((l) => l && !l.startsWith("#"));
73
- } catch {
74
- return [];
75
- }
76
- }
77
-
78
- function matchesGitignore(
79
- patterns: string[],
80
- relPath: string,
81
- isDir: boolean,
82
- ): boolean {
83
- const name = path.basename(relPath);
84
- for (const pattern of patterns) {
85
- if (pattern.endsWith("/")) {
86
- if (isDir && name === pattern.slice(0, -1)) return true;
87
- continue;
88
- }
89
- if (pattern.startsWith("!")) continue;
90
- if (pattern.includes("*")) {
91
- const regex = new RegExp(
92
- "^" +
93
- pattern
94
- .replace(/\./g, "\\.")
95
- .replace(/\*\*/g, ".*")
96
- .replace(/\*/g, "[^/]*") +
97
- "$",
98
- );
99
- if (regex.test(name) || regex.test(relPath)) return true;
100
- continue;
101
- }
102
- if (
103
- name === pattern ||
104
- relPath === pattern ||
105
- relPath.startsWith(pattern + "/")
106
- )
107
- return true;
108
- }
109
- return false;
110
- }
111
-
112
- function walkDir(dir: string, base = dir, patterns?: string[]): string[] {
113
- const p = patterns ?? parseGitignore(base);
114
- const results: string[] = [];
115
- let entries: string[];
116
- try {
117
- entries = readdirSync(dir, { encoding: "utf-8" });
118
- } catch {
119
- return results;
120
- }
121
- for (const entry of entries) {
122
- if (SKIP_DIRS.has(entry)) continue;
123
- const full = path.join(dir, entry);
124
- const rel = path.relative(base, full).replace(/\\/g, "/");
125
- let isDir = false;
126
- try {
127
- isDir = statSync(full).isDirectory();
128
- } catch {
129
- continue;
130
- }
131
- if (matchesGitignore(p, rel, isDir)) continue;
132
- if (isDir) results.push(...walkDir(full, base, p));
133
- else results.push(rel);
134
- }
135
- return results;
136
- }
137
-
138
- function StatsPanel({
139
- resolvedPath,
140
- stats,
141
- }: {
142
- resolvedPath: string;
143
- stats: CodeStats;
144
- }) {
145
- const langs = topLanguages(stats.languages);
146
- return (
147
- <Box flexDirection="column" marginTop={1} gap={0}>
148
- <Text bold color="cyan">
149
- {figures.hamburger} {path.basename(resolvedPath)}
150
- </Text>
151
- <Divider />
152
- <StatRow label="Lines of Code" value={formatNumber(stats.codeLines)} />
153
- <StatRow label="Total Lines" value={formatNumber(stats.totalLines)} />
154
- <StatRow label="Files" value={formatNumber(stats.totalFiles)} />
155
- <StatRow label="Languages" value={langs || "—"} />
156
- <StatRow label="Functions" value={formatNumber(stats.functions)} />
157
- <StatRow label="Classes" value={formatNumber(stats.classes)} />
158
- <StatRow label="Comment Lines" value={formatNumber(stats.commentLines)} />
159
- <StatRow label="Blank Lines" value={formatNumber(stats.blankLines)} />
160
- <Divider />
161
- </Box>
162
- );
163
- }
164
-
165
- export const ReviewCommand = ({
166
- path: inputPath,
167
- onExit,
168
- }: {
169
- path: string;
170
- onExit?: () => void;
171
- }) => {
172
- const [stage, setStage] = useState<ReviewStage>({ type: "scanning" });
173
-
174
- const [preloadedResult, setPreloadedResult] = useState<
175
- import("../types/repo").AnalysisResult | null
176
- >(null);
177
- const resolvedPath = path.resolve(inputPath);
178
-
179
- useEffect(() => {
180
- if (!existsSync(resolvedPath)) {
181
- setStage({ type: "error", message: `Path not found: ${resolvedPath}` });
182
- return;
183
- }
184
-
185
- fetchFileTree(resolvedPath)
186
- .catch(() => walkDir(resolvedPath))
187
- .then((fileTree) => {
188
- const stats = computeStats(resolvedPath, fileTree);
189
- const files = readImportantFiles(resolvedPath, fileTree);
190
-
191
- if (lensFileExists(resolvedPath)) {
192
- setStage({ type: "lens-menu", fileTree, files, stats });
193
- } else {
194
- setStage({ type: "stats", stats, files, fileTree });
195
- }
196
- })
197
- .catch((err: unknown) =>
198
- setStage({
199
- type: "error",
200
- message: err instanceof Error ? err.message : "Failed to scan",
201
- }),
202
- );
203
- }, [resolvedPath]);
204
-
205
- const handleLensChoice = (
206
- choice: LensMenuChoice,
207
- fileTree: string[],
208
- files: ImportantFile[],
209
- stats: CodeStats,
210
- ) => {
211
- const lf = readLensFile(resolvedPath);
212
-
213
- if (choice === "use-cached" && lf) {
214
- setPreloadedResult(lensFileToAnalysisResult(lf));
215
- setStage({ type: "stats", stats, files, fileTree });
216
- return;
217
- }
218
-
219
- if (choice === "fix-issues" && lf) {
220
- setPreloadedResult(lensFileToAnalysisResult(lf));
221
- setStage({ type: "stats", stats, files, fileTree });
222
- return;
223
- }
224
-
225
- if (choice === "security" && lf) {
226
- setPreloadedResult(lensFileToAnalysisResult(lf));
227
- setStage({ type: "stats", stats, files, fileTree });
228
- return;
229
- }
230
-
231
- setStage({ type: "stats", stats, files, fileTree });
232
- };
233
-
234
- if (stage.type === "scanning") {
235
- return (
236
- <Box marginTop={1} gap={1}>
237
- <Text color="cyan">{figures.pointer}</Text>
238
- <Text>Scanning codebase...</Text>
239
- </Box>
240
- );
241
- }
242
-
243
- if (stage.type === "error") {
244
- return (
245
- <Box marginTop={1}>
246
- <Text color="red">
247
- {figures.cross} {stage.message}
248
- </Text>
249
- </Box>
250
- );
251
- }
252
-
253
- if (stage.type === "lens-menu") {
254
- const lf = readLensFile(resolvedPath);
255
- if (!lf) {
256
- setStage({
257
- type: "stats",
258
- stats: stage.stats,
259
- files: stage.files,
260
- fileTree: stage.fileTree,
261
- });
262
- return null;
263
- }
264
- const { fileTree, files, stats } = stage;
265
- return (
266
- <Box flexDirection="column" gap={1}>
267
- <StatsPanel resolvedPath={resolvedPath} stats={stats} />
268
- <LensFileMenu
269
- repoPath={resolvedPath}
270
- lensFile={lf}
271
- onChoice={(choice) =>
272
- handleLensChoice(choice, fileTree, files, stats)
273
- }
274
- />
275
- </Box>
276
- );
277
- }
278
-
279
- const { stats, files, fileTree } = stage;
280
-
281
- return (
282
- <Box flexDirection="column" gap={1}>
283
- <StatsPanel resolvedPath={resolvedPath} stats={stats} />
284
- <RepoAnalysis
285
- repoUrl={resolvedPath}
286
- repoPath={resolvedPath}
287
- fileTree={fileTree}
288
- files={files}
289
- preloadedResult={preloadedResult ?? undefined}
290
- onExit={onExit}
291
- />
292
- </Box>
293
- );
294
- };
@@ -1,56 +0,0 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
3
- import figures from "figures";
4
- import path from "path";
5
- import { existsSync } from "fs";
6
- import { RunRunner } from "../components/watch/RunRunner";
7
- import { RED } from "../colors";
8
-
9
- interface Props {
10
- cmd: string;
11
- path: string;
12
- clean: boolean;
13
- fixAll: boolean;
14
- autoRestart: boolean;
15
- prompt?: string;
16
- }
17
-
18
- export function RunCommand({
19
- cmd,
20
- path: inputPath,
21
- clean,
22
- fixAll,
23
- autoRestart,
24
- prompt,
25
- }: Props) {
26
- const repoPath = path.resolve(inputPath);
27
-
28
- if (!cmd.trim()) {
29
- return (
30
- <Box marginTop={1}>
31
- <Text color={RED}>{figures.cross} Usage: lens watch "bun dev"</Text>
32
- </Box>
33
- );
34
- }
35
-
36
- if (!existsSync(repoPath)) {
37
- return (
38
- <Box marginTop={1}>
39
- <Text color={RED}>
40
- {figures.cross} Path not found: {repoPath}
41
- </Text>
42
- </Box>
43
- );
44
- }
45
-
46
- return (
47
- <RunRunner
48
- cmd={cmd}
49
- repoPath={repoPath}
50
- clean={clean}
51
- fixAll={fixAll}
52
- autoRestart={autoRestart}
53
- extraPrompt={prompt}
54
- />
55
- );
56
- }
@@ -1,36 +0,0 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
3
- import figures from "figures";
4
- import { existsSync } from "fs";
5
- import path from "path";
6
- import { PromptRunner } from "../components/task/TaskRunner";
7
-
8
- export const TaskCommand = ({
9
- prompt,
10
- path: inputPath,
11
- }: {
12
- prompt: string;
13
- path: string;
14
- }) => {
15
- const resolvedPath = path.resolve(inputPath);
16
-
17
- if (!existsSync(resolvedPath)) {
18
- return (
19
- <Box marginTop={1}>
20
- <Text color="red">
21
- {figures.cross} Path not found: {resolvedPath}
22
- </Text>
23
- </Box>
24
- );
25
- }
26
-
27
- if (!prompt.trim()) {
28
- return (
29
- <Box marginTop={1}>
30
- <Text color="red">{figures.cross} Prompt cannot be empty.</Text>
31
- </Box>
32
- );
33
- }
34
-
35
- return <PromptRunner repoPath={resolvedPath} userPrompt={prompt} />;
36
- };
@@ -1,195 +0,0 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
3
- import { ACCENT, GREEN, RED } from "../../colors";
4
- import type { Message } from "../../types/chat";
5
-
6
- function InlineText({ text }: { text: string }) {
7
- const parts = text.split(/(`[^`]+`|\*\*[^*]+\*\*)/g);
8
- return (
9
- <>
10
- {parts.map((part, i) => {
11
- if (part.startsWith("`") && part.endsWith("`")) {
12
- return (
13
- <Text key={i} color={ACCENT}>
14
- {part.slice(1, -1)}
15
- </Text>
16
- );
17
- }
18
- if (part.startsWith("**") && part.endsWith("**")) {
19
- return (
20
- <Text key={i} bold color="white">
21
- {part.slice(2, -2)}
22
- </Text>
23
- );
24
- }
25
- return (
26
- <Text key={i} color="white">
27
- {part}
28
- </Text>
29
- );
30
- })}
31
- </>
32
- );
33
- }
34
-
35
- function CodeBlock({ lang, code }: { lang: string; code: string }) {
36
- return (
37
- <Box flexDirection="column">
38
- {code.split("\n").map((line, i) => (
39
- <Text key={i} color={ACCENT}>
40
- {" "}
41
- {line}
42
- </Text>
43
- ))}
44
- </Box>
45
- );
46
- }
47
-
48
- function MessageBody({ content }: { content: string }) {
49
- const segments = content.split(/(```[\s\S]*?```)/g);
50
-
51
- return (
52
- <Box flexDirection="column">
53
- {segments.map((seg, si) => {
54
- if (seg.startsWith("```")) {
55
- const lines = seg.slice(3).split("\n");
56
- const lang = lines[0]?.trim() ?? "";
57
- const code = lines
58
- .slice(1)
59
- .join("\n")
60
- .replace(/```\s*$/, "")
61
- .trimEnd();
62
- return <CodeBlock key={si} lang={lang} code={code} />;
63
- }
64
-
65
- const lines = seg.split("\n").filter((l) => l.trim() !== "");
66
- return (
67
- <Box key={si} flexDirection="column">
68
- {lines.map((line, li) => {
69
- if (line.match(/^[-*•]\s/)) {
70
- return (
71
- <Box key={li} gap={1}>
72
- <Text color={ACCENT}>*</Text>
73
- <InlineText text={line.slice(2).trim()} />
74
- </Box>
75
- );
76
- }
77
-
78
- if (line.match(/^\d+\.\s/)) {
79
- const num = line.match(/^(\d+)\.\s/)![1];
80
- return (
81
- <Box key={li} gap={1}>
82
- <Text color="gray">{num}.</Text>
83
- <InlineText text={line.replace(/^\d+\.\s/, "").trim()} />
84
- </Box>
85
- );
86
- }
87
-
88
- return (
89
- <Box key={li}>
90
- <InlineText text={line} />
91
- </Box>
92
- );
93
- })}
94
- </Box>
95
- );
96
- })}
97
- </Box>
98
- );
99
- }
100
-
101
- function summarizeToolContent(toolName: string, content: string): string {
102
- // For write-file, extract just the path
103
- if (toolName === "write-file" || toolName === "read-file") {
104
- const pathMatch = content.match(/"path"\s*:\s*"([^"]+)"/);
105
- if (pathMatch) return pathMatch[1]!;
106
- }
107
- // For changes blocks, just say what changed
108
- if (content.includes('"summary"')) {
109
- const summaryMatch = content.match(/"summary"\s*:\s*"([^"]+)"/);
110
- if (summaryMatch) return summaryMatch[1]!;
111
- }
112
- return content.length > 120 ? content.slice(0, 120) + "…" : content;
113
- }
114
-
115
- export function StaticMessage({ msg }: { msg: Message }) {
116
- if (msg.role === "user") {
117
- return (
118
- <Box
119
- marginBottom={1}
120
- gap={1}
121
- backgroundColor={"#1a1a1a"}
122
- paddingLeft={1}
123
- paddingRight={2}
124
- >
125
- <Text color="gray">{">"}</Text>
126
- <Text color="white" bold>
127
- {msg.content}
128
- </Text>
129
- </Box>
130
- );
131
- }
132
-
133
- if (msg.type === "tool") {
134
- const icons: Record<string, string> = {
135
- shell: "$",
136
- fetch: "~>",
137
- "read-file": "r",
138
- "write-file": "w",
139
- search: "?",
140
- };
141
- const icon = icons[msg.toolName] ?? "·";
142
- const label =
143
- msg.toolName === "shell"
144
- ? msg.content
145
- : msg.toolName === "search"
146
- ? `"${msg.content}"`
147
- : summarizeToolContent(msg.toolName, msg.content);
148
-
149
- return (
150
- <Box flexDirection="column" marginBottom={1}>
151
- <Box gap={1}>
152
- <Text color={msg.approved ? ACCENT : RED}>{icon}</Text>
153
- <Text color={msg.approved ? "gray" : RED} dimColor={!msg.approved}>
154
- {label}
155
- </Text>
156
- {!msg.approved && <Text color={RED}>denied</Text>}
157
- </Box>
158
- {msg.approved && msg.result && (
159
- <Box marginLeft={2}>
160
- <Text color="gray">
161
- {msg.result.split("\n")[0]?.slice(0, 120)}
162
- {(msg.result.split("\n")[0]?.length ?? 0) > 120 ? "…" : ""}
163
- </Text>
164
- </Box>
165
- )}
166
- </Box>
167
- );
168
- }
169
-
170
- if (msg.type === "plan") {
171
- return (
172
- <Box flexDirection="column" marginBottom={1}>
173
- <Box gap={1}>
174
- <Text color={ACCENT}>*</Text>
175
- <MessageBody content={msg.content} />
176
- </Box>
177
- <Box marginLeft={2} gap={1}>
178
- <Text color={msg.applied ? GREEN : "gray"}>
179
- {msg.applied ? "✓" : "·"}
180
- </Text>
181
- <Text color={msg.applied ? GREEN : "gray"} dimColor={!msg.applied}>
182
- {msg.applied ? "changes applied" : "changes skipped"}
183
- </Text>
184
- </Box>
185
- </Box>
186
- );
187
- }
188
-
189
- return (
190
- <Box marginBottom={1} gap={1}>
191
- <Text color={ACCENT}>●</Text>
192
- <MessageBody content={msg.content} />
193
- </Box>
194
- );
195
- }