@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
@@ -0,0 +1,238 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput, Static } from "ink";
3
+ import { InputBox, ShortcutBar, ACCENT, GREEN } from "@ridit/ink-ui";
4
+ import { existsSync, readFileSync } from "fs";
5
+ import { join } from "path";
6
+ import {
7
+ addMessage,
8
+ chat,
9
+ createSession,
10
+ getMessages,
11
+ getSystemPrompt,
12
+ saveSession,
13
+ } from "@ridit/lens-core";
14
+ import { Message, extractText } from "./Message";
15
+ import { ToolCall } from "./ToolCall";
16
+
17
+ const cwd = process.cwd();
18
+ const hasLensMd = existsSync(join(cwd, "LENS.md"));
19
+
20
+ interface ToolCallItem {
21
+ id: string;
22
+ tool: string;
23
+ args: unknown;
24
+ status: "running" | "done";
25
+ }
26
+
27
+ interface Turn {
28
+ id: string;
29
+ userText: string;
30
+ toolCalls: ToolCallItem[];
31
+ assistantText: string;
32
+ }
33
+
34
+ function buildTurnsFromSession(
35
+ messages: Array<{ role: string; content: unknown }>,
36
+ ): Turn[] {
37
+ const turns: Turn[] = [];
38
+ for (let i = 0; i < messages.length; i++) {
39
+ const msg = messages[i]!;
40
+ if (msg.role !== "user") continue;
41
+ const next = messages[i + 1];
42
+ if (next?.role === "assistant") {
43
+ turns.push({
44
+ id: crypto.randomUUID(),
45
+ userText: extractText(msg.content),
46
+ toolCalls: [],
47
+ assistantText: extractText(next.content),
48
+ });
49
+ i++;
50
+ }
51
+ }
52
+ return turns;
53
+ }
54
+
55
+ export function ChatView() {
56
+ const [session, setSession] = useState(() => createSession(cwd));
57
+ const [turns, setTurns] = useState<Turn[]>(() =>
58
+ buildTurnsFromSession(
59
+ createSession(cwd).messages.filter(
60
+ (m) => m.role === "user" || m.role === "assistant",
61
+ ),
62
+ ),
63
+ );
64
+ const [currentChunk, setCurrentChunk] = useState("");
65
+ const [isLoading, setIsLoading] = useState(false);
66
+ const [liveToolCalls, setLiveToolCalls] = useState<ToolCallItem[]>([]);
67
+ const [currentUserText, setCurrentUserText] = useState("");
68
+ const [inputValue, setInputValue] = useState("");
69
+ const [inputKey, setInputKey] = useState(0);
70
+
71
+ useInput((_, key) => {
72
+ if (key.escape && isLoading) {
73
+ setIsLoading(false);
74
+ setCurrentChunk("");
75
+ setLiveToolCalls([]);
76
+ setCurrentUserText("");
77
+ }
78
+ });
79
+
80
+ const handleSubmit = async (val: string) => {
81
+ if (!val.trim() || isLoading) return;
82
+
83
+ setInputValue("");
84
+ setInputKey((k) => k + 1);
85
+ setLiveToolCalls([]);
86
+ setCurrentUserText(val);
87
+
88
+ const updated = addMessage(session, "user", val);
89
+ setSession(updated);
90
+ setIsLoading(true);
91
+ setCurrentChunk("");
92
+
93
+ const turnId = crypto.randomUUID();
94
+ const turnToolCalls: ToolCallItem[] = [];
95
+
96
+ try {
97
+ await chat({
98
+ messages: getMessages(updated),
99
+ system: getSystemPrompt(cwd),
100
+ onChunk: (chunk) => setCurrentChunk((prev) => prev + chunk),
101
+ onToolCall: (tool, args) => {
102
+ // For full-content writes, capture the existing file so we can show removals
103
+ let enrichedArgs = args;
104
+ const WRITE_TOOLS = new Set(["write_file", "write", "create_file", "create", "overwrite_file"]);
105
+ if (WRITE_TOOLS.has(tool) && typeof args === "object" && args) {
106
+ const a = args as Record<string, unknown>;
107
+ const filePath = String(a.path ?? a.file_path ?? a.filename ?? "");
108
+ if (filePath) {
109
+ const abs = join(cwd, filePath);
110
+ try {
111
+ enrichedArgs = { ...a, _prevContent: readFileSync(abs, "utf-8") };
112
+ } catch { /* file doesn't exist yet */ }
113
+ }
114
+ }
115
+
116
+ const id = crypto.randomUUID();
117
+ const item: ToolCallItem = { id, tool, args: enrichedArgs, status: "running" };
118
+ turnToolCalls.push(item);
119
+ setLiveToolCalls((prev) => [...prev, item]);
120
+ setTimeout(() => {
121
+ const idx = turnToolCalls.findIndex((tc) => tc.id === id);
122
+ if (idx !== -1)
123
+ turnToolCalls[idx] = { ...turnToolCalls[idx]!, status: "done" };
124
+ setLiveToolCalls((prev) =>
125
+ prev.map((tc) => (tc.id === id ? { ...tc, status: "done" } : tc)),
126
+ );
127
+ }, 500);
128
+ },
129
+ onFinish: (text) => {
130
+ setSession((prev) => {
131
+ const final = addMessage(prev, "assistant", text);
132
+ saveSession(final);
133
+ return final;
134
+ });
135
+ setTurns((prev) => [
136
+ ...prev,
137
+ {
138
+ id: turnId,
139
+ userText: val,
140
+ toolCalls: turnToolCalls.map((tc) => ({ ...tc, status: "done" })),
141
+ assistantText: text,
142
+ },
143
+ ]);
144
+ setCurrentUserText("");
145
+ setCurrentChunk("");
146
+ setIsLoading(false);
147
+ setLiveToolCalls([]);
148
+ },
149
+ });
150
+ } catch (err) {
151
+ const message = err instanceof Error ? err.message : String(err);
152
+ setTurns((prev) => [
153
+ ...prev,
154
+ {
155
+ id: turnId,
156
+ userText: val,
157
+ toolCalls: turnToolCalls.map((tc) => ({ ...tc, status: "done" })),
158
+ assistantText: `Error: ${message}`,
159
+ },
160
+ ]);
161
+ setCurrentUserText("");
162
+ setCurrentChunk("");
163
+ setIsLoading(false);
164
+ setLiveToolCalls([]);
165
+ }
166
+ };
167
+
168
+ return (
169
+ <Box flexDirection="column" marginTop={1}>
170
+ {/* Status hints */}
171
+ {hasLensMd && (
172
+ <Box gap={1} marginBottom={1} paddingLeft={1}>
173
+ <Text color={GREEN} dimColor>✓</Text>
174
+ <Text color="gray" dimColor>LENS.md loaded</Text>
175
+ </Box>
176
+ )}
177
+
178
+ {/* Completed turns — frozen by Static */}
179
+ <Static items={turns}>
180
+ {(turn) => (
181
+ <Box key={turn.id} flexDirection="column" marginBottom={1}>
182
+ <Message role="user">{turn.userText}</Message>
183
+ {turn.toolCalls.length > 0 && (
184
+ <Box flexDirection="column">
185
+ {turn.toolCalls.map((tc) => (
186
+ <ToolCall
187
+ key={tc.id}
188
+ tool={tc.tool}
189
+ args={tc.args}
190
+ status="done"
191
+ />
192
+ ))}
193
+ </Box>
194
+ )}
195
+ <Message role="assistant">{turn.assistantText}</Message>
196
+ </Box>
197
+ )}
198
+ </Static>
199
+
200
+ {/* In-progress turn */}
201
+ {isLoading && (
202
+ <Box flexDirection="column">
203
+ <Message role="user">{currentUserText}</Message>
204
+ {liveToolCalls.length > 0 && (
205
+ <Box flexDirection="column">
206
+ {liveToolCalls.map((tc) => (
207
+ <ToolCall
208
+ key={tc.id}
209
+ tool={tc.tool}
210
+ args={tc.args}
211
+ status={tc.status}
212
+ />
213
+ ))}
214
+ </Box>
215
+ )}
216
+ {currentChunk ? (
217
+ <Message role="assistant">{currentChunk}</Message>
218
+ ) : (
219
+ <Box gap={1} paddingLeft={1}>
220
+ <Text color={ACCENT} dimColor>◆</Text>
221
+ <Text color="gray" dimColor>thinking...</Text>
222
+ </Box>
223
+ )}
224
+ </Box>
225
+ )}
226
+
227
+ <InputBox
228
+ value={inputValue}
229
+ onChange={setInputValue}
230
+ onSubmit={handleSubmit}
231
+ inputKey={inputKey}
232
+ placeholder="Ask anything about your codebase..."
233
+ disabled={isLoading}
234
+ />
235
+ <ShortcutBar />
236
+ </Box>
237
+ );
238
+ }
@@ -0,0 +1,46 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { MessageBody, ACCENT } from "@ridit/ink-ui";
4
+
5
+ interface MessageProps {
6
+ role: "user" | "assistant" | "tool" | "system";
7
+ children: unknown;
8
+ }
9
+
10
+ export function extractText(content: unknown): string {
11
+ if (typeof content === "string") return content;
12
+ if (Array.isArray(content)) {
13
+ return content
14
+ .filter((c) => c?.type === "text")
15
+ .map((c) => c.text ?? "")
16
+ .join("");
17
+ }
18
+ return "";
19
+ }
20
+
21
+ export function Message({ children, role }: MessageProps) {
22
+ const text = extractText(children);
23
+ if (!text) return null;
24
+
25
+ if (role === "user") {
26
+ return (
27
+ <Box gap={1} paddingLeft={1}>
28
+ <Text color={ACCENT} dimColor>
29
+
30
+ </Text>
31
+ <Text color="white">{text}</Text>
32
+ </Box>
33
+ );
34
+ }
35
+
36
+ if (role === "assistant") {
37
+ return (
38
+ <Box gap={1}>
39
+ <Text color={ACCENT}>◆</Text>
40
+ <MessageBody content={text} />
41
+ </Box>
42
+ );
43
+ }
44
+
45
+ return null;
46
+ }
@@ -0,0 +1,67 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { GREEN, YELLOW, RED } from "@ridit/ink-ui";
4
+ import {
5
+ FILE_WRITE_TOOLS,
6
+ FILE_READ_TOOLS,
7
+ extractFileDiff,
8
+ getArgDetail,
9
+ getLabel,
10
+ } from "./toolcall-utils";
11
+
12
+ interface ToolCallProps {
13
+ tool: string;
14
+ args: unknown;
15
+ status: "running" | "done";
16
+ tokenCount?: number;
17
+ duration?: number;
18
+ }
19
+
20
+ export function ToolCall({ tool, args, status, tokenCount }: ToolCallProps) {
21
+ const isFileTool = FILE_WRITE_TOOLS.has(tool) || FILE_READ_TOOLS.has(tool);
22
+ const diff = isFileTool ? extractFileDiff(tool, args) : null;
23
+ const detail = getArgDetail(tool, args);
24
+ const isRunning = status === "running";
25
+ const label = getLabel(tool, isRunning);
26
+
27
+ const hasDiffContent = diff && (diff.additions.length > 0 || diff.removals.length > 0);
28
+
29
+ return (
30
+ <Box flexDirection="column" marginLeft={2}>
31
+ <Box gap={1}>
32
+ <Text color={isRunning ? YELLOW : GREEN} dimColor={!isRunning}>
33
+ {isRunning ? "◆" : "✓"}
34
+ </Text>
35
+ <Text color={isRunning ? "white" : "gray"} dimColor={!isRunning}>
36
+ {label}
37
+ </Text>
38
+ {detail && (
39
+ <Text color="gray" dimColor>
40
+ {detail}
41
+ {isRunning ? "..." : ""}
42
+ </Text>
43
+ )}
44
+ {!isRunning && tokenCount && (
45
+ <Text color="gray" dimColor>· {tokenCount} tokens</Text>
46
+ )}
47
+ </Box>
48
+
49
+ {hasDiffContent && (
50
+ <Box flexDirection="column" marginLeft={4}>
51
+ {diff!.removals.map((line, i) => (
52
+ <Text key={`r${i}`} color={RED} dimColor>
53
+ {"- "}
54
+ {line}
55
+ </Text>
56
+ ))}
57
+ {diff!.additions.map((line, i) => (
58
+ <Text key={`a${i}`} color={GREEN} dimColor>
59
+ {"+ "}
60
+ {line}
61
+ </Text>
62
+ ))}
63
+ </Box>
64
+ )}
65
+ </Box>
66
+ );
67
+ }