@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
@@ -0,0 +1,331 @@
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import figures from "figures";
4
+ import {
5
+ addProvider,
6
+ setActiveProvider,
7
+ removeProvider,
8
+ getConfiguredProviders,
9
+ loadConfig,
10
+ type Provider,
11
+ } from "@ridit/lens-core";
12
+ import { ProviderTypeStep } from "./ProviderTypeStep";
13
+ import { ApiKeyStep } from "./ApiKeyStep";
14
+ import { ModelStep } from "./ModelStep";
15
+ import { ACCENT, GREEN, RED } from "../../colors";
16
+
17
+ type Stage =
18
+ | { type: "menu" }
19
+ | { type: "provider-type" }
20
+ | { type: "credentials"; provider: Provider }
21
+ | { type: "model"; provider: Provider; apiKey: string; baseURL?: string }
22
+ | { type: "remove-pick" }
23
+ | { type: "remove-confirm"; provider: Provider }
24
+ | { type: "switch-pick" }
25
+ | { type: "done"; message: string };
26
+
27
+ type MenuAction = "provider-type" | "remove-pick" | "switch-pick";
28
+
29
+ const MENU_OPTIONS: { label: string; action: MenuAction }[] = [
30
+ { label: "Add / update a provider", action: "provider-type" },
31
+ { label: "Remove a provider", action: "remove-pick" },
32
+ { label: "Switch active provider", action: "switch-pick" },
33
+ ];
34
+
35
+ function CompletedStep({ label }: { label: string }) {
36
+ return (
37
+ <Text color={GREEN}>
38
+ {figures.tick} {label}
39
+ </Text>
40
+ );
41
+ }
42
+
43
+ function Menu({
44
+ completedSteps,
45
+ onSelect,
46
+ }: {
47
+ completedSteps: string[];
48
+ onSelect: (action: MenuAction) => void;
49
+ }) {
50
+ const config = loadConfig();
51
+ const configured = getConfiguredProviders();
52
+ const [index, setIndex] = useState(0);
53
+
54
+ useInput((_, key) => {
55
+ if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
56
+ if (key.downArrow) setIndex((i) => Math.min(MENU_OPTIONS.length - 1, i + 1));
57
+ if (key.return) onSelect(MENU_OPTIONS[index]!.action);
58
+ });
59
+
60
+ return (
61
+ <Box flexDirection="column" gap={1}>
62
+ {completedSteps.map((s, i) => <CompletedStep key={i} label={s} />)}
63
+ <Text bold color={ACCENT}>Lens — provider setup</Text>
64
+ {configured.length > 0 && (
65
+ <Text color="gray" dimColor>
66
+ {figures.info} active: <Text color="white">{config.activeProvider}</Text>
67
+ {" "}({configured.length} configured)
68
+ </Text>
69
+ )}
70
+ {MENU_OPTIONS.map((opt, i) => (
71
+ <Box key={opt.action} marginLeft={1}>
72
+ <Text color={i === index ? ACCENT : "white"}>
73
+ {i === index ? figures.arrowRight : " "}{" "}
74
+ <Text bold={i === index}>{opt.label}</Text>
75
+ </Text>
76
+ </Box>
77
+ ))}
78
+ <Text color="gray" dimColor>↑↓ navigate · enter to select</Text>
79
+ </Box>
80
+ );
81
+ }
82
+
83
+ function RemovePick({
84
+ completedSteps,
85
+ onSelect,
86
+ onBack,
87
+ }: {
88
+ completedSteps: string[];
89
+ onSelect: (provider: Provider) => void;
90
+ onBack: () => void;
91
+ }) {
92
+ const configured = getConfiguredProviders();
93
+ const [index, setIndex] = useState(0);
94
+
95
+ useInput((_, key) => {
96
+ if (key.escape) { onBack(); return; }
97
+ if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
98
+ if (key.downArrow) setIndex((i) => Math.min(configured.length - 1, i + 1));
99
+ if (key.return && configured.length > 0) onSelect(configured[index]!);
100
+ });
101
+
102
+ if (configured.length === 0) {
103
+ return (
104
+ <Box flexDirection="column" gap={1}>
105
+ {completedSteps.map((s, i) => <CompletedStep key={i} label={s} />)}
106
+ <Text color="gray">{figures.info} No providers configured.</Text>
107
+ <Text color="gray" dimColor>esc to go back</Text>
108
+ </Box>
109
+ );
110
+ }
111
+
112
+ return (
113
+ <Box flexDirection="column" gap={1}>
114
+ {completedSteps.map((s, i) => <CompletedStep key={i} label={s} />)}
115
+ <Text bold color={ACCENT}>Remove a provider</Text>
116
+ {configured.map((p, i) => (
117
+ <Box key={p} marginLeft={1}>
118
+ <Text color={i === index ? RED : "white"}>
119
+ {i === index ? figures.arrowRight : " "}{" "}
120
+ <Text bold={i === index}>{p}</Text>
121
+ </Text>
122
+ </Box>
123
+ ))}
124
+ <Text color="gray" dimColor>↑↓ navigate · enter to select · esc to cancel</Text>
125
+ </Box>
126
+ );
127
+ }
128
+
129
+ function RemoveConfirm({
130
+ provider,
131
+ completedSteps,
132
+ onConfirm,
133
+ onBack,
134
+ }: {
135
+ provider: Provider;
136
+ completedSteps: string[];
137
+ onConfirm: () => void;
138
+ onBack: () => void;
139
+ }) {
140
+ useInput((input, key) => {
141
+ if (input === "y" || input === "Y") onConfirm();
142
+ else if (key.escape || input === "n" || input === "N") onBack();
143
+ });
144
+
145
+ return (
146
+ <Box flexDirection="column" gap={1}>
147
+ {completedSteps.map((s, i) => <CompletedStep key={i} label={s} />)}
148
+ <Text color={RED}>
149
+ {figures.warning} Remove <Text bold>{provider}</Text>? (y/n)
150
+ </Text>
151
+ </Box>
152
+ );
153
+ }
154
+
155
+ function SwitchPick({
156
+ completedSteps,
157
+ onSelect,
158
+ onBack,
159
+ }: {
160
+ completedSteps: string[];
161
+ onSelect: (provider: Provider) => void;
162
+ onBack: () => void;
163
+ }) {
164
+ const config = loadConfig();
165
+ const configured = getConfiguredProviders();
166
+ const [index, setIndex] = useState(0);
167
+
168
+ useInput((_, key) => {
169
+ if (key.escape) { onBack(); return; }
170
+ if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
171
+ if (key.downArrow) setIndex((i) => Math.min(configured.length - 1, i + 1));
172
+ if (key.return && configured.length > 0) onSelect(configured[index]!);
173
+ });
174
+
175
+ if (configured.length === 0) {
176
+ return (
177
+ <Box flexDirection="column" gap={1}>
178
+ {completedSteps.map((s, i) => <CompletedStep key={i} label={s} />)}
179
+ <Text color="gray">{figures.info} No providers configured.</Text>
180
+ <Text color="gray" dimColor>esc to go back</Text>
181
+ </Box>
182
+ );
183
+ }
184
+
185
+ return (
186
+ <Box flexDirection="column" gap={1}>
187
+ {completedSteps.map((s, i) => <CompletedStep key={i} label={s} />)}
188
+ <Text bold color={ACCENT}>Switch active provider</Text>
189
+ {configured.map((p, i) => {
190
+ const isActive = p === config.activeProvider;
191
+ const isSelected = i === index;
192
+ return (
193
+ <Box key={p} marginLeft={1}>
194
+ <Text color={isSelected ? ACCENT : "white"}>
195
+ {isSelected ? figures.arrowRight : " "}{" "}
196
+ <Text bold={isSelected}>{p}</Text>
197
+ {isActive && <Text color="gray">{" "}active</Text>}
198
+ </Text>
199
+ </Box>
200
+ );
201
+ })}
202
+ <Text color="gray" dimColor>↑↓ navigate · enter to select · esc to cancel</Text>
203
+ </Box>
204
+ );
205
+ }
206
+
207
+ function DoneScreen({ message, onDone }: { message: string; onDone: () => void }) {
208
+ useInput((_: string, key: { return: boolean; escape: boolean }) => {
209
+ if (key.return || key.escape) onDone();
210
+ });
211
+ return (
212
+ <Box flexDirection="column" gap={1}>
213
+ <Text color={GREEN}>{figures.tick} {message}</Text>
214
+ <Text color="gray" dimColor>press enter to continue</Text>
215
+ </Box>
216
+ );
217
+ }
218
+
219
+ export function ProviderSetup({ onDone }: { onDone: () => void }) {
220
+ const [stage, setStage] = useState<Stage>({ type: "menu" });
221
+ const [completedSteps, setCompletedSteps] = useState<string[]>([]);
222
+
223
+ const pushStep = (label: string) => setCompletedSteps((s) => [...s, label]);
224
+ const goMenu = () => setStage({ type: "menu" });
225
+
226
+ if (stage.type === "menu") {
227
+ return (
228
+ <Menu
229
+ completedSteps={completedSteps}
230
+ onSelect={(action) => setStage({ type: action } as Stage)}
231
+ />
232
+ );
233
+ }
234
+
235
+ if (stage.type === "provider-type") {
236
+ return (
237
+ <Box flexDirection="column" gap={1}>
238
+ {completedSteps.map((s, i) => <CompletedStep key={i} label={s} />)}
239
+ <ProviderTypeStep
240
+ onSelect={(provider) => {
241
+ pushStep(`Provider: ${provider}`);
242
+ setStage({ type: "credentials", provider });
243
+ }}
244
+ />
245
+ </Box>
246
+ );
247
+ }
248
+
249
+ if (stage.type === "credentials") {
250
+ return (
251
+ <Box flexDirection="column" gap={1}>
252
+ {completedSteps.map((s, i) => <CompletedStep key={i} label={s} />)}
253
+ <ApiKeyStep
254
+ providerType={stage.provider}
255
+ onSubmit={(apiKey, baseURL) => {
256
+ if (stage.provider === "ollama") {
257
+ pushStep(`Base URL: ${baseURL ?? "http://localhost:11434"}`);
258
+ } else {
259
+ pushStep("API key saved");
260
+ if (baseURL) pushStep(`Base URL: ${baseURL}`);
261
+ }
262
+ setStage({ type: "model", provider: stage.provider, apiKey, baseURL });
263
+ }}
264
+ />
265
+ </Box>
266
+ );
267
+ }
268
+
269
+ if (stage.type === "model") {
270
+ return (
271
+ <Box flexDirection="column" gap={1}>
272
+ {completedSteps.map((s, i) => <CompletedStep key={i} label={s} />)}
273
+ <ModelStep
274
+ providerType={stage.provider}
275
+ onSelect={(model) => {
276
+ addProvider(stage.provider, {
277
+ apiKey: stage.apiKey,
278
+ model,
279
+ baseURL: stage.baseURL,
280
+ });
281
+ setActiveProvider(stage.provider);
282
+ pushStep(`Model: ${model}`);
283
+ setStage({ type: "done", message: `Provider set to ${stage.provider} (${model})` });
284
+ }}
285
+ />
286
+ </Box>
287
+ );
288
+ }
289
+
290
+ if (stage.type === "remove-pick") {
291
+ return (
292
+ <RemovePick
293
+ completedSteps={completedSteps}
294
+ onSelect={(provider) => setStage({ type: "remove-confirm", provider })}
295
+ onBack={goMenu}
296
+ />
297
+ );
298
+ }
299
+
300
+ if (stage.type === "remove-confirm") {
301
+ return (
302
+ <RemoveConfirm
303
+ provider={stage.provider}
304
+ completedSteps={completedSteps}
305
+ onConfirm={() => {
306
+ removeProvider(stage.provider);
307
+ pushStep(`Removed: ${stage.provider}`);
308
+ setStage({ type: "done", message: `Provider ${stage.provider} removed` });
309
+ }}
310
+ onBack={goMenu}
311
+ />
312
+ );
313
+ }
314
+
315
+ if (stage.type === "switch-pick") {
316
+ return (
317
+ <SwitchPick
318
+ completedSteps={completedSteps}
319
+ onSelect={(provider) => {
320
+ setActiveProvider(provider);
321
+ pushStep(`Switched to: ${provider}`);
322
+ setStage({ type: "done", message: `Now using ${provider}` });
323
+ }}
324
+ onBack={goMenu}
325
+ />
326
+ );
327
+ }
328
+
329
+ // done
330
+ return <DoneScreen message={stage.message} onDone={onDone} />;
331
+ }
@@ -1,61 +1,53 @@
1
- import { Box, Text, useInput } from "ink";
2
- import figures from "figures";
3
- import { useState } from "react";
4
- import type { ProviderType } from "../../types/config";
5
- import { ACCENT, TEXT } from "../../colors";
6
-
7
- const OPTIONS: { type: ProviderType; label: string; description: string }[] = [
8
- { type: "anthropic", label: "Anthropic", description: "Claude models" },
9
- { type: "openai", label: "OpenAI", description: "GPT models" },
10
- { type: "ollama", label: "Ollama", description: "Local models" },
11
- {
12
- type: "custom",
13
- label: "Custom provider",
14
- description: "Any OpenAI-compatible API",
15
- },
16
- ];
17
-
18
- export const ProviderTypeStep = ({
19
- onSelect,
20
- onBack,
21
- }: {
22
- onSelect: (type: ProviderType) => void;
23
- onBack?: () => void;
24
- }) => {
25
- const [index, setIndex] = useState(0);
26
-
27
- useInput((_, key) => {
28
- if (key.escape) {
29
- onBack?.();
30
- return;
31
- }
32
- if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
33
- if (key.downArrow) setIndex((i) => Math.min(OPTIONS.length - 1, i + 1));
34
- if (key.return) onSelect(OPTIONS[index]!.type);
35
- });
36
-
37
- return (
38
- <Box flexDirection="column" gap={1}>
39
- <Text color={TEXT}>Select a provider</Text>
40
- {OPTIONS.map((opt, i) => {
41
- const selected = i === index;
42
- return (
43
- <Box key={opt.type} marginLeft={1}>
44
- <Text color={selected ? ACCENT : "white"}>
45
- {selected ? figures.arrowRight : " "}
46
- {" "}
47
- <Text bold={selected}>{opt.label}</Text>
48
- <Text color="gray">
49
- {" "}
50
- {opt.description}
51
- </Text>
52
- </Text>
53
- </Box>
54
- );
55
- })}
56
- <Text color="gray">
57
- ↑↓ navigate · enter to select{onBack ? " · esc back" : ""}
58
- </Text>
59
- </Box>
60
- );
61
- };
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import figures from "figures";
4
+ import type { Provider } from "@ridit/lens-core";
5
+ import { ACCENT } from "../../colors";
6
+
7
+ const OPTIONS: { type: Provider; label: string; description: string }[] = [
8
+ { type: "anthropic", label: "Anthropic", description: "Claude models" },
9
+ { type: "openai", label: "OpenAI", description: "GPT models" },
10
+ { type: "google", label: "Google", description: "Gemini models" },
11
+ { type: "groq", label: "Groq", description: "Fast open-source models" },
12
+ { type: "openrouter", label: "OpenRouter", description: "Multi-provider gateway" },
13
+ { type: "ollama", label: "Ollama", description: "Local models" },
14
+ { type: "custom", label: "Custom", description: "Any OpenAI-compatible API" },
15
+ ];
16
+
17
+ export function ProviderTypeStep({
18
+ onSelect,
19
+ }: {
20
+ onSelect: (type: Provider) => void;
21
+ }) {
22
+ const [index, setIndex] = useState(0);
23
+
24
+ useInput((_, key) => {
25
+ if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
26
+ if (key.downArrow) setIndex((i) => Math.min(OPTIONS.length - 1, i + 1));
27
+ if (key.return) onSelect(OPTIONS[index]!.type);
28
+ });
29
+
30
+ return (
31
+ <Box flexDirection="column" gap={1}>
32
+ <Text bold color={ACCENT}>
33
+ Select a provider
34
+ </Text>
35
+ {OPTIONS.map((opt, i) => {
36
+ const selected = i === index;
37
+ return (
38
+ <Box key={opt.type} marginLeft={1}>
39
+ <Text color={selected ? ACCENT : "white"}>
40
+ {selected ? figures.arrowRight : " "}
41
+ {" "}
42
+ <Text bold={selected}>{opt.label}</Text>
43
+ <Text color="gray">{" "}{opt.description}</Text>
44
+ </Text>
45
+ </Box>
46
+ );
47
+ })}
48
+ <Text color="gray" dimColor>
49
+ ↑↓ navigate · enter to select
50
+ </Text>
51
+ </Box>
52
+ );
53
+ }
@@ -1,69 +1,68 @@
1
- import { Box, Text } from "ink";
2
- import Spinner from "ink-spinner";
3
- import { ACCENT } from "../../colors";
4
- import { useThinkingPhrase, type ThinkingKind } from "../../utils/thinking";
5
- import type { Step } from "../../types/repo";
6
-
7
- const LABELS: Record<string, string> = {
8
- cloning: "cloning repository",
9
- "fetching-tree": "fetching repository structure",
10
- "reading-files": "reading important files",
11
- };
12
-
13
- const kindMap: Record<string, ThinkingKind> = {
14
- cloning: "cloning",
15
- "fetching-tree": "analyzing",
16
- "reading-files": "analyzing",
17
- };
18
-
19
- function ActiveStep({ type }: { type: string }) {
20
- const phrase = useThinkingPhrase(true, kindMap[type], 4321);
21
- const label = LABELS[type] ?? type;
22
- return (
23
- <Box gap={1}>
24
- <Text color={ACCENT}>
25
- <Spinner />
26
- </Text>
27
- <Text color={ACCENT}>{phrase}</Text>
28
- </Box>
29
- );
30
- }
31
-
32
- export const StepRow = ({ step }: { step: Step }) => {
33
- if (step.type === "error") {
34
- return (
35
- <Box gap={1}>
36
- <Text color="red">✗</Text>
37
- <Text color="red">{step.message}</Text>
38
- </Box>
39
- );
40
- }
41
-
42
- if (step.type === "folder-exists") {
43
- return (
44
- <Box flexDirection="column">
45
- <Box gap={1}>
46
- <Text color="yellow">!</Text>
47
- <Text color="gray">folder already exists at </Text>
48
- <Text color="white">{step.repoPath}</Text>
49
- </Box>
50
- <Box gap={1} marginLeft={2}>
51
- <Text color="gray">y re-clone · n use existing</Text>
52
- </Box>
53
- </Box>
54
- );
55
- }
56
-
57
- const label = LABELS[step.type] ?? step.type;
58
-
59
- if (step.status === "done") {
60
- return (
61
- <Box gap={1}>
62
- <Text color="green">✓</Text>
63
- <Text color="gray">{label}</Text>
64
- </Box>
65
- );
66
- }
67
-
68
- return <ActiveStep type={step.type} />;
69
- };
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import Spinner from "ink-spinner";
4
+ import { ACCENT } from "../../colors";
5
+ import { useThinkingPhrase, type ThinkingKind } from "../../utils/thinking";
6
+
7
+ export type Step =
8
+ | { type: "cloning"; status: "pending" | "done" }
9
+ | { type: "folder-exists"; status: "pending"; repoPath: string }
10
+ | { type: "error"; message: string };
11
+
12
+ const LABELS: Record<string, string> = {
13
+ cloning: "cloning repository",
14
+ };
15
+
16
+ const kindMap: Record<string, ThinkingKind> = {
17
+ cloning: "cloning",
18
+ };
19
+
20
+ function ActiveStep({ type }: { type: string }) {
21
+ const phrase = useThinkingPhrase(true, kindMap[type] ?? "general", 4321);
22
+ const label = LABELS[type] ?? type;
23
+ return (
24
+ <Box gap={1}>
25
+ <Text color={ACCENT}>
26
+ <Spinner />
27
+ </Text>
28
+ <Text color={ACCENT}>{phrase}</Text>
29
+ </Box>
30
+ );
31
+ }
32
+
33
+ export function StepRow({ step }: { step: Step }) {
34
+ if (step.type === "error") {
35
+ return (
36
+ <Box gap={1}>
37
+ <Text color="red">✗</Text>
38
+ <Text color="red">{step.message}</Text>
39
+ </Box>
40
+ );
41
+ }
42
+
43
+ if (step.type === "folder-exists") {
44
+ return (
45
+ <Box flexDirection="column">
46
+ <Box gap={1}>
47
+ <Text color="yellow">!</Text>
48
+ <Text color="gray">folder already exists at </Text>
49
+ <Text color="white">{step.repoPath}</Text>
50
+ </Box>
51
+ <Box gap={1} marginLeft={2}>
52
+ <Text color="gray">y re-clone · n use existing</Text>
53
+ </Box>
54
+ </Box>
55
+ );
56
+ }
57
+
58
+ if (step.status === "done") {
59
+ return (
60
+ <Box gap={1}>
61
+ <Text color="green">✓</Text>
62
+ <Text color="gray">{LABELS[step.type] ?? step.type}</Text>
63
+ </Box>
64
+ );
65
+ }
66
+
67
+ return <ActiveStep type={step.type} />;
68
+ }