@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.
Files changed (96) hide show
  1. package/dist/index.mjs +105368 -274002
  2. package/package.json +13 -19
  3. package/src/colors.ts +15 -15
  4. package/src/commands/chat.tsx +32 -23
  5. package/src/commands/provider.tsx +11 -238
  6. package/src/commands/repo.tsx +66 -120
  7. package/src/commands/timeline.tsx +11 -22
  8. package/src/components/ChatView.tsx +238 -0
  9. package/src/components/Message.tsx +46 -0
  10. package/src/components/ToolCall.tsx +67 -0
  11. package/src/components/chat/ChatView.tsx +550 -0
  12. package/src/components/chat/Message.tsx +152 -0
  13. package/src/components/chat/StatusBar.tsx +214 -0
  14. package/src/components/chat/TextArea.tsx +173 -176
  15. package/src/components/provider/ApiKeyStep.tsx +207 -199
  16. package/src/components/provider/ModelStep.tsx +90 -88
  17. package/src/components/provider/ProviderSetup.tsx +331 -0
  18. package/src/components/provider/ProviderTypeStep.tsx +53 -61
  19. package/src/components/repo/StepRow.tsx +68 -69
  20. package/src/components/timeline/TimelineView.tsx +840 -0
  21. package/src/components/toolcall-utils.ts +103 -0
  22. package/src/components/watch/RunView.tsx +497 -0
  23. package/src/hooks/useChatInput.ts +49 -0
  24. package/src/hooks/useCommandHandler.ts +117 -0
  25. package/src/index.tsx +386 -139
  26. package/src/utils/git.ts +149 -155
  27. package/src/utils/repo.ts +62 -69
  28. package/src/utils/thinking.tsx +64 -0
  29. package/src/utils/watch.ts +165 -307
  30. package/tests/message.test.ts +38 -0
  31. package/tests/toolcall-utils.test.ts +111 -0
  32. package/tsconfig.json +8 -24
  33. package/CLAUDE.md +0 -50
  34. package/LENS.md +0 -48
  35. package/LICENSE +0 -21
  36. package/README.md +0 -93
  37. package/addons/README.md +0 -55
  38. package/addons/clean-cache.js +0 -48
  39. package/addons/generate-readme.js +0 -67
  40. package/addons/git-stats.js +0 -29
  41. package/addons/run-tests.js +0 -127
  42. package/src/commands/commit.tsx +0 -668
  43. package/src/commands/review.tsx +0 -294
  44. package/src/commands/run.tsx +0 -56
  45. package/src/commands/task.tsx +0 -36
  46. package/src/components/chat/ChatMessage.tsx +0 -195
  47. package/src/components/chat/ChatOverlays.tsx +0 -399
  48. package/src/components/chat/ChatRunner.tsx +0 -517
  49. package/src/components/chat/hooks/useChat.ts +0 -631
  50. package/src/components/chat/hooks/useChatInput.ts +0 -79
  51. package/src/components/chat/hooks/useCommandHandlers.ts +0 -327
  52. package/src/components/provider/ProviderPicker.tsx +0 -76
  53. package/src/components/provider/RemoveProviderStep.tsx +0 -82
  54. package/src/components/repo/DiffViewer.tsx +0 -175
  55. package/src/components/repo/FileReviewer.tsx +0 -70
  56. package/src/components/repo/FileViewer.tsx +0 -60
  57. package/src/components/repo/IssueFixer.tsx +0 -666
  58. package/src/components/repo/LensFileMenu.tsx +0 -115
  59. package/src/components/repo/NoProviderPrompt.tsx +0 -28
  60. package/src/components/repo/PreviewRunner.tsx +0 -217
  61. package/src/components/repo/RepoAnalysis.tsx +0 -534
  62. package/src/components/task/TaskRunner.tsx +0 -396
  63. package/src/components/timeline/CommitDetail.tsx +0 -272
  64. package/src/components/timeline/CommitList.tsx +0 -162
  65. package/src/components/timeline/TimelineChat.tsx +0 -166
  66. package/src/components/timeline/TimelineRunner.tsx +0 -1285
  67. package/src/components/watch/RunRunner.tsx +0 -929
  68. package/src/prompts/fewshot.ts +0 -252
  69. package/src/prompts/index.ts +0 -2
  70. package/src/prompts/system.ts +0 -285
  71. package/src/tools/chart.ts +0 -202
  72. package/src/tools/convert-image.ts +0 -312
  73. package/src/tools/files.ts +0 -253
  74. package/src/tools/git.ts +0 -603
  75. package/src/tools/index.ts +0 -17
  76. package/src/tools/pdf.ts +0 -164
  77. package/src/tools/shell.ts +0 -96
  78. package/src/tools/view-image.ts +0 -335
  79. package/src/tools/web.ts +0 -212
  80. package/src/types/chat.ts +0 -86
  81. package/src/types/config.ts +0 -20
  82. package/src/types/repo.ts +0 -54
  83. package/src/utils/addons/loadAddons.ts +0 -34
  84. package/src/utils/ai.ts +0 -321
  85. package/src/utils/chat.ts +0 -326
  86. package/src/utils/chatHistory.ts +0 -121
  87. package/src/utils/config.ts +0 -61
  88. package/src/utils/files.ts +0 -105
  89. package/src/utils/intentClassifier.ts +0 -58
  90. package/src/utils/lensfile.ts +0 -142
  91. package/src/utils/llm.ts +0 -81
  92. package/src/utils/memory.ts +0 -209
  93. package/src/utils/preview.ts +0 -119
  94. package/src/utils/stats.ts +0 -174
  95. package/src/utils/tools/builtins.ts +0 -377
  96. package/src/utils/tools/registry.ts +0 -105
