@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.
@@ -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
- const PROVIDERS = [
26
- { id: "anthropic", name: "Anthropic (Claude)", hint: "Claude Sonnet, Opus, Haiku" },
27
- { id: "openai", name: "OpenAI", hint: "GPT-4o, o3" },
28
- { id: "google", name: "Google AI", hint: "Gemini 2.0 Flash, 2.5 Pro" },
29
- { id: "ollama", name: "Ollama (Local)", hint: "No API key needed" },
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
- const MODELS: Record<string, { id: string; name: string; hint?: string }[]> = {
38
- anthropic: [
39
- { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", hint: "recommended - fast & smart" },
40
- { id: "claude-opus-4-6", name: "Claude Opus 4.6", hint: "most capable" },
41
- { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", hint: "fastest" },
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
- const ENV_VARS: Record<string, string> = {
64
- anthropic: "ANTHROPIC_API_KEY",
65
- openai: "OPENAI_API_KEY",
66
- google: "GOOGLE_API_KEY",
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
- const KEY_URLS: Record<string, string> = {
70
- anthropic: "https://console.anthropic.com/settings/keys",
71
- openai: "https://platform.openai.com/api-keys",
72
- google: "https://aistudio.google.com/apikey",
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" ? OAUTH_MODELS : (MODELS[chosenProviderId] || []);
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
- // Escape go back or close
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
- if (step === "oauth-paste" || step === "oauth-exchanging") {
132
- setStep("model");
133
- setOauthError("");
134
- } else if (step === "apikey") {
135
- setStep("model");
136
- setError("");
137
- } else if (step === "model") {
138
- setStep(chosenProviderId === "anthropic" ? "auth-method" : "provider");
139
- } else if (step === "auth-method") {
140
- setStep("provider");
141
- } else {
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 === "anthropic") {
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 (Anthropic only) ──
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(models.length - 1, s + 1));
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 && key.sequence.length === 1 && !key.ctrl && !key.meta) {
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 === "s") {
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 && key.sequence.length === 1 && !key.ctrl && !key.meta) {
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 supports Haiku only)`
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 code then Enter Ctrl+S Toggle visible Esc Cancel"}</text>
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+B", "Sidebar");
104
- shortcut("Ctrl+N", "New session");
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+X", "Commands");
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
- <text
116
- key={i}
117
- fg={line.fg}
118
- attributes={line.bold ? TextAttributes.BOLD : undefined}
119
- >
120
- {line.text}
121
- </text>
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 with border */}
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}>{"^N:New ^P:Model ^C:Quit"}</text>
72
+ <text fg={t.textMuted}>{"^P:Commands ^O:Model ^C:Quit"}</text>
73
73
  </box>
74
74
  </box>
75
75
  );
@@ -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 || "default";
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
@@ -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: "Toggle plan mode" },
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" },