@ridit/lens 0.2.2 → 0.2.5

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.
@@ -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
  }
@@ -62,6 +62,10 @@ const COMMANDS = [
62
62
  { cmd: "/clear history", desc: "wipe session memory for this repo" },
63
63
  { cmd: "/review", desc: "review current codebase" },
64
64
  { cmd: "/auto", desc: "toggle auto-approve for read/search tools" },
65
+ {
66
+ cmd: "/auto --force-all",
67
+ desc: "auto-approve ALL tools including shell and writes (⚠ dangerous)",
68
+ },
65
69
  { cmd: "/chat", desc: "chat history commands" },
66
70
  { cmd: "/chat list", desc: "list saved chats for this repo" },
67
71
  { cmd: "/chat load", desc: "load a saved chat by name" },
@@ -128,6 +132,71 @@ function CommandPalette({
128
132
  );
129
133
  }
130
134
 
135
+ function ForceAllWarning({
136
+ onConfirm,
137
+ }: {
138
+ onConfirm: (confirmed: boolean) => void;
139
+ }) {
140
+ const [input, setInput] = useState("");
141
+
142
+ return (
143
+ <Box flexDirection="column" marginY={1} gap={1}>
144
+ <Box gap={1}>
145
+ <Text color="red" bold>
146
+ ⚠ WARNING
147
+ </Text>
148
+ </Box>
149
+ <Box flexDirection="column" marginLeft={2} gap={1}>
150
+ <Text color="yellow">
151
+ Force-all mode auto-approves EVERY tool without asking — including:
152
+ </Text>
153
+ <Text color="red" dimColor>
154
+ {" "}
155
+ · shell commands (rm, git, npm, anything)
156
+ </Text>
157
+ <Text color="red" dimColor>
158
+ {" "}
159
+ · file writes and deletes
160
+ </Text>
161
+ <Text color="red" dimColor>
162
+ {" "}
163
+ · folder deletes
164
+ </Text>
165
+ <Text color="red" dimColor>
166
+ {" "}
167
+ · external fetches and URL opens
168
+ </Text>
169
+ <Text color="yellow" dimColor>
170
+ The AI can modify or delete files without any confirmation.
171
+ </Text>
172
+ <Text color="yellow" dimColor>
173
+ Only use this in throwaway environments or when you fully trust the
174
+ task.
175
+ </Text>
176
+ </Box>
177
+ <Box gap={1} marginTop={1}>
178
+ <Text color="gray">Type </Text>
179
+ <Text color="white" bold>
180
+ yes
181
+ </Text>
182
+ <Text color="gray"> to enable, or press </Text>
183
+ <Text color="white" bold>
184
+ esc
185
+ </Text>
186
+ <Text color="gray"> to cancel: </Text>
187
+ <TextInput
188
+ value={input}
189
+ onChange={setInput}
190
+ onSubmit={(v) => onConfirm(v.trim().toLowerCase() === "yes")}
191
+ placeholder="yes / esc to cancel"
192
+ />
193
+ </Box>
194
+ </Box>
195
+ );
196
+ }
197
+
198
+ import TextInput from "ink-text-input";
199
+
131
200
  export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
132
201
  const [stage, setStage] = useState<ChatStage>({ type: "picking-provider" });
133
202
  const [committed, setCommitted] = useState<Message[]>([]);
@@ -140,6 +209,8 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
140
209
  const [showTimeline, setShowTimeline] = useState(false);
141
210
  const [showReview, setShowReview] = useState(false);
142
211
  const [autoApprove, setAutoApprove] = useState(false);
212
+ const [forceApprove, setForceApprove] = useState(false);
213
+ const [showForceWarning, setShowForceWarning] = useState(false);
143
214
  const [chatName, setChatName] = useState<string | null>(null);
144
215
  const chatNameRef = useRef<string | null>(null);
145
216
  const [recentChats, setRecentChats] = useState<string[]>([]);
@@ -154,11 +225,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
154
225
 
155
226
  const abortControllerRef = useRef<AbortController | null>(null);
156
227
  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
228
  const batchApprovedRef = useRef(false);
163
229
 
164
230
  const thinkingPhrase = useThinkingPhrase(stage.type === "thinking");
@@ -190,6 +256,28 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
190
256
  setStage({ type: "idle" });
191
257
  };
