@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/commands/review.tsx
DELETED
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import figures from "figures";
|
|
4
|
-
import { useEffect, useState } from "react";
|
|
5
|
-
import path from "path";
|
|
6
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
7
|
-
import { fetchFileTree, readImportantFiles } from "../utils/files";
|
|
8
|
-
import { computeStats, formatNumber, topLanguages } from "../utils/stats";
|
|
9
|
-
import { RepoAnalysis } from "../components/repo/RepoAnalysis";
|
|
10
|
-
import { LensFileMenu } from "../components/repo/LensFileMenu";
|
|
11
|
-
import {
|
|
12
|
-
lensFileExists,
|
|
13
|
-
readLensFile,
|
|
14
|
-
lensFileToAnalysisResult,
|
|
15
|
-
} from "../utils/lensfile";
|
|
16
|
-
import type { ImportantFile } from "../types/repo";
|
|
17
|
-
import type { CodeStats } from "../utils/stats";
|
|
18
|
-
import type { LensMenuChoice } from "../components/repo/LensFileMenu";
|
|
19
|
-
|
|
20
|
-
type ReviewStage =
|
|
21
|
-
| { type: "scanning" }
|
|
22
|
-
| {
|
|
23
|
-
type: "lens-menu";
|
|
24
|
-
fileTree: string[];
|
|
25
|
-
files: ImportantFile[];
|
|
26
|
-
stats: CodeStats;
|
|
27
|
-
}
|
|
28
|
-
| {
|
|
29
|
-
type: "stats";
|
|
30
|
-
stats: CodeStats;
|
|
31
|
-
files: ImportantFile[];
|
|
32
|
-
fileTree: string[];
|
|
33
|
-
}
|
|
34
|
-
| { type: "error"; message: string };
|
|
35
|
-
|
|
36
|
-
function StatRow({ label, value }: { label: string; value: string }) {
|
|
37
|
-
const PAD = 20;
|
|
38
|
-
return (
|
|
39
|
-
<Box>
|
|
40
|
-
<Text color="gray">{label.padEnd(PAD, " ")}</Text>
|
|
41
|
-
<Text color="white" bold>
|
|
42
|
-
{value}
|
|
43
|
-
</Text>
|
|
44
|
-
</Box>
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function Divider() {
|
|
49
|
-
return <Text color="gray">{"─".repeat(36)}</Text>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const SKIP_DIRS = new Set([
|
|
53
|
-
"node_modules",
|
|
54
|
-
".git",
|
|
55
|
-
"dist",
|
|
56
|
-
"build",
|
|
57
|
-
".next",
|
|
58
|
-
"out",
|
|
59
|
-
"coverage",
|
|
60
|
-
"__pycache__",
|
|
61
|
-
".venv",
|
|
62
|
-
"venv",
|
|
63
|
-
]);
|
|
64
|
-
|
|
65
|
-
function parseGitignore(dir: string): string[] {
|
|
66
|
-
const p = path.join(dir, ".gitignore");
|
|
67
|
-
if (!existsSync(p)) return [];
|
|
68
|
-
try {
|
|
69
|
-
return readFileSync(p, "utf-8")
|
|
70
|
-
.split("\n")
|
|
71
|
-
.map((l) => l.trim())
|
|
72
|
-
.filter((l) => l && !l.startsWith("#"));
|
|
73
|
-
} catch {
|
|
74
|
-
return [];
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function matchesGitignore(
|
|
79
|
-
patterns: string[],
|
|
80
|
-
relPath: string,
|
|
81
|
-
isDir: boolean,
|
|
82
|
-
): boolean {
|
|
83
|
-
const name = path.basename(relPath);
|
|
84
|
-
for (const pattern of patterns) {
|
|
85
|
-
if (pattern.endsWith("/")) {
|
|
86
|
-
if (isDir && name === pattern.slice(0, -1)) return true;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (pattern.startsWith("!")) continue;
|
|
90
|
-
if (pattern.includes("*")) {
|
|
91
|
-
const regex = new RegExp(
|
|
92
|
-
"^" +
|
|
93
|
-
pattern
|
|
94
|
-
.replace(/\./g, "\\.")
|
|
95
|
-
.replace(/\*\*/g, ".*")
|
|
96
|
-
.replace(/\*/g, "[^/]*") +
|
|
97
|
-
"$",
|
|
98
|
-
);
|
|
99
|
-
if (regex.test(name) || regex.test(relPath)) return true;
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (
|
|
103
|
-
name === pattern ||
|
|
104
|
-
relPath === pattern ||
|
|
105
|
-
relPath.startsWith(pattern + "/")
|
|
106
|
-
)
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function walkDir(dir: string, base = dir, patterns?: string[]): string[] {
|
|
113
|
-
const p = patterns ?? parseGitignore(base);
|
|
114
|
-
const results: string[] = [];
|
|
115
|
-
let entries: string[];
|
|
116
|
-
try {
|
|
117
|
-
entries = readdirSync(dir, { encoding: "utf-8" });
|
|
118
|
-
} catch {
|
|
119
|
-
return results;
|
|
120
|
-
}
|
|
121
|
-
for (const entry of entries) {
|
|
122
|
-
if (SKIP_DIRS.has(entry)) continue;
|
|
123
|
-
const full = path.join(dir, entry);
|
|
124
|
-
const rel = path.relative(base, full).replace(/\\/g, "/");
|
|
125
|
-
let isDir = false;
|
|
126
|
-
try {
|
|
127
|
-
isDir = statSync(full).isDirectory();
|
|
128
|
-
} catch {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (matchesGitignore(p, rel, isDir)) continue;
|
|
132
|
-
if (isDir) results.push(...walkDir(full, base, p));
|
|
133
|
-
else results.push(rel);
|
|
134
|
-
}
|
|
135
|
-
return results;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function StatsPanel({
|
|
139
|
-
resolvedPath,
|
|
140
|
-
stats,
|
|
141
|
-
}: {
|
|
142
|
-
resolvedPath: string;
|
|
143
|
-
stats: CodeStats;
|
|
144
|
-
}) {
|
|
145
|
-
const langs = topLanguages(stats.languages);
|
|
146
|
-
return (
|
|
147
|
-
<Box flexDirection="column" marginTop={1} gap={0}>
|
|
148
|
-
<Text bold color="cyan">
|
|
149
|
-
{figures.hamburger} {path.basename(resolvedPath)}
|
|
150
|
-
</Text>
|
|
151
|
-
<Divider />
|
|
152
|
-
<StatRow label="Lines of Code" value={formatNumber(stats.codeLines)} />
|
|
153
|
-
<StatRow label="Total Lines" value={formatNumber(stats.totalLines)} />
|
|
154
|
-
<StatRow label="Files" value={formatNumber(stats.totalFiles)} />
|
|
155
|
-
<StatRow label="Languages" value={langs || "—"} />
|
|
156
|
-
<StatRow label="Functions" value={formatNumber(stats.functions)} />
|
|
157
|
-
<StatRow label="Classes" value={formatNumber(stats.classes)} />
|
|
158
|
-
<StatRow label="Comment Lines" value={formatNumber(stats.commentLines)} />
|
|
159
|
-
<StatRow label="Blank Lines" value={formatNumber(stats.blankLines)} />
|
|
160
|
-
<Divider />
|
|
161
|
-
</Box>
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export const ReviewCommand = ({
|
|
166
|
-
path: inputPath,
|
|
167
|
-
onExit,
|
|
168
|
-
}: {
|
|
169
|
-
path: string;
|
|
170
|
-
onExit?: () => void;
|
|
171
|
-
}) => {
|
|
172
|
-
const [stage, setStage] = useState<ReviewStage>({ type: "scanning" });
|
|
173
|
-
|
|
174
|
-
const [preloadedResult, setPreloadedResult] = useState<
|
|
175
|
-
import("../types/repo").AnalysisResult | null
|
|
176
|
-
>(null);
|
|
177
|
-
const resolvedPath = path.resolve(inputPath);
|
|
178
|
-
|
|
179
|
-
useEffect(() => {
|
|
180
|
-
if (!existsSync(resolvedPath)) {
|
|
181
|
-
setStage({ type: "error", message: `Path not found: ${resolvedPath}` });
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
fetchFileTree(resolvedPath)
|
|
186
|
-
.catch(() => walkDir(resolvedPath))
|
|
187
|
-
.then((fileTree) => {
|
|
188
|
-
const stats = computeStats(resolvedPath, fileTree);
|
|
189
|
-
const files = readImportantFiles(resolvedPath, fileTree);
|
|
190
|
-
|
|
191
|
-
if (lensFileExists(resolvedPath)) {
|
|
192
|
-
setStage({ type: "lens-menu", fileTree, files, stats });
|
|
193
|
-
} else {
|
|
194
|
-
setStage({ type: "stats", stats, files, fileTree });
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
.catch((err: unknown) =>
|
|
198
|
-
setStage({
|
|
199
|
-
type: "error",
|
|
200
|
-
message: err instanceof Error ? err.message : "Failed to scan",
|
|
201
|
-
}),
|
|
202
|
-
);
|
|
203
|
-
}, [resolvedPath]);
|
|
204
|
-
|
|
205
|
-
const handleLensChoice = (
|
|
206
|
-
choice: LensMenuChoice,
|
|
207
|
-
fileTree: string[],
|
|
208
|
-
files: ImportantFile[],
|
|
209
|
-
stats: CodeStats,
|
|
210
|
-
) => {
|
|
211
|
-
const lf = readLensFile(resolvedPath);
|
|
212
|
-
|
|
213
|
-
if (choice === "use-cached" && lf) {
|
|
214
|
-
setPreloadedResult(lensFileToAnalysisResult(lf));
|
|
215
|
-
setStage({ type: "stats", stats, files, fileTree });
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (choice === "fix-issues" && lf) {
|
|
220
|
-
setPreloadedResult(lensFileToAnalysisResult(lf));
|
|
221
|
-
setStage({ type: "stats", stats, files, fileTree });
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (choice === "security" && lf) {
|
|
226
|
-
setPreloadedResult(lensFileToAnalysisResult(lf));
|
|
227
|
-
setStage({ type: "stats", stats, files, fileTree });
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
setStage({ type: "stats", stats, files, fileTree });
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
if (stage.type === "scanning") {
|
|
235
|
-
return (
|
|
236
|
-
<Box marginTop={1} gap={1}>
|
|
237
|
-
<Text color="cyan">{figures.pointer}</Text>
|
|
238
|
-
<Text>Scanning codebase...</Text>
|
|
239
|
-
</Box>
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (stage.type === "error") {
|
|
244
|
-
return (
|
|
245
|
-
<Box marginTop={1}>
|
|
246
|
-
<Text color="red">
|
|
247
|
-
{figures.cross} {stage.message}
|
|
248
|
-
</Text>
|
|
249
|
-
</Box>
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (stage.type === "lens-menu") {
|
|
254
|
-
const lf = readLensFile(resolvedPath);
|
|
255
|
-
if (!lf) {
|
|
256
|
-
setStage({
|
|
257
|
-
type: "stats",
|
|
258
|
-
stats: stage.stats,
|
|
259
|
-
files: stage.files,
|
|
260
|
-
fileTree: stage.fileTree,
|
|
261
|
-
});
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
264
|
-
const { fileTree, files, stats } = stage;
|
|
265
|
-
return (
|
|
266
|
-
<Box flexDirection="column" gap={1}>
|
|
267
|
-
<StatsPanel resolvedPath={resolvedPath} stats={stats} />
|
|
268
|
-
<LensFileMenu
|
|
269
|
-
repoPath={resolvedPath}
|
|
270
|
-
lensFile={lf}
|
|
271
|
-
onChoice={(choice) =>
|
|
272
|
-
handleLensChoice(choice, fileTree, files, stats)
|
|
273
|
-
}
|
|
274
|
-
/>
|
|
275
|
-
</Box>
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const { stats, files, fileTree } = stage;
|
|
280
|
-
|
|
281
|
-
return (
|
|
282
|
-
<Box flexDirection="column" gap={1}>
|
|
283
|
-
<StatsPanel resolvedPath={resolvedPath} stats={stats} />
|
|
284
|
-
<RepoAnalysis
|
|
285
|
-
repoUrl={resolvedPath}
|
|
286
|
-
repoPath={resolvedPath}
|
|
287
|
-
fileTree={fileTree}
|
|
288
|
-
files={files}
|
|
289
|
-
preloadedResult={preloadedResult ?? undefined}
|
|
290
|
-
onExit={onExit}
|
|
291
|
-
/>
|
|
292
|
-
</Box>
|
|
293
|
-
);
|
|
294
|
-
};
|
package/src/commands/run.tsx
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import figures from "figures";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import { existsSync } from "fs";
|
|
6
|
-
import { RunRunner } from "../components/watch/RunRunner";
|
|
7
|
-
import { RED } from "../colors";
|
|
8
|
-
|
|
9
|
-
interface Props {
|
|
10
|
-
cmd: string;
|
|
11
|
-
path: string;
|
|
12
|
-
clean: boolean;
|
|
13
|
-
fixAll: boolean;
|
|
14
|
-
autoRestart: boolean;
|
|
15
|
-
prompt?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function RunCommand({
|
|
19
|
-
cmd,
|
|
20
|
-
path: inputPath,
|
|
21
|
-
clean,
|
|
22
|
-
fixAll,
|
|
23
|
-
autoRestart,
|
|
24
|
-
prompt,
|
|
25
|
-
}: Props) {
|
|
26
|
-
const repoPath = path.resolve(inputPath);
|
|
27
|
-
|
|
28
|
-
if (!cmd.trim()) {
|
|
29
|
-
return (
|
|
30
|
-
<Box marginTop={1}>
|
|
31
|
-
<Text color={RED}>{figures.cross} Usage: lens watch "bun dev"</Text>
|
|
32
|
-
</Box>
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (!existsSync(repoPath)) {
|
|
37
|
-
return (
|
|
38
|
-
<Box marginTop={1}>
|
|
39
|
-
<Text color={RED}>
|
|
40
|
-
{figures.cross} Path not found: {repoPath}
|
|
41
|
-
</Text>
|
|
42
|
-
</Box>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<RunRunner
|
|
48
|
-
cmd={cmd}
|
|
49
|
-
repoPath={repoPath}
|
|
50
|
-
clean={clean}
|
|
51
|
-
fixAll={fixAll}
|
|
52
|
-
autoRestart={autoRestart}
|
|
53
|
-
extraPrompt={prompt}
|
|
54
|
-
/>
|
|
55
|
-
);
|
|
56
|
-
}
|
package/src/commands/task.tsx
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import figures from "figures";
|
|
4
|
-
import { existsSync } from "fs";
|
|
5
|
-
import path from "path";
|
|
6
|
-
import { PromptRunner } from "../components/task/TaskRunner";
|
|
7
|
-
|
|
8
|
-
export const TaskCommand = ({
|
|
9
|
-
prompt,
|
|
10
|
-
path: inputPath,
|
|
11
|
-
}: {
|
|
12
|
-
prompt: string;
|
|
13
|
-
path: string;
|
|
14
|
-
}) => {
|
|
15
|
-
const resolvedPath = path.resolve(inputPath);
|
|
16
|
-
|
|
17
|
-
if (!existsSync(resolvedPath)) {
|
|
18
|
-
return (
|
|
19
|
-
<Box marginTop={1}>
|
|
20
|
-
<Text color="red">
|
|
21
|
-
{figures.cross} Path not found: {resolvedPath}
|
|
22
|
-
</Text>
|
|
23
|
-
</Box>
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (!prompt.trim()) {
|
|
28
|
-
return (
|
|
29
|
-
<Box marginTop={1}>
|
|
30
|
-
<Text color="red">{figures.cross} Prompt cannot be empty.</Text>
|
|
31
|
-
</Box>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return <PromptRunner repoPath={resolvedPath} userPrompt={prompt} />;
|
|
36
|
-
};
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import { ACCENT, GREEN, RED } from "../../colors";
|
|
4
|
-
import type { Message } from "../../types/chat";
|
|
5
|
-
|
|
6
|
-
function InlineText({ text }: { text: string }) {
|
|
7
|
-
const parts = text.split(/(`[^`]+`|\*\*[^*]+\*\*)/g);
|
|
8
|
-
return (
|
|
9
|
-
<>
|
|
10
|
-
{parts.map((part, i) => {
|
|
11
|
-
if (part.startsWith("`") && part.endsWith("`")) {
|
|
12
|
-
return (
|
|
13
|
-
<Text key={i} color={ACCENT}>
|
|
14
|
-
{part.slice(1, -1)}
|
|
15
|
-
</Text>
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
if (part.startsWith("**") && part.endsWith("**")) {
|
|
19
|
-
return (
|
|
20
|
-
<Text key={i} bold color="white">
|
|
21
|
-
{part.slice(2, -2)}
|
|
22
|
-
</Text>
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
return (
|
|
26
|
-
<Text key={i} color="white">
|
|
27
|
-
{part}
|
|
28
|
-
</Text>
|
|
29
|
-
);
|
|
30
|
-
})}
|
|
31
|
-
</>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function CodeBlock({ lang, code }: { lang: string; code: string }) {
|
|
36
|
-
return (
|
|
37
|
-
<Box flexDirection="column">
|
|
38
|
-
{code.split("\n").map((line, i) => (
|
|
39
|
-
<Text key={i} color={ACCENT}>
|
|
40
|
-
{" "}
|
|
41
|
-
{line}
|
|
42
|
-
</Text>
|
|
43
|
-
))}
|
|
44
|
-
</Box>
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function MessageBody({ content }: { content: string }) {
|
|
49
|
-
const segments = content.split(/(```[\s\S]*?```)/g);
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<Box flexDirection="column">
|
|
53
|
-
{segments.map((seg, si) => {
|
|
54
|
-
if (seg.startsWith("```")) {
|
|
55
|
-
const lines = seg.slice(3).split("\n");
|
|
56
|
-
const lang = lines[0]?.trim() ?? "";
|
|
57
|
-
const code = lines
|
|
58
|
-
.slice(1)
|
|
59
|
-
.join("\n")
|
|
60
|
-
.replace(/```\s*$/, "")
|
|
61
|
-
.trimEnd();
|
|
62
|
-
return <CodeBlock key={si} lang={lang} code={code} />;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const lines = seg.split("\n").filter((l) => l.trim() !== "");
|
|
66
|
-
return (
|
|
67
|
-
<Box key={si} flexDirection="column">
|
|
68
|
-
{lines.map((line, li) => {
|
|
69
|
-
if (line.match(/^[-*•]\s/)) {
|
|
70
|
-
return (
|
|
71
|
-
<Box key={li} gap={1}>
|
|
72
|
-
<Text color={ACCENT}>*</Text>
|
|
73
|
-
<InlineText text={line.slice(2).trim()} />
|
|
74
|
-
</Box>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (line.match(/^\d+\.\s/)) {
|
|
79
|
-
const num = line.match(/^(\d+)\.\s/)![1];
|
|
80
|
-
return (
|
|
81
|
-
<Box key={li} gap={1}>
|
|
82
|
-
<Text color="gray">{num}.</Text>
|
|
83
|
-
<InlineText text={line.replace(/^\d+\.\s/, "").trim()} />
|
|
84
|
-
</Box>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<Box key={li}>
|
|
90
|
-
<InlineText text={line} />
|
|
91
|
-
</Box>
|
|
92
|
-
);
|
|
93
|
-
})}
|
|
94
|
-
</Box>
|
|
95
|
-
);
|
|
96
|
-
})}
|
|
97
|
-
</Box>
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function summarizeToolContent(toolName: string, content: string): string {
|
|
102
|
-
// For write-file, extract just the path
|
|
103
|
-
if (toolName === "write-file" || toolName === "read-file") {
|
|
104
|
-
const pathMatch = content.match(/"path"\s*:\s*"([^"]+)"/);
|
|
105
|
-
if (pathMatch) return pathMatch[1]!;
|
|
106
|
-
}
|
|
107
|
-
// For changes blocks, just say what changed
|
|
108
|
-
if (content.includes('"summary"')) {
|
|
109
|
-
const summaryMatch = content.match(/"summary"\s*:\s*"([^"]+)"/);
|
|
110
|
-
if (summaryMatch) return summaryMatch[1]!;
|
|
111
|
-
}
|
|
112
|
-
return content.length > 120 ? content.slice(0, 120) + "…" : content;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function StaticMessage({ msg }: { msg: Message }) {
|
|
116
|
-
if (msg.role === "user") {
|
|
117
|
-
return (
|
|
118
|
-
<Box
|
|
119
|
-
marginBottom={1}
|
|
120
|
-
gap={1}
|
|
121
|
-
backgroundColor={"#1a1a1a"}
|
|
122
|
-
paddingLeft={1}
|
|
123
|
-
paddingRight={2}
|
|
124
|
-
>
|
|
125
|
-
<Text color="gray">{">"}</Text>
|
|
126
|
-
<Text color="white" bold>
|
|
127
|
-
{msg.content}
|
|
128
|
-
</Text>
|
|
129
|
-
</Box>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (msg.type === "tool") {
|
|
134
|
-
const icons: Record<string, string> = {
|
|
135
|
-
shell: "$",
|
|
136
|
-
fetch: "~>",
|
|
137
|
-
"read-file": "r",
|
|
138
|
-
"write-file": "w",
|
|
139
|
-
search: "?",
|
|
140
|
-
};
|
|
141
|
-
const icon = icons[msg.toolName] ?? "·";
|
|
142
|
-
const label =
|
|
143
|
-
msg.toolName === "shell"
|
|
144
|
-
? msg.content
|
|
145
|
-
: msg.toolName === "search"
|
|
146
|
-
? `"${msg.content}"`
|
|
147
|
-
: summarizeToolContent(msg.toolName, msg.content);
|
|
148
|
-
|
|
149
|
-
return (
|
|
150
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
151
|
-
<Box gap={1}>
|
|
152
|
-
<Text color={msg.approved ? ACCENT : RED}>{icon}</Text>
|
|
153
|
-
<Text color={msg.approved ? "gray" : RED} dimColor={!msg.approved}>
|
|
154
|
-
{label}
|
|
155
|
-
</Text>
|
|
156
|
-
{!msg.approved && <Text color={RED}>denied</Text>}
|
|
157
|
-
</Box>
|
|
158
|
-
{msg.approved && msg.result && (
|
|
159
|
-
<Box marginLeft={2}>
|
|
160
|
-
<Text color="gray">
|
|
161
|
-
{msg.result.split("\n")[0]?.slice(0, 120)}
|
|
162
|
-
{(msg.result.split("\n")[0]?.length ?? 0) > 120 ? "…" : ""}
|
|
163
|
-
</Text>
|
|
164
|
-
</Box>
|
|
165
|
-
)}
|
|
166
|
-
</Box>
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (msg.type === "plan") {
|
|
171
|
-
return (
|
|
172
|
-
<Box flexDirection="column" marginBottom={1}>
|
|
173
|
-
<Box gap={1}>
|
|
174
|
-
<Text color={ACCENT}>*</Text>
|
|
175
|
-
<MessageBody content={msg.content} />
|
|
176
|
-
</Box>
|
|
177
|
-
<Box marginLeft={2} gap={1}>
|
|
178
|
-
<Text color={msg.applied ? GREEN : "gray"}>
|
|
179
|
-
{msg.applied ? "✓" : "·"}
|
|
180
|
-
</Text>
|
|
181
|
-
<Text color={msg.applied ? GREEN : "gray"} dimColor={!msg.applied}>
|
|
182
|
-
{msg.applied ? "changes applied" : "changes skipped"}
|
|
183
|
-
</Text>
|
|
184
|
-
</Box>
|
|
185
|
-
</Box>
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return (
|
|
190
|
-
<Box marginBottom={1} gap={1}>
|
|
191
|
-
<Text color={ACCENT}>●</Text>
|
|
192
|
-
<MessageBody content={msg.content} />
|
|
193
|
-
</Box>
|
|
194
|
-
);
|
|
195
|
-
}
|