import { useState, useEffect, useMemo, useRef, type CSSProperties, type ReactNode } from 'react' import QRCode from 'qrcode' import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, type DragEndEvent, } from '@dnd-kit/core' import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { Copy, Eye, EyeOff, GripVertical, PowerOff, QrCode, RotateCw } from 'lucide-react' import { useSettingsStore, UI_ZOOM_DEFAULT, UI_ZOOM_MIN, UI_ZOOM_MAX, UI_ZOOM_STEP } from '../stores/settingsStore' import { useProviderStore } from '../stores/providerStore' import { useTranslation, type TranslationKey } 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 { PermissionModeSelector } from '../components/controls/PermissionModeSelector' import type { ThemeMode, UpdateProxyMode, NetworkProxyMode, WebSearchMode, AppMode, ChatSendBehavior, OutputStyleSource } from '../types/settings' import type { Locale } from '../i18n' import type { SavedProvider, UpdateProviderInput, ProviderTestResult, ModelMapping, Model1mSupport, 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 { TraceList } from './TraceList' import { ActivitySettings } from './ActivitySettings' import { MemorySettings } from './MemorySettings' import { useUIStore } from '../stores/uiStore' import { ClaudeOfficialLogin } from '../components/settings/ClaudeOfficialLogin' import { ChatGPTOfficialLogin } from '../components/settings/ChatGPTOfficialLogin' import { BUILT_IN_PROVIDER_IDS, CLAUDE_OFFICIAL_PROVIDER_ID, OPENAI_OFFICIAL_PROVIDER_ID, } from '../constants/openaiOfficialProvider' import { useUpdateStore } from '../stores/updateStore' import { getBaseUrl } from '../api/client' import { formatBytes } from '../lib/formatBytes' import { isDesktopRuntime } from '../lib/desktopRuntime' import { getDesktopHost } from '../lib/desktopHost' import { publicAssetPath } from '../lib/publicAsset' import { getDesktopNotificationPermission, notifyDesktop, getDesktopNotificationPlatform, openDesktopNotificationSettings, requestDesktopNotificationPermission, type DesktopNotificationPermission, } from '../lib/desktopNotifications' import { API_KEY_JSON_PLACEHOLDER, maskSettingsJsonSecrets, restoreSettingsJsonSecrets, stripProviderSettingsJsonEnv, } from '../lib/providerSettingsJson' import { copyTextToClipboard } from '../components/chat/clipboard' const NETWORK_TIMEOUT_MIN_SECONDS = 30 const NETWORK_TIMEOUT_MAX_SECONDS = 1800 const NETWORK_TIMEOUT_STEP_SECONDS = 30 const SETTINGS_CHECKBOX_INPUT_CLASS = 'settings-checkbox-input peer' const BUILT_IN_OUTPUT_STYLE_TRANSLATION_KEYS = { default: { label: 'settings.general.outputStyleBuiltin.default.label', description: 'settings.general.outputStyleBuiltin.default.description', }, Explanatory: { label: 'settings.general.outputStyleBuiltin.explanatory.label', description: 'settings.general.outputStyleBuiltin.explanatory.description', }, Learning: { label: 'settings.general.outputStyleBuiltin.learning.label', description: 'settings.general.outputStyleBuiltin.learning.description', }, } satisfies Record 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 } } function isLanH5BaseUrl(url: URL): boolean { return url.protocol === 'http:' && !!url.port && ( url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname.startsWith('10.') || url.hostname.startsWith('192.168.') || /^172\.(1[6-9]|2\d|3[0-1])\./.test(url.hostname) || url.hostname.startsWith('169.254.') ) } function extractH5AccessAddressDraft(baseUrl: string | null): string { if (!baseUrl) return '' try { const url = new URL(baseUrl) return isLanH5BaseUrl(url) ? url.hostname : baseUrl } catch { return baseUrl } } function extractHostnameFromUrl(value: string | null): string | null { if (!value) return null try { return new URL(value).hostname || null } catch { return null } } function extractH5AccessPort(baseUrl: string | null): string | null { if (!baseUrl) return null try { const url = new URL(baseUrl) return url.port || null } catch { return null } } // Mirrors the server-side fixedPort range (h5AccessService MIN/MAX_FIXED_PORT). function parseH5FixedPortDraft(draft: string): number | null | 'invalid' { const trimmed = draft.trim() if (!trimmed) return null if (!/^\d{1,5}$/.test(trimmed)) return 'invalid' const port = Number(trimmed) return port >= 1024 && port <= 65535 ? port : 'invalid' } // Mirrors the server-side disconnect grace range (h5AccessService // MIN/MAX_DISCONNECT_GRACE_SECONDS). Empty = use the built-in 30s default. function parseH5GraceDraft(draft: string): number | null | 'invalid' { const trimmed = draft.trim() if (!trimmed) return null if (!/^\d{1,5}$/.test(trimmed)) return 'invalid' const seconds = Number(trimmed) return seconds >= 5 && seconds <= 86400 ? seconds : 'invalid' } function buildH5PublicBaseUrlFromHostDraft(draft: string, currentBaseUrl: string | null): string | null { const trimmed = draft.trim() if (!trimmed) return null if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return trimmed try { const current = currentBaseUrl ? new URL(currentBaseUrl) : null if (!current) return trimmed const port = current.port ? `:${current.port}` : '' const path = current.pathname === '/' ? '' : current.pathname.replace(/\/+$/, '') return `${current.protocol}//${trimmed}${port}${path}` } catch { return trimmed } } export function Settings() { const activeTab = useUIStore((s) => s.activeSettingsTab) const setActiveTab = useUIStore((s) => s.setActiveSettingsTab) const pendingSettingsTab = useUIStore((s) => s.pendingSettingsTab) const t = useTranslation() useEffect(() => { if (!pendingSettingsTab) return setActiveTab(pendingSettingsTab) useUIStore.getState().setPendingSettingsTab(null) }, [pendingSettingsTab, setActiveTab]) return (
{/* Tab navigation */}
setActiveTab('providers')} /> setActiveTab('general')} /> setActiveTab('h5Access')} /> setActiveTab('adapters')} /> setActiveTab('terminal')} /> setActiveTab('mcp')} /> setActiveTab('agents')} /> setActiveTab('skills')} /> setActiveTab('memory')} /> setActiveTab('plugins')} /> setActiveTab('computerUse')} /> setActiveTab('activity')} /> setActiveTab('trace')} /> setActiveTab('diagnostics')} />
setActiveTab('about')} />
{/* Tab content; trace embeds a full-bleed page that manages its own scroll */}
{activeTab === 'providers' && } {activeTab === 'activity' && } {activeTab === 'general' && } {activeTab === 'h5Access' && } {activeTab === 'adapters' && } {activeTab === 'terminal' && } {activeTab === 'mcp' && } {activeTab === 'agents' && } {activeTab === 'skills' && } {activeTab === 'memory' && } {activeTab === 'plugins' && } {activeTab === 'computerUse' && } {activeTab === 'trace' && } {activeTab === 'diagnostics' && } {activeTab === 'about' && }
) } function TabButton({ icon, label, active, onClick }: { icon: string; label: string; active: boolean; onClick: () => void }) { return ( ) } // ─── Provider Settings ────────────────────────────────────── type ProviderListItem = | { id: typeof CLAUDE_OFFICIAL_PROVIDER_ID; kind: 'claude-official' } | { id: typeof OPENAI_OFFICIAL_PROVIDER_ID; kind: 'openai-official' } | { id: string; kind: 'saved'; provider: SavedProvider } function defaultProviderOrder(providers: SavedProvider[]): string[] { return [ ...providers.map((provider) => provider.id), ...BUILT_IN_PROVIDER_IDS, ] } function normalizeProviderOrder(providerOrder: string[] | undefined, providers: SavedProvider[]): string[] { const knownIds = new Set(defaultProviderOrder(providers)) const seen = new Set() const order: string[] = [] const source = providerOrder && providerOrder.length > 0 ? providerOrder : defaultProviderOrder(providers) for (const id of source) { if (!knownIds.has(id) || seen.has(id)) continue seen.add(id) order.push(id) } for (const id of defaultProviderOrder(providers)) { if (seen.has(id)) continue seen.add(id) order.push(id) } return order } function buildProviderListItems( providers: SavedProvider[], providerOrder: string[] | undefined, ): ProviderListItem[] { const savedItems = new Map( providers.map((provider) => [ provider.id, { id: provider.id, kind: 'saved', provider } satisfies ProviderListItem, ]), ) const items = new Map([ [CLAUDE_OFFICIAL_PROVIDER_ID, { id: CLAUDE_OFFICIAL_PROVIDER_ID, kind: 'claude-official' }], [OPENAI_OFFICIAL_PROVIDER_ID, { id: OPENAI_OFFICIAL_PROVIDER_ID, kind: 'openai-official' }], ...savedItems, ]) return normalizeProviderOrder(providerOrder, providers) .map((id) => items.get(id)) .filter((item): item is ProviderListItem => item !== undefined) } function providerItemTestId(item: ProviderListItem): string { switch (item.kind) { case 'claude-official': return 'claude-official-provider' case 'openai-official': return 'openai-official-provider' case 'saved': return `provider-${item.provider.id}` } } function ProviderSettings() { const { providers, providerOrder, activeId, hasLoadedProviders, presets, isLoading, isPresetsLoading, fetchProviders, fetchPresets, deleteProvider, reorderProviders, 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>({}) const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 6 }, }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ) 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 providerItems = useMemo( () => buildProviderListItems(providers, providerOrder), [providerOrder, providers], ) const handleProviderDragEnd = (event: DragEndEvent) => { const { active, over } = event if (!over || active.id === over.id) return const ids = providerItems.map((item) => item.id) const oldIndex = ids.indexOf(String(active.id)) const newIndex = ids.indexOf(String(over.id)) if (oldIndex === -1 || newIndex === -1) return void reorderProviders(arrayMove(ids, oldIndex, newIndex)) } const isClaudeOfficialActive = hasLoadedProviders && activeId === null const isOpenAIOfficialActive = hasLoadedProviders && activeId === OPENAI_OFFICIAL_PROVIDER_ID return (

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

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