192
258
 
259
+ const TOOL_TAG_NAMES = [
260
+ "shell",
261
+ "fetch",
262
+ "read-file",
263
+ "read-folder",
264
+ "grep",
265
+ "write-file",
266
+ "delete-file",
267
+ "delete-folder",
268
+ "open-url",
269
+ "generate-pdf",
270
+ "search",
271
+ "clone",
272
+ "changes",
273
+ ];
274
+
275
+ function isLikelyTruncated(text: string): boolean {
276
+ return TOOL_TAG_NAMES.some(
277
+ (tag) => text.includes(`<${tag}>`) && !text.includes(`</${tag}>`),
278
+ );
279
+ }
280
+
193
281
  const processResponse = (
194
282
  raw: string,
195
283
  currentAll: Message[],
@@ -201,7 +289,20 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
201
289
  return;
202
290
  }
203
291
 
204
- // Handle inline memory operations
292
+ // Guard: response cut off mid-tool-tag (context limit hit during generation)
293
+ if (isLikelyTruncated(raw)) {
294
+ const truncMsg: Message = {
295
+ role: "assistant",
296
+ content:
297
+ "(response cut off — the model hit its output limit mid-tool-call. Try asking it to continue, or simplify the request.)",
298
+ type: "text",
299
+ };
300
+ setAllMessages([...currentAll, truncMsg]);
301
+ setCommitted((prev) => [...prev, truncMsg]);
302
+ setStage({ type: "idle" });
303
+ return;
304
+ }
305
+
205
306
  const memAddMatches = [
206
307
  ...raw.matchAll(/<memory-add>([\s\S]*?)<\/memory-add>/g),
207
308
  ];
@@ -223,8 +324,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
223
324
 
224
325
  const parsed = parseResponse(cleanRaw);
225
326
 
226
- // ── changes (diff preview UI) ──────────────────────────────────────────
227
-
228
327
  if (parsed.kind === "changes") {
229
328
  batchApprovedRef.current = false;
230
329
  if (parsed.patches.length === 0) {
@@ -259,8 +358,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
259
358
  return;
260
359
  }
261
360
 
262
- // ── clone (git clone UI flow) ──────────────────────────────────────────
263
-
264
361
  if (parsed.kind === "clone") {
265
362
  batchApprovedRef.current = false;
266
363
  if (parsed.content) {
@@ -280,10 +377,22 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
280
377
  return;
281
378
  }
282
379
 
283
- // ── text ──────────────────────────────────────────────────────────────
284
-
285
380
  if (parsed.kind === "text") {
286
381
  batchApprovedRef.current = false;
382
+
383
+ if (!parsed.content.trim()) {
384
+ const stallMsg: Message = {
385
+ role: "assistant",
386
+ content:
387
+ '(no response — the model may have stalled. Try sending a short follow-up like "continue" or start a new message.)',
388
+ type: "text",
389
+ };
390
+ setAllMessages([...currentAll, stallMsg]);
391
+ setCommitted((prev) => [...prev, stallMsg]);
392
+ setStage({ type: "idle" });
393
+ return;
394
+ }
395
+
287
396
  const msg: Message = {
288
397
  role: "assistant",
289
398
  content: parsed.content,
@@ -309,8 +418,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
309
418
  return;
310
419
  }
311
420
 
312
- // ── generic tool ──────────────────────────────────────────────────────
313
-
314
421
  const tool = registry.get(parsed.toolName);
315
422
  if (!tool) {
316
423
  batchApprovedRef.current = false;
@@ -332,8 +439,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
332
439
  const isSafe = tool.safe ?? false;
333
440
 
334
441
  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
442
  if (approved && remainder) {
338
443
  batchApprovedRef.current = true;
339
444
  }
@@ -372,7 +477,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
372
477
  ? String(tool.summariseInput(parsed.input))
373
478
  : parsed.rawInput,
374
479
  summary: result.split("\n")[0]?.slice(0, 120) ?? "",
375
- repoPath,
376
480
  });
377
481
  }
378
482
 
@@ -393,13 +497,11 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
393
497
  setAllMessages(withTool);
394
498
  setCommitted((prev) => [...prev, toolMsg]);
395
499
 
396
- // Chain: process remainder immediately, no API round-trip needed.
397
500
  if (approved && remainder && remainder.length > 0) {
398
501
  processResponse(remainder, withTool, signal);
399
502
  return;
400
503
  }
401
504
 
402
- // Chain ended (or was never chained) — clear batch approval.
403
505
  batchApprovedRef.current = false;
404
506
 
405
507
  const nextAbort = new AbortController();
@@ -410,9 +512,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
410
512
  .catch(handleError(withTool));
411
513
  };
