import { useState, useEffect, useMemo, useRef, type ReactNode } from 'react' import QRCode from 'qrcode' import { Copy, Eye, EyeOff, PowerOff, QrCode, RotateCw } from 'lucide-react' import { useSettingsStore, UI_ZOOM_MIN, UI_ZOOM_MAX, UI_ZOOM_STEP } from '../stores/settingsStore' import { useProviderStore } from '../stores/providerStore' import { useTranslation } from '../i18n' import { Modal } from '../components/shared/Modal' import { ConfirmDialog } from '../components/shared/ConfirmDialog' import { Input } from '../components/shared/Input' import { Button } from '../components/shared/Button' import { Dropdown } from '../components/shared/Dropdown' import type { PermissionMode, EffortLevel, ThemeMode, WebSearchMode, AppMode } from '../types/settings' import type { Locale } from '../i18n' import type { SavedProvider, UpdateProviderInput, ProviderTestResult, ModelMapping, ApiFormat, ProviderAuthStrategy } from '../types/provider' import type { ProviderPreset } from '../types/providerPreset' import { AdapterSettings } from './AdapterSettings' import { useAgentStore } from '../stores/agentStore' import { useSessionStore } from '../stores/sessionStore' import type { AgentDefinition, AgentSource } from '../api/agents' import { MarkdownRenderer } from '../components/markdown/MarkdownRenderer' import { useSkillStore } from '../stores/skillStore' import { SkillList } from '../components/skills/SkillList' import { SkillDetail } from '../components/skills/SkillDetail' import { usePluginStore } from '../stores/pluginStore' import { PluginList } from '../components/plugins/PluginList' import { PluginDetail } from '../components/plugins/PluginDetail' import { ComputerUseSettings } from './ComputerUseSettings' import { McpSettings } from './McpSettings' import { TerminalSettings } from './TerminalSettings' import { DiagnosticsSettings } from './DiagnosticsSettings' import { ActivitySettings } from './ActivitySettings' import { useUIStore, type SettingsTab } from '../stores/uiStore' import { ClaudeOfficialLogin } from '../components/settings/ClaudeOfficialLogin' import { useUpdateStore } from '../stores/updateStore' import { formatBytes } from '../lib/formatBytes' import { isTauriRuntime } from '../lib/desktopRuntime' import { getDesktopNotificationPermission, notifyDesktop, openDesktopNotificationSettings, requestDesktopNotificationPermission, type DesktopNotificationPermission, } from '../lib/desktopNotifications' import { API_KEY_JSON_PLACEHOLDER, maskSettingsJsonSecrets, restoreSettingsJsonSecrets, stripProviderSettingsJsonEnv, } from '../lib/providerSettingsJson' import { copyTextToClipboard } from '../components/chat/clipboard' function buildH5LaunchUrl(baseUrl: string | null, token: string | null): string | null { if (!baseUrl) return null try { const url = new URL(baseUrl) if (token) { url.searchParams.set('serverUrl', baseUrl) url.searchParams.set('h5Token', token) } return url.toString().replace(/\/$/, '') } catch { return token ? `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}serverUrl=${encodeURIComponent(baseUrl)}&h5Token=${encodeURIComponent(token)}` : baseUrl } } export function Settings() { const [activeTab, setActiveTab] = useState('providers') const pendingSettingsTab = useUIStore((s) => s.pendingSettingsTab) const t = useTranslation() useEffect(() => { if (!pendingSettingsTab) return setActiveTab(pendingSettingsTab) useUIStore.getState().setPendingSettingsTab(null) }, [pendingSettingsTab]) return (
{/* Tab navigation */}
setActiveTab('providers')} /> setActiveTab('permissions')} /> setActiveTab('general')} /> setActiveTab('h5Access')} /> setActiveTab('adapters')} /> setActiveTab('terminal')} /> setActiveTab('mcp')} /> setActiveTab('agents')} /> setActiveTab('skills')} /> setActiveTab('plugins')} /> setActiveTab('computerUse')} /> setActiveTab('activity')} /> setActiveTab('diagnostics')} />
setActiveTab('about')} />
{/* Tab content */}
{activeTab === 'providers' && } {activeTab === 'permissions' && } {activeTab === 'activity' && } {activeTab === 'general' && } {activeTab === 'h5Access' && } {activeTab === 'adapters' && } {activeTab === 'terminal' && } {activeTab === 'mcp' && } {activeTab === 'agents' && } {activeTab === 'skills' && } {activeTab === 'plugins' && } {activeTab === 'computerUse' && } {activeTab === 'diagnostics' && } {activeTab === 'about' && }
) } function TabButton({ icon, label, active, onClick }: { icon: string; label: string; active: boolean; onClick: () => void }) { return ( ) } // ─── Provider Settings ────────────────────────────────────── function ProviderSettings() { const { providers, activeId, hasLoadedProviders, presets, isLoading, isPresetsLoading, fetchProviders, fetchPresets, deleteProvider, activateProvider, activateOfficial, testProvider, } = useProviderStore() const fetchSettings = useSettingsStore((s) => s.fetchAll) const t = useTranslation() const [editingProvider, setEditingProvider] = useState(null) const [showCreateModal, setShowCreateModal] = useState(false) const [pendingDeleteProvider, setPendingDeleteProvider] = useState(null) const [isDeletingProvider, setIsDeletingProvider] = useState(false) const [testResults, setTestResults] = useState>({}) useEffect(() => { void fetchProviders() void fetchPresets() }, [fetchPresets, fetchProviders]) const presetMap = useMemo( () => new Map(presets.map((preset) => [preset.id, preset])), [presets], ) const handleDelete = async (provider: SavedProvider) => { if (activeId === provider.id) return setPendingDeleteProvider(provider) } const confirmDelete = async () => { if (!pendingDeleteProvider) return setIsDeletingProvider(true) try { await deleteProvider(pendingDeleteProvider.id) setPendingDeleteProvider(null) } catch (error) { console.error(error) } finally { setIsDeletingProvider(false) } } const handleTest = async (provider: SavedProvider) => { setTestResults((r) => ({ ...r, [provider.id]: { loading: true } })) try { const result = await testProvider(provider.id) setTestResults((r) => ({ ...r, [provider.id]: { loading: false, result } })) } catch { setTestResults((r) => ({ ...r, [provider.id]: { loading: false, result: { connectivity: { success: false, latencyMs: 0, error: t('settings.providers.requestFailed') } } } })) } } const handleActivate = async (id: string) => { await activateProvider(id) await fetchSettings() } const handleActivateOfficial = async () => { await activateOfficial() await fetchSettings() } const isOfficialActive = hasLoadedProviders && activeId === null return (

