@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,199 +1,207 @@
1
- import { Box, Text, useInput } from "ink";
2
- import TextInput from "ink-text-input";
3
- import { useState } from "react";
4
- import { execSync } from "child_process";
5
- import type { ProviderType } from "../../types/config";
6
- import { ACCENT, TEXT } from "../../colors";
7
-
8
- const LABELS: Record<ProviderType, string> = {
9
- anthropic: "Anthropic API key",
10
- gemini: "Gemini API key",
11
- openai: "OpenAI API key",
12
- ollama: "Ollama base URL",
13
- custom: "API key",
14
- };
15
-
16
- function readClipboard(): string | null {
17
- try {
18
- if (process.platform === "win32") {
19
- return execSync("powershell -command Get-Clipboard", {
20
- encoding: "utf-8",
21
- }).trim();
22
- } else if (process.platform === "darwin") {
23
- return execSync("pbpaste", { encoding: "utf-8" }).trim();
24
- } else {
25
- try {
26
- return execSync("xclip -selection clipboard -o", {
27
- encoding: "utf-8",
28
- }).trim();
29
- } catch {
30
- return execSync("xsel --clipboard --output", {
31
- encoding: "utf-8",
32
- }).trim();
33
- }
34
- }
35
- } catch {
36
- return null;
37
- }
38
- }
39
-
40
- type CustomResult = { apiKey: string; baseUrl?: string };
41
- type Field = "apiKey" | "baseUrl";
42
-
43
- const SimpleInput = ({
44
- providerType,
45
- onSubmit,
46
- onBack,
47
- }: {
48
- providerType: Exclude<ProviderType, "custom">;
49
- onSubmit: (value: string) => void;
50
- onBack?: () => void;
51
- }) => {
52
- const isPassword = providerType !== "ollama";
53
- const [value, setValue] = useState(
54
- providerType === "ollama" ? "http://localhost:11434" : "",
55
- );
56
-
57
- useInput((input, key) => {
58
- if (key.escape) {
59
- onBack?.();
60
- return;
61
- }
62
- if (key.ctrl && input === "v") {
63
- const clip = readClipboard();
64
- if (clip) setValue((v) => v + clip);
65
- }
66
- });
67
-
68
- return (
69
- <Box flexDirection="column" gap={1}>
70
- <Text color={TEXT}>{LABELS[providerType]}</Text>
71
- <Box borderStyle="round" borderColor="gray" paddingX={1}>
72
- <TextInput
73
- value={value}
74
- onChange={setValue}
75
- onSubmit={(v) => {
76
- if (v.trim()) onSubmit(v.trim());
77
- }}
78
- mask={isPassword ? "*" : undefined}
79
- placeholder={
80
- providerType === "ollama" ? "http://localhost:11434" : ""
81
- }
82
- />
83
- </Box>
84
- <Text color="gray">
85
- enter to confirm · ctrl+v to paste{onBack ? " · esc back" : ""}
86
- </Text>
87
- </Box>
88
- );
89
- };
90
-
91
- const CustomInput = ({
92
- onSubmit,
93
- onBack,
94
- }: {
95
- onSubmit: (result: CustomResult) => void;
96
- onBack?: () => void;
97
- }) => {
98
- const [activeField, setActiveField] = useState<Field>("apiKey");
99
- const [apiKey, setApiKey] = useState("");
100
- const [baseUrl, setBaseUrl] = useState("");
101
-
102
- useInput((input, key) => {
103
- if (key.escape) {
104
- onBack?.();
105
- return;
106
- }
107
- if (key.tab) {
108
- setActiveField((f) => (f === "apiKey" ? "baseUrl" : "apiKey"));
109
- return;
110
- }
111
- if (key.ctrl && input === "v") {
112
- const clip = readClipboard();
113
- if (clip) {
114
- if (activeField === "apiKey") setApiKey((v) => v + clip);
115
- else setBaseUrl((v) => v + clip);
116
- }
117
- }
118
- });
119
-
120
- const fields: { id: Field; label: string; placeholder: string }[] = [
121
- { id: "apiKey", label: "API key", placeholder: "sk-..." },
122
- {
123
- id: "baseUrl",
124
- label: "Base URL (optional)",
125
- placeholder: "https://api.example.com/v1",
126
- },
127
- ];
128
-
129
- return (
130
- <Box flexDirection="column" gap={1}>
131
- {fields.map(({ id, label, placeholder }) => {
132
- const isActive = activeField === id;
133
- const val = id === "apiKey" ? apiKey : baseUrl;
134
- return (
135
- <Box key={id} flexDirection="column" gap={0}>
136
- <Text color={isActive ? ACCENT : "gray"}>
137
- {isActive ? "›" : " "} {label}
138
- </Text>
139
- <Box
140
- borderStyle="round"
141
- borderColor={isActive ? ACCENT : "gray"}
142
- paddingX={1}
143
- >
144
- {isActive ? (
145
- <TextInput
146
- value={val}
147
- onChange={id === "apiKey" ? setApiKey : setBaseUrl}
148
- onSubmit={() => {
149
- if (id === "apiKey" && apiKey.trim()) {
150
- setActiveField("baseUrl");
151
- } else if (id === "baseUrl" && apiKey.trim()) {
152
- onSubmit({
153
- apiKey: apiKey.trim(),
154
- baseUrl: baseUrl.trim() || undefined,
155
- });
156
- }
157
- }}
158
- mask={id === "apiKey" ? "*" : undefined}
159
- placeholder={placeholder}
160
- />
161
- ) : (
162
- <Text color={val ? "white" : "gray"}>
163
- {id === "apiKey" && val
164
- ? "*".repeat(val.length)
165
- : val || placeholder}
166
- </Text>
167
- )}
168
- </Box>
169
- </Box>
170
- );
171
- })}
172
- <Text color="gray">
173
- enter to next · tab to switch · ctrl+v to paste
174
- {onBack ? " · esc back" : ""}
175
- </Text>
176
- </Box>
177
- );
178
- };
179
-
180
- export const ApiKeyStep = ({
181
- providerType,
182
- onSubmit,
183
- onBack,
184
- }: {
185
- providerType: ProviderType;
186
- onSubmit: (value: string | CustomResult) => void;
187
- onBack?: () => void;
188
- }) => {
189
- if (providerType === "custom") {
190
- return <CustomInput onSubmit={onSubmit} onBack={onBack} />;
191
- }
192
- return (
193
- <SimpleInput
194
- providerType={providerType}
195
- onSubmit={onSubmit}
196
- onBack={onBack}
197
- />
198
- );
199
- };
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput, type Key } from "ink";
3
+ import { execSync } from "child_process";
4
+ import type { Provider } from "@ridit/lens-core";
5
+ import { ACCENT } from "../../colors";
6
+
7
+ function readClipboard(): string | null {
8
+ try {
9
+ if (process.platform === "win32") {
10
+ return execSync("powershell -command Get-Clipboard", { encoding: "utf-8" }).trim();
11
+ } else if (process.platform === "darwin") {
12
+ return execSync("pbpaste", { encoding: "utf-8" }).trim();
13
+ } else {
14
+ try {
15
+ return execSync("xclip -selection clipboard -o", { encoding: "utf-8" }).trim();
16
+ } catch {
17
+ return execSync("xsel --clipboard --output", { encoding: "utf-8" }).trim();
18
+ }
19
+ }
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ const LABELS: Partial<Record<Provider, string>> = {
26
+ anthropic: "Anthropic API key",
27
+ openai: "OpenAI API key",
28
+ google: "Google API key",
29
+ groq: "Groq API key",
30
+ openrouter: "OpenRouter API key",
31
+ ollama: "Ollama base URL (leave blank for http://localhost:11434)",
32
+ };
33
+
34
+ function useFieldInput(initial: string, onPasteError: (v: boolean) => void) {
35
+ const [value, setValue] = useState(initial);
36
+
37
+ const handle = (input: string, key: Key) => {
38
+ if (key.backspace || key.delete) {
39
+ setValue((v) => v.slice(0, -1));
40
+ onPasteError(false);
41
+ return;
42
+ }
43
+ if (key.ctrl && input === "v") {
44
+ const clip = readClipboard();
45
+ if (clip) {
46
+ setValue((v) => v + clip);
47
+ onPasteError(false);
48
+ } else {
49
+ onPasteError(true);
50
+ }
51
+ return;
52
+ }
53
+ if (key.ctrl && input === "a") {
54
+ setValue("");
55
+ return;
56
+ }
57
+ if (!key.ctrl && !key.meta && input) {
58
+ setValue((v) => v + input);
59
+ onPasteError(false);
60
+ }
61
+ };
62
+
63
+ return { value, setValue, handle };
64
+ }
65
+
66
+ // Single-field input (API key or base URL)
67
+ function SimpleInput({
68
+ providerType,
69
+ onSubmit,
70
+ }: {
71
+ providerType: Exclude<Provider, "custom">;
72
+ onSubmit: (value: string) => void;
73
+ }) {
74
+ const [pasteError, setPasteError] = useState(false);
75
+ const isOllama = providerType === "ollama";
76
+ const { value, handle } = useFieldInput(
77
+ isOllama ? "http://localhost:11434" : "",
78
+ setPasteError,
79
+ );
80
+
81
+ useInput((input, key) => {
82
+ if (key.return) {
83
+ onSubmit(value.trim());
84
+ return;
85
+ }
86
+ handle(input, key);
87
+ });
88
+
89
+ const display = isOllama ? value : "•".repeat(value.length);
90
+
91
+ return (
92
+ <Box flexDirection="column" gap={1}>
93
+ <Text bold color={ACCENT}>
94
+ {LABELS[providerType] ?? "API key"}
95
+ </Text>
96
+ <Box borderStyle="round" borderColor="gray" paddingX={1}>
97
+ <Text>{display || " "}</Text>
98
+ </Box>
99
+ {pasteError ? (
100
+ <Text color="red">⚠ Could not read clipboard</Text>
101
+ ) : (
102
+ <Text color="gray" dimColor>
103
+ enter to confirm · ctrl+v to paste · ctrl+a to clear
104
+ </Text>
105
+ )}
106
+ </Box>
107
+ );
108
+ }
109
+
110
+ // Two-field input for custom provider (API key + base URL)
111
+ function CustomInput({
112
+ onSubmit,
113
+ }: {
114
+ onSubmit: (apiKey: string, baseURL?: string) => void;
115
+ }) {
116
+ type Field = "apiKey" | "baseUrl";
117
+ const [activeField, setActiveField] = useState<Field>("apiKey");
118
+ const [pasteError, setPasteError] = useState(false);
119
+
120
+ const apiKeyField = useFieldInput("", setPasteError);
121
+ const baseUrlField = useFieldInput("", setPasteError);
122
+ const active = activeField === "apiKey" ? apiKeyField : baseUrlField;
123
+
124
+ useInput((input, key) => {
125
+ if (key.tab) {
126
+ setActiveField((f) => (f === "apiKey" ? "baseUrl" : "apiKey"));
127
+ setPasteError(false);
128
+ return;
129
+ }
130
+ if (key.return) {
131
+ if (activeField === "apiKey" && apiKeyField.value.trim()) {
132
+ setActiveField("baseUrl");
133
+ return;
134
+ }
135
+ if (activeField === "baseUrl") {
136
+ onSubmit(
137
+ apiKeyField.value.trim(),
138
+ baseUrlField.value.trim() || undefined,
139
+ );
140
+ return;
141
+ }
142
+ }
143
+ active.handle(input, key);
144
+ });
145
+
146
+ const fields: { id: Field; label: string; password: boolean; placeholder: string }[] = [
147
+ { id: "apiKey", label: "API key", password: true, placeholder: "sk-..." },
148
+ { id: "baseUrl", label: "Base URL", password: false, placeholder: "https://api.example.com/v1" },
149
+ ];
150
+
151
+ return (
152
+ <Box flexDirection="column" gap={1}>
153
+ <Text bold color={ACCENT}>
154
+ Custom provider
155
+ </Text>
156
+ {fields.map(({ id, label, password, placeholder }) => {
157
+ const isActive = activeField === id;
158
+ const val = id === "apiKey" ? apiKeyField.value : baseUrlField.value;
159
+ const display = password ? "•".repeat(val.length) : val;
160
+ return (
161
+ <Box key={id} flexDirection="column">
162
+ <Text color={isActive ? ACCENT : "gray"}>
163
+ {isActive ? "" : " "} {label}
164
+ {id === "baseUrl" ? " (optional)" : ""}
165
+ </Text>
166
+ <Box borderStyle="round" borderColor={isActive ? ACCENT : "gray"} paddingX={1}>
167
+ <Text color={val ? "white" : "gray"}>{display || placeholder}</Text>
168
+ </Box>
169
+ </Box>
170
+ );
171
+ })}
172
+ {pasteError ? (
173
+ <Text color="red">⚠ Could not read clipboard</Text>
174
+ ) : (
175
+ <Text color="gray" dimColor>
176
+ enter to next · tab to switch · ctrl+v to paste · ctrl+a to clear
177
+ </Text>
178
+ )}
179
+ </Box>
180
+ );
181
+ }
182
+
183
+ export function ApiKeyStep({
184
+ providerType,
185
+ onSubmit,
186
+ }: {
187
+ providerType: Provider;
188
+ onSubmit: (apiKey: string, baseURL?: string) => void;
189
+ }) {
190
+ if (providerType === "custom") {
191
+ return <CustomInput onSubmit={onSubmit} />;
192
+ }
193
+ if (providerType === "ollama") {
194
+ return (
195
+ <SimpleInput
196
+ providerType="ollama"
197
+ onSubmit={(baseUrl) => onSubmit("ollama", baseUrl || "http://localhost:11434")}
198
+ />
199
+ );
200
+ }
201
+ return (
202
+ <SimpleInput
203
+ providerType={providerType}
204
+ onSubmit={(apiKey) => onSubmit(apiKey)}
205
+ />
206
+ );
207
+ }
@@ -1,88 +1,90 @@
1
- import { Box, Text, useInput } from "ink";
2
- import TextInput from "ink-text-input";
3
- import figures from "figures";
4
- import { useState } from "react";
5
- import { DEFAULT_MODELS } from "../../utils/config";
6
- import type { ProviderType } from "../../types/config";
7
- import { TEXT } from "../../colors";
8
-
9
- export const ModelStep = ({
10
- providerType,
11
- onSelect,
12
- onBack,
13
- }: {
14
- providerType: ProviderType;
15
- onSelect: (model: string) => void;
16
- onBack?: () => void;
17
- }) => {
18
- const models = DEFAULT_MODELS[providerType] ?? [];
19
- const [index, setIndex] = useState(0);
20
- const [custom, setCustom] = useState("");
21
- const [typing, setTyping] = useState(models.length === 0);
22
-
23
- useInput((_, key) => {
24
- if (key.escape) {
25
- if (typing && models.length > 0) {
26
- setTyping(false);
27
- return;
28
- }
29
- onBack?.();
30
- return;
31
- }
32
- if (typing) return;
33
- if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
34
- if (key.downArrow) setIndex((i) => Math.min(models.length, i + 1));
35
- if (key.return) {
36
- if (index === models.length) setTyping(true);
37
- else onSelect(models[index]!);
38
- }
39
- });
40
-
41
- return (
42
- <Box flexDirection="column" gap={1}>
43
- <Text bold color="cyan">
44
- Select a model
45
- </Text>
46
- {models.map((m, i) => {
47
- const selected = !typing && i === index;
48
- return (
49
- <Box key={m} marginLeft={1}>
50
- <Text color={selected ? "cyan" : "white"}>
51
- {selected ? figures.arrowRight : " "}
52
- {" "}
53
- {m}
54
- </Text>
55
- </Box>
56
- );
57
- })}
58
- <Box marginLeft={1}>
59
- {typing ? (
60
- <Box gap={1}>
61
- <Text color={TEXT}>
62
- {figures.arrowRight}
63
- {" "}Custom:{" "}
64
- </Text>
65
- <TextInput
66
- value={custom}
67
- onChange={setCustom}
68
- onSubmit={(v) => {
69
- if (v.trim()) onSelect(v.trim());
70
- }}
71
- placeholder="enter model name"
72
- />
73
- </Box>
74
- ) : (
75
- <Text color={index === models.length ? "cyan" : "gray"}>
76
- {index === models.length ? figures.arrowRight : " "}
77
- {" "}Enter custom model name
78
- </Text>
79
- )}
80
- </Box>
81
- <Text color="gray">
82
- {typing
83
- ? "enter to confirm · esc back"
84
- : `↑↓ navigate · enter to select${onBack ? " · esc back" : ""}`}
85
- </Text>
86
- </Box>
87
- );
88
- };
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 DEFAULT_MODELS: Partial<Record<Provider, string[]>> = {
8
+ anthropic: [
9
+ "claude-sonnet-4-5-20250514",
10
+ "claude-opus-4-5-20250514",
11
+ "claude-haiku-4-5-20251001",
12
+ ],
13
+ openai: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"],
14
+ google: ["gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash"],
15
+ groq: ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768"],
16
+ openrouter: ["openai/gpt-4o", "openai/gpt-4o-mini", "anthropic/claude-3.5-sonnet"],
17
+ ollama: ["llama3.2", "llama3.1", "mistral", "codellama", "phi3"],
18
+ custom: [],
19
+ };
20
+
21
+ export function ModelStep({
22
+ providerType,
23
+ onSelect,
24
+ }: {
25
+ providerType: Provider;
26
+ onSelect: (model: string) => void;
27
+ }) {
28
+ const models = DEFAULT_MODELS[providerType] ?? [];
29
+ const [index, setIndex] = useState(0);
30
+ const [custom, setCustom] = useState("");
31
+ const [typing, setTyping] = useState(models.length === 0);
32
+
33
+ useInput((input, key) => {
34
+ if (typing) {
35
+ if (key.return && custom.trim()) {
36
+ onSelect(custom.trim());
37
+ return;
38
+ }
39
+ if (key.backspace || key.delete) {
40
+ setCustom((v) => v.slice(0, -1));
41
+ return;
42
+ }
43
+ if (!key.ctrl && !key.meta && input) setCustom((v) => v + input);
44
+ return;
45
+ }
46
+ if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
47
+ if (key.downArrow) setIndex((i) => Math.min(models.length, i + 1));
48
+ if (key.return) {
49
+ if (index === models.length) setTyping(true);
50
+ else onSelect(models[index]!);
51
+ }
52
+ });
53
+
54
+ return (
55
+ <Box flexDirection="column" gap={1}>
56
+ <Text bold color={ACCENT}>
57
+ Select a model
58
+ </Text>
59
+ {models.map((m, i) => {
60
+ const selected = !typing && i === index;
61
+ return (
62
+ <Box key={m} marginLeft={1}>
63
+ <Text color={selected ? ACCENT : "white"}>
64
+ {selected ? figures.arrowRight : " "}
65
+ {" "}
66
+ {m}
67
+ </Text>
68
+ </Box>
69
+ );
70
+ })}
71
+ <Box marginLeft={1}>
72
+ <Text color={index === models.length && !typing ? ACCENT : "gray"}>
73
+ {index === models.length && !typing ? figures.arrowRight : " "}
74
+ {" "}
75
+ {typing ? (
76
+ <Text>
77
+ Model name:{" "}
78
+ <Text color="white">{custom || " "}</Text>
79
+ </Text>
80
+ ) : (
81
+ "Enter custom model name"
82
+ )}
83
+ </Text>
84
+ </Box>
85
+ <Text color="gray" dimColor>
86
+ ↑↓ navigate · enter to select
87
+ </Text>
88
+ </Box>
89
+ );
90
+ }