@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.
@@ -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
- export function useChat(repoPath: string) {
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(false);
42
- const [forceApprove, setForceApprove] = useState(false);
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
- setAllMessages([...currentAll, errMsg]);
89
- setCommitted((prev) => [...prev, errMsg]);
111
+ pushMsg(errMsg, currentAll);
90
112
  setStage({ type: "idle" });
91
113
  };
92
114
 
93
- const TOOL_TAG_NAMES = [
94
- "shell",
95
- "fetch",
96
- "read-file",
97
- "read-folder",
98
- "grep",
99
- "write-file",
100
- "delete-file",
101
- "delete-folder",
102
- "open-url",
103
- "generate-pdf",
104
- "search",
105
- "clone",
106
- "changes",
107
- ];
108
-
109
- function isLikelyTruncated(text: string): boolean {
110
- return TOOL_TAG_NAMES.some(
111
- (tag) => text.includes(`<${tag}>`) && !text.includes(`</${tag}>`),
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
- const processResponse = (
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
- if (signal.aborted) {
121
- batchApprovedRef.current = false;
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
- if (isLikelyTruncated(raw)) {
127
- const truncMsg: Message = {
128
- role: "assistant",
129
- content:
130
- "(response cut off — the model hit its output limit mid-tool-call. Try asking it to continue, or simplify the request.)",
131
- type: "text",
132
- };
133
- setAllMessages([...currentAll, truncMsg]);
134
- setCommitted((prev) => [...prev, truncMsg]);
135
- setStage({ type: "idle" });
136
- return;
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
- const memAddMatches = [
195
+ const processMemoryTags = (raw: string): string => {
196
+ const addMatches = [
140
197
  ...raw.matchAll(/<memory-add>([\s\S]*?)<\/memory-add>/g),
141
198
  ];
142
- const memDelMatches = [
199
+ const delMatches = [
143
200
  ...raw.matchAll(/<memory-delete>([\s\S]*?)<\/memory-delete>/g),
144
201
  ];
145
- for (const match of memAddMatches) {
146
- const content = match[1]!.trim();
202
+ for (const m of addMatches) {
203
+ const content = m[1]!.trim();
147
204
  if (content) addMemory(content, repoPath);
148
205
  }
149
- for (const match of memDelMatches) {
150
- const id = match[1]!.trim();
206
+ for (const m of delMatches) {
207
+ const id = m[1]!.trim();
151
208
  if (id) deleteMemory(id, repoPath);
152
209
  }
153
- const cleanRaw = raw
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
- const parsed = parseResponse(cleanRaw);
159
-
160
- if (parsed.kind === "changes") {
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
- if (parsed.patches.length === 0) {
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: parsed.content,
240
+ content:
241
+ raw.trim() ||
242
+ "(response was empty after multiple continuation attempts)",
166
243
  type: "text",
167
244
  };
168
- setAllMessages([...currentAll, msg]);
169
- setCommitted((prev) => [...prev, msg]);
245
+ pushMsg(msg, currentAll);
170
246
  setStage({ type: "idle" });
171
247
  return;
172
248
  }
173
- const assistantMsg: Message = {
174
- role: "assistant",
175
- content: parsed.content,
176
- type: "plan",
177
- patches: parsed.patches,
178
- applied: false,
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
- if (parsed.kind === "clone") {
195
- batchApprovedRef.current = false;
196
- if (parsed.content) {
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 — the model may have stalled. Try sending a short follow-up like "continue" or start a new message.)',
269
+ '(no response — try sending "continue" or start a new message)',
221
270
  type: "text",
222
271
  };
223
- setAllMessages([...currentAll, stallMsg]);
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 = [...currentAll, msg];
235
- setAllMessages(withMsg);
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
- const tool = registry.get(parsed.toolName);
255
- if (!tool) {
301
+ if (parsed.kind === "changes") {
256
302
  batchApprovedRef.current = false;
257
- setStage({ type: "idle" });
258
- return;
259
- }
260
303
 
261
- if (parsed.content) {
262
- const preambleMsg: Message = {
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: "text",
345
+ type: "plan",
346
+ patches: parsed.patches,
347
+ diffLines,
348
+ applied: false,
266
349
  };
267
- setAllMessages([...currentAll, preambleMsg]);
268
- setCommitted((prev) => [...prev, preambleMsg]);
269
- }
350
+ const withAssistant = [...currentAll, assistantMsg];
351
+ setAllMessages(withAssistant);
352
+ setCommitted((prev) => [...prev, assistantMsg]);
353
+ setPendingMsgIndex(withAssistant.length - 1);
270
354
 
271
- const remainder = parsed.remainder;
272
- const isSafe = tool.safe ?? false;
355
+ setStage({
356
+ type: "preview",
357
+ patches: parsed.patches,
358
+ diffLines,
359
+ scrollOffset: 0,
360
+ pendingMessages: currentAll,
361
+ });
362
+ return;
363
+ }
273
364
 
274
- const executeAndContinue = async (approved: boolean) => {
275
- if (approved && remainder) {
276
- batchApprovedRef.current = true;
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
- const currentProvider = providerRef.current;
280
- const currentSystemPrompt = systemPromptRef.current;
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
- let result = "(denied by user)";
289
-
290
- if (approved) {
291
- const cacheKey = isSafe
292
- ? `${parsed.toolName}:${parsed.rawInput}`
293
- : null;
294
- if (cacheKey && toolResultCache.current.has(cacheKey)) {
295
- result =
296
- toolResultCache.current.get(cacheKey)! +
297
- "\n\n[NOTE: This result was already retrieved earlier. Do not request it again.]";
298
- } else {
299
- try {
300
- setStage({ type: "thinking" });
301
- const toolResult = await tool.execute(parsed.input, {
302
- repoPath,
303
- messages: currentAll,
304
- });
305
- result = toolResult.value;
306
- if (cacheKey && toolResult.kind === "text") {
307
- toolResultCache.current.set(cacheKey, result);
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
- if (approved && !result.startsWith("Error:")) {
316
- appendMemory({
317
- kind: "shell-run",
318
- detail: tool.summariseInput
319
- ? String(tool.summariseInput(parsed.input))
320
- : parsed.rawInput,
321
- summary: result.split("\n")[0]?.slice(0, 120) ?? "",
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
- const displayContent = tool.summariseInput
326
- ? String(tool.summariseInput(parsed.input))
327
- : parsed.rawInput;
453
+ const displayContent = tool.summariseInput
454
+ ? String(tool.summariseInput(parsed.input))
455
+ : parsed.rawInput;
328
456
 
329
- const toolMsg: Message = {
330
- role: "assistant",
331
- type: "tool",
332
- toolName: parsed.toolName as any,
333
- content: displayContent,
334
- result,
335
- approved,
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
- const withTool = [...currentAll, toolMsg];
339
- setAllMessages(withTool);
340
- setCommitted((prev) => [...prev, toolMsg]);
466
+ const withTool = pushMsg(toolMsg, currentAll);
341
467
 
342
- if (approved && remainder && remainder.length > 0) {
343
- processResponse(remainder, withTool, signal);
344
- return;
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
- batchApprovedRef.current = false;
478
+ batchApprovedRef.current = false;
348
479
 
349
- const nextAbort = new AbortController();
350
- abortControllerRef.current = nextAbort;
351
- setStage({ type: "thinking" });
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
- if (!r.trim()) {
357
- const nudged: Message[] = [
358
- ...withTool,
359
- { role: "user", content: "Please continue.", type: "text" },
360
- ];
361
- return callChat(
362
- currentProvider,
363
- currentSystemPrompt,
364
- nudged,
365
- nextAbort.signal,
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
- if (forceApprove || (autoApprove && isSafe) || batchApprovedRef.current) {
378
- executeAndContinue(true);
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 permLabel = tool.permissionLabel ?? tool.name;
383
- const permValue = tool.summariseInput
384
- ? String(tool.summariseInput(parsed.input))
385
- : parsed.rawInput;
386
-
387
- setStage({
388
- type: "permission",
389
- tool: {
390
- type: parsed.toolName as any,
391
- _display: permValue,
392
- _label: permLabel,
393
- } as any,
394
- pendingMessages: currentAll,
395
- resolve: executeAndContinue,
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
- const scopedSystemPrompt = currentSystemPrompt.replace(
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((raw: string) => processResponse(raw, nextAll, abort.signal))
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
- appendMemory({
481
- kind: "code-applied",
482
- detail: patches.map((p) => p.path).join(", "),
483
- summary: `Applied changes to ${patches.length} file(s)`,
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
- appendMemory({
492
- kind: "code-skipped",
493
- detail: patches.map((p: { path: string }) => p.path).join(", "),
494
- summary: `Skipped changes to ${patches.length} file(s)`,
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,