@ridit/lens 0.2.4 → 0.2.6
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/LENS.md +32 -68
- package/README.md +91 -0
- package/addons/README.md +3 -0
- package/addons/run-tests.js +127 -0
- package/dist/index.mjs +226459 -2638
- package/package.json +13 -4
- package/src/colors.ts +5 -0
- package/src/commands/commit.tsx +686 -0
- package/src/commands/provider.tsx +36 -22
- package/src/components/__tests__/Header.test.tsx +9 -0
- package/src/components/chat/ChatMessage.tsx +6 -6
- package/src/components/chat/ChatOverlays.tsx +20 -10
- package/src/components/chat/ChatRunner.tsx +197 -31
- package/src/components/provider/ApiKeyStep.tsx +77 -121
- package/src/components/provider/ModelStep.tsx +35 -20
- package/src/components/{repo → provider}/ProviderPicker.tsx +1 -1
- package/src/components/provider/ProviderTypeStep.tsx +12 -5
- package/src/components/provider/RemoveProviderStep.tsx +7 -8
- package/src/components/repo/RepoAnalysis.tsx +1 -1
- package/src/components/task/TaskRunner.tsx +1 -1
- package/src/components/timeline/CommitDetail.tsx +2 -4
- package/src/components/timeline/CommitList.tsx +2 -14
- package/src/components/timeline/TimelineChat.tsx +1 -2
- package/src/components/timeline/TimelineRunner.tsx +506 -423
- package/src/index.tsx +38 -0
- package/src/prompts/fewshot.ts +144 -47
- package/src/prompts/system.ts +25 -21
- package/src/tools/chart.ts +210 -0
- package/src/tools/convert-image.ts +312 -0
- package/src/tools/files.ts +1 -9
- package/src/tools/git.ts +577 -0
- package/src/tools/index.ts +17 -13
- package/src/tools/pdf.ts +136 -78
- package/src/tools/view-image.ts +335 -0
- package/src/tools/web.ts +0 -4
- package/src/utils/addons/loadAddons.ts +6 -3
- package/src/utils/chat.ts +38 -23
- package/src/utils/thinking.tsx +275 -162
- package/src/utils/tools/builtins.ts +39 -32
- package/src/utils/tools/registry.ts +0 -14
- package/tsconfig.json +2 -2
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { Box, Text, useInput
|
|
1
|
+
import { Box, Text, useInput } from "ink";
|
|
2
|
+
import TextInput from "ink-text-input";
|
|
2
3
|
import { useState } from "react";
|
|
3
4
|
import { execSync } from "child_process";
|
|
4
5
|
import type { ProviderType } from "../../types/config";
|
|
6
|
+
import { ACCENT, TEXT } from "../../colors";
|
|
5
7
|
|
|
6
8
|
const LABELS: Record<ProviderType, string> = {
|
|
7
9
|
anthropic: "Anthropic API key",
|
|
8
10
|
gemini: "Gemini API key",
|
|
9
11
|
openai: "OpenAI API key",
|
|
10
|
-
ollama: "Ollama base URL
|
|
12
|
+
ollama: "Ollama base URL",
|
|
11
13
|
custom: "API key",
|
|
12
14
|
};
|
|
13
15
|
|
|
@@ -38,184 +40,139 @@ function readClipboard(): string | null {
|
|
|
38
40
|
type CustomResult = { apiKey: string; baseUrl?: string };
|
|
39
41
|
type Field = "apiKey" | "baseUrl";
|
|
40
42
|
|
|
41
|
-
const useFieldInput = (initial: string, onPasteError: (v: boolean) => void) => {
|
|
42
|
-
const [value, setValue] = useState(initial);
|
|
43
|
-
|
|
44
|
-
const handle = (input: string, key: Key) => {
|
|
45
|
-
if (key.backspace || key.delete) {
|
|
46
|
-
setValue((v) => v.slice(0, -1));
|
|
47
|
-
onPasteError(false);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (key.ctrl && input === "v") {
|
|
51
|
-
const clip = readClipboard();
|
|
52
|
-
if (clip) {
|
|
53
|
-
setValue((v) => v + clip);
|
|
54
|
-
onPasteError(false);
|
|
55
|
-
} else onPasteError(true);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
if (key.ctrl && input === "a") {
|
|
59
|
-
setValue("");
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
if (!key.ctrl && !key.meta && input) {
|
|
63
|
-
setValue((v) => v + input);
|
|
64
|
-
onPasteError(false);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
return { value, setValue, handle };
|
|
69
|
-
};
|
|
70
|
-
|
|
71
43
|
const SimpleInput = ({
|
|
72
44
|
providerType,
|
|
73
45
|
onSubmit,
|
|
74
|
-
|
|
46
|
+
onBack,
|
|
75
47
|
}: {
|
|
76
48
|
providerType: Exclude<ProviderType, "custom">;
|
|
77
49
|
onSubmit: (value: string) => void;
|
|
78
|
-
|
|
50
|
+
onBack?: () => void;
|
|
79
51
|
}) => {
|
|
80
|
-
const [pasteError, setPasteError] = useState(false);
|
|
81
52
|
const isPassword = providerType !== "ollama";
|
|
82
|
-
const
|
|
53
|
+
const [value, setValue] = useState(
|
|
83
54
|
providerType === "ollama" ? "http://localhost:11434" : "",
|
|
84
|
-
setPasteError,
|
|
85
55
|
);
|
|
86
56
|
|
|
87
57
|
useInput((input, key) => {
|
|
88
|
-
if (key.
|
|
89
|
-
|
|
58
|
+
if (key.escape) {
|
|
59
|
+
onBack?.();
|
|
90
60
|
return;
|
|
91
61
|
}
|
|
92
|
-
if (key.
|
|
93
|
-
|
|
94
|
-
|
|
62
|
+
if (key.ctrl && input === "v") {
|
|
63
|
+
const clip = readClipboard();
|
|
64
|
+
if (clip) setValue((v) => v + clip);
|
|
95
65
|
}
|
|
96
|
-
handle(input, key);
|
|
97
66
|
});
|
|
98
67
|
|
|
99
|
-
const display = isPassword ? "•".repeat(value.length) : value;
|
|
100
|
-
|
|
101
68
|
return (
|
|
102
69
|
<Box flexDirection="column" gap={1}>
|
|
103
|
-
<Text
|
|
104
|
-
{LABELS[providerType]}
|
|
105
|
-
</Text>
|
|
70
|
+
<Text color={TEXT}>{LABELS[providerType]}</Text>
|
|
106
71
|
<Box borderStyle="round" borderColor="gray" paddingX={1}>
|
|
107
|
-
<
|
|
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
|
+
/>
|
|
108
83
|
</Box>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
<Text color="gray">
|
|
113
|
-
enter to confirm · ctrl+v to paste · ctrl+a to clear
|
|
114
|
-
{onSkip ? " · esc to skip" : ""}
|
|
115
|
-
</Text>
|
|
116
|
-
)}
|
|
84
|
+
<Text color="gray">
|
|
85
|
+
enter to confirm · ctrl+v to paste{onBack ? " · esc back" : ""}
|
|
86
|
+
</Text>
|
|
117
87
|
</Box>
|
|
118
88
|
);
|
|
119
89
|
};
|
|
120
90
|
|
|
121
91
|
const CustomInput = ({
|
|
122
92
|
onSubmit,
|
|
123
|
-
|
|
93
|
+
onBack,
|
|
124
94
|
}: {
|
|
125
95
|
onSubmit: (result: CustomResult) => void;
|
|
126
|
-
|
|
96
|
+
onBack?: () => void;
|
|
127
97
|
}) => {
|
|
128
98
|
const [activeField, setActiveField] = useState<Field>("apiKey");
|
|
129
|
-
const [
|
|
130
|
-
|
|
131
|
-
const apiKeyField = useFieldInput("", setPasteError);
|
|
132
|
-
const baseUrlField = useFieldInput("", setPasteError);
|
|
133
|
-
|
|
134
|
-
const active = activeField === "apiKey" ? apiKeyField : baseUrlField;
|
|
99
|
+
const [apiKey, setApiKey] = useState("");
|
|
100
|
+
const [baseUrl, setBaseUrl] = useState("");
|
|
135
101
|
|
|
136
102
|
useInput((input, key) => {
|
|
137
|
-
if (key.escape
|
|
138
|
-
|
|
103
|
+
if (key.escape) {
|
|
104
|
+
onBack?.();
|
|
139
105
|
return;
|
|
140
106
|
}
|
|
141
|
-
|
|
142
107
|
if (key.tab) {
|
|
143
108
|
setActiveField((f) => (f === "apiKey" ? "baseUrl" : "apiKey"));
|
|
144
|
-
setPasteError(false);
|
|
145
109
|
return;
|
|
146
110
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
if (activeField === "baseUrl" && apiKeyField.value.trim()) {
|
|
154
|
-
onSubmit({
|
|
155
|
-
apiKey: apiKeyField.value.trim(),
|
|
156
|
-
baseUrl: baseUrlField.value.trim() || undefined,
|
|
157
|
-
});
|
|
158
|
-
return;
|
|
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);
|
|
159
116
|
}
|
|
160
117
|
}
|
|
161
|
-
|
|
162
|
-
active.handle(input, key);
|
|
163
118
|
});
|
|
164
119
|
|
|
165
|
-
const fields: {
|
|
166
|
-
id:
|
|
167
|
-
label: string;
|
|
168
|
-
password: boolean;
|
|
169
|
-
placeholder: string;
|
|
170
|
-
}[] = [
|
|
171
|
-
{ id: "apiKey", label: "API key", password: true, placeholder: "sk-..." },
|
|
120
|
+
const fields: { id: Field; label: string; placeholder: string }[] = [
|
|
121
|
+
{ id: "apiKey", label: "API key", placeholder: "sk-..." },
|
|
172
122
|
{
|
|
173
123
|
id: "baseUrl",
|
|
174
|
-
label: "Base URL",
|
|
175
|
-
password: false,
|
|
124
|
+
label: "Base URL (optional)",
|
|
176
125
|
placeholder: "https://api.example.com/v1",
|
|
177
126
|
},
|
|
178
127
|
];
|
|
179
128
|
|
|
180
129
|
return (
|
|
181
130
|
<Box flexDirection="column" gap={1}>
|
|
182
|
-
|
|
183
|
-
Custom provider
|
|
184
|
-
</Text>
|
|
185
|
-
|
|
186
|
-
{fields.map(({ id, label, password, placeholder }) => {
|
|
131
|
+
{fields.map(({ id, label, placeholder }) => {
|
|
187
132
|
const isActive = activeField === id;
|
|
188
|
-
const val = id === "apiKey" ?
|
|
189
|
-
const display = password ? "•".repeat(val.length) : val;
|
|
190
|
-
|
|
133
|
+
const val = id === "apiKey" ? apiKey : baseUrl;
|
|
191
134
|
return (
|
|
192
135
|
<Box key={id} flexDirection="column" gap={0}>
|
|
193
|
-
<Text color={isActive ?
|
|
136
|
+
<Text color={isActive ? ACCENT : "gray"}>
|
|
194
137
|
{isActive ? "›" : " "} {label}
|
|
195
|
-
{id === "baseUrl" ? " (optional)" : ""}
|
|
196
138
|
</Text>
|
|
197
139
|
<Box
|
|
198
140
|
borderStyle="round"
|
|
199
|
-
borderColor={isActive ?
|
|
141
|
+
borderColor={isActive ? ACCENT : "gray"}
|
|
200
142
|
paddingX={1}
|
|
201
143
|
>
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
)}
|
|
205
168
|
</Box>
|
|
206
169
|
</Box>
|
|
207
170
|
);
|
|
208
171
|
})}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
<Text color="gray">
|
|
214
|
-
enter to next field · tab to switch · ctrl+v to paste · ctrl+a to
|
|
215
|
-
clear
|
|
216
|
-
{onSkip ? " · esc to skip" : ""}
|
|
217
|
-
</Text>
|
|
218
|
-
)}
|
|
172
|
+
<Text color="gray">
|
|
173
|
+
enter to next · tab to switch · ctrl+v to paste
|
|
174
|
+
{onBack ? " · esc back" : ""}
|
|
175
|
+
</Text>
|
|
219
176
|
</Box>
|
|
220
177
|
);
|
|
221
178
|
};
|
|
@@ -223,21 +180,20 @@ const CustomInput = ({
|
|
|
223
180
|
export const ApiKeyStep = ({
|
|
224
181
|
providerType,
|
|
225
182
|
onSubmit,
|
|
226
|
-
|
|
183
|
+
onBack,
|
|
227
184
|
}: {
|
|
228
185
|
providerType: ProviderType;
|
|
229
186
|
onSubmit: (value: string | CustomResult) => void;
|
|
230
|
-
|
|
187
|
+
onBack?: () => void;
|
|
231
188
|
}) => {
|
|
232
189
|
if (providerType === "custom") {
|
|
233
|
-
return <CustomInput onSubmit={onSubmit}
|
|
190
|
+
return <CustomInput onSubmit={onSubmit} onBack={onBack} />;
|
|
234
191
|
}
|
|
235
|
-
|
|
236
192
|
return (
|
|
237
193
|
<SimpleInput
|
|
238
194
|
providerType={providerType}
|
|
239
195
|
onSubmit={onSubmit}
|
|
240
|
-
|
|
196
|
+
onBack={onBack}
|
|
241
197
|
/>
|
|
242
198
|
);
|
|
243
199
|
};
|
|
@@ -1,34 +1,35 @@
|
|
|
1
1
|
import { Box, Text, useInput } from "ink";
|
|
2
|
+
import TextInput from "ink-text-input";
|
|
2
3
|
import figures from "figures";
|
|
3
4
|
import { useState } from "react";
|
|
4
5
|
import { DEFAULT_MODELS } from "../../utils/config";
|
|
5
6
|
import type { ProviderType } from "../../types/config";
|
|
7
|
+
import { TEXT } from "../../colors";
|
|
6
8
|
|
|
7
9
|
export const ModelStep = ({
|
|
8
10
|
providerType,
|
|
9
11
|
onSelect,
|
|
12
|
+
onBack,
|
|
10
13
|
}: {
|
|
11
14
|
providerType: ProviderType;
|
|
12
15
|
onSelect: (model: string) => void;
|
|
16
|
+
onBack?: () => void;
|
|
13
17
|
}) => {
|
|
14
18
|
const models = DEFAULT_MODELS[providerType] ?? [];
|
|
15
19
|
const [index, setIndex] = useState(0);
|
|
16
20
|
const [custom, setCustom] = useState("");
|
|
17
21
|
const [typing, setTyping] = useState(models.length === 0);
|
|
18
22
|
|
|
19
|
-
useInput((
|
|
20
|
-
if (
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
+
useInput((_, key) => {
|
|
24
|
+
if (key.escape) {
|
|
25
|
+
if (typing && models.length > 0) {
|
|
26
|
+
setTyping(false);
|
|
23
27
|
return;
|
|
24
28
|
}
|
|
25
|
-
|
|
26
|
-
setCustom((v) => v.slice(0, -1));
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
if (!key.ctrl && !key.meta && input) setCustom((v) => v + input);
|
|
29
|
+
onBack?.();
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
+
if (typing) return;
|
|
32
33
|
if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
|
|
33
34
|
if (key.downArrow) setIndex((i) => Math.min(models.length, i + 1));
|
|
34
35
|
if (key.return) {
|
|
@@ -55,19 +56,33 @@ export const ModelStep = ({
|
|
|
55
56
|
);
|
|
56
57
|
})}
|
|
57
58
|
<Box marginLeft={1}>
|
|
58
|
-
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
Custom: <Text color="white">{custom || " "}</Text>
|
|
59
|
+
{typing ? (
|
|
60
|
+
<Box gap={1}>
|
|
61
|
+
<Text color={TEXT}>
|
|
62
|
+
{figures.arrowRight}
|
|
63
|
+
{" "}Custom:{" "}
|
|
64
64
|
</Text>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
)}
|
|
69
80
|
</Box>
|
|
70
|
-
<Text color="gray"
|
|
81
|
+
<Text color="gray">
|
|
82
|
+
{typing
|
|
83
|
+
? "enter to confirm · esc back"
|
|
84
|
+
: `↑↓ navigate · enter to select${onBack ? " · esc back" : ""}`}
|
|
85
|
+
</Text>
|
|
71
86
|
</Box>
|
|
72
87
|
);
|
|
73
88
|
};
|
|
@@ -2,6 +2,7 @@ import { Box, Text, useInput } from "ink";
|
|
|
2
2
|
import figures from "figures";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import type { ProviderType } from "../../types/config";
|
|
5
|
+
import { ACCENT, TEXT } from "../../colors";
|
|
5
6
|
|
|
6
7
|
const OPTIONS: { type: ProviderType; label: string; description: string }[] = [
|
|
7
8
|
{ type: "anthropic", label: "Anthropic", description: "Claude models" },
|
|
@@ -16,12 +17,18 @@ const OPTIONS: { type: ProviderType; label: string; description: string }[] = [
|
|
|
16
17
|
|
|
17
18
|
export const ProviderTypeStep = ({
|
|
18
19
|
onSelect,
|
|
20
|
+
onBack,
|
|
19
21
|
}: {
|
|
20
22
|
onSelect: (type: ProviderType) => void;
|
|
23
|
+
onBack?: () => void;
|
|
21
24
|
}) => {
|
|
22
25
|
const [index, setIndex] = useState(0);
|
|
23
26
|
|
|
24
27
|
useInput((_, key) => {
|
|
28
|
+
if (key.escape) {
|
|
29
|
+
onBack?.();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
25
32
|
if (key.upArrow) setIndex((i) => Math.max(0, i - 1));
|
|
26
33
|
if (key.downArrow) setIndex((i) => Math.min(OPTIONS.length - 1, i + 1));
|
|
27
34
|
if (key.return) onSelect(OPTIONS[index]!.type);
|
|
@@ -29,14 +36,12 @@ export const ProviderTypeStep = ({
|
|
|
29
36
|
|
|
30
37
|
return (
|
|
31
38
|
<Box flexDirection="column" gap={1}>
|
|
32
|
-
<Text
|
|
33
|
-
Select a provider
|
|
34
|
-
</Text>
|
|
39
|
+
<Text color={TEXT}>Select a provider</Text>
|
|
35
40
|
{OPTIONS.map((opt, i) => {
|
|
36
41
|
const selected = i === index;
|
|
37
42
|
return (
|
|
38
43
|
<Box key={opt.type} marginLeft={1}>
|
|
39
|
-
<Text color={selected ?
|
|
44
|
+
<Text color={selected ? ACCENT : "white"}>
|
|
40
45
|
{selected ? figures.arrowRight : " "}
|
|
41
46
|
{" "}
|
|
42
47
|
<Text bold={selected}>{opt.label}</Text>
|
|
@@ -48,7 +53,9 @@ export const ProviderTypeStep = ({
|
|
|
48
53
|
</Box>
|
|
49
54
|
);
|
|
50
55
|
})}
|
|
51
|
-
<Text color="gray"
|
|
56
|
+
<Text color="gray">
|
|
57
|
+
↑↓ navigate · enter to select{onBack ? " · esc back" : ""}
|
|
58
|
+
</Text>
|
|
52
59
|
</Box>
|
|
53
60
|
);
|
|
54
61
|
};
|
|
@@ -3,6 +3,7 @@ import figures from "figures";
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { loadConfig, saveConfig } from "../../utils/config";
|
|
5
5
|
import type { Provider } from "../../types/config";
|
|
6
|
+
import { RED, TEXT } from "../../colors";
|
|
6
7
|
|
|
7
8
|
export const RemoveProviderStep = ({ onDone }: { onDone: () => void }) => {
|
|
8
9
|
const config = loadConfig();
|
|
@@ -38,7 +39,7 @@ export const RemoveProviderStep = ({ onDone }: { onDone: () => void }) => {
|
|
|
38
39
|
if (providers.length === 0) {
|
|
39
40
|
return (
|
|
40
41
|
<Box marginTop={1}>
|
|
41
|
-
<Text color="gray">
|
|
42
|
+
<Text color="gray">No providers configured.</Text>
|
|
42
43
|
</Box>
|
|
43
44
|
);
|
|
44
45
|
}
|
|
@@ -48,8 +49,8 @@ export const RemoveProviderStep = ({ onDone }: { onDone: () => void }) => {
|
|
|
48
49
|
if (confirming && selected) {
|
|
49
50
|
return (
|
|
50
51
|
<Box flexDirection="column" gap={1} marginTop={1}>
|
|
51
|
-
<Text color=
|
|
52
|
-
{figures.warning} Remove <Text
|
|
52
|
+
<Text color={RED}>
|
|
53
|
+
{figures.warning} Remove <Text>{selected.name}</Text>? (y/n)
|
|
53
54
|
</Text>
|
|
54
55
|
</Box>
|
|
55
56
|
);
|
|
@@ -57,17 +58,15 @@ export const RemoveProviderStep = ({ onDone }: { onDone: () => void }) => {
|
|
|
57
58
|
|
|
58
59
|
return (
|
|
59
60
|
<Box flexDirection="column" gap={1} marginTop={1}>
|
|
60
|
-
<Text
|
|
61
|
-
Remove a provider
|
|
62
|
-
</Text>
|
|
61
|
+
<Text color={TEXT}>Remove a provider</Text>
|
|
63
62
|
{providers.map((p, i) => {
|
|
64
63
|
const isSelected = i === index;
|
|
65
64
|
return (
|
|
66
65
|
<Box key={p.id} marginLeft={1}>
|
|
67
|
-
<Text color={isSelected ?
|
|
66
|
+
<Text color={isSelected ? RED : "white"}>
|
|
68
67
|
{isSelected ? figures.arrowRight : " "}
|
|
69
68
|
{" "}
|
|
70
|
-
<Text
|
|
69
|
+
<Text>{p.name}</Text>
|
|
71
70
|
<Text color="gray">
|
|
72
71
|
{" "}
|
|
73
72
|
{p.type} · {p.model}
|
|
@@ -7,7 +7,7 @@ import { writeFileSync } from "fs";
|
|
|
7
7
|
import path from "path";
|
|
8
8
|
import { ACCENT } from "../../colors";
|
|
9
9
|
import { requestFileList, analyzeRepo } from "../../utils/ai";
|
|
10
|
-
import { ProviderPicker } from "
|
|
10
|
+
import { ProviderPicker } from "../provider/ProviderPicker";
|
|
11
11
|
import { PreviewRunner } from "./PreviewRunner";
|
|
12
12
|
import { IssueFixer } from "./IssueFixer";
|
|
13
13
|
import { writeLensFile } from "../../utils/lensfile";
|
|
@@ -8,7 +8,7 @@ import path from "path";
|
|
|
8
8
|
import { ACCENT } from "../../colors";
|
|
9
9
|
import { callModelRaw } from "../../utils/ai";
|
|
10
10
|
import { DiffViewer, buildDiffs } from "../repo/DiffViewer";
|
|
11
|
-
import { ProviderPicker } from "../
|
|
11
|
+
import { ProviderPicker } from "../provider/ProviderPicker";
|
|
12
12
|
import type { DiffLine, FilePatch } from "../repo/DiffViewer";
|
|
13
13
|
import type { Provider } from "../../types/config";
|
|
14
14
|
import type { ImportantFile } from "../../types/repo";
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import type { Commit, DiffFile } from "../../utils/git";
|
|
4
|
-
|
|
5
|
-
const ACCENT = "#FF8C00";
|
|
4
|
+
import { ACCENT } from "../../colors";
|
|
6
5
|
|
|
7
6
|
type Props = {
|
|
8
7
|
commit: Commit | null;
|
|
@@ -73,7 +72,6 @@ export function CommitDetail({
|
|
|
73
72
|
|
|
74
73
|
const divider = "─".repeat(Math.max(0, width - 2));
|
|
75
74
|
|
|
76
|
-
// Build all diff lines for scrolling
|
|
77
75
|
const allDiffLines: Array<{
|
|
78
76
|
type: string;
|
|
79
77
|
content: string;
|
|
@@ -177,7 +175,7 @@ export function CommitDetail({
|
|
|
177
175
|
{/* stats bar */}
|
|
178
176
|
<Box paddingX={1} marginTop={1} gap={3}>
|
|
179
177
|
<Text color="green">+{commit.insertions} insertions</Text>
|
|
180
|
-
<Text color="red">-{commit.deletions}
|
|
178
|
+
<Text color="red">-{commit.deletions}</Text>
|
|
181
179
|
<Text color="gray" dimColor>
|
|
182
180
|
{commit.filesChanged} file{commit.filesChanged !== 1 ? "s" : ""}{" "}
|
|
183
181
|
changed
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import type { Commit } from "../../utils/git";
|
|
4
|
-
|
|
5
|
-
const ACCENT = "#FF8C00";
|
|
4
|
+
import { ACCENT } from "../../colors";
|
|
6
5
|
|
|
7
6
|
type Props = {
|
|
8
7
|
commits: Commit[];
|
|
@@ -29,7 +28,6 @@ function formatRefs(refs: string): string {
|
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
function shortDate(dateStr: string): string {
|
|
32
|
-
// "2026-03-12 14:22:01 +0530" → "Mar 12"
|
|
33
31
|
try {
|
|
34
32
|
const d = new Date(dateStr);
|
|
35
33
|
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
@@ -82,8 +80,7 @@ export function CommitList({
|
|
|
82
80
|
const refs = formatRefs(commit.refs);
|
|
83
81
|
const date = shortDate(commit.date);
|
|
84
82
|
|
|
85
|
-
|
|
86
|
-
const prefixLen = 14; // symbol + hash + date
|
|
83
|
+
const prefixLen = 14;
|
|
87
84
|
const maxMsg = Math.max(10, width - prefixLen - 3);
|
|
88
85
|
const msg =
|
|
89
86
|
commit.message.length > maxMsg
|
|
@@ -92,22 +89,18 @@ export function CommitList({
|
|
|
92
89
|
|
|
93
90
|
return (
|
|
94
91
|
<Box key={commit.hash} paddingX={1} flexDirection="column">
|
|
95
|
-
{/* graph line above (not first) */}
|
|
96
92
|
{i > 0 && (
|
|
97
93
|
<Text color="gray" dimColor>
|
|
98
94
|
{"│"}
|
|
99
95
|
</Text>
|
|
100
96
|
)}
|
|
101
97
|
<Box gap={1}>
|
|
102
|
-
{/* selection indicator */}
|
|
103
98
|
<Text color={isSelected ? ACCENT : "gray"}>
|
|
104
99
|
{isSelected ? "▶" : " "}
|
|
105
100
|
</Text>
|
|
106
101
|
|
|
107
|
-
{/* graph node */}
|
|
108
102
|
<Text color={isSelected ? ACCENT : color}>{symbol}</Text>
|
|
109
103
|
|
|
110
|
-
{/* short hash */}
|
|
111
104
|
<Text
|
|
112
105
|
color={isSelected ? "white" : "gray"}
|
|
113
106
|
dimColor={!isSelected}
|
|
@@ -115,12 +108,10 @@ export function CommitList({
|
|
|
115
108
|
{commit.shortHash}
|
|
116
109
|
</Text>
|
|
117
110
|
|
|
118
|
-
{/* date */}
|
|
119
111
|
<Text color="cyan" dimColor={!isSelected}>
|
|
120
112
|
{date}
|
|
121
113
|
</Text>
|
|
122
114
|
|
|
123
|
-
{/* message */}
|
|
124
115
|
<Text
|
|
125
116
|
color={isSelected ? "white" : "gray"}
|
|
126
117
|
bold={isSelected}
|
|
@@ -130,14 +121,12 @@ export function CommitList({
|
|
|
130
121
|
</Text>
|
|
131
122
|
</Box>
|
|
132
123
|
|
|
133
|
-
{/* refs on selected */}
|
|
134
124
|
{isSelected && refs && (
|
|
135
125
|
<Box paddingLeft={4}>
|
|
136
126
|
<Text color="yellow">{refs}</Text>
|
|
137
127
|
</Box>
|
|
138
128
|
)}
|
|
139
129
|
|
|
140
|
-
{/* stat summary on selected */}
|
|
141
130
|
{isSelected && (
|
|
142
131
|
<Box paddingLeft={4} gap={2}>
|
|
143
132
|
<Text color="gray" dimColor>
|
|
@@ -159,7 +148,6 @@ export function CommitList({
|
|
|
159
148
|
);
|
|
160
149
|
})}
|
|
161
150
|
|
|
162
|
-
{/* scroll hint */}
|
|
163
151
|
<Box paddingX={1} marginTop={1}>
|
|
164
152
|
<Text color="gray" dimColor>
|
|
165
153
|
{scrollOffset > 0 ? "↑ more above" : ""}
|
|
@@ -5,8 +5,7 @@ import type { Commit } from "../../utils/git";
|
|
|
5
5
|
import { summarizeTimeline } from "../../utils/git";
|
|
6
6
|
import type { Provider } from "../../types/config";
|
|
7
7
|
import { callChat } from "../../utils/chat";
|
|
8
|
-
|
|
9
|
-
const ACCENT = "#FF8C00";
|
|
8
|
+
import { ACCENT } from "../../colors";
|
|
10
9
|
|
|
11
10
|
type TLMessage = { role: "user" | "assistant"; content: string; type: "text" };
|
|
12
11
|
|