{t('settings.providers.title')}

{t('settings.providers.description')}

{/* Official provider — always visible at top */}
!isOfficialActive && handleActivateOfficial()} >
{t('settings.providers.officialName')} {isOfficialActive && ( {t('settings.providers.default')} )}
{t('settings.providers.officialDesc')}
{isOfficialActive && (
)}
{/* Saved providers */} {isLoading && providers.length === 0 ? (
) : (
{providers.map((provider) => { const isActive = activeId === provider.id const test = testResults[provider.id] const preset = presetMap.get(provider.presetId) return (
{provider.name} {preset && preset.id !== 'custom' && ( {preset.name} )} {provider.apiFormat && provider.apiFormat !== 'anthropic' && ( {provider.apiFormat === 'openai_chat' ? 'OpenAI Chat' : 'OpenAI Responses'} )} {isActive && ( {t('settings.providers.default')} )}
{provider.baseUrl} · {provider.models.main}
{test && !test.loading && test.result && (
{test.result.connectivity.success ? t('settings.providers.connectivityOk', { latency: String(test.result.connectivity.latencyMs) }) : t('settings.providers.connectivityFailed', { error: test.result.connectivity.error || '' })} {test.result.proxy && ( {test.result.proxy.success ? t('settings.providers.proxyOk', { latency: String(test.result.proxy.latencyMs) }) : t('settings.providers.proxyFailed', { error: test.result.proxy.error || '' })} )}
)}
{!isActive && ( )} {!isActive && ( )}
) })}
)} {/* Create Modal — conditionally rendered so state resets on close */} {showCreateModal && ( setShowCreateModal(false)} mode="create" presets={presets} /> )} {/* Edit Modal */} {editingProvider && ( setEditingProvider(null)} mode="edit" provider={editingProvider} presets={presets} /> )} { if (isDeletingProvider) return setPendingDeleteProvider(null) }} onConfirm={confirmDelete} title={t('common.delete')} body={pendingDeleteProvider ? t('settings.providers.confirmDelete', { name: pendingDeleteProvider.name }) : ''} confirmLabel={t('common.delete')} cancelLabel={t('common.cancel')} confirmVariant="danger" loading={isDeletingProvider} />
) } // ─── Provider Form Modal ────────────────────────────────────── type ProviderFormProps = { open: boolean onClose: () => void mode: 'create' | 'edit' provider?: SavedProvider presets: ProviderPreset[] } function requirePreset(preset: ProviderPreset | undefined): ProviderPreset { if (!preset) { throw new Error('Provider presets are not configured') } return preset } const AUTO_COMPACT_WINDOW_ENV_KEY = 'CLAUDE_CODE_AUTO_COMPACT_WINDOW' const MODEL_CONTEXT_WINDOWS_ENV_KEY = 'CLAUDE_CODE_MODEL_CONTEXT_WINDOWS' const MODEL_CONTEXT_WINDOW_MIN = 16000 const MODEL_CONTEXT_WINDOW_MAX = 10000000 const MODEL_SLOTS = ['main', 'haiku', 'sonnet', 'opus'] as const const DEFAULT_PROVIDER_AUTH_STRATEGY: ProviderAuthStrategy = 'auth_token' const AUTH_ENV_KEYS = new Set(['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN']) type ModelSlot = typeof MODEL_SLOTS[number] type ModelContextInputs = Record function formatContextWindow(value: number): string { return value.toLocaleString('en-US') } function getPresetAutoCompactWindow(preset: ProviderPreset): string { return preset.defaultEnv?.[AUTO_COMPACT_WINDOW_ENV_KEY] ?? '' } function getPresetAuthStrategy(preset: ProviderPreset): ProviderAuthStrategy { return preset.authStrategy ?? DEFAULT_PROVIDER_AUTH_STRATEGY } function omitAuthEnv(env: Record | undefined): Record { if (!env) return {} return Object.fromEntries( Object.entries(env).filter(([key]) => !AUTH_ENV_KEYS.has(key.toUpperCase())), ) } function getProviderAuthValue(apiKey: string, preset: ProviderPreset): string { return apiKey || preset.defaultEnv?.ANTHROPIC_AUTH_TOKEN || preset.defaultEnv?.ANTHROPIC_API_KEY || (preset.needsApiKey ? '(your API key)' : '') } function buildSettingsJsonAuthEnv( apiFormat: ApiFormat, authStrategy: ProviderAuthStrategy, apiKey: string, preset: ProviderPreset, ): Record { if (apiFormat !== 'anthropic') { return { ANTHROPIC_API_KEY: 'proxy-managed' } } const value = getProviderAuthValue(apiKey, preset) switch (authStrategy) { case 'api_key': return value ? { ANTHROPIC_API_KEY: value } : {} case 'auth_token': return value ? { ANTHROPIC_AUTH_TOKEN: value } : {} case 'auth_token_empty_api_key': return { ANTHROPIC_API_KEY: '', ...(value ? { ANTHROPIC_AUTH_TOKEN: value } : {}), } case 'dual_same_token': return value ? { ANTHROPIC_API_KEY: value, ANTHROPIC_AUTH_TOKEN: value } : {} case 'dual_dummy': return { ANTHROPIC_API_KEY: 'dummy', ANTHROPIC_AUTH_TOKEN: 'dummy' } } } function inferAuthStrategyFromEnv(env: Record): ProviderAuthStrategy | null { if (env.ANTHROPIC_API_KEY === 'dummy' && env.ANTHROPIC_AUTH_TOKEN === 'dummy') return 'dual_dummy' if (env.ANTHROPIC_API_KEY === '' && env.ANTHROPIC_AUTH_TOKEN) return 'auth_token_empty_api_key' if (env.ANTHROPIC_API_KEY && env.ANTHROPIC_AUTH_TOKEN && env.ANTHROPIC_API_KEY === env.ANTHROPIC_AUTH_TOKEN) return 'dual_same_token' if (env.ANTHROPIC_AUTH_TOKEN) return 'auth_token' if (env.ANTHROPIC_API_KEY) return 'api_key' return null } function parseAutoCompactWindowInput(value: string): number | undefined { const trimmed = value.trim() if (!trimmed) return undefined const parsed = Number(trimmed) if (!Number.isInteger(parsed)) return undefined if (parsed < MODEL_CONTEXT_WINDOW_MIN || parsed > MODEL_CONTEXT_WINDOW_MAX) return undefined return parsed } function getAutoCompactWindowErrorKey(value: string): 'number' | 'range' | null { const trimmed = value.trim() if (!trimmed) return null const parsed = Number(trimmed) if (!Number.isInteger(parsed)) return 'number' if (parsed < MODEL_CONTEXT_WINDOW_MIN || parsed > MODEL_CONTEXT_WINDOW_MAX) return 'range' return null } function parseModelContextWindowsInput(value: string): number | undefined { return parseAutoCompactWindowInput(value) } function getModelContextWindowErrorKey(value: string): 'number' | 'range' | null { return getAutoCompactWindowErrorKey(value) } function getModelContextInputValue( model: string | undefined, preset: ProviderPreset, provider?: SavedProvider, ): string { const trimmedModel = model?.trim() if (!trimmedModel) return '' const value = provider?.modelContextWindows?.[trimmedModel] ?? preset.modelContextWindows?.[trimmedModel] return value !== undefined ? String(value) : '' } function getModelContextInputs( models: ModelMapping, preset: ProviderPreset, provider?: SavedProvider, ): ModelContextInputs { const inputs = {} as ModelContextInputs for (const slot of MODEL_SLOTS) { inputs[slot] = getModelContextInputValue(models[slot], preset, provider) } return inputs } function buildModelContextWindows( models: ModelMapping, inputs: ModelContextInputs, ): Record { const windows: Record = {} for (const slot of MODEL_SLOTS) { const model = models[slot]?.trim() const parsed = parseModelContextWindowsInput(inputs[slot]) if (model && parsed !== undefined) { windows[model] = parsed } } return windows } function updateSettingsJsonAutoCompactWindow(raw: string, value: string): string { try { const parsed = JSON.parse(raw || '{}') as { env?: Record } const existingEnv = parsed.env && typeof parsed.env === 'object' && !Array.isArray(parsed.env) ? parsed.env : {} const env = { ...existingEnv } const trimmed = value.trim() if (trimmed) { env[AUTO_COMPACT_WINDOW_ENV_KEY] = trimmed } else { delete env[AUTO_COMPACT_WINDOW_ENV_KEY] } parsed.env = env return JSON.stringify(parsed, null, 2) } catch { return raw } } function updateSettingsJsonModelContextWindows( raw: string, modelContextWindows: Record, ): string { try { const parsed = JSON.parse(raw || '{}') as { env?: Record } const existingEnv = parsed.env && typeof parsed.env === 'object' && !Array.isArray(parsed.env) ? parsed.env : {} const env = { ...existingEnv } if (Object.keys(modelContextWindows).length > 0) { env[MODEL_CONTEXT_WINDOWS_ENV_KEY] = JSON.stringify(modelContextWindows) } else { delete env[MODEL_CONTEXT_WINDOWS_ENV_KEY] } parsed.env = env return JSON.stringify(parsed, null, 2) } catch { return raw } } function updateSettingsJsonModels(raw: string, models: ModelMapping): string { try { const parsed = JSON.parse(raw || '{}') as { env?: Record } const existingEnv = parsed.env && typeof parsed.env === 'object' && !Array.isArray(parsed.env) ? parsed.env : {} parsed.env = { ...existingEnv, ANTHROPIC_MODEL: models.main, ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haiku, ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnet, ANTHROPIC_DEFAULT_OPUS_MODEL: models.opus, } return JSON.stringify(parsed, null, 2) } catch { return raw } } function updateSettingsJsonProviderConnection( raw: string, apiFormat: ApiFormat, authStrategy: ProviderAuthStrategy, apiKey: string, preset: ProviderPreset, baseUrl: string, ): string { try { const parsed = JSON.parse(raw || '{}') as { env?: Record } const existingEnv = parsed.env && typeof parsed.env === 'object' && !Array.isArray(parsed.env) ? parsed.env : {} const env = { ...existingEnv } delete env.ANTHROPIC_API_KEY delete env.ANTHROPIC_AUTH_TOKEN env.ANTHROPIC_BASE_URL = apiFormat !== 'anthropic' ? 'http://127.0.0.1:3456/proxy' : baseUrl Object.assign(env, buildSettingsJsonAuthEnv(apiFormat, authStrategy, apiKey, preset)) parsed.env = env return JSON.stringify(parsed, null, 2) } catch { return raw } } function buildFallbackPreset(provider?: SavedProvider): ProviderPreset { return { id: provider?.presetId ?? 'custom', name: provider?.name ?? 'Custom', baseUrl: provider?.baseUrl ?? '', apiFormat: provider?.apiFormat ?? 'anthropic', authStrategy: provider?.authStrategy, defaultModels: provider?.models ?? { main: '', haiku: '', sonnet: '', opus: '' }, modelContextWindows: provider?.modelContextWindows, defaultEnv: provider?.autoCompactWindow !== undefined ? { [AUTO_COMPACT_WINDOW_ENV_KEY]: String(provider.autoCompactWindow) } : undefined, needsApiKey: true, websiteUrl: '', } } function openExternalUrl(url: string) { if (!isTauriRuntime()) { window.open(url, '_blank', 'noopener,noreferrer') return } void import('@tauri-apps/plugin-shell') .then((mod) => mod.open(url)) .catch(() => window.open(url, '_blank', 'noopener,noreferrer')) } function ProviderFormModal({ open, onClose, mode, provider, presets }: ProviderFormProps) { const { createProvider, updateProvider, testConfig } = useProviderStore() const fetchSettings = useSettingsStore((s) => s.fetchAll) const t = useTranslation() const availablePresets = presets.filter((p) => p.id !== 'official') const regularPresets = availablePresets.filter((p) => !p.featured) const featuredPresets = availablePresets.filter((p) => p.featured) const presetDefaultEnvKeys = useMemo( () => presets.flatMap((preset) => Object.keys(preset.defaultEnv ?? {})), [presets], ) const fallbackPreset = provider ? buildFallbackPreset(provider) : requirePreset(availablePresets[availablePresets.length - 1]) const initialPreset = requirePreset( provider ? availablePresets.find((p) => p.id === provider.presetId) ?? fallbackPreset : availablePresets[0] ?? fallbackPreset, ) const [selectedPreset, setSelectedPreset] = useState(initialPreset) const [name, setName] = useState(provider?.name ?? initialPreset.name) const [baseUrl, setBaseUrl] = useState(provider?.baseUrl ?? initialPreset.baseUrl) const [apiFormat, setApiFormat] = useState(provider?.apiFormat ?? initialPreset.apiFormat ?? 'anthropic') const [authStrategy, setAuthStrategy] = useState(provider?.authStrategy ?? getPresetAuthStrategy(initialPreset)) const [apiKey, setApiKey] = useState(provider?.apiKey ?? '') const [showApiKey, setShowApiKey] = useState(false) const [notes, setNotes] = useState(provider?.notes ?? '') const [models, setModels] = useState(provider?.models ?? { ...initialPreset.defaultModels }) const [modelContextInputs, setModelContextInputs] = useState( getModelContextInputs(provider?.models ?? initialPreset.defaultModels, initialPreset, provider), ) const [autoCompactWindow, setAutoCompactWindow] = useState( provider?.autoCompactWindow !== undefined ? String(provider.autoCompactWindow) : getPresetAutoCompactWindow(initialPreset), ) const [showContextSettings, setShowContextSettings] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false) const [testResult, setTestResult] = useState(null) const [isTesting, setIsTesting] = useState(false) const [settingsJson, setSettingsJson] = useState('') const [settingsJsonError, setSettingsJsonError] = useState(null) const jsonPastedRef = useRef(false) // Load current settings.json and merge provider env vars useEffect(() => { // Skip if JSON was just populated by user paste if (jsonPastedRef.current) { jsonPastedRef.current = false return } import('../api/providers').then(({ providersApi }) => { providersApi.getSettings().then((settings) => { const needsProxy = apiFormat !== 'anthropic' const autoCompactWindowEnv = autoCompactWindow.trim() const modelContextWindows = buildModelContextWindows(models, modelContextInputs) const existingEnv = (settings.env as Record) || {} const cleanedEnv = stripProviderSettingsJsonEnv(existingEnv, presetDefaultEnvKeys) const merged = { ...settings, skipWebFetchPreflight: settings.skipWebFetchPreflight ?? true, env: { ...cleanedEnv, ...omitAuthEnv(selectedPreset.defaultEnv), ...(autoCompactWindowEnv ? { [AUTO_COMPACT_WINDOW_ENV_KEY]: autoCompactWindowEnv } : {}), ...(Object.keys(modelContextWindows).length > 0 ? { [MODEL_CONTEXT_WINDOWS_ENV_KEY]: JSON.stringify(modelContextWindows) } : {}), ANTHROPIC_BASE_URL: needsProxy ? 'http://127.0.0.1:3456/proxy' : baseUrl, ...buildSettingsJsonAuthEnv(apiFormat, authStrategy, apiKey, selectedPreset), ANTHROPIC_MODEL: models.main, ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haiku, ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnet, ANTHROPIC_DEFAULT_OPUS_MODEL: models.opus, }, } setSettingsJson(JSON.stringify(merged, null, 2)) }).catch(() => { setSettingsJson(JSON.stringify({}, null, 2)) }) }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedPreset.id]) const handlePresetChange = (preset: ProviderPreset) => { setSelectedPreset(preset) setName(preset.name) setBaseUrl(preset.baseUrl) setApiFormat(preset.apiFormat ?? 'anthropic') setAuthStrategy(getPresetAuthStrategy(preset)) setModels({ ...preset.defaultModels }) setModelContextInputs(getModelContextInputs(preset.defaultModels, preset)) setAutoCompactWindow(getPresetAutoCompactWindow(preset)) setShowContextSettings(false) setTestResult(null) } const isCustom = selectedPreset.id === 'custom' const requiresApiKey = selectedPreset.needsApiKey !== false const autoCompactWindowErrorKey = getAutoCompactWindowErrorKey(autoCompactWindow) const modelContextWindowErrorSlots = MODEL_SLOTS.filter((slot) => getModelContextWindowErrorKey(modelContextInputs[slot])) const canSubmit = name.trim() && baseUrl.trim() && (mode === 'edit' || !requiresApiKey || apiKey.trim()) && models.main.trim() && !settingsJsonError && !autoCompactWindowErrorKey && modelContextWindowErrorSlots.length === 0 const apiKeyUrl = selectedPreset.apiKeyUrl?.trim() const promoText = selectedPreset.promoText?.trim() const displayedSettingsJson = showApiKey ? settingsJson : maskSettingsJsonSecrets(settingsJson) const apiFormatItems = [ { value: 'anthropic' as const, label: t('settings.providers.apiFormatAnthropic'), icon: hub, }, { value: 'openai_chat' as const, label: t('settings.providers.apiFormatOpenaiChat'), icon: forum, }, { value: 'openai_responses' as const, label: t('settings.providers.apiFormatOpenaiResponses'), icon: route, }, ] const selectedApiFormatLabel = apiFormatItems.find((item) => item.value === apiFormat)?.label ?? t('settings.providers.apiFormatAnthropic') const authStrategyItems = [ { value: 'auth_token' as const, label: t('settings.providers.authStrategyAuthToken'), description: t('settings.providers.authStrategyAuthTokenDesc'), icon: key, }, { value: 'auth_token_empty_api_key' as const, label: t('settings.providers.authStrategyAuthTokenEmptyApiKey'), description: t('settings.providers.authStrategyAuthTokenEmptyApiKeyDesc'), icon: key_off, }, { value: 'api_key' as const, label: t('settings.providers.authStrategyApiKey'), description: t('settings.providers.authStrategyApiKeyDesc'), icon: vpn_key, }, { value: 'dual_same_token' as const, label: t('settings.providers.authStrategyDualSameToken'), description: t('settings.providers.authStrategyDualSameTokenDesc'), icon: sync_alt, }, { value: 'dual_dummy' as const, label: t('settings.providers.authStrategyDualDummy'), description: t('settings.providers.authStrategyDualDummyDesc'), icon: construction, }, ] satisfies Array<{ value: ProviderAuthStrategy; label: string; description: string; icon: ReactNode }> const selectedAuthStrategyLabel = authStrategyItems.find((item) => item.value === authStrategy)?.label ?? t('settings.providers.authStrategyAuthToken') const configuredContextWindows = buildModelContextWindows(models, modelContextInputs) const configuredContextSummary = Object.entries(configuredContextWindows) .filter(([model], index, entries) => entries.findIndex(([candidate]) => candidate === model) === index) .map(([model, value]) => `${model}: ${formatContextWindow(value)}`) const parsedFallbackContextWindow = parseAutoCompactWindowInput(autoCompactWindow) const fallbackContextSummary = parsedFallbackContextWindow !== undefined ? t('settings.providers.contextFallbackSummary', { tokens: formatContextWindow(parsedFallbackContextWindow), }) : t('settings.providers.contextFallbackAuto') const contextSummary = configuredContextSummary.length > 0 ? [...configuredContextSummary, fallbackContextSummary].join(' · ') : t('settings.providers.contextSummaryAuto') const shouldShowContextFields = showContextSettings || modelContextWindowErrorSlots.length > 0 || !!autoCompactWindowErrorKey const handleAutoCompactWindowChange = (value: string) => { setAutoCompactWindow(value) setSettingsJson((current) => updateSettingsJsonAutoCompactWindow(current, value)) } const handleBaseUrlChange = (value: string) => { setBaseUrl(value) setSettingsJson((current) => updateSettingsJsonProviderConnection(current, apiFormat, authStrategy, apiKey, selectedPreset, value)) } const handleApiKeyChange = (value: string) => { setApiKey(value) setSettingsJson((current) => updateSettingsJsonProviderConnection(current, apiFormat, authStrategy, value, selectedPreset, baseUrl)) } const handleApiFormatChange = (value: ApiFormat) => { setApiFormat(value) setSettingsJson((current) => updateSettingsJsonProviderConnection(current, value, authStrategy, apiKey, selectedPreset, baseUrl)) } const handleAuthStrategyChange = (value: ProviderAuthStrategy) => { setAuthStrategy(value) setSettingsJson((current) => updateSettingsJsonProviderConnection(current, apiFormat, value, apiKey, selectedPreset, baseUrl)) } const handleModelChange = (slot: ModelSlot, value: string) => { const nextModels = { ...models, [slot]: value } const nextInputs = { ...modelContextInputs, [slot]: getModelContextInputValue(value, selectedPreset, provider), } setModels(nextModels) setModelContextInputs(nextInputs) setSettingsJson((current) => updateSettingsJsonModelContextWindows( updateSettingsJsonModels(current, nextModels), buildModelContextWindows(nextModels, nextInputs), )) } const handleModelContextWindowChange = (slot: ModelSlot, value: string) => { const nextInputs = { ...modelContextInputs, [slot]: value } setModelContextInputs(nextInputs) setSettingsJson((current) => updateSettingsJsonModelContextWindows( current, buildModelContextWindows(models, nextInputs), )) } const renderPresetButton = (preset: ProviderPreset) => ( ) const handleSubmit = async () => { if (!canSubmit) return const parsedAutoCompactWindow = parseAutoCompactWindowInput(autoCompactWindow) const parsedModelContextWindows = buildModelContextWindows(models, modelContextInputs) setIsSubmitting(true) try { // Write the edited cc-haha settings.json first so provider-specific model // settings never conflict with the user's global ~/.claude/settings.json. if (settingsJson.trim()) { try { const parsed = restoreSettingsJsonSecrets(JSON.parse(settingsJson), settingsJson, apiKey) const { providersApi } = await import('../api/providers') await providersApi.updateSettings(parsed) } catch { // JSON validation already prevents this } } if (mode === 'create') { await createProvider({ presetId: selectedPreset.id, name: name.trim(), apiKey: apiKey.trim(), authStrategy, baseUrl: baseUrl.trim(), apiFormat, models, ...(parsedAutoCompactWindow !== undefined && { autoCompactWindow: parsedAutoCompactWindow }), ...(Object.keys(parsedModelContextWindows).length > 0 && { modelContextWindows: parsedModelContextWindows }), notes: notes.trim() || undefined, }) } else if (provider) { const input: UpdateProviderInput = { name: name.trim(), baseUrl: baseUrl.trim(), authStrategy, apiFormat, models, autoCompactWindow: parsedAutoCompactWindow ?? null, modelContextWindows: Object.keys(parsedModelContextWindows).length > 0 ? parsedModelContextWindows : null, notes: notes.trim() || undefined, } if (apiKey.trim()) input.apiKey = apiKey.trim() await updateProvider(provider.id, input) } await fetchSettings() onClose() } catch (err) { console.error('Failed to save provider:', err) } finally { setIsSubmitting(false) } } const handleTest = async () => { if (!baseUrl.trim() || !models.main.trim()) return setIsTesting(true) setTestResult(null) try { let result: ProviderTestResult if (mode === 'edit' && provider && !apiKey.trim()) { result = await useProviderStore.getState().testProvider(provider.id, { baseUrl: baseUrl.trim(), modelId: models.main.trim(), apiFormat, authStrategy, }) } else { if (requiresApiKey && !apiKey.trim()) return result = await testConfig({ baseUrl: baseUrl.trim(), apiKey: apiKey.trim() || selectedPreset.defaultEnv?.ANTHROPIC_AUTH_TOKEN || 'local', modelId: models.main.trim(), authStrategy, apiFormat, }) } setTestResult(result) } catch { setTestResult({ connectivity: { success: false, latencyMs: 0, error: t('settings.providers.requestFailed') } }) } finally { setIsTesting(false) } } return ( } >
{/* Preset chips */} {mode === 'create' && (
{regularPresets.map(renderPresetButton)}
{featuredPresets.length > 0 && (
{featuredPresets.map(renderPresetButton)}
)}
)} setName(e.target.value)} placeholder={t('settings.providers.namePlaceholder')} /> setNotes(e.target.value)} placeholder={t('settings.providers.notesPlaceholder')} /> handleBaseUrlChange(e.target.value)} placeholder={t('settings.providers.baseUrlPlaceholder')} /> {/* API Format */} {(isCustom || mode === 'edit') ? (
items={apiFormatItems} value={apiFormat} onChange={handleApiFormatChange} width="100%" className="block w-full" trigger={ } /> {apiFormat !== 'anthropic' && (

{t('settings.providers.proxyHint')}

)}
) : apiFormat !== 'anthropic' ? (
{apiFormat === 'openai_chat' ? t('settings.providers.apiFormatOpenaiChat') : t('settings.providers.apiFormatOpenaiResponses')}
) : null} {apiFormat === 'anthropic' && (
items={authStrategyItems} value={authStrategy} onChange={handleAuthStrategyChange} width="100%" className="block w-full" trigger={ } />
)}
handleApiKeyChange(e.target.value)} placeholder="sk-..." className="h-10 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] px-3 pr-10 text-sm text-[var(--color-text-primary)] outline-none transition-colors duration-150 placeholder:text-[var(--color-text-tertiary)] focus:border-[var(--color-border-focus)] focus:shadow-[var(--shadow-focus-ring)]" />
{(apiKeyUrl || promoText) && (
{apiKeyUrl && ( )} {promoText && ( )}
)} {/* Model Mapping */}
handleModelChange('main', e.target.value)} placeholder="Model ID" /> handleModelChange('haiku', e.target.value)} placeholder={t('settings.providers.sameAsMain')} /> handleModelChange('sonnet', e.target.value)} placeholder={t('settings.providers.sameAsMain')} /> handleModelChange('opus', e.target.value)} placeholder={t('settings.providers.sameAsMain')} />
{shouldShowContextFields && (
{MODEL_SLOTS.map((slot) => { const errorKey = getModelContextWindowErrorKey(modelContextInputs[slot]) const labelKey = slot === 'main' ? 'settings.providers.mainContextWindow' : slot === 'haiku' ? 'settings.providers.haikuContextWindow' : slot === 'sonnet' ? 'settings.providers.sonnetContextWindow' : 'settings.providers.opusContextWindow' return (
handleModelContextWindowChange(slot, e.target.value)} placeholder={t('settings.providers.contextWindowPlaceholder')} /> {errorKey && (

{errorKey === 'number' ? t('settings.providers.modelContextWindowNumberError') : t('settings.providers.modelContextWindowRangeError')}

)}
) })}

{t('settings.providers.modelContextWindowsDesc')}

handleAutoCompactWindowChange(e.target.value)} placeholder={t('settings.providers.autoCompactWindowPlaceholder')} /> {autoCompactWindowErrorKey ? (

{autoCompactWindowErrorKey === 'number' ? t('settings.providers.autoCompactWindowNumberError') : t('settings.providers.autoCompactWindowRangeError')}

) : (

{t('settings.providers.autoCompactWindowDesc')}

)}
)}
{/* Test connection */}
{testResult && (
{testResult.connectivity.success ? t('settings.providers.connectivityOk', { latency: String(testResult.connectivity.latencyMs) }) : t('settings.providers.connectivityFailed', { error: testResult.connectivity.error || '' })} {testResult.proxy && ( {testResult.proxy.success ? t('settings.providers.proxyOk', { latency: String(testResult.proxy.latencyMs) }) : t('settings.providers.proxyFailed', { error: testResult.proxy.error || '' })} )}
)}
{/* Settings JSON — editable, shown for all presets including official */}