@cdoing/opentuicli 0.1.21 → 0.1.26
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/README.md +22 -13
- package/dist/cdoing-tui-darwin-arm64/bin/cdoing-tui +0 -0
- package/package.json +12 -9
- package/dist/index.js +0 -64
- package/dist/index.js.map +0 -7
- package/esbuild.config.cjs +0 -45
- package/src/app.tsx +0 -787
- package/src/components/dialog-command.tsx +0 -207
- package/src/components/dialog-help.tsx +0 -151
- package/src/components/dialog-model.tsx +0 -142
- package/src/components/dialog-status.tsx +0 -84
- package/src/components/dialog-theme.tsx +0 -318
- package/src/components/input-area.tsx +0 -380
- package/src/components/loading-spinner.tsx +0 -28
- package/src/components/message-list.tsx +0 -546
- package/src/components/permission-prompt.tsx +0 -72
- package/src/components/session-browser.tsx +0 -231
- package/src/components/session-footer.tsx +0 -30
- package/src/components/session-header.tsx +0 -39
- package/src/components/setup-wizard.tsx +0 -542
- package/src/components/sidebar.tsx +0 -183
- package/src/components/status-bar.tsx +0 -76
- package/src/components/toast.tsx +0 -139
- package/src/context/sdk.tsx +0 -40
- package/src/context/theme.tsx +0 -640
- package/src/index.ts +0 -50
- package/src/lib/autocomplete.ts +0 -262
- package/src/lib/context-providers.ts +0 -98
- package/src/lib/history.ts +0 -164
- package/src/lib/terminal-title.ts +0 -15
- package/src/routes/home.tsx +0 -148
- package/src/routes/session.tsx +0 -1309
- package/src/store/settings.ts +0 -107
- package/tsconfig.json +0 -23
|
@@ -1,542 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SetupWizard — Interactive setup overlay for configuring provider, model, API key, or OAuth
|
|
3
|
-
*
|
|
4
|
-
* Steps:
|
|
5
|
-
* 1. Select provider
|
|
6
|
-
* 2. Select auth method (API key or OAuth — Anthropic only)
|
|
7
|
-
* 3. Select model
|
|
8
|
-
* 4. Enter API key / OAuth code
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import * as fs from "fs";
|
|
12
|
-
import * as path from "path";
|
|
13
|
-
import * as os from "os";
|
|
14
|
-
import { execSync } from "child_process";
|
|
15
|
-
import { TextAttributes } from "@opentui/core";
|
|
16
|
-
import { useState, useRef, useEffect } from "react";
|
|
17
|
-
import { useKeyboard } from "@opentui/react";
|
|
18
|
-
import { useTheme } from "../context/theme";
|
|
19
|
-
import { getProviders } from "@cdoing/ai";
|
|
20
|
-
import { getOAuthProvider, supportsOAuth } from "@cdoing/core";
|
|
21
|
-
|
|
22
|
-
export interface SetupWizardProps {
|
|
23
|
-
onComplete: (config: { provider: string; model: string; apiKey?: string; oauthToken?: string }) => void;
|
|
24
|
-
onClose: () => void;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// ── Data from centralized catalog ─────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
const _catalog = getProviders();
|
|
30
|
-
|
|
31
|
-
const PROVIDERS = _catalog.map((p: { id: string; label: string; hint: string }) => ({
|
|
32
|
-
id: p.id,
|
|
33
|
-
name: p.label,
|
|
34
|
-
hint: p.hint,
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
const AUTH_METHODS = [
|
|
38
|
-
{ id: "apikey", name: "API Key", hint: "all models available - console.anthropic.com" },
|
|
39
|
-
{ id: "oauth", name: "OAuth", hint: "Claude Pro/Max - opens browser" },
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
// All models per provider — from catalog
|
|
43
|
-
const MODELS: Record<string, { id: string; name: string; hint?: string }[]> = {};
|
|
44
|
-
for (const p of _catalog) {
|
|
45
|
-
MODELS[p.id] = p.models.map((m: { id: string; label: string; hint?: string }) => ({ id: m.id, name: m.label, hint: m.hint }));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Get OAuth models for a provider from core config */
|
|
49
|
-
function getOAuthModels(providerId: string) {
|
|
50
|
-
const config = getOAuthProvider(providerId);
|
|
51
|
-
if (!config?.models || config.models.length === 0) {
|
|
52
|
-
return config?.defaultModel
|
|
53
|
-
? [{ id: config.defaultModel, name: config.defaultModel, hint: "OAuth" }]
|
|
54
|
-
: [];
|
|
55
|
-
}
|
|
56
|
-
return config.models.map((m: { id: string; name: string; hint?: string }) => ({ id: m.id, name: m.name, hint: m.hint || "OAuth" }));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function providerSupportsOAuth(providerId: string): boolean {
|
|
60
|
-
return supportsOAuth(providerId);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Env vars and key URLs from catalog
|
|
64
|
-
const ENV_VARS: Record<string, string> = {};
|
|
65
|
-
const KEY_URLS: Record<string, string> = {};
|
|
66
|
-
for (const p of _catalog as Array<{ id: string; envVar?: string; keyUrl?: string }>) {
|
|
67
|
-
if (p.envVar) ENV_VARS[p.id] = p.envVar;
|
|
68
|
-
if (p.keyUrl) KEY_URLS[p.id] = p.keyUrl;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
type Step = "provider" | "auth-method" | "model" | "apikey" | "oauth-paste" | "oauth-exchanging";
|
|
72
|
-
|
|
73
|
-
function readClipboard(): string {
|
|
74
|
-
try {
|
|
75
|
-
if (process.platform === "darwin") return execSync("pbpaste", { encoding: "utf-8" });
|
|
76
|
-
try { return execSync("xclip -selection clipboard -o", { encoding: "utf-8" }); }
|
|
77
|
-
catch { return execSync("xsel --clipboard --output", { encoding: "utf-8" }); }
|
|
78
|
-
} catch { return ""; }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function openBrowser(url: string): void {
|
|
82
|
-
try {
|
|
83
|
-
const cmd = process.platform === "darwin" ? `open "${url}"`
|
|
84
|
-
: process.platform === "win32" ? `start "" "${url}"`
|
|
85
|
-
: `xdg-open "${url}"`;
|
|
86
|
-
execSync(cmd, { stdio: "ignore", timeout: 3000 });
|
|
87
|
-
} catch { /* browser open is best-effort */ }
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function SetupWizard(props: SetupWizardProps) {
|
|
91
|
-
const { theme, customBg } = useTheme();
|
|
92
|
-
const t = theme;
|
|
93
|
-
|
|
94
|
-
const closedRef = useRef(false);
|
|
95
|
-
const [closed, setClosed] = useState(false);
|
|
96
|
-
const [step, setStep] = useState<Step>("provider");
|
|
97
|
-
const [selectedProvider, setSelectedProvider] = useState(0);
|
|
98
|
-
const [selectedAuth, setSelectedAuth] = useState(0);
|
|
99
|
-
const [selectedModel, setSelectedModel] = useState(0);
|
|
100
|
-
const [authMethod, setAuthMethod] = useState<"apikey" | "oauth">("apikey");
|
|
101
|
-
const [chosenProviderId, setChosenProviderId] = useState("anthropic");
|
|
102
|
-
const [chosenModelId, setChosenModelId] = useState("");
|
|
103
|
-
const [apiKeyInput, setApiKeyInput] = useState("");
|
|
104
|
-
const [showKey, setShowKey] = useState(false);
|
|
105
|
-
const [error, setError] = useState("");
|
|
106
|
-
// Custom model input
|
|
107
|
-
const [customModelInput, setCustomModelInput] = useState("");
|
|
108
|
-
const [isCustomModel, setIsCustomModel] = useState(false);
|
|
109
|
-
|
|
110
|
-
// OAuth state
|
|
111
|
-
const [oauthUrl, setOauthUrl] = useState("");
|
|
112
|
-
const [oauthVerifier, setOauthVerifier] = useState("");
|
|
113
|
-
const [oauthCodeInput, setOauthCodeInput] = useState("");
|
|
114
|
-
const [showCode, setShowCode] = useState(false);
|
|
115
|
-
const [oauthError, setOauthError] = useState("");
|
|
116
|
-
const exchangingRef = useRef(false);
|
|
117
|
-
|
|
118
|
-
const provider = PROVIDERS[selectedProvider];
|
|
119
|
-
const models = authMethod === "oauth" ? getOAuthModels(chosenProviderId) : (MODELS[chosenProviderId] || []);
|
|
120
|
-
|
|
121
|
-
// Generate OAuth URL when entering oauth-paste step
|
|
122
|
-
useEffect(() => {
|
|
123
|
-
if (step !== "oauth-paste") return;
|
|
124
|
-
try {
|
|
125
|
-
const { generateOAuthUrl } = require("@cdoing/core");
|
|
126
|
-
const { url, codeVerifier } = generateOAuthUrl(chosenProviderId);
|
|
127
|
-
setOauthUrl(url);
|
|
128
|
-
setOauthVerifier(codeVerifier);
|
|
129
|
-
setOauthCodeInput("");
|
|
130
|
-
setOauthError("");
|
|
131
|
-
openBrowser(url);
|
|
132
|
-
} catch {
|
|
133
|
-
setOauthError("OAuth not available. Install @cdoing/core with OAuth support.");
|
|
134
|
-
}
|
|
135
|
-
}, [step, chosenProviderId]);
|
|
136
|
-
|
|
137
|
-
useKeyboard((key: any) => {
|
|
138
|
-
// If already closed, ignore keys
|
|
139
|
-
if (closedRef.current) return;
|
|
140
|
-
|
|
141
|
-
// Escape — close wizard (parent handles navigation to home)
|
|
142
|
-
if (key.name === "escape") {
|
|
143
|
-
closedRef.current = true;
|
|
144
|
-
setClosed(true);
|
|
145
|
-
props.onClose();
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Ctrl+C — close wizard
|
|
150
|
-
if (key.ctrl && key.name === "c") {
|
|
151
|
-
closedRef.current = true;
|
|
152
|
-
setClosed(true);
|
|
153
|
-
props.onClose();
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ── Step 1: Provider ──
|
|
158
|
-
if (step === "provider") {
|
|
159
|
-
if (key.name === "up" || key.name === "k") {
|
|
160
|
-
setSelectedProvider((s) => Math.max(0, s - 1));
|
|
161
|
-
} else if (key.name === "down" || key.name === "j") {
|
|
162
|
-
setSelectedProvider((s) => Math.min(PROVIDERS.length - 1, s + 1));
|
|
163
|
-
} else if (key.name === "return") {
|
|
164
|
-
const p = PROVIDERS[selectedProvider];
|
|
165
|
-
setChosenProviderId(p.id);
|
|
166
|
-
setSelectedModel(0);
|
|
167
|
-
if (providerSupportsOAuth(p.id)) {
|
|
168
|
-
setSelectedAuth(0);
|
|
169
|
-
setStep("auth-method");
|
|
170
|
-
} else {
|
|
171
|
-
setAuthMethod("apikey");
|
|
172
|
-
setStep("model");
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ── Step 2: Auth method (any OAuth-capable provider) ──
|
|
179
|
-
if (step === "auth-method") {
|
|
180
|
-
if (key.name === "up" || key.name === "k") {
|
|
181
|
-
setSelectedAuth((s) => Math.max(0, s - 1));
|
|
182
|
-
} else if (key.name === "down" || key.name === "j") {
|
|
183
|
-
setSelectedAuth((s) => Math.min(AUTH_METHODS.length - 1, s + 1));
|
|
184
|
-
} else if (key.name === "return") {
|
|
185
|
-
const auth = AUTH_METHODS[selectedAuth].id as "apikey" | "oauth";
|
|
186
|
-
setAuthMethod(auth);
|
|
187
|
-
setSelectedModel(0);
|
|
188
|
-
setStep("model");
|
|
189
|
-
}
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// ── Step 3: Model (presets + custom input) ──
|
|
194
|
-
if (step === "model") {
|
|
195
|
-
// Custom model text input mode
|
|
196
|
-
if (isCustomModel) {
|
|
197
|
-
if (key.name === "return") {
|
|
198
|
-
const m = customModelInput.trim();
|
|
199
|
-
if (!m) return;
|
|
200
|
-
setChosenModelId(m);
|
|
201
|
-
if (chosenProviderId === "ollama") {
|
|
202
|
-
saveAndComplete(chosenProviderId, m, undefined);
|
|
203
|
-
} else if (authMethod === "oauth") {
|
|
204
|
-
exchangingRef.current = false;
|
|
205
|
-
setStep("oauth-paste");
|
|
206
|
-
} else {
|
|
207
|
-
setStep("apikey");
|
|
208
|
-
setApiKeyInput("");
|
|
209
|
-
setError("");
|
|
210
|
-
}
|
|
211
|
-
} else if ((key.ctrl || key.meta) && key.name === "v") {
|
|
212
|
-
const clip = readClipboard().trim();
|
|
213
|
-
if (clip) setCustomModelInput((s) => s + clip);
|
|
214
|
-
} else if (key.name === "backspace") {
|
|
215
|
-
setCustomModelInput((s) => s.slice(0, -1));
|
|
216
|
-
} else if (key.ctrl && key.name === "u") {
|
|
217
|
-
setCustomModelInput("");
|
|
218
|
-
} else if (key.sequence && !key.ctrl && !key.meta) {
|
|
219
|
-
setCustomModelInput((s) => s + key.sequence);
|
|
220
|
-
}
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Preset selection with "Custom..." at bottom
|
|
225
|
-
const totalItems = models.length + 1;
|
|
226
|
-
if (key.name === "up" || key.name === "k") {
|
|
227
|
-
setSelectedModel((s) => Math.max(0, s - 1));
|
|
228
|
-
} else if (key.name === "down" || key.name === "j") {
|
|
229
|
-
setSelectedModel((s) => Math.min(totalItems - 1, s + 1));
|
|
230
|
-
} else if (key.name === "return") {
|
|
231
|
-
if (selectedModel === models.length) {
|
|
232
|
-
// Selected "Custom model..."
|
|
233
|
-
setIsCustomModel(true);
|
|
234
|
-
setCustomModelInput("");
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
const m = models[selectedModel];
|
|
238
|
-
setChosenModelId(m.id);
|
|
239
|
-
if (chosenProviderId === "ollama") {
|
|
240
|
-
saveAndComplete(chosenProviderId, m.id, undefined);
|
|
241
|
-
} else if (authMethod === "oauth") {
|
|
242
|
-
exchangingRef.current = false;
|
|
243
|
-
setStep("oauth-paste");
|
|
244
|
-
} else {
|
|
245
|
-
setStep("apikey");
|
|
246
|
-
setApiKeyInput("");
|
|
247
|
-
setError("");
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// ── Step 4a: API key ──
|
|
254
|
-
if (step === "apikey") {
|
|
255
|
-
if (key.name === "return") {
|
|
256
|
-
if (!apiKeyInput.trim()) {
|
|
257
|
-
setError("API key cannot be empty");
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
saveAndComplete(chosenProviderId, chosenModelId, apiKeyInput.trim());
|
|
261
|
-
} else if ((key.ctrl || key.meta) && key.name === "v") {
|
|
262
|
-
const clip = readClipboard().trim();
|
|
263
|
-
if (clip) { setApiKeyInput((s) => s + clip); setError(""); }
|
|
264
|
-
} else if (key.ctrl && key.name === "s") {
|
|
265
|
-
setShowKey((s) => !s);
|
|
266
|
-
} else if (key.name === "backspace") {
|
|
267
|
-
setApiKeyInput((s) => s.slice(0, -1));
|
|
268
|
-
setError("");
|
|
269
|
-
} else if (key.ctrl && key.name === "u") {
|
|
270
|
-
setApiKeyInput("");
|
|
271
|
-
setError("");
|
|
272
|
-
} else if (key.sequence && !key.ctrl && !key.meta) {
|
|
273
|
-
setApiKeyInput((s) => s + key.sequence);
|
|
274
|
-
setError("");
|
|
275
|
-
}
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// ── Step 4b: OAuth code paste ──
|
|
280
|
-
if (step === "oauth-paste") {
|
|
281
|
-
if (key.name === "return") {
|
|
282
|
-
const code = oauthCodeInput.trim();
|
|
283
|
-
if (!code || exchangingRef.current) return;
|
|
284
|
-
exchangingRef.current = true;
|
|
285
|
-
setStep("oauth-exchanging");
|
|
286
|
-
try {
|
|
287
|
-
const { exchangeOAuthCode } = require("@cdoing/core");
|
|
288
|
-
exchangeOAuthCode(code, oauthVerifier, chosenProviderId)
|
|
289
|
-
.then((tokens: any) => {
|
|
290
|
-
saveAndComplete(chosenProviderId, chosenModelId, undefined);
|
|
291
|
-
props.onComplete({
|
|
292
|
-
provider: chosenProviderId,
|
|
293
|
-
model: chosenModelId,
|
|
294
|
-
oauthToken: tokens.access_token,
|
|
295
|
-
});
|
|
296
|
-
})
|
|
297
|
-
.catch((err: Error) => {
|
|
298
|
-
exchangingRef.current = false;
|
|
299
|
-
setOauthError(err.message);
|
|
300
|
-
setStep("oauth-paste");
|
|
301
|
-
});
|
|
302
|
-
} catch {
|
|
303
|
-
exchangingRef.current = false;
|
|
304
|
-
setOauthError("OAuth exchange failed");
|
|
305
|
-
setStep("oauth-paste");
|
|
306
|
-
}
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if ((key.ctrl || key.meta) && key.name === "v") {
|
|
310
|
-
const clip = readClipboard().trim();
|
|
311
|
-
if (clip) { setOauthCodeInput((s) => s + clip); setOauthError(""); }
|
|
312
|
-
} else if (key.ctrl && key.name === "s") {
|
|
313
|
-
setShowCode((s) => !s);
|
|
314
|
-
} else if (key.name === "backspace") {
|
|
315
|
-
setOauthCodeInput((s) => s.slice(0, -1));
|
|
316
|
-
setOauthError("");
|
|
317
|
-
} else if (key.ctrl && key.name === "u") {
|
|
318
|
-
setOauthCodeInput("");
|
|
319
|
-
setOauthError("");
|
|
320
|
-
} else if (key.sequence && !key.ctrl && !key.meta) {
|
|
321
|
-
setOauthCodeInput((s) => s + key.sequence);
|
|
322
|
-
setOauthError("");
|
|
323
|
-
}
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
const saveAndComplete = (prov: string, model: string, apiKey?: string) => {
|
|
329
|
-
const configDir = path.join(os.homedir(), ".cdoing");
|
|
330
|
-
const configPath = path.join(configDir, "config.json");
|
|
331
|
-
|
|
332
|
-
let config: Record<string, any> = {};
|
|
333
|
-
try {
|
|
334
|
-
if (fs.existsSync(configPath)) {
|
|
335
|
-
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
336
|
-
}
|
|
337
|
-
} catch {}
|
|
338
|
-
|
|
339
|
-
config.provider = prov;
|
|
340
|
-
config.model = model;
|
|
341
|
-
if (apiKey) {
|
|
342
|
-
if (!config.apiKeys) config.apiKeys = {};
|
|
343
|
-
config.apiKeys[prov] = apiKey;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
347
|
-
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
348
|
-
|
|
349
|
-
props.onComplete({ provider: prov, model, apiKey });
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
// Step numbers for display
|
|
353
|
-
const isAnthropic = chosenProviderId === "anthropic";
|
|
354
|
-
const total = isAnthropic ? 4 : chosenProviderId === "ollama" ? 2 : 3;
|
|
355
|
-
const stepLabel = (s: Step): string => {
|
|
356
|
-
switch (s) {
|
|
357
|
-
case "provider": return `1/${total}`;
|
|
358
|
-
case "auth-method": return "2/4";
|
|
359
|
-
case "model": return isAnthropic ? "3/4" : chosenProviderId === "ollama" ? "2/2" : "2/3";
|
|
360
|
-
case "apikey": return isAnthropic ? "4/4" : "3/3";
|
|
361
|
-
case "oauth-paste": return "4/4";
|
|
362
|
-
case "oauth-exchanging": return "4/4";
|
|
363
|
-
default: return "";
|
|
364
|
-
}
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
// Workaround: OpenTUI doesn't re-render parent on setDialog("none"),
|
|
368
|
-
// so we hide the wizard internally when closed
|
|
369
|
-
if (closed) return <box><text>{""}</text></box>;
|
|
370
|
-
|
|
371
|
-
return (
|
|
372
|
-
<box
|
|
373
|
-
borderStyle="single"
|
|
374
|
-
borderColor={t.primary}
|
|
375
|
-
backgroundColor={customBg || t.bg}
|
|
376
|
-
paddingX={2}
|
|
377
|
-
paddingY={1}
|
|
378
|
-
flexDirection="column"
|
|
379
|
-
flexGrow={1}
|
|
380
|
-
>
|
|
381
|
-
<text fg={t.primary} attributes={TextAttributes.BOLD}>
|
|
382
|
-
{"Setup Wizard"}
|
|
383
|
-
</text>
|
|
384
|
-
<text>{""}</text>
|
|
385
|
-
|
|
386
|
-
{/* Step 1: Provider */}
|
|
387
|
-
{step === "provider" && (
|
|
388
|
-
<box flexDirection="column">
|
|
389
|
-
<text fg={t.text} attributes={TextAttributes.BOLD}>
|
|
390
|
-
{`Step ${stepLabel("provider")}: Select Provider`}
|
|
391
|
-
</text>
|
|
392
|
-
<text>{""}</text>
|
|
393
|
-
{PROVIDERS.map((p: { id: string; name: string; hint: string }, i: number) => (
|
|
394
|
-
<text
|
|
395
|
-
key={p.id}
|
|
396
|
-
fg={selectedProvider === i ? t.primary : t.textMuted}
|
|
397
|
-
attributes={selectedProvider === i ? TextAttributes.BOLD : undefined}
|
|
398
|
-
>
|
|
399
|
-
{` ${selectedProvider === i ? ">" : " "} ${p.name} ${selectedProvider === i ? p.hint : ""}`}
|
|
400
|
-
</text>
|
|
401
|
-
))}
|
|
402
|
-
<text>{""}</text>
|
|
403
|
-
<text fg={t.textMuted}>{" Up/Down Navigate Enter Select Esc Cancel"}</text>
|
|
404
|
-
</box>
|
|
405
|
-
)}
|
|
406
|
-
|
|
407
|
-
{/* Step 2: Auth method (Anthropic only) */}
|
|
408
|
-
{step === "auth-method" && (
|
|
409
|
-
<box flexDirection="column">
|
|
410
|
-
<text fg={t.text} attributes={TextAttributes.BOLD}>
|
|
411
|
-
{`Step ${stepLabel("auth-method")}: How do you want to authenticate?`}
|
|
412
|
-
</text>
|
|
413
|
-
<text>{""}</text>
|
|
414
|
-
{AUTH_METHODS.map((a, i) => (
|
|
415
|
-
<text
|
|
416
|
-
key={a.id}
|
|
417
|
-
fg={selectedAuth === i ? t.primary : t.textMuted}
|
|
418
|
-
attributes={selectedAuth === i ? TextAttributes.BOLD : undefined}
|
|
419
|
-
>
|
|
420
|
-
{` ${selectedAuth === i ? ">" : " "} ${a.name} ${selectedAuth === i ? a.hint : ""}`}
|
|
421
|
-
</text>
|
|
422
|
-
))}
|
|
423
|
-
<text>{""}</text>
|
|
424
|
-
<text fg={t.textMuted}>{" Up/Down Navigate Enter Select Esc Back"}</text>
|
|
425
|
-
</box>
|
|
426
|
-
)}
|
|
427
|
-
|
|
428
|
-
{/* Step 3: Model */}
|
|
429
|
-
{step === "model" && !isCustomModel && (
|
|
430
|
-
<box flexDirection="column">
|
|
431
|
-
<text fg={t.text} attributes={TextAttributes.BOLD}>
|
|
432
|
-
{authMethod === "oauth"
|
|
433
|
-
? `Step ${stepLabel("model")}: Select Model (OAuth — ${provider.name})`
|
|
434
|
-
: `Step ${stepLabel("model")}: Select Model (${provider.name})`}
|
|
435
|
-
</text>
|
|
436
|
-
<text>{""}</text>
|
|
437
|
-
{models.map((m: { id: string; name: string; hint?: string }, i: number) => (
|
|
438
|
-
<text
|
|
439
|
-
key={m.id}
|
|
440
|
-
fg={selectedModel === i ? t.primary : t.textMuted}
|
|
441
|
-
attributes={selectedModel === i ? TextAttributes.BOLD : undefined}
|
|
442
|
-
>
|
|
443
|
-
{` ${selectedModel === i ? ">" : " "} ${m.name}${m.hint ? ` ${m.hint}` : ""}`}
|
|
444
|
-
</text>
|
|
445
|
-
))}
|
|
446
|
-
<text
|
|
447
|
-
fg={selectedModel === models.length ? t.primary : t.textMuted}
|
|
448
|
-
attributes={selectedModel === models.length ? TextAttributes.BOLD : undefined}
|
|
449
|
-
>
|
|
450
|
-
{` ${selectedModel === models.length ? ">" : " "} Custom model... type any model name`}
|
|
451
|
-
</text>
|
|
452
|
-
<text>{""}</text>
|
|
453
|
-
<text fg={t.textMuted}>{" Up/Down Navigate Enter Select Esc Back"}</text>
|
|
454
|
-
</box>
|
|
455
|
-
)}
|
|
456
|
-
|
|
457
|
-
{/* Step 3b: Custom model input */}
|
|
458
|
-
{step === "model" && isCustomModel && (
|
|
459
|
-
<box flexDirection="column">
|
|
460
|
-
<text fg={t.text} attributes={TextAttributes.BOLD}>
|
|
461
|
-
{`Step ${stepLabel("model")}: Custom Model (${provider.name})`}
|
|
462
|
-
</text>
|
|
463
|
-
<text>{""}</text>
|
|
464
|
-
<text fg={t.text}>
|
|
465
|
-
{` > ${customModelInput}|`}
|
|
466
|
-
</text>
|
|
467
|
-
<text>{""}</text>
|
|
468
|
-
<text fg={t.textMuted}>{" Type model ID then Enter Esc Back"}</text>
|
|
469
|
-
</box>
|
|
470
|
-
)}
|
|
471
|
-
|
|
472
|
-
{/* Step 4a: API key */}
|
|
473
|
-
{step === "apikey" && (
|
|
474
|
-
<box flexDirection="column">
|
|
475
|
-
<text fg={t.text} attributes={TextAttributes.BOLD}>
|
|
476
|
-
{`Step ${stepLabel("apikey")}: Enter API Key`}
|
|
477
|
-
</text>
|
|
478
|
-
<text>{""}</text>
|
|
479
|
-
<text fg={t.textDim}>
|
|
480
|
-
{` Provider: ${chosenProviderId} Model: ${chosenModelId}`}
|
|
481
|
-
</text>
|
|
482
|
-
{KEY_URLS[chosenProviderId] && (
|
|
483
|
-
<text fg={t.textDim}>
|
|
484
|
-
{` Get a key: ${KEY_URLS[chosenProviderId]}`}
|
|
485
|
-
</text>
|
|
486
|
-
)}
|
|
487
|
-
<text fg={t.textDim}>
|
|
488
|
-
{` Environment variable: ${ENV_VARS[chosenProviderId] || "N/A"}`}
|
|
489
|
-
</text>
|
|
490
|
-
<text>{""}</text>
|
|
491
|
-
<text fg={t.text}>
|
|
492
|
-
{` > ${showKey ? apiKeyInput : apiKeyInput.replace(/./g, "*")}|`}
|
|
493
|
-
</text>
|
|
494
|
-
{error && (
|
|
495
|
-
<text fg={t.error}>{`\n ${error}`}</text>
|
|
496
|
-
)}
|
|
497
|
-
<text>{""}</text>
|
|
498
|
-
<text fg={t.textMuted}>{" Enter Save Ctrl+V Paste Ctrl+S Toggle visibility Ctrl+U Clear Esc Back"}</text>
|
|
499
|
-
</box>
|
|
500
|
-
)}
|
|
501
|
-
|
|
502
|
-
{/* Step 4b: OAuth paste */}
|
|
503
|
-
{step === "oauth-paste" && (
|
|
504
|
-
<box flexDirection="column">
|
|
505
|
-
<text fg={t.text} attributes={TextAttributes.BOLD}>
|
|
506
|
-
{`Step ${stepLabel("oauth-paste")}: OAuth Login`}
|
|
507
|
-
</text>
|
|
508
|
-
<text>{""}</text>
|
|
509
|
-
<text fg={t.text}>{" 1. Browser opening to Claude login..."}</text>
|
|
510
|
-
{oauthUrl ? (
|
|
511
|
-
<text fg={t.textDim}>{` If it didn't open: ${oauthUrl.substring(0, 70)}...`}</text>
|
|
512
|
-
) : null}
|
|
513
|
-
<text fg={t.text}>{" 2. Approve -> you'll land on a page with a code in the URL"}</text>
|
|
514
|
-
<text fg={t.text}>{" 3. Copy the code= value from the URL and paste below"}</text>
|
|
515
|
-
{oauthError ? (
|
|
516
|
-
<>
|
|
517
|
-
<text>{""}</text>
|
|
518
|
-
<text fg={t.error}>{` Error: ${oauthError}`}</text>
|
|
519
|
-
</>
|
|
520
|
-
) : null}
|
|
521
|
-
<text>{""}</text>
|
|
522
|
-
<text fg={t.text}>
|
|
523
|
-
{` Code: ${showCode ? oauthCodeInput : oauthCodeInput.replace(/./g, "*")}|`}
|
|
524
|
-
</text>
|
|
525
|
-
<text>{""}</text>
|
|
526
|
-
<text fg={t.textMuted}>{" Ctrl+V Paste Enter Submit Ctrl+S Toggle visible Esc Cancel"}</text>
|
|
527
|
-
</box>
|
|
528
|
-
)}
|
|
529
|
-
|
|
530
|
-
{/* Step 4b: OAuth exchanging */}
|
|
531
|
-
{step === "oauth-exchanging" && (
|
|
532
|
-
<box flexDirection="column">
|
|
533
|
-
<text fg={t.text} attributes={TextAttributes.BOLD}>
|
|
534
|
-
{`Step ${stepLabel("oauth-exchanging")}: OAuth Login`}
|
|
535
|
-
</text>
|
|
536
|
-
<text>{""}</text>
|
|
537
|
-
<text fg={t.warning}>{" Exchanging code for tokens..."}</text>
|
|
538
|
-
</box>
|
|
539
|
-
)}
|
|
540
|
-
</box>
|
|
541
|
-
);
|
|
542
|
-
}
|