@ridit/lens 0.3.7 → 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/dist/index.mjs +725 -460
- 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 +19 -13
- package/src/components/chat/ChatRunner.tsx +53 -13
- package/src/components/chat/TextArea.tsx +10 -9
- package/src/components/chat/hooks/useChat.ts +373 -273
- package/src/components/chat/hooks/useCommandHandlers.ts +0 -4
- package/src/components/repo/RepoAnalysis.tsx +3 -3
- package/src/components/task/TaskRunner.tsx +3 -3
- package/src/index.tsx +13 -2
- package/src/prompts/system.ts +16 -17
- package/src/types/chat.ts +3 -1
- package/src/utils/chat.ts +57 -4
- package/src/utils/intentClassifier.ts +0 -15
- package/src/utils/thinking.tsx +64 -0
- package/src/utils/tools/builtins.ts +27 -12
|
@@ -31,7 +31,16 @@ import {
|
|
|
31
31
|
type ChatResult,
|
|
32
32
|
} from "../../../utils/chat";
|
|
33
33
|
|
|
34
|
-
|
|
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) {
|
|
35
44
|
const [stage, setStage] = useState<ChatStage>({ type: "picking-provider" });
|
|
36
45
|
const [committed, setCommitted] = useState<Message[]>([]);
|
|
37
46
|
const [provider, setProvider] = useState<Provider | null>(null);
|
|
@@ -41,8 +50,8 @@ export function useChat(repoPath: string) {
|
|
|
41
50
|
const [clonedUrls, setClonedUrls] = useState<Set<string>>(new Set());
|
|
42
51
|
const [showTimeline, setShowTimeline] = useState(false);
|
|
43
52
|
const [showReview, setShowReview] = useState(false);
|
|
44
|
-
const [autoApprove, setAutoApprove] = useState(
|
|
45
|
-
const [forceApprove, setForceApprove] = useState(
|
|
53
|
+
const [autoApprove, setAutoApprove] = useState(autoForce);
|
|
54
|
+
const [forceApprove, setForceApprove] = useState(autoForce);
|
|
46
55
|
const [showForceWarning, setShowForceWarning] = useState(false);
|
|
47
56
|
const [chatName, setChatName] = useState<string | null>(null);
|
|
48
57
|
const [recentChats, setRecentChats] = useState<string[]>([]);
|
|
@@ -53,6 +62,7 @@ export function useChat(repoPath: string) {
|
|
|
53
62
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
54
63
|
const toolResultCache = useRef<Map<string, string>>(new Map());
|
|
55
64
|
const batchApprovedRef = useRef(false);
|
|
65
|
+
const forceApproveRef = useRef(autoForce);
|
|
56
66
|
|
|
57
67
|
const updateChatName = (name: string) => {
|
|
58
68
|
chatNameRef.current = name;
|
|
@@ -65,6 +75,9 @@ export function useChat(repoPath: string) {
|
|
|
65
75
|
React.useEffect(() => {
|
|
66
76
|
systemPromptRef.current = systemPrompt;
|
|
67
77
|
}, [systemPrompt]);
|
|
78
|
+
React.useEffect(() => {
|
|
79
|
+
forceApproveRef.current = forceApprove;
|
|
80
|
+
}, [forceApprove]);
|
|
68
81
|
|
|
69
82
|
React.useEffect(() => {
|
|
70
83
|
const chats = listChats(repoPath);
|
|
@@ -77,6 +90,13 @@ export function useChat(repoPath: string) {
|
|
|
77
90
|
}
|
|
78
91
|
}, [allMessages]);
|
|
79
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
|
+
|
|
80
100
|
const handleError = (currentAll: Message[]) => (err: unknown) => {
|
|
81
101
|
batchApprovedRef.current = false;
|
|
82
102
|
if (err instanceof Error && err.name === "AbortError") {
|
|
@@ -88,41 +108,132 @@ export function useChat(repoPath: string) {
|
|
|
88
108
|
content: `Error: ${err instanceof Error ? err.message : "Something went wrong"}`,
|
|
89
109
|
type: "text",
|
|
90
110
|
};
|
|
91
|
-
|
|
92
|
-
setCommitted((prev) => [...prev, errMsg]);
|
|
111
|
+
pushMsg(errMsg, currentAll);
|
|
93
112
|
setStage({ type: "idle" });
|
|
94
113
|
};
|
|
95
114
|
|
|
96
|
-
const
|
|
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 };
|
|
97
125
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
for (
|
|
101
|
-
if (
|
|
102
|
-
|
|
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;
|
|
139
|
+
|
|
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
|
+
}
|
|
103
156
|
}
|
|
104
|
-
// Check unclosed fenced code blocks (```tool\n... without closing ```)
|
|
105
|
-
const fences = text.match(/```/g);
|
|
106
|
-
if (fences && fences.length % 2 !== 0) return true;
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
157
|
|
|
110
|
-
|
|
158
|
+
return { text: "", truncated: false };
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const MAX_CONTINUATIONS = 3;
|
|
162
|
+
|
|
163
|
+
const handleTruncation = async (
|
|
164
|
+
raw: string,
|
|
165
|
+
currentAll: Message[],
|
|
166
|
+
signal: AbortSignal,
|
|
167
|
+
depth: number,
|
|
168
|
+
): Promise<{ text: string; messages: Message[] } | null> => {
|
|
169
|
+
if (depth >= MAX_CONTINUATIONS) return null;
|
|
170
|
+
|
|
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
|
+
};
|
|
194
|
+
|
|
195
|
+
const processMemoryTags = (raw: string): string => {
|
|
196
|
+
const addMatches = [
|
|
197
|
+
...raw.matchAll(/<memory-add>([\s\S]*?)<\/memory-add>/g),
|
|
198
|
+
];
|
|
199
|
+
const delMatches = [
|
|
200
|
+
...raw.matchAll(/<memory-delete>([\s\S]*?)<\/memory-delete>/g),
|
|
201
|
+
];
|
|
202
|
+
for (const m of addMatches) {
|
|
203
|
+
const content = m[1]!.trim();
|
|
204
|
+
if (content) addMemory(content, repoPath);
|
|
205
|
+
}
|
|
206
|
+
for (const m of delMatches) {
|
|
207
|
+
const id = m[1]!.trim();
|
|
208
|
+
if (id) deleteMemory(id, repoPath);
|
|
209
|
+
}
|
|
210
|
+
return raw
|
|
211
|
+
.replace(/<memory-add>[\s\S]*?<\/memory-add>/g, "")
|
|
212
|
+
.replace(/<memory-delete>[\s\S]*?<\/memory-delete>/g, "")
|
|
213
|
+
.trim();
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const processResponse = async (
|
|
111
217
|
raw: string,
|
|
112
218
|
currentAll: Message[],
|
|
113
219
|
signal: AbortSignal,
|
|
114
220
|
truncated = false,
|
|
115
|
-
|
|
116
|
-
) => {
|
|
221
|
+
continuationDepth = 0,
|
|
222
|
+
): Promise<void> => {
|
|
117
223
|
if (signal.aborted) {
|
|
118
224
|
batchApprovedRef.current = false;
|
|
119
225
|
setStage({ type: "idle" });
|
|
120
226
|
return;
|
|
121
227
|
}
|
|
122
228
|
|
|
123
|
-
if (truncated ||
|
|
124
|
-
|
|
125
|
-
|
|
229
|
+
if (truncated || hasUnclosedToolTag(raw)) {
|
|
230
|
+
const cont = await handleTruncation(
|
|
231
|
+
raw,
|
|
232
|
+
currentAll,
|
|
233
|
+
signal,
|
|
234
|
+
continuationDepth,
|
|
235
|
+
);
|
|
236
|
+
if (!cont) {
|
|
126
237
|
batchApprovedRef.current = false;
|
|
127
238
|
const msg: Message = {
|
|
128
239
|
role: "assistant",
|
|
@@ -131,110 +242,116 @@ export function useChat(repoPath: string) {
|
|
|
131
242
|
"(response was empty after multiple continuation attempts)",
|
|
132
243
|
type: "text",
|
|
133
244
|
};
|
|
134
|
-
|
|
135
|
-
setCommitted((prev) => [...prev, msg]);
|
|
245
|
+
pushMsg(msg, currentAll);
|
|
136
246
|
setStage({ type: "idle" });
|
|
137
247
|
return;
|
|
138
248
|
}
|
|
249
|
+
return processResponse(
|
|
250
|
+
cont.text,
|
|
251
|
+
cont.messages,
|
|
252
|
+
signal,
|
|
253
|
+
false,
|
|
254
|
+
continuationDepth + 1,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
139
257
|
|
|
140
|
-
|
|
141
|
-
const partialMsg: Message = {
|
|
142
|
-
role: "assistant",
|
|
143
|
-
content: raw,
|
|
144
|
-
type: "text",
|
|
145
|
-
};
|
|
146
|
-
const nudgeMsg: Message = {
|
|
147
|
-
role: "user",
|
|
148
|
-
content:
|
|
149
|
-
"Your response was cut off. Please continue exactly from where you left off.",
|
|
150
|
-
type: "text",
|
|
151
|
-
};
|
|
152
|
-
const withContext = [...currentAll, partialMsg, nudgeMsg];
|
|
258
|
+
const cleanRaw = processMemoryTags(raw);
|
|
153
259
|
|
|
154
|
-
|
|
155
|
-
role: "assistant",
|
|
156
|
-
content: `(response cut off — auto-continuing ${continueCount + 1}/${MAX_AUTO_CONTINUES}…)`,
|
|
157
|
-
type: "text",
|
|
158
|
-
};
|
|
159
|
-
setAllMessages([...currentAll, truncMsg]);
|
|
160
|
-
setCommitted((prev) => [...prev, truncMsg]);
|
|
260
|
+
const parsed = parseResponse(cleanRaw);
|
|
161
261
|
|
|
162
|
-
|
|
163
|
-
|
|
262
|
+
if (parsed.kind === "text") {
|
|
263
|
+
batchApprovedRef.current = false;
|
|
164
264
|
|
|
165
|
-
if (!
|
|
265
|
+
if (!parsed.content.trim()) {
|
|
266
|
+
const stallMsg: Message = {
|
|
267
|
+
role: "assistant",
|
|
268
|
+
content:
|
|
269
|
+
'(no response — try sending "continue" or start a new message)',
|
|
270
|
+
type: "text",
|
|
271
|
+
};
|
|
272
|
+
pushMsg(stallMsg, currentAll);
|
|
166
273
|
setStage({ type: "idle" });
|
|
167
274
|
return;
|
|
168
275
|
}
|
|
169
276
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
withContext,
|
|
177
|
-
nextAbort.signal,
|
|
178
|
-
)
|
|
179
|
-
.then((result: ChatResult) => {
|
|
180
|
-
if (nextAbort.signal.aborted) return;
|
|
181
|
-
processResponse(
|
|
182
|
-
result.text ?? "",
|
|
183
|
-
withContext,
|
|
184
|
-
nextAbort.signal,
|
|
185
|
-
result.truncated,
|
|
186
|
-
continueCount + 1,
|
|
187
|
-
);
|
|
188
|
-
})
|
|
189
|
-
.catch(handleError(withContext));
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
277
|
+
const msg: Message = {
|
|
278
|
+
role: "assistant",
|
|
279
|
+
content: parsed.content,
|
|
280
|
+
type: "text",
|
|
281
|
+
};
|
|
282
|
+
const withMsg = pushMsg(msg, currentAll);
|
|
192
283
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
284
|
+
const lastUserMsg = [...currentAll]
|
|
285
|
+
.reverse()
|
|
286
|
+
.find((m) => m.role === "user");
|
|
287
|
+
const githubUrl = lastUserMsg
|
|
288
|
+
? extractGithubUrl(lastUserMsg.content)
|
|
289
|
+
: null;
|
|
290
|
+
if (githubUrl && !clonedUrls.has(githubUrl)) {
|
|
291
|
+
setTimeout(
|
|
292
|
+
() => setStage({ type: "clone-offer", repoUrl: githubUrl }),
|
|
293
|
+
80,
|
|
294
|
+
);
|
|
295
|
+
} else {
|
|
296
|
+
setStage({ type: "idle" });
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
206
299
|
}
|
|
207
|
-
const cleanRaw = raw
|
|
208
|
-
.replace(/<memory-add>[\s\S]*?<\/memory-add>/g, "")
|
|
209
|
-
.replace(/<memory-delete>[\s\S]*?<\/memory-delete>/g, "")
|
|
210
|
-
.trim();
|
|
211
|
-
|
|
212
|
-
const parsed = parseResponse(cleanRaw);
|
|
213
300
|
|
|
214
301
|
if (parsed.kind === "changes") {
|
|
215
302
|
batchApprovedRef.current = false;
|
|
303
|
+
|
|
216
304
|
if (parsed.patches.length === 0) {
|
|
217
305
|
const msg: Message = {
|
|
218
306
|
role: "assistant",
|
|
219
307
|
content: parsed.content,
|
|
220
308
|
type: "text",
|
|
221
309
|
};
|
|
222
|
-
|
|
223
|
-
setCommitted((prev) => [...prev, msg]);
|
|
310
|
+
pushMsg(msg, currentAll);
|
|
224
311
|
setStage({ type: "idle" });
|
|
225
312
|
return;
|
|
226
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
|
+
|
|
227
342
|
const assistantMsg: Message = {
|
|
228
343
|
role: "assistant",
|
|
229
344
|
content: parsed.content,
|
|
230
345
|
type: "plan",
|
|
231
346
|
patches: parsed.patches,
|
|
347
|
+
diffLines,
|
|
232
348
|
applied: false,
|
|
233
349
|
};
|
|
234
350
|
const withAssistant = [...currentAll, assistantMsg];
|
|
235
351
|
setAllMessages(withAssistant);
|
|
352
|
+
setCommitted((prev) => [...prev, assistantMsg]);
|
|
236
353
|
setPendingMsgIndex(withAssistant.length - 1);
|
|
237
|
-
|
|
354
|
+
|
|
238
355
|
setStage({
|
|
239
356
|
type: "preview",
|
|
240
357
|
patches: parsed.patches,
|
|
@@ -253,8 +370,7 @@ export function useChat(repoPath: string) {
|
|
|
253
370
|
content: parsed.content,
|
|
254
371
|
type: "text",
|
|
255
372
|
};
|
|
256
|
-
|
|
257
|
-
setCommitted((prev) => [...prev, preambleMsg]);
|
|
373
|
+
pushMsg(preambleMsg, currentAll);
|
|
258
374
|
}
|
|
259
375
|
setStage({
|
|
260
376
|
type: "clone-offer",
|
|
@@ -264,215 +380,194 @@ export function useChat(repoPath: string) {
|
|
|
264
380
|
return;
|
|
265
381
|
}
|
|
266
382
|
|
|
267
|
-
if (parsed.kind === "
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const stallMsg: Message = {
|
|
272
|
-
role: "assistant",
|
|
273
|
-
content:
|
|
274
|
-
'(no response — the model may have stalled. Try sending a short follow-up like "continue" or start a new message.)',
|
|
275
|
-
type: "text",
|
|
276
|
-
};
|
|
277
|
-
setAllMessages([...currentAll, stallMsg]);
|
|
278
|
-
setCommitted((prev) => [...prev, stallMsg]);
|
|
383
|
+
if (parsed.kind === "tool") {
|
|
384
|
+
const tool = registry.get(parsed.toolName);
|
|
385
|
+
if (!tool) {
|
|
386
|
+
batchApprovedRef.current = false;
|
|
279
387
|
setStage({ type: "idle" });
|
|
280
388
|
return;
|
|
281
389
|
}
|
|
282
390
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
setCommitted((prev) => [...prev, msg]);
|
|
291
|
-
const lastUserMsg = [...currentAll]
|
|
292
|
-
.reverse()
|
|
293
|
-
.find((m) => m.role === "user");
|
|
294
|
-
const githubUrl = lastUserMsg
|
|
295
|
-
? extractGithubUrl(lastUserMsg.content)
|
|
296
|
-
: null;
|
|
297
|
-
if (githubUrl && !clonedUrls.has(githubUrl)) {
|
|
298
|
-
setTimeout(
|
|
299
|
-
() => setStage({ type: "clone-offer", repoUrl: githubUrl }),
|
|
300
|
-
80,
|
|
301
|
-
);
|
|
302
|
-
} else {
|
|
303
|
-
setStage({ type: "idle" });
|
|
391
|
+
if (parsed.content) {
|
|
392
|
+
const preambleMsg: Message = {
|
|
393
|
+
role: "assistant",
|
|
394
|
+
content: parsed.content,
|
|
395
|
+
type: "text",
|
|
396
|
+
};
|
|
397
|
+
pushMsg(preambleMsg, currentAll);
|
|
304
398
|
}
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const tool = registry.get(parsed.toolName);
|
|
309
|
-
if (!tool) {
|
|
310
|
-
batchApprovedRef.current = false;
|
|
311
|
-
setStage({ type: "idle" });
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
399
|
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
role: "assistant",
|
|
318
|
-
content: parsed.content,
|
|
319
|
-
type: "text",
|
|
320
|
-
};
|
|
321
|
-
setAllMessages([...currentAll, preambleMsg]);
|
|
322
|
-
setCommitted((prev) => [...prev, preambleMsg]);
|
|
323
|
-
}
|
|
400
|
+
const isSafe = tool.safe ?? false;
|
|
401
|
+
const remainder = parsed.remainder;
|
|
324
402
|
|
|
325
|
-
|
|
326
|
-
|
|
403
|
+
const executeAndContinue = async (approved: boolean): Promise<void> => {
|
|
404
|
+
if (approved && remainder) batchApprovedRef.current = true;
|
|
327
405
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
batchApprovedRef.current = true;
|
|
331
|
-
}
|
|
406
|
+
const currentProvider = providerRef.current;
|
|
407
|
+
const currentSystemPrompt = systemPromptRef.current;
|
|
332
408
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
setStage({ type: "idle" });
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
409
|
+
if (!currentProvider) {
|
|
410
|
+
batchApprovedRef.current = false;
|
|
411
|
+
setStage({ type: "idle" });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
341
414
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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"}`;
|
|
362
438
|
}
|
|
363
|
-
} catch (err: unknown) {
|
|
364
|
-
result = `Error: ${err instanceof Error ? err.message : "failed"}`;
|
|
365
439
|
}
|
|
366
440
|
}
|
|
367
|
-
}
|
|
368
441
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
+
}
|
|
452
|
+
|
|
453
|
+
const displayContent = tool.summariseInput
|
|
454
|
+
? String(tool.summariseInput(parsed.input))
|
|
455
|
+
: parsed.rawInput;
|
|
456
|
+
|
|
457
|
+
const toolMsg: Message = {
|
|
458
|
+
role: "assistant",
|
|
459
|
+
type: "tool",
|
|
460
|
+
toolName: parsed.toolName as any,
|
|
461
|
+
content: displayContent,
|
|
375
462
|
result,
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
463
|
+
approved,
|
|
464
|
+
};
|
|
379
465
|
|
|
380
|
-
|
|
381
|
-
? String(tool.summariseInput(parsed.input))
|
|
382
|
-
: parsed.rawInput;
|
|
466
|
+
const withTool = pushMsg(toolMsg, currentAll);
|
|
383
467
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
468
|
+
if (approved && remainder && remainder.length > 0) {
|
|
469
|
+
return processResponse(
|
|
470
|
+
remainder,
|
|
471
|
+
withTool,
|
|
472
|
+
signal,
|
|
473
|
+
false,
|
|
474
|
+
continuationDepth,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
392
477
|
|
|
393
|
-
|
|
394
|
-
setAllMessages(withTool);
|
|
395
|
-
setCommitted((prev) => [...prev, toolMsg]);
|
|
478
|
+
batchApprovedRef.current = false;
|
|
396
479
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
480
|
+
const nextAbort = new AbortController();
|
|
481
|
+
abortControllerRef.current = nextAbort;
|
|
482
|
+
setStage({ type: "thinking" });
|
|
401
483
|
|
|
402
|
-
|
|
484
|
+
try {
|
|
485
|
+
const nextResult = await callNext(withTool, nextAbort.signal);
|
|
403
486
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
maxRetries = 3,
|
|
411
|
-
): Promise<ChatResult> => {
|
|
412
|
-
let currentMessages = messages;
|
|
413
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
414
|
-
if (nextAbort.signal.aborted)
|
|
415
|
-
return { text: "", truncated: false };
|
|
416
|
-
const result = await callChat(
|
|
417
|
-
currentProvider,
|
|
418
|
-
currentSystemPrompt,
|
|
419
|
-
currentMessages,
|
|
420
|
-
nextAbort.signal,
|
|
421
|
-
);
|
|
422
|
-
if (result.text.trim()) return result;
|
|
423
|
-
const nudgeMsg: Message = {
|
|
424
|
-
role: "assistant",
|
|
425
|
-
content: `(model stalled — auto-continuing, attempt ${i + 1}/${maxRetries})`,
|
|
426
|
-
type: "text",
|
|
427
|
-
};
|
|
428
|
-
setCommitted((prev) => [...prev, nudgeMsg]);
|
|
429
|
-
setAllMessages((prev) => [...prev, nudgeMsg]);
|
|
430
|
-
currentMessages = [
|
|
431
|
-
...currentMessages,
|
|
432
|
-
{
|
|
433
|
-
role: "user",
|
|
434
|
-
content:
|
|
435
|
-
"Please continue. Provide your response to the previous tool output.",
|
|
487
|
+
if (nextAbort.signal.aborted) return;
|
|
488
|
+
|
|
489
|
+
if (!nextResult.text.trim()) {
|
|
490
|
+
const stallMsg: Message = {
|
|
491
|
+
role: "assistant",
|
|
492
|
+
content: '(model stopped responding — try sending "continue")',
|
|
436
493
|
type: "text",
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
494
|
+
};
|
|
495
|
+
pushMsg(stallMsg, withTool);
|
|
496
|
+
setStage({ type: "idle" });
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
442
499
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
if (nextAbort.signal.aborted) return;
|
|
446
|
-
processResponse(
|
|
447
|
-
result.text ?? "",
|
|
500
|
+
return processResponse(
|
|
501
|
+
nextResult.text,
|
|
448
502
|
withTool,
|
|
449
503
|
nextAbort.signal,
|
|
450
|
-
|
|
504
|
+
nextResult.truncated,
|
|
451
505
|
);
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
|
|
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;
|
|
455
519
|
|
|
456
|
-
|
|
457
|
-
|
|
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" });
|
|
458
536
|
return;
|
|
459
537
|
}
|
|
460
538
|
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
:
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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));
|
|
476
571
|
};
|
|
477
572
|
|
|
478
573
|
const sendMessage = (
|
|
@@ -509,7 +604,6 @@ export function useChat(repoPath: string) {
|
|
|
509
604
|
/## TOOLS[\s\S]*?(?=\n## (?!TOOLS))/,
|
|
510
605
|
scopedToolsSection + "\n\n",
|
|
511
606
|
);
|
|
512
|
-
|
|
513
607
|
if (sessionSummary) {
|
|
514
608
|
scopedSystemPrompt = scopedSystemPrompt.replace(
|
|
515
609
|
/## CODEBASE/,
|
|
@@ -518,6 +612,7 @@ export function useChat(repoPath: string) {
|
|
|
518
612
|
}
|
|
519
613
|
|
|
520
614
|
setStage({ type: "thinking" });
|
|
615
|
+
|
|
521
616
|
callChat(currentProvider, scopedSystemPrompt, nextAll, abort.signal)
|
|
522
617
|
.then((result: ChatResult) =>
|
|
523
618
|
processResponse(result.text, nextAll, abort.signal, result.truncated),
|
|
@@ -529,6 +624,7 @@ export function useChat(repoPath: string) {
|
|
|
529
624
|
setProvider(p);
|
|
530
625
|
providerRef.current = p;
|
|
531
626
|
setStage({ type: "loading" });
|
|
627
|
+
|
|
532
628
|
fetchFileTree(repoPath)
|
|
533
629
|
.catch(() => walkDir(repoPath))
|
|
534
630
|
.then((fileTree) => {
|
|
@@ -542,13 +638,16 @@ export function useChat(repoPath: string) {
|
|
|
542
638
|
const prompt =
|
|
543
639
|
buildSystemPrompt(importantFiles, historySummary, toolsSection) +
|
|
544
640
|
lensContext;
|
|
641
|
+
|
|
545
642
|
setSystemPrompt(prompt);
|
|
546
643
|
systemPromptRef.current = prompt;
|
|
644
|
+
|
|
547
645
|
const greeting: Message = {
|
|
548
646
|
role: "assistant",
|
|
549
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`,
|
|
550
648
|
type: "text",
|
|
551
649
|
};
|
|
650
|
+
|
|
552
651
|
setCommitted([greeting]);
|
|
553
652
|
setAllMessages([greeting]);
|
|
554
653
|
setStage({ type: "idle" });
|
|
@@ -624,6 +723,7 @@ export function useChat(repoPath: string) {
|
|
|
624
723
|
handleProviderDone,
|
|
625
724
|
abortThinking,
|
|
626
725
|
applyPatchesAndContinue,
|
|
726
|
+
continueAfterChanges,
|
|
627
727
|
skipPatches,
|
|
628
728
|
processResponse,
|
|
629
729
|
handleError,
|