@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,631 +0,0 @@
1
- import { useState, useRef } from "react";
2
- import React from "react";
3
- import type { Provider } from "../../../types/config";
4
- import { classifyIntent } from "../../../utils/intentClassifier";
5
- import type { Message, ChatStage } from "../../../types/chat";
6
- import {
7
- saveChat,
8
- listChats,
9
- getChatNameSuggestions,
10
- } from "../../../utils/chatHistory";
11
- import {
12
- appendMemory,
13
- buildMemorySummary,
14
- addMemory,
15
- deleteMemory,
16
- getSessionToolSummary,
17
- logToolCall,
18
- } from "../../../utils/memory";
19
- import { fetchFileTree, readImportantFiles } from "../../../utils/files";
20
- import { readLensFile } from "../../../utils/lensfile";
21
- import { registry } from "../../../utils/tools/registry";
22
- import { buildDiffs } from "../../repo/DiffViewer";
23
- import {
24
- walkDir,
25
- applyPatches,
26
- extractGithubUrl,
27
- toCloneUrl,
28
- buildSystemPrompt,
29
- parseResponse,
30
- callChat,
31
- type ChatResult,
32
- } from "../../../utils/chat";
33
-
34
- export function useChat(repoPath: string) {
35
- const [stage, setStage] = useState<ChatStage>({ type: "picking-provider" });
36
- const [committed, setCommitted] = useState<Message[]>([]);
37
- const [provider, setProvider] = useState<Provider | null>(null);
38
- const [systemPrompt, setSystemPrompt] = useState("");
39
- const [pendingMsgIndex, setPendingMsgIndex] = useState<number | null>(null);
40
- const [allMessages, setAllMessages] = useState<Message[]>([]);
41
- const [clonedUrls, setClonedUrls] = useState<Set<string>>(new Set());
42
- const [showTimeline, setShowTimeline] = useState(false);
43
- const [showReview, setShowReview] = useState(false);
44
- const [autoApprove, setAutoApprove] = useState(false);
45
- const [forceApprove, setForceApprove] = useState(false);
46
- const [showForceWarning, setShowForceWarning] = useState(false);
47
- const [chatName, setChatName] = useState<string | null>(null);
48
- const [recentChats, setRecentChats] = useState<string[]>([]);
49
-
50
- const chatNameRef = useRef<string | null>(null);
51
- const providerRef = useRef<Provider | null>(null);
52
- const systemPromptRef = useRef<string>("");
53
- const abortControllerRef = useRef<AbortController | null>(null);
54
- const toolResultCache = useRef<Map<string, string>>(new Map());
55
- const batchApprovedRef = useRef(false);
56
-
57
- const updateChatName = (name: string) => {
58
- chatNameRef.current = name;
59
- setChatName(name);
60
- };
61
-
62
- React.useEffect(() => {
63
- providerRef.current = provider;
64
- }, [provider]);
65
- React.useEffect(() => {
66
- systemPromptRef.current = systemPrompt;
67
- }, [systemPrompt]);
68
-
69
- React.useEffect(() => {
70
- const chats = listChats(repoPath);
71
- setRecentChats(chats.slice(0, 10).map((c) => c.name));
72
- }, [repoPath]);
73
-
74
- React.useEffect(() => {
75
- if (chatNameRef.current && allMessages.length > 1) {
76
- saveChat(chatNameRef.current, repoPath, allMessages);
77
- }
78
- }, [allMessages]);
79
-
80
- const handleError = (currentAll: Message[]) => (err: unknown) => {
81
- batchApprovedRef.current = false;
82
- if (err instanceof Error && err.name === "AbortError") {
83
- setStage({ type: "idle" });
84
- return;
85
- }
86
- const errMsg: Message = {
87
- role: "assistant",
88
- content: `Error: ${err instanceof Error ? err.message : "Something went wrong"}`,
89
- type: "text",
90
- };
91
- setAllMessages([...currentAll, errMsg]);
92
- setCommitted((prev) => [...prev, errMsg]);
93
- setStage({ type: "idle" });
94
- };
95
-
96
- const MAX_AUTO_CONTINUES = 3;
97
-
98
- function isLikelyTruncated(text: string): boolean {
99
- // Check unclosed XML tool tags (dynamic — includes addon tools)
100
- for (const tag of registry.names()) {
101
- if (text.includes(`<${tag}>`) && !text.includes(`</${tag}>`))
102
- return true;
103
- }
104
- // Check unclosed fenced code blocks (```tool\n... without closing ```)
105
- const fences = text.match(/```/g);
106
- if (fences && fences.length % 2 !== 0) return true;
107
- return false;
108
- }
109
-
110
- const processResponse = (
111
- raw: string,
112
- currentAll: Message[],
113
- signal: AbortSignal,
114
- truncated = false,
115
- continueCount = 0,
116
- ) => {
117
- if (signal.aborted) {
118
- batchApprovedRef.current = false;
119
- setStage({ type: "idle" });
120
- return;
121
- }
122
-
123
- if (truncated || isLikelyTruncated(raw)) {
124
- if (continueCount >= MAX_AUTO_CONTINUES) {
125
- // Give up after max attempts — show whatever we have
126
- batchApprovedRef.current = false;
127
- const msg: Message = {
128
- role: "assistant",
129
- content:
130
- raw.trim() ||
131
- "(response was empty after multiple continuation attempts)",
132
- type: "text",
133
- };
134
- setAllMessages([...currentAll, msg]);
135
- setCommitted((prev) => [...prev, msg]);
136
- setStage({ type: "idle" });
137
- return;
138
- }
139
-
140
- // Include the partial response so the model knows where it left off
141
- const partialMsg: Message = {
142
- role: "assistant",
143
- content: raw,
144
- type: "text",
145
- };
146
- const nudgeMsg: Message = {
147
- role: "user",
148
- content:
149
- "Your response was cut off. Please continue exactly from where you left off.",
150
- type: "text",
151
- };
152
- const withContext = [...currentAll, partialMsg, nudgeMsg];
153
-
154
- const truncMsg: Message = {
155
- role: "assistant",
156
- content: `(response cut off — auto-continuing ${continueCount + 1}/${MAX_AUTO_CONTINUES}…)`,
157
- type: "text",
158
- };
159
- setAllMessages([...currentAll, truncMsg]);
160
- setCommitted((prev) => [...prev, truncMsg]);
161
-
162
- const currentProvider = providerRef.current;
163
- const currentSystemPrompt = systemPromptRef.current;
164
-
165
- if (!currentProvider) {
166
- setStage({ type: "idle" });
167
- return;
168
- }
169
-
170
- const nextAbort = new AbortController();
171
- abortControllerRef.current = nextAbort;
172
- setStage({ type: "thinking" });
173
- callChat(
174
- currentProvider,
175
- currentSystemPrompt,
176
- withContext,
177
- nextAbort.signal,
178
- )
179
- .then((result: ChatResult) => {
180
- if (nextAbort.signal.aborted) return;
181
- processResponse(
182
- result.text ?? "",
183
- withContext,
184
- nextAbort.signal,
185
- result.truncated,
186
- continueCount + 1,
187
- );
188
- })
189
- .catch(handleError(withContext));
190
- return;
191
- }
192
-
193
- const memAddMatches = [
194
- ...raw.matchAll(/<memory-add>([\s\S]*?)<\/memory-add>/g),
195
- ];
196
- const memDelMatches = [
197
- ...raw.matchAll(/<memory-delete>([\s\S]*?)<\/memory-delete>/g),
198
- ];
199
- for (const match of memAddMatches) {
200
- const content = match[1]!.trim();
201
- if (content) addMemory(content, repoPath);
202
- }
203
- for (const match of memDelMatches) {
204
- const id = match[1]!.trim();
205
- if (id) deleteMemory(id, repoPath);
206
- }
207
- const cleanRaw = raw
208
- .replace(/<memory-add>[\s\S]*?<\/memory-add>/g, "")
209
- .replace(/<memory-delete>[\s\S]*?<\/memory-delete>/g, "")
210
- .trim();
211
-
212
- const parsed = parseResponse(cleanRaw);
213
-
214
- if (parsed.kind === "changes") {
215
- batchApprovedRef.current = false;
216
- if (parsed.patches.length === 0) {
217
- const msg: Message = {
218
- role: "assistant",
219
- content: parsed.content,
220
- type: "text",
221
- };
222
- setAllMessages([...currentAll, msg]);
223
- setCommitted((prev) => [...prev, msg]);
224
- setStage({ type: "idle" });
225
- return;
226
- }
227
- const assistantMsg: Message = {
228
- role: "assistant",
229
- content: parsed.content,
230
- type: "plan",
231
- patches: parsed.patches,
232
- applied: false,
233
- };
234
- const withAssistant = [...currentAll, assistantMsg];
235
- setAllMessages(withAssistant);
236
- setPendingMsgIndex(withAssistant.length - 1);
237
- const diffLines = buildDiffs(repoPath, parsed.patches);
238
- setStage({
239
- type: "preview",
240
- patches: parsed.patches,
241
- diffLines,
242
- scrollOffset: 0,
243
- pendingMessages: currentAll,
244
- });
245
- return;
246
- }
247
-
248
- if (parsed.kind === "clone") {
249
- batchApprovedRef.current = false;
250
- if (parsed.content) {
251
- const preambleMsg: Message = {
252
- role: "assistant",
253
- content: parsed.content,
254
- type: "text",
255
- };
256
- setAllMessages([...currentAll, preambleMsg]);
257
- setCommitted((prev) => [...prev, preambleMsg]);
258
- }
259
- setStage({
260
- type: "clone-offer",
261
- repoUrl: parsed.repoUrl,
262
- launchAnalysis: true,
263
- });
264
- return;
265
- }
266
-
267
- if (parsed.kind === "text") {
268
- batchApprovedRef.current = false;
269
-
270
- if (!parsed.content.trim()) {
271
- const stallMsg: Message = {
272
- role: "assistant",
273
- content:
274
- '(no response — the model may have stalled. Try sending a short follow-up like "continue" or start a new message.)',
275
- type: "text",
276
- };
277
- setAllMessages([...currentAll, stallMsg]);
278
- setCommitted((prev) => [...prev, stallMsg]);
279
- setStage({ type: "idle" });
280
- return;
281
- }
282
-
283
- const msg: Message = {
284
- role: "assistant",
285
- content: parsed.content,
286
- type: "text",
287
- };
288
- const withMsg = [...currentAll, msg];
289
- setAllMessages(withMsg);
290
- setCommitted((prev) => [...prev, msg]);
291
- const lastUserMsg = [...currentAll]
292
- .reverse()
293
- .find((m) => m.role === "user");
294
- const githubUrl = lastUserMsg
295
- ? extractGithubUrl(lastUserMsg.content)
296
- : null;
297
- if (githubUrl && !clonedUrls.has(githubUrl)) {
298
- setTimeout(
299
- () => setStage({ type: "clone-offer", repoUrl: githubUrl }),
300
- 80,
301
- );
302
- } else {
303
- setStage({ type: "idle" });
304
- }
305
- return;
306
- }
307
-
308
- const tool = registry.get(parsed.toolName);
309
- if (!tool) {
310
- batchApprovedRef.current = false;
311
- setStage({ type: "idle" });
312
- return;
313
- }
314
-
315
- if (parsed.content) {
316
- const preambleMsg: Message = {
317
- role: "assistant",
318
- content: parsed.content,
319
- type: "text",
320
- };
321
- setAllMessages([...currentAll, preambleMsg]);
322
- setCommitted((prev) => [...prev, preambleMsg]);
323
- }
324
-
325
- const remainder = parsed.remainder;
326
- const isSafe = tool.safe ?? false;
327
-
328
- const executeAndContinue = async (approved: boolean) => {
329
- if (approved && remainder) {
330
- batchApprovedRef.current = true;
331
- }
332
-
333
- const currentProvider = providerRef.current;
334
- const currentSystemPrompt = systemPromptRef.current;
335
-
336
- if (!currentProvider) {
337
- batchApprovedRef.current = false;
338
- setStage({ type: "idle" });
339
- return;
340
- }
341
-
342
- let result = "(denied by user)";
343
-
344
- if (approved) {
345
- const cacheKey = isSafe
346
- ? `${parsed.toolName}:${parsed.rawInput}`
347
- : null;
348
- if (cacheKey && toolResultCache.current.has(cacheKey)) {
349
- result =
350
- toolResultCache.current.get(cacheKey)! +
351
- "\n\n[NOTE: This result was already retrieved earlier. Do not request it again.]";
352
- } else {
353
- try {
354
- setStage({ type: "thinking" });
355
- const toolResult = await tool.execute(parsed.input, {
356
- repoPath,
357
- messages: currentAll,
358
- });
359
- result = toolResult.value;
360
- if (cacheKey && toolResult.kind === "text") {
361
- toolResultCache.current.set(cacheKey, result);
362
- }
363
- } catch (err: unknown) {
364
- result = `Error: ${err instanceof Error ? err.message : "failed"}`;
365
- }
366
- }
367
- }
368
-
369
- if (approved && !result.startsWith("Error:")) {
370
- logToolCall(
371
- parsed.toolName,
372
- tool.summariseInput
373
- ? String(tool.summariseInput(parsed.input))
374
- : parsed.rawInput,
375
- result,
376
- repoPath,
377
- );
378
- }
379
-
380
- const displayContent = tool.summariseInput
381
- ? String(tool.summariseInput(parsed.input))
382
- : parsed.rawInput;
383
-
384
- const toolMsg: Message = {
385
- role: "assistant",
386
- type: "tool",
387
- toolName: parsed.toolName as any,
388
- content: displayContent,
389
- result,
390
- approved,
391
- };
392
-
393
- const withTool = [...currentAll, toolMsg];
394
- setAllMessages(withTool);
395
- setCommitted((prev) => [...prev, toolMsg]);
396
-
397
- if (approved && remainder && remainder.length > 0) {
398
- processResponse(remainder, withTool, signal, truncated, continueCount);
399
- return;
400
- }
401
-
402
- batchApprovedRef.current = false;
403
-
404
- const nextAbort = new AbortController();
405
- abortControllerRef.current = nextAbort;
406
- setStage({ type: "thinking" });
407
-
408
- const callWithAutoContinue = async (
409
- messages: Message[],
410
- maxRetries = 3,
411
- ): Promise<ChatResult> => {
412
- let currentMessages = messages;
413
- for (let i = 0; i < maxRetries; i++) {
414
- if (nextAbort.signal.aborted)
415
- return { text: "", truncated: false };
416
- const result = await callChat(
417
- currentProvider,
418
- currentSystemPrompt,
419
- currentMessages,
420
- nextAbort.signal,
421
- );
422
- if (result.text.trim()) return result;
423
- const nudgeMsg: Message = {
424
- role: "assistant",
425
- content: `(model stalled — auto-continuing, attempt ${i + 1}/${maxRetries})`,
426
- type: "text",
427
- };
428
- setCommitted((prev) => [...prev, nudgeMsg]);
429
- setAllMessages((prev) => [...prev, nudgeMsg]);
430
- currentMessages = [
431
- ...currentMessages,
432
- {
433
- role: "user",
434
- content:
435
- "Please continue. Provide your response to the previous tool output.",
436
- type: "text",
437
- },
438
- ];
439
- }
440
- return { text: "", truncated: false };
441
- };
442
-
443
- callWithAutoContinue(withTool)
444
- .then((result: ChatResult) => {
445
- if (nextAbort.signal.aborted) return;
446
- processResponse(
447
- result.text ?? "",
448
- withTool,
449
- nextAbort.signal,
450
- result.truncated,
451
- );
452
- })
453
- .catch(handleError(withTool));
454
- };
455
-
456
- if (forceApprove || isSafe || batchApprovedRef.current) {
457
- executeAndContinue(true);
458
- return;
459
- }
460
-
461
- const permLabel = tool.permissionLabel ?? tool.name;
462
- const permValue = tool.summariseInput
463
- ? String(tool.summariseInput(parsed.input))
464
- : parsed.rawInput;
465
-
466
- setStage({
467
- type: "permission",
468
- tool: {
469
- type: parsed.toolName as any,
470
- _display: permValue,
471
- _label: permLabel,
472
- } as any,
473
- pendingMessages: currentAll,
474
- resolve: executeAndContinue,
475
- });
476
- };
477
-
478
- const sendMessage = (
479
- text: string,
480
- currentProvider: Provider,
481
- currentSystemPrompt: string,
482
- currentAllMessages: Message[],
483
- ) => {
484
- const userMsg: Message = { role: "user", content: text, type: "text" };
485
- const nextAll = [...currentAllMessages, userMsg];
486
- setCommitted((prev) => [...prev, userMsg]);
487
- setAllMessages(nextAll);
488
- batchApprovedRef.current = false;
489
-
490
- if (!chatName) {
491
- const name =
492
- getChatNameSuggestions(nextAll)[0] ??
493
- `chat-${new Date().toISOString().slice(0, 10)}`;
494
- updateChatName(name);
495
- setRecentChats((prev) =>
496
- [name, ...prev.filter((n) => n !== name)].slice(0, 10),
497
- );
498
- saveChat(name, repoPath, nextAll);
499
- }
500
-
501
- const abort = new AbortController();
502
- abortControllerRef.current = abort;
503
-
504
- const intent = classifyIntent(text);
505
- const scopedToolsSection = registry.buildSystemPromptSection(intent);
506
- const sessionSummary = getSessionToolSummary(repoPath);
507
-
508
- let scopedSystemPrompt = currentSystemPrompt.replace(
509
- /## TOOLS[\s\S]*?(?=\n## (?!TOOLS))/,
510
- scopedToolsSection + "\n\n",
511
- );
512
-
513
- if (sessionSummary) {
514
- scopedSystemPrompt = scopedSystemPrompt.replace(
515
- /## CODEBASE/,
516
- sessionSummary + "\n\n## CODEBASE",
517
- );
518
- }
519
-
520
- setStage({ type: "thinking" });
521
- callChat(currentProvider, scopedSystemPrompt, nextAll, abort.signal)
522
- .then((result: ChatResult) =>
523
- processResponse(result.text, nextAll, abort.signal, result.truncated),
524
- )
525
- .catch(handleError(nextAll));
526
- };
527
-
528
- const handleProviderDone = (p: Provider) => {
529
- setProvider(p);
530
- providerRef.current = p;
531
- setStage({ type: "loading" });
532
- fetchFileTree(repoPath)
533
- .catch(() => walkDir(repoPath))
534
- .then((fileTree) => {
535
- const importantFiles = readImportantFiles(repoPath, fileTree);
536
- const historySummary = buildMemorySummary(repoPath);
537
- const lensFile = readLensFile(repoPath);
538
- const lensContext = lensFile
539
- ? `\n\n## LENS.md (previous analysis)\n${lensFile.overview}\n\nImportant folders: ${lensFile.importantFolders.join(", ")}\nSuggestions: ${lensFile.suggestions.slice(0, 3).join("; ")}`
540
- : "";
541
- const toolsSection = registry.buildSystemPromptSection();
542
- const prompt =
543
- buildSystemPrompt(importantFiles, historySummary, toolsSection) +
544
- lensContext;
545
- setSystemPrompt(prompt);
546
- systemPromptRef.current = prompt;
547
- const greeting: Message = {
548
- role: "assistant",
549
- content: `Welcome to Lens\nCodebase loaded — ${importantFiles.length} files indexed.${historySummary ? "\n\nI have memory of previous actions in this repo." : ""}${lensFile ? "\n\nFound LENS.md — I have context from a previous analysis of this repo." : ""}\nAsk me anything, tell me what to build, share a URL, or ask me to read/write files.\n\nTip: type /timeline to browse commit history.\nTip: ⭐ Star Lens on GitHub — github.com/ridit-jangra/Lens`,
550
- type: "text",
551
- };
552
- setCommitted([greeting]);
553
- setAllMessages([greeting]);
554
- setStage({ type: "idle" });
555
- })
556
- .catch(() => setStage({ type: "idle" }));
557
- };
558
-
559
- const abortThinking = () => {
560
- abortControllerRef.current?.abort();
561
- abortControllerRef.current = null;
562
- batchApprovedRef.current = false;
563
- setStage({ type: "idle" });
564
- };
565
-
566
- const applyPatchesAndContinue = (patches: any[]) => {
567
- try {
568
- applyPatches(repoPath, patches);
569
- logToolCall(
570
- "changes",
571
- patches.map((p) => p.path).join(", "),
572
- `Applied changes to ${patches.length} file(s)`,
573
- repoPath,
574
- );
575
- } catch {
576
- /* non-fatal */
577
- }
578
- };
579
-
580
- const skipPatches = (patches: any[]) => {
581
- logToolCall(
582
- "changes-skipped",
583
- patches.map((p: { path: string }) => p.path).join(", "),
584
- `Skipped changes to ${patches.length} file(s)`,
585
- repoPath,
586
- );
587
- };
588
-
589
- return {
590
- stage,
591
- setStage,
592
- committed,
593
- setCommitted,
594
- provider,
595
- setProvider,
596
- systemPrompt,
597
- allMessages,
598
- setAllMessages,
599
- clonedUrls,
600
- setClonedUrls,
601
- showTimeline,
602
- setShowTimeline,
603
- showReview,
604
- setShowReview,
605
- autoApprove,
606
- setAutoApprove,
607
- forceApprove,
608
- setForceApprove,
609
- showForceWarning,
610
- setShowForceWarning,
611
- chatName,
612
- setChatName,
613
- recentChats,
614
- setRecentChats,
615
- pendingMsgIndex,
616
- setPendingMsgIndex,
617
-
618
- chatNameRef,
619
- providerRef,
620
- batchApprovedRef,
621
-
622
- updateChatName,
623
- sendMessage,
624
- handleProviderDone,
625
- abortThinking,
626
- applyPatchesAndContinue,
627
- skipPatches,
628
- processResponse,
629
- handleError,
630
- };
631
- }
@@ -1,79 +0,0 @@
1
- import { useState, useRef } from "react";
2
- import { useInput } from "ink";
3
- import { COMMANDS } from "./useCommandHandlers";
4
- import type { ChatStage } from "../../../types/chat";
5
-
6
- export function useChatInput(
7
- stage: ChatStage,
8
- showTimeline: boolean,
9
- showForceWarning: boolean,
10
- onAbortThinking: () => void,
11
- onStageKeyInput: (input: string, key: any) => void,
12
- ) {
13
- const [inputValue, setInputValue] = useState("");
14
- const [inputKey, setInputKey] = useState(0);
15
- const inputHistoryRef = useRef<string[]>([]);
16
- const historyIndexRef = useRef<number>(-1);
17
-
18
- const pushHistory = (text: string) => {
19
- inputHistoryRef.current = [
20
- text,
21
- ...inputHistoryRef.current.filter((m) => m !== text),
22
- ].slice(0, 50);
23
- historyIndexRef.current = -1;
24
- };
25
-
26
- useInput((input, key) => {
27
- if (showTimeline) return;
28
-
29
- if (showForceWarning && key.escape) {
30
- onStageKeyInput(input, key);
31
- return;
32
- }
33
-
34
- if (stage.type === "thinking" && key.escape) {
35
- onAbortThinking();
36
- return;
37
- }
38
-
39
- if (stage.type === "idle") {
40
- if (key.ctrl && input === "c") {
41
- process.exit(0);
42
- return;
43
- }
44
- if (key.upArrow && inputHistoryRef.current.length > 0) {
45
- const next = Math.min(
46
- historyIndexRef.current + 1,
47
- inputHistoryRef.current.length - 1,
48
- );
49
- historyIndexRef.current = next;
50
- setInputValue(inputHistoryRef.current[next]!);
51
- setInputKey((k) => k + 1);
52
- return;
53
- }
54
- if (key.downArrow) {
55
- const next = historyIndexRef.current - 1;
56
- historyIndexRef.current = next;
57
- setInputValue(next < 0 ? "" : inputHistoryRef.current[next]!);
58
- setInputKey((k) => k + 1);
59
- return;
60
- }
61
- if (key.tab && inputValue.startsWith("/")) {
62
- const q = inputValue.toLowerCase();
63
- const match = COMMANDS.find((c) => c.cmd.startsWith(q));
64
- if (match) setInputValue(match.cmd);
65
- return;
66
- }
67
- return;
68
- }
69
-
70
- onStageKeyInput(input, key);
71
- });
72
-
73
- return {
74
- inputValue,
75
- setInputValue,
76
- inputKey,
77
- pushHistory,
78
- };
79
- }