412
514
 
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) {
515
+ if (forceApprove || (autoApprove && isSafe) || batchApprovedRef.current) {
416
516
  executeAndContinue(true);
417
517
  return;
418
518
  }
@@ -446,7 +546,41 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
446
546
  return;
447
547
  }
448
548
 
549
+ // /auto --force-all — show warning first
550
+ if (text.trim().toLowerCase() === "/auto --force-all") {
551
+ if (forceApprove) {
552
+ // Toggle off immediately, no warning needed
553
+ setForceApprove(false);
554
+ setAutoApprove(false);
555
+ const msg: Message = {
556
+ role: "assistant",
557
+ content: "Force-all mode OFF — tools will ask for permission again.",
558
+ type: "text",
559
+ };
560
+ setCommitted((prev) => [...prev, msg]);
561
+ setAllMessages((prev) => [...prev, msg]);
562
+ } else {
563
+ setShowForceWarning(true);
564
+ }
565
+ return;
566
+ }
567
+
449
568
  if (text.trim().toLowerCase() === "/auto") {
569
+ // /auto never enables force-all, only toggles safe auto-approve
570
+ if (forceApprove) {
571
+ // Step down from force-all to normal auto
572
+ setForceApprove(false);
573
+ setAutoApprove(true);
574
+ const msg: Message = {
575
+ role: "assistant",
576
+ content:
577
+ "Force-all mode OFF — switched to normal auto-approve (safe tools only).",
578
+ type: "text",
579
+ };
580
+ setCommitted((prev) => [...prev, msg]);
581
+ setAllMessages((prev) => [...prev, msg]);
582
+ return;
583
+ }
450
584
  const next = !autoApprove;
451
585
  setAutoApprove(next);
452
586
  const msg: Message = {
@@ -696,7 +830,8 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
696
830
  const nextAll = [...allMessages, userMsg];
697
831
  setCommitted((prev) => [...prev, userMsg]);
698
832
  setAllMessages(nextAll);
699
- toolResultCache.current.clear();
833
+ // Do NOT clear toolResultCache here — safe tool results (read-file, read-folder, grep)
834
+ // persist across the whole session so the model never re-reads the same resource twice.
700
835
  batchApprovedRef.current = false;
701
836
 
702
837
  inputHistoryRef.current = [
@@ -728,6 +863,12 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
728
863
  useInput((input, key) => {
729
864
  if (showTimeline) return;
730
865
 
866
+ // Esc cancels the force-all warning
867
+ if (showForceWarning && key.escape) {
868
+ setShowForceWarning(false);
869
+ return;
870
+ }
871
+
731
872
  if (stage.type === "thinking" && key.escape) {
732
873
  abortControllerRef.current?.abort();
733
874
  abortControllerRef.current = null;
@@ -786,7 +927,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
786
927
  kind: "url-fetched",
787
928
  detail: repoUrl,
788
929
  summary: `Cloned ${repoName} — ${fileCount} files`,
789
- repoPath,
790
930
  });
791
931
  setClonedUrls((prev) => new Set([...prev, repoUrl]));
792
932
  setStage({
@@ -924,7 +1064,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
924
1064
  .map((p: { path: string }) => p.path)
925
1065
  .join(", "),
926
1066
  summary: `Skipped changes to ${msg.patches.length} file(s)`,
927
- repoPath,
928
1067
  });
929
1068
  }
930
1069
  }
@@ -939,7 +1078,6 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
939
1078
  kind: "code-applied",
940
1079
  detail: stage.patches.map((p) => p.path).join(", "),
941
1080
  summary: `Applied changes to ${stage.patches.length} file(s)`,
942
- repoPath,
943
1081
  });
