@ridit/lens 0.3.7 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +725 -460
- package/package.json +1 -1
- package/src/commands/chat.tsx +14 -20
- package/src/components/chat/ChatMessage.tsx +46 -4
- package/src/components/chat/ChatOverlays.tsx +19 -13
- package/src/components/chat/ChatRunner.tsx +53 -13
- package/src/components/chat/TextArea.tsx +10 -9
- package/src/components/chat/hooks/useChat.ts +373 -273
- package/src/components/chat/hooks/useCommandHandlers.ts +0 -4
- package/src/components/repo/RepoAnalysis.tsx +3 -3
- package/src/components/task/TaskRunner.tsx +3 -3
- package/src/index.tsx +13 -2
- package/src/prompts/system.ts +16 -17
- package/src/types/chat.ts +3 -1
- package/src/utils/chat.ts +57 -4
- package/src/utils/intentClassifier.ts +0 -15
- package/src/utils/thinking.tsx +64 -0
- package/src/utils/tools/builtins.ts +27 -12
|
@@ -66,10 +66,6 @@ function makeMsg(content: string): Message {
|
|
|
66
66
|
return { role: "assistant", content, type: "text" };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/**
|
|
70
|
-
* Returns true if the command was handled, false if it should fall through
|
|
71
|
-
* to the normal message flow.
|
|
72
|
-
*/
|
|
73
69
|
export function handleCommand(text: string, ctx: CommandContext): boolean {
|
|
74
70
|
const t = text.trim().toLowerCase();
|
|
75
71
|
|
|
@@ -105,7 +105,7 @@ function AskingFilesStep() {
|
|
|
105
105
|
return (
|
|
106
106
|
<Box gap={1}>
|
|
107
107
|
<Text color={ACCENT}>
|
|
108
|
-
<Spinner />
|
|
108
|
+
<Spinner type="moon" />
|
|
109
109
|
</Text>
|
|
110
110
|
<Text color={ACCENT}>{phrase}</Text>
|
|
111
111
|
</Box>
|
|
@@ -117,7 +117,7 @@ function AnalyzingStep() {
|
|
|
117
117
|
return (
|
|
118
118
|
<Box gap={1}>
|
|
119
119
|
<Text color={ACCENT}>
|
|
120
|
-
<Spinner />
|
|
120
|
+
<Spinner type="earth" />
|
|
121
121
|
</Text>
|
|
122
122
|
<Text color={ACCENT}>{phrase}</Text>
|
|
123
123
|
</Box>
|
|
@@ -366,7 +366,7 @@ export const RepoAnalysis = ({
|
|
|
366
366
|
return (
|
|
367
367
|
<Box marginTop={1}>
|
|
368
368
|
<Text color={ACCENT}>
|
|
369
|
-
<Spinner />
|
|
369
|
+
<Spinner type="arc" />
|
|
370
370
|
</Text>
|
|
371
371
|
<Box marginLeft={1}>
|
|
372
372
|
<Text>Writing file...</Text>
|
|
@@ -88,7 +88,7 @@ function ThinkingAboutStep({ prompt }: { prompt: string }) {
|
|
|
88
88
|
return (
|
|
89
89
|
<Box gap={1}>
|
|
90
90
|
<Text color={ACCENT}>
|
|
91
|
-
<Spinner />
|
|
91
|
+
<Spinner type="triangle" />
|
|
92
92
|
</Text>
|
|
93
93
|
<Text color={ACCENT}>{phrase}</Text>
|
|
94
94
|
<Text color="gray">"{prompt}"</Text>
|
|
@@ -270,7 +270,7 @@ export const PromptRunner = ({
|
|
|
270
270
|
return (
|
|
271
271
|
<Box marginTop={1} gap={1}>
|
|
272
272
|
<Text color={ACCENT}>
|
|
273
|
-
<Spinner />
|
|
273
|
+
<Spinner type="dots2" />
|
|
274
274
|
</Text>
|
|
275
275
|
<Text>Reading codebase...</Text>
|
|
276
276
|
</Box>
|
|
@@ -322,7 +322,7 @@ export const PromptRunner = ({
|
|
|
322
322
|
return (
|
|
323
323
|
<Box marginTop={1} gap={1}>
|
|
324
324
|
<Text color={ACCENT}>
|
|
325
|
-
<Spinner />
|
|
325
|
+
<Spinner type="bouncingBar" />
|
|
326
326
|
</Text>
|
|
327
327
|
<Text>Applying changes...</Text>
|
|
328
328
|
</Box>
|
package/src/index.tsx
CHANGED
|
@@ -16,6 +16,11 @@ import { loadAddons } from "./utils/addons/loadAddons";
|
|
|
16
16
|
registerBuiltins();
|
|
17
17
|
await loadAddons();
|
|
18
18
|
|
|
19
|
+
if (process.stdout.isTTY) {
|
|
20
|
+
process.stdout.write("\x1b[>4;1m");
|
|
21
|
+
process.on("exit", () => process.stdout.write("\x1b[>4;0m"));
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
const program = new Command();
|
|
20
25
|
|
|
21
26
|
program
|
|
@@ -51,8 +56,14 @@ program
|
|
|
51
56
|
.command("chat")
|
|
52
57
|
.description("Chat with your codebase — ask questions or make changes")
|
|
53
58
|
.option("-p, --path <path>", "Path to the repo", ".")
|
|
54
|
-
.
|
|
55
|
-
|
|
59
|
+
.option(
|
|
60
|
+
"--auto-force",
|
|
61
|
+
"Start with force-all mode enabled (auto-approves all tools)",
|
|
62
|
+
)
|
|
63
|
+
.action((opts: { path: string; autoForce: boolean }) => {
|
|
64
|
+
render(
|
|
65
|
+
<ChatCommand path={opts.path} autoForce={opts.autoForce ?? false} />,
|
|
66
|
+
);
|
|
56
67
|
});
|
|
57
68
|
|
|
58
69
|
program
|
package/src/prompts/system.ts
CHANGED
|
@@ -46,23 +46,22 @@ Use memory-delete when the user asks you to forget something or a memory is outd
|
|
|
46
46
|
|
|
47
47
|
1. ONE tool per response — emit the XML tag, then stop. Never chain tools in one response except when scaffolding (see below).
|
|
48
48
|
2. NEVER call a tool more than once for the same path in a session. If write-file or shell returned a result, it succeeded. Move on immediately.
|
|
49
|
-
3.
|
|
50
|
-
4.
|
|
51
|
-
5.
|
|
52
|
-
6. NEVER
|
|
53
|
-
7.
|
|
54
|
-
8.
|
|
55
|
-
9. When
|
|
56
|
-
10.
|
|
57
|
-
11.
|
|
58
|
-
12.
|
|
59
|
-
13.
|
|
60
|
-
14.
|
|
61
|
-
15. When
|
|
62
|
-
16.
|
|
63
|
-
17.
|
|
64
|
-
18.
|
|
65
|
-
19. NEVER use shell for filesystem inspection or searching — always use grep, read-file, or read-folder instead.
|
|
49
|
+
3. shell is ONLY for running code, installing packages, building, and testing. NEVER use shell to inspect the filesystem or read files — use read-file, read-folder, or grep instead.
|
|
50
|
+
4. NEVER use shell to run git clone — use the clone tag instead.
|
|
51
|
+
5. NEVER read a file you just wrote. The write output confirms success.
|
|
52
|
+
6. NEVER apologize and redo a tool call — one attempt is enough, trust the output.
|
|
53
|
+
7. When the user asks you to CREATE a brand new file, use write-file immediately — do NOT read first. write-file content must be the COMPLETE file content.
|
|
54
|
+
8. When the user asks you to MODIFY or FIX an existing file, read it first, then propose the edit using changes — NEVER use write-file on an existing file.
|
|
55
|
+
9. When fixing multiple files, use read-files to read ALL of them first, then emit one changes tag with all patches together.
|
|
56
|
+
10. If a read-folder or read-file returns not found, accept it and move on — do NOT retry the same path.
|
|
57
|
+
11. Every shell command runs from the repo root — cd has no persistent effect. Use full paths or combine with && e.g. cd myapp && bun run index.ts
|
|
58
|
+
12. write-file paths are relative to the repo root — use full relative paths e.g. myapp/src/index.tsx not src/index.tsx
|
|
59
|
+
13. When explaining how to use a tool in text, use [tag] bracket notation — NEVER emit a real XML tool tag as part of an explanation.
|
|
60
|
+
14. NEVER use markdown formatting in plain text responses — no bold, no headings, no bullet points. Only use fenced code blocks when showing actual code.
|
|
61
|
+
15. When scaffolding multiple NEW files, emit ONE write-file per response and wait for the result before writing the next file.
|
|
62
|
+
16. When you identify a bug or error, ALWAYS propose the fix immediately using changes. Never describe the fix without proposing it.
|
|
63
|
+
17. NEVER use shell for filesystem inspection or searching — always use grep, read-file, or read-folder instead.
|
|
64
|
+
18. changes patches must include the COMPLETE new file content for each path — never partial content.
|
|
66
65
|
|
|
67
66
|
## ADDON FORMAT
|
|
68
67
|
|
package/src/types/chat.ts
CHANGED
|
@@ -33,7 +33,8 @@ export type Message =
|
|
|
33
33
|
| "delete-folder"
|
|
34
34
|
| "open-url"
|
|
35
35
|
| "generate-pdf"
|
|
36
|
-
| "search"
|
|
36
|
+
| "search"
|
|
37
|
+
| "changes";
|
|
37
38
|
content: string;
|
|
38
39
|
result: string;
|
|
39
40
|
approved: boolean;
|
|
@@ -43,6 +44,7 @@ export type Message =
|
|
|
43
44
|
type: "plan";
|
|
44
45
|
content: string;
|
|
45
46
|
patches: FilePatch[];
|
|
47
|
+
diffLines?: DiffLine[][];
|
|
46
48
|
applied: boolean;
|
|
47
49
|
};
|
|
48
50
|
|
package/src/utils/chat.ts
CHANGED
|
@@ -195,12 +195,22 @@ export function toCloneUrl(url: string): string {
|
|
|
195
195
|
return clean.endsWith(".git") ? clean : `${clean}.git`;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
// Replace buildApiMessages in src/utils/chat.ts
|
|
199
|
+
|
|
198
200
|
function buildApiMessages(
|
|
199
201
|
messages: Message[],
|
|
200
202
|
): { role: string; content: string }[] {
|
|
201
203
|
const recent = messages.slice(-MAX_HISTORY);
|
|
202
204
|
|
|
203
|
-
|
|
205
|
+
const writtenFiles: string[] = [];
|
|
206
|
+
for (const m of recent) {
|
|
207
|
+
if (m.type === "tool" && m.toolName === "write-file" && m.approved) {
|
|
208
|
+
const pathMatch = m.content.match(/^(.+?)\s*\(/);
|
|
209
|
+
if (pathMatch?.[1]) writtenFiles.push(pathMatch[1]);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return recent.map((m, idx) => {
|
|
204
214
|
if (m.type === "tool") {
|
|
205
215
|
if (!m.approved) {
|
|
206
216
|
return {
|
|
@@ -209,11 +219,39 @@ function buildApiMessages(
|
|
|
209
219
|
"The tool call was denied by the user. Please respond without using that tool.",
|
|
210
220
|
};
|
|
211
221
|
}
|
|
222
|
+
|
|
223
|
+
if (m.toolName === "write-file") {
|
|
224
|
+
const remaining = writtenFiles.slice(
|
|
225
|
+
writtenFiles.indexOf(m.content.split(" (")[0]!) + 1,
|
|
226
|
+
);
|
|
227
|
+
const doneList = writtenFiles
|
|
228
|
+
.slice(0, writtenFiles.indexOf(m.content.split(" (")[0]!) + 1)
|
|
229
|
+
.map((f) => `- ${f}`)
|
|
230
|
+
.join("\n");
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
role: "user",
|
|
234
|
+
content: [
|
|
235
|
+
`Tool result for write-file (${m.content}):`,
|
|
236
|
+
"",
|
|
237
|
+
m.result,
|
|
238
|
+
"",
|
|
239
|
+
`Files written so far:`,
|
|
240
|
+
doneList || `- ${m.content.split(" (")[0]}`,
|
|
241
|
+
"",
|
|
242
|
+
remaining.length > 0
|
|
243
|
+
? `Continue with the NEXT file. Do NOT rewrite any file already written above.`
|
|
244
|
+
: `All files written. Summarize what was created.`,
|
|
245
|
+
].join("\n"),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
212
249
|
return {
|
|
213
250
|
role: "user",
|
|
214
|
-
content: `
|
|
251
|
+
content: `Tool result for ${m.toolName} (${m.content}):\n\n${m.result}\n\nPlease continue your response based on this output.`,
|
|
215
252
|
};
|
|
216
253
|
}
|
|
254
|
+
|
|
217
255
|
return { role: m.role, content: m.content };
|
|
218
256
|
});
|
|
219
257
|
}
|
|
@@ -262,7 +300,7 @@ export async function callChat(
|
|
|
262
300
|
}
|
|
263
301
|
|
|
264
302
|
const controller = new AbortController();
|
|
265
|
-
const timer = setTimeout(() => controller.abort(),
|
|
303
|
+
const timer = setTimeout(() => controller.abort(), 180_000);
|
|
266
304
|
abortSignal?.addEventListener("abort", () => controller.abort());
|
|
267
305
|
|
|
268
306
|
try {
|
|
@@ -310,7 +348,22 @@ export async function callChat(
|
|
|
310
348
|
}
|
|
311
349
|
} catch (err) {
|
|
312
350
|
clearTimeout(timer);
|
|
313
|
-
if (err instanceof Error && err.name === "AbortError")
|
|
351
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
352
|
+
if (abortSignal?.aborted) throw err;
|
|
353
|
+
if (retries > 0) {
|
|
354
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
355
|
+
return callChat(
|
|
356
|
+
provider,
|
|
357
|
+
systemPrompt,
|
|
358
|
+
messages,
|
|
359
|
+
abortSignal,
|
|
360
|
+
retries - 1,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
throw new Error(
|
|
364
|
+
"Request timed out. The model took too long to respond (>3 min).",
|
|
365
|
+
);
|
|
366
|
+
}
|
|
314
367
|
if (retries > 0) {
|
|
315
368
|
await new Promise((r) => setTimeout(r, 1500));
|
|
316
369
|
return callChat(
|
|
@@ -1,48 +1,33 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Classifies user message intent to scope which tools the LLM is allowed to use.
|
|
3
|
-
*
|
|
4
|
-
* readonly → only read/search/fetch tools exposed (no write, delete, shell)
|
|
5
|
-
* mutating → all tools exposed
|
|
6
|
-
* any → all tools exposed (ambiguous / can't tell)
|
|
7
|
-
*/
|
|
8
1
|
export type Intent = "readonly" | "mutating" | "any";
|
|
9
2
|
|
|
10
3
|
const READONLY_PATTERNS: RegExp[] = [
|
|
11
|
-
// listing / exploring
|
|
12
4
|
/\b(list|ls|dir|show|display|print|dump)\b/i,
|
|
13
5
|
/\bwhat(('?s| is| are| does)\b| files| folder)/i,
|
|
14
6
|
/\b(folder|directory|file) (structure|tree|layout|contents?)\b/i,
|
|
15
7
|
/\bexplore\b/i,
|
|
16
8
|
|
|
17
|
-
// reading / explaining
|
|
18
9
|
/\b(read|open|view|look at|check out|inspect|peek)\b/i,
|
|
19
10
|
/\b(explain|describe|summarize|summarise|tell me about|walk me through)\b/i,
|
|
20
11
|
/\bhow does\b/i,
|
|
21
12
|
/\bwhat('?s| is) (in|inside|this|that|the)\b/i,
|
|
22
13
|
|
|
23
|
-
// searching
|
|
24
14
|
/\b(find|search|grep|locate|where is|where are)\b/i,
|
|
25
15
|
/\b(look for|scan|trace)\b/i,
|
|
26
16
|
|
|
27
|
-
// understanding
|
|
28
17
|
/\bunderstand\b/i,
|
|
29
18
|
/\bshow me (how|what|where|why)\b/i,
|
|
30
19
|
];
|
|
31
20
|
|
|
32
21
|
const MUTATING_PATTERNS: RegExp[] = [
|
|
33
|
-
// writing
|
|
34
22
|
/\b(write|create|make|generate|add|build|scaffold|init|initialize|setup|set up)\b/i,
|
|
35
23
|
/\b(new file|new folder|new component|new page|new route)\b/i,
|
|
36
24
|
|
|
37
|
-
// editing
|
|
38
25
|
/\b(edit|modify|update|change|refactor|rename|move|migrate)\b/i,
|
|
39
26
|
/\b(fix|patch|resolve|correct|debug|repair)\b/i,
|
|
40
27
|
/\b(implement|add .+ to|insert|inject|append|prepend)\b/i,
|
|
41
28
|
|
|
42
|
-
// deleting
|
|
43
29
|
/\b(delete|remove|drop|clean ?up|purge|wipe)\b/i,
|
|
44
30
|
|
|
45
|
-
// running
|
|
46
31
|
/\b(run|execute|install|deploy|build|test|start|launch|compile|lint|format)\b/i,
|
|
47
32
|
];
|
|
48
33
|
|
package/src/utils/thinking.tsx
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
|
+
const TIPS = [
|
|
4
|
+
"use /auto to toggle auto-approve for safe tools",
|
|
5
|
+
"ctrl+f to toggle force-all mode",
|
|
6
|
+
"shift+enter for a new line in the input",
|
|
7
|
+
"↑ / ↓ arrows navigate message history",
|
|
8
|
+
"ctrl+w deletes the previous word",
|
|
9
|
+
"ctrl+delete deletes the next word",
|
|
10
|
+
"/timeline to browse commit history",
|
|
11
|
+
"/review to analyze the current codebase",
|
|
12
|
+
"/memory list to view stored memories",
|
|
13
|
+
"/chat list to see saved conversations",
|
|
14
|
+
"esc cancels a running response",
|
|
15
|
+
"/clear history resets session memory",
|
|
16
|
+
"tab autocompletes slash commands",
|
|
17
|
+
"lens commit --auto for fast AI commits",
|
|
18
|
+
"lens run \"bun dev\" to watch and auto-fix errors",
|
|
19
|
+
];
|
|
20
|
+
|
|
3
21
|
const PHRASES: Record<string, string[]> = {
|
|
4
22
|
general: [
|
|
5
23
|
"marinating on that... 🍖",
|
|
@@ -321,6 +339,52 @@ const PHRASES: Record<string, string[]> = {
|
|
|
321
339
|
|
|
322
340
|
export type ThinkingKind = keyof typeof PHRASES;
|
|
323
341
|
|
|
342
|
+
export function useThinkingTip(active: boolean, intervalMs = 8000): string {
|
|
343
|
+
const [index, setIndex] = useState(() => Math.floor(Math.random() * TIPS.length));
|
|
344
|
+
const usedRef = useRef<Set<number>>(new Set());
|
|
345
|
+
|
|
346
|
+
useEffect(() => {
|
|
347
|
+
if (!active) return;
|
|
348
|
+
const pickUnused = () => {
|
|
349
|
+
if (usedRef.current.size >= TIPS.length) usedRef.current.clear();
|
|
350
|
+
let next: number;
|
|
351
|
+
do {
|
|
352
|
+
next = Math.floor(Math.random() * TIPS.length);
|
|
353
|
+
} while (usedRef.current.has(next));
|
|
354
|
+
usedRef.current.add(next);
|
|
355
|
+
return next;
|
|
356
|
+
};
|
|
357
|
+
setIndex(pickUnused());
|
|
358
|
+
const id = setInterval(() => setIndex(pickUnused()), intervalMs);
|
|
359
|
+
return () => clearInterval(id);
|
|
360
|
+
}, [active, intervalMs]);
|
|
361
|
+
|
|
362
|
+
return TIPS[index]!;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function useThinkingTimer(active: boolean): string {
|
|
366
|
+
const [seconds, setSeconds] = useState(0);
|
|
367
|
+
const startRef = useRef<number | null>(null);
|
|
368
|
+
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
if (active) {
|
|
371
|
+
startRef.current = Date.now();
|
|
372
|
+
setSeconds(0);
|
|
373
|
+
const id = setInterval(() => {
|
|
374
|
+
setSeconds(Math.floor((Date.now() - (startRef.current ?? Date.now())) / 1000));
|
|
375
|
+
}, 1000);
|
|
376
|
+
return () => clearInterval(id);
|
|
377
|
+
} else {
|
|
378
|
+
startRef.current = null;
|
|
379
|
+
setSeconds(0);
|
|
380
|
+
}
|
|
381
|
+
}, [active]);
|
|
382
|
+
|
|
383
|
+
if (!active || seconds === 0) return "";
|
|
384
|
+
if (seconds < 60) return `${seconds}s`;
|
|
385
|
+
return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
386
|
+
}
|
|
387
|
+
|
|
324
388
|
export function useThinkingPhrase(
|
|
325
389
|
active: boolean,
|
|
326
390
|
kind: ThinkingKind = "general",
|
|
@@ -136,41 +136,56 @@ export const writeFileTool: Tool<WriteFileInput> = {
|
|
|
136
136
|
systemPromptEntry: (i) =>
|
|
137
137
|
`### ${i}. write-file — create or overwrite a file\n<write-file>\n{"path": "data/output.csv", "content": "col1,col2\\nval1,val2"}\n</write-file>`,
|
|
138
138
|
parseInput: (body) => {
|
|
139
|
-
const tryParse = (s: string) => {
|
|
139
|
+
const tryParse = (s: string): WriteFileInput | null => {
|
|
140
140
|
try {
|
|
141
|
-
const parsed = JSON.parse(s) as { path
|
|
142
|
-
if (
|
|
143
|
-
|
|
141
|
+
const parsed = JSON.parse(s) as { path?: unknown; content?: unknown };
|
|
142
|
+
if (
|
|
143
|
+
typeof parsed.path !== "string" ||
|
|
144
|
+
typeof parsed.content !== "string"
|
|
145
|
+
)
|
|
146
|
+
return null;
|
|
147
|
+
return {
|
|
148
|
+
path: parsed.path.replace(/\\/g, "/"),
|
|
149
|
+
content: parsed.content,
|
|
150
|
+
};
|
|
144
151
|
} catch {
|
|
145
152
|
return null;
|
|
146
153
|
}
|
|
147
154
|
};
|
|
148
155
|
|
|
156
|
+
// attempt 1: parse as-is
|
|
149
157
|
const first = tryParse(body.trim());
|
|
150
158
|
if (first) return first;
|
|
151
159
|
|
|
160
|
+
// attempt 2: escape unescaped control characters
|
|
152
161
|
try {
|
|
153
162
|
const sanitized = body
|
|
154
163
|
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
|
|
155
|
-
.replace(
|
|
156
|
-
.replace(
|
|
157
|
-
.replace(
|
|
164
|
+
.replace(/(?<!\\)\n/g, "\\n")
|
|
165
|
+
.replace(/(?<!\\)\r/g, "\\r")
|
|
166
|
+
.replace(/(?<!\\)\t/g, "\\t");
|
|
158
167
|
const second = tryParse(sanitized);
|
|
159
168
|
if (second) return second;
|
|
160
169
|
} catch {}
|
|
161
170
|
|
|
171
|
+
// attempt 3: regex extraction as last resort
|
|
162
172
|
const pathMatch = body.match(/"path"\s*:\s*"([^"]+)"/);
|
|
163
|
-
const contentMatch = body.match(/"content"\s*:\s*"([\s\S]
|
|
164
|
-
if (pathMatch && contentMatch
|
|
173
|
+
const contentMatch = body.match(/"content"\s*:\s*"([\s\S]*?)"\s*}?\s*$/);
|
|
174
|
+
if (pathMatch?.[1] && contentMatch?.[1] !== undefined) {
|
|
165
175
|
return {
|
|
166
|
-
path: pathMatch[1]
|
|
167
|
-
content: contentMatch[1]
|
|
176
|
+
path: pathMatch[1].replace(/\\/g, "/"),
|
|
177
|
+
content: contentMatch[1]
|
|
178
|
+
.replace(/\\n/g, "\n")
|
|
179
|
+
.replace(/\\r/g, "\r")
|
|
180
|
+
.replace(/\\t/g, "\t"),
|
|
168
181
|
};
|
|
169
182
|
}
|
|
170
183
|
|
|
171
184
|
return null;
|
|
172
185
|
},
|
|
173
|
-
|
|
186
|
+
// null-safe — parseInput can return null if AI sends malformed JSON
|
|
187
|
+
summariseInput: (input) =>
|
|
188
|
+
input ? `${input.path} (${input.content.length} bytes)` : "unknown file",
|
|
174
189
|
execute: ({ path: filePath, content }, ctx) => ({
|
|
175
190
|
kind: "text",
|
|
176
191
|
value: writeFile(filePath, content, ctx.repoPath),
|