diff --git a/desktop/src/pages/Settings.tsx b/desktop/src/pages/Settings.tsx index 5ad3777d..ef9150d1 100644 --- a/desktop/src/pages/Settings.tsx +++ b/desktop/src/pages/Settings.tsx @@ -6,7 +6,8 @@ import { Modal } from '../components/shared/Modal' import { Input } from '../components/shared/Input' import { Button } from '../components/shared/Button' import type { PermissionMode, EffortLevel } from '../types/settings' -import type { Provider, ProviderModel, UpdateProviderInput, ProviderTestResult } from '../types/provider' +import { PROVIDER_PRESETS } from '../config/providerPresets' +import type { SavedProvider, UpdateProviderInput, ProviderTestResult, ModelMapping } from '../types/provider' type SettingsTab = 'providers' | 'permissions' | 'general' @@ -65,26 +66,21 @@ function TabButton({ icon, label, active, onClick }: { icon: string; label: stri // ─── Provider Settings ────────────────────────────────────── function ProviderSettings() { - const { providers, isLoading, fetchProviders, deleteProvider, activateProvider, testProvider } = useProviderStore() + const { providers, activeId, isLoading, fetchProviders, deleteProvider, activateProvider, activateOfficial, testProvider } = useProviderStore() const fetchSettings = useSettingsStore((s) => s.fetchAll) - const [editingProvider, setEditingProvider] = useState(null) + const [editingProvider, setEditingProvider] = useState(null) const [showCreateModal, setShowCreateModal] = useState(false) - const [activatingProvider, setActivatingProvider] = useState(null) const [testResults, setTestResults] = useState>({}) useEffect(() => { fetchProviders() }, [fetchProviders]) - const handleDelete = async (provider: Provider) => { - if (provider.isActive) return + const handleDelete = async (provider: SavedProvider) => { + if (activeId === provider.id) return if (!window.confirm(`Delete provider "${provider.name}"? This cannot be undone.`)) return - try { - await deleteProvider(provider.id) - } catch (err) { - console.error('Failed to delete provider:', err) - } + await deleteProvider(provider.id).catch(console.error) } - const handleTest = async (provider: Provider) => { + const handleTest = async (provider: SavedProvider) => { setTestResults((r) => ({ ...r, [provider.id]: { loading: true } })) try { const result = await testProvider(provider.id) @@ -94,12 +90,18 @@ function ProviderSettings() { } } - const handleActivate = async (providerId: string, modelId: string) => { - await activateProvider(providerId, modelId) + const handleActivate = async (id: string) => { + await activateProvider(id) await fetchSettings() - setActivatingProvider(null) } + const handleActivateOfficial = async () => { + await activateOfficial() + await fetchSettings() + } + + const isOfficialActive = activeId === null + return (
@@ -113,64 +115,75 @@ function ProviderSettings() {
- {isLoading && providers.length === 0 ? ( -
-
+ {/* Official provider card */} +
!isOfficialActive && handleActivateOfficial()} + > + +
+
+ Claude Official + {isOfficialActive && ( + ACTIVE + )} +
+
Anthropic native — no API key required
- ) : providers.length === 0 ? ( -
- dns -

No providers configured

- +
+ + {/* Saved providers */} + {isLoading && providers.length === 0 ? ( +
+
) : (
{providers.map((provider) => { + const isActive = activeId === provider.id const test = testResults[provider.id] + const preset = PROVIDER_PRESETS.find((p) => p.id === provider.presetId) return (
- {/* Status dot */} - - - {/* Info */} +
{provider.name} - {provider.isActive && ( + {preset && preset.id !== 'custom' && ( + {preset.name} + )} + {isActive && ( ACTIVE )}
- {provider.baseUrl} · {provider.models.length} model{provider.models.length !== 1 ? 's' : ''} + {provider.baseUrl} · {provider.models.main}
- {/* Test result inline */} {test && !test.loading && test.result && (
{test.result.success ? `Connected (${test.result.latencyMs}ms)` : `Failed: ${test.result.error}`}
)}
- - {/* Actions */}
- {!provider.isActive && ( - + {!isActive && ( + )} - + - {!provider.isActive && ( - + {!isActive && ( + )}
@@ -180,43 +193,11 @@ function ProviderSettings() { )} {/* Create Modal */} - setShowCreateModal(false)} - mode="create" - /> + setShowCreateModal(false)} mode="create" /> {/* Edit Modal */} {editingProvider && ( - setEditingProvider(null)} - mode="edit" - provider={editingProvider} - /> - )} - - {/* Activate — model picker */} - {activatingProvider && ( - setActivatingProvider(null)} title={`Activate ${activatingProvider.name}`} width={420}> -

Select a model to use with this provider:

-
- {activatingProvider.models.map((m) => ( - - ))} -
-
+ setEditingProvider(null)} mode="edit" provider={editingProvider} /> )}
) @@ -228,47 +209,61 @@ type ProviderFormProps = { open: boolean onClose: () => void mode: 'create' | 'edit' - provider?: Provider + provider?: SavedProvider } function ProviderFormModal({ open, onClose, mode, provider }: ProviderFormProps) { const { createProvider, updateProvider, testConfig } = useProviderStore() const fetchSettings = useSettingsStore((s) => s.fetchAll) - const [name, setName] = useState(provider?.name ?? '') - const [baseUrl, setBaseUrl] = useState(provider?.baseUrl ?? '') + const availablePresets = PROVIDER_PRESETS.filter((p) => p.id !== 'official') + const initialPreset = provider ? availablePresets.find((p) => p.id === provider.presetId) || availablePresets.at(-1)! : availablePresets[0] + + const [selectedPreset, setSelectedPreset] = useState(initialPreset) + const [name, setName] = useState(provider?.name ?? initialPreset.name) + const [baseUrl, setBaseUrl] = useState(provider?.baseUrl ?? initialPreset.baseUrl) const [apiKey, setApiKey] = useState('') const [notes, setNotes] = useState(provider?.notes ?? '') - const [models, setModels] = useState(provider?.models ?? [{ id: '', name: '', description: '', context: '' }]) + const [models, setModels] = useState(provider?.models ?? { ...initialPreset.defaultModels }) + const [showAdvanced, setShowAdvanced] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) const [testResult, setTestResult] = useState(null) const [isTesting, setIsTesting] = useState(false) - const canSubmit = name.trim() && baseUrl.trim() && (mode === 'edit' || apiKey.trim()) && models.some((m) => m.id.trim() && m.name.trim()) + const handlePresetChange = (preset: typeof initialPreset) => { + setSelectedPreset(preset) + setName(preset.name) + setBaseUrl(preset.baseUrl) + setModels({ ...preset.defaultModels }) + setTestResult(null) + } + + const isCustom = selectedPreset.id === 'custom' + const canSubmit = name.trim() && baseUrl.trim() && (mode === 'edit' || apiKey.trim()) && models.main.trim() const handleSubmit = async () => { if (!canSubmit) return setIsSubmitting(true) try { - const validModels = models.filter((m) => m.id.trim() && m.name.trim()) if (mode === 'create') { await createProvider({ + presetId: selectedPreset.id, name: name.trim(), - baseUrl: baseUrl.trim(), apiKey: apiKey.trim(), - models: validModels, + baseUrl: baseUrl.trim(), + models, notes: notes.trim() || undefined, }) } else if (provider) { const input: UpdateProviderInput = { name: name.trim(), baseUrl: baseUrl.trim(), - models: validModels, + models, notes: notes.trim() || undefined, } if (apiKey.trim()) input.apiKey = apiKey.trim() await updateProvider(provider.id, input) - if (provider.isActive) await fetchSettings() + if (useProviderStore.getState().activeId === provider.id) await fetchSettings() } onClose() } catch (err) { @@ -279,18 +274,16 @@ function ProviderFormModal({ open, onClose, mode, provider }: ProviderFormProps) } const handleTest = async () => { - const firstModel = models.find((m) => m.id.trim()) - if (!baseUrl.trim() || !firstModel) return + if (!baseUrl.trim() || !models.main.trim()) return setIsTesting(true) setTestResult(null) try { let result: ProviderTestResult if (mode === 'edit' && provider && !apiKey.trim()) { - // Edit mode without new key — test saved provider directly result = await useProviderStore.getState().testProvider(provider.id) } else { if (!apiKey.trim()) return - result = await testConfig({ baseUrl: baseUrl.trim(), apiKey: apiKey.trim(), modelId: firstModel.id.trim() }) + result = await testConfig({ baseUrl: baseUrl.trim(), apiKey: apiKey.trim(), modelId: models.main.trim() }) } setTestResult(result) } catch { @@ -300,12 +293,6 @@ function ProviderFormModal({ open, onClose, mode, provider }: ProviderFormProps) } } - const addModel = () => setModels([...models, { id: '', name: '', description: '', context: '' }]) - const removeModel = (index: number) => setModels(models.filter((_, i) => i !== index)) - const updateModel = (index: number, field: keyof ProviderModel, value: string) => { - setModels(models.map((m, i) => i === index ? { ...m, [field]: value } : m)) - } - return (
- setName(e.target.value)} placeholder="e.g. OpenRouter, Anthropic Official" /> - setBaseUrl(e.target.value)} placeholder="https://api.anthropic.com" /> + {/* Preset chips (create mode only) */} + {mode === 'create' && ( +
+ +
+ {availablePresets.map((preset) => ( + + ))} +
+
+ )} + + setName(e.target.value)} placeholder="Provider name" /> + + {/* Base URL — always visible for custom, read-only hint for presets */} + {isCustom || mode === 'edit' ? ( + setBaseUrl(e.target.value)} placeholder="https://api.example.com/anthropic" /> + ) : ( +
+ +
+ {baseUrl} +
+
+ )} + setApiKey(e.target.value)} - placeholder={mode === 'edit' ? '****' : 'sk-ant-...'} + placeholder={mode === 'edit' ? '****' : 'sk-...'} /> + setNotes(e.target.value)} placeholder="Optional notes..." /> - {/* Models */} -
-
- - -
- {models.map((m, i) => ( -
-
- updateModel(i, 'id', e.target.value)} - placeholder="Model ID *" - className="h-8 px-2 text-xs rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] outline-none focus:border-[var(--color-border-focus)]" - /> - updateModel(i, 'name', e.target.value)} - placeholder="Display name *" - className="h-8 px-2 text-xs rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] outline-none focus:border-[var(--color-border-focus)]" - /> - updateModel(i, 'description', e.target.value)} - placeholder="Description" - className="h-8 px-2 text-xs rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] outline-none focus:border-[var(--color-border-focus)]" - /> - updateModel(i, 'context', e.target.value)} - placeholder="Context (e.g. 200k)" - className="h-8 px-2 text-xs rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] text-[var(--color-text-primary)] placeholder:text-[var(--color-text-tertiary)] outline-none focus:border-[var(--color-border-focus)]" - /> -
- {models.length > 1 && ( - - )} + {/* Advanced: Model Mapping */} +
+ + {showAdvanced && ( +
+ setModels({ ...models, main: e.target.value })} placeholder="Model ID" /> + setModels({ ...models, haiku: e.target.value })} placeholder="Same as main" /> + setModels({ ...models, sonnet: e.target.value })} placeholder="Same as main" /> + setModels({ ...models, opus: e.target.value })} placeholder="Same as main" />
- ))} + )}
{/* Test connection */}
- {testResult && (