@cdoing/opentuicli 0.1.6 → 0.1.19
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/dist/index.js +53 -38
- package/dist/index.js.map +4 -4
- package/package.json +5 -4
- package/src/app.tsx +260 -39
- package/src/components/dialog-command.tsx +110 -107
- package/src/components/dialog-help.tsx +48 -124
- package/src/components/dialog-model.tsx +98 -49
- package/src/components/dialog-status.tsx +46 -84
- package/src/components/dialog-theme.tsx +197 -171
- package/src/components/input-area.tsx +74 -12
- package/src/components/message-list.tsx +250 -42
- package/src/components/permission-prompt.tsx +2 -1
- package/src/components/session-browser.tsx +71 -60
- package/src/components/session-footer.tsx +2 -2
- package/src/components/session-header.tsx +1 -1
- package/src/components/setup-wizard.tsx +149 -70
- package/src/components/sidebar.tsx +66 -13
- package/src/components/status-bar.tsx +2 -2
- package/src/context/theme.tsx +109 -1
- package/src/lib/autocomplete.ts +5 -1
- package/src/routes/home.tsx +2 -2
- package/src/routes/session.tsx +141 -18
- package/src/store/settings.ts +107 -0
|
@@ -16,64 +16,68 @@ import { TextAttributes } from "@opentui/core";
|
|
|
16
16
|
import { useState, useRef, useEffect } from "react";
|
|
17
17
|
import { useKeyboard } from "@opentui/react";
|
|
18
18
|
import { useTheme } from "../context/theme";
|
|
19
|
+
import { getProviders } from "@cdoing/ai";
|
|
20
|
+
import { getOAuthProvider, supportsOAuth } from "@cdoing/core";
|
|
19
21
|
|
|
20
22
|
export interface SetupWizardProps {
|
|
21
23
|
onComplete: (config: { provider: string; model: string; apiKey?: string; oauthToken?: string }) => void;
|
|
22
24
|
onClose: () => void;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
}));
|
|
31
36
|
|
|
32
37
|
const AUTH_METHODS = [
|
|
33
38
|
{ id: "apikey", name: "API Key", hint: "all models available - console.anthropic.com" },
|
|
34
39
|
{ id: "oauth", name: "OAuth", hint: "Claude Pro/Max - opens browser" },
|
|
35
40
|
];
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
],
|
|
43
|
-
openai: [
|
|
44
|
-
{ id: "gpt-4o", name: "GPT-4o", hint: "recommended" },
|
|
45
|
-
{ id: "gpt-4o-mini", name: "GPT-4o mini", hint: "fastest" },
|
|
46
|
-
{ id: "o3", name: "o3", hint: "reasoning" },
|
|
47
|
-
],
|
|
48
|
-
google: [
|
|
49
|
-
{ id: "gemini-2.0-flash", name: "Gemini 2.0 Flash", hint: "recommended" },
|
|
50
|
-
{ id: "gemini-2.5-pro", name: "Gemini 2.5 Pro", hint: "most capable" },
|
|
51
|
-
],
|
|
52
|
-
ollama: [
|
|
53
|
-
{ id: "llama3.1", name: "Llama 3.1", hint: "general purpose" },
|
|
54
|
-
{ id: "codellama", name: "Code Llama", hint: "code-focused" },
|
|
55
|
-
{ id: "mistral", name: "Mistral", hint: "fast & capable" },
|
|
56
|
-
],
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const OAUTH_MODELS = [
|
|
60
|
-
{ id: "claude-haiku-4-5", name: "Claude Haiku 4.5", hint: "only model supported with OAuth" },
|
|
61
|
-
];
|
|
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
|
+
}
|
|
62
47
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
}
|
|
68
58
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|
|
74
70
|
|
|
75
71
|
type Step = "provider" | "auth-method" | "model" | "apikey" | "oauth-paste" | "oauth-exchanging";
|
|
76
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
|
+
|
|
77
81
|
function openBrowser(url: string): void {
|
|
78
82
|
try {
|
|
79
83
|
const cmd = process.platform === "darwin" ? `open "${url}"`
|
|
@@ -84,9 +88,11 @@ function openBrowser(url: string): void {
|
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
export function SetupWizard(props: SetupWizardProps) {
|
|
87
|
-
const { theme } = useTheme();
|
|
91
|
+
const { theme, customBg } = useTheme();
|
|
88
92
|
const t = theme;
|
|
89
93
|
|
|
94
|
+
const closedRef = useRef(false);
|
|
95
|
+
const [closed, setClosed] = useState(false);
|
|
90
96
|
const [step, setStep] = useState<Step>("provider");
|
|
91
97
|
const [selectedProvider, setSelectedProvider] = useState(0);
|
|
92
98
|
const [selectedAuth, setSelectedAuth] = useState(0);
|
|
@@ -97,6 +103,9 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
97
103
|
const [apiKeyInput, setApiKeyInput] = useState("");
|
|
98
104
|
const [showKey, setShowKey] = useState(false);
|
|
99
105
|
const [error, setError] = useState("");
|
|
106
|
+
// Custom model input
|
|
107
|
+
const [customModelInput, setCustomModelInput] = useState("");
|
|
108
|
+
const [isCustomModel, setIsCustomModel] = useState(false);
|
|
100
109
|
|
|
101
110
|
// OAuth state
|
|
102
111
|
const [oauthUrl, setOauthUrl] = useState("");
|
|
@@ -107,7 +116,7 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
107
116
|
const exchangingRef = useRef(false);
|
|
108
117
|
|
|
109
118
|
const provider = PROVIDERS[selectedProvider];
|
|
110
|
-
const models = authMethod === "oauth" ?
|
|
119
|
+
const models = authMethod === "oauth" ? getOAuthModels(chosenProviderId) : (MODELS[chosenProviderId] || []);
|
|
111
120
|
|
|
112
121
|
// Generate OAuth URL when entering oauth-paste step
|
|
113
122
|
useEffect(() => {
|
|
@@ -126,21 +135,22 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
126
135
|
}, [step, chosenProviderId]);
|
|
127
136
|
|
|
128
137
|
useKeyboard((key: any) => {
|
|
129
|
-
//
|
|
138
|
+
// If already closed, ignore keys
|
|
139
|
+
if (closedRef.current) return;
|
|
140
|
+
|
|
141
|
+
// Escape — close wizard (parent handles navigation to home)
|
|
130
142
|
if (key.name === "escape") {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
props.onClose();
|
|
143
|
-
}
|
|
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();
|
|
144
154
|
return;
|
|
145
155
|
}
|
|
146
156
|
|
|
@@ -154,7 +164,7 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
154
164
|
const p = PROVIDERS[selectedProvider];
|
|
155
165
|
setChosenProviderId(p.id);
|
|
156
166
|
setSelectedModel(0);
|
|
157
|
-
if (p.id
|
|
167
|
+
if (providerSupportsOAuth(p.id)) {
|
|
158
168
|
setSelectedAuth(0);
|
|
159
169
|
setStep("auth-method");
|
|
160
170
|
} else {
|
|
@@ -165,7 +175,7 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
165
175
|
return;
|
|
166
176
|
}
|
|
167
177
|
|
|
168
|
-
// ── Step 2: Auth method (
|
|
178
|
+
// ── Step 2: Auth method (any OAuth-capable provider) ──
|
|
169
179
|
if (step === "auth-method") {
|
|
170
180
|
if (key.name === "up" || key.name === "k") {
|
|
171
181
|
setSelectedAuth((s) => Math.max(0, s - 1));
|
|
@@ -180,13 +190,50 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
180
190
|
return;
|
|
181
191
|
}
|
|
182
192
|
|
|
183
|
-
// ── Step 3: Model ──
|
|
193
|
+
// ── Step 3: Model (presets + custom input) ──
|
|
184
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;
|
|
185
226
|
if (key.name === "up" || key.name === "k") {
|
|
186
227
|
setSelectedModel((s) => Math.max(0, s - 1));
|
|
187
228
|
} else if (key.name === "down" || key.name === "j") {
|
|
188
|
-
setSelectedModel((s) => Math.min(
|
|
229
|
+
setSelectedModel((s) => Math.min(totalItems - 1, s + 1));
|
|
189
230
|
} else if (key.name === "return") {
|
|
231
|
+
if (selectedModel === models.length) {
|
|
232
|
+
// Selected "Custom model..."
|
|
233
|
+
setIsCustomModel(true);
|
|
234
|
+
setCustomModelInput("");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
190
237
|
const m = models[selectedModel];
|
|
191
238
|
setChosenModelId(m.id);
|
|
192
239
|
if (chosenProviderId === "ollama") {
|
|
@@ -211,6 +258,9 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
211
258
|
return;
|
|
212
259
|
}
|
|
213
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(""); }
|
|
214
264
|
} else if (key.ctrl && key.name === "s") {
|
|
215
265
|
setShowKey((s) => !s);
|
|
216
266
|
} else if (key.name === "backspace") {
|
|
@@ -219,7 +269,7 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
219
269
|
} else if (key.ctrl && key.name === "u") {
|
|
220
270
|
setApiKeyInput("");
|
|
221
271
|
setError("");
|
|
222
|
-
} else if (key.sequence &&
|
|
272
|
+
} else if (key.sequence && !key.ctrl && !key.meta) {
|
|
223
273
|
setApiKeyInput((s) => s + key.sequence);
|
|
224
274
|
setError("");
|
|
225
275
|
}
|
|
@@ -256,7 +306,10 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
256
306
|
}
|
|
257
307
|
return;
|
|
258
308
|
}
|
|
259
|
-
if (key.ctrl && key.name === "
|
|
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") {
|
|
260
313
|
setShowCode((s) => !s);
|
|
261
314
|
} else if (key.name === "backspace") {
|
|
262
315
|
setOauthCodeInput((s) => s.slice(0, -1));
|
|
@@ -264,7 +317,7 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
264
317
|
} else if (key.ctrl && key.name === "u") {
|
|
265
318
|
setOauthCodeInput("");
|
|
266
319
|
setOauthError("");
|
|
267
|
-
} else if (key.sequence &&
|
|
320
|
+
} else if (key.sequence && !key.ctrl && !key.meta) {
|
|
268
321
|
setOauthCodeInput((s) => s + key.sequence);
|
|
269
322
|
setOauthError("");
|
|
270
323
|
}
|
|
@@ -311,10 +364,15 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
311
364
|
}
|
|
312
365
|
};
|
|
313
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
|
+
|
|
314
371
|
return (
|
|
315
372
|
<box
|
|
316
373
|
borderStyle="single"
|
|
317
374
|
borderColor={t.primary}
|
|
375
|
+
backgroundColor={customBg || t.bg}
|
|
318
376
|
paddingX={2}
|
|
319
377
|
paddingY={1}
|
|
320
378
|
flexDirection="column"
|
|
@@ -332,7 +390,7 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
332
390
|
{`Step ${stepLabel("provider")}: Select Provider`}
|
|
333
391
|
</text>
|
|
334
392
|
<text>{""}</text>
|
|
335
|
-
{PROVIDERS.map((p, i) => (
|
|
393
|
+
{PROVIDERS.map((p: { id: string; name: string; hint: string }, i: number) => (
|
|
336
394
|
<text
|
|
337
395
|
key={p.id}
|
|
338
396
|
fg={selectedProvider === i ? t.primary : t.textMuted}
|
|
@@ -368,15 +426,15 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
368
426
|
)}
|
|
369
427
|
|
|
370
428
|
{/* Step 3: Model */}
|
|
371
|
-
{step === "model" && (
|
|
429
|
+
{step === "model" && !isCustomModel && (
|
|
372
430
|
<box flexDirection="column">
|
|
373
431
|
<text fg={t.text} attributes={TextAttributes.BOLD}>
|
|
374
432
|
{authMethod === "oauth"
|
|
375
|
-
? `Step ${stepLabel("model")}: Select Model (OAuth
|
|
433
|
+
? `Step ${stepLabel("model")}: Select Model (OAuth — ${provider.name})`
|
|
376
434
|
: `Step ${stepLabel("model")}: Select Model (${provider.name})`}
|
|
377
435
|
</text>
|
|
378
436
|
<text>{""}</text>
|
|
379
|
-
{models.map((m, i) => (
|
|
437
|
+
{models.map((m: { id: string; name: string; hint?: string }, i: number) => (
|
|
380
438
|
<text
|
|
381
439
|
key={m.id}
|
|
382
440
|
fg={selectedModel === i ? t.primary : t.textMuted}
|
|
@@ -385,11 +443,32 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
385
443
|
{` ${selectedModel === i ? ">" : " "} ${m.name}${m.hint ? ` ${m.hint}` : ""}`}
|
|
386
444
|
</text>
|
|
387
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>
|
|
388
452
|
<text>{""}</text>
|
|
389
453
|
<text fg={t.textMuted}>{" Up/Down Navigate Enter Select Esc Back"}</text>
|
|
390
454
|
</box>
|
|
391
455
|
)}
|
|
392
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
|
+
|
|
393
472
|
{/* Step 4a: API key */}
|
|
394
473
|
{step === "apikey" && (
|
|
395
474
|
<box flexDirection="column">
|
|
@@ -416,7 +495,7 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
416
495
|
<text fg={t.error}>{`\n ${error}`}</text>
|
|
417
496
|
)}
|
|
418
497
|
<text>{""}</text>
|
|
419
|
-
<text fg={t.textMuted}>{" Enter Save Ctrl+S Toggle visibility Ctrl+U Clear Esc Back"}</text>
|
|
498
|
+
<text fg={t.textMuted}>{" Enter Save Ctrl+V Paste Ctrl+S Toggle visibility Ctrl+U Clear Esc Back"}</text>
|
|
420
499
|
</box>
|
|
421
500
|
)}
|
|
422
501
|
|
|
@@ -444,7 +523,7 @@ export function SetupWizard(props: SetupWizardProps) {
|
|
|
444
523
|
{` Code: ${showCode ? oauthCodeInput : oauthCodeInput.replace(/./g, "*")}|`}
|
|
445
524
|
</text>
|
|
446
525
|
<text>{""}</text>
|
|
447
|
-
<text fg={t.textMuted}>{" Paste
|
|
526
|
+
<text fg={t.textMuted}>{" Ctrl+V Paste Enter Submit Ctrl+S Toggle visible Esc Cancel"}</text>
|
|
448
527
|
</box>
|
|
449
528
|
)}
|
|
450
529
|
|
|
@@ -98,32 +98,85 @@ export function Sidebar(props: SidebarProps) {
|
|
|
98
98
|
sep();
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// ── LSP ──
|
|
102
|
+
header("LSP");
|
|
103
|
+
row("", "LSPs will activate as files are read");
|
|
104
|
+
sep();
|
|
105
|
+
|
|
101
106
|
// ── Shortcuts ──
|
|
102
107
|
header("Shortcuts");
|
|
103
|
-
shortcut("Ctrl+
|
|
104
|
-
shortcut("Ctrl+
|
|
105
|
-
shortcut("Ctrl+P", "Model");
|
|
108
|
+
shortcut("Ctrl+P", "Commands");
|
|
109
|
+
shortcut("Ctrl+O", "Model");
|
|
106
110
|
shortcut("Ctrl+T", "Theme");
|
|
111
|
+
shortcut("Ctrl+N", "New session");
|
|
107
112
|
shortcut("Ctrl+S", "Sessions");
|
|
108
|
-
shortcut("Ctrl+
|
|
113
|
+
shortcut("Ctrl+B", "Sidebar");
|
|
109
114
|
shortcut("F1", "Help");
|
|
110
115
|
|
|
116
|
+
// ── Getting started card ──
|
|
117
|
+
const cardW = W - 2;
|
|
118
|
+
const cardLines: Array<{ text: string; fg: any; bold?: boolean }> = [];
|
|
119
|
+
cardLines.push({ text: ` ◆ Getting started`, fg: t.primary, bold: true });
|
|
120
|
+
cardLines.push({ text: ``, fg: t.textDim });
|
|
121
|
+
cardLines.push({ text: ` Cdoing includes free models`, fg: t.textMuted });
|
|
122
|
+
cardLines.push({ text: ` so you can start immediately.`, fg: t.textMuted });
|
|
123
|
+
cardLines.push({ text: ``, fg: t.textDim });
|
|
124
|
+
cardLines.push({ text: ` Connect from 75+ providers to`, fg: t.textMuted });
|
|
125
|
+
cardLines.push({ text: ` use other models, including`, fg: t.textMuted });
|
|
126
|
+
cardLines.push({ text: ` Claude, GPT, Gemini etc`, fg: t.textMuted });
|
|
127
|
+
cardLines.push({ text: ``, fg: t.textDim });
|
|
128
|
+
cardLines.push({ text: ` Connect provider /setup`, fg: t.text, bold: true });
|
|
129
|
+
|
|
111
130
|
return (
|
|
112
|
-
<box width={W + 2} flexDirection="column">
|
|
131
|
+
<box width={W + 2} flexDirection="column" backgroundColor={t.bgSubtle}>
|
|
113
132
|
<box flexDirection="column" flexGrow={1}>
|
|
114
133
|
{lines.map((line, i) => (
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
134
|
+
<box key={i} height={1}>
|
|
135
|
+
<text
|
|
136
|
+
fg={line.fg}
|
|
137
|
+
attributes={line.bold ? TextAttributes.BOLD : undefined}
|
|
138
|
+
>
|
|
139
|
+
{line.text}
|
|
140
|
+
</text>
|
|
141
|
+
</box>
|
|
122
142
|
))}
|
|
123
|
-
{/* Fill remaining space
|
|
143
|
+
{/* Fill remaining space */}
|
|
124
144
|
<box flexGrow={1}>
|
|
125
145
|
<text fg={t.border}>{"│"}</text>
|
|
126
146
|
</box>
|
|
147
|
+
{/* Getting started card at bottom */}
|
|
148
|
+
<box flexDirection="column" paddingX={1}>
|
|
149
|
+
<box height={1}>
|
|
150
|
+
<text fg={t.border}>{"┌" + "─".repeat(cardW) + "┐"}</text>
|
|
151
|
+
</box>
|
|
152
|
+
{cardLines.map((cl, i) => (
|
|
153
|
+
<box key={`card-${i}`} height={1}>
|
|
154
|
+
<text fg={t.border}>{"│"}</text>
|
|
155
|
+
<text fg={cl.fg} attributes={cl.bold ? TextAttributes.BOLD : undefined}>
|
|
156
|
+
{cl.text.padEnd(cardW)}
|
|
157
|
+
</text>
|
|
158
|
+
<text fg={t.border}>{"│"}</text>
|
|
159
|
+
</box>
|
|
160
|
+
))}
|
|
161
|
+
<box height={1}>
|
|
162
|
+
<text fg={t.border}>{"└" + "─".repeat(cardW) + "┘"}</text>
|
|
163
|
+
</box>
|
|
164
|
+
</box>
|
|
165
|
+
{/* Working dir + version footer */}
|
|
166
|
+
<box height={1} paddingX={1}>
|
|
167
|
+
<text fg={t.textDim}>
|
|
168
|
+
{trunc((() => {
|
|
169
|
+
const home = process.env.HOME || "";
|
|
170
|
+
return home && props.workingDir.startsWith(home)
|
|
171
|
+
? "~" + props.workingDir.slice(home.length) : props.workingDir;
|
|
172
|
+
})(), W - 2)}
|
|
173
|
+
</text>
|
|
174
|
+
</box>
|
|
175
|
+
<box height={1} paddingX={1}>
|
|
176
|
+
<text fg={t.success}>{"● "}</text>
|
|
177
|
+
<text fg={t.text} attributes={TextAttributes.BOLD}>{"Cdoing"}</text>
|
|
178
|
+
<text fg={t.textDim}>{" Agent"}</text>
|
|
179
|
+
</box>
|
|
127
180
|
</box>
|
|
128
181
|
</box>
|
|
129
182
|
);
|
|
@@ -33,7 +33,7 @@ export function StatusBar(props: StatusBarProps) {
|
|
|
33
33
|
const contextBar = pct > 0 ? ` ctx:${pct}%` : "";
|
|
34
34
|
|
|
35
35
|
return (
|
|
36
|
-
<box height={1} flexDirection="row" justifyContent="space-between">
|
|
36
|
+
<box height={1} flexDirection="row" justifyContent="space-between" backgroundColor={t.bgSubtle}>
|
|
37
37
|
<box flexDirection="row">
|
|
38
38
|
<text fg={t.primary} attributes={TextAttributes.BOLD}>
|
|
39
39
|
{` ${props.provider}`}
|
|
@@ -69,7 +69,7 @@ export function StatusBar(props: StatusBarProps) {
|
|
|
69
69
|
<box flexDirection="row">
|
|
70
70
|
<text fg={t.textDim}>{shortDir}</text>
|
|
71
71
|
<text fg={t.textDim}>{" │ "}</text>
|
|
72
|
-
<text fg={t.textMuted}>{"^
|
|
72
|
+
<text fg={t.textMuted}>{"^P:Commands ^O:Model ^C:Quit"}</text>
|
|
73
73
|
</box>
|
|
74
74
|
</box>
|
|
75
75
|
);
|
package/src/context/theme.tsx
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { RGBA, rgbToHex, SyntaxStyle } from "@opentui/core";
|
|
14
14
|
import { createContext, useContext, useState, useEffect, useRef, useMemo } from "react";
|
|
15
15
|
import type { ReactNode } from "react";
|
|
16
|
+
import { useSettingsStore } from "../store/settings";
|
|
16
17
|
|
|
17
18
|
export interface Theme {
|
|
18
19
|
text: RGBA;
|
|
@@ -274,6 +275,110 @@ export const THEMES: Record<string, ThemeDef> = {
|
|
|
274
275
|
warning: "#b88d00", info: "#0090a0", border: "#d0d0d0",
|
|
275
276
|
}),
|
|
276
277
|
},
|
|
278
|
+
kanagawa: {
|
|
279
|
+
name: "Kanagawa",
|
|
280
|
+
dark: buildTheme({
|
|
281
|
+
bg: "#1F1F28", bgSubtle: "#2A2A37", text: "#DCD7BA", textMuted: "#727169", textDim: "#54546D",
|
|
282
|
+
primary: "#7E9CD8", secondary: "#957FB8", success: "#98BB6C", error: "#E82424",
|
|
283
|
+
warning: "#D7A657", info: "#76946A", border: "#54546D",
|
|
284
|
+
}),
|
|
285
|
+
light: buildTheme({
|
|
286
|
+
bg: "#F2E9DE", bgSubtle: "#EAE4D7", text: "#54433A", textMuted: "#9E9389", textDim: "#B8AFA6",
|
|
287
|
+
primary: "#2D4F67", secondary: "#957FB8", success: "#98BB6C", error: "#E82424",
|
|
288
|
+
warning: "#D7A657", info: "#76946A", border: "#D4CBBF",
|
|
289
|
+
}),
|
|
290
|
+
},
|
|
291
|
+
nightowl: {
|
|
292
|
+
name: "Night Owl",
|
|
293
|
+
dark: buildTheme({
|
|
294
|
+
bg: "#011627", bgSubtle: "#0b2942", text: "#d6deeb", textMuted: "#637777", textDim: "#44596b",
|
|
295
|
+
primary: "#82aaff", secondary: "#c792ea", success: "#addb67", error: "#ef5350",
|
|
296
|
+
warning: "#ffcb8b", info: "#7fdbca", border: "#1d3b53",
|
|
297
|
+
}),
|
|
298
|
+
light: buildTheme({
|
|
299
|
+
bg: "#fbfbfb", bgSubtle: "#f0f0f0", text: "#403f53", textMuted: "#989fb1", textDim: "#c0c0c0",
|
|
300
|
+
primary: "#4876d6", secondary: "#994cc3", success: "#4d804e", error: "#de3d3b",
|
|
301
|
+
warning: "#e0af68", info: "#0c969b", border: "#d9d9d9",
|
|
302
|
+
}),
|
|
303
|
+
},
|
|
304
|
+
onedark: {
|
|
305
|
+
name: "One Dark",
|
|
306
|
+
dark: buildTheme({
|
|
307
|
+
bg: "#282c34", bgSubtle: "#2c313c", text: "#abb2bf", textMuted: "#636d83", textDim: "#4b5263",
|
|
308
|
+
primary: "#61afef", secondary: "#c678dd", success: "#98c379", error: "#e06c75",
|
|
309
|
+
warning: "#e5c07b", info: "#56b6c2", border: "#3e4451",
|
|
310
|
+
}),
|
|
311
|
+
light: buildTheme({
|
|
312
|
+
bg: "#fafafa", bgSubtle: "#f0f0f0", text: "#383a42", textMuted: "#a0a1a7", textDim: "#c0c0c0",
|
|
313
|
+
primary: "#4078f2", secondary: "#a626a4", success: "#50a14f", error: "#e45649",
|
|
314
|
+
warning: "#c18401", info: "#0184bc", border: "#d3d3d3",
|
|
315
|
+
}),
|
|
316
|
+
},
|
|
317
|
+
matrix: {
|
|
318
|
+
name: "Matrix",
|
|
319
|
+
dark: buildTheme({
|
|
320
|
+
bg: "#0a0e0a", bgSubtle: "#0e130d", text: "#62ff94", textMuted: "#8ca391", textDim: "#3d4a44",
|
|
321
|
+
primary: "#2eff6a", secondary: "#00efff", success: "#62ff94", error: "#ff4b4b",
|
|
322
|
+
warning: "#e6ff57", info: "#30b3ff", border: "#1e2a1b",
|
|
323
|
+
}),
|
|
324
|
+
light: buildTheme({
|
|
325
|
+
bg: "#eef3ea", bgSubtle: "#e4ebe1", text: "#203022", textMuted: "#748476", textDim: "#a0b0a5",
|
|
326
|
+
primary: "#1cc24b", secondary: "#24f6d9", success: "#1cc24b", error: "#ff4b4b",
|
|
327
|
+
warning: "#e6ff57", info: "#30b3ff", border: "#748476",
|
|
328
|
+
}),
|
|
329
|
+
},
|
|
330
|
+
flexoki: {
|
|
331
|
+
name: "Flexoki",
|
|
332
|
+
dark: buildTheme({
|
|
333
|
+
bg: "#100f0f", bgSubtle: "#1c1b1a", text: "#cecdc3", textMuted: "#878580", textDim: "#575653",
|
|
334
|
+
primary: "#4385be", secondary: "#8b7ec8", success: "#879a39", error: "#d14d41",
|
|
335
|
+
warning: "#da702c", info: "#3aa99f", border: "#343331",
|
|
336
|
+
}),
|
|
337
|
+
light: buildTheme({
|
|
338
|
+
bg: "#fffcf0", bgSubtle: "#f2f0e5", text: "#100f0f", textMuted: "#878580", textDim: "#b7b5ac",
|
|
339
|
+
primary: "#205ea6", secondary: "#5e409d", success: "#66800b", error: "#af3029",
|
|
340
|
+
warning: "#bc5215", info: "#24837b", border: "#e6e4d9",
|
|
341
|
+
}),
|
|
342
|
+
},
|
|
343
|
+
cursor: {
|
|
344
|
+
name: "Cursor",
|
|
345
|
+
dark: buildTheme({
|
|
346
|
+
bg: "#1e1e1e", bgSubtle: "#252526", text: "#d4d4d4", textMuted: "#808080", textDim: "#5a5a5a",
|
|
347
|
+
primary: "#007acc", secondary: "#c586c0", success: "#6a9955", error: "#f44747",
|
|
348
|
+
warning: "#cca700", info: "#4ec9b0", border: "#333333",
|
|
349
|
+
}),
|
|
350
|
+
light: buildTheme({
|
|
351
|
+
bg: "#ffffff", bgSubtle: "#f3f3f3", text: "#1e1e1e", textMuted: "#808080", textDim: "#b0b0b0",
|
|
352
|
+
primary: "#007acc", secondary: "#af00db", success: "#388a34", error: "#cd3131",
|
|
353
|
+
warning: "#bf8803", info: "#16825d", border: "#e5e5e5",
|
|
354
|
+
}),
|
|
355
|
+
},
|
|
356
|
+
vercel: {
|
|
357
|
+
name: "Vercel",
|
|
358
|
+
dark: buildTheme({
|
|
359
|
+
bg: "#000000", bgSubtle: "#111111", text: "#ededed", textMuted: "#888888", textDim: "#444444",
|
|
360
|
+
primary: "#ffffff", secondary: "#888888", success: "#0070f3", error: "#ee0000",
|
|
361
|
+
warning: "#f5a623", info: "#0070f3", border: "#333333",
|
|
362
|
+
}),
|
|
363
|
+
light: buildTheme({
|
|
364
|
+
bg: "#ffffff", bgSubtle: "#fafafa", text: "#000000", textMuted: "#666666", textDim: "#999999",
|
|
365
|
+
primary: "#000000", secondary: "#666666", success: "#0070f3", error: "#ee0000",
|
|
366
|
+
warning: "#f5a623", info: "#0070f3", border: "#eaeaea",
|
|
367
|
+
}),
|
|
368
|
+
},
|
|
369
|
+
"osaka-jade": {
|
|
370
|
+
name: "Osaka Jade",
|
|
371
|
+
dark: buildTheme({
|
|
372
|
+
bg: "#111c18", bgSubtle: "#1a2520", text: "#C1C497", textMuted: "#53685B", textDim: "#3d4a44",
|
|
373
|
+
primary: "#2DD5B7", secondary: "#D2689C", success: "#549e6a", error: "#FF5345",
|
|
374
|
+
warning: "#E5C736", info: "#2DD5B7", border: "#3d4a44",
|
|
375
|
+
}),
|
|
376
|
+
light: buildTheme({
|
|
377
|
+
bg: "#F6F5DD", bgSubtle: "#E8E7CC", text: "#111c18", textMuted: "#53685B", textDim: "#A8A78C",
|
|
378
|
+
primary: "#1faa90", secondary: "#a8527a", success: "#3d7a52", error: "#c7392d",
|
|
379
|
+
warning: "#b5a020", info: "#1faa90", border: "#A8A78C",
|
|
380
|
+
}),
|
|
381
|
+
},
|
|
277
382
|
};
|
|
278
383
|
|
|
279
384
|
/** Get sorted list of theme IDs */
|
|
@@ -439,7 +544,7 @@ export function ThemeProvider(props: {
|
|
|
439
544
|
: props.mode === "auto" ? (props.detectedMode || "dark")
|
|
440
545
|
: "dark";
|
|
441
546
|
|
|
442
|
-
const initialThemeId = props.themeId || "
|
|
547
|
+
const initialThemeId = props.themeId || "vercel";
|
|
443
548
|
|
|
444
549
|
const [themeId, setThemeIdState] = useState(initialThemeId);
|
|
445
550
|
const [currentMode, setCurrentMode] = useState<"dark" | "light">(initialMode);
|
|
@@ -473,6 +578,7 @@ export function ThemeProvider(props: {
|
|
|
473
578
|
const colors = getThemeColors(id, modeRef.current);
|
|
474
579
|
setTheme(colors);
|
|
475
580
|
applyTerminalBg(colors);
|
|
581
|
+
useSettingsStore.getState().setThemeId(id);
|
|
476
582
|
};
|
|
477
583
|
|
|
478
584
|
const setMode = (m: "dark" | "light") => {
|
|
@@ -481,6 +587,7 @@ export function ThemeProvider(props: {
|
|
|
481
587
|
const colors = getThemeColors(themeIdRef.current, m);
|
|
482
588
|
setTheme(colors);
|
|
483
589
|
applyTerminalBg(colors);
|
|
590
|
+
useSettingsStore.getState().setMode(m);
|
|
484
591
|
};
|
|
485
592
|
|
|
486
593
|
const setCustomBg = (hex: string | null) => {
|
|
@@ -503,6 +610,7 @@ export function ThemeProvider(props: {
|
|
|
503
610
|
} else {
|
|
504
611
|
restoreTerminalBackground();
|
|
505
612
|
}
|
|
613
|
+
useSettingsStore.getState().setSyncTerminalBg(sync);
|
|
506
614
|
};
|
|
507
615
|
|
|
508
616
|
// Sync terminal bg on mount — force it immediately
|
package/src/lib/autocomplete.ts
CHANGED
|
@@ -31,7 +31,11 @@ export const SLASH_COMMANDS: SlashCommand[] = [
|
|
|
31
31
|
{ name: "/view", description: "View a conversation" },
|
|
32
32
|
{ name: "/fork", description: "Fork current conversation" },
|
|
33
33
|
{ name: "/delete", description: "Delete a conversation" },
|
|
34
|
-
{ name: "/plan", description: "
|
|
34
|
+
{ name: "/plan", description: "Plan mode (approve/reject/show/off)" },
|
|
35
|
+
{ name: "/plan approve", description: "Approve plan and start building" },
|
|
36
|
+
{ name: "/plan reject", description: "Reject plan" },
|
|
37
|
+
{ name: "/plan show", description: "Show current plan" },
|
|
38
|
+
{ name: "/plan off", description: "Cancel plan mode" },
|
|
35
39
|
{ name: "/tasks", description: "Show active tasks" },
|
|
36
40
|
{ name: "/memory", description: "Show agent memory" },
|
|
37
41
|
{ name: "/permissions", description: "Show permission rules" },
|