item.id)} strategy={verticalListSortingStrategy} >
{providerItems.map((item) => { if (item.kind === 'claude-official') { return ( {t('settings.providers.default')} ) : null} details={isClaudeOfficialActive ? (
) : null} /> ) } if (item.kind === 'openai-official') { return ( handleActivate(OPENAI_OFFICIAL_PROVIDER_ID) : undefined} title={t('settings.providers.openaiOfficialName')} subtitle={t('settings.providers.openaiOfficialDesc')} badges={isOpenAIOfficialActive ? ( {t('settings.providers.default')} ) : null} details={isOpenAIOfficialActive ? (
) : null} /> ) } const provider = item.provider const isActive = activeId === provider.id const test = testResults[provider.id] const preset = presetMap.get(provider.presetId) return ( handleActivate(provider.id) : undefined} title={provider.name} subtitle={`${provider.baseUrl} · ${provider.models.main}`} badges={( <> {preset && preset.id !== 'custom' && ( {preset.name} )} {provider.apiFormat && provider.apiFormat !== 'anthropic' && ( {provider.apiFormat === 'openai_chat' ? 'OpenAI Chat' : 'OpenAI Responses'} )} {isActive && ( {t('settings.providers.default')} )} )} result={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 || '' })} )}
) : null} actions={( <> {!isActive && ( )} {!isActive && ( )} )} /> ) })}
{isLoading && providers.length === 0 ? (
) : null} {/* 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} />
) } type SortableProviderCardProps = { item: ProviderListItem isActive: boolean dragLabel: string title: ReactNode subtitle: ReactNode badges?: ReactNode result?: ReactNode actions?: ReactNode details?: ReactNode onActivate?: () => void } function SortableProviderCard({ item, isActive, dragLabel, title, subtitle, badges, result, actions, details, onActivate, }: SortableProviderCardProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: item.id }) const style: CSSProperties = { transform: CSS.Transform.toString(transform), transition, zIndex: isDragging ? 20 : undefined, } return (
{actions && (
{actions}
)}
{details}
) } // ─── 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 DISABLE_EXPERIMENTAL_BETAS_ENV_KEY = 'CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS' const MODEL_CONTEXT_WINDOW_MIN = 16000 const MODEL_CONTEXT_WINDOW_MAX = 10000000 const MODEL_1M_CONTEXT_WINDOW = 1000000 const MODEL_SLOTS = ['main', 'haiku', 'sonnet', 'opus'] as const const DEFAULT_MODEL_1M_SUPPORT: Model1mSupport = { main: false, haiku: false, sonnet: false, opus: false, } 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 hasModel1mMarker(model: string): boolean { return /\[1m\]$/i.test(model.trim()) || /:1m$/i.test(model.trim()) } function stripModel1mMarker(model: string): string { return model.trim().replace(/\[1m\]$/i, '').replace(/:1m$/i, '').trim() } function stripModel1mMarkers(models: ModelMapping): ModelMapping { return { main: stripModel1mMarker(models.main), haiku: stripModel1mMarker(models.haiku), sonnet: stripModel1mMarker(models.sonnet), opus: stripModel1mMarker(models.opus), } } function getInitialModel1mSupport( models: ModelMapping, provider?: SavedProvider, ): Model1mSupport { return { main: provider?.model1mSupport?.main === true || hasModel1mMarker(models.main), haiku: provider?.model1mSupport?.haiku === true || hasModel1mMarker(models.haiku), sonnet: provider?.model1mSupport?.sonnet === true || hasModel1mMarker(models.sonnet), opus: provider?.model1mSupport?.opus === true || hasModel1mMarker(models.opus), } } function applyModel1mSupport(model: string, enabled: boolean): string { const stripped = stripModel1mMarker(model) return enabled && stripped ? `${stripped}[1m]` : stripped } function applyModel1mSupportMapping( models: ModelMapping, model1mSupport: Model1mSupport, ): ModelMapping { return { main: applyModel1mSupport(models.main, model1mSupport.main), haiku: applyModel1mSupport(models.haiku, model1mSupport.haiku), sonnet: applyModel1mSupport(models.sonnet, model1mSupport.sonnet), opus: applyModel1mSupport(models.opus, model1mSupport.opus), } } function hasAnyModel1mSupport(model1mSupport: Model1mSupport): boolean { return MODEL_SLOTS.some((slot) => model1mSupport[slot]) } function shouldFill1mContextWindow(value: string): boolean { const parsed = parseModelContextWindowsInput(value) return parsed === undefined || parsed < MODEL_1M_CONTEXT_WINDOW } function apply1mSupportToContextInput( inputs: ModelContextInputs, slot: ModelSlot, enabled: boolean, ): ModelContextInputs { if (!enabled || !shouldFill1mContextWindow(inputs[slot])) return inputs return { ...inputs, [slot]: String(MODEL_1M_CONTEXT_WINDOW) } } function apply1mSupportToContextInputs( inputs: ModelContextInputs, model1mSupport: Model1mSupport, ): ModelContextInputs { let nextInputs = inputs for (const slot of MODEL_SLOTS) { nextInputs = apply1mSupportToContextInput(nextInputs, slot, model1mSupport[slot]) } return nextInputs } function normalizeModelMapping(models: ModelMapping): ModelMapping { const main = models.main.trim() return { main, haiku: models.haiku.trim() || main, sonnet: models.sonnet.trim() || main, opus: models.opus.trim() || main, } } function readSettingsEnvString(env: Record, key: string): string | undefined { const value = env[key] if (typeof value !== 'string') return undefined const trimmed = value.trim() return trimmed || undefined } function readModelMappingFromSettingsEnv(env: Record): Partial { const haiku = readSettingsEnvString(env, 'ANTHROPIC_DEFAULT_HAIKU_MODEL') const sonnet = readSettingsEnvString(env, 'ANTHROPIC_DEFAULT_SONNET_MODEL') const opus = readSettingsEnvString(env, 'ANTHROPIC_DEFAULT_OPUS_MODEL') const main = readSettingsEnvString(env, 'ANTHROPIC_MODEL') ?? sonnet ?? haiku ?? opus return { ...(main ? { main } : {}), ...(haiku ? { haiku } : {}), ...(sonnet ? { sonnet } : {}), ...(opus ? { opus } : {}), } } function applyToolSearchEnv( env: Record, apiFormat: ApiFormat, toolSearchEnabled: boolean, ): void { delete env.ENABLE_TOOL_SEARCH if (apiFormat === 'anthropic') { env.ENABLE_TOOL_SEARCH = toolSearchEnabled ? 'true' : 'false' } } function applyDisableExperimentalBetasEnv( env: Record, disableExperimentalBetas: boolean, ): void { if (disableExperimentalBetas) { env[DISABLE_EXPERIMENTAL_BETAS_ENV_KEY] = '1' } else { delete env[DISABLE_EXPERIMENTAL_BETAS_ENV_KEY] } } function updateSettingsJsonToolSearch( raw: string, apiFormat: ApiFormat, toolSearchEnabled: boolean, ): 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 } applyToolSearchEnv(env, apiFormat, toolSearchEnabled) parsed.env = env return JSON.stringify(parsed, null, 2) } catch { return raw } } function updateSettingsJsonDisableExperimentalBetas( raw: string, disableExperimentalBetas: boolean, ): 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 } applyDisableExperimentalBetasEnv(env, disableExperimentalBetas) parsed.env = env return JSON.stringify(parsed, null, 2) } catch { return raw } } function readToolSearchEnabledFromEnv(env: Record): boolean { const value = env.ENABLE_TOOL_SEARCH if (typeof value === 'boolean') return value if (typeof value === 'number') return value !== 0 if (typeof value === 'string') { const normalized = value.trim().toLowerCase() if (['0', 'false', 'off', 'no'].includes(normalized)) return false if (['1', 'true', 'on', 'yes', 'auto'].includes(normalized) || normalized.startsWith('auto:')) { return true } } return true } function readDisableExperimentalBetasFromEnv(env: Record): boolean { const value = env[DISABLE_EXPERIMENTAL_BETAS_ENV_KEY] if (typeof value === 'boolean') return value if (typeof value === 'number') return value !== 0 if (typeof value === 'string') { const normalized = value.trim().toLowerCase() if (['0', 'false', 'off', 'no'].includes(normalized)) return false if (['1', 'true', 'on', 'yes'].includes(normalized)) return true } return false } 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, model1mSupport: Model1mSupport = DEFAULT_MODEL_1M_SUPPORT, ): 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 runtimeModels = applyModel1mSupportMapping(models, model1mSupport) parsed.env = { ...existingEnv, ANTHROPIC_MODEL: runtimeModels.main, ANTHROPIC_DEFAULT_HAIKU_MODEL: runtimeModels.haiku, ANTHROPIC_DEFAULT_SONNET_MODEL: runtimeModels.sonnet, ANTHROPIC_DEFAULT_OPUS_MODEL: runtimeModels.opus, } return JSON.stringify(parsed, null, 2) } catch { return raw } } function updateSettingsJsonProviderConnection( raw: string, apiFormat: ApiFormat, authStrategy: ProviderAuthStrategy, apiKey: string, preset: ProviderPreset, baseUrl: string, proxyBaseUrl: string, toolSearchEnabled = true, disableExperimentalBetas = false, ): 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 applyToolSearchEnv(env, apiFormat, toolSearchEnabled) applyDisableExperimentalBetasEnv(env, disableExperimentalBetas) env.ANTHROPIC_BASE_URL = apiFormat !== 'anthropic' ? proxyBaseUrl : baseUrl Object.assign(env, buildSettingsJsonAuthEnv(apiFormat, authStrategy, apiKey, preset)) parsed.env = env return JSON.stringify(parsed, null, 2) } catch { return raw } } function getProviderProxyBaseUrl(): string { return `${getBaseUrl().replace(/\/$/, '')}/proxy` } 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) { void getDesktopHost().shell.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 initialModels = stripModel1mMarkers(provider?.models ?? initialPreset.defaultModels) const initialModel1mSupport = getInitialModel1mSupport( provider?.models ?? initialPreset.defaultModels, provider, ) const initialModelContextInputs = apply1mSupportToContextInputs( getModelContextInputs(initialModels, initialPreset, provider), initialModel1mSupport, ) 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(initialModels) const [model1mSupport, setModel1mSupport] = useState(initialModel1mSupport) const [modelContextInputs, setModelContextInputs] = useState(initialModelContextInputs) const [autoCompactWindow, setAutoCompactWindow] = useState( provider?.autoCompactWindow !== undefined ? String(provider.autoCompactWindow) : getPresetAutoCompactWindow(initialPreset), ) const [toolSearchEnabled, setToolSearchEnabled] = useState(provider?.toolSearchEnabled ?? true) const [disableExperimentalBetas, setDisableExperimentalBetas] = useState(provider?.disableExperimentalBetas ?? false) 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) const providerProxyBaseUrl = useMemo(() => getProviderProxyBaseUrl(), []) // 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 normalizedModels = normalizeModelMapping(models) const runtimeModels = applyModel1mSupportMapping(normalizedModels, model1mSupport) const existingEnv = (settings.env as Record) || {} const cleanedEnv = stripProviderSettingsJsonEnv(existingEnv, presetDefaultEnvKeys) const mergedEnv: Record = { ...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 ? providerProxyBaseUrl : baseUrl, ...buildSettingsJsonAuthEnv(apiFormat, authStrategy, apiKey, selectedPreset), ANTHROPIC_MODEL: runtimeModels.main, ANTHROPIC_DEFAULT_HAIKU_MODEL: runtimeModels.haiku, ANTHROPIC_DEFAULT_SONNET_MODEL: runtimeModels.sonnet, ANTHROPIC_DEFAULT_OPUS_MODEL: runtimeModels.opus, } applyToolSearchEnv(mergedEnv, apiFormat, toolSearchEnabled) applyDisableExperimentalBetasEnv(mergedEnv, disableExperimentalBetas) const merged = { ...settings, skipWebFetchPreflight: settings.skipWebFetchPreflight ?? true, env: mergedEnv, } setSettingsJson(JSON.stringify(merged, null, 2)) }).catch(() => { setSettingsJson(JSON.stringify({}, null, 2)) }) }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedPreset.id, providerProxyBaseUrl]) const handlePresetChange = (preset: ProviderPreset) => { setSelectedPreset(preset) setName(preset.name) setBaseUrl(preset.baseUrl) setApiFormat(preset.apiFormat ?? 'anthropic') setAuthStrategy(getPresetAuthStrategy(preset)) const nextModels = stripModel1mMarkers(preset.defaultModels) const nextModel1mSupport = getInitialModel1mSupport(preset.defaultModels) const nextModelContextInputs = apply1mSupportToContextInputs( getModelContextInputs(nextModels, preset), nextModel1mSupport, ) setModels(nextModels) setModel1mSupport(nextModel1mSupport) setModelContextInputs(nextModelContextInputs) setAutoCompactWindow(getPresetAutoCompactWindow(preset)) setToolSearchEnabled(true) setDisableExperimentalBetas(false) 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 toolSearchUnsupported = apiFormat !== 'anthropic' const toolSearchDescription = toolSearchUnsupported ? t('settings.providers.toolSearchUnsupported') : t('settings.providers.toolSearchDesc') 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, providerProxyBaseUrl, toolSearchEnabled, disableExperimentalBetas)) } const handleApiKeyChange = (value: string) => { setApiKey(value) setSettingsJson((current) => updateSettingsJsonProviderConnection(current, apiFormat, authStrategy, value, selectedPreset, baseUrl, providerProxyBaseUrl, toolSearchEnabled, disableExperimentalBetas)) } const handleApiFormatChange = (value: ApiFormat) => { setApiFormat(value) setSettingsJson((current) => updateSettingsJsonProviderConnection(current, value, authStrategy, apiKey, selectedPreset, baseUrl, providerProxyBaseUrl, toolSearchEnabled, disableExperimentalBetas)) } const handleAuthStrategyChange = (value: ProviderAuthStrategy) => { setAuthStrategy(value) setSettingsJson((current) => updateSettingsJsonProviderConnection(current, apiFormat, value, apiKey, selectedPreset, baseUrl, providerProxyBaseUrl, toolSearchEnabled, disableExperimentalBetas)) } const handleToolSearchToggle = (enabled: boolean) => { if (toolSearchUnsupported) return setToolSearchEnabled(enabled) setSettingsJson((current) => updateSettingsJsonToolSearch(current, apiFormat, enabled)) } const handleDisableExperimentalBetasToggle = (disabled: boolean) => { setDisableExperimentalBetas(disabled) setSettingsJson((current) => updateSettingsJsonDisableExperimentalBetas(current, disabled)) } const handleModelChange = (slot: ModelSlot, value: string) => { const hasMarker = hasModel1mMarker(value) const nextModels = { ...models, [slot]: stripModel1mMarker(value) } const nextModel1mSupport = hasMarker ? { ...model1mSupport, [slot]: true } : model1mSupport const nextInputs = { ...modelContextInputs, [slot]: getModelContextInputValue(nextModels[slot], selectedPreset, provider), } const nextInputsWith1mSupport = apply1mSupportToContextInput( nextInputs, slot, nextModel1mSupport[slot], ) setModels(nextModels) setModel1mSupport(nextModel1mSupport) setModelContextInputs(nextInputsWith1mSupport) setSettingsJson((current) => updateSettingsJsonModelContextWindows( updateSettingsJsonModels(current, normalizeModelMapping(nextModels), nextModel1mSupport), buildModelContextWindows(nextModels, nextInputsWith1mSupport), )) } const handleModel1mSupportChange = (slot: ModelSlot, enabled: boolean) => { const nextModel1mSupport = { ...model1mSupport, [slot]: enabled } const nextInputs = apply1mSupportToContextInput(modelContextInputs, slot, enabled) setModel1mSupport(nextModel1mSupport) setModelContextInputs(nextInputs) setSettingsJson((current) => updateSettingsJsonModelContextWindows( updateSettingsJsonModels(current, normalizeModelMapping(models), nextModel1mSupport), buildModelContextWindows(models, 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 || isSubmitting) return const normalizedModels = normalizeModelMapping(models) const parsedAutoCompactWindow = parseAutoCompactWindowInput(autoCompactWindow) const parsedModelContextWindows = buildModelContextWindows(models, modelContextInputs) const storedModel1mSupport = hasAnyModel1mSupport(model1mSupport) ? model1mSupport : undefined 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: normalizedModels, ...(storedModel1mSupport !== undefined && { model1mSupport: storedModel1mSupport }), ...(parsedAutoCompactWindow !== undefined && { autoCompactWindow: parsedAutoCompactWindow }), ...(Object.keys(parsedModelContextWindows).length > 0 && { modelContextWindows: parsedModelContextWindows }), toolSearchEnabled, ...(disableExperimentalBetas && { disableExperimentalBetas }), notes: notes.trim() || undefined, }) } else if (provider) { const input: UpdateProviderInput = { name: name.trim(), baseUrl: baseUrl.trim(), authStrategy, apiFormat, models: normalizedModels, model1mSupport: storedModel1mSupport ?? null, autoCompactWindow: parsedAutoCompactWindow ?? null, modelContextWindows: Object.keys(parsedModelContextWindows).length > 0 ? parsedModelContextWindows : null, toolSearchEnabled, disableExperimentalBetas, 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 handleClose = () => { if (isSubmitting) return onClose() } 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 */}
{MODEL_SLOTS.map((slot) => { const labelKey = slot === 'main' ? 'settings.providers.mainModel' : slot === 'haiku' ? 'settings.providers.haikuModel' : slot === 'sonnet' ? 'settings.providers.sonnetModel' : 'settings.providers.opusModel' const label = t(labelKey) return (
handleModelChange(slot, e.target.value)} placeholder={slot === 'main' ? t('settings.providers.modelIdPlaceholder') : 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 */}