@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
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export const TOOL_LABELS: Record<string, [string, string]> = {
|
|
2
|
+
read_file: ["Reading", "Read"],
|
|
3
|
+
read: ["Reading", "Read"],
|
|
4
|
+
view_file: ["Reading", "Read"],
|
|
5
|
+
cat: ["Reading", "Read"],
|
|
6
|
+
write_file: ["Writing", "Wrote"],
|
|
7
|
+
write: ["Writing", "Wrote"],
|
|
8
|
+
create_file: ["Creating", "Created"],
|
|
9
|
+
create: ["Creating", "Created"],
|
|
10
|
+
overwrite_file: ["Overwriting", "Overwrote"],
|
|
11
|
+
edit_file: ["Editing", "Edited"],
|
|
12
|
+
edit: ["Editing", "Edited"],
|
|
13
|
+
str_replace: ["Editing", "Edited"],
|
|
14
|
+
patch_file: ["Patching", "Patched"],
|
|
15
|
+
grep: ["Searching", "Searched"],
|
|
16
|
+
glob: ["Finding files", "Found files"],
|
|
17
|
+
search: ["Searching", "Searched"],
|
|
18
|
+
ripgrep: ["Searching", "Searched"],
|
|
19
|
+
bash: ["Running", "Ran"],
|
|
20
|
+
run_command: ["Running", "Ran"],
|
|
21
|
+
execute: ["Running", "Ran"],
|
|
22
|
+
shell: ["Running", "Ran"],
|
|
23
|
+
ls: ["Listing", "Listed"],
|
|
24
|
+
list: ["Listing", "Listed"],
|
|
25
|
+
list_files: ["Listing", "Listed"],
|
|
26
|
+
web_search: ["Searching web", "Searched web"],
|
|
27
|
+
web_fetch: ["Fetching", "Fetched"],
|
|
28
|
+
fetch: ["Fetching", "Fetched"],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const FILE_WRITE_TOOLS = new Set([
|
|
32
|
+
"write_file", "edit_file", "create_file", "str_replace",
|
|
33
|
+
"edit", "write", "create", "overwrite_file", "patch_file",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
export const FILE_READ_TOOLS = new Set([
|
|
37
|
+
"read_file", "read", "view_file", "cat",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
export interface FileDiff {
|
|
41
|
+
path: string;
|
|
42
|
+
removals: string[];
|
|
43
|
+
additions: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function extractFileDiff(tool: string, args: unknown): FileDiff | null {
|
|
47
|
+
if (typeof args !== "object" || !args) return null;
|
|
48
|
+
const a = args as Record<string, unknown>;
|
|
49
|
+
|
|
50
|
+
const path = String(a.path ?? a.file_path ?? a.filename ?? "");
|
|
51
|
+
if (!path) return null;
|
|
52
|
+
|
|
53
|
+
if (FILE_READ_TOOLS.has(tool)) {
|
|
54
|
+
return { path, removals: [], additions: [] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const old = a.old_string ?? a.old_str ?? a.old;
|
|
58
|
+
const newContent = a.new_string ?? a.new_str ?? a.new;
|
|
59
|
+
if (old !== undefined || newContent !== undefined) {
|
|
60
|
+
return {
|
|
61
|
+
path,
|
|
62
|
+
removals: old ? String(old).split("\n") : [],
|
|
63
|
+
additions: newContent ? String(newContent).split("\n") : [],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const content = a.content ?? a.new_content;
|
|
68
|
+
if (content !== undefined) {
|
|
69
|
+
const prev = a._prevContent;
|
|
70
|
+
return {
|
|
71
|
+
path,
|
|
72
|
+
removals: prev ? String(prev).split("\n") : [],
|
|
73
|
+
additions: String(content).split("\n"),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { path, removals: [], additions: [] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getArgDetail(tool: string, args: unknown): string {
|
|
81
|
+
if (typeof args !== "object" || !args) return String(args ?? "");
|
|
82
|
+
const a = args as Record<string, unknown>;
|
|
83
|
+
|
|
84
|
+
const path = a.path ?? a.file_path ?? a.filename;
|
|
85
|
+
if (path) return String(path);
|
|
86
|
+
|
|
87
|
+
const pattern = a.pattern ?? a.query ?? a.command ?? a.cmd;
|
|
88
|
+
if (pattern) return String(pattern).slice(0, 50);
|
|
89
|
+
|
|
90
|
+
return Object.values(a)
|
|
91
|
+
.filter((v) => typeof v === "string")
|
|
92
|
+
.join(", ")
|
|
93
|
+
.slice(0, 50);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getLabel(tool: string, running: boolean): string {
|
|
97
|
+
const entry = TOOL_LABELS[tool];
|
|
98
|
+
if (entry) return running ? entry[0] : entry[1];
|
|
99
|
+
const base = tool.replace(/_/g, " ");
|
|
100
|
+
return running
|
|
101
|
+
? base.charAt(0).toUpperCase() + base.slice(1) + "ing"
|
|
102
|
+
: base.charAt(0).toUpperCase() + base.slice(1) + "ed";
|
|
103
|
+
}
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
3
|
+
import Spinner from "ink-spinner";
|
|
4
|
+
import { chat, getSystemPrompt } from "@ridit/lens-core";
|
|
5
|
+
import { spawnWatch, readPackageJson } from "../../utils/watch";
|
|
6
|
+
import type { ErrorChunk, WatchProcess } from "../../utils/watch";
|
|
7
|
+
import { ACCENT, GREEN, RED } from "../../colors";
|
|
8
|
+
|
|
9
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
type Stage = "running" | "crashed";
|
|
12
|
+
|
|
13
|
+
type ToolEntry = { tool: string; label: string };
|
|
14
|
+
|
|
15
|
+
type Investigation = {
|
|
16
|
+
id: string;
|
|
17
|
+
chunk: ErrorChunk;
|
|
18
|
+
status: "thinking" | "done" | "failed";
|
|
19
|
+
toolLog: ToolEntry[];
|
|
20
|
+
response: string;
|
|
21
|
+
startTime: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type PendingError = { id: string; chunk: ErrorChunk };
|
|
25
|
+
|
|
26
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
let _idCounter = 0;
|
|
29
|
+
const nextId = () => (++_idCounter).toString(36);
|
|
30
|
+
|
|
31
|
+
function stripAnsi(s: string) {
|
|
32
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const TOOL_ICONS: Record<string, string> = {
|
|
36
|
+
bash: "$", read: "r", write: "w", grep: "/", ls: "d", remember: "·",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function toolLabel(tool: string, args: unknown): string {
|
|
40
|
+
if (!args || typeof args !== "object") return tool;
|
|
41
|
+
const a = args as Record<string, unknown>;
|
|
42
|
+
switch (tool) {
|
|
43
|
+
case "read": return String(a.path ?? a.file_path ?? "");
|
|
44
|
+
case "write": return String(a.path ?? a.file_path ?? "");
|
|
45
|
+
case "bash": return String(a.command ?? "").slice(0, 60);
|
|
46
|
+
case "grep": return String(a.pattern ?? "");
|
|
47
|
+
case "ls": return String(a.path ?? ".");
|
|
48
|
+
default: return "";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildWatchSystemPrompt(repoPath: string, cmd: string, deps: string): string {
|
|
53
|
+
const base = getSystemPrompt(repoPath);
|
|
54
|
+
return `${base}
|
|
55
|
+
|
|
56
|
+
## Watch Mode
|
|
57
|
+
You are monitoring a running dev process: \`${cmd}\`
|
|
58
|
+
${deps ? `Project dependencies: ${deps}` : ""}
|
|
59
|
+
|
|
60
|
+
When given an error:
|
|
61
|
+
1. Use read, grep, and ls tools to investigate — find the actual cause
|
|
62
|
+
2. If you can fix it, use the write tool to apply the fix directly
|
|
63
|
+
3. After investigating, respond with a short summary: what the error was, what caused it, what you did to fix it (or how to fix it manually)
|
|
64
|
+
|
|
65
|
+
Work autonomously. Don't ask for permission — investigate and fix, then summarize.`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildErrorMessage(chunk: ErrorChunk): string {
|
|
69
|
+
const lines = chunk.lines.join("\n").slice(0, 2000);
|
|
70
|
+
const ctx = chunk.contextBefore.length > 0
|
|
71
|
+
? `\nContext before error:\n\`\`\`\n${chunk.contextBefore.join("\n")}\n\`\`\``
|
|
72
|
+
: "";
|
|
73
|
+
const loc = chunk.filePath
|
|
74
|
+
? `\nError location: ${chunk.filePath}${chunk.lineNumber ? `:${chunk.lineNumber}` : ""}`
|
|
75
|
+
: "";
|
|
76
|
+
return `Error detected in dev process:\n\`\`\`\n${lines}\n\`\`\`${ctx}${loc}\n\nInvestigate and fix this.`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Sub-components ────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function LogPane({ logs }: { logs: { text: string; isErr: boolean }[] }) {
|
|
82
|
+
const rows = process.stdout.rows ?? 24;
|
|
83
|
+
const visible = logs.slice(-Math.max(4, rows - 14));
|
|
84
|
+
return (
|
|
85
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
86
|
+
{visible.map((l, i) => (
|
|
87
|
+
<Text key={i} color={l.isErr ? RED : "gray"} dimColor={!l.isErr}>
|
|
88
|
+
{l.text.slice(0, 200)}
|
|
89
|
+
</Text>
|
|
90
|
+
))}
|
|
91
|
+
</Box>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ConfirmCard({ chunk }: { chunk: ErrorChunk }) {
|
|
96
|
+
const cols = process.stdout.columns ?? 80;
|
|
97
|
+
const rule = "─".repeat(Math.min(cols - 2, 72));
|
|
98
|
+
const preview = chunk.lines[0]?.slice(0, 70) ?? "error detected";
|
|
99
|
+
return (
|
|
100
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
101
|
+
<Text color="gray" dimColor>{rule}</Text>
|
|
102
|
+
<Box gap={1}>
|
|
103
|
+
<Text color={RED}>✖</Text>
|
|
104
|
+
<Text color="white">{preview}</Text>
|
|
105
|
+
</Box>
|
|
106
|
+
{chunk.filePath && (
|
|
107
|
+
<Box marginLeft={2} gap={1}>
|
|
108
|
+
<Text color="gray" dimColor>›</Text>
|
|
109
|
+
<Text color="gray" dimColor>
|
|
110
|
+
{chunk.filePath}{chunk.lineNumber ? `:${chunk.lineNumber}` : ""}
|
|
111
|
+
</Text>
|
|
112
|
+
</Box>
|
|
113
|
+
)}
|
|
114
|
+
<Box marginLeft={2} marginTop={1} gap={1}>
|
|
115
|
+
<Text color={GREEN}>y</Text><Text color="gray" dimColor> investigate · </Text>
|
|
116
|
+
<Text color="gray">n</Text><Text color="gray" dimColor> skip</Text>
|
|
117
|
+
</Box>
|
|
118
|
+
</Box>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function ThinkingCard({ inv }: { inv: Investigation }) {
|
|
123
|
+
const [elapsed, setElapsed] = useState(0);
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const t = setInterval(() => setElapsed(Math.floor((Date.now() - inv.startTime) / 1000)), 1000);
|
|
126
|
+
return () => clearInterval(t);
|
|
127
|
+
}, [inv.startTime]);
|
|
128
|
+
|
|
129
|
+
const preview = inv.chunk.lines[0]?.slice(0, 60) ?? "";
|
|
130
|
+
return (
|
|
131
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
132
|
+
<Box gap={1}>
|
|
133
|
+
<Text color={ACCENT}><Spinner /></Text>
|
|
134
|
+
<Text color="gray" dimColor>{preview}</Text>
|
|
135
|
+
<Text color="gray" dimColor>{elapsed}s</Text>
|
|
136
|
+
</Box>
|
|
137
|
+
{inv.toolLog.slice(-4).map((t, i) => (
|
|
138
|
+
<Box key={i} marginLeft={2} gap={1}>
|
|
139
|
+
<Text color={ACCENT} dimColor>{TOOL_ICONS[t.tool] ?? "·"}</Text>
|
|
140
|
+
<Text color="gray" dimColor>{t.label}</Text>
|
|
141
|
+
</Box>
|
|
142
|
+
))}
|
|
143
|
+
</Box>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function ResultCard({ inv }: { inv: Investigation }) {
|
|
148
|
+
const cols = process.stdout.columns ?? 80;
|
|
149
|
+
const rule = "─".repeat(Math.min(cols - 2, 72));
|
|
150
|
+
const errorLine = inv.chunk.lines[0]?.slice(0, 70) ?? "error";
|
|
151
|
+
const wroteFiles = inv.toolLog.filter((t) => t.tool === "write");
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
155
|
+
<Text color="gray" dimColor>{rule}</Text>
|
|
156
|
+
<Box gap={1}>
|
|
157
|
+
<Text color={inv.status === "failed" ? RED : GREEN}>
|
|
158
|
+
{inv.status === "failed" ? "✖" : "✔"}
|
|
159
|
+
</Text>
|
|
160
|
+
<Text color="white" bold>{errorLine}</Text>
|
|
161
|
+
</Box>
|
|
162
|
+
{wroteFiles.length > 0 && (
|
|
163
|
+
<Box flexDirection="column" marginLeft={2} marginTop={0}>
|
|
164
|
+
{wroteFiles.map((t, i) => (
|
|
165
|
+
<Box key={i} gap={1}>
|
|
166
|
+
<Text color={ACCENT}>w</Text>
|
|
167
|
+
<Text color="gray">{t.label}</Text>
|
|
168
|
+
</Box>
|
|
169
|
+
))}
|
|
170
|
+
</Box>
|
|
171
|
+
)}
|
|
172
|
+
{inv.response.trim() && (
|
|
173
|
+
<Box marginLeft={2} marginTop={1}>
|
|
174
|
+
<Text color="gray">{inv.response.trim().slice(0, 300)}</Text>
|
|
175
|
+
</Box>
|
|
176
|
+
)}
|
|
177
|
+
</Box>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function InputCard({ prompt, value }: { prompt: string; value: string }) {
|
|
182
|
+
return (
|
|
183
|
+
<Box flexDirection="column" marginBottom={1} gap={1}>
|
|
184
|
+
<Box gap={1}>
|
|
185
|
+
<Text color={ACCENT}>?</Text>
|
|
186
|
+
<Text color="white">{prompt}</Text>
|
|
187
|
+
</Box>
|
|
188
|
+
<Box marginLeft={2} gap={1}>
|
|
189
|
+
<Text color={ACCENT}>›</Text>
|
|
190
|
+
<Text color="white">{value}</Text>
|
|
191
|
+
<Text color={ACCENT}>▋</Text>
|
|
192
|
+
</Box>
|
|
193
|
+
</Box>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Main component ────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
const MAX_LOGS = 120;
|
|
200
|
+
|
|
201
|
+
export function RunView({
|
|
202
|
+
cmd,
|
|
203
|
+
repoPath,
|
|
204
|
+
fixAll = false,
|
|
205
|
+
autoRestart = false,
|
|
206
|
+
}: {
|
|
207
|
+
cmd: string;
|
|
208
|
+
repoPath: string;
|
|
209
|
+
fixAll?: boolean;
|
|
210
|
+
autoRestart?: boolean;
|
|
211
|
+
}) {
|
|
212
|
+
const [stage, setStage] = useState<Stage>("running");
|
|
213
|
+
const [exitCode, setExitCode] = useState<number | null>(null);
|
|
214
|
+
const [logs, setLogs] = useState<{ text: string; isErr: boolean }[]>([]);
|
|
215
|
+
const [investigations, setInvestigations] = useState<Investigation[]>([]);
|
|
216
|
+
const [pending, setPending] = useState<PendingError[]>([]);
|
|
217
|
+
const [inputRequest, setInputRequest] = useState<string | null>(null);
|
|
218
|
+
const [inputValue, setInputValue] = useState("");
|
|
219
|
+
|
|
220
|
+
const processRef = useRef<WatchProcess | null>(null);
|
|
221
|
+
const activeCountRef = useRef(0);
|
|
222
|
+
const pendingExitRef = useRef<number | null | undefined>(undefined);
|
|
223
|
+
const abortRefs = useRef<Map<string, AbortController>>(new Map());
|
|
224
|
+
const systemPromptRef = useRef("");
|
|
225
|
+
|
|
226
|
+
const currentPending = pending[0] ?? null;
|
|
227
|
+
|
|
228
|
+
const updateInv = (id: string, patch: Partial<Investigation>) =>
|
|
229
|
+
setInvestigations((prev) =>
|
|
230
|
+
prev.map((inv) => (inv.id === id ? { ...inv, ...patch } : inv)),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// ── Investigation ──────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
const investigate = async (id: string, chunk: ErrorChunk) => {
|
|
236
|
+
const abort = new AbortController();
|
|
237
|
+
abortRefs.current.set(id, abort);
|
|
238
|
+
|
|
239
|
+
const inv: Investigation = {
|
|
240
|
+
id,
|
|
241
|
+
chunk,
|
|
242
|
+
status: "thinking",
|
|
243
|
+
toolLog: [],
|
|
244
|
+
response: "",
|
|
245
|
+
startTime: Date.now(),
|
|
246
|
+
};
|
|
247
|
+
setInvestigations((prev) => [...prev.slice(-6), inv]);
|
|
248
|
+
|
|
249
|
+
const finish = (status: "done" | "failed") => {
|
|
250
|
+
updateInv(id, { status });
|
|
251
|
+
activeCountRef.current -= 1;
|
|
252
|
+
if (activeCountRef.current === 0 && pendingExitRef.current !== undefined) {
|
|
253
|
+
setStage("crashed");
|
|
254
|
+
setExitCode(pendingExitRef.current);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
await chat({
|
|
260
|
+
messages: [{ role: "user", content: buildErrorMessage(chunk) }],
|
|
261
|
+
system: systemPromptRef.current,
|
|
262
|
+
onBeforeToolCall: () => Promise.resolve(true),
|
|
263
|
+
onToolCall: (tool, args) => {
|
|
264
|
+
const label = toolLabel(tool, args);
|
|
265
|
+
updateInv(id, {
|
|
266
|
+
toolLog: [...(investigations.find((i) => i.id === id)?.toolLog ?? []),
|
|
267
|
+
{ tool, label }],
|
|
268
|
+
});
|
|
269
|
+
// append to log live
|
|
270
|
+
setInvestigations((prev) =>
|
|
271
|
+
prev.map((inv) =>
|
|
272
|
+
inv.id === id
|
|
273
|
+
? { ...inv, toolLog: [...inv.toolLog, { tool, label }] }
|
|
274
|
+
: inv,
|
|
275
|
+
),
|
|
276
|
+
);
|
|
277
|
+
},
|
|
278
|
+
onToolResult: () => {},
|
|
279
|
+
onChunk: () => {},
|
|
280
|
+
onFinish: (text) => {
|
|
281
|
+
setInvestigations((prev) =>
|
|
282
|
+
prev.map((inv) =>
|
|
283
|
+
inv.id === id ? { ...inv, response: text, status: "done" } : inv,
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
activeCountRef.current -= 1;
|
|
287
|
+
if (activeCountRef.current === 0 && pendingExitRef.current !== undefined) {
|
|
288
|
+
setStage("crashed");
|
|
289
|
+
setExitCode(pendingExitRef.current);
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
} catch (e: unknown) {
|
|
294
|
+
if (abort.signal.aborted) return;
|
|
295
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
296
|
+
setInvestigations((prev) =>
|
|
297
|
+
prev.map((inv) =>
|
|
298
|
+
inv.id === id
|
|
299
|
+
? { ...inv, response: `Investigation failed: ${msg}`, status: "failed" }
|
|
300
|
+
: inv,
|
|
301
|
+
),
|
|
302
|
+
);
|
|
303
|
+
finish("failed");
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// ── Process lifecycle ──────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
const startWatching = () => {
|
|
310
|
+
const deps = readPackageJson(repoPath);
|
|
311
|
+
systemPromptRef.current = buildWatchSystemPrompt(repoPath, cmd, deps);
|
|
312
|
+
|
|
313
|
+
const proc = spawnWatch(cmd, repoPath);
|
|
314
|
+
processRef.current = proc;
|
|
315
|
+
|
|
316
|
+
proc.onLog((line, isErr) => {
|
|
317
|
+
const text = stripAnsi(line).slice(0, 200);
|
|
318
|
+
setLogs((prev) => {
|
|
319
|
+
const next = [...prev, { text, isErr }];
|
|
320
|
+
return next.length > MAX_LOGS ? next.slice(-MAX_LOGS) : next;
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
proc.onError((chunk) => {
|
|
325
|
+
const id = nextId();
|
|
326
|
+
activeCountRef.current += 1;
|
|
327
|
+
if (fixAll) {
|
|
328
|
+
investigate(id, chunk);
|
|
329
|
+
} else {
|
|
330
|
+
setPending((prev) => [...prev, { id, chunk }]);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
proc.onInputRequest((prompt) => {
|
|
335
|
+
setInputRequest(prompt);
|
|
336
|
+
setInputValue("");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
proc.onExit((code) => {
|
|
340
|
+
pendingExitRef.current = code;
|
|
341
|
+
if (activeCountRef.current === 0) {
|
|
342
|
+
setStage("crashed");
|
|
343
|
+
setExitCode(code);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const handleRestart = () => {
|
|
349
|
+
pendingExitRef.current = undefined;
|
|
350
|
+
activeCountRef.current = 0;
|
|
351
|
+
abortRefs.current.forEach((a) => a.abort());
|
|
352
|
+
abortRefs.current.clear();
|
|
353
|
+
processRef.current?.kill();
|
|
354
|
+
setInvestigations([]);
|
|
355
|
+
setLogs([]);
|
|
356
|
+
setPending([]);
|
|
357
|
+
setStage("running");
|
|
358
|
+
setExitCode(null);
|
|
359
|
+
startWatching();
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
startWatching();
|
|
364
|
+
return () => {
|
|
365
|
+
processRef.current?.kill();
|
|
366
|
+
abortRefs.current.forEach((a) => a.abort());
|
|
367
|
+
};
|
|
368
|
+
}, []);
|
|
369
|
+
|
|
370
|
+
useEffect(() => {
|
|
371
|
+
if (autoRestart && stage === "crashed") {
|
|
372
|
+
const t = setTimeout(handleRestart, 1500);
|
|
373
|
+
return () => clearTimeout(t);
|
|
374
|
+
}
|
|
375
|
+
}, [stage]);
|
|
376
|
+
|
|
377
|
+
// ── Keyboard ───────────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
useInput((input, key) => {
|
|
380
|
+
if (key.ctrl && input === "c") {
|
|
381
|
+
processRef.current?.kill();
|
|
382
|
+
process.exit(0);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (inputRequest !== null) {
|
|
386
|
+
if (key.return) {
|
|
387
|
+
processRef.current?.sendInput(inputValue);
|
|
388
|
+
setInputRequest(null);
|
|
389
|
+
setInputValue("");
|
|
390
|
+
} else if (key.backspace || key.delete) {
|
|
391
|
+
setInputValue((v) => v.slice(0, -1));
|
|
392
|
+
} else if (input && !key.ctrl && !key.meta) {
|
|
393
|
+
setInputValue((v) => v + input);
|
|
394
|
+
}
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (stage === "crashed" && (input === "r" || input === "R")) {
|
|
399
|
+
handleRestart();
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (currentPending) {
|
|
404
|
+
if (input === "y" || input === "Y") {
|
|
405
|
+
const { id, chunk } = currentPending;
|
|
406
|
+
setPending((prev) => prev.filter((p) => p.id !== id));
|
|
407
|
+
investigate(id, chunk);
|
|
408
|
+
} else if (input === "n" || input === "N") {
|
|
409
|
+
activeCountRef.current -= 1;
|
|
410
|
+
setPending((prev) => prev.slice(1));
|
|
411
|
+
if (activeCountRef.current === 0 && pendingExitRef.current !== undefined) {
|
|
412
|
+
setStage("crashed");
|
|
413
|
+
setExitCode(pendingExitRef.current);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// ── Render ─────────────────────────────────────────────────────────────────
|
|
420
|
+
|
|
421
|
+
const cols = process.stdout.columns ?? 80;
|
|
422
|
+
const thinking = investigations.filter((i) => i.status === "thinking");
|
|
423
|
+
const done = investigations.filter((i) => i.status !== "thinking");
|
|
424
|
+
|
|
425
|
+
return (
|
|
426
|
+
<Box flexDirection="column">
|
|
427
|
+
{/* Header */}
|
|
428
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
429
|
+
<Box gap={2}>
|
|
430
|
+
<Text color={ACCENT} bold>◆ lens run</Text>
|
|
431
|
+
<Text color="gray" dimColor>·</Text>
|
|
432
|
+
<Text color="white" dimColor>{cmd}</Text>
|
|
433
|
+
{fixAll && <><Text color="gray" dimColor>·</Text><Text color={GREEN}>fix-all</Text></>}
|
|
434
|
+
{autoRestart && <><Text color="gray" dimColor>·</Text><Text color="gray">auto-restart</Text></>}
|
|
435
|
+
</Box>
|
|
436
|
+
<Text color="gray" dimColor>{"─".repeat(Math.min(cols, 80))}</Text>
|
|
437
|
+
</Box>
|
|
438
|
+
|
|
439
|
+
{/* Process logs */}
|
|
440
|
+
{logs.length > 0 && <LogPane logs={logs} />}
|
|
441
|
+
{stage === "running" && logs.length === 0 && (
|
|
442
|
+
<Box gap={1} marginBottom={1}>
|
|
443
|
+
<Text color={ACCENT}><Spinner /></Text>
|
|
444
|
+
<Text color="gray" dimColor>waiting for output…</Text>
|
|
445
|
+
</Box>
|
|
446
|
+
)}
|
|
447
|
+
|
|
448
|
+
{/* Process input request */}
|
|
449
|
+
{inputRequest !== null && <InputCard prompt={inputRequest} value={inputValue} />}
|
|
450
|
+
|
|
451
|
+
{/* Completed investigations */}
|
|
452
|
+
{done.map((inv) => <ResultCard key={inv.id} inv={inv} />)}
|
|
453
|
+
|
|
454
|
+
{/* Active investigations */}
|
|
455
|
+
{thinking.map((inv) => <ThinkingCard key={inv.id} inv={inv} />)}
|
|
456
|
+
|
|
457
|
+
{/* Pending confirm */}
|
|
458
|
+
{currentPending && <ConfirmCard chunk={currentPending.chunk} />}
|
|
459
|
+
{pending.length > 1 && (
|
|
460
|
+
<Box marginLeft={2} marginBottom={1}>
|
|
461
|
+
<Text color="gray" dimColor>+{pending.length - 1} more error{pending.length > 2 ? "s" : ""} queued</Text>
|
|
462
|
+
</Box>
|
|
463
|
+
)}
|
|
464
|
+
|
|
465
|
+
{/* Crashed */}
|
|
466
|
+
{stage === "crashed" && (
|
|
467
|
+
<Box flexDirection="column" marginTop={1} gap={1}>
|
|
468
|
+
<Box gap={1}>
|
|
469
|
+
<Text color={RED}>✖</Text>
|
|
470
|
+
<Text color="white">process exited{exitCode !== null ? ` (code ${exitCode})` : ""}</Text>
|
|
471
|
+
</Box>
|
|
472
|
+
{autoRestart ? (
|
|
473
|
+
<Box gap={1}>
|
|
474
|
+
<Text color={ACCENT}><Spinner /></Text>
|
|
475
|
+
<Text color="gray" dimColor>restarting…</Text>
|
|
476
|
+
</Box>
|
|
477
|
+
) : (
|
|
478
|
+
<Box gap={1}>
|
|
479
|
+
<Text color={ACCENT}>r</Text><Text color="white"> restart</Text>
|
|
480
|
+
<Text color="gray" dimColor> · ctrl+c quit</Text>
|
|
481
|
+
</Box>
|
|
482
|
+
)}
|
|
483
|
+
</Box>
|
|
484
|
+
)}
|
|
485
|
+
|
|
486
|
+
{/* Footer hint */}
|
|
487
|
+
{stage === "running" && (
|
|
488
|
+
<Box marginTop={1}>
|
|
489
|
+
<Text color="gray" dimColor>
|
|
490
|
+
watching for errors · ctrl+c stop
|
|
491
|
+
{!fixAll ? " · errors will prompt y/n" : ""}
|
|
492
|
+
</Text>
|
|
493
|
+
</Box>
|
|
494
|
+
)}
|
|
495
|
+
</Box>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useState, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
export function useChatInput(initialMessage?: string) {
|
|
4
|
+
const [inputValue, setInputValue] = useState(initialMessage ?? "");
|
|
5
|
+
const [inputKey, setInputKey] = useState(0);
|
|
6
|
+
const historyRef = useRef<string[]>([]);
|
|
7
|
+
const historyIndexRef = useRef<number>(-1);
|
|
8
|
+
|
|
9
|
+
const pushHistory = (text: string) => {
|
|
10
|
+
historyRef.current = [
|
|
11
|
+
text,
|
|
12
|
+
...historyRef.current.filter((m) => m !== text),
|
|
13
|
+
].slice(0, 50);
|
|
14
|
+
historyIndexRef.current = -1;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const historyUp = () => {
|
|
18
|
+
if (historyRef.current.length === 0) return;
|
|
19
|
+
const next = Math.min(
|
|
20
|
+
historyIndexRef.current + 1,
|
|
21
|
+
historyRef.current.length - 1,
|
|
22
|
+
);
|
|
23
|
+
historyIndexRef.current = next;
|
|
24
|
+
setInputValue(historyRef.current[next]!);
|
|
25
|
+
setInputKey((k) => k + 1);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const historyDown = () => {
|
|
29
|
+
const next = historyIndexRef.current - 1;
|
|
30
|
+
historyIndexRef.current = next;
|
|
31
|
+
setInputValue(next < 0 ? "" : historyRef.current[next]!);
|
|
32
|
+
setInputKey((k) => k + 1);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const clear = () => {
|
|
36
|
+
setInputValue("");
|
|
37
|
+
setInputKey((k) => k + 1);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
inputValue,
|
|
42
|
+
setInputValue,
|
|
43
|
+
inputKey,
|
|
44
|
+
pushHistory,
|
|
45
|
+
historyUp,
|
|
46
|
+
historyDown,
|
|
47
|
+
clear,
|
|
48
|
+
};
|
|
49
|
+
}
|