@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.
- package/LENS.md +32 -68
- package/README.md +91 -0
- package/addons/README.md +3 -0
- package/addons/run-tests.js +127 -0
- package/dist/index.mjs +226459 -2638
- package/package.json +13 -4
- package/src/colors.ts +5 -0
- package/src/commands/commit.tsx +686 -0
- package/src/commands/provider.tsx +36 -22
- package/src/components/__tests__/Header.test.tsx +9 -0
- package/src/components/chat/ChatMessage.tsx +6 -6
- package/src/components/chat/ChatOverlays.tsx +20 -10
- package/src/components/chat/ChatRunner.tsx +197 -31
- package/src/components/provider/ApiKeyStep.tsx +77 -121
- package/src/components/provider/ModelStep.tsx +35 -20
- package/src/components/{repo → provider}/ProviderPicker.tsx +1 -1
- package/src/components/provider/ProviderTypeStep.tsx +12 -5
- package/src/components/provider/RemoveProviderStep.tsx +7 -8
- package/src/components/repo/RepoAnalysis.tsx +1 -1
- package/src/components/task/TaskRunner.tsx +1 -1
- package/src/components/timeline/CommitDetail.tsx +2 -4
- package/src/components/timeline/CommitList.tsx +2 -14
- package/src/components/timeline/TimelineChat.tsx +1 -2
- package/src/components/timeline/TimelineRunner.tsx +506 -423
- package/src/index.tsx +38 -0
- package/src/prompts/fewshot.ts +144 -47
- package/src/prompts/system.ts +25 -21
- package/src/tools/chart.ts +210 -0
- package/src/tools/convert-image.ts +312 -0
- package/src/tools/files.ts +1 -9
- package/src/tools/git.ts +577 -0
- package/src/tools/index.ts +17 -13
- package/src/tools/pdf.ts +136 -78
- package/src/tools/view-image.ts +335 -0
- package/src/tools/web.ts +0 -4
- package/src/utils/addons/loadAddons.ts +6 -3
- package/src/utils/chat.ts +38 -23
- package/src/utils/thinking.tsx +275 -162
- package/src/utils/tools/builtins.ts +39 -32
- package/src/utils/tools/registry.ts +0 -14
- 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=
|
|
56
|
-
{figures.
|
|
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
|
-
{
|
|
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 ?
|
|
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=
|
|
86
|
-
{figures.
|
|
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=
|
|
99
|
-
{figures.
|
|
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=
|
|
117
|
-
{figures.
|
|
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=
|
|
163
|
-
{figures.
|
|
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=
|
|
187
|
-
{figures.
|
|
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=
|
|
214
|
-
{figures.
|
|
226
|
+
<Text key={i} color={GREEN}>
|
|
227
|
+
{figures.arrowRight} {s}
|
|
215
228
|
</Text>
|
|
216
229
|
))}
|
|
217
|
-
<Text color=
|
|
230
|
+
<Text color={GREEN}>
|
|
231
|
+
{figures.arrowRight} Provider configured successfully
|
|
232
|
+
</Text>
|
|
218
233
|
<Text color="gray">
|
|
219
|
-
|
|
220
|
-
providers.
|
|
234
|
+
Run <Text color={CYAN}>lens provider</Text> again to manage providers.
|
|
221
235
|
</Text>
|
|
222
236
|
</Box>
|
|
223
237
|
);
|
|
@@ -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 :
|
|
153
|
-
<Text color={msg.approved ? "gray" :
|
|
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=
|
|
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 ?
|
|
178
|
+
<Text color={msg.applied ? GREEN : "gray"}>
|
|
179
179
|
{msg.applied ? "✓" : "·"}
|
|
180
180
|
</Text>
|
|
181
|
-
<Text color={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({
|
|
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
|
-
|
|
196
|
-
{
|
|
197
|
-
|
|
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=
|
|
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=
|
|
309
|
-
<Text color=
|
|
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 ?
|
|
350
|
+
<Text color={p.isNew ? GREEN : "yellow"}>
|
|
341
351
|
{p.isNew ? "+" : "~"}
|
|
342
352
|
</Text>
|
|
343
|
-
<Text color={p.isNew ?
|
|
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 "../
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
{
|
|
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>
|