@ridit/lens 0.2.4 → 0.2.6

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.
Files changed (41) hide show
  1. package/LENS.md +32 -68
  2. package/README.md +91 -0
  3. package/addons/README.md +3 -0
  4. package/addons/run-tests.js +127 -0
  5. package/dist/index.mjs +226459 -2638
  6. package/package.json +13 -4
  7. package/src/colors.ts +5 -0
  8. package/src/commands/commit.tsx +686 -0
  9. package/src/commands/provider.tsx +36 -22
  10. package/src/components/__tests__/Header.test.tsx +9 -0
  11. package/src/components/chat/ChatMessage.tsx +6 -6
  12. package/src/components/chat/ChatOverlays.tsx +20 -10
  13. package/src/components/chat/ChatRunner.tsx +197 -31
  14. package/src/components/provider/ApiKeyStep.tsx +77 -121
  15. package/src/components/provider/ModelStep.tsx +35 -20
  16. package/src/components/{repo → provider}/ProviderPicker.tsx +1 -1
  17. package/src/components/provider/ProviderTypeStep.tsx +12 -5
  18. package/src/components/provider/RemoveProviderStep.tsx +7 -8
  19. package/src/components/repo/RepoAnalysis.tsx +1 -1
  20. package/src/components/task/TaskRunner.tsx +1 -1
  21. package/src/components/timeline/CommitDetail.tsx +2 -4
  22. package/src/components/timeline/CommitList.tsx +2 -14
  23. package/src/components/timeline/TimelineChat.tsx +1 -2
  24. package/src/components/timeline/TimelineRunner.tsx +506 -423
  25. package/src/index.tsx +38 -0
  26. package/src/prompts/fewshot.ts +144 -47
  27. package/src/prompts/system.ts +25 -21
  28. package/src/tools/chart.ts +210 -0
  29. package/src/tools/convert-image.ts +312 -0
  30. package/src/tools/files.ts +1 -9
  31. package/src/tools/git.ts +577 -0
  32. package/src/tools/index.ts +17 -13
  33. package/src/tools/pdf.ts +136 -78
  34. package/src/tools/view-image.ts +335 -0
  35. package/src/tools/web.ts +0 -4
  36. package/src/utils/addons/loadAddons.ts +6 -3
  37. package/src/utils/chat.ts +38 -23
  38. package/src/utils/thinking.tsx +275 -162
  39. package/src/utils/tools/builtins.ts +39 -32
  40. package/src/utils/tools/registry.ts +0 -14
  41. package/tsconfig.json +2 -2
@@ -8,6 +8,7 @@ import { ApiKeyStep } from "../components/provider/ApiKeyStep";
8
8
  import { ModelStep } from "../components/provider/ModelStep";
9
9
  import { RemoveProviderStep } from "../components/provider/RemoveProviderStep";
10
10
  import type { Provider, ProviderType } from "../types/config";
11
+ import { ACCENT, CYAN, GREEN, TEXT } from "../colors";
11
12
 
12
13
  type InitStage =
13
14
  | { type: "menu" }
