@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,517 +0,0 @@
1
- import React from "react";
2
- import { Box, Text, Static, useInput } from "ink";
3
- import Spinner from "ink-spinner";
4
- import { useState } from "react";
5
- import path from "path";
6
- import os from "os";
7
- import { TextArea } from "./TextArea";
8
- import { ACCENT } from "../../colors";
9
- import { ProviderPicker } from "../provider/ProviderPicker";
10
- import { startCloneRepo } from "../../utils/repo";
11
- import { useThinkingPhrase } from "../../utils/thinking";
12
- import { walkDir, applyPatches, toCloneUrl } from "../../utils/chat";
13
- import { appendMemory } from "../../utils/memory";
14
- import { getChatNameSuggestions, saveChat } from "../../utils/chatHistory";
15
- import { StaticMessage } from "./ChatMessage";
16
- import {
17
- PermissionPrompt,
18
- InputBox,
19
- ShortcutBar,
20
- TypewriterText,
21
- CloneOfferView,
22
- CloningView,
23
- CloneExistsView,
24
- CloneDoneView,
25
- CloneErrorView,
26
- PreviewView,
27
- ViewingFileView,
28
- } from "./ChatOverlays";
29
- import { TimelineRunner } from "../timeline/TimelineRunner";
30
- import { ReviewCommand } from "../../commands/review";
31
- import type { Message } from "../../types/chat";
32
- import { useChat } from "./hooks/useChat";
33
- import { useChatInput } from "./hooks/useChatInput";
34
- import { handleCommand, COMMANDS } from "./hooks/useCommandHandlers";
35
-
36
- function CommandPalette({
37
- query,
38
- recentChats,
39
- }: {
40
- query: string;
41
- recentChats: string[];
42
- }) {
43
- const q = query.toLowerCase();
44
- const isChatLoad = q.startsWith("/chat load") || q.startsWith("/chat delete");
45
- const chatFilter = isChatLoad
46
- ? q.startsWith("/chat load")
47
- ? q.slice("/chat load".length).trim()
48
- : q.slice("/chat delete".length).trim()
49
- : "";
50
- const filteredChats = chatFilter
51
- ? recentChats.filter((n) => n.toLowerCase().includes(chatFilter))
52
- : recentChats;
53
- const matches = COMMANDS.filter((c) => c.cmd.startsWith(q));
54
- if (!matches.length && !isChatLoad) return null;
55
- if (!matches.length && isChatLoad && filteredChats.length === 0) return null;
56
- return (
57
- <Box flexDirection="column" marginBottom={1} marginLeft={2}>
58
- {matches.map((c, i) => {
59
- const isExact = c.cmd === query;
60
- return (
61
- <Box key={i} gap={2}>
62
- <Text color={isExact ? ACCENT : "white"} bold={isExact}>
63
- {c.cmd}
64
- </Text>
65
- <Text color="gray" dimColor>
66
- {c.desc}
67
- </Text>
68
- </Box>
69
- );
70
- })}
71
- {isChatLoad && filteredChats.length > 0 && (
72
- <Box flexDirection="column" marginTop={matches.length ? 1 : 0}>
73
- <Text color="gray" dimColor>
74
- {chatFilter ? `matching "${chatFilter}":` : "recent chats:"}
75
- </Text>
76
- {filteredChats.map((name, i) => (
77
- <Box key={i} gap={1} marginLeft={2}>
78
- <Text color={ACCENT}>·</Text>
79
- <Text color="white">{name}</Text>
80
- </Box>
81
- ))}
82
- </Box>
83
- )}
84
- </Box>
85
- );
86
- }
87
-
88
- function ForceAllWarning({
89
- onConfirm,
90
- }: {
91
- onConfirm: (confirmed: boolean) => void;
92
- }) {
93
- const [input, setInput] = useState("");
94
- return (
95
- <Box flexDirection="column" marginY={1} gap={1}>
96
- <Box gap={1}>
97
- <Text color="red" bold>
98
- ⚠ WARNING
99
- </Text>
100
- </Box>
101
- <Box flexDirection="column" marginLeft={2} gap={1}>
102
- <Text color="yellow">
103
- Force-all mode auto-approves EVERY tool without asking — including:
104
- </Text>
105
- <Text color="red" dimColor>
106
- {" "}
107
- · shell commands (rm, git, npm, anything)
108
- </Text>
109
- <Text color="red" dimColor>
110
- {" "}
111
- · file writes and deletes
112
- </Text>
113
- <Text color="red" dimColor>
114
- {" "}
115
- · folder deletes
116
- </Text>
117
- <Text color="red" dimColor>
118
- {" "}
119
- · external fetches and URL opens
120
- </Text>
121
- <Text color="yellow" dimColor>
122
- The AI can modify or delete files without any confirmation.
123
- </Text>
124
- <Text color="yellow" dimColor>
125
- Only use this in throwaway environments or when you fully trust the
126
- task.
127
- </Text>
128
- </Box>
129
- <Box gap={1} marginTop={1}>
130
- <Text color="gray">Type </Text>
131
- <Text color="white" bold>
132
- yes
133
- </Text>
134
- <Text color="gray"> to enable, or press </Text>
135
- <Text color="white" bold>
136
- esc
137
- </Text>
138
- <Text color="gray"> to cancel: </Text>
139
- <TextArea
140
- value={input}
141
- onChange={setInput}
142
- onSubmit={(v) => onConfirm(v.trim().toLowerCase() === "yes")}
143
- placeholder="yes / esc to cancel"
144
- />
145
- </Box>
146
- </Box>
147
- );
148
- }
149
-
150
- export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
151
- const chat = useChat(repoPath);
152
- const thinkingPhrase = useThinkingPhrase(chat.stage.type === "thinking");
153
-
154
- const handleStageKey = (input: string, key: any) => {
155
- const { stage } = chat;
156
-
157
- if (chat.showForceWarning && key.escape) {
158
- chat.setShowForceWarning(false);
159
- return;
160
- }
161
-
162
- if (stage.type === "clone-offer") {
163
- if (input === "y" || input === "Y" || key.return) {
164
- const { repoUrl } = stage;
165
- const launch = stage.launchAnalysis ?? false;
166
- const cloneUrl = toCloneUrl(repoUrl);
167
- chat.setStage({ type: "cloning", repoUrl });
168
- startCloneRepo(cloneUrl).then((result) => {
169
- if (result.done) {
170
- const repoName =
171
- cloneUrl
172
- .split("/")
173
- .pop()
174
- ?.replace(/\.git$/, "") ?? "repo";
175
- const destPath = path.join(os.tmpdir(), repoName);
176
- const fileCount = walkDir(destPath).length;
177
- appendMemory({
178
- kind: "url-fetched",
179
- detail: repoUrl,
180
- summary: `Cloned ${repoName} — ${fileCount} files`,
181
- });
182
- chat.setClonedUrls((prev) => new Set([...prev, repoUrl]));
183
- chat.setStage({
184
- type: "clone-done",
185
- repoUrl,
186
- destPath,
187
- fileCount,
188
- launchAnalysis: launch,
189
- });
190
- } else if (result.folderExists && result.repoPath) {
191
- chat.setStage({
192
- type: "clone-exists",
193
- repoUrl,
194
- repoPath: result.repoPath,
195
- });
196
- } else {
197
- chat.setStage({
198
- type: "clone-error",
199
- message:
200
- !result.folderExists && result.error
201
- ? result.error
202
- : "Clone failed",
203
- });
204
- }
205
- });
206
- return;
207
- }
208
- if (input === "n" || input === "N" || key.escape)
209
- chat.setStage({ type: "idle" });
210
- return;
211
- }
212
-
213
- if (stage.type === "clone-exists") {
214
- if (input === "y" || input === "Y") {
215
- const { repoUrl, repoPath: existingPath } = stage;
216
- chat.setStage({ type: "cloning", repoUrl });
217
- startCloneRepo(toCloneUrl(repoUrl), { forceReclone: true }).then(
218
- (result) => {
219
- if (result.done) {
220
- chat.setStage({
221
- type: "clone-done",
222
- repoUrl,
223
- destPath: existingPath,
224
- fileCount: walkDir(existingPath).length,
225
- });
226
- } else {
227
- chat.setStage({
228
- type: "clone-error",
229
- message:
230
- !result.folderExists && result.error
231
- ? result.error
232
- : "Clone failed",
233
- });
234
- }
235
- },
236
- );
237
- return;
238
- }
239
- if (input === "n" || input === "N") {
240
- const { repoUrl, repoPath: existingPath } = stage;
241
- chat.setStage({
242
- type: "clone-done",
243
- repoUrl,
244
- destPath: existingPath,
245
- fileCount: walkDir(existingPath).length,
246
- });
247
- return;
248
- }
249
- return;
250
- }
251
-
252
- if (stage.type === "clone-done" || stage.type === "clone-error") {
253
- if (key.return || key.escape) {
254
- if (stage.type === "clone-done") {
255
- const repoName = stage.repoUrl.split("/").pop() ?? "repo";
256
- const summaryMsg: Message = {
257
- role: "assistant",
258
- type: "text",
259
- content: `Cloned **${repoName}** (${stage.fileCount} files) to \`${stage.destPath}\`.\n\nAsk me anything about it — I can read files, explain how it works, or suggest improvements.`,
260
- };
261
- const contextMsg: Message = {
262
- role: "assistant",
263
- type: "tool",
264
- toolName: "fetch",
265
- content: stage.repoUrl,
266
- result: `Clone complete. Repo: ${repoName}. Local path: ${stage.destPath}. ${stage.fileCount} files.`,
267
- approved: true,
268
- };
269
- chat.setAllMessages([...chat.allMessages, contextMsg, summaryMsg]);
270
- chat.setCommitted((prev) => [...prev, summaryMsg]);
271
- chat.setStage({ type: "idle" });
272
- } else {
273
- chat.setStage({ type: "idle" });
274
- }
275
- }
276
- return;
277
- }
278
-
279
- if (stage.type === "cloning") return;
280
-
281
- if (stage.type === "permission") {
282
- if (input === "y" || input === "Y" || key.return) {
283
- stage.resolve(true);
284
- return;
285
- }
286
- if (input === "n" || input === "N" || key.escape) {
287
- chat.batchApprovedRef.current = false;
288
- stage.resolve(false);
289
- return;
290
- }
291
- return;
292
- }
293
-
294
- if (stage.type === "preview") {
295
- if (key.upArrow) {
296
- chat.setStage({
297
- ...stage,
298
- scrollOffset: Math.max(0, stage.scrollOffset - 1),
299
- });
300
- return;
301
- }
302
- if (key.downArrow) {
303
- chat.setStage({ ...stage, scrollOffset: stage.scrollOffset + 1 });
304
- return;
305
- }
306
- if (key.escape || input === "s" || input === "S") {
307
- if (chat.pendingMsgIndex !== null) {
308
- const msg = chat.allMessages[chat.pendingMsgIndex];
309
- if (msg?.type === "plan") {
310
- chat.setCommitted((prev) => [...prev, { ...msg, applied: false }]);
311
- chat.skipPatches(msg.patches);
312
- }
313
- }
314
- chat.setPendingMsgIndex(null);
315
- chat.setStage({ type: "idle" });
316
- return;
317
- }
318
- if (key.return || input === "a" || input === "A") {
319
- if (chat.pendingMsgIndex !== null) {
320
- const msg = chat.allMessages[chat.pendingMsgIndex];
321
- if (msg?.type === "plan") {
322
- chat.applyPatchesAndContinue(msg.patches);
323
- const applied: Message = { ...msg, applied: true };
324
- chat.setAllMessages((prev) =>
325
- prev.map((m, i) => (i === chat.pendingMsgIndex ? applied : m)),
326
- );
327
- chat.setCommitted((prev) => [...prev, applied]);
328
- }
329
- }
330
- chat.setPendingMsgIndex(null);
331
- chat.setStage({ type: "idle" });
332
- return;
333
- }
334
- }
335
-
336
- if (stage.type === "viewing-file") {
337
- if (key.upArrow) {
338
- chat.setStage({
339
- ...stage,
340
- scrollOffset: Math.max(0, stage.scrollOffset - 1),
341
- });
342
- return;
343
- }
344
- if (key.downArrow) {
345
- chat.setStage({ ...stage, scrollOffset: stage.scrollOffset + 1 });
346
- return;
347
- }
348
- if (key.escape || key.return) {
349
- chat.setStage({ type: "idle" });
350
- return;
351
- }
352
- }
353
- };
354
-
355
- const chatInput = useChatInput(
356
- chat.stage,
357
- chat.showTimeline,
358
- chat.showForceWarning,
359
- chat.abortThinking,
360
- handleStageKey,
361
- );
362
-
363
- const sendMessage = (text: string) => {
364
- if (!chat.provider) return;
365
-
366
- const handled = handleCommand(text, {
367
- repoPath,
368
- allMessages: chat.allMessages,
369
- autoApprove: chat.autoApprove,
370
- forceApprove: chat.forceApprove,
371
- chatName: chat.chatName,
372
- chatNameRef: chat.chatNameRef,
373
- setShowTimeline: chat.setShowTimeline,
374
- setShowReview: chat.setShowReview,
375
- setShowForceWarning: chat.setShowForceWarning,
376
- setForceApprove: chat.setForceApprove,
377
- setAutoApprove: chat.setAutoApprove,
378
- setAllMessages: chat.setAllMessages as any,
379
- setCommitted: chat.setCommitted as any,
380
- setRecentChats: chat.setRecentChats,
381
- updateChatName: chat.updateChatName,
382
- });
383
-
384
- if (handled) return;
385
-
386
- chatInput.pushHistory(text);
387
- chat.sendMessage(text, chat.provider, chat.systemPrompt, chat.allMessages);
388
-
389
- if (!chat.chatName) {
390
- const name =
391
- getChatNameSuggestions([
392
- ...chat.allMessages,
393
- { role: "user", content: text, type: "text" },
394
- ])[0] ?? `chat-${new Date().toISOString().slice(0, 10)}`;
395
- chat.updateChatName(name);
396
- chat.setRecentChats((prev) =>
397
- [name, ...prev.filter((n) => n !== name)].slice(0, 10),
398
- );
399
- }
400
- };
401
-
402
- const { stage } = chat;
403
-
404
- if (stage.type === "picking-provider")
405
- return <ProviderPicker onDone={chat.handleProviderDone} />;
406
- if (stage.type === "loading")
407
- return (
408
- <Box gap={1} marginTop={1}>
409
- <Text color={ACCENT}>*</Text>
410
- <Text color={ACCENT}>
411
- <Spinner />
412
- </Text>
413
- <Text color="gray" dimColor>
414
- indexing codebase…
415
- </Text>
416
- </Box>
417
- );
418
- if (chat.showTimeline)
419
- return (
420
- <TimelineRunner
421
- repoPath={repoPath}
422
- onExit={() => chat.setShowTimeline(false)}
423
- />
424
- );
425
- if (chat.showReview)
426
- return (
427
- <ReviewCommand path={repoPath} onExit={() => chat.setShowReview(false)} />
428
- );
429
- if (stage.type === "clone-offer")
430
- return <CloneOfferView stage={stage} committed={chat.committed} />;
431
- if (stage.type === "cloning")
432
- return <CloningView stage={stage} committed={chat.committed} />;
433
- if (stage.type === "clone-exists")
434
- return <CloneExistsView stage={stage} committed={chat.committed} />;
435
- if (stage.type === "clone-done")
436
- return <CloneDoneView stage={stage} committed={chat.committed} />;
437
- if (stage.type === "clone-error")
438
- return <CloneErrorView stage={stage} committed={chat.committed} />;
439
- if (stage.type === "preview")
440
- return <PreviewView stage={stage} committed={chat.committed} />;
441
- if (stage.type === "viewing-file")
442
- return <ViewingFileView stage={stage} committed={chat.committed} />;
443
-
444
- return (
445
- <Box flexDirection="column">
446
- <Static items={chat.committed}>
447
- {(msg, i) => <StaticMessage key={i} msg={msg} />}
448
- </Static>
449
-
450
- {chat.showForceWarning && (
451
- <ForceAllWarning
452
- onConfirm={(confirmed) => {
453
- chat.setShowForceWarning(false);
454
- if (confirmed) {
455
- chat.setForceApprove(true);
456
- chat.setAutoApprove(true);
457
- const msg: Message = {
458
- role: "assistant",
459
- content:
460
- "⚡⚡ Force-all mode ON — ALL tools auto-approved including shell and writes. Type /auto --force-all again to disable.",
461
- type: "text",
462
- };
463
- chat.setCommitted((prev) => [...prev, msg]);
464
- chat.setAllMessages((prev: Message[]) => [...prev, msg]);
465
- } else {
466
- const msg: Message = {
467
- role: "assistant",
468
- content: "Force-all cancelled.",
469
- type: "text",
470
- };
471
- chat.setCommitted((prev) => [...prev, msg]);
472
- chat.setAllMessages((prev: Message[]) => [...prev, msg]);
473
- }
474
- }}
475
- />
476
- )}
477
-
478
- {!chat.showForceWarning && stage.type === "thinking" && (
479
- <Box gap={1}>
480
- <Text color={ACCENT}>●</Text>
481
- <TypewriterText text={thinkingPhrase} />
482
- <Text color="gray" dimColor>
483
- · esc cancel
484
- </Text>
485
- </Box>
486
- )}
487
-
488
- {!chat.showForceWarning && stage.type === "permission" && (
489
- <PermissionPrompt tool={stage.tool} onDecide={stage.resolve} />
490
- )}
491
-
492
- {!chat.showForceWarning && stage.type === "idle" && (
493
- <Box flexDirection="column">
494
- {chatInput.inputValue.startsWith("/") && (
495
- <CommandPalette
496
- query={chatInput.inputValue}
497
- recentChats={chat.recentChats}
498
- />
499
- )}
500
- <InputBox
501
- value={chatInput.inputValue}
502
- onChange={(v) => chatInput.setInputValue(v)}
503
- onSubmit={(val) => {
504
- if (val.trim()) sendMessage(val.trim());
505
- chatInput.setInputValue("");
506
- }}
507
- inputKey={chatInput.inputKey}
508
- />
509
- <ShortcutBar
510
- autoApprove={chat.autoApprove}
511
- forceApprove={chat.forceApprove}
512
- />
513
- </Box>
514
- )}
515
- </Box>
516
- );
517
- };