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