@ridit/lens 0.2.1 → 0.2.2
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.
- package/dist/index.mjs +629 -461
- package/package.json +1 -1
- package/src/components/chat/ChatOverlays.tsx +73 -46
- package/src/components/chat/ChatRunner.tsx +203 -369
- package/src/index.tsx +3 -0
- package/src/prompts/system.ts +79 -102
- package/src/utils/chat.ts +95 -176
- package/src/utils/tools/builtins.ts +324 -0
- package/src/utils/tools/registry.ts +119 -0
|
@@ -17,20 +17,9 @@ import {
|
|
|
17
17
|
extractGithubUrl,
|
|
18
18
|
toCloneUrl,
|
|
19
19
|
parseCloneTag,
|
|
20
|
-
runShell,
|
|
21
|
-
fetchUrl,
|
|
22
|
-
readFile,
|
|
23
|
-
readFolder,
|
|
24
|
-
grepFiles,
|
|
25
|
-
deleteFile,
|
|
26
|
-
deleteFolder,
|
|
27
|
-
openUrl,
|
|
28
|
-
generatePdf,
|
|
29
|
-
writeFile,
|
|
30
20
|
buildSystemPrompt,
|
|
31
21
|
parseResponse,
|
|
32
22
|
callChat,
|
|
33
|
-
searchWeb,
|
|
34
23
|
} from "../../utils/chat";
|
|
35
24
|
import {
|
|
36
25
|
saveChat,
|
|
@@ -66,6 +55,7 @@ import {
|
|
|
66
55
|
} from "../../utils/memory";
|
|
67
56
|
import { readLensFile } from "../../utils/lensfile";
|
|
68
57
|
import { ReviewCommand } from "../../commands/review";
|
|
58
|
+
import { registry } from "../../utils/tools/registry";
|
|
69
59
|
|
|
70
60
|
const COMMANDS = [
|
|
71
61
|
{ cmd: "/timeline", desc: "browse commit history" },
|
|
@@ -94,8 +84,6 @@ function CommandPalette({
|
|
|
94
84
|
recentChats: string[];
|
|
95
85
|
}) {
|
|
96
86
|
const q = query.toLowerCase();
|
|
97
|
-
|
|
98
|
-
// If typing "/chat load <something>", stay visible and filter chats
|
|
99
87
|
const isChatLoad = q.startsWith("/chat load") || q.startsWith("/chat delete");
|
|
100
88
|
const chatFilter = isChatLoad
|
|
101
89
|
? q.startsWith("/chat load")
|
|
@@ -105,13 +93,9 @@ function CommandPalette({
|
|
|
105
93
|
const filteredChats = chatFilter
|
|
106
94
|
? recentChats.filter((n) => n.toLowerCase().includes(chatFilter))
|
|
107
95
|
: recentChats;
|
|
108
|
-
|
|
109
96
|
const matches = COMMANDS.filter((c) => c.cmd.startsWith(q));
|
|
110
|
-
|
|
111
|
-
// Keep palette open if we're in /chat load mode even after space
|
|
112
97
|
if (!matches.length && !isChatLoad) return null;
|
|
113
98
|
if (!matches.length && isChatLoad && filteredChats.length === 0) return null;
|
|
114
|
-
|
|
115
99
|
return (
|
|
116
100
|
<Box flexDirection="column" marginBottom={1} marginLeft={2}>
|
|
117
101
|
{matches.map((c, i) => {
|
|
@@ -170,39 +154,28 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
170
154
|
|
|
171
155
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
172
156
|
const toolResultCache = useRef<Map<string, string>>(new Map());
|
|
173
|
-
|
|
174
|
-
|
|
157
|
+
|
|
158
|
+
// When the user approves a tool that has chained remainder calls, we
|
|
159
|
+
// automatically approve subsequent tools in the same chain so the user
|
|
160
|
+
// doesn't have to press y for every file in a 10-file scaffold.
|
|
161
|
+
// This ref is set to true on the first approval and cleared when the chain ends.
|
|
162
|
+
const batchApprovedRef = useRef(false);
|
|
163
|
+
|
|
175
164
|
const thinkingPhrase = useThinkingPhrase(stage.type === "thinking");
|
|
176
165
|
|
|
177
|
-
// Load recent chats on mount
|
|
178
166
|
React.useEffect(() => {
|
|
179
167
|
const chats = listChats(repoPath);
|
|
180
168
|
setRecentChats(chats.slice(0, 10).map((c) => c.name));
|
|
181
169
|
}, [repoPath]);
|
|
182
170
|
|
|
183
|
-
// Auto-save whenever messages change
|
|
184
171
|
React.useEffect(() => {
|
|
185
172
|
if (chatNameRef.current && allMessages.length > 1) {
|
|
186
173
|
saveChat(chatNameRef.current, repoPath, allMessages);
|
|
187
174
|
}
|
|
188
175
|
}, [allMessages]);
|
|
189
176
|
|
|
190
|
-
const flushBuffer = () => {
|
|
191
|
-
const buf = inputBuffer.current;
|
|
192
|
-
if (!buf) return;
|
|
193
|
-
inputBuffer.current = "";
|
|
194
|
-
setInputValue((v) => v + buf);
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const scheduleFlush = () => {
|
|
198
|
-
if (flushTimer.current !== null) return;
|
|
199
|
-
flushTimer.current = setTimeout(() => {
|
|
200
|
-
flushTimer.current = null;
|
|
201
|
-
flushBuffer();
|
|
202
|
-
}, 16);
|
|
203
|
-
};
|
|
204
|
-
|
|
205
177
|
const handleError = (currentAll: Message[]) => (err: unknown) => {
|
|
178
|
+
batchApprovedRef.current = false;
|
|
206
179
|
if (err instanceof Error && err.name === "AbortError") {
|
|
207
180
|
setStage({ type: "idle" });
|
|
208
181
|
return;
|
|
@@ -223,11 +196,12 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
223
196
|
signal: AbortSignal,
|
|
224
197
|
) => {
|
|
225
198
|
if (signal.aborted) {
|
|
199
|
+
batchApprovedRef.current = false;
|
|
226
200
|
setStage({ type: "idle" });
|
|
227
201
|
return;
|
|
228
202
|
}
|
|
229
203
|
|
|
230
|
-
// Handle inline memory operations
|
|
204
|
+
// Handle inline memory operations
|
|
231
205
|
const memAddMatches = [
|
|
232
206
|
...raw.matchAll(/<memory-add>([\s\S]*?)<\/memory-add>/g),
|
|
233
207
|
];
|
|
@@ -242,7 +216,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
242
216
|
const id = match[1]!.trim();
|
|
243
217
|
if (id) deleteMemory(id, repoPath);
|
|
244
218
|
}
|
|
245
|
-
// Strip memory tags from raw before parsing
|
|
246
219
|
const cleanRaw = raw
|
|
247
220
|
.replace(/<memory-add>[\s\S]*?<\/memory-add>/g, "")
|
|
248
221
|
.replace(/<memory-delete>[\s\S]*?<\/memory-delete>/g, "")
|
|
@@ -250,7 +223,10 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
250
223
|
|
|
251
224
|
const parsed = parseResponse(cleanRaw);
|
|
252
225
|
|
|
226
|
+
// ── changes (diff preview UI) ──────────────────────────────────────────
|
|
227
|
+
|
|
253
228
|
if (parsed.kind === "changes") {
|
|
229
|
+
batchApprovedRef.current = false;
|
|
254
230
|
if (parsed.patches.length === 0) {
|
|
255
231
|
const msg: Message = {
|
|
256
232
|
role: "assistant",
|
|
@@ -283,52 +259,10 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
283
259
|
return;
|
|
284
260
|
}
|
|
285
261
|
|
|
286
|
-
|
|
287
|
-
parsed.kind === "shell" ||
|
|
288
|
-
parsed.kind === "fetch" ||
|
|
289
|
-
parsed.kind === "read-file" ||
|
|
290
|
-
parsed.kind === "read-folder" ||
|
|
291
|
-
parsed.kind === "grep" ||
|
|
292
|
-
parsed.kind === "write-file" ||
|
|
293
|
-
parsed.kind === "delete-file" ||
|
|
294
|
-
parsed.kind === "delete-folder" ||
|
|
295
|
-
parsed.kind === "open-url" ||
|
|
296
|
-
parsed.kind === "generate-pdf" ||
|
|
297
|
-
parsed.kind === "search"
|
|
298
|
-
) {
|
|
299
|
-
let tool: Parameters<typeof PermissionPrompt>[0]["tool"];
|
|
300
|
-
if (parsed.kind === "shell") {
|
|
301
|
-
tool = { type: "shell", command: parsed.command };
|
|
302
|
-
} else if (parsed.kind === "fetch") {
|
|
303
|
-
tool = { type: "fetch", url: parsed.url };
|
|
304
|
-
} else if (parsed.kind === "read-file") {
|
|
305
|
-
tool = { type: "read-file", filePath: parsed.filePath };
|
|
306
|
-
} else if (parsed.kind === "read-folder") {
|
|
307
|
-
tool = { type: "read-folder", folderPath: parsed.folderPath };
|
|
308
|
-
} else if (parsed.kind === "grep") {
|
|
309
|
-
tool = { type: "grep", pattern: parsed.pattern, glob: parsed.glob };
|
|
310
|
-
} else if (parsed.kind === "delete-file") {
|
|
311
|
-
tool = { type: "delete-file", filePath: parsed.filePath };
|
|
312
|
-
} else if (parsed.kind === "delete-folder") {
|
|
313
|
-
tool = { type: "delete-folder", folderPath: parsed.folderPath };
|
|
314
|
-
} else if (parsed.kind === "open-url") {
|
|
315
|
-
tool = { type: "open-url", url: parsed.url };
|
|
316
|
-
} else if (parsed.kind === "generate-pdf") {
|
|
317
|
-
tool = {
|
|
318
|
-
type: "generate-pdf",
|
|
319
|
-
filePath: parsed.filePath,
|
|
320
|
-
content: parsed.pdfContent,
|
|
321
|
-
};
|
|
322
|
-
} else if (parsed.kind === "search") {
|
|
323
|
-
tool = { type: "search", query: parsed.query };
|
|
324
|
-
} else {
|
|
325
|
-
tool = {
|
|
326
|
-
type: "write-file",
|
|
327
|
-
filePath: parsed.filePath,
|
|
328
|
-
fileContent: parsed.fileContent,
|
|
329
|
-
};
|
|
330
|
-
}
|
|
262
|
+
// ── clone (git clone UI flow) ──────────────────────────────────────────
|
|
331
263
|
|
|
264
|
+
if (parsed.kind === "clone") {
|
|
265
|
+
batchApprovedRef.current = false;
|
|
332
266
|
if (parsed.content) {
|
|
333
267
|
const preambleMsg: Message = {
|
|
334
268
|
role: "assistant",
|
|
@@ -338,236 +272,166 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
338
272
|
setAllMessages([...currentAll, preambleMsg]);
|
|
339
273
|
setCommitted((prev) => [...prev, preambleMsg]);
|
|
340
274
|
}
|
|
275
|
+
setStage({
|
|
276
|
+
type: "clone-offer",
|
|
277
|
+
repoUrl: parsed.repoUrl,
|
|
278
|
+
launchAnalysis: true,
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
341
282
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
parsed.
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
283
|
+
// ── text ──────────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
if (parsed.kind === "text") {
|
|
286
|
+
batchApprovedRef.current = false;
|
|
287
|
+
const msg: Message = {
|
|
288
|
+
role: "assistant",
|
|
289
|
+
content: parsed.content,
|
|
290
|
+
type: "text",
|
|
291
|
+
};
|
|
292
|
+
const withMsg = [...currentAll, msg];
|
|
293
|
+
setAllMessages(withMsg);
|
|
294
|
+
setCommitted((prev) => [...prev, msg]);
|
|
295
|
+
const lastUserMsg = [...currentAll]
|
|
296
|
+
.reverse()
|
|
297
|
+
.find((m) => m.role === "user");
|
|
298
|
+
const githubUrl = lastUserMsg
|
|
299
|
+
? extractGithubUrl(lastUserMsg.content)
|
|
300
|
+
: null;
|
|
301
|
+
if (githubUrl && !clonedUrls.has(githubUrl)) {
|
|
302
|
+
setTimeout(
|
|
303
|
+
() => setStage({ type: "clone-offer", repoUrl: githubUrl }),
|
|
304
|
+
80,
|
|
305
|
+
);
|
|
306
|
+
} else {
|
|
307
|
+
setStage({ type: "idle" });
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ── generic tool ──────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
const tool = registry.get(parsed.toolName);
|
|
315
|
+
if (!tool) {
|
|
316
|
+
batchApprovedRef.current = false;
|
|
317
|
+
setStage({ type: "idle" });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (parsed.content) {
|
|
322
|
+
const preambleMsg: Message = {
|
|
323
|
+
role: "assistant",
|
|
324
|
+
content: parsed.content,
|
|
325
|
+
type: "text",
|
|
326
|
+
};
|
|
327
|
+
setAllMessages([...currentAll, preambleMsg]);
|
|
328
|
+
setCommitted((prev) => [...prev, preambleMsg]);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const remainder = parsed.remainder;
|
|
332
|
+
const isSafe = tool.safe ?? false;
|
|
333
|
+
|
|
334
|
+
const executeAndContinue = async (approved: boolean) => {
|
|
335
|
+
// If the user approved this tool and there are more in the chain,
|
|
336
|
+
// mark the batch as approved so subsequent tools skip the prompt.
|
|
337
|
+
if (approved && remainder) {
|
|
338
|
+
batchApprovedRef.current = true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let result = "(denied by user)";
|
|
342
|
+
|
|
343
|
+
if (approved) {
|
|
344
|
+
const cacheKey = isSafe
|
|
345
|
+
? `${parsed.toolName}:${parsed.rawInput}`
|
|
346
|
+
: null;
|
|
347
|
+
if (cacheKey && toolResultCache.current.has(cacheKey)) {
|
|
348
|
+
result =
|
|
349
|
+
toolResultCache.current.get(cacheKey)! +
|
|
350
|
+
"\n\n[NOTE: This result was already retrieved earlier. Do not request it again.]";
|
|
351
|
+
} else {
|
|
352
|
+
try {
|
|
353
|
+
setStage({ type: "thinking" });
|
|
354
|
+
const toolResult = await tool.execute(parsed.input, {
|
|
355
|
+
repoPath,
|
|
356
|
+
messages: currentAll,
|
|
357
|
+
});
|
|
358
|
+
result = toolResult.value;
|
|
359
|
+
if (cacheKey && toolResult.kind === "text") {
|
|
360
|
+
toolResultCache.current.set(cacheKey, result);
|
|
405
361
|
}
|
|
362
|
+
} catch (err: unknown) {
|
|
363
|
+
result = `Error: ${err instanceof Error ? err.message : "failed"}`;
|
|
406
364
|
}
|
|
407
365
|
}
|
|
366
|
+
}
|
|
408
367
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
"generate-pdf": "file-written",
|
|
420
|
-
"write-file": "file-written",
|
|
421
|
-
search: "url-fetched",
|
|
422
|
-
} as const;
|
|
423
|
-
appendMemory({
|
|
424
|
-
kind: kindMap[parsed.kind as keyof typeof kindMap] ?? "shell-run",
|
|
425
|
-
detail:
|
|
426
|
-
parsed.kind === "shell"
|
|
427
|
-
? parsed.command
|
|
428
|
-
: parsed.kind === "fetch"
|
|
429
|
-
? parsed.url
|
|
430
|
-
: parsed.kind === "search"
|
|
431
|
-
? parsed.query
|
|
432
|
-
: parsed.kind === "read-folder"
|
|
433
|
-
? parsed.folderPath
|
|
434
|
-
: parsed.kind === "grep"
|
|
435
|
-
? `${parsed.pattern} ${parsed.glob}`
|
|
436
|
-
: parsed.kind === "delete-file"
|
|
437
|
-
? parsed.filePath
|
|
438
|
-
: parsed.kind === "delete-folder"
|
|
439
|
-
? parsed.folderPath
|
|
440
|
-
: parsed.kind === "open-url"
|
|
441
|
-
? parsed.url
|
|
442
|
-
: parsed.kind === "generate-pdf"
|
|
443
|
-
? parsed.filePath
|
|
444
|
-
: parsed.filePath,
|
|
445
|
-
summary: result.split("\n")[0]?.slice(0, 120) ?? "",
|
|
446
|
-
repoPath,
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
const toolName =
|
|
451
|
-
parsed.kind === "shell"
|
|
452
|
-
? "shell"
|
|
453
|
-
: parsed.kind === "fetch"
|
|
454
|
-
? "fetch"
|
|
455
|
-
: parsed.kind === "read-file"
|
|
456
|
-
? "read-file"
|
|
457
|
-
: parsed.kind === "read-folder"
|
|
458
|
-
? "read-folder"
|
|
459
|
-
: parsed.kind === "grep"
|
|
460
|
-
? "grep"
|
|
461
|
-
: parsed.kind === "delete-file"
|
|
462
|
-
? "delete-file"
|
|
463
|
-
: parsed.kind === "delete-folder"
|
|
464
|
-
? "delete-folder"
|
|
465
|
-
: parsed.kind === "open-url"
|
|
466
|
-
? "open-url"
|
|
467
|
-
: parsed.kind === "generate-pdf"
|
|
468
|
-
? "generate-pdf"
|
|
469
|
-
: parsed.kind === "search"
|
|
470
|
-
? "search"
|
|
471
|
-
: "write-file";
|
|
472
|
-
|
|
473
|
-
const toolContent =
|
|
474
|
-
parsed.kind === "shell"
|
|
475
|
-
? parsed.command
|
|
476
|
-
: parsed.kind === "fetch"
|
|
477
|
-
? parsed.url
|
|
478
|
-
: parsed.kind === "search"
|
|
479
|
-
? parsed.query
|
|
480
|
-
: parsed.kind === "read-folder"
|
|
481
|
-
? parsed.folderPath
|
|
482
|
-
: parsed.kind === "grep"
|
|
483
|
-
? `${parsed.pattern} — ${parsed.glob}`
|
|
484
|
-
: parsed.kind === "delete-file"
|
|
485
|
-
? parsed.filePath
|
|
486
|
-
: parsed.kind === "delete-folder"
|
|
487
|
-
? parsed.folderPath
|
|
488
|
-
: parsed.kind === "open-url"
|
|
489
|
-
? parsed.url
|
|
490
|
-
: parsed.kind === "generate-pdf"
|
|
491
|
-
? parsed.filePath
|
|
492
|
-
: parsed.filePath;
|
|
493
|
-
|
|
494
|
-
const toolMsg: Message = {
|
|
495
|
-
role: "assistant",
|
|
496
|
-
type: "tool",
|
|
497
|
-
toolName,
|
|
498
|
-
content: toolContent,
|
|
499
|
-
result,
|
|
500
|
-
approved,
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
const withTool = [...currentAll, toolMsg];
|
|
504
|
-
setAllMessages(withTool);
|
|
505
|
-
setCommitted((prev) => [...prev, toolMsg]);
|
|
368
|
+
if (approved && !result.startsWith("Error:")) {
|
|
369
|
+
appendMemory({
|
|
370
|
+
kind: "shell-run",
|
|
371
|
+
detail: tool.summariseInput
|
|
372
|
+
? String(tool.summariseInput(parsed.input))
|
|
373
|
+
: parsed.rawInput,
|
|
374
|
+
summary: result.split("\n")[0]?.slice(0, 120) ?? "",
|
|
375
|
+
repoPath,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
506
378
|
|
|
507
|
-
|
|
508
|
-
|
|
379
|
+
const displayContent = tool.summariseInput
|
|
380
|
+
? String(tool.summariseInput(parsed.input))
|
|
381
|
+
: parsed.rawInput;
|
|
509
382
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
383
|
+
const toolMsg: Message = {
|
|
384
|
+
role: "assistant",
|
|
385
|
+
type: "tool",
|
|
386
|
+
toolName: parsed.toolName as any,
|
|
387
|
+
content: displayContent,
|
|
388
|
+
result,
|
|
389
|
+
approved,
|
|
514
390
|
};
|
|
515
391
|
|
|
516
|
-
|
|
517
|
-
|
|
392
|
+
const withTool = [...currentAll, toolMsg];
|
|
393
|
+
setAllMessages(withTool);
|
|
394
|
+
setCommitted((prev) => [...prev, toolMsg]);
|
|
395
|
+
|
|
396
|
+
// Chain: process remainder immediately, no API round-trip needed.
|
|
397
|
+
if (approved && remainder && remainder.length > 0) {
|
|
398
|
+
processResponse(remainder, withTool, signal);
|
|
518
399
|
return;
|
|
519
400
|
}
|
|
520
401
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
tool,
|
|
524
|
-
pendingMessages: currentAll,
|
|
525
|
-
resolve: executeAndContinue,
|
|
526
|
-
});
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
402
|
+
// Chain ended (or was never chained) — clear batch approval.
|
|
403
|
+
batchApprovedRef.current = false;
|
|
529
404
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
repoUrl: parsed.repoUrl,
|
|
543
|
-
launchAnalysis: true,
|
|
544
|
-
});
|
|
405
|
+
const nextAbort = new AbortController();
|
|
406
|
+
abortControllerRef.current = nextAbort;
|
|
407
|
+
setStage({ type: "thinking" });
|
|
408
|
+
callChat(provider!, systemPrompt, withTool, nextAbort.signal)
|
|
409
|
+
.then((r: string) => processResponse(r, withTool, nextAbort.signal))
|
|
410
|
+
.catch(handleError(withTool));
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// Auto-approve if: tool is safe, or global auto-approve is on, or we're
|
|
414
|
+
// already inside a user-approved batch chain.
|
|
415
|
+
if ((autoApprove && isSafe) || batchApprovedRef.current) {
|
|
416
|
+
executeAndContinue(true);
|
|
545
417
|
return;
|
|
546
418
|
}
|
|
547
419
|
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
if (githubUrl && !clonedUrls.has(githubUrl)) {
|
|
565
|
-
setTimeout(() => {
|
|
566
|
-
setStage({ type: "clone-offer", repoUrl: githubUrl });
|
|
567
|
-
}, 80);
|
|
568
|
-
} else {
|
|
569
|
-
setStage({ type: "idle" });
|
|
570
|
-
}
|
|
420
|
+
const permLabel = tool.permissionLabel ?? tool.name;
|
|
421
|
+
const permValue = tool.summariseInput
|
|
422
|
+
? String(tool.summariseInput(parsed.input))
|
|
423
|
+
: parsed.rawInput;
|
|
424
|
+
|
|
425
|
+
setStage({
|
|
426
|
+
type: "permission",
|
|
427
|
+
tool: {
|
|
428
|
+
type: parsed.toolName as any,
|
|
429
|
+
_display: permValue,
|
|
430
|
+
_label: permLabel,
|
|
431
|
+
} as any,
|
|
432
|
+
pendingMessages: currentAll,
|
|
433
|
+
resolve: executeAndContinue,
|
|
434
|
+
});
|
|
571
435
|
};
|
|
572
436
|
|
|
573
437
|
const sendMessage = (text: string) => {
|
|
@@ -577,7 +441,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
577
441
|
setShowTimeline(true);
|
|
578
442
|
return;
|
|
579
443
|
}
|
|
580
|
-
|
|
581
444
|
if (text.trim().toLowerCase() === "/review") {
|
|
582
445
|
setShowReview(true);
|
|
583
446
|
return;
|
|
@@ -589,7 +452,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
589
452
|
const msg: Message = {
|
|
590
453
|
role: "assistant",
|
|
591
454
|
content: next
|
|
592
|
-
? "Auto-approve ON — read, search,
|
|
455
|
+
? "Auto-approve ON — safe tools (read, search, fetch) will run without asking."
|
|
593
456
|
: "Auto-approve OFF — all tools will ask for permission.",
|
|
594
457
|
type: "text",
|
|
595
458
|
};
|
|
@@ -600,17 +463,16 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
600
463
|
|
|
601
464
|
if (text.trim().toLowerCase() === "/clear history") {
|
|
602
465
|
clearRepoMemory(repoPath);
|
|
603
|
-
const
|
|
466
|
+
const msg: Message = {
|
|
604
467
|
role: "assistant",
|
|
605
468
|
content: "History cleared for this repo.",
|
|
606
469
|
type: "text",
|
|
607
470
|
};
|
|
608
|
-
setCommitted((prev) => [...prev,
|
|
609
|
-
setAllMessages((prev) => [...prev,
|
|
471
|
+
setCommitted((prev) => [...prev, msg]);
|
|
472
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
610
473
|
return;
|
|
611
474
|
}
|
|
612
475
|
|
|
613
|
-
// bare /chat — show usage
|
|
614
476
|
if (text.trim().toLowerCase() === "/chat") {
|
|
615
477
|
const msg: Message = {
|
|
616
478
|
role: "assistant",
|
|
@@ -623,7 +485,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
623
485
|
return;
|
|
624
486
|
}
|
|
625
487
|
|
|
626
|
-
// /chat rename <newname>
|
|
627
488
|
if (text.trim().toLowerCase().startsWith("/chat rename")) {
|
|
628
489
|
const parts = text.trim().split(/\s+/);
|
|
629
490
|
const newName = parts.slice(2).join("-");
|
|
@@ -657,14 +518,13 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
657
518
|
return;
|
|
658
519
|
}
|
|
659
520
|
|
|
660
|
-
// /chat delete <name>
|
|
661
521
|
if (text.trim().toLowerCase().startsWith("/chat delete")) {
|
|
662
522
|
const parts = text.trim().split(/\s+/);
|
|
663
523
|
const name = parts.slice(2).join("-");
|
|
664
524
|
if (!name) {
|
|
665
525
|
const msg: Message = {
|
|
666
526
|
role: "assistant",
|
|
667
|
-
content: "Usage: `/chat delete <
|
|
527
|
+
content: "Usage: `/chat delete <n>`",
|
|
668
528
|
type: "text",
|
|
669
529
|
};
|
|
670
530
|
setCommitted((prev) => [...prev, msg]);
|
|
@@ -682,7 +542,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
682
542
|
setAllMessages((prev) => [...prev, msg]);
|
|
683
543
|
return;
|
|
684
544
|
}
|
|
685
|
-
// If deleting the current chat, clear the name so it gets re-named on next message
|
|
686
545
|
if (chatNameRef.current === name) {
|
|
687
546
|
chatNameRef.current = null;
|
|
688
547
|
setChatName(null);
|
|
@@ -698,7 +557,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
698
557
|
return;
|
|
699
558
|
}
|
|
700
559
|
|
|
701
|
-
// /chat list
|
|
702
560
|
if (text.trim().toLowerCase() === "/chat list") {
|
|
703
561
|
const chats = listChats(repoPath);
|
|
704
562
|
const content =
|
|
@@ -716,7 +574,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
716
574
|
return;
|
|
717
575
|
}
|
|
718
576
|
|
|
719
|
-
// /chat load <n>
|
|
720
577
|
if (text.trim().toLowerCase().startsWith("/chat load")) {
|
|
721
578
|
const parts = text.trim().split(/\s+/);
|
|
722
579
|
const name = parts.slice(2).join("-");
|
|
@@ -758,7 +615,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
758
615
|
return;
|
|
759
616
|
}
|
|
760
617
|
|
|
761
|
-
// /memory list
|
|
762
618
|
if (
|
|
763
619
|
text.trim().toLowerCase() === "/memory list" ||
|
|
764
620
|
text.trim().toLowerCase() === "/memory"
|
|
@@ -767,14 +623,15 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
767
623
|
const content =
|
|
768
624
|
mems.length === 0
|
|
769
625
|
? "No memories stored for this repo yet."
|
|
770
|
-
: `Memories for this repo:\n\n${mems
|
|
626
|
+
: `Memories for this repo:\n\n${mems
|
|
627
|
+
.map((m) => `- [${m.id}] ${m.content}`)
|
|
628
|
+
.join("\n")}`;
|
|
771
629
|
const msg: Message = { role: "assistant", content, type: "text" };
|
|
772
630
|
setCommitted((prev) => [...prev, msg]);
|
|
773
631
|
setAllMessages((prev) => [...prev, msg]);
|
|
774
632
|
return;
|
|
775
633
|
}
|
|
776
634
|
|
|
777
|
-
// /memory add <content>
|
|
778
635
|
if (text.trim().toLowerCase().startsWith("/memory add")) {
|
|
779
636
|
const content = text.trim().slice("/memory add".length).trim();
|
|
780
637
|
if (!content) {
|
|
@@ -798,7 +655,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
798
655
|
return;
|
|
799
656
|
}
|
|
800
657
|
|
|
801
|
-
// /memory delete <id>
|
|
802
658
|
if (text.trim().toLowerCase().startsWith("/memory delete")) {
|
|
803
659
|
const id = text.trim().split(/\s+/)[2];
|
|
804
660
|
if (!id) {
|
|
@@ -824,7 +680,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
824
680
|
return;
|
|
825
681
|
}
|
|
826
682
|
|
|
827
|
-
// /memory clear
|
|
828
683
|
if (text.trim().toLowerCase() === "/memory clear") {
|
|
829
684
|
clearRepoMemory(repoPath);
|
|
830
685
|
const msg: Message = {
|
|
@@ -842,15 +697,14 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
842
697
|
setCommitted((prev) => [...prev, userMsg]);
|
|
843
698
|
setAllMessages(nextAll);
|
|
844
699
|
toolResultCache.current.clear();
|
|
700
|
+
batchApprovedRef.current = false;
|
|
845
701
|
|
|
846
|
-
// Track input history for up/down navigation
|
|
847
702
|
inputHistoryRef.current = [
|
|
848
703
|
text,
|
|
849
704
|
...inputHistoryRef.current.filter((m) => m !== text),
|
|
850
705
|
].slice(0, 50);
|
|
851
706
|
historyIndexRef.current = -1;
|
|
852
707
|
|
|
853
|
-
// Auto-name chat on first user message
|
|
854
708
|
if (!chatName) {
|
|
855
709
|
const name =
|
|
856
710
|
getChatNameSuggestions(nextAll)[0] ??
|
|
@@ -877,6 +731,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
877
731
|
if (stage.type === "thinking" && key.escape) {
|
|
878
732
|
abortControllerRef.current?.abort();
|
|
879
733
|
abortControllerRef.current = null;
|
|
734
|
+
batchApprovedRef.current = false;
|
|
880
735
|
setStage({ type: "idle" });
|
|
881
736
|
return;
|
|
882
737
|
}
|
|
@@ -886,7 +741,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
886
741
|
process.exit(0);
|
|
887
742
|
return;
|
|
888
743
|
}
|
|
889
|
-
|
|
890
744
|
if (key.upArrow && inputHistoryRef.current.length > 0) {
|
|
891
745
|
const next = Math.min(
|
|
892
746
|
historyIndexRef.current + 1,
|
|
@@ -897,16 +751,13 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
897
751
|
setInputKey((k) => k + 1);
|
|
898
752
|
return;
|
|
899
753
|
}
|
|
900
|
-
|
|
901
754
|
if (key.downArrow) {
|
|
902
755
|
const next = historyIndexRef.current - 1;
|
|
903
756
|
historyIndexRef.current = next;
|
|
904
|
-
|
|
905
|
-
setInputValue(val);
|
|
757
|
+
setInputValue(next < 0 ? "" : inputHistoryRef.current[next]!);
|
|
906
758
|
setInputKey((k) => k + 1);
|
|
907
759
|
return;
|
|
908
760
|
}
|
|
909
|
-
|
|
910
761
|
if (key.tab && inputValue.startsWith("/")) {
|
|
911
762
|
const q = inputValue.toLowerCase();
|
|
912
763
|
const match = COMMANDS.find((c) => c.cmd.startsWith(q));
|
|
@@ -971,37 +822,36 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
971
822
|
if (stage.type === "clone-exists") {
|
|
972
823
|
if (input === "y" || input === "Y") {
|
|
973
824
|
const { repoUrl, repoPath: existingPath } = stage;
|
|
974
|
-
const cloneUrl = toCloneUrl(repoUrl);
|
|
975
825
|
setStage({ type: "cloning", repoUrl });
|
|
976
|
-
startCloneRepo(
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
826
|
+
startCloneRepo(toCloneUrl(repoUrl), { forceReclone: true }).then(
|
|
827
|
+
(result) => {
|
|
828
|
+
if (result.done) {
|
|
829
|
+
setStage({
|
|
830
|
+
type: "clone-done",
|
|
831
|
+
repoUrl,
|
|
832
|
+
destPath: existingPath,
|
|
833
|
+
fileCount: walkDir(existingPath).length,
|
|
834
|
+
});
|
|
835
|
+
} else {
|
|
836
|
+
setStage({
|
|
837
|
+
type: "clone-error",
|
|
838
|
+
message:
|
|
839
|
+
!result.folderExists && result.error
|
|
840
|
+
? result.error
|
|
841
|
+
: "Clone failed",
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
);
|
|
995
846
|
return;
|
|
996
847
|
}
|
|
997
848
|
if (input === "n" || input === "N") {
|
|
998
849
|
const { repoUrl, repoPath: existingPath } = stage;
|
|
999
|
-
const fileCount = walkDir(existingPath).length;
|
|
1000
850
|
setStage({
|
|
1001
851
|
type: "clone-done",
|
|
1002
852
|
repoUrl,
|
|
1003
853
|
destPath: existingPath,
|
|
1004
|
-
fileCount,
|
|
854
|
+
fileCount: walkDir(existingPath).length,
|
|
1005
855
|
});
|
|
1006
856
|
return;
|
|
1007
857
|
}
|
|
@@ -1022,7 +872,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
1022
872
|
type: "tool",
|
|
1023
873
|
toolName: "fetch",
|
|
1024
874
|
content: stage.repoUrl,
|
|
1025
|
-
result: `Clone complete. Repo: ${repoName}. Local path: ${stage.destPath}. ${stage.fileCount} files
|
|
875
|
+
result: `Clone complete. Repo: ${repoName}. Local path: ${stage.destPath}. ${stage.fileCount} files.`,
|
|
1026
876
|
approved: true,
|
|
1027
877
|
};
|
|
1028
878
|
const withClone = [...allMessages, contextMsg, summaryMsg];
|
|
@@ -1044,6 +894,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
1044
894
|
return;
|
|
1045
895
|
}
|
|
1046
896
|
if (input === "n" || input === "N" || key.escape) {
|
|
897
|
+
batchApprovedRef.current = false;
|
|
1047
898
|
stage.resolve(false);
|
|
1048
899
|
return;
|
|
1049
900
|
}
|
|
@@ -1138,26 +989,16 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
1138
989
|
const historySummary = buildMemorySummary(repoPath);
|
|
1139
990
|
const lensFile = readLensFile(repoPath);
|
|
1140
991
|
const lensContext = lensFile
|
|
1141
|
-
? `
|
|
1142
|
-
|
|
1143
|
-
## LENS.md (previous analysis)
|
|
1144
|
-
${lensFile.overview}
|
|
1145
|
-
|
|
1146
|
-
Important folders: ${lensFile.importantFolders.join(", ")}
|
|
1147
|
-
Suggestions: ${lensFile.suggestions.slice(0, 3).join("; ")}`
|
|
992
|
+
? `\n\n## LENS.md (previous analysis)\n${lensFile.overview}\n\nImportant folders: ${lensFile.importantFolders.join(", ")}\nSuggestions: ${lensFile.suggestions.slice(0, 3).join("; ")}`
|
|
1148
993
|
: "";
|
|
994
|
+
const toolsSection = registry.buildSystemPromptSection();
|
|
1149
995
|
setSystemPrompt(
|
|
1150
|
-
buildSystemPrompt(importantFiles, historySummary) +
|
|
996
|
+
buildSystemPrompt(importantFiles, historySummary, toolsSection) +
|
|
997
|
+
lensContext,
|
|
1151
998
|
);
|
|
1152
|
-
const historyNote = historySummary
|
|
1153
|
-
? "\n\nI have memory of previous actions in this repo."
|
|
1154
|
-
: "";
|
|
1155
|
-
const lensGreetNote = lensFile
|
|
1156
|
-
? "\n\nFound LENS.md — I have context from a previous analysis of this repo."
|
|
1157
|
-
: "";
|
|
1158
999
|
const greeting: Message = {
|
|
1159
1000
|
role: "assistant",
|
|
1160
|
-
content: `Welcome to Lens
|
|
1001
|
+
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.`,
|
|
1161
1002
|
type: "text",
|
|
1162
1003
|
};
|
|
1163
1004
|
setCommitted([greeting]);
|
|
@@ -1169,8 +1010,7 @@ Suggestions: ${lensFile.suggestions.slice(0, 3).join("; ")}`
|
|
|
1169
1010
|
|
|
1170
1011
|
if (stage.type === "picking-provider")
|
|
1171
1012
|
return <ProviderPicker onDone={handleProviderDone} />;
|
|
1172
|
-
|
|
1173
|
-
if (stage.type === "loading") {
|
|
1013
|
+
if (stage.type === "loading")
|
|
1174
1014
|
return (
|
|
1175
1015
|
<Box gap={1} marginTop={1}>
|
|
1176
1016
|
<Text color={ACCENT}>*</Text>
|
|
@@ -1182,23 +1022,17 @@ Suggestions: ${lensFile.suggestions.slice(0, 3).join("; ")}`
|
|
|
1182
1022
|
</Text>
|
|
1183
1023
|
</Box>
|
|
1184
1024
|
);
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
if (showTimeline) {
|
|
1025
|
+
if (showTimeline)
|
|
1188
1026
|
return (
|
|
1189
1027
|
<TimelineRunner
|
|
1190
1028
|
repoPath={repoPath}
|
|
1191
1029
|
onExit={() => setShowTimeline(false)}
|
|
1192
1030
|
/>
|
|
1193
1031
|
);
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
if (showReview) {
|
|
1032
|
+
if (showReview)
|
|
1197
1033
|
return (
|
|
1198
1034
|
<ReviewCommand path={repoPath} onExit={() => setShowReview(false)} />
|
|
1199
1035
|
);
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
1036
|
if (stage.type === "clone-offer")
|
|
1203
1037
|
return <CloneOfferView stage={stage} committed={committed} />;
|
|
1204
1038
|
if (stage.type === "cloning")
|