@ridit/lens 0.3.3 → 0.3.5
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/LENS.md +48 -0
- package/README.md +16 -14
- package/dist/index.mjs +4741 -4367
- package/package.json +1 -2
- package/src/commands/{watch.tsx → run.tsx} +3 -3
- package/src/components/chat/ChatRunner.tsx +131 -877
- package/src/components/chat/hooks/useChat.ts +531 -0
- package/src/components/chat/hooks/useChatInput.ts +79 -0
- package/src/components/chat/hooks/useCommandHandlers.ts +327 -0
- package/src/components/repo/LensFileMenu.tsx +2 -9
- package/src/components/repo/RepoAnalysis.tsx +241 -50
- package/src/components/watch/{WatchRunner.tsx → RunRunner.tsx} +1 -2
- package/src/index.tsx +12 -17
- package/src/tools/chart.ts +0 -8
- package/src/types/repo.ts +15 -3
- package/src/utils/ai.ts +108 -20
- package/src/utils/lensfile.ts +83 -18
|
@@ -1,34 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Box, Text, Static, useInput } from "ink";
|
|
3
3
|
import Spinner from "ink-spinner";
|
|
4
|
-
import { useState
|
|
4
|
+
import { useState } from "react";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import os from "os";
|
|
7
7
|
import TextInput from "ink-text-input";
|
|
8
8
|
import { ACCENT } from "../../colors";
|
|
9
|
-
import { buildDiffs } from "../repo/DiffViewer";
|
|
10
9
|
import { ProviderPicker } from "../provider/ProviderPicker";
|
|
11
|
-
import { fetchFileTree, readImportantFiles } from "../../utils/files";
|
|
12
10
|
import { startCloneRepo } from "../../utils/repo";
|
|
13
11
|
import { useThinkingPhrase } from "../../utils/thinking";
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
applyPatches,
|
|
18
|
-
extractGithubUrl,
|
|
19
|
-
toCloneUrl,
|
|
20
|
-
parseCloneTag,
|
|
21
|
-
buildSystemPrompt,
|
|
22
|
-
parseResponse,
|
|
23
|
-
callChat,
|
|
24
|
-
} from "../../utils/chat";
|
|
25
|
-
import {
|
|
26
|
-
saveChat,
|
|
27
|
-
loadChat,
|
|
28
|
-
listChats,
|
|
29
|
-
deleteChat,
|
|
30
|
-
getChatNameSuggestions,
|
|
31
|
-
} from "../../utils/chatHistory";
|
|
12
|
+
import { walkDir, applyPatches, toCloneUrl } from "../../utils/chat";
|
|
13
|
+
import { appendMemory } from "../../utils/memory";
|
|
14
|
+
import { getChatNameSuggestions, saveChat } from "../../utils/chatHistory";
|
|
32
15
|
import { StaticMessage } from "./ChatMessage";
|
|
33
16
|
import {
|
|
34
17
|
PermissionPrompt,
|
|
@@ -44,48 +27,17 @@ import {
|
|
|
44
27
|
ViewingFileView,
|
|
45
28
|
} from "./ChatOverlays";
|
|
46
29
|
import { TimelineRunner } from "../timeline/TimelineRunner";
|
|
47
|
-
import type { Provider } from "../../types/config";
|
|
48
|
-
import type { Message, ChatStage } from "../../types/chat";
|
|
49
|
-
import {
|
|
50
|
-
appendMemory,
|
|
51
|
-
buildMemorySummary,
|
|
52
|
-
clearRepoMemory,
|
|
53
|
-
addMemory,
|
|
54
|
-
deleteMemory,
|
|
55
|
-
listMemories,
|
|
56
|
-
} from "../../utils/memory";
|
|
57
|
-
import { readLensFile } from "../../utils/lensfile";
|
|
58
30
|
import { ReviewCommand } from "../../commands/review";
|
|
59
|
-
import {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
{ cmd: "/clear history", desc: "wipe session memory for this repo" },
|
|
64
|
-
{ cmd: "/review", desc: "review current codebase" },
|
|
65
|
-
{ cmd: "/auto", desc: "toggle auto-approve for read/search tools" },
|
|
66
|
-
{
|
|
67
|
-
cmd: "/auto --force-all",
|
|
68
|
-
desc: "auto-approve ALL tools including shell and writes (⚠ dangerous)",
|
|
69
|
-
},
|
|
70
|
-
{ cmd: "/chat", desc: "chat history commands" },
|
|
71
|
-
{ cmd: "/chat list", desc: "list saved chats for this repo" },
|
|
72
|
-
{ cmd: "/chat load", desc: "load a saved chat by name" },
|
|
73
|
-
{ cmd: "/chat rename", desc: "rename the current chat" },
|
|
74
|
-
{ cmd: "/chat delete", desc: "delete a saved chat by name" },
|
|
75
|
-
{ cmd: "/memory", desc: "memory commands" },
|
|
76
|
-
{ cmd: "/memory list", desc: "list all memories for this repo" },
|
|
77
|
-
{ cmd: "/memory add", desc: "add a memory" },
|
|
78
|
-
{ cmd: "/memory delete", desc: "delete a memory by id" },
|
|
79
|
-
{ cmd: "/memory clear", desc: "clear all memories for this repo" },
|
|
80
|
-
];
|
|
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";
|
|
81
35
|
|
|
82
36
|
function CommandPalette({
|
|
83
37
|
query,
|
|
84
|
-
onSelect,
|
|
85
38
|
recentChats,
|
|
86
39
|
}: {
|
|
87
40
|
query: string;
|
|
88
|
-
onSelect: (cmd: string) => void;
|
|
89
41
|
recentChats: string[];
|
|
90
42
|
}) {
|
|
91
43
|
const q = query.toLowerCase();
|
|
@@ -139,7 +91,6 @@ function ForceAllWarning({
|
|
|
139
91
|
onConfirm: (confirmed: boolean) => void;
|
|
140
92
|
}) {
|
|
141
93
|
const [input, setInput] = useState("");
|
|
142
|
-
|
|
143
94
|
return (
|
|
144
95
|
<Box flexDirection="column" marginY={1} gap={1}>
|
|
145
96
|
<Box gap={1}>
|
|
@@ -197,713 +148,14 @@ function ForceAllWarning({
|
|
|
197
148
|
}
|
|
198
149
|
|
|
199
150
|
export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
const [provider, setProvider] = useState<Provider | null>(null);
|
|
203
|
-
const [systemPrompt, setSystemPrompt] = useState("");
|
|
204
|
-
const [inputValue, setInputValue] = useState("");
|
|
205
|
-
const [pendingMsgIndex, setPendingMsgIndex] = useState<number | null>(null);
|
|
206
|
-
const [allMessages, setAllMessages] = useState<Message[]>([]);
|
|
207
|
-
const [clonedUrls, setClonedUrls] = useState<Set<string>>(new Set());
|
|
208
|
-
const [showTimeline, setShowTimeline] = useState(false);
|
|
209
|
-
const [showReview, setShowReview] = useState(false);
|
|
210
|
-
const [autoApprove, setAutoApprove] = useState(false);
|
|
211
|
-
const [forceApprove, setForceApprove] = useState(false);
|
|
212
|
-
const [showForceWarning, setShowForceWarning] = useState(false);
|
|
213
|
-
const [chatName, setChatName] = useState<string | null>(null);
|
|
214
|
-
const chatNameRef = useRef<string | null>(null);
|
|
215
|
-
const [recentChats, setRecentChats] = useState<string[]>([]);
|
|
216
|
-
const inputHistoryRef = useRef<string[]>([]);
|
|
217
|
-
const historyIndexRef = useRef<number>(-1);
|
|
218
|
-
const [inputKey, setInputKey] = useState(0);
|
|
219
|
-
|
|
220
|
-
const updateChatName = (name: string) => {
|
|
221
|
-
chatNameRef.current = name;
|
|
222
|
-
setChatName(name);
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
const abortControllerRef = useRef<AbortController | null>(null);
|
|
226
|
-
const toolResultCache = useRef<Map<string, string>>(new Map());
|
|
227
|
-
const batchApprovedRef = useRef(false);
|
|
228
|
-
|
|
229
|
-
const thinkingPhrase = useThinkingPhrase(stage.type === "thinking");
|
|
230
|
-
|
|
231
|
-
React.useEffect(() => {
|
|
232
|
-
const chats = listChats(repoPath);
|
|
233
|
-
setRecentChats(chats.slice(0, 10).map((c) => c.name));
|
|
234
|
-
}, [repoPath]);
|
|
235
|
-
|
|
236
|
-
React.useEffect(() => {
|
|
237
|
-
if (chatNameRef.current && allMessages.length > 1) {
|
|
238
|
-
saveChat(chatNameRef.current, repoPath, allMessages);
|
|
239
|
-
}
|
|
240
|
-
}, [allMessages]);
|
|
241
|
-
|
|
242
|
-
const handleError = (currentAll: Message[]) => (err: unknown) => {
|
|
243
|
-
batchApprovedRef.current = false;
|
|
244
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
245
|
-
setStage({ type: "idle" });
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
const errMsg: Message = {
|
|
249
|
-
role: "assistant",
|
|
250
|
-
content: `Error: ${err instanceof Error ? err.message : "Something went wrong"}`,
|
|
251
|
-
type: "text",
|
|
252
|
-
};
|
|
253
|
-
setAllMessages([...currentAll, errMsg]);
|
|
254
|
-
setCommitted((prev) => [...prev, errMsg]);
|
|
255
|
-
setStage({ type: "idle" });
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
const TOOL_TAG_NAMES = [
|
|
259
|
-
"shell",
|
|
260
|
-
"fetch",
|
|
261
|
-
"read-file",
|
|
262
|
-
"read-folder",
|
|
263
|
-
"grep",
|
|
264
|
-
"write-file",
|
|
265
|
-
"delete-file",
|
|
266
|
-
"delete-folder",
|
|
267
|
-
"open-url",
|
|
268
|
-
"generate-pdf",
|
|
269
|
-
"search",
|
|
270
|
-
"clone",
|
|
271
|
-
"changes",
|
|
272
|
-
];
|
|
273
|
-
|
|
274
|
-
function isLikelyTruncated(text: string): boolean {
|
|
275
|
-
return TOOL_TAG_NAMES.some(
|
|
276
|
-
(tag) => text.includes(`<${tag}>`) && !text.includes(`</${tag}>`),
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const processResponse = (
|
|
281
|
-
raw: string,
|
|
282
|
-
currentAll: Message[],
|
|
283
|
-
signal: AbortSignal,
|
|
284
|
-
) => {
|
|
285
|
-
if (signal.aborted) {
|
|
286
|
-
batchApprovedRef.current = false;
|
|
287
|
-
setStage({ type: "idle" });
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Guard: response cut off mid-tool-tag (context limit hit during generation)
|
|
292
|
-
if (isLikelyTruncated(raw)) {
|
|
293
|
-
const truncMsg: Message = {
|
|
294
|
-
role: "assistant",
|
|
295
|
-
content:
|
|
296
|
-
"(response cut off — the model hit its output limit mid-tool-call. Try asking it to continue, or simplify the request.)",
|
|
297
|
-
type: "text",
|
|
298
|
-
};
|
|
299
|
-
setAllMessages([...currentAll, truncMsg]);
|
|
300
|
-
setCommitted((prev) => [...prev, truncMsg]);
|
|
301
|
-
setStage({ type: "idle" });
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const memAddMatches = [
|
|
306
|
-
...raw.matchAll(/<memory-add>([\s\S]*?)<\/memory-add>/g),
|
|
307
|
-
];
|
|
308
|
-
const memDelMatches = [
|
|
309
|
-
...raw.matchAll(/<memory-delete>([\s\S]*?)<\/memory-delete>/g),
|
|
310
|
-
];
|
|
311
|
-
for (const match of memAddMatches) {
|
|
312
|
-
const content = match[1]!.trim();
|
|
313
|
-
if (content) addMemory(content, repoPath);
|
|
314
|
-
}
|
|
315
|
-
for (const match of memDelMatches) {
|
|
316
|
-
const id = match[1]!.trim();
|
|
317
|
-
if (id) deleteMemory(id, repoPath);
|
|
318
|
-
}
|
|
319
|
-
const cleanRaw = raw
|
|
320
|
-
.replace(/<memory-add>[\s\S]*?<\/memory-add>/g, "")
|
|
321
|
-
.replace(/<memory-delete>[\s\S]*?<\/memory-delete>/g, "")
|
|
322
|
-
.trim();
|
|
323
|
-
|
|
324
|
-
const parsed = parseResponse(cleanRaw);
|
|
325
|
-
|
|
326
|
-
if (parsed.kind === "changes") {
|
|
327
|
-
batchApprovedRef.current = false;
|
|
328
|
-
if (parsed.patches.length === 0) {
|
|
329
|
-
const msg: Message = {
|
|
330
|
-
role: "assistant",
|
|
331
|
-
content: parsed.content,
|
|
332
|
-
type: "text",
|
|
333
|
-
};
|
|
334
|
-
setAllMessages([...currentAll, msg]);
|
|
335
|
-
setCommitted((prev) => [...prev, msg]);
|
|
336
|
-
setStage({ type: "idle" });
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
const assistantMsg: Message = {
|
|
340
|
-
role: "assistant",
|
|
341
|
-
content: parsed.content,
|
|
342
|
-
type: "plan",
|
|
343
|
-
patches: parsed.patches,
|
|
344
|
-
applied: false,
|
|
345
|
-
};
|
|
346
|
-
const withAssistant = [...currentAll, assistantMsg];
|
|
347
|
-
setAllMessages(withAssistant);
|
|
348
|
-
setPendingMsgIndex(withAssistant.length - 1);
|
|
349
|
-
const diffLines = buildDiffs(repoPath, parsed.patches);
|
|
350
|
-
setStage({
|
|
351
|
-
type: "preview",
|
|
352
|
-
patches: parsed.patches,
|
|
353
|
-
diffLines,
|
|
354
|
-
scrollOffset: 0,
|
|
355
|
-
pendingMessages: currentAll,
|
|
356
|
-
});
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (parsed.kind === "clone") {
|
|
361
|
-
batchApprovedRef.current = false;
|
|
362
|
-
if (parsed.content) {
|
|
363
|
-
const preambleMsg: Message = {
|
|
364
|
-
role: "assistant",
|
|
365
|
-
content: parsed.content,
|
|
366
|
-
type: "text",
|
|
367
|
-
};
|
|
368
|
-
setAllMessages([...currentAll, preambleMsg]);
|
|
369
|
-
setCommitted((prev) => [...prev, preambleMsg]);
|
|
370
|
-
}
|
|
371
|
-
setStage({
|
|
372
|
-
type: "clone-offer",
|
|
373
|
-
repoUrl: parsed.repoUrl,
|
|
374
|
-
launchAnalysis: true,
|
|
375
|
-
});
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (parsed.kind === "text") {
|
|
380
|
-
batchApprovedRef.current = false;
|
|
381
|
-
|
|
382
|
-
if (!parsed.content.trim()) {
|
|
383
|
-
const stallMsg: Message = {
|
|
384
|
-
role: "assistant",
|
|
385
|
-
content:
|
|
386
|
-
'(no response — the model may have stalled. Try sending a short follow-up like "continue" or start a new message.)',
|
|
387
|
-
type: "text",
|
|
388
|
-
};
|
|
389
|
-
setAllMessages([...currentAll, stallMsg]);
|
|
390
|
-
setCommitted((prev) => [...prev, stallMsg]);
|
|
391
|
-
setStage({ type: "idle" });
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const msg: Message = {
|
|
396
|
-
role: "assistant",
|
|
397
|
-
content: parsed.content,
|
|
398
|
-
type: "text",
|
|
399
|
-
};
|
|
400
|
-
const withMsg = [...currentAll, msg];
|
|
401
|
-
setAllMessages(withMsg);
|
|
402
|
-
setCommitted((prev) => [...prev, msg]);
|
|
403
|
-
const lastUserMsg = [...currentAll]
|
|
404
|
-
.reverse()
|
|
405
|
-
.find((m) => m.role === "user");
|
|
406
|
-
const githubUrl = lastUserMsg
|
|
407
|
-
? extractGithubUrl(lastUserMsg.content)
|
|
408
|
-
: null;
|
|
409
|
-
if (githubUrl && !clonedUrls.has(githubUrl)) {
|
|
410
|
-
setTimeout(
|
|
411
|
-
() => setStage({ type: "clone-offer", repoUrl: githubUrl }),
|
|
412
|
-
80,
|
|
413
|
-
);
|
|
414
|
-
} else {
|
|
415
|
-
setStage({ type: "idle" });
|
|
416
|
-
}
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const tool = registry.get(parsed.toolName);
|
|
421
|
-
if (!tool) {
|
|
422
|
-
batchApprovedRef.current = false;
|
|
423
|
-
setStage({ type: "idle" });
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
if (parsed.content) {
|
|
428
|
-
const preambleMsg: Message = {
|
|
429
|
-
role: "assistant",
|
|
430
|
-
content: parsed.content,
|
|
431
|
-
type: "text",
|
|
432
|
-
};
|
|
433
|
-
setAllMessages([...currentAll, preambleMsg]);
|
|
434
|
-
setCommitted((prev) => [...prev, preambleMsg]);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const remainder = parsed.remainder;
|
|
438
|
-
const isSafe = tool.safe ?? false;
|
|
439
|
-
|
|
440
|
-
const executeAndContinue = async (approved: boolean) => {
|
|
441
|
-
if (approved && remainder) {
|
|
442
|
-
batchApprovedRef.current = true;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
let result = "(denied by user)";
|
|
446
|
-
|
|
447
|
-
if (approved) {
|
|
448
|
-
const cacheKey = isSafe
|
|
449
|
-
? `${parsed.toolName}:${parsed.rawInput}`
|
|
450
|
-
: null;
|
|
451
|
-
if (cacheKey && toolResultCache.current.has(cacheKey)) {
|
|
452
|
-
result =
|
|
453
|
-
toolResultCache.current.get(cacheKey)! +
|
|
454
|
-
"\n\n[NOTE: This result was already retrieved earlier. Do not request it again.]";
|
|
455
|
-
} else {
|
|
456
|
-
try {
|
|
457
|
-
setStage({ type: "thinking" });
|
|
458
|
-
const toolResult = await tool.execute(parsed.input, {
|
|
459
|
-
repoPath,
|
|
460
|
-
messages: currentAll,
|
|
461
|
-
});
|
|
462
|
-
result = toolResult.value;
|
|
463
|
-
if (cacheKey && toolResult.kind === "text") {
|
|
464
|
-
toolResultCache.current.set(cacheKey, result);
|
|
465
|
-
}
|
|
466
|
-
} catch (err: unknown) {
|
|
467
|
-
result = `Error: ${err instanceof Error ? err.message : "failed"}`;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (approved && !result.startsWith("Error:")) {
|
|
473
|
-
appendMemory({
|
|
474
|
-
kind: "shell-run",
|
|
475
|
-
detail: tool.summariseInput
|
|
476
|
-
? String(tool.summariseInput(parsed.input))
|
|
477
|
-
: parsed.rawInput,
|
|
478
|
-
summary: result.split("\n")[0]?.slice(0, 120) ?? "",
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const displayContent = tool.summariseInput
|
|
483
|
-
? String(tool.summariseInput(parsed.input))
|
|
484
|
-
: parsed.rawInput;
|
|
485
|
-
|
|
486
|
-
const toolMsg: Message = {
|
|
487
|
-
role: "assistant",
|
|
488
|
-
type: "tool",
|
|
489
|
-
toolName: parsed.toolName as any,
|
|
490
|
-
content: displayContent,
|
|
491
|
-
result,
|
|
492
|
-
approved,
|
|
493
|
-
};
|
|
494
|
-
|
|
495
|
-
const withTool = [...currentAll, toolMsg];
|
|
496
|
-
setAllMessages(withTool);
|
|
497
|
-
setCommitted((prev) => [...prev, toolMsg]);
|
|
498
|
-
|
|
499
|
-
if (approved && remainder && remainder.length > 0) {
|
|
500
|
-
processResponse(remainder, withTool, signal);
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
batchApprovedRef.current = false;
|
|
505
|
-
|
|
506
|
-
const nextAbort = new AbortController();
|
|
507
|
-
abortControllerRef.current = nextAbort;
|
|
508
|
-
setStage({ type: "thinking" });
|
|
509
|
-
callChat(provider!, systemPrompt, withTool, nextAbort.signal)
|
|
510
|
-
.then((r: string) => processResponse(r, withTool, nextAbort.signal))
|
|
511
|
-
.catch(handleError(withTool));
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
if (forceApprove || (autoApprove && isSafe) || batchApprovedRef.current) {
|
|
515
|
-
executeAndContinue(true);
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const permLabel = tool.permissionLabel ?? tool.name;
|
|
520
|
-
const permValue = tool.summariseInput
|
|
521
|
-
? String(tool.summariseInput(parsed.input))
|
|
522
|
-
: parsed.rawInput;
|
|
523
|
-
|
|
524
|
-
setStage({
|
|
525
|
-
type: "permission",
|
|
526
|
-
tool: {
|
|
527
|
-
type: parsed.toolName as any,
|
|
528
|
-
_display: permValue,
|
|
529
|
-
_label: permLabel,
|
|
530
|
-
} as any,
|
|
531
|
-
pendingMessages: currentAll,
|
|
532
|
-
resolve: executeAndContinue,
|
|
533
|
-
});
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
const sendMessage = (text: string) => {
|
|
537
|
-
if (!provider) return;
|
|
538
|
-
|
|
539
|
-
if (text.trim().toLowerCase() === "/timeline") {
|
|
540
|
-
setShowTimeline(true);
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
if (text.trim().toLowerCase() === "/review") {
|
|
544
|
-
setShowReview(true);
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
// /auto --force-all — show warning first
|
|
549
|
-
if (text.trim().toLowerCase() === "/auto --force-all") {
|
|
550
|
-
if (forceApprove) {
|
|
551
|
-
// Toggle off immediately, no warning needed
|
|
552
|
-
setForceApprove(false);
|
|
553
|
-
setAutoApprove(false);
|
|
554
|
-
const msg: Message = {
|
|
555
|
-
role: "assistant",
|
|
556
|
-
content: "Force-all mode OFF — tools will ask for permission again.",
|
|
557
|
-
type: "text",
|
|
558
|
-
};
|
|
559
|
-
setCommitted((prev) => [...prev, msg]);
|
|
560
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
561
|
-
} else {
|
|
562
|
-
setShowForceWarning(true);
|
|
563
|
-
}
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
if (text.trim().toLowerCase() === "/auto") {
|
|
568
|
-
// /auto never enables force-all, only toggles safe auto-approve
|
|
569
|
-
if (forceApprove) {
|
|
570
|
-
// Step down from force-all to normal auto
|
|
571
|
-
setForceApprove(false);
|
|
572
|
-
setAutoApprove(true);
|
|
573
|
-
const msg: Message = {
|
|
574
|
-
role: "assistant",
|
|
575
|
-
content:
|
|
576
|
-
"Force-all mode OFF — switched to normal auto-approve (safe tools only).",
|
|
577
|
-
type: "text",
|
|
578
|
-
};
|
|
579
|
-
setCommitted((prev) => [...prev, msg]);
|
|
580
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
const next = !autoApprove;
|
|
584
|
-
setAutoApprove(next);
|
|
585
|
-
const msg: Message = {
|
|
586
|
-
role: "assistant",
|
|
587
|
-
content: next
|
|
588
|
-
? "Auto-approve ON — safe tools (read, search, fetch) will run without asking."
|
|
589
|
-
: "Auto-approve OFF — all tools will ask for permission.",
|
|
590
|
-
type: "text",
|
|
591
|
-
};
|
|
592
|
-
setCommitted((prev) => [...prev, msg]);
|
|
593
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
if (text.trim().toLowerCase() === "/clear history") {
|
|
598
|
-
clearRepoMemory(repoPath);
|
|
599
|
-
const msg: Message = {
|
|
600
|
-
role: "assistant",
|
|
601
|
-
content: "History cleared for this repo.",
|
|
602
|
-
type: "text",
|
|
603
|
-
};
|
|
604
|
-
setCommitted((prev) => [...prev, msg]);
|
|
605
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
if (text.trim().toLowerCase() === "/chat") {
|
|
610
|
-
const msg: Message = {
|
|
611
|
-
role: "assistant",
|
|
612
|
-
content:
|
|
613
|
-
"Chat commands: `/chat list` · `/chat load <n>` · `/chat rename <n>` · `/chat delete <n>`",
|
|
614
|
-
type: "text",
|
|
615
|
-
};
|
|
616
|
-
setCommitted((prev) => [...prev, msg]);
|
|
617
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
if (text.trim().toLowerCase().startsWith("/chat rename")) {
|
|
622
|
-
const parts = text.trim().split(/\s+/);
|
|
623
|
-
const newName = parts.slice(2).join("-");
|
|
624
|
-
if (!newName) {
|
|
625
|
-
const msg: Message = {
|
|
626
|
-
role: "assistant",
|
|
627
|
-
content: "Usage: `/chat rename <new-name>`",
|
|
628
|
-
type: "text",
|
|
629
|
-
};
|
|
630
|
-
setCommitted((prev) => [...prev, msg]);
|
|
631
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
const oldName = chatNameRef.current;
|
|
635
|
-
if (oldName) deleteChat(oldName);
|
|
636
|
-
updateChatName(newName);
|
|
637
|
-
saveChat(newName, repoPath, allMessages);
|
|
638
|
-
setRecentChats((prev) =>
|
|
639
|
-
[newName, ...prev.filter((n) => n !== newName && n !== oldName)].slice(
|
|
640
|
-
0,
|
|
641
|
-
10,
|
|
642
|
-
),
|
|
643
|
-
);
|
|
644
|
-
const msg: Message = {
|
|
645
|
-
role: "assistant",
|
|
646
|
-
content: `Chat renamed to **${newName}**.`,
|
|
647
|
-
type: "text",
|
|
648
|
-
};
|
|
649
|
-
setCommitted((prev) => [...prev, msg]);
|
|
650
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
if (text.trim().toLowerCase().startsWith("/chat delete")) {
|
|
655
|
-
const parts = text.trim().split(/\s+/);
|
|
656
|
-
const name = parts.slice(2).join("-");
|
|
657
|
-
if (!name) {
|
|
658
|
-
const msg: Message = {
|
|
659
|
-
role: "assistant",
|
|
660
|
-
content: "Usage: `/chat delete <n>`",
|
|
661
|
-
type: "text",
|
|
662
|
-
};
|
|
663
|
-
setCommitted((prev) => [...prev, msg]);
|
|
664
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
const deleted = deleteChat(name);
|
|
668
|
-
if (!deleted) {
|
|
669
|
-
const msg: Message = {
|
|
670
|
-
role: "assistant",
|
|
671
|
-
content: `Chat **${name}** not found.`,
|
|
672
|
-
type: "text",
|
|
673
|
-
};
|
|
674
|
-
setCommitted((prev) => [...prev, msg]);
|
|
675
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
if (chatNameRef.current === name) {
|
|
679
|
-
chatNameRef.current = null;
|
|
680
|
-
setChatName(null);
|
|
681
|
-
}
|
|
682
|
-
setRecentChats((prev) => prev.filter((n) => n !== name));
|
|
683
|
-
const msg: Message = {
|
|
684
|
-
role: "assistant",
|
|
685
|
-
content: `Chat **${name}** deleted.`,
|
|
686
|
-
type: "text",
|
|
687
|
-
};
|
|
688
|
-
setCommitted((prev) => [...prev, msg]);
|
|
689
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
if (text.trim().toLowerCase() === "/chat list") {
|
|
694
|
-
const chats = listChats(repoPath);
|
|
695
|
-
const content =
|
|
696
|
-
chats.length === 0
|
|
697
|
-
? "No saved chats for this repo yet."
|
|
698
|
-
: `Saved chats:\n\n${chats
|
|
699
|
-
.map(
|
|
700
|
-
(c) =>
|
|
701
|
-
`- **${c.name}** · ${c.userMessageCount} messages · ${new Date(c.savedAt).toLocaleString()}`,
|
|
702
|
-
)
|
|
703
|
-
.join("\n")}`;
|
|
704
|
-
const msg: Message = { role: "assistant", content, type: "text" };
|
|
705
|
-
setCommitted((prev) => [...prev, msg]);
|
|
706
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
if (text.trim().toLowerCase().startsWith("/chat load")) {
|
|
711
|
-
const parts = text.trim().split(/\s+/);
|
|
712
|
-
const name = parts.slice(2).join("-");
|
|
713
|
-
if (!name) {
|
|
714
|
-
const chats = listChats(repoPath);
|
|
715
|
-
const content =
|
|
716
|
-
chats.length === 0
|
|
717
|
-
? "No saved chats found."
|
|
718
|
-
: `Specify a chat name. Recent chats:\n\n${chats
|
|
719
|
-
.slice(0, 10)
|
|
720
|
-
.map((c) => `- **${c.name}**`)
|
|
721
|
-
.join("\n")}`;
|
|
722
|
-
const msg: Message = { role: "assistant", content, type: "text" };
|
|
723
|
-
setCommitted((prev) => [...prev, msg]);
|
|
724
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
const saved = loadChat(name);
|
|
728
|
-
if (!saved) {
|
|
729
|
-
const msg: Message = {
|
|
730
|
-
role: "assistant",
|
|
731
|
-
content: `Chat **${name}** not found. Use \`/chat list\` to see saved chats.`,
|
|
732
|
-
type: "text",
|
|
733
|
-
};
|
|
734
|
-
setCommitted((prev) => [...prev, msg]);
|
|
735
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
736
|
-
return;
|
|
737
|
-
}
|
|
738
|
-
updateChatName(name);
|
|
739
|
-
setAllMessages(saved.messages);
|
|
740
|
-
setCommitted(saved.messages);
|
|
741
|
-
const notice: Message = {
|
|
742
|
-
role: "assistant",
|
|
743
|
-
content: `Loaded chat **${name}** · ${saved.userMessageCount} messages · saved ${new Date(saved.savedAt).toLocaleString()}`,
|
|
744
|
-
type: "text",
|
|
745
|
-
};
|
|
746
|
-
setCommitted((prev) => [...prev, notice]);
|
|
747
|
-
setAllMessages((prev) => [...prev, notice]);
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (
|
|
752
|
-
text.trim().toLowerCase() === "/memory list" ||
|
|
753
|
-
text.trim().toLowerCase() === "/memory"
|
|
754
|
-
) {
|
|
755
|
-
const mems = listMemories(repoPath);
|
|
756
|
-
const content =
|
|
757
|
-
mems.length === 0
|
|
758
|
-
? "No memories stored for this repo yet."
|
|
759
|
-
: `Memories for this repo:\n\n${mems
|
|
760
|
-
.map((m) => `- [${m.id}] ${m.content}`)
|
|
761
|
-
.join("\n")}`;
|
|
762
|
-
const msg: Message = { role: "assistant", content, type: "text" };
|
|
763
|
-
setCommitted((prev) => [...prev, msg]);
|
|
764
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
if (text.trim().toLowerCase().startsWith("/memory add")) {
|
|
769
|
-
const content = text.trim().slice("/memory add".length).trim();
|
|
770
|
-
if (!content) {
|
|
771
|
-
const msg: Message = {
|
|
772
|
-
role: "assistant",
|
|
773
|
-
content: "Usage: `/memory add <content>`",
|
|
774
|
-
type: "text",
|
|
775
|
-
};
|
|
776
|
-
setCommitted((prev) => [...prev, msg]);
|
|
777
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
const mem = addMemory(content, repoPath);
|
|
781
|
-
const msg: Message = {
|
|
782
|
-
role: "assistant",
|
|
783
|
-
content: `Memory saved **[${mem.id}]**: ${mem.content}`,
|
|
784
|
-
type: "text",
|
|
785
|
-
};
|
|
786
|
-
setCommitted((prev) => [...prev, msg]);
|
|
787
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
if (text.trim().toLowerCase().startsWith("/memory delete")) {
|
|
792
|
-
const id = text.trim().split(/\s+/)[2];
|
|
793
|
-
if (!id) {
|
|
794
|
-
const msg: Message = {
|
|
795
|
-
role: "assistant",
|
|
796
|
-
content: "Usage: `/memory delete <id>`",
|
|
797
|
-
type: "text",
|
|
798
|
-
};
|
|
799
|
-
setCommitted((prev) => [...prev, msg]);
|
|
800
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
const deleted = deleteMemory(id, repoPath);
|
|
804
|
-
const msg: Message = {
|
|
805
|
-
role: "assistant",
|
|
806
|
-
content: deleted
|
|
807
|
-
? `Memory **[${id}]** deleted.`
|
|
808
|
-
: `Memory **[${id}]** not found.`,
|
|
809
|
-
type: "text",
|
|
810
|
-
};
|
|
811
|
-
setCommitted((prev) => [...prev, msg]);
|
|
812
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
if (text.trim().toLowerCase() === "/memory clear") {
|
|
817
|
-
clearRepoMemory(repoPath);
|
|
818
|
-
const msg: Message = {
|
|
819
|
-
role: "assistant",
|
|
820
|
-
content: "All memories cleared for this repo.",
|
|
821
|
-
type: "text",
|
|
822
|
-
};
|
|
823
|
-
setCommitted((prev) => [...prev, msg]);
|
|
824
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
const userMsg: Message = { role: "user", content: text, type: "text" };
|
|
829
|
-
const nextAll = [...allMessages, userMsg];
|
|
830
|
-
setCommitted((prev) => [...prev, userMsg]);
|
|
831
|
-
setAllMessages(nextAll);
|
|
832
|
-
// Do NOT clear toolResultCache here — safe tool results (read-file, read-folder, grep)
|
|
833
|
-
// persist across the whole session so the model never re-reads the same resource twice.
|
|
834
|
-
batchApprovedRef.current = false;
|
|
835
|
-
|
|
836
|
-
inputHistoryRef.current = [
|
|
837
|
-
text,
|
|
838
|
-
...inputHistoryRef.current.filter((m) => m !== text),
|
|
839
|
-
].slice(0, 50);
|
|
840
|
-
historyIndexRef.current = -1;
|
|
841
|
-
|
|
842
|
-
if (!chatName) {
|
|
843
|
-
const name =
|
|
844
|
-
getChatNameSuggestions(nextAll)[0] ??
|
|
845
|
-
`chat-${new Date().toISOString().slice(0, 10)}`;
|
|
846
|
-
updateChatName(name);
|
|
847
|
-
setRecentChats((prev) =>
|
|
848
|
-
[name, ...prev.filter((n) => n !== name)].slice(0, 10),
|
|
849
|
-
);
|
|
850
|
-
saveChat(name, repoPath, nextAll);
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
const abort = new AbortController();
|
|
854
|
-
abortControllerRef.current = abort;
|
|
855
|
-
|
|
856
|
-
setStage({ type: "thinking" });
|
|
857
|
-
callChat(provider, systemPrompt, nextAll, abort.signal)
|
|
858
|
-
.then((raw: string) => processResponse(raw, nextAll, abort.signal))
|
|
859
|
-
.catch(handleError(nextAll));
|
|
860
|
-
};
|
|
151
|
+
const chat = useChat(repoPath);
|
|
152
|
+
const thinkingPhrase = useThinkingPhrase(chat.stage.type === "thinking");
|
|
861
153
|
|
|
862
|
-
|
|
863
|
-
|
|
154
|
+
const handleStageKey = (input: string, key: any) => {
|
|
155
|
+
const { stage } = chat;
|
|
864
156
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
setShowForceWarning(false);
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
if (stage.type === "thinking" && key.escape) {
|
|
872
|
-
abortControllerRef.current?.abort();
|
|
873
|
-
abortControllerRef.current = null;
|
|
874
|
-
batchApprovedRef.current = false;
|
|
875
|
-
setStage({ type: "idle" });
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
if (stage.type === "idle") {
|
|
880
|
-
if (key.ctrl && input === "c") {
|
|
881
|
-
process.exit(0);
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
if (key.upArrow && inputHistoryRef.current.length > 0) {
|
|
885
|
-
const next = Math.min(
|
|
886
|
-
historyIndexRef.current + 1,
|
|
887
|
-
inputHistoryRef.current.length - 1,
|
|
888
|
-
);
|
|
889
|
-
historyIndexRef.current = next;
|
|
890
|
-
setInputValue(inputHistoryRef.current[next]!);
|
|
891
|
-
setInputKey((k) => k + 1);
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
if (key.downArrow) {
|
|
895
|
-
const next = historyIndexRef.current - 1;
|
|
896
|
-
historyIndexRef.current = next;
|
|
897
|
-
setInputValue(next < 0 ? "" : inputHistoryRef.current[next]!);
|
|
898
|
-
setInputKey((k) => k + 1);
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
if (key.tab && inputValue.startsWith("/")) {
|
|
902
|
-
const q = inputValue.toLowerCase();
|
|
903
|
-
const match = COMMANDS.find((c) => c.cmd.startsWith(q));
|
|
904
|
-
if (match) setInputValue(match.cmd);
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
157
|
+
if (chat.showForceWarning && key.escape) {
|
|
158
|
+
chat.setShowForceWarning(false);
|
|
907
159
|
return;
|
|
908
160
|
}
|
|
909
161
|
|
|
@@ -912,7 +164,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
912
164
|
const { repoUrl } = stage;
|
|
913
165
|
const launch = stage.launchAnalysis ?? false;
|
|
914
166
|
const cloneUrl = toCloneUrl(repoUrl);
|
|
915
|
-
setStage({ type: "cloning", repoUrl });
|
|
167
|
+
chat.setStage({ type: "cloning", repoUrl });
|
|
916
168
|
startCloneRepo(cloneUrl).then((result) => {
|
|
917
169
|
if (result.done) {
|
|
918
170
|
const repoName =
|
|
@@ -927,8 +179,8 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
927
179
|
detail: repoUrl,
|
|
928
180
|
summary: `Cloned ${repoName} — ${fileCount} files`,
|
|
929
181
|
});
|
|
930
|
-
setClonedUrls((prev) => new Set([...prev, repoUrl]));
|
|
931
|
-
setStage({
|
|
182
|
+
chat.setClonedUrls((prev) => new Set([...prev, repoUrl]));
|
|
183
|
+
chat.setStage({
|
|
932
184
|
type: "clone-done",
|
|
933
185
|
repoUrl,
|
|
934
186
|
destPath,
|
|
@@ -936,13 +188,13 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
936
188
|
launchAnalysis: launch,
|
|
937
189
|
});
|
|
938
190
|
} else if (result.folderExists && result.repoPath) {
|
|
939
|
-
setStage({
|
|
191
|
+
chat.setStage({
|
|
940
192
|
type: "clone-exists",
|
|
941
193
|
repoUrl,
|
|
942
194
|
repoPath: result.repoPath,
|
|
943
195
|
});
|
|
944
196
|
} else {
|
|
945
|
-
setStage({
|
|
197
|
+
chat.setStage({
|
|
946
198
|
type: "clone-error",
|
|
947
199
|
message:
|
|
948
200
|
!result.folderExists && result.error
|
|
@@ -954,25 +206,25 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
954
206
|
return;
|
|
955
207
|
}
|
|
956
208
|
if (input === "n" || input === "N" || key.escape)
|
|
957
|
-
setStage({ type: "idle" });
|
|
209
|
+
chat.setStage({ type: "idle" });
|
|
958
210
|
return;
|
|
959
211
|
}
|
|
960
212
|
|
|
961
213
|
if (stage.type === "clone-exists") {
|
|
962
214
|
if (input === "y" || input === "Y") {
|
|
963
215
|
const { repoUrl, repoPath: existingPath } = stage;
|
|
964
|
-
setStage({ type: "cloning", repoUrl });
|
|
216
|
+
chat.setStage({ type: "cloning", repoUrl });
|
|
965
217
|
startCloneRepo(toCloneUrl(repoUrl), { forceReclone: true }).then(
|
|
966
218
|
(result) => {
|
|
967
219
|
if (result.done) {
|
|
968
|
-
setStage({
|
|
220
|
+
chat.setStage({
|
|
969
221
|
type: "clone-done",
|
|
970
222
|
repoUrl,
|
|
971
223
|
destPath: existingPath,
|
|
972
224
|
fileCount: walkDir(existingPath).length,
|
|
973
225
|
});
|
|
974
226
|
} else {
|
|
975
|
-
setStage({
|
|
227
|
+
chat.setStage({
|
|
976
228
|
type: "clone-error",
|
|
977
229
|
message:
|
|
978
230
|
!result.folderExists && result.error
|
|
@@ -986,7 +238,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
986
238
|
}
|
|
987
239
|
if (input === "n" || input === "N") {
|
|
988
240
|
const { repoUrl, repoPath: existingPath } = stage;
|
|
989
|
-
setStage({
|
|
241
|
+
chat.setStage({
|
|
990
242
|
type: "clone-done",
|
|
991
243
|
repoUrl,
|
|
992
244
|
destPath: existingPath,
|
|
@@ -1014,12 +266,11 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
1014
266
|
result: `Clone complete. Repo: ${repoName}. Local path: ${stage.destPath}. ${stage.fileCount} files.`,
|
|
1015
267
|
approved: true,
|
|
1016
268
|
};
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
setStage({ type: "idle" });
|
|
269
|
+
chat.setAllMessages([...chat.allMessages, contextMsg, summaryMsg]);
|
|
270
|
+
chat.setCommitted((prev) => [...prev, summaryMsg]);
|
|
271
|
+
chat.setStage({ type: "idle" });
|
|
1021
272
|
} else {
|
|
1022
|
-
setStage({ type: "idle" });
|
|
273
|
+
chat.setStage({ type: "idle" });
|
|
1023
274
|
}
|
|
1024
275
|
}
|
|
1025
276
|
return;
|
|
@@ -1033,7 +284,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
1033
284
|
return;
|
|
1034
285
|
}
|
|
1035
286
|
if (input === "n" || input === "N" || key.escape) {
|
|
1036
|
-
batchApprovedRef.current = false;
|
|
287
|
+
chat.batchApprovedRef.current = false;
|
|
1037
288
|
stage.resolve(false);
|
|
1038
289
|
return;
|
|
1039
290
|
}
|
|
@@ -1042,111 +293,116 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
1042
293
|
|
|
1043
294
|
if (stage.type === "preview") {
|
|
1044
295
|
if (key.upArrow) {
|
|
1045
|
-
setStage({
|
|
296
|
+
chat.setStage({
|
|
1046
297
|
...stage,
|
|
1047
298
|
scrollOffset: Math.max(0, stage.scrollOffset - 1),
|
|
1048
299
|
});
|
|
1049
300
|
return;
|
|
1050
301
|
}
|
|
1051
302
|
if (key.downArrow) {
|
|
1052
|
-
setStage({ ...stage, scrollOffset: stage.scrollOffset + 1 });
|
|
303
|
+
chat.setStage({ ...stage, scrollOffset: stage.scrollOffset + 1 });
|
|
1053
304
|
return;
|
|
1054
305
|
}
|
|
1055
306
|
if (key.escape || input === "s" || input === "S") {
|
|
1056
|
-
if (pendingMsgIndex !== null) {
|
|
1057
|
-
const msg = allMessages[pendingMsgIndex];
|
|
307
|
+
if (chat.pendingMsgIndex !== null) {
|
|
308
|
+
const msg = chat.allMessages[chat.pendingMsgIndex];
|
|
1058
309
|
if (msg?.type === "plan") {
|
|
1059
|
-
setCommitted((prev) => [...prev, { ...msg, applied: false }]);
|
|
1060
|
-
|
|
1061
|
-
kind: "code-skipped",
|
|
1062
|
-
detail: msg.patches
|
|
1063
|
-
.map((p: { path: string }) => p.path)
|
|
1064
|
-
.join(", "),
|
|
1065
|
-
summary: `Skipped changes to ${msg.patches.length} file(s)`,
|
|
1066
|
-
});
|
|
310
|
+
chat.setCommitted((prev) => [...prev, { ...msg, applied: false }]);
|
|
311
|
+
chat.skipPatches(msg.patches);
|
|
1067
312
|
}
|
|
1068
313
|
}
|
|
1069
|
-
setPendingMsgIndex(null);
|
|
1070
|
-
setStage({ type: "idle" });
|
|
314
|
+
chat.setPendingMsgIndex(null);
|
|
315
|
+
chat.setStage({ type: "idle" });
|
|
1071
316
|
return;
|
|
1072
317
|
}
|
|
1073
318
|
if (key.return || input === "a" || input === "A") {
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
appendMemory({
|
|
1077
|
-
kind: "code-applied",
|
|
1078
|
-
detail: stage.patches.map((p) => p.path).join(", "),
|
|
1079
|
-
summary: `Applied changes to ${stage.patches.length} file(s)`,
|
|
1080
|
-
});
|
|
1081
|
-
} catch {
|
|
1082
|
-
/* non-fatal */
|
|
1083
|
-
}
|
|
1084
|
-
if (pendingMsgIndex !== null) {
|
|
1085
|
-
const msg = allMessages[pendingMsgIndex];
|
|
319
|
+
if (chat.pendingMsgIndex !== null) {
|
|
320
|
+
const msg = chat.allMessages[chat.pendingMsgIndex];
|
|
1086
321
|
if (msg?.type === "plan") {
|
|
322
|
+
chat.applyPatchesAndContinue(msg.patches);
|
|
1087
323
|
const applied: Message = { ...msg, applied: true };
|
|
1088
|
-
setAllMessages((prev) =>
|
|
1089
|
-
prev.map((m, i) => (i === pendingMsgIndex ? applied : m)),
|
|
324
|
+
chat.setAllMessages((prev) =>
|
|
325
|
+
prev.map((m, i) => (i === chat.pendingMsgIndex ? applied : m)),
|
|
1090
326
|
);
|
|
1091
|
-
setCommitted((prev) => [...prev, applied]);
|
|
327
|
+
chat.setCommitted((prev) => [...prev, applied]);
|
|
1092
328
|
}
|
|
1093
329
|
}
|
|
1094
|
-
setPendingMsgIndex(null);
|
|
1095
|
-
setStage({ type: "idle" });
|
|
330
|
+
chat.setPendingMsgIndex(null);
|
|
331
|
+
chat.setStage({ type: "idle" });
|
|
1096
332
|
return;
|
|
1097
333
|
}
|
|
1098
334
|
}
|
|
1099
335
|
|
|
1100
336
|
if (stage.type === "viewing-file") {
|
|
1101
337
|
if (key.upArrow) {
|
|
1102
|
-
setStage({
|
|
338
|
+
chat.setStage({
|
|
1103
339
|
...stage,
|
|
1104
340
|
scrollOffset: Math.max(0, stage.scrollOffset - 1),
|
|
1105
341
|
});
|
|
1106
342
|
return;
|
|
1107
343
|
}
|
|
1108
344
|
if (key.downArrow) {
|
|
1109
|
-
setStage({ ...stage, scrollOffset: stage.scrollOffset + 1 });
|
|
345
|
+
chat.setStage({ ...stage, scrollOffset: stage.scrollOffset + 1 });
|
|
1110
346
|
return;
|
|
1111
347
|
}
|
|
1112
348
|
if (key.escape || key.return) {
|
|
1113
|
-
setStage({ type: "idle" });
|
|
349
|
+
chat.setStage({ type: "idle" });
|
|
1114
350
|
return;
|
|
1115
351
|
}
|
|
1116
352
|
}
|
|
1117
|
-
}
|
|
353
|
+
};
|
|
1118
354
|
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
+
}
|
|
1146
400
|
};
|
|
1147
401
|
|
|
402
|
+
const { stage } = chat;
|
|
403
|
+
|
|
1148
404
|
if (stage.type === "picking-provider")
|
|
1149
|
-
return <ProviderPicker onDone={handleProviderDone} />;
|
|
405
|
+
return <ProviderPicker onDone={chat.handleProviderDone} />;
|
|
1150
406
|
if (stage.type === "loading")
|
|
1151
407
|
return (
|
|
1152
408
|
<Box gap={1} marginTop={1}>
|
|
@@ -1159,68 +415,67 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
1159
415
|
</Text>
|
|
1160
416
|
</Box>
|
|
1161
417
|
);
|
|
1162
|
-
if (showTimeline)
|
|
418
|
+
if (chat.showTimeline)
|
|
1163
419
|
return (
|
|
1164
420
|
<TimelineRunner
|
|
1165
421
|
repoPath={repoPath}
|
|
1166
|
-
onExit={() => setShowTimeline(false)}
|
|
422
|
+
onExit={() => chat.setShowTimeline(false)}
|
|
1167
423
|
/>
|
|
1168
424
|
);
|
|
1169
|
-
if (showReview)
|
|
425
|
+
if (chat.showReview)
|
|
1170
426
|
return (
|
|
1171
|
-
<ReviewCommand path={repoPath} onExit={() => setShowReview(false)} />
|
|
427
|
+
<ReviewCommand path={repoPath} onExit={() => chat.setShowReview(false)} />
|
|
1172
428
|
);
|
|
1173
429
|
if (stage.type === "clone-offer")
|
|
1174
|
-
return <CloneOfferView stage={stage} committed={committed} />;
|
|
430
|
+
return <CloneOfferView stage={stage} committed={chat.committed} />;
|
|
1175
431
|
if (stage.type === "cloning")
|
|
1176
|
-
return <CloningView stage={stage} committed={committed} />;
|
|
432
|
+
return <CloningView stage={stage} committed={chat.committed} />;
|
|
1177
433
|
if (stage.type === "clone-exists")
|
|
1178
|
-
return <CloneExistsView stage={stage} committed={committed} />;
|
|
434
|
+
return <CloneExistsView stage={stage} committed={chat.committed} />;
|
|
1179
435
|
if (stage.type === "clone-done")
|
|
1180
|
-
return <CloneDoneView stage={stage} committed={committed} />;
|
|
436
|
+
return <CloneDoneView stage={stage} committed={chat.committed} />;
|
|
1181
437
|
if (stage.type === "clone-error")
|
|
1182
|
-
return <CloneErrorView stage={stage} committed={committed} />;
|
|
438
|
+
return <CloneErrorView stage={stage} committed={chat.committed} />;
|
|
1183
439
|
if (stage.type === "preview")
|
|
1184
|
-
return <PreviewView stage={stage} committed={committed} />;
|
|
440
|
+
return <PreviewView stage={stage} committed={chat.committed} />;
|
|
1185
441
|
if (stage.type === "viewing-file")
|
|
1186
|
-
return <ViewingFileView stage={stage} committed={committed} />;
|
|
442
|
+
return <ViewingFileView stage={stage} committed={chat.committed} />;
|
|
1187
443
|
|
|
1188
444
|
return (
|
|
1189
445
|
<Box flexDirection="column">
|
|
1190
|
-
<Static items={committed}>
|
|
446
|
+
<Static items={chat.committed}>
|
|
1191
447
|
{(msg, i) => <StaticMessage key={i} msg={msg} />}
|
|
1192
448
|
</Static>
|
|
1193
449
|
|
|
1194
|
-
{
|
|
1195
|
-
{showForceWarning && (
|
|
450
|
+
{chat.showForceWarning && (
|
|
1196
451
|
<ForceAllWarning
|
|
1197
452
|
onConfirm={(confirmed) => {
|
|
1198
|
-
setShowForceWarning(false);
|
|
453
|
+
chat.setShowForceWarning(false);
|
|
1199
454
|
if (confirmed) {
|
|
1200
|
-
setForceApprove(true);
|
|
1201
|
-
setAutoApprove(true);
|
|
455
|
+
chat.setForceApprove(true);
|
|
456
|
+
chat.setAutoApprove(true);
|
|
1202
457
|
const msg: Message = {
|
|
1203
458
|
role: "assistant",
|
|
1204
459
|
content:
|
|
1205
460
|
"⚡⚡ Force-all mode ON — ALL tools auto-approved including shell and writes. Type /auto --force-all again to disable.",
|
|
1206
461
|
type: "text",
|
|
1207
462
|
};
|
|
1208
|
-
setCommitted((prev) => [...prev, msg]);
|
|
1209
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
463
|
+
chat.setCommitted((prev) => [...prev, msg]);
|
|
464
|
+
chat.setAllMessages((prev: Message[]) => [...prev, msg]);
|
|
1210
465
|
} else {
|
|
1211
466
|
const msg: Message = {
|
|
1212
467
|
role: "assistant",
|
|
1213
468
|
content: "Force-all cancelled.",
|
|
1214
469
|
type: "text",
|
|
1215
470
|
};
|
|
1216
|
-
setCommitted((prev) => [...prev, msg]);
|
|
1217
|
-
setAllMessages((prev) => [...prev, msg]);
|
|
471
|
+
chat.setCommitted((prev) => [...prev, msg]);
|
|
472
|
+
chat.setAllMessages((prev: Message[]) => [...prev, msg]);
|
|
1218
473
|
}
|
|
1219
474
|
}}
|
|
1220
475
|
/>
|
|
1221
476
|
)}
|
|
1222
477
|
|
|
1223
|
-
{!showForceWarning && stage.type === "thinking" && (
|
|
478
|
+
{!chat.showForceWarning && stage.type === "thinking" && (
|
|
1224
479
|
<Box gap={1}>
|
|
1225
480
|
<Text color={ACCENT}>●</Text>
|
|
1226
481
|
<TypewriterText text={thinkingPhrase} />
|
|
@@ -1230,32 +485,31 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
1230
485
|
</Box>
|
|
1231
486
|
)}
|
|
1232
487
|
|
|
1233
|
-
{!showForceWarning && stage.type === "permission" && (
|
|
488
|
+
{!chat.showForceWarning && stage.type === "permission" && (
|
|
1234
489
|
<PermissionPrompt tool={stage.tool} onDecide={stage.resolve} />
|
|
1235
490
|
)}
|
|
1236
491
|
|
|
1237
|
-
{!showForceWarning && stage.type === "idle" && (
|
|
492
|
+
{!chat.showForceWarning && stage.type === "idle" && (
|
|
1238
493
|
<Box flexDirection="column">
|
|
1239
|
-
{inputValue.startsWith("/") && (
|
|
494
|
+
{chatInput.inputValue.startsWith("/") && (
|
|
1240
495
|
<CommandPalette
|
|
1241
|
-
query={inputValue}
|
|
1242
|
-
|
|
1243
|
-
recentChats={recentChats}
|
|
496
|
+
query={chatInput.inputValue}
|
|
497
|
+
recentChats={chat.recentChats}
|
|
1244
498
|
/>
|
|
1245
499
|
)}
|
|
1246
500
|
<InputBox
|
|
1247
|
-
value={inputValue}
|
|
1248
|
-
onChange={(v) =>
|
|
1249
|
-
historyIndexRef.current = -1;
|
|
1250
|
-
setInputValue(v);
|
|
1251
|
-
}}
|
|
501
|
+
value={chatInput.inputValue}
|
|
502
|
+
onChange={(v) => chatInput.setInputValue(v)}
|
|
1252
503
|
onSubmit={(val) => {
|
|
1253
504
|
if (val.trim()) sendMessage(val.trim());
|
|
1254
|
-
setInputValue("");
|
|
505
|
+
chatInput.setInputValue("");
|
|
1255
506
|
}}
|
|
1256
|
-
inputKey={inputKey}
|
|
507
|
+
inputKey={chatInput.inputKey}
|
|
508
|
+
/>
|
|
509
|
+
<ShortcutBar
|
|
510
|
+
autoApprove={chat.autoApprove}
|
|
511
|
+
forceApprove={chat.forceApprove}
|
|
1257
512
|
/>
|
|
1258
|
-
<ShortcutBar autoApprove={autoApprove} forceApprove={forceApprove} />
|
|
1259
513
|
</Box>
|
|
1260
514
|
)}
|
|
1261
515
|
</Box>
|