@ridit/lens 0.3.7 → 0.3.9
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 +105368 -274002
- package/package.json +13 -19
- package/src/colors.ts +15 -15
- package/src/commands/chat.tsx +32 -23
- package/src/commands/provider.tsx +11 -238
- package/src/commands/repo.tsx +66 -120
- package/src/commands/timeline.tsx +11 -22
- package/src/components/ChatView.tsx +238 -0
- package/src/components/Message.tsx +46 -0
- package/src/components/ToolCall.tsx +67 -0
- package/src/components/chat/ChatView.tsx +550 -0
- package/src/components/chat/Message.tsx +152 -0
- package/src/components/chat/StatusBar.tsx +214 -0
- package/src/components/chat/TextArea.tsx +173 -176
- package/src/components/provider/ApiKeyStep.tsx +207 -199
- package/src/components/provider/ModelStep.tsx +90 -88
- package/src/components/provider/ProviderSetup.tsx +331 -0
- package/src/components/provider/ProviderTypeStep.tsx +53 -61
- package/src/components/repo/StepRow.tsx +68 -69
- package/src/components/timeline/TimelineView.tsx +840 -0
- package/src/components/toolcall-utils.ts +103 -0
- package/src/components/watch/RunView.tsx +497 -0
- package/src/hooks/useChatInput.ts +49 -0
- package/src/hooks/useCommandHandler.ts +117 -0
- package/src/index.tsx +386 -139
- package/src/utils/git.ts +149 -155
- package/src/utils/repo.ts +62 -69
- package/src/utils/thinking.tsx +64 -0
- package/src/utils/watch.ts +165 -307
- package/tests/message.test.ts +38 -0
- package/tests/toolcall-utils.test.ts +111 -0
- package/tsconfig.json +8 -24
- package/CLAUDE.md +0 -50
- package/LENS.md +0 -48
- package/LICENSE +0 -21
- package/README.md +0 -93
- package/addons/README.md +0 -55
- package/addons/clean-cache.js +0 -48
- package/addons/generate-readme.js +0 -67
- package/addons/git-stats.js +0 -29
- package/addons/run-tests.js +0 -127
- package/src/commands/commit.tsx +0 -668
- package/src/commands/review.tsx +0 -294
- package/src/commands/run.tsx +0 -56
- package/src/commands/task.tsx +0 -36
- package/src/components/chat/ChatMessage.tsx +0 -195
- package/src/components/chat/ChatOverlays.tsx +0 -399
- package/src/components/chat/ChatRunner.tsx +0 -517
- package/src/components/chat/hooks/useChat.ts +0 -631
- package/src/components/chat/hooks/useChatInput.ts +0 -79
- package/src/components/chat/hooks/useCommandHandlers.ts +0 -327
- package/src/components/provider/ProviderPicker.tsx +0 -76
- package/src/components/provider/RemoveProviderStep.tsx +0 -82
- package/src/components/repo/DiffViewer.tsx +0 -175
- package/src/components/repo/FileReviewer.tsx +0 -70
- package/src/components/repo/FileViewer.tsx +0 -60
- package/src/components/repo/IssueFixer.tsx +0 -666
- package/src/components/repo/LensFileMenu.tsx +0 -115
- package/src/components/repo/NoProviderPrompt.tsx +0 -28
- package/src/components/repo/PreviewRunner.tsx +0 -217
- package/src/components/repo/RepoAnalysis.tsx +0 -534
- package/src/components/task/TaskRunner.tsx +0 -396
- package/src/components/timeline/CommitDetail.tsx +0 -272
- package/src/components/timeline/CommitList.tsx +0 -162
- package/src/components/timeline/TimelineChat.tsx +0 -166
- package/src/components/timeline/TimelineRunner.tsx +0 -1285
- package/src/components/watch/RunRunner.tsx +0 -929
- package/src/prompts/fewshot.ts +0 -252
- package/src/prompts/index.ts +0 -2
- package/src/prompts/system.ts +0 -285
- package/src/tools/chart.ts +0 -202
- package/src/tools/convert-image.ts +0 -312
- package/src/tools/files.ts +0 -253
- package/src/tools/git.ts +0 -603
- package/src/tools/index.ts +0 -17
- package/src/tools/pdf.ts +0 -164
- package/src/tools/shell.ts +0 -96
- package/src/tools/view-image.ts +0 -335
- package/src/tools/web.ts +0 -212
- package/src/types/chat.ts +0 -86
- package/src/types/config.ts +0 -20
- package/src/types/repo.ts +0 -54
- package/src/utils/addons/loadAddons.ts +0 -34
- package/src/utils/ai.ts +0 -321
- package/src/utils/chat.ts +0 -326
- package/src/utils/chatHistory.ts +0 -121
- package/src/utils/config.ts +0 -61
- package/src/utils/files.ts +0 -105
- package/src/utils/intentClassifier.ts +0 -58
- package/src/utils/lensfile.ts +0 -142
- package/src/utils/llm.ts +0 -81
- package/src/utils/memory.ts +0 -209
- package/src/utils/preview.ts +0 -119
- package/src/utils/stats.ts +0 -174
- package/src/utils/tools/builtins.ts +0 -377
- package/src/utils/tools/registry.ts +0 -105
package/src/utils/watch.ts
CHANGED
|
@@ -1,307 +1,165 @@
|
|
|
1
|
-
import { spawn, type ChildProcess } from "child_process";
|
|
2
|
-
import { readFileSync, existsSync } from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
|
|
5
|
-
export type WatchProcess = {
|
|
6
|
-
kill: () => void;
|
|
7
|
-
onLog: (cb: (line: string, isErr: boolean) => void) => void;
|
|
8
|
-
onError: (cb: (chunk: ErrorChunk) => void) => void;
|
|
9
|
-
onExit: (cb: (code: number | null) => void) => void;
|
|
10
|
-
onInputRequest: (cb: (prompt: string) => void) => void;
|
|
11
|
-
sendInput: (text: string) => void;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export type ErrorChunk = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
/
|
|
35
|
-
/
|
|
36
|
-
/
|
|
37
|
-
/
|
|
38
|
-
/
|
|
39
|
-
/
|
|
40
|
-
/
|
|
41
|
-
/
|
|
42
|
-
/
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
]
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const processLine = (line: string, isErr: boolean) => {
|
|
168
|
-
recentLines.push(line);
|
|
169
|
-
if (recentLines.length > 30) recentLines.shift();
|
|
170
|
-
|
|
171
|
-
logCallbacks.forEach((cb) => cb(line, isErr));
|
|
172
|
-
|
|
173
|
-
if (isErrorLine(line)) {
|
|
174
|
-
errorBuffer.push(line);
|
|
175
|
-
if (errorTimer) clearTimeout(errorTimer);
|
|
176
|
-
errorTimer = setTimeout(flushError, 300);
|
|
177
|
-
} else if (errorBuffer.length > 0) {
|
|
178
|
-
errorBuffer.push(line);
|
|
179
|
-
if (errorTimer) clearTimeout(errorTimer);
|
|
180
|
-
errorTimer = setTimeout(flushError, 300);
|
|
181
|
-
} else if (!isErr && isInputRequest(line)) {
|
|
182
|
-
inputRequestCallbacks.forEach((cb) => cb(line.trim()));
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
child.stdout?.on("data", (data: Buffer) => {
|
|
187
|
-
data
|
|
188
|
-
.toString()
|
|
189
|
-
.split("\n")
|
|
190
|
-
.filter(Boolean)
|
|
191
|
-
.forEach((l) => processLine(l, false));
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
child.stderr?.on("data", (data: Buffer) => {
|
|
195
|
-
data
|
|
196
|
-
.toString()
|
|
197
|
-
.split("\n")
|
|
198
|
-
.filter(Boolean)
|
|
199
|
-
.forEach((l) => processLine(l, true));
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
child.on("close", (code) => {
|
|
203
|
-
if (errorTimer) clearTimeout(errorTimer);
|
|
204
|
-
flushError();
|
|
205
|
-
exitCallbacks.forEach((cb) => cb(code));
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
kill: () => child.kill(),
|
|
210
|
-
onLog: (cb) => logCallbacks.push(cb),
|
|
211
|
-
onError: (cb) => errorCallbacks.push(cb),
|
|
212
|
-
onExit: (cb) => exitCallbacks.push(cb),
|
|
213
|
-
onInputRequest: (cb) => inputRequestCallbacks.push(cb),
|
|
214
|
-
sendInput: (text) => {
|
|
215
|
-
child.stdin?.write(text + "\n");
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export function readFileContext(
|
|
221
|
-
filePath: string,
|
|
222
|
-
repoPath: string,
|
|
223
|
-
lineNumber?: number,
|
|
224
|
-
): string {
|
|
225
|
-
const candidates = [
|
|
226
|
-
filePath,
|
|
227
|
-
path.join(repoPath, filePath),
|
|
228
|
-
path.resolve(repoPath, filePath),
|
|
229
|
-
];
|
|
230
|
-
|
|
231
|
-
for (const p of candidates) {
|
|
232
|
-
if (!existsSync(p)) continue;
|
|
233
|
-
try {
|
|
234
|
-
const content = readFileSync(p, "utf-8");
|
|
235
|
-
if (!lineNumber) return content.slice(0, 3000);
|
|
236
|
-
|
|
237
|
-
const lines = content.split("\n");
|
|
238
|
-
const start = Math.max(0, lineNumber - 30);
|
|
239
|
-
const end = Math.min(lines.length, lineNumber + 30);
|
|
240
|
-
return lines
|
|
241
|
-
.slice(start, end)
|
|
242
|
-
.map((l, i) => `${start + i + 1}: ${l}`)
|
|
243
|
-
.join("\n");
|
|
244
|
-
} catch {
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return "";
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export function readPackageJson(repoPath: string): string {
|
|
252
|
-
const p = path.join(repoPath, "package.json");
|
|
253
|
-
if (!existsSync(p)) return "";
|
|
254
|
-
try {
|
|
255
|
-
const pkg = JSON.parse(readFileSync(p, "utf-8")) as Record<string, unknown>;
|
|
256
|
-
const deps = {
|
|
257
|
-
...((pkg.dependencies as object) ?? {}),
|
|
258
|
-
...((pkg.devDependencies as object) ?? {}),
|
|
259
|
-
};
|
|
260
|
-
return Object.keys(deps).slice(0, 30).join(", ");
|
|
261
|
-
} catch {
|
|
262
|
-
return "";
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export function buildWatchPrompt(
|
|
267
|
-
chunk: ErrorChunk,
|
|
268
|
-
fileContext: string,
|
|
269
|
-
deps: string,
|
|
270
|
-
repoPath: string,
|
|
271
|
-
): string {
|
|
272
|
-
return `You are a senior developer assistant watching a dev server. An error just occurred.
|
|
273
|
-
|
|
274
|
-
Error output:
|
|
275
|
-
\`\`\`
|
|
276
|
-
${chunk.lines.join("\n").slice(0, 2000)}
|
|
277
|
-
\`\`\`
|
|
278
|
-
|
|
279
|
-
${chunk.contextBefore.length > 0 ? `Log context (lines before error):\n\`\`\`\n${chunk.contextBefore.join("\n")}\n\`\`\`` : ""}
|
|
280
|
-
|
|
281
|
-
${fileContext ? `File content${chunk.lineNumber ? ` (around line ${chunk.lineNumber})` : ""}:\n\`\`\`\n${fileContext.slice(0, 2500)}\n\`\`\`` : ""}
|
|
282
|
-
|
|
283
|
-
${deps ? `Project dependencies: ${deps}` : ""}
|
|
284
|
-
Repo path: ${repoPath}
|
|
285
|
-
${chunk.filePath ? `Error in file: ${chunk.filePath}` : ""}
|
|
286
|
-
|
|
287
|
-
Respond ONLY with a JSON object (no markdown, no backticks) with this exact shape:
|
|
288
|
-
{
|
|
289
|
-
"errorSummary": "one line — what went wrong",
|
|
290
|
-
"simplified": "2-3 sentences in plain language — what this error means and why it usually happens",
|
|
291
|
-
"fix": "specific actionable fix — reference actual file names and line numbers if known",
|
|
292
|
-
"patch": null
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
If you can provide a code fix, replace "patch" with:
|
|
296
|
-
{
|
|
297
|
-
"path": "relative/file/path.ts",
|
|
298
|
-
"content": "complete corrected file content",
|
|
299
|
-
"isNew": false
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
Rules:
|
|
303
|
-
- Be specific — mention actual files, variables, function names from the error
|
|
304
|
-
- Don't be generic ("check if variable is defined") — say WHERE
|
|
305
|
-
- patch should only be included when you are confident in the fix
|
|
306
|
-
- Keep simplified under 60 words`;
|
|
307
|
-
}
|
|
1
|
+
import { spawn, type ChildProcess } from "child_process";
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
export type WatchProcess = {
|
|
6
|
+
kill: () => void;
|
|
7
|
+
onLog: (cb: (line: string, isErr: boolean) => void) => void;
|
|
8
|
+
onError: (cb: (chunk: ErrorChunk) => void) => void;
|
|
9
|
+
onExit: (cb: (code: number | null) => void) => void;
|
|
10
|
+
onInputRequest: (cb: (prompt: string) => void) => void;
|
|
11
|
+
sendInput: (text: string) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ErrorChunk = {
|
|
15
|
+
lines: string[];
|
|
16
|
+
contextBefore: string[];
|
|
17
|
+
filePath?: string;
|
|
18
|
+
lineNumber?: number;
|
|
19
|
+
timestamp: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const ERROR_PATTERNS = [
|
|
23
|
+
/error:/i,
|
|
24
|
+
/TypeError/,
|
|
25
|
+
/ReferenceError/,
|
|
26
|
+
/SyntaxError/,
|
|
27
|
+
/RangeError/,
|
|
28
|
+
/Cannot find module/,
|
|
29
|
+
/Cannot read propert/,
|
|
30
|
+
/is not defined/,
|
|
31
|
+
/is not a function/,
|
|
32
|
+
/Unhandled/,
|
|
33
|
+
/ENOENT/,
|
|
34
|
+
/EADDRINUSE/,
|
|
35
|
+
/failed to compile/i,
|
|
36
|
+
/Build failed/i,
|
|
37
|
+
/Module not found/i,
|
|
38
|
+
/unexpected token/i,
|
|
39
|
+
/Traceback \(most recent call last\)/,
|
|
40
|
+
/NameError/,
|
|
41
|
+
/AttributeError/,
|
|
42
|
+
/ImportError/,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const NOISE_PATTERNS = [/^\s*at\s+/, /^\s*\^+\s*$/, /^\s*$/, /^\s*warn/i];
|
|
46
|
+
|
|
47
|
+
const INPUT_REQUEST_PATTERNS = [/:\s*$/, /\?\s*$/, /input/i, /press\s+\w/i];
|
|
48
|
+
|
|
49
|
+
function isErrorLine(line: string): boolean {
|
|
50
|
+
return ERROR_PATTERNS.some((p) => p.test(line));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isNoise(line: string): boolean {
|
|
54
|
+
return NOISE_PATTERNS.some((p) => p.test(line));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isInputRequest(line: string): boolean {
|
|
58
|
+
const stripped = line.replace(/\x1b\[[0-9;]*m/g, "").trim();
|
|
59
|
+
if (!stripped) return false;
|
|
60
|
+
return INPUT_REQUEST_PATTERNS.some((p) => p.test(stripped));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractFilePath(lines: string[]): {
|
|
64
|
+
filePath?: string;
|
|
65
|
+
lineNumber?: number;
|
|
66
|
+
} {
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
const m = line.match(
|
|
69
|
+
/([./][\w./\\-]+\.(tsx?|jsx?|mjs|cjs|ts|js|py)):(\d+)/,
|
|
70
|
+
);
|
|
71
|
+
if (m) return { filePath: m[1], lineNumber: parseInt(m[3]!, 10) };
|
|
72
|
+
|
|
73
|
+
const pyM = line.match(/File "([^"]+\.py)",\s*line\s*(\d+)/);
|
|
74
|
+
if (pyM) return { filePath: pyM[1], lineNumber: parseInt(pyM[2]!, 10) };
|
|
75
|
+
}
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function spawnWatch(cmd: string, cwd: string): WatchProcess {
|
|
80
|
+
const [bin, ...args] = cmd.split(/\s+/) as [string, ...string[]];
|
|
81
|
+
|
|
82
|
+
const child: ChildProcess = spawn(bin, args, {
|
|
83
|
+
cwd,
|
|
84
|
+
shell: true,
|
|
85
|
+
env: { ...process.env, FORCE_COLOR: "1" },
|
|
86
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const logCbs: ((line: string, isErr: boolean) => void)[] = [];
|
|
90
|
+
const errorCbs: ((chunk: ErrorChunk) => void)[] = [];
|
|
91
|
+
const exitCbs: ((code: number | null) => void)[] = [];
|
|
92
|
+
const inputCbs: ((prompt: string) => void)[] = [];
|
|
93
|
+
|
|
94
|
+
const recentLines: string[] = [];
|
|
95
|
+
let errorBuffer: string[] = [];
|
|
96
|
+
let errorTimer: ReturnType<typeof setTimeout> | null = null;
|
|
97
|
+
const seenErrors = new Set<string>();
|
|
98
|
+
|
|
99
|
+
const flushError = () => {
|
|
100
|
+
if (errorBuffer.length === 0) return;
|
|
101
|
+
const key = errorBuffer.slice(0, 3).join("\n").slice(0, 120);
|
|
102
|
+
if (seenErrors.has(key)) { errorBuffer = []; return; }
|
|
103
|
+
seenErrors.add(key);
|
|
104
|
+
|
|
105
|
+
const { filePath, lineNumber } = extractFilePath(errorBuffer);
|
|
106
|
+
const chunk: ErrorChunk = {
|
|
107
|
+
lines: errorBuffer.filter((l) => !isNoise(l)).slice(0, 20),
|
|
108
|
+
contextBefore: recentLines.slice(-15),
|
|
109
|
+
filePath,
|
|
110
|
+
lineNumber,
|
|
111
|
+
timestamp: Date.now(),
|
|
112
|
+
};
|
|
113
|
+
errorCbs.forEach((cb) => cb(chunk));
|
|
114
|
+
errorBuffer = [];
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const processLine = (line: string, isErr: boolean) => {
|
|
118
|
+
recentLines.push(line);
|
|
119
|
+
if (recentLines.length > 30) recentLines.shift();
|
|
120
|
+
logCbs.forEach((cb) => cb(line, isErr));
|
|
121
|
+
|
|
122
|
+
if (isErrorLine(line)) {
|
|
123
|
+
errorBuffer.push(line);
|
|
124
|
+
if (errorTimer) clearTimeout(errorTimer);
|
|
125
|
+
errorTimer = setTimeout(flushError, 300);
|
|
126
|
+
} else if (errorBuffer.length > 0) {
|
|
127
|
+
errorBuffer.push(line);
|
|
128
|
+
if (errorTimer) clearTimeout(errorTimer);
|
|
129
|
+
errorTimer = setTimeout(flushError, 300);
|
|
130
|
+
} else if (!isErr && isInputRequest(line)) {
|
|
131
|
+
inputCbs.forEach((cb) => cb(line.trim()));
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
child.stdout?.on("data", (data: Buffer) =>
|
|
136
|
+
data.toString().split("\n").filter(Boolean).forEach((l) => processLine(l, false)),
|
|
137
|
+
);
|
|
138
|
+
child.stderr?.on("data", (data: Buffer) =>
|
|
139
|
+
data.toString().split("\n").filter(Boolean).forEach((l) => processLine(l, true)),
|
|
140
|
+
);
|
|
141
|
+
child.on("close", (code) => {
|
|
142
|
+
if (errorTimer) clearTimeout(errorTimer);
|
|
143
|
+
flushError();
|
|
144
|
+
exitCbs.forEach((cb) => cb(code));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
kill: () => child.kill(),
|
|
149
|
+
onLog: (cb) => logCbs.push(cb),
|
|
150
|
+
onError: (cb) => errorCbs.push(cb),
|
|
151
|
+
onExit: (cb) => exitCbs.push(cb),
|
|
152
|
+
onInputRequest: (cb) => inputCbs.push(cb),
|
|
153
|
+
sendInput: (text) => { child.stdin?.write(text + "\n"); },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function readPackageJson(repoPath: string): string {
|
|
158
|
+
const p = path.join(repoPath, "package.json");
|
|
159
|
+
if (!existsSync(p)) return "";
|
|
160
|
+
try {
|
|
161
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8")) as Record<string, unknown>;
|
|
162
|
+
const deps = { ...((pkg.dependencies as object) ?? {}), ...((pkg.devDependencies as object) ?? {}) };
|
|
163
|
+
return Object.keys(deps).slice(0, 30).join(", ");
|
|
164
|
+
} catch { return ""; }
|
|
165
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { extractText } from "../src/components/Message";
|
|
3
|
+
|
|
4
|
+
describe("extractText", () => {
|
|
5
|
+
it("returns a plain string as-is", () => {
|
|
6
|
+
expect(extractText("hello world")).toBe("hello world");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("extracts text from a content array", () => {
|
|
10
|
+
const content = [
|
|
11
|
+
{ type: "text", text: "hello " },
|
|
12
|
+
{ type: "text", text: "world" },
|
|
13
|
+
];
|
|
14
|
+
expect(extractText(content)).toBe("hello world");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("skips non-text content blocks", () => {
|
|
18
|
+
const content = [
|
|
19
|
+
{ type: "image", url: "http://example.com/img.png" },
|
|
20
|
+
{ type: "text", text: "caption" },
|
|
21
|
+
];
|
|
22
|
+
expect(extractText(content)).toBe("caption");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns empty string for null / undefined", () => {
|
|
26
|
+
expect(extractText(null)).toBe("");
|
|
27
|
+
expect(extractText(undefined)).toBe("");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns empty string for empty array", () => {
|
|
31
|
+
expect(extractText([])).toBe("");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("handles missing text field gracefully", () => {
|
|
35
|
+
const content = [{ type: "text" }];
|
|
36
|
+
expect(extractText(content)).toBe("");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { extractFileDiff, getLabel, getArgDetail } from "../src/components/toolcall-utils";
|
|
3
|
+
|
|
4
|
+
describe("getLabel", () => {
|
|
5
|
+
it("returns running label for known tools", () => {
|
|
6
|
+
expect(getLabel("read_file", true)).toBe("Reading");
|
|
7
|
+
expect(getLabel("write_file", true)).toBe("Writing");
|
|
8
|
+
expect(getLabel("bash", true)).toBe("Running");
|
|
9
|
+
expect(getLabel("ls", true)).toBe("Listing");
|
|
10
|
+
expect(getLabel("str_replace", true)).toBe("Editing");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns done label for known tools", () => {
|
|
14
|
+
expect(getLabel("read_file", false)).toBe("Read");
|
|
15
|
+
expect(getLabel("write_file", false)).toBe("Wrote");
|
|
16
|
+
expect(getLabel("bash", false)).toBe("Ran");
|
|
17
|
+
expect(getLabel("ls", false)).toBe("Listed");
|
|
18
|
+
expect(getLabel("create_file", false)).toBe("Created");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("falls back gracefully for unknown tools", () => {
|
|
22
|
+
expect(getLabel("some_tool", true)).toBe("Some tooling");
|
|
23
|
+
expect(getLabel("some_tool", false)).toBe("Some tooled");
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("extractFileDiff", () => {
|
|
28
|
+
it("returns null for non-object args", () => {
|
|
29
|
+
expect(extractFileDiff("write_file", null)).toBeNull();
|
|
30
|
+
expect(extractFileDiff("write_file", "string")).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns null when no path in args", () => {
|
|
34
|
+
expect(extractFileDiff("write_file", { content: "hello" })).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns path with empty diff for read tools", () => {
|
|
38
|
+
const result = extractFileDiff("read_file", { path: "src/foo.ts" });
|
|
39
|
+
expect(result).toEqual({ path: "src/foo.ts", removals: [], additions: [] });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("handles file_path and filename aliases", () => {
|
|
43
|
+
expect(extractFileDiff("read", { file_path: "a.ts" })?.path).toBe("a.ts");
|
|
44
|
+
expect(extractFileDiff("read", { filename: "b.ts" })?.path).toBe("b.ts");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("parses str_replace style args (old_string / new_string)", () => {
|
|
48
|
+
const result = extractFileDiff("str_replace", {
|
|
49
|
+
path: "src/foo.ts",
|
|
50
|
+
old_string: "const x = 1;\nconst y = 2;",
|
|
51
|
+
new_string: "const x = 10;",
|
|
52
|
+
});
|
|
53
|
+
expect(result?.path).toBe("src/foo.ts");
|
|
54
|
+
expect(result?.removals).toEqual(["const x = 1;", "const y = 2;"]);
|
|
55
|
+
expect(result?.additions).toEqual(["const x = 10;"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("parses old_str / new_str aliases", () => {
|
|
59
|
+
const result = extractFileDiff("edit", {
|
|
60
|
+
path: "foo.ts",
|
|
61
|
+
old_str: "old",
|
|
62
|
+
new_str: "new",
|
|
63
|
+
});
|
|
64
|
+
expect(result?.removals).toEqual(["old"]);
|
|
65
|
+
expect(result?.additions).toEqual(["new"]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("parses full content write with no previous content", () => {
|
|
69
|
+
const result = extractFileDiff("write_file", {
|
|
70
|
+
path: "new.ts",
|
|
71
|
+
content: "line1\nline2\nline3",
|
|
72
|
+
});
|
|
73
|
+
expect(result?.removals).toEqual([]);
|
|
74
|
+
expect(result?.additions).toEqual(["line1", "line2", "line3"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("includes prev content as removals when _prevContent is present", () => {
|
|
78
|
+
const result = extractFileDiff("write_file", {
|
|
79
|
+
path: "existing.ts",
|
|
80
|
+
content: "new content",
|
|
81
|
+
_prevContent: "old content\nmore old",
|
|
82
|
+
});
|
|
83
|
+
expect(result?.removals).toEqual(["old content", "more old"]);
|
|
84
|
+
expect(result?.additions).toEqual(["new content"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("returns empty diff for file tool with no content args", () => {
|
|
88
|
+
const result = extractFileDiff("write_file", { path: "foo.ts" });
|
|
89
|
+
expect(result).toEqual({ path: "foo.ts", removals: [], additions: [] });
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("getArgDetail", () => {
|
|
94
|
+
it("returns path when present", () => {
|
|
95
|
+
expect(getArgDetail("read", { path: "src/index.ts" })).toBe("src/index.ts");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("prefers path over other fields", () => {
|
|
99
|
+
expect(getArgDetail("bash", { path: "foo.ts", command: "ls" })).toBe("foo.ts");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("falls back to query/command when no path", () => {
|
|
103
|
+
expect(getArgDetail("bash", { command: "npm install" })).toBe("npm install");
|
|
104
|
+
expect(getArgDetail("grep", { query: "TODO" })).toBe("TODO");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("handles non-object args", () => {
|
|
108
|
+
expect(getArgDetail("tool", null)).toBe("");
|
|
109
|
+
expect(getArgDetail("tool", "plain")).toBe("plain");
|
|
110
|
+
});
|
|
111
|
+
});
|
package/tsconfig.json
CHANGED
|
@@ -1,24 +1,8 @@
|
|
|
1
|
-
{
|
|
2
|
-
"
|
|
3
|
-
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"outDir": "dist",
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"verbatimModuleSyntax": true,
|
|
12
|
-
|
|
13
|
-
"strict": true,
|
|
14
|
-
"skipLibCheck": true,
|
|
15
|
-
"noFallthroughCasesInSwitch": true,
|
|
16
|
-
"noUncheckedIndexedAccess": true,
|
|
17
|
-
"noImplicitOverride": true,
|
|
18
|
-
|
|
19
|
-
"noUnusedLocals": false,
|
|
20
|
-
"noUnusedParameters": false,
|
|
21
|
-
"noPropertyAccessFromIndexSignature": false
|
|
22
|
-
},
|
|
23
|
-
"include": ["src"]
|
|
24
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"extends": "@ridit/typescript-config/base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"jsx": "react-jsx",
|
|
5
|
+
"outDir": "dist"
|
|
6
|
+
},
|
|
7
|
+
"include": ["src"]
|
|
8
|
+
}
|