944
1082
  } catch {
945
1083
  /* non-fatal */
@@ -1054,7 +1192,36 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
1054
1192
  {(msg, i) => <StaticMessage key={i} msg={msg} />}
1055
1193
  </Static>
1056
1194
 
1057
- {stage.type === "thinking" && (
1195
+ {/* Force-all warning overlay */}
1196
+ {showForceWarning && (
1197
+ <ForceAllWarning
1198
+ onConfirm={(confirmed) => {
1199
+ setShowForceWarning(false);
1200
+ if (confirmed) {
1201
+ setForceApprove(true);
1202
+ setAutoApprove(true);
1203
+ const msg: Message = {
1204
+ role: "assistant",
1205
+ content:
1206
+ "⚡⚡ Force-all mode ON — ALL tools auto-approved including shell and writes. Type /auto --force-all again to disable.",
1207
+ type: "text",
1208
+ };
1209
+ setCommitted((prev) => [...prev, msg]);
1210
+ setAllMessages((prev) => [...prev, msg]);
1211
+ } else {
1212
+ const msg: Message = {
1213
+ role: "assistant",
1214
+ content: "Force-all cancelled.",
1215
+ type: "text",
1216
+ };
1217
+ setCommitted((prev) => [...prev, msg]);
1218
+ setAllMessages((prev) => [...prev, msg]);
1219
+ }
1220
+ }}
1221
+ />
1222
+ )}
1223
+
1224
+ {!showForceWarning && stage.type === "thinking" && (
1058
1225
  <Box gap={1}>
1059
1226
  <Text color={ACCENT}>●</Text>
1060
1227
  <TypewriterText text={thinkingPhrase} />
@@ -1064,11 +1231,11 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
1064
1231
  </Box>
1065
1232
  )}
1066
1233
 
1067
- {stage.type === "permission" && (
1234
+ {!showForceWarning && stage.type === "permission" && (
1068
1235
  <PermissionPrompt tool={stage.tool} onDecide={stage.resolve} />
1069
1236
  )}
1070
1237
 
1071
- {stage.type === "idle" && (
1238
+ {!showForceWarning && stage.type === "idle" && (
1072
1239
  <Box flexDirection="column">
1073
1240
  {inputValue.startsWith("/") && (
1074
1241
  <CommandPalette
@@ -1089,7 +1256,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
1089
1256
  }}
1090
1257
  inputKey={inputKey}
1091
1258
  />
1092
- <ShortcutBar autoApprove={autoApprove} />
1259
+ <ShortcutBar autoApprove={autoApprove} forceApprove={forceApprove} />
1093
1260
  </Box>
1094
1261
  )}
1095
1262
  </Box>
@@ -1,8 +1,7 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import type { Commit, DiffFile } from "../../utils/git";
4
-
5
- const ACCENT = "#FF8C00";
4
+ import { ACCENT } from "../../colors";
6
5
 
7
6
  type Props = {
8
7
  commit: Commit | null;
@@ -73,7 +72,6 @@ export function CommitDetail({
73
72
 
74
73
  const divider = "─".repeat(Math.max(0, width - 2));
75
74
 
76
- // Build all diff lines for scrolling
77
75
  const allDiffLines: Array<{
78
76
  type: string;
79
77
  content: string;
@@ -177,7 +175,7 @@ export function CommitDetail({
177
175
  {/* stats bar */}
178
176
  <Box paddingX={1} marginTop={1} gap={3}>
179
177
  <Text color="green">+{commit.insertions} insertions</Text>
180
- <Text color="red">-{commit.deletions} deletions</Text>
178
+ <Text color="red">-{commit.deletions}</Text>
181
179
  <Text color="gray" dimColor>
182
180
  {commit.filesChanged} file{commit.filesChanged !== 1 ? "s" : ""}{" "}
183
181
  changed
@@ -1,8 +1,7 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
3
  import type { Commit } from "../../utils/git";
4
-
5
- const ACCENT = "#FF8C00";
4
+ import { ACCENT } from "../../colors";
6
5
 
7
6
  type Props = {
8
7
  commits: Commit[];
@@ -29,7 +28,6 @@ function formatRefs(refs: string): string {
29
28
  }
30
29
 
31
30
  function shortDate(dateStr: string): string {
32
- // "2026-03-12 14:22:01 +0530" → "Mar 12"
33
31
  try {
34
32
  const d = new Date(dateStr);
35
33
  return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
@@ -82,8 +80,7 @@ export function CommitList({
82
80
  const refs = formatRefs(commit.refs);
83
81
  const date = shortDate(commit.date);
84
82
 
85
- // truncate message to fit width
86
- const prefixLen = 14; // symbol + hash + date
83
+ const prefixLen = 14;
87
84
  const maxMsg = Math.max(10, width - prefixLen - 3);
88
85
  const msg =
89
86
  commit.message.length > maxMsg
@@ -92,22 +89,18 @@ export function CommitList({
92
89
 
93
90
  return (
94
91
  <Box key={commit.hash} paddingX={1} flexDirection="column">
95
- {/* graph line above (not first) */}
96
92
  {i > 0 && (
97
93
  <Text color="gray" dimColor>
98
94
  {"│"}
99
95
  </Text>
100
96
  )}
101
97
  <Box gap={1}>
102
- {/* selection indicator */}
103
98
  <Text color={isSelected ? ACCENT : "gray"}>
104
99
  {isSelected ? "▶" : " "}
105
100
  </Text>
106
101
 
107
- {/* graph node */}
108
102
  <Text color={isSelected ? ACCENT : color}>{symbol}</Text>
109
103
 
110
- {/* short hash */}
111
104
  <Text
112
105
  color={isSelected ? "white" : "gray"}
113
106
  dimColor={!isSelected}
@@ -115,12 +108,10 @@ export function CommitList({
115
108
  {commit.shortHash}
116
109
  </Text>
117
110
 
118
- {/* date */}
119
111
  <Text color="cyan" dimColor={!isSelected}>
120
112
  {date}
121
113
  </Text>
122
114
 
123
- {/* message */}
124
115
  <Text
125
116
  color={isSelected ? "white" : "gray"}
126
117
  bold={isSelected}
@@ -130,14 +121,12 @@ export function CommitList({
130
121
  </Text>
131
122
  </Box>
132
123
 
133
- {/* refs on selected */}
134
124
  {isSelected && refs && (
135
125
  <Box paddingLeft={4}>
136
126
  <Text color="yellow">{refs}</Text>
137
127
  </Box>
138
128
  )}
139
129
 
140
- {/* stat summary on selected */}
141
130
  {isSelected && (
142
131
  <Box paddingLeft={4} gap={2}>
143
132
  <Text color="gray" dimColor>
@@ -159,7 +148,6 @@ export function CommitList({
159
148
  );
160
149
  })}
161
150
 
162
- {/* scroll hint */}
163
151
  <Box paddingX={1} marginTop={1}>
164
152
  <Text color="gray" dimColor>
165
153
  {scrollOffset > 0 ? "↑ more above" : ""}
@@ -5,8 +5,7 @@ import type { Commit } from "../../utils/git";
5
5
  import { summarizeTimeline } from "../../utils/git";
6
6
  import type { Provider } from "../../types/config";
7
7
  import { callChat } from "../../utils/chat";
8
-
9
- const ACCENT = "#FF8C00";
8
+ import { ACCENT } from "../../colors";
10
9
 
11
10
  type TLMessage = { role: "user" | "assistant"; content: string; type: "text" };
12
11