@@ -34,6 +35,8 @@ export const InitCommand = () => {
34
35
  const [menuIndex, setMenuIndex] = useState(0);
35
36
 
36
37
  const pushStep = (label: string) => setCompletedSteps((s) => [...s, label]);
38
+ const popStep = () => setCompletedSteps((s) => s.slice(0, -1));
39
+ const popSteps = (n: number) => setCompletedSteps((s) => s.slice(0, -n));
37
40
 
38
41
  useInput((input, key) => {
39
42
  if (stage.type !== "menu") return;
@@ -52,21 +55,18 @@ export const InitCommand = () => {
52
55
  return (
53
56
  <Box flexDirection="column" gap={1}>
54
57
  {completedSteps.map((s, i) => (
55
- <Text key={i} color="green">
56
- {figures.tick} {s}
58
+ <Text key={i} color={GREEN}>
59
+ {figures.arrowRight} {s}
57
60
  </Text>
58
61
  ))}
59
- <Text bold color="cyan">
60
- Lens — provider setup
61
- </Text>
62
62
  {config.providers.length > 0 && (
63
63
  <Text color="gray">
64
- {figures.info} {config.providers.length} provider(s) configured
64
+ {config.providers.length} provider(s) configured
65
65
  </Text>
66
66
  )}
67
67
  {MENU_OPTIONS.map((opt, i) => (
68
68
  <Box key={opt.action} marginLeft={1}>
69
- <Text color={i === menuIndex ? "cyan" : "white"}>
69
+ <Text color={i === menuIndex ? ACCENT : "white"}>
70
70
  {i === menuIndex ? figures.arrowRight : " "}
71
71
  {" "}
72
72
  {opt.label}
@@ -82,8 +82,8 @@ export const InitCommand = () => {
82
82
  return (
83
83
  <Box flexDirection="column" gap={1}>
84
84
  {completedSteps.map((s, i) => (
85
- <Text key={i} color="green">
86
- {figures.tick} {s}
85
+ <Text key={i} color={GREEN}>
86
+ {figures.arrowRight} {s}
87
87
  </Text>
88
88
  ))}
89
89
  <RemoveProviderStep onDone={() => setStage({ type: "menu" })} />
@@ -95,8 +95,8 @@ export const InitCommand = () => {
95
95
  return (
96
96
  <Box flexDirection="column" gap={1}>
97
97
  {completedSteps.map((s, i) => (
98
- <Text key={i} color="green">
99
- {figures.tick} {s}
98
+ <Text key={i} color={GREEN}>
99
+ {figures.arrowRight} {s}
100
100
  </Text>
101
101
  ))}
102
102
  <ProviderTypeStep
@@ -104,6 +104,7 @@ export const InitCommand = () => {
104
104
  pushStep(`Provider: ${providerType}`);
105
105
  setStage({ type: "api-key", providerType });
106
106
  }}
107
+ onBack={() => setStage({ type: "menu" })}
107
108
  />
108
109
  </Box>
109
110
  );
@@ -113,8 +114,8 @@ export const InitCommand = () => {
113
114
  return (
114
115
  <Box flexDirection="column" gap={1}>
115
116
  {completedSteps.map((s, i) => (
116
- <Text key={i} color="green">
117
- {figures.tick} {s}
117
+ <Text key={i} color={GREEN}>
118
+ {figures.arrowRight} {s}
118
119
  </Text>
119
120
  ))}
120
121
  <ApiKeyStep
@@ -150,6 +151,10 @@ export const InitCommand = () => {
150
151
  });
151
152
  }
152
153
  }}
154
+ onBack={() => {
155
+ popStep();
156
+ setStage({ type: "provider-type" });
157
+ }}
153
158
  />
154
159
  </Box>
155
160
  );
@@ -159,8 +164,8 @@ export const InitCommand = () => {
159
164
  return (
160
165
  <Box flexDirection="column" gap={1}>
161
166
  {completedSteps.map((s, i) => (
162
- <Text key={i} color="green">
163
- {figures.tick} {s}
167
+ <Text key={i} color={GREEN}>
168
+ {figures.arrowRight} {s}
164
169
  </Text>
165
170
  ))}
166
171
  <ApiKeyStep
@@ -174,6 +179,10 @@ export const InitCommand = () => {
174
179
  baseUrl: baseUrl as string,
175
180
  });
176
181
  }}
182
+ onBack={() => {
183
+ popStep();
184
+ setStage({ type: "api-key", providerType: stage.providerType });
185
+ }}
177
186
  />
178
187
  </Box>
179
188
  );
@@ -183,8 +192,8 @@ export const InitCommand = () => {
183
192
  return (
184
193
  <Box flexDirection="column" gap={1}>
185
194
  {completedSteps.map((s, i) => (
186
- <Text key={i} color="green">
187
- {figures.tick} {s}
195
+ <Text key={i} color={GREEN}>
196
+ {figures.arrowRight} {s}
188
197
  </Text>
189
198
  ))}
190
199
  <ModelStep
@@ -202,6 +211,10 @@ export const InitCommand = () => {
202
211
  pushStep(`Model: ${model}`);
203
212
  setStage({ type: "done", provider });
204
213
  }}
214
+ onBack={() => {
215
+ popStep();
216
+ setStage({ type: "api-key", providerType: stage.providerType });
217
+ }}
205
218
  />
206
219
  </Box>
207
220
  );
@@ -210,14 +223,15 @@ export const InitCommand = () => {
210
223
  return (
211
224
  <Box flexDirection="column" gap={1}>
212
225
  {completedSteps.map((s, i) => (
213
- <Text key={i} color="green">
214
- {figures.tick} {s}
226
+ <Text key={i} color={GREEN}>
227
+ {figures.arrowRight} {s}
215
228
  </Text>
216
229
  ))}
217
- <Text color="green">{figures.tick} Provider configured successfully</Text>
230
+ <Text color={GREEN}>
231
+ {figures.arrowRight} Provider configured successfully
232
+ </Text>
218
233
  <Text color="gray">
219
- {figures.info} Run <Text color="cyan">lens init</Text> again to manage
220
- providers.
234
+ Run <Text color={CYAN}>lens provider</Text> again to manage providers.
221
235
  </Text>
222
236
  </Box>
223
237
  );
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import Header from '../Header';
4
+
5
+ describe('Header', () => {
6
+ it('renders without crashing', () => {
7
+ render(<Header />);
8
+ });
9
+ });
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
- import { ACCENT } from "../../colors";
3
+ import { ACCENT, GREEN, RED } from "../../colors";
4
4
  import type { Message } from "../../types/chat";
5
5
 
6
6
  function InlineText({ text }: { text: string }) {
@@ -149,11 +149,11 @@ export function StaticMessage({ msg }: { msg: Message }) {
149
149
  return (
150
150
  <Box flexDirection="column" marginBottom={1}>
151
151
  <Box gap={1}>
152
- <Text color={msg.approved ? ACCENT : "red"}>{icon}</Text>
153
- <Text color={msg.approved ? "gray" : "red"} dimColor={!msg.approved}>
152
+ <Text color={msg.approved ? ACCENT : RED}>{icon}</Text>
153
+ <Text color={msg.approved ? "gray" : RED} dimColor={!msg.approved}>
154
154
  {label}
155
155
  </Text>
156
- {!msg.approved && <Text color="red">denied</Text>}
156
+ {!msg.approved && <Text color={RED}>denied</Text>}
157
157
  </Box>
158
158
  {msg.approved && msg.result && (
159
159
  <Box marginLeft={2}>
@@ -175,10 +175,10 @@ export function StaticMessage({ msg }: { msg: Message }) {
175
175
  <MessageBody content={msg.content} />
176
176
  </Box>
177
177
  <Box marginLeft={2} gap={1}>
178
- <Text color={msg.applied ? "green" : "gray"}>
178
+ <Text color={msg.applied ? GREEN : "gray"}>
179
179
  {msg.applied ? "✓" : "·"}
180
180
  </Text>
181
- <Text color={msg.applied ? "green" : "gray"} dimColor={!msg.applied}>
181
+ <Text color={msg.applied ? GREEN : "gray"} dimColor={!msg.applied}>
182
182
  {msg.applied ? "changes applied" : "changes skipped"}
183
183
  </Text>
184
184
  </Box>
@@ -2,7 +2,7 @@ import React from "react";
2
2
  import { Box, Static, Text } from "ink";
3
3
  import Spinner from "ink-spinner";
4
4
  import TextInput from "ink-text-input";
5
- import { ACCENT } from "../../colors";
5
+ import { ACCENT, GREEN, RED } from "../../colors";
6
6
  import { DiffViewer } from "../repo/DiffViewer";
7
7
  import { StaticMessage } from "./ChatMessage";
8
8
  import type { DiffLine, FilePatch } from "../repo/DiffViewer";
@@ -186,15 +186,25 @@ export function TypewriterText({
186
186
  return <Text color={color}>{displayed}</Text>;
187
187
  }
188
188
 
189
- export function ShortcutBar({ autoApprove }: { autoApprove?: boolean }) {
189
+ export function ShortcutBar({
190
+ autoApprove,
191
+ forceApprove,
192
+ }: {
193
+ autoApprove?: boolean;
194
+ forceApprove?: boolean;
195
+ }) {
190
196
  return (
191
197
  <Box gap={3} marginTop={0}>
192
198
  <Text color="gray" dimColor>
193
199
  enter send · ^v paste · ^c exit
194
200
  </Text>
195
- <Text color={autoApprove ? "green" : "gray"} dimColor={!autoApprove}>
196
- {autoApprove ? "⚡ auto" : "/auto"}
197
- </Text>
201
+ {forceApprove ? (
202
+ <Text color={RED}>⚡⚡ force-all</Text>
203
+ ) : (
204
+ <Text color={autoApprove ? GREEN : "gray"} dimColor={!autoApprove}>
205
+ {autoApprove ? "⚡ auto" : "/auto"}
206
+ </Text>
207
+ )}
198
208
  </Box>
199
209
  );
200
210
  }
@@ -279,7 +289,7 @@ export function CloneDoneView({
279
289
  <History committed={committed} />
280
290
  <Box flexDirection="column" marginY={1}>
281
291
  <Box gap={1}>
282
- <Text color="green">✓</Text>
292
+ <Text color={GREEN}>✓</Text>
283
293
  <Text color="white" bold>
284
294
  {repoName}
285
295
  </Text>
@@ -305,8 +315,8 @@ export function CloneErrorView({
305
315
  <History committed={committed} />
306
316
  <Box flexDirection="column" marginY={1}>
307
317
  <Box gap={1}>
308
- <Text color="red">✗</Text>
309
- <Text color="red">{stage.message}</Text>
318
+ <Text color={RED}>✗</Text>
319
+ <Text color={RED}>{stage.message}</Text>
310
320
  </Box>
311
321
  <Hint text=" enter/esc continue" />
312
322
  </Box>
@@ -337,10 +347,10 @@ export function PreviewView({
337
347
  <Box flexDirection="column" marginLeft={2} marginTop={1}>
338
348
  {patches.map((p) => (
339
349
  <Box key={p.path} gap={1}>
340
- <Text color={p.isNew ? "green" : "yellow"}>
350
+ <Text color={p.isNew ? GREEN : "yellow"}>
341
351
  {p.isNew ? "+" : "~"}
342
352
  </Text>
343
- <Text color={p.isNew ? "green" : "yellow"}>{p.path}</Text>
353
+ <Text color={p.isNew ? GREEN : "yellow"}>{p.path}</Text>
344
354
  {p.isNew && (
345
355
  <Text color="gray" dimColor>
346
356
  new
@@ -4,9 +4,10 @@ import Spinner from "ink-spinner";
4
4
  import { useState, useRef } from "react";
5
5
  import path from "path";
6
6
  import os from "os";
7
+ import TextInput from "ink-text-input";
7
8
  import { ACCENT } from "../../colors";
8
9
  import { buildDiffs } from "../repo/DiffViewer";
9
- import { ProviderPicker } from "../repo/ProviderPicker";
10
+ import { ProviderPicker } from "../provider/ProviderPicker";
10
11
  import { fetchFileTree, readImportantFiles } from "../../utils/files";
11
12
  import { startCloneRepo } from "../../utils/repo";
12
13
  import { useThinkingPhrase } from "../../utils/thinking";
@@ -62,6 +63,10 @@ const COMMANDS = [
62
63
  { cmd: "/clear history", desc: "wipe session memory for this repo" },
63
64
  { cmd: "/review", desc: "review current codebase" },
64
65
  { cmd: "/auto", desc: "toggle auto-approve for read/search tools" },
66
+ {
67
+ cmd: "/auto --force-all",
68
+ desc: "auto-approve ALL tools including shell and writes (⚠ dangerous)",
69
+ },
65
70
  { cmd: "/chat", desc: "chat history commands" },
66
71
  { cmd: "/chat list", desc: "list saved chats for this repo" },
67
72
  { cmd: "/chat load", desc: "load a saved chat by name" },
@@ -128,6 +133,69 @@ function CommandPalette({
128
133
  );
129
134
  }
130
135
 
136
+ function ForceAllWarning({
137
+ onConfirm,
138
+ }: {
139
+ onConfirm: (confirmed: boolean) => void;
140
+ }) {
141
+ const [input, setInput] = useState("");
142
+
143
+ return (
144
+ <Box flexDirection="column" marginY={1} gap={1}>
145
+ <Box gap={1}>
146
+ <Text color="red" bold>
147
+ ⚠ WARNING
148
+ </Text>
149
+ </Box>
150
+ <Box flexDirection="column" marginLeft={2} gap={1}>
151
+ <Text color="yellow">
152
+ Force-all mode auto-approves EVERY tool without asking — including:
153
+ </Text>
154
+ <Text color="red" dimColor>
155
+ {" "}
156
+ · shell commands (rm, git, npm, anything)
157
+ </Text>
158
+ <Text color="red" dimColor>
159
+ {" "}
160
+ · file writes and deletes
161
+ </Text>
162
+ <Text color="red" dimColor>
163
+ {" "}
164
+ · folder deletes
165
+ </Text>
166
+ <Text color="red" dimColor>
167
+ {" "}
168
+ · external fetches and URL opens
169
+ </Text>
170
+ <Text color="yellow" dimColor>
171
+ The AI can modify or delete files without any confirmation.
172
+ </Text>
173
+ <Text color="yellow" dimColor>
174
+ Only use this in throwaway environments or when you fully trust the
175
+ task.
176
+ </Text>
177
+ </Box>
178
+ <Box gap={1} marginTop={1}>
179
+ <Text color="gray">Type </Text>
180
+ <Text color="white" bold>
181
+ yes
182
+ </Text>
183
+ <Text color="gray"> to enable, or press </Text>
184
+ <Text color="white" bold>
185
+ esc
186
+ </Text>
187
+ <Text color="gray"> to cancel: </Text>
188
+ <TextInput
189
+ value={input}
190
+ onChange={setInput}
191
+ onSubmit={(v) => onConfirm(v.trim().toLowerCase() === "yes")}
192
+ placeholder="yes / esc to cancel"
193
+ />
194
+ </Box>
195
+ </Box>
196
+ );
197
+ }
198
+
131
199
  export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
132
200
  const [stage, setStage] = useState<ChatStage>({ type: "picking-provider" });
133
201
  const [committed, setCommitted] = useState<Message[]>([]);
@@ -140,6 +208,8 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
140
208
  const [showTimeline, setShowTimeline] = useState(false);
141
209
  const [showReview, setShowReview] = useState(false);
142
210
  const [autoApprove, setAutoApprove] = useState(false);
211
+ const [forceApprove, setForceApprove] = useState(false);
212
+ const [showForceWarning, setShowForceWarning] = useState(false);
143
213
  const [chatName, setChatName] = useState<string | null>(null);
144
214
  const chatNameRef = useRef<string | null>(null);
145
215
  const [recentChats, setRecentChats] = useState<string[]>([]);
@@ -154,11 +224,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
154
224
 
155
225
  const abortControllerRef = useRef<AbortController | null>(null);
156
226
  const toolResultCache = useRef<Map<string, string>>(new Map());
157
-
158
- // When the user approves a tool that has chained remainder calls, we
159
- // automatically approve subsequent tools in the same chain so the user
160
- // doesn't have to press y for every file in a 10-file scaffold.
161
- // This ref is set to true on the first approval and cleared when the chain ends.
162
227
  const batchApprovedRef = useRef(false);
163
228
 
164
229
  const thinkingPhrase = useThinkingPhrase(stage.type === "thinking");
@@ -190,6 +255,28 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
190
255
  setStage({ type: "idle" });
191
256
  };
192
257
 
258
+ const TOOL_TAG_NAMES = [
259
+ "shell",
260
+ "fetch",
261
+ "read-file",
262
+ "read-folder",
263
+ "grep",
264
+ "write-file",
265
+ "delete-file",
266
+ "delete-folder",
267
+ "open-url",
268
+ "generate-pdf",
269
+ "search",
270
+ "clone",
271
+ "changes",
272
+ ];
273
+
274
+ function isLikelyTruncated(text: string): boolean {
275
+ return TOOL_TAG_NAMES.some(
276
+ (tag) => text.includes(`<${tag}>`) && !text.includes(`</${tag}>`),
277
+ );
278
+ }
279
+
193
280
  const processResponse = (
194
281
  raw: string,
195
282
  currentAll: Message[],
@@ -201,7 +288,20 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
201
288
  return;
202
289
  }
203
290
 
204
- // Handle inline memory operations
291
+ // Guard: response cut off mid-tool-tag (context limit hit during generation)
292
+ if (isLikelyTruncated(raw)) {
293
+ const truncMsg: Message = {
294
+ role: "assistant",
295
+ content:
296
+ "(response cut off — the model hit its output limit mid-tool-call. Try asking it to continue, or simplify the request.)",
297
+ type: "text",
298
+ };
299
+ setAllMessages([...currentAll, truncMsg]);
300
+ setCommitted((prev) => [...prev, truncMsg]);
301
+ setStage({ type: "idle" });
302
+ return;
303
+ }
304
+
205
305
  const memAddMatches = [
206
306
  ...raw.matchAll(/<memory-add>([\s\S]*?)<\/memory-add>/g),
207
307
  ];
@@ -223,8 +323,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
223
323
 
224
324
  const parsed = parseResponse(cleanRaw);
225
325
 
226
- // ── changes (diff preview UI) ──────────────────────────────────────────
227
-
228
326
  if (parsed.kind === "changes") {
229
327
  batchApprovedRef.current = false;
230
328
  if (parsed.patches.length === 0) {
@@ -259,8 +357,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
259
357
  return;
260
358
  }
261
359
 
262
- // ── clone (git clone UI flow) ──────────────────────────────────────────
263
-
264
360
  if (parsed.kind === "clone") {
265
361
  batchApprovedRef.current = false;
266
362
  if (parsed.content) {
@@ -280,10 +376,22 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
280
376
  return;
281
377
  }
282
378
 
283
- // ── text ──────────────────────────────────────────────────────────────
284
-
285
379
  if (parsed.kind === "text") {
286
380
  batchApprovedRef.current = false;
381
+
382
+ if (!parsed.content.trim()) {
383
+ const stallMsg: Message = {
384
+ role: "assistant",
385
+ content:
386
+ '(no response — the model may have stalled. Try sending a short follow-up like "continue" or start a new message.)',
387
+ type: "text",
388
+ };
389
+ setAllMessages([...currentAll, stallMsg]);
390
+ setCommitted((prev) => [...prev, stallMsg]);
391
+ setStage({ type: "idle" });
392
+ return;
393
+ }
394
+
287
395
  const msg: Message = {
288
396
  role: "assistant",
289
397
  content: parsed.content,
@@ -309,8 +417,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
309
417
  return;
310
418
  }
311
419
 
312
- // ── generic tool ──────────────────────────────────────────────────────
313
-
314
420
  const tool = registry.get(parsed.toolName);
315
421
  if (!tool) {
316
422
  batchApprovedRef.current = false;
@@ -332,8 +438,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
332
438
  const isSafe = tool.safe ?? false;
333
439
 
334
440
  const executeAndContinue = async (approved: boolean) => {
335
- // If the user approved this tool and there are more in the chain,
336
- // mark the batch as approved so subsequent tools skip the prompt.
337
441
  if (approved && remainder) {
338
442
  batchApprovedRef.current = true;
339
443
  }
@@ -372,7 +476,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
372
476
  ? String(tool.summariseInput(parsed.input))
373
477
  : parsed.rawInput,
374
478
  summary: result.split("\n")[0]?.slice(0, 120) ?? "",
375
- repoPath,
376
479
  });
377
480
  }
378
481
 
@@ -393,13 +496,11 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
393
496
  setAllMessages(withTool);
394
497
  setCommitted((prev) => [...prev, toolMsg]);
395
498
 
396
- // Chain: process remainder immediately, no API round-trip needed.
397
499
  if (approved && remainder && remainder.length > 0) {
398
500
  processResponse(remainder, withTool, signal);
399
501
  return;
400
502
  }
401
503
 
402
- // Chain ended (or was never chained) — clear batch approval.
403
504
  batchApprovedRef.current = false;
404
505
 
405
506
  const nextAbort = new AbortController();
@@ -410,9 +511,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
410
511
  .catch(handleError(withTool));
411
512
  };
412
513
 
413
- // Auto-approve if: tool is safe, or global auto-approve is on, or we're
414
- // already inside a user-approved batch chain.
415
- if ((autoApprove && isSafe) || batchApprovedRef.current) {
514
+ if (forceApprove || (autoApprove && isSafe) || batchApprovedRef.current) {
416
515
  executeAndContinue(true);
417
516
  return;
418
517
  }
@@ -446,7 +545,41 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
446
545
  return;
447
546
  }
448
547
 
548
+ // /auto --force-all — show warning first
549
+ if (text.trim().toLowerCase() === "/auto --force-all") {
550
+ if (forceApprove) {
551
+ // Toggle off immediately, no warning needed
552
+ setForceApprove(false);
553
+ setAutoApprove(false);
554
+ const msg: Message = {
555
+ role: "assistant",
556
+ content: "Force-all mode OFF — tools will ask for permission again.",
557
+ type: "text",
558
+ };
559
+ setCommitted((prev) => [...prev, msg]);
560
+ setAllMessages((prev) => [...prev, msg]);
561
+ } else {
562
+ setShowForceWarning(true);
563
+ }
564
+ return;
565
+ }
566
+
449
567
  if (text.trim().toLowerCase() === "/auto") {
568
+ // /auto never enables force-all, only toggles safe auto-approve
569
+ if (forceApprove) {
570
+ // Step down from force-all to normal auto
571
+ setForceApprove(false);
572
+ setAutoApprove(true);
573
+ const msg: Message = {
574
+ role: "assistant",
575
+ content:
576
+ "Force-all mode OFF — switched to normal auto-approve (safe tools only).",
577
+ type: "text",
578
+ };
579
+ setCommitted((prev) => [...prev, msg]);
580
+ setAllMessages((prev) => [...prev, msg]);
581
+ return;
582
+ }
450
583
  const next = !autoApprove;
451
584
  setAutoApprove(next);
452
585
  const msg: Message = {
@@ -696,7 +829,8 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
696
829
  const nextAll = [...allMessages, userMsg];
697
830
  setCommitted((prev) => [...prev, userMsg]);
698
831
  setAllMessages(nextAll);
699
- toolResultCache.current.clear();
832
+ // Do NOT clear toolResultCache here — safe tool results (read-file, read-folder, grep)
833
+ // persist across the whole session so the model never re-reads the same resource twice.
700
834
  batchApprovedRef.current = false;
701
835
 
702
836
  inputHistoryRef.current = [
@@ -728,6 +862,12 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
728
862
  useInput((input, key) => {
729
863
  if (showTimeline) return;
730
864
 
865
+ // Esc cancels the force-all warning
866
+ if (showForceWarning && key.escape) {
867
+ setShowForceWarning(false);
868
+ return;
869
+ }
870
+
731
871
  if (stage.type === "thinking" && key.escape) {
732
872
  abortControllerRef.current?.abort();
733
873
  abortControllerRef.current = null;
@@ -786,7 +926,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
786
926
  kind: "url-fetched",
787
927
  detail: repoUrl,
788
928
  summary: `Cloned ${repoName} — ${fileCount} files`,
789
- repoPath,
790
929
  });
791
930
  setClonedUrls((prev) => new Set([...prev, repoUrl]));
792
931
  setStage({
@@ -924,7 +1063,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
924
1063
  .map((p: { path: string }) => p.path)
925
1064
  .join(", "),
926
1065
  summary: `Skipped changes to ${msg.patches.length} file(s)`,
927
- repoPath,
928
1066
  });
929
1067
  }
930
1068
  }
@@ -939,7 +1077,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
939
1077
  kind: "code-applied",
940
1078
  detail: stage.patches.map((p) => p.path).join(", "),
941
1079
  summary: `Applied changes to ${stage.patches.length} file(s)`,
942
- repoPath,
943
1080
  });
944
1081
  } catch {
945
1082
  /* non-fatal */
@@ -1054,7 +1191,36 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
1054
1191
  {(msg, i) => <StaticMessage key={i} msg={msg} />}
1055
1192
  </Static>
1056
1193
 
1057
- {stage.type === "thinking" && (
1194
+ {/* Force-all warning overlay */}
1195
+ {showForceWarning && (
1196
+ <ForceAllWarning
1197
+ onConfirm={(confirmed) => {
1198
+ setShowForceWarning(false);
1199
+ if (confirmed) {
1200
+ setForceApprove(true);
1201
+ setAutoApprove(true);
1202
+ const msg: Message = {
1203
+ role: "assistant",
1204
+ content:
1205
+ "⚡⚡ Force-all mode ON — ALL tools auto-approved including shell and writes. Type /auto --force-all again to disable.",
1206
+ type: "text",
1207
+ };
1208
+ setCommitted((prev) => [...prev, msg]);
1209
+ setAllMessages((prev) => [...prev, msg]);
1210
+ } else {
1211
+ const msg: Message = {
1212
+ role: "assistant",
1213
+ content: "Force-all cancelled.",
1214
+ type: "text",
1215
+ };
1216
+ setCommitted((prev) => [...prev, msg]);
1217
+ setAllMessages((prev) => [...prev, msg]);
1218
+ }
1219
+ }}
1220
+ />
1221
+ )}
1222
+
1223
+ {!showForceWarning && stage.type === "thinking" && (
1058
1224
  <Box gap={1}>
1059
1225
  <Text color={ACCENT}>●</Text>
1060
1226
  <TypewriterText text={thinkingPhrase} />
@@ -1064,11 +1230,11 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
1064
1230
  </Box>
1065
1231
  )}
1066
1232
 
1067
- {stage.type === "permission" && (
1233
+ {!showForceWarning && stage.type === "permission" && (
1068
1234
  <PermissionPrompt tool={stage.tool} onDecide={stage.resolve} />
1069
1235
  )}
1070
1236
 
1071
- {stage.type === "idle" && (
1237
+ {!showForceWarning && stage.type === "idle" && (
1072
1238
  <Box flexDirection="column">
1073
1239
  {inputValue.startsWith("/") && (
1074
1240
  <CommandPalette
@@ -1089,7 +1255,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
1089
1255
  }}
1090
1256
  inputKey={inputKey}
1091
1257
  />
1092
- <ShortcutBar autoApprove={autoApprove} />
1258
+ <ShortcutBar autoApprove={autoApprove} forceApprove={forceApprove} />
1093
1259
  </Box>
1094
1260
  )}
1095
1261
  </Box>