@@ -1,396 +0,0 @@
1
- import React from "react";
2
- import { Box, Text, useInput } from "ink";
3
- import Spinner from "ink-spinner";
4
- import figures from "figures";
5
- import { useState } from "react";
6
- import { writeFileSync, existsSync, mkdirSync } from "fs";
7
- import path from "path";
8
- import { ACCENT } from "../../colors";
9
- import { callModelRaw } from "../../utils/ai";
10
- import { DiffViewer, buildDiffs } from "../repo/DiffViewer";
11
- import { ProviderPicker } from "../provider/ProviderPicker";
12
- import type { DiffLine, FilePatch } from "../repo/DiffViewer";
13
- import type { Provider } from "../../types/config";
14
- import type { ImportantFile } from "../../types/repo";
15
- import { fetchFileTree, readImportantFiles } from "../../utils/files";
16
- import { readFileSync, readdirSync, statSync } from "fs";
17
- import { useThinkingPhrase } from "../../utils/thinking";
18
-
19
- type Stage =
20
- | { type: "picking-provider" }
21
- | { type: "reading-files" }
22
- | { type: "thinking" }
23
- | {
24
- type: "preview";
25
- plan: PromptPlan;
26
- diffLines: DiffLine[][];
27
- scrollOffset: number;
28
- }
29
- | { type: "applying" }
30
- | { type: "done"; applied: AppliedFile[] }
31
- | {
32
- type: "viewing-file";
33
- file: AppliedFile;
34
- diffLines: DiffLine[];
35
- scrollOffset: number;
36
- }
37
- | { type: "error"; message: string };
38
-
39
- type PromptPlan = {
40
- summary: string;
41
- patches: FilePatch[];
42
- };
43
-
44
- type AppliedFile = {
45
- path: string;
46
- isNew: boolean;
47
- patch: FilePatch;
48
- };
49
-
50
- function buildPrompt(userPrompt: string, files: ImportantFile[]): string {
51
- const fileList = files
52
- .map((f) => `### ${f.path}\n\`\`\`\n${f.content.slice(0, 3000)}\n\`\`\``)
53
- .join("\n\n");
54
-
55
- return `You are a senior software engineer working on a codebase. The user has made the following request:
56
-
57
- "${userPrompt}"
58
-
59
- Here are the relevant files in the codebase:
60
-
61
- ${fileList}
62
-
63
- Fulfill the user's request by providing the complete new content for any files that need to be created or modified.
64
-
65
- Respond ONLY with a JSON object (no markdown, no explanation) with this exact shape:
66
- {
67
- "summary": "2-3 sentence explanation of what you did and why",
68
- "patches": [
69
- {
70
- "path": "relative/path/to/file.ts",
71
- "content": "complete new file content here",
72
- "isNew": false
73
- }
74
- ]
75
- }
76
-
77
- Rules:
78
- - Always provide the COMPLETE file content, not diffs or partial content
79
- - isNew should be true only if you are creating a brand new file
80
- - Only include files that actually need changes
81
- - Keep changes focused on fulfilling the request
82
- - Do not change unrelated code
83
- - If the request is impossible or unclear, return an empty patches array with an explanation in summary`;
84
- }
85
-
86
- function ThinkingAboutStep({ prompt }: { prompt: string }) {
87
- const phrase = useThinkingPhrase(true, "task");
88
- return (
89
- <Box gap={1}>
90
- <Text color={ACCENT}>
91
- <Spinner />
92
- </Text>
93
- <Text color={ACCENT}>{phrase}</Text>
94
- <Text color="gray">"{prompt}"</Text>
95
- </Box>
96
- );
97
- }
98
-
99
- function applyPatches(repoPath: string, plan: PromptPlan): AppliedFile[] {
100
- const applied: AppliedFile[] = [];
101
- for (const patch of plan.patches) {
102
- const fullPath = path.join(repoPath, patch.path);
103
- const dir = path.dirname(fullPath);
104
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
105
- writeFileSync(fullPath, patch.content, "utf-8");
106
- applied.push({ path: patch.path, isNew: patch.isNew, patch });
107
- }
108
- return applied;
109
- }
110
-
111
- const SKIP_DIRS = new Set([
112
- "node_modules",
113
- ".git",
114
- "dist",
115
- "build",
116
- ".next",
117
- "out",
118
- "coverage",
119
- "__pycache__",
120
- ".venv",
121
- "venv",
122
- ]);
123
-
124
- function walkDir(dir: string, base = dir): string[] {
125
- const results: string[] = [];
126
- let entries: string[];
127
- try {
128
- entries = readdirSync(dir, { encoding: "utf-8" });
129
- } catch {
130
- return results;
131
- }
132
- for (const entry of entries) {
133
- if (SKIP_DIRS.has(entry)) continue;
134
- const full = path.join(dir, entry);
135
- const rel = path.relative(base, full).replace(/\\/g, "/");
136
- let isDir = false;
137
- try {
138
- isDir = statSync(full).isDirectory();
139
- } catch {
140
- continue;
141
- }
142
- if (isDir) results.push(...walkDir(full, base));
143
- else results.push(rel);
144
- }
145
- return results;
146
- }
147
-
148
- export const PromptRunner = ({
149
- repoPath,
150
- userPrompt,
151
- }: {
152
- repoPath: string;
153
- userPrompt: string;
154
- }) => {
155
- const [stage, setStage] = useState<Stage>({ type: "picking-provider" });
156
- const [selectedIndex, setSelectedIndex] = useState(0);
157
- const [files, setFiles] = useState<ImportantFile[]>([]);
158
-
159
- const handleProviderDone = (provider: Provider) => {
160
- setStage({ type: "reading-files" });
161
-
162
- fetchFileTree(repoPath)
163
- .catch(() => walkDir(repoPath))
164
- .then((fileTree) => {
165
- const importantFiles = readImportantFiles(repoPath, fileTree);
166
- setFiles(importantFiles);
167
- setStage({ type: "thinking" });
168
- return callModelRaw(provider, buildPrompt(userPrompt, importantFiles));
169
- })
170
- .then((text) => {
171
- const cleaned = text.replace(/```json|```/g, "").trim();
172
- const match = cleaned.match(/\{[\s\S]*\}/);
173
- if (!match) throw new Error("No JSON in model response");
174
- const plan = JSON.parse(match[0]) as PromptPlan;
175
-
176
- if (plan.patches.length === 0) {
177
- setStage({
178
- type: "error",
179
- message: plan.summary || "Model made no changes.",
180
- });
181
- return;
182
- }
183
-
184
- const diffLines = buildDiffs(repoPath, plan.patches);
185
- setStage({ type: "preview", plan, diffLines, scrollOffset: 0 });
186
- })
187
- .catch((err: unknown) =>
188
- setStage({
189
- type: "error",
190
- message: err instanceof Error ? err.message : "Something went wrong",
191
- }),
192
- );
193
- };
194
-
195
- useInput((_, key) => {
196
- if (stage.type === "preview") {
197
- if (key.upArrow)
198
- setStage({
199
- ...stage,
200
- scrollOffset: Math.max(0, stage.scrollOffset - 1),
201
- });
202
- if (key.downArrow)
203
- setStage({ ...stage, scrollOffset: stage.scrollOffset + 1 });
204
- if (key.escape) {
205
- process.exit(0);
206
- return;
207
- }
208
- if (key.return) {
209
- setStage({ type: "applying" });
210
- try {
211
- const applied = applyPatches(repoPath, stage.plan);
212
- setStage({ type: "done", applied });
213
- } catch (err: unknown) {
214
- setStage({
215
- type: "error",
216
- message:
217
- err instanceof Error ? err.message : "Failed to write files",
218
- });
219
- }
220
- }
221
- return;
222
- }
223
-
224
- if (stage.type === "done") {
225
- if (key.escape) {
226
- process.exit(0);
227
- return;
228
- }
229
- if (key.upArrow) setSelectedIndex((i) => Math.max(0, i - 1));
230
- if (key.downArrow)
231
- setSelectedIndex((i) => Math.min(stage.applied.length - 1, i + 1));
232
- if (key.return) {
233
- const file = stage.applied[selectedIndex];
234
- if (!file) return;
235
- const diffLines = buildDiffs(repoPath, [file.patch])[0] ?? [];
236
- setStage({ type: "viewing-file", file, diffLines, scrollOffset: 0 });
237
- }
238
- return;
239
- }
240
-
241
- if (stage.type === "viewing-file") {
242
- if (key.upArrow)
243
- setStage({
244
- ...stage,
245
- scrollOffset: Math.max(0, stage.scrollOffset - 1),
246
- });
247
- if (key.downArrow)
248
- setStage({ ...stage, scrollOffset: stage.scrollOffset + 1 });
249
- if (key.escape || key.return) {
250
- setStage((prev) => {
251
- if (prev.type !== "viewing-file") return prev;
252
-
253
- process.exit(0);
254
- return prev;
255
- });
256
- }
257
- return;
258
- }
259
-
260
- if (stage.type === "error") {
261
- if (key.return || key.escape) process.exit(1);
262
- }
263
- });
264
-
265
- if (stage.type === "picking-provider") {
266
- return <ProviderPicker onDone={handleProviderDone} />;
267
- }
268
-
269
- if (stage.type === "reading-files") {
270
- return (
271
- <Box marginTop={1} gap={1}>
272
- <Text color={ACCENT}>
273
- <Spinner />
274
- </Text>
275
- <Text>Reading codebase...</Text>
276
- </Box>
277
- );
278
- }
279
-
280
- if (stage.type === "thinking") {
281
- return (
282
- <Box flexDirection="column" marginTop={1} gap={1}>
283
- <ThinkingAboutStep prompt={userPrompt} />
284
- {files.length > 0 && (
285
- <Box flexDirection="column" marginLeft={2}>
286
- <Text color="gray">Using {files.length} files as context</Text>
287
- </Box>
288
- )}
289
- </Box>
290
- );
291
- }
292
-
293
- if (stage.type === "preview") {
294
- const { plan, diffLines, scrollOffset } = stage;
295
- return (
296
- <Box flexDirection="column" marginTop={1} gap={1}>
297
- <Text bold color="cyan">
298
- {figures.info} Proposed Changes
299
- </Text>
300
- <Text color="white">{plan.summary}</Text>
301
- <Box flexDirection="column" marginTop={1}>
302
- <Text color="gray">{plan.patches.length} file(s) to change:</Text>
303
- {plan.patches.map((p) => (
304
- <Text key={p.path} color={p.isNew ? "green" : "yellow"}>
305
- {" "}
306
- {p.isNew ? figures.tick : figures.bullet} {p.path}
307
- {p.isNew && <Text color="gray"> (new)</Text>}
308
- </Text>
309
- ))}
310
- </Box>
311
- <DiffViewer
312
- patches={plan.patches}
313
- diffs={diffLines}
314
- scrollOffset={scrollOffset}
315
- />
316
- <Text color="gray">↑↓ scroll · enter to apply · esc to cancel</Text>
317
- </Box>
318
- );
319
- }
320
-
321
- if (stage.type === "applying") {
322
- return (
323
- <Box marginTop={1} gap={1}>
324
- <Text color={ACCENT}>
325
- <Spinner />
326
- </Text>
327
- <Text>Applying changes...</Text>
328
- </Box>
329
- );
330
- }
331
-
332
- if (stage.type === "done") {
333
- return (
334
- <Box flexDirection="column" marginTop={1} gap={1}>
335
- <Text bold color="green">
336
- {figures.tick} Done
337
- </Text>
338
- <Text color="gray">{stage.applied.length} file(s) written</Text>
339
- {stage.applied.map((f, i) => {
340
- const isSelected = i === selectedIndex;
341
- return (
342
- <Box key={f.path} marginLeft={1}>
343
- <Text color={isSelected ? "cyan" : "green"}>
344
- {isSelected
345
- ? figures.arrowRight
346
- : f.isNew
347
- ? figures.tick
348
- : figures.bullet}{" "}
349
- {f.path}
350
- {f.isNew && <Text color="gray"> (new)</Text>}
351
- {isSelected && <Text color="gray"> · enter to view diff</Text>}
352
- </Text>
353
- </Box>
354
- );
355
- })}
356
- <Text color="gray">↑↓ navigate · enter to view diff · esc to exit</Text>
357
- </Box>
358
- );
359
- }
360
-
361
- if (stage.type === "viewing-file") {
362
- const { file, diffLines, scrollOffset } = stage;
363
- return (
364
- <Box flexDirection="column" marginTop={1} gap={1}>
365
- <Box gap={1}>
366
- <Text bold color="cyan">
367
- {figures.info}
368
- </Text>
369
- <Text bold color="cyan">
370
- {file.path}
371
- </Text>
372
- <Text color="gray">{file.isNew ? "(new file)" : "(modified)"}</Text>
373
- </Box>
374
- <DiffViewer
375
- patches={[file.patch]}
376
- diffs={[diffLines]}
377
- scrollOffset={scrollOffset}
378
- />
379
- <Text color="gray">↑↓ scroll · esc or enter to exit</Text>
380
- </Box>
381
- );
382
- }
383
-
384
- if (stage.type === "error") {
385
- return (
386
- <Box flexDirection="column" marginTop={1} gap={1}>
387
- <Text color="red">
388
- {figures.cross} {stage.message}
389
- </Text>
390
- <Text color="gray">enter or esc to exit</Text>
391
- </Box>
392
- );
393
- }
394
-
395
- return null;
396
- };
@@ -1,272 +0,0 @@
1
- import React from "react";
2
- import { Box, Text } from "ink";
3
- import type { Commit, DiffFile } from "../../utils/git";
4
- import { ACCENT } from "../../colors";
5
-
6
- type Props = {
7
- commit: Commit | null;
8
- diff: DiffFile[];
9
- diffLoading: boolean;
10
- diffScrollOffset: number;
11
- showFullDiff: boolean;
12
- width: number;
13
- height: number;
14
- };
15
-
16
- function formatFullDate(dateStr: string): string {
17
- try {
18
- const d = new Date(dateStr);
19
- return d.toLocaleDateString("en-US", {
20
- weekday: "short",
21
- year: "numeric",
22
- month: "short",
23
- day: "numeric",
24
- hour: "2-digit",
25
- minute: "2-digit",
26
- });
27
- } catch {
28
- return dateStr;
29
- }
30
- }
31
-
32
- function statusIcon(status: DiffFile["status"]): {
33
- icon: string;
34
- color: string;
35
- } {
36
- switch (status) {
37
- case "added":
38
- return { icon: "+", color: "green" };
39
- case "deleted":
40
- return { icon: "-", color: "red" };
41
- case "renamed":
42
- return { icon: "→", color: "yellow" };
43
- default:
44
- return { icon: "~", color: "cyan" };
45
- }
46
- }
47
-
48
- export function CommitDetail({
49
- commit,
50
- diff,
51
- diffLoading,
52
- diffScrollOffset,
53
- showFullDiff,
54
- width,
55
- height,
56
- }: Props) {
57
- if (!commit) {
58
- return (
59
- <Box
60
- width={width}
61
- height={height}
62
- flexDirection="column"
63
- alignItems="center"
64
- justifyContent="center"
65
- >
66
- <Text color="gray" dimColor>
67
- select a commit to view details
68
- </Text>
69
- </Box>
70
- );
71
- }
72
-
73
- const divider = "─".repeat(Math.max(0, width - 2));
74
-
75
- const allDiffLines: Array<{
76
- type: string;
77
- content: string;
78
- fileHeader?: string;
79
- }> = [];
80
- for (const file of diff) {
81
- const { icon, color } = statusIcon(file.status);
82
- allDiffLines.push({
83
- type: "fileheader",
84
- content: `${icon} ${file.path}`,
85
- fileHeader: color,
86
- });
87
- allDiffLines.push({
88
- type: "filestat",
89
- content: ` +${file.insertions} -${file.deletions}`,
90
- });
91
- if (showFullDiff) {
92
- for (const line of file.lines) {
93
- allDiffLines.push({ type: line.type, content: line.content });
94
- }
95
- }
96
- }
97
-
98
- const visibleDiffLines = allDiffLines.slice(
99
- diffScrollOffset,
100
- diffScrollOffset + Math.max(1, height - 18),
101
- );
102
-
103
- return (
104
- <Box width={width} flexDirection="column">
105
- {/* ── Commit header ── */}
106
- <Box paddingX={1} marginBottom={1}>
107
- <Text color="gray" dimColor>
108
- {divider}
109
- </Text>
110
- </Box>
111
-
112
- <Box paddingX={1} gap={2}>
113
- <Text color={ACCENT} bold>
114
- ◉ {commit.shortHash}
115
- </Text>
116
- {commit.parents.length > 1 && <Text color="magenta">merge commit</Text>}
117
- {commit.refs && (
118
- <Text color="yellow">
119
- {commit.refs
120
- .split(",")
121
- .map((r) => r.trim())
122
- .filter(Boolean)
123
- .slice(0, 2)
124
- .join(" ")}
125
- </Text>
126
- )}
127
- </Box>
128
-
129
- {/* message */}
130
- <Box paddingX={1} marginTop={1}>
131
- <Text color="white" bold wrap="wrap">
132
- {commit.message}
133
- </Text>
134
- </Box>
135
- {commit.body && (
136
- <Box paddingX={1} marginTop={1}>
137
- <Text color="gray" wrap="wrap">
138
- {commit.body}
139
- </Text>
140
- </Box>
141
- )}
142
-
143
- {/* meta */}
144
- <Box paddingX={1} marginTop={1} flexDirection="column" gap={0}>
145
- <Box gap={2}>
146
- <Text color="gray" dimColor>
147
- author
148
- </Text>
149
- <Text color="cyan">{commit.author}</Text>
150
- <Text color="gray" dimColor>
151
- &lt;{commit.email}&gt;
152
- </Text>
153
- </Box>
154
- <Box gap={2}>
155
- <Text color="gray" dimColor>
156
- date{" "}
157
- </Text>
158
- <Text color="white">{formatFullDate(commit.date)}</Text>
159
- <Text color="gray" dimColor>
160
- ({commit.relativeDate})
161
- </Text>
162
- </Box>
163
- {commit.parents.length > 0 && (
164
- <Box gap={2}>
165
- <Text color="gray" dimColor>
166
- parent
167
- </Text>
168
- <Text color="gray">
169
- {commit.parents.map((p) => p.slice(0, 7)).join(", ")}
170
- </Text>
171
- </Box>
172
- )}
173
- </Box>
174
-
175
- {/* stats bar */}
176
- <Box paddingX={1} marginTop={1} gap={3}>
177
- <Text color="green">+{commit.insertions} insertions</Text>
178
- <Text color="red">-{commit.deletions}</Text>
179
- <Text color="gray" dimColor>
180
- {commit.filesChanged} file{commit.filesChanged !== 1 ? "s" : ""}{" "}
181
- changed
182
- </Text>
183
- </Box>
184
-
185
- {/* ── Diff section ── */}
186
- <Box paddingX={1} marginTop={1}>
187
- <Text color="gray" dimColor>
188
- {divider}
189
- </Text>
190
- </Box>
191
-
192
- <Box paddingX={1} marginBottom={1} gap={2}>
193
- <Text color={ACCENT}>CHANGES</Text>
194
- <Text color="gray" dimColor>
195
- {showFullDiff ? "[d] collapse diff" : "[d] expand diff"}
196
- </Text>
197
- {diffLoading && (
198
- <Text color="gray" dimColor>
199
- loading…
200
- </Text>
201
- )}
202
- </Box>
203
-
204
- {/* diff lines */}
205
- {visibleDiffLines.map((line, i) => {
206
- if (line.type === "fileheader") {
207
- return (
208
- <Box key={i} paddingX={1}>
209
- <Text color={line.fileHeader ?? "white"} bold>
210
- {line.content}
211
- </Text>
212
- </Box>
213
- );
214
- }
215
- if (line.type === "filestat") {
216
- return (
217
- <Box key={i} paddingX={1}>
218
- <Text color="gray" dimColor>
219
- {line.content}
220
- </Text>
221
- </Box>
222
- );
223
- }
224
- if (line.type === "header") {
225
- return (
226
- <Box key={i} paddingX={1}>
227
- <Text color="cyan" dimColor>
228
- {line.content.slice(0, width - 4)}
229
- </Text>
230
- </Box>
231
- );
232
- }
233
- if (line.type === "add") {
234
- return (
235
- <Box key={i} paddingX={1}>
236
- <Text color="green">
237
- {"+"}
238
- {line.content.slice(0, width - 5)}
239
- </Text>
240
- </Box>
241
- );
242
- }
243
- if (line.type === "remove") {
244
- return (
245
- <Box key={i} paddingX={1}>
246
- <Text color="red">
247
- {"-"}
248
- {line.content.slice(0, width - 5)}
249
- </Text>
250
- </Box>
251
- );
252
- }
253
- return (
254
- <Box key={i} paddingX={1}>
255
- <Text color="gray" dimColor>
256
- {" "}
257
- {line.content.slice(0, width - 5)}
258
- </Text>
259
- </Box>
260
- );
261
- })}
262
-
263
- {allDiffLines.length > visibleDiffLines.length + diffScrollOffset && (
264
- <Box paddingX={1} marginTop={1}>
265
- <Text color="gray" dimColor>
266
- ↓ scroll diff with shift+↑↓
267
- </Text>
268
- </Box>
269
- )}
270
- </Box>
271
- );
272
- }