@ridit/lens 0.1.9 → 0.2.1
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 +1553 -1139
- package/package.json +1 -1
- package/src/components/chat/ChatMessage.tsx +15 -1
- package/src/components/chat/ChatOverlays.tsx +8 -1
- package/src/components/chat/ChatRunner.tsx +381 -31
- package/src/prompts/fewshot.ts +377 -0
- package/src/prompts/index.ts +2 -0
- package/src/prompts/system.ts +190 -0
- package/src/tools/files.ts +261 -0
- package/src/tools/index.ts +13 -0
- package/src/tools/pdf.ts +106 -0
- package/src/tools/shell.ts +96 -0
- package/src/tools/web.ts +216 -0
- package/src/utils/chat.ts +40 -1308
- package/src/utils/chatHistory.ts +121 -0
- package/src/utils/files.ts +1 -0
- package/src/utils/memory.ts +137 -0
- package/src/utils/history.ts +0 -86
package/package.json
CHANGED
|
@@ -98,6 +98,20 @@ function MessageBody({ content }: { content: string }) {
|
|
|
98
98
|
);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
function summarizeToolContent(toolName: string, content: string): string {
|
|
102
|
+
// For write-file, extract just the path
|
|
103
|
+
if (toolName === "write-file" || toolName === "read-file") {
|
|
104
|
+
const pathMatch = content.match(/"path"\s*:\s*"([^"]+)"/);
|
|
105
|
+
if (pathMatch) return pathMatch[1]!;
|
|
106
|
+
}
|
|
107
|
+
// For changes blocks, just say what changed
|
|
108
|
+
if (content.includes('"summary"')) {
|
|
109
|
+
const summaryMatch = content.match(/"summary"\s*:\s*"([^"]+)"/);
|
|
110
|
+
if (summaryMatch) return summaryMatch[1]!;
|
|
111
|
+
}
|
|
112
|
+
return content.length > 120 ? content.slice(0, 120) + "…" : content;
|
|
113
|
+
}
|
|
114
|
+
|
|
101
115
|
export function StaticMessage({ msg }: { msg: Message }) {
|
|
102
116
|
if (msg.role === "user") {
|
|
103
117
|
return (
|
|
@@ -130,7 +144,7 @@ export function StaticMessage({ msg }: { msg: Message }) {
|
|
|
130
144
|
? msg.content
|
|
131
145
|
: msg.toolName === "search"
|
|
132
146
|
? `"${msg.content}"`
|
|
133
|
-
: msg.content;
|
|
147
|
+
: summarizeToolContent(msg.toolName, msg.content);
|
|
134
148
|
|
|
135
149
|
return (
|
|
136
150
|
<Box flexDirection="column" marginBottom={1}>
|
|
@@ -100,10 +100,12 @@ export function InputBox({
|
|
|
100
100
|
value,
|
|
101
101
|
onChange,
|
|
102
102
|
onSubmit,
|
|
103
|
+
inputKey,
|
|
103
104
|
}: {
|
|
104
105
|
value: string;
|
|
105
106
|
onChange: (v: string) => void;
|
|
106
107
|
onSubmit: (v: string) => void;
|
|
108
|
+
inputKey?: number;
|
|
107
109
|
}) {
|
|
108
110
|
return (
|
|
109
111
|
<Box
|
|
@@ -117,7 +119,12 @@ export function InputBox({
|
|
|
117
119
|
>
|
|
118
120
|
<Box gap={1}>
|
|
119
121
|
<Text color={ACCENT}>{">"}</Text>
|
|
120
|
-
<TextInput
|
|
122
|
+
<TextInput
|
|
123
|
+
key={inputKey}
|
|
124
|
+
value={value}
|
|
125
|
+
onChange={onChange}
|
|
126
|
+
onSubmit={onSubmit}
|
|
127
|
+
/>
|
|
121
128
|
</Box>
|
|
122
129
|
</Box>
|
|
123
130
|
);
|
|
@@ -32,6 +32,13 @@ import {
|
|
|
32
32
|
callChat,
|
|
33
33
|
searchWeb,
|
|
34
34
|
} from "../../utils/chat";
|
|
35
|
+
import {
|
|
36
|
+
saveChat,
|
|
37
|
+
loadChat,
|
|
38
|
+
listChats,
|
|
39
|
+
deleteChat,
|
|
40
|
+
getChatNameSuggestions,
|
|
41
|
+
} from "../../utils/chatHistory";
|
|
35
42
|
import { StaticMessage } from "./ChatMessage";
|
|
36
43
|
import {
|
|
37
44
|
PermissionPrompt,
|
|
@@ -50,30 +57,60 @@ import { TimelineRunner } from "../timeline/TimelineRunner";
|
|
|
50
57
|
import type { Provider } from "../../types/config";
|
|
51
58
|
import type { Message, ChatStage } from "../../types/chat";
|
|
52
59
|
import {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
appendMemory,
|
|
61
|
+
buildMemorySummary,
|
|
62
|
+
clearRepoMemory,
|
|
63
|
+
addMemory,
|
|
64
|
+
deleteMemory,
|
|
65
|
+
listMemories,
|
|
66
|
+
} from "../../utils/memory";
|
|
57
67
|
import { readLensFile } from "../../utils/lensfile";
|
|
58
68
|
import { ReviewCommand } from "../../commands/review";
|
|
59
69
|
|
|
60
70
|
const COMMANDS = [
|
|
61
71
|
{ cmd: "/timeline", desc: "browse commit history" },
|
|
62
72
|
{ cmd: "/clear history", desc: "wipe session memory for this repo" },
|
|
63
|
-
{ cmd: "/review", desc: "review current
|
|
73
|
+
{ cmd: "/review", desc: "review current codebase" },
|
|
64
74
|
{ cmd: "/auto", desc: "toggle auto-approve for read/search tools" },
|
|
75
|
+
{ cmd: "/chat", desc: "chat history commands" },
|
|
76
|
+
{ cmd: "/chat list", desc: "list saved chats for this repo" },
|
|
77
|
+
{ cmd: "/chat load", desc: "load a saved chat by name" },
|
|
78
|
+
{ cmd: "/chat rename", desc: "rename the current chat" },
|
|
79
|
+
{ cmd: "/chat delete", desc: "delete a saved chat by name" },
|
|
80
|
+
{ cmd: "/memory", desc: "memory commands" },
|
|
81
|
+
{ cmd: "/memory list", desc: "list all memories for this repo" },
|
|
82
|
+
{ cmd: "/memory add", desc: "add a memory" },
|
|
83
|
+
{ cmd: "/memory delete", desc: "delete a memory by id" },
|
|
84
|
+
{ cmd: "/memory clear", desc: "clear all memories for this repo" },
|
|
65
85
|
];
|
|
66
86
|
|
|
67
87
|
function CommandPalette({
|
|
68
88
|
query,
|
|
69
89
|
onSelect,
|
|
90
|
+
recentChats,
|
|
70
91
|
}: {
|
|
71
92
|
query: string;
|
|
72
93
|
onSelect: (cmd: string) => void;
|
|
94
|
+
recentChats: string[];
|
|
73
95
|
}) {
|
|
74
96
|
const q = query.toLowerCase();
|
|
97
|
+
|
|
98
|
+
// If typing "/chat load <something>", stay visible and filter chats
|
|
99
|
+
const isChatLoad = q.startsWith("/chat load") || q.startsWith("/chat delete");
|
|
100
|
+
const chatFilter = isChatLoad
|
|
101
|
+
? q.startsWith("/chat load")
|
|
102
|
+
? q.slice("/chat load".length).trim()
|
|
103
|
+
: q.slice("/chat delete".length).trim()
|
|
104
|
+
: "";
|
|
105
|
+
const filteredChats = chatFilter
|
|
106
|
+
? recentChats.filter((n) => n.toLowerCase().includes(chatFilter))
|
|
107
|
+
: recentChats;
|
|
108
|
+
|
|
75
109
|
const matches = COMMANDS.filter((c) => c.cmd.startsWith(q));
|
|
76
|
-
|
|
110
|
+
|
|
111
|
+
// Keep palette open if we're in /chat load mode even after space
|
|
112
|
+
if (!matches.length && !isChatLoad) return null;
|
|
113
|
+
if (!matches.length && isChatLoad && filteredChats.length === 0) return null;
|
|
77
114
|
|
|
78
115
|
return (
|
|
79
116
|
<Box flexDirection="column" marginBottom={1} marginLeft={2}>
|
|
@@ -90,6 +127,19 @@ function CommandPalette({
|
|
|
90
127
|
</Box>
|
|
91
128
|
);
|
|
92
129
|
})}
|
|
130
|
+
{isChatLoad && filteredChats.length > 0 && (
|
|
131
|
+
<Box flexDirection="column" marginTop={matches.length ? 1 : 0}>
|
|
132
|
+
<Text color="gray" dimColor>
|
|
133
|
+
{chatFilter ? `matching "${chatFilter}":` : "recent chats:"}
|
|
134
|
+
</Text>
|
|
135
|
+
{filteredChats.map((name, i) => (
|
|
136
|
+
<Box key={i} gap={1} marginLeft={2}>
|
|
137
|
+
<Text color={ACCENT}>·</Text>
|
|
138
|
+
<Text color="white">{name}</Text>
|
|
139
|
+
</Box>
|
|
140
|
+
))}
|
|
141
|
+
</Box>
|
|
142
|
+
)}
|
|
93
143
|
</Box>
|
|
94
144
|
);
|
|
95
145
|
}
|
|
@@ -106,19 +156,37 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
106
156
|
const [showTimeline, setShowTimeline] = useState(false);
|
|
107
157
|
const [showReview, setShowReview] = useState(false);
|
|
108
158
|
const [autoApprove, setAutoApprove] = useState(false);
|
|
159
|
+
const [chatName, setChatName] = useState<string | null>(null);
|
|
160
|
+
const chatNameRef = useRef<string | null>(null);
|
|
161
|
+
const [recentChats, setRecentChats] = useState<string[]>([]);
|
|
162
|
+
const inputHistoryRef = useRef<string[]>([]);
|
|
163
|
+
const historyIndexRef = useRef<number>(-1);
|
|
164
|
+
const [inputKey, setInputKey] = useState(0);
|
|
165
|
+
|
|
166
|
+
const updateChatName = (name: string) => {
|
|
167
|
+
chatNameRef.current = name;
|
|
168
|
+
setChatName(name);
|
|
169
|
+
};
|
|
109
170
|
|
|
110
|
-
// Abort controller for the currently in-flight API call.
|
|
111
|
-
// Pressing ESC while thinking aborts the request and drops the response.
|
|
112
171
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
113
|
-
|
|
114
|
-
// Cache of tool results within a single conversation turn to prevent
|
|
115
|
-
// the model from re-calling tools it already ran with the same args
|
|
116
172
|
const toolResultCache = useRef<Map<string, string>>(new Map());
|
|
117
|
-
|
|
118
173
|
const inputBuffer = useRef("");
|
|
119
174
|
const flushTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
120
175
|
const thinkingPhrase = useThinkingPhrase(stage.type === "thinking");
|
|
121
176
|
|
|
177
|
+
// Load recent chats on mount
|
|
178
|
+
React.useEffect(() => {
|
|
179
|
+
const chats = listChats(repoPath);
|
|
180
|
+
setRecentChats(chats.slice(0, 10).map((c) => c.name));
|
|
181
|
+
}, [repoPath]);
|
|
182
|
+
|
|
183
|
+
// Auto-save whenever messages change
|
|
184
|
+
React.useEffect(() => {
|
|
185
|
+
if (chatNameRef.current && allMessages.length > 1) {
|
|
186
|
+
saveChat(chatNameRef.current, repoPath, allMessages);
|
|
187
|
+
}
|
|
188
|
+
}, [allMessages]);
|
|
189
|
+
|
|
122
190
|
const flushBuffer = () => {
|
|
123
191
|
const buf = inputBuffer.current;
|
|
124
192
|
if (!buf) return;
|
|
@@ -135,7 +203,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
135
203
|
};
|
|
136
204
|
|
|
137
205
|
const handleError = (currentAll: Message[]) => (err: unknown) => {
|
|
138
|
-
// Silently drop aborted requests — user pressed ESC intentionally
|
|
139
206
|
if (err instanceof Error && err.name === "AbortError") {
|
|
140
207
|
setStage({ type: "idle" });
|
|
141
208
|
return;
|
|
@@ -155,13 +222,33 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
155
222
|
currentAll: Message[],
|
|
156
223
|
signal: AbortSignal,
|
|
157
224
|
) => {
|
|
158
|
-
// If ESC was pressed before we got here, silently drop the response
|
|
159
225
|
if (signal.aborted) {
|
|
160
226
|
setStage({ type: "idle" });
|
|
161
227
|
return;
|
|
162
228
|
}
|
|
163
229
|
|
|
164
|
-
|
|
230
|
+
// Handle inline memory operations the model may emit
|
|
231
|
+
const memAddMatches = [
|
|
232
|
+
...raw.matchAll(/<memory-add>([\s\S]*?)<\/memory-add>/g),
|
|
233
|
+
];
|
|
234
|
+
const memDelMatches = [
|
|
235
|
+
...raw.matchAll(/<memory-delete>([\s\S]*?)<\/memory-delete>/g),
|
|
236
|
+
];
|
|
237
|
+
for (const match of memAddMatches) {
|
|
238
|
+
const content = match[1]!.trim();
|
|
239
|
+
if (content) addMemory(content, repoPath);
|
|
240
|
+
}
|
|
241
|
+
for (const match of memDelMatches) {
|
|
242
|
+
const id = match[1]!.trim();
|
|
243
|
+
if (id) deleteMemory(id, repoPath);
|
|
244
|
+
}
|
|
245
|
+
// Strip memory tags from raw before parsing
|
|
246
|
+
const cleanRaw = raw
|
|
247
|
+
.replace(/<memory-add>[\s\S]*?<\/memory-add>/g, "")
|
|
248
|
+
.replace(/<memory-delete>[\s\S]*?<\/memory-delete>/g, "")
|
|
249
|
+
.trim();
|
|
250
|
+
|
|
251
|
+
const parsed = parseResponse(cleanRaw);
|
|
165
252
|
|
|
166
253
|
if (parsed.kind === "changes") {
|
|
167
254
|
if (parsed.patches.length === 0) {
|
|
@@ -252,7 +339,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
252
339
|
setCommitted((prev) => [...prev, preambleMsg]);
|
|
253
340
|
}
|
|
254
341
|
|
|
255
|
-
// Safe tools that can be auto-approved (no side effects)
|
|
256
342
|
const isSafeTool =
|
|
257
343
|
parsed.kind === "read-file" ||
|
|
258
344
|
parsed.kind === "read-folder" ||
|
|
@@ -334,7 +420,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
334
420
|
"write-file": "file-written",
|
|
335
421
|
search: "url-fetched",
|
|
336
422
|
} as const;
|
|
337
|
-
|
|
423
|
+
appendMemory({
|
|
338
424
|
kind: kindMap[parsed.kind as keyof typeof kindMap] ?? "shell-run",
|
|
339
425
|
detail:
|
|
340
426
|
parsed.kind === "shell"
|
|
@@ -418,7 +504,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
418
504
|
setAllMessages(withTool);
|
|
419
505
|
setCommitted((prev) => [...prev, toolMsg]);
|
|
420
506
|
|
|
421
|
-
// Create a fresh abort controller for the follow-up call
|
|
422
507
|
const nextAbort = new AbortController();
|
|
423
508
|
abortControllerRef.current = nextAbort;
|
|
424
509
|
|
|
@@ -514,7 +599,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
514
599
|
}
|
|
515
600
|
|
|
516
601
|
if (text.trim().toLowerCase() === "/clear history") {
|
|
517
|
-
|
|
602
|
+
clearRepoMemory(repoPath);
|
|
518
603
|
const clearedMsg: Message = {
|
|
519
604
|
role: "assistant",
|
|
520
605
|
content: "History cleared for this repo.",
|
|
@@ -525,13 +610,258 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
525
610
|
return;
|
|
526
611
|
}
|
|
527
612
|
|
|
613
|
+
// bare /chat — show usage
|
|
614
|
+
if (text.trim().toLowerCase() === "/chat") {
|
|
615
|
+
const msg: Message = {
|
|
616
|
+
role: "assistant",
|
|
617
|
+
content:
|
|
618
|
+
"Chat commands: `/chat list` · `/chat load <n>` · `/chat rename <n>` · `/chat delete <n>`",
|
|
619
|
+
type: "text",
|
|
620
|
+
};
|
|
621
|
+
setCommitted((prev) => [...prev, msg]);
|
|
622
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// /chat rename <newname>
|
|
627
|
+
if (text.trim().toLowerCase().startsWith("/chat rename")) {
|
|
628
|
+
const parts = text.trim().split(/\s+/);
|
|
629
|
+
const newName = parts.slice(2).join("-");
|
|
630
|
+
if (!newName) {
|
|
631
|
+
const msg: Message = {
|
|
632
|
+
role: "assistant",
|
|
633
|
+
content: "Usage: `/chat rename <new-name>`",
|
|
634
|
+
type: "text",
|
|
635
|
+
};
|
|
636
|
+
setCommitted((prev) => [...prev, msg]);
|
|
637
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const oldName = chatNameRef.current;
|
|
641
|
+
if (oldName) deleteChat(oldName);
|
|
642
|
+
updateChatName(newName);
|
|
643
|
+
saveChat(newName, repoPath, allMessages);
|
|
644
|
+
setRecentChats((prev) =>
|
|
645
|
+
[newName, ...prev.filter((n) => n !== newName && n !== oldName)].slice(
|
|
646
|
+
0,
|
|
647
|
+
10,
|
|
648
|
+
),
|
|
649
|
+
);
|
|
650
|
+
const msg: Message = {
|
|
651
|
+
role: "assistant",
|
|
652
|
+
content: `Chat renamed to **${newName}**.`,
|
|
653
|
+
type: "text",
|
|
654
|
+
};
|
|
655
|
+
setCommitted((prev) => [...prev, msg]);
|
|
656
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// /chat delete <name>
|
|
661
|
+
if (text.trim().toLowerCase().startsWith("/chat delete")) {
|
|
662
|
+
const parts = text.trim().split(/\s+/);
|
|
663
|
+
const name = parts.slice(2).join("-");
|
|
664
|
+
if (!name) {
|
|
665
|
+
const msg: Message = {
|
|
666
|
+
role: "assistant",
|
|
667
|
+
content: "Usage: `/chat delete <name>`",
|
|
668
|
+
type: "text",
|
|
669
|
+
};
|
|
670
|
+
setCommitted((prev) => [...prev, msg]);
|
|
671
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
const deleted = deleteChat(name);
|
|
675
|
+
if (!deleted) {
|
|
676
|
+
const msg: Message = {
|
|
677
|
+
role: "assistant",
|
|
678
|
+
content: `Chat **${name}** not found.`,
|
|
679
|
+
type: "text",
|
|
680
|
+
};
|
|
681
|
+
setCommitted((prev) => [...prev, msg]);
|
|
682
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
// If deleting the current chat, clear the name so it gets re-named on next message
|
|
686
|
+
if (chatNameRef.current === name) {
|
|
687
|
+
chatNameRef.current = null;
|
|
688
|
+
setChatName(null);
|
|
689
|
+
}
|
|
690
|
+
setRecentChats((prev) => prev.filter((n) => n !== name));
|
|
691
|
+
const msg: Message = {
|
|
692
|
+
role: "assistant",
|
|
693
|
+
content: `Chat **${name}** deleted.`,
|
|
694
|
+
type: "text",
|
|
695
|
+
};
|
|
696
|
+
setCommitted((prev) => [...prev, msg]);
|
|
697
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// /chat list
|
|
702
|
+
if (text.trim().toLowerCase() === "/chat list") {
|
|
703
|
+
const chats = listChats(repoPath);
|
|
704
|
+
const content =
|
|
705
|
+
chats.length === 0
|
|
706
|
+
? "No saved chats for this repo yet."
|
|
707
|
+
: `Saved chats:\n\n${chats
|
|
708
|
+
.map(
|
|
709
|
+
(c) =>
|
|
710
|
+
`- **${c.name}** · ${c.userMessageCount} messages · ${new Date(c.savedAt).toLocaleString()}`,
|
|
711
|
+
)
|
|
712
|
+
.join("\n")}`;
|
|
713
|
+
const msg: Message = { role: "assistant", content, type: "text" };
|
|
714
|
+
setCommitted((prev) => [...prev, msg]);
|
|
715
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// /chat load <n>
|
|
720
|
+
if (text.trim().toLowerCase().startsWith("/chat load")) {
|
|
721
|
+
const parts = text.trim().split(/\s+/);
|
|
722
|
+
const name = parts.slice(2).join("-");
|
|
723
|
+
if (!name) {
|
|
724
|
+
const chats = listChats(repoPath);
|
|
725
|
+
const content =
|
|
726
|
+
chats.length === 0
|
|
727
|
+
? "No saved chats found."
|
|
728
|
+
: `Specify a chat name. Recent chats:\n\n${chats
|
|
729
|
+
.slice(0, 10)
|
|
730
|
+
.map((c) => `- **${c.name}**`)
|
|
731
|
+
.join("\n")}`;
|
|
732
|
+
const msg: Message = { role: "assistant", content, type: "text" };
|
|
733
|
+
setCommitted((prev) => [...prev, msg]);
|
|
734
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const saved = loadChat(name);
|
|
738
|
+
if (!saved) {
|
|
739
|
+
const msg: Message = {
|
|
740
|
+
role: "assistant",
|
|
741
|
+
content: `Chat **${name}** not found. Use \`/chat list\` to see saved chats.`,
|
|
742
|
+
type: "text",
|
|
743
|
+
};
|
|
744
|
+
setCommitted((prev) => [...prev, msg]);
|
|
745
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
updateChatName(name);
|
|
749
|
+
setAllMessages(saved.messages);
|
|
750
|
+
setCommitted(saved.messages);
|
|
751
|
+
const notice: Message = {
|
|
752
|
+
role: "assistant",
|
|
753
|
+
content: `Loaded chat **${name}** · ${saved.userMessageCount} messages · saved ${new Date(saved.savedAt).toLocaleString()}`,
|
|
754
|
+
type: "text",
|
|
755
|
+
};
|
|
756
|
+
setCommitted((prev) => [...prev, notice]);
|
|
757
|
+
setAllMessages((prev) => [...prev, notice]);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// /memory list
|
|
762
|
+
if (
|
|
763
|
+
text.trim().toLowerCase() === "/memory list" ||
|
|
764
|
+
text.trim().toLowerCase() === "/memory"
|
|
765
|
+
) {
|
|
766
|
+
const mems = listMemories(repoPath);
|
|
767
|
+
const content =
|
|
768
|
+
mems.length === 0
|
|
769
|
+
? "No memories stored for this repo yet."
|
|
770
|
+
: `Memories for this repo:\n\n${mems.map((m) => `- [${m.id}] ${m.content}`).join("\n")}`;
|
|
771
|
+
const msg: Message = { role: "assistant", content, type: "text" };
|
|
772
|
+
setCommitted((prev) => [...prev, msg]);
|
|
773
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// /memory add <content>
|
|
778
|
+
if (text.trim().toLowerCase().startsWith("/memory add")) {
|
|
779
|
+
const content = text.trim().slice("/memory add".length).trim();
|
|
780
|
+
if (!content) {
|
|
781
|
+
const msg: Message = {
|
|
782
|
+
role: "assistant",
|
|
783
|
+
content: "Usage: `/memory add <content>`",
|
|
784
|
+
type: "text",
|
|
785
|
+
};
|
|
786
|
+
setCommitted((prev) => [...prev, msg]);
|
|
787
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const mem = addMemory(content, repoPath);
|
|
791
|
+
const msg: Message = {
|
|
792
|
+
role: "assistant",
|
|
793
|
+
content: `Memory saved **[${mem.id}]**: ${mem.content}`,
|
|
794
|
+
type: "text",
|
|
795
|
+
};
|
|
796
|
+
setCommitted((prev) => [...prev, msg]);
|
|
797
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// /memory delete <id>
|
|
802
|
+
if (text.trim().toLowerCase().startsWith("/memory delete")) {
|
|
803
|
+
const id = text.trim().split(/\s+/)[2];
|
|
804
|
+
if (!id) {
|
|
805
|
+
const msg: Message = {
|
|
806
|
+
role: "assistant",
|
|
807
|
+
content: "Usage: `/memory delete <id>`",
|
|
808
|
+
type: "text",
|
|
809
|
+
};
|
|
810
|
+
setCommitted((prev) => [...prev, msg]);
|
|
811
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const deleted = deleteMemory(id, repoPath);
|
|
815
|
+
const msg: Message = {
|
|
816
|
+
role: "assistant",
|
|
817
|
+
content: deleted
|
|
818
|
+
? `Memory **[${id}]** deleted.`
|
|
819
|
+
: `Memory **[${id}]** not found.`,
|
|
820
|
+
type: "text",
|
|
821
|
+
};
|
|
822
|
+
setCommitted((prev) => [...prev, msg]);
|
|
823
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// /memory clear
|
|
828
|
+
if (text.trim().toLowerCase() === "/memory clear") {
|
|
829
|
+
clearRepoMemory(repoPath);
|
|
830
|
+
const msg: Message = {
|
|
831
|
+
role: "assistant",
|
|
832
|
+
content: "All memories cleared for this repo.",
|
|
833
|
+
type: "text",
|
|
834
|
+
};
|
|
835
|
+
setCommitted((prev) => [...prev, msg]);
|
|
836
|
+
setAllMessages((prev) => [...prev, msg]);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
|
|
528
840
|
const userMsg: Message = { role: "user", content: text, type: "text" };
|
|
529
841
|
const nextAll = [...allMessages, userMsg];
|
|
530
842
|
setCommitted((prev) => [...prev, userMsg]);
|
|
531
843
|
setAllMessages(nextAll);
|
|
532
844
|
toolResultCache.current.clear();
|
|
533
845
|
|
|
534
|
-
//
|
|
846
|
+
// Track input history for up/down navigation
|
|
847
|
+
inputHistoryRef.current = [
|
|
848
|
+
text,
|
|
849
|
+
...inputHistoryRef.current.filter((m) => m !== text),
|
|
850
|
+
].slice(0, 50);
|
|
851
|
+
historyIndexRef.current = -1;
|
|
852
|
+
|
|
853
|
+
// Auto-name chat on first user message
|
|
854
|
+
if (!chatName) {
|
|
855
|
+
const name =
|
|
856
|
+
getChatNameSuggestions(nextAll)[0] ??
|
|
857
|
+
`chat-${new Date().toISOString().slice(0, 10)}`;
|
|
858
|
+
updateChatName(name);
|
|
859
|
+
setRecentChats((prev) =>
|
|
860
|
+
[name, ...prev.filter((n) => n !== name)].slice(0, 10),
|
|
861
|
+
);
|
|
862
|
+
saveChat(name, repoPath, nextAll);
|
|
863
|
+
}
|
|
864
|
+
|
|
535
865
|
const abort = new AbortController();
|
|
536
866
|
abortControllerRef.current = abort;
|
|
537
867
|
|
|
@@ -544,7 +874,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
544
874
|
useInput((input, key) => {
|
|
545
875
|
if (showTimeline) return;
|
|
546
876
|
|
|
547
|
-
// ESC while thinking → abort the in-flight request and go idle
|
|
548
877
|
if (stage.type === "thinking" && key.escape) {
|
|
549
878
|
abortControllerRef.current?.abort();
|
|
550
879
|
abortControllerRef.current = null;
|
|
@@ -558,6 +887,26 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
558
887
|
return;
|
|
559
888
|
}
|
|
560
889
|
|
|
890
|
+
if (key.upArrow && inputHistoryRef.current.length > 0) {
|
|
891
|
+
const next = Math.min(
|
|
892
|
+
historyIndexRef.current + 1,
|
|
893
|
+
inputHistoryRef.current.length - 1,
|
|
894
|
+
);
|
|
895
|
+
historyIndexRef.current = next;
|
|
896
|
+
setInputValue(inputHistoryRef.current[next]!);
|
|
897
|
+
setInputKey((k) => k + 1);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (key.downArrow) {
|
|
902
|
+
const next = historyIndexRef.current - 1;
|
|
903
|
+
historyIndexRef.current = next;
|
|
904
|
+
const val = next < 0 ? "" : inputHistoryRef.current[next]!;
|
|
905
|
+
setInputValue(val);
|
|
906
|
+
setInputKey((k) => k + 1);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
|
|
561
910
|
if (key.tab && inputValue.startsWith("/")) {
|
|
562
911
|
const q = inputValue.toLowerCase();
|
|
563
912
|
const match = COMMANDS.find((c) => c.cmd.startsWith(q));
|
|
@@ -582,7 +931,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
582
931
|
?.replace(/\.git$/, "") ?? "repo";
|
|
583
932
|
const destPath = path.join(os.tmpdir(), repoName);
|
|
584
933
|
const fileCount = walkDir(destPath).length;
|
|
585
|
-
|
|
934
|
+
appendMemory({
|
|
586
935
|
kind: "url-fetched",
|
|
587
936
|
detail: repoUrl,
|
|
588
937
|
summary: `Cloned ${repoName} — ${fileCount} files`,
|
|
@@ -663,13 +1012,11 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
663
1012
|
if (key.return || key.escape) {
|
|
664
1013
|
if (stage.type === "clone-done") {
|
|
665
1014
|
const repoName = stage.repoUrl.split("/").pop() ?? "repo";
|
|
666
|
-
|
|
667
1015
|
const summaryMsg: Message = {
|
|
668
1016
|
role: "assistant",
|
|
669
1017
|
type: "text",
|
|
670
1018
|
content: `Cloned **${repoName}** (${stage.fileCount} files) to \`${stage.destPath}\`.\n\nAsk me anything about it — I can read files, explain how it works, or suggest improvements.`,
|
|
671
1019
|
};
|
|
672
|
-
|
|
673
1020
|
const contextMsg: Message = {
|
|
674
1021
|
role: "assistant",
|
|
675
1022
|
type: "tool",
|
|
@@ -720,7 +1067,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
720
1067
|
const msg = allMessages[pendingMsgIndex];
|
|
721
1068
|
if (msg?.type === "plan") {
|
|
722
1069
|
setCommitted((prev) => [...prev, { ...msg, applied: false }]);
|
|
723
|
-
|
|
1070
|
+
appendMemory({
|
|
724
1071
|
kind: "code-skipped",
|
|
725
1072
|
detail: msg.patches
|
|
726
1073
|
.map((p: { path: string }) => p.path)
|
|
@@ -737,7 +1084,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
737
1084
|
if (key.return || input === "a" || input === "A") {
|
|
738
1085
|
try {
|
|
739
1086
|
applyPatches(repoPath, stage.patches);
|
|
740
|
-
|
|
1087
|
+
appendMemory({
|
|
741
1088
|
kind: "code-applied",
|
|
742
1089
|
detail: stage.patches.map((p) => p.path).join(", "),
|
|
743
1090
|
summary: `Applied changes to ${stage.patches.length} file(s)`,
|
|
@@ -788,7 +1135,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
|
|
|
788
1135
|
.catch(() => walkDir(repoPath))
|
|
789
1136
|
.then((fileTree) => {
|
|
790
1137
|
const importantFiles = readImportantFiles(repoPath, fileTree);
|
|
791
|
-
const historySummary =
|
|
1138
|
+
const historySummary = buildMemorySummary(repoPath);
|
|
792
1139
|
const lensFile = readLensFile(repoPath);
|
|
793
1140
|
const lensContext = lensFile
|
|
794
1141
|
? `
|
|
@@ -892,18 +1239,21 @@ Suggestions: ${lensFile.suggestions.slice(0, 3).join("; ")}`
|
|
|
892
1239
|
{inputValue.startsWith("/") && (
|
|
893
1240
|
<CommandPalette
|
|
894
1241
|
query={inputValue}
|
|
895
|
-
onSelect={(cmd) =>
|
|
896
|
-
|
|
897
|
-
}}
|
|
1242
|
+
onSelect={(cmd) => setInputValue(cmd)}
|
|
1243
|
+
recentChats={recentChats}
|
|
898
1244
|
/>
|
|
899
1245
|
)}
|
|
900
1246
|
<InputBox
|
|
901
1247
|
value={inputValue}
|
|
902
|
-
onChange={
|
|
1248
|
+
onChange={(v) => {
|
|
1249
|
+
historyIndexRef.current = -1;
|
|
1250
|
+
setInputValue(v);
|
|
1251
|
+
}}
|
|
903
1252
|
onSubmit={(val) => {
|
|
904
1253
|
if (val.trim()) sendMessage(val.trim());
|
|
905
1254
|
setInputValue("");
|
|
906
1255
|
}}
|
|
1256
|
+
inputKey={inputKey}
|
|
907
1257
|
/>
|
|
908
1258
|
<ShortcutBar autoApprove={autoApprove} />
|
|
909
1259
|
</Box>
|