// ======================================== // Settings Page // ======================================== // Application settings and configuration with CLI tools management import { useState, useCallback, useEffect } from 'react'; import { useIntl } from 'react-intl'; import { Settings, Moon, Bell, Cpu, RefreshCw, RotateCcw, Check, X, ChevronDown, ChevronUp, Languages, Plus, MessageSquareText, Monitor, Terminal, AlertTriangle, Package, Home, Folder, FolderOpen, Calendar, File, ArrowUpCircle, Save, } from 'lucide-react'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Badge } from '@/components/ui/Badge'; import { ThemeSelector } from '@/components/shared/ThemeSelector'; import { useTheme } from '@/hooks'; import { toast } from 'sonner'; import { useConfigStore, selectCliTools, selectDefaultCliTool, selectUserPreferences } from '@/stores/configStore'; import type { CliToolConfig, UserPreferences } from '@/types/store'; import { cn } from '@/lib/utils'; import { LanguageSwitcher } from '@/components/layout/LanguageSwitcher'; import { useChineseResponseStatus, useToggleChineseResponse, useWindowsPlatformStatus, useToggleWindowsPlatform, useCodexCliEnhancementStatus, useToggleCodexCliEnhancement, useRefreshCodexCliEnhancement, useCcwInstallStatus, useCliToolStatus, useCcwInstallations, useUpgradeCcwInstallation, } from '@/hooks/useSystemSettings'; import { RemoteNotificationSection } from '@/components/settings/RemoteNotificationSection'; import { A2UIPreferencesSection } from '@/components/settings/A2UIPreferencesSection'; // ========== File Path Input with Native File Picker ========== interface FilePathInputProps { value: string; onChange: (value: string) => void; placeholder: string; } function FilePathInput({ value, onChange, placeholder }: FilePathInputProps) { const handleBrowse = async () => { const { selectFile } = await import('@/lib/nativeDialog'); const initialDir = value ? value.replace(/[/\\][^/\\]*$/, '') : undefined; const selected = await selectFile(initialDir); if (selected) { onChange(selected); } }; return (
onChange(e.target.value)} placeholder={placeholder} className="flex-1" />
); } // ========== Tool Config File Helpers ========== /** Tools that use .env file for environment variables */ const ENV_FILE_TOOLS = new Set(['gemini', 'qwen', 'opencode']); /** Tools that use --settings for Claude CLI settings file */ const SETTINGS_FILE_TOOLS = new Set(['claude']); function getConfigFileType(toolId: string): 'envFile' | 'settingsFile' | 'none' { if (ENV_FILE_TOOLS.has(toolId)) return 'envFile'; if (SETTINGS_FILE_TOOLS.has(toolId)) return 'settingsFile'; return 'none'; } // ========== CLI Tool Card Component ========== interface CliToolCardProps { toolId: string; config: CliToolConfig; isDefault: boolean; isExpanded: boolean; toolAvailable?: boolean; isSaving?: boolean; onToggleExpand: () => void; onToggleEnabled: () => void; onSetDefault: () => void; onUpdateModel: (field: 'primaryModel' | 'secondaryModel', value: string) => void; onUpdateTags: (tags: string[]) => void; onUpdateAvailableModels: (models: string[]) => void; onUpdateEnvFile: (envFile: string | undefined) => void; onUpdateSettingsFile: (settingsFile: string | undefined) => void; onUpdateEffort: (effort: string | undefined) => void; onSaveToBackend: () => void; } function CliToolCard({ toolId, config, isDefault, isExpanded, toolAvailable, isSaving, onToggleExpand, onToggleEnabled, onSetDefault, onUpdateModel, onUpdateTags, onUpdateAvailableModels, onUpdateEnvFile, onUpdateSettingsFile, onUpdateEffort, onSaveToBackend, }: CliToolCardProps) { const { formatMessage } = useIntl(); // Local state for tag and model input const [tagInput, setTagInput] = useState(''); const [modelInput, setModelInput] = useState(''); // Handler for adding tags const handleAddTag = () => { const newTag = tagInput.trim(); if (newTag && !config.tags.includes(newTag)) { onUpdateTags([...config.tags, newTag]); setTagInput(''); } }; // Handler for removing tags const handleRemoveTag = (tagToRemove: string) => { onUpdateTags(config.tags.filter((t) => t !== tagToRemove)); }; // Handler for adding available models const handleAddModel = () => { const newModel = modelInput.trim(); const currentModels = config.availableModels || []; if (newModel && !currentModels.includes(newModel)) { onUpdateAvailableModels([...currentModels, newModel]); setModelInput(''); } }; // Handler for removing available models const handleRemoveModel = (modelToRemove: string) => { const currentModels = config.availableModels || []; onUpdateAvailableModels(currentModels.filter((m) => m !== modelToRemove)); }; // Predefined tags const predefinedTags = ['分析', 'Debug', 'implementation', 'refactoring', 'testing']; const configFileType = getConfigFileType(toolId); return ( {/* Header */}
{toolId} {isDefault && ( {formatMessage({ id: 'settings.cliTools.default' })} )} {config.type} {toolAvailable !== undefined && ( )}

{config.primaryModel}

{isExpanded ? ( ) : ( )}
{/* Tags */} {config.tags && config.tags.length > 0 && (
{config.tags.map((tag) => ( {tag} ))}
)}
{/* Expanded Content */} {isExpanded && (
onUpdateModel('primaryModel', e.target.value)} className="mt-1" />
onUpdateModel('secondaryModel', e.target.value)} className="mt-1" />
{/* Tags Section */}

{formatMessage({ id: 'apiSettings.cliSettings.tagsDescription' })}

{config.tags.map((tag) => ( {tag} ))} setTagInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleAddTag(); } }} placeholder={config.tags.length === 0 ? formatMessage({ id: 'apiSettings.cliSettings.tagInputPlaceholder' }) : ''} className="flex-1 min-w-[120px] bg-transparent border-0 outline-none text-sm placeholder:text-muted-foreground" />
{/* Predefined Tags */}
{formatMessage({ id: 'apiSettings.cliSettings.predefinedTags' })}: {predefinedTags.map((predefinedTag) => ( ))}
{/* Available Models Section */}
{(config.availableModels || []).map((model) => ( {model} ))} setModelInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); handleAddModel(); } }} placeholder={(config.availableModels || []).length === 0 ? formatMessage({ id: 'apiSettings.cliSettings.availableModelsPlaceholder' }) : ''} className="flex-1 min-w-[120px] bg-transparent border-0 outline-none text-sm placeholder:text-muted-foreground" />

{formatMessage({ id: 'apiSettings.cliSettings.availableModelsHint' })}

{/* Env File - for gemini/qwen/opencode */} {configFileType === 'envFile' && (
onUpdateEnvFile(v || undefined)} placeholder={formatMessage({ id: 'settings.cliTools.envFilePlaceholder' })} />

{formatMessage({ id: 'settings.cliTools.envFileHint' })}

)} {/* Settings File - for claude only */} {configFileType === 'settingsFile' && (
onUpdateSettingsFile(v || undefined)} placeholder={formatMessage({ id: 'apiSettings.cliSettings.settingsFilePlaceholder' })} />

{formatMessage({ id: 'apiSettings.cliSettings.settingsFileHint' })}

)} {/* Effort Level - for claude only */} {configFileType === 'settingsFile' && (
{(['low', 'medium', 'high'] as const).map((level) => { const effectiveEffort = config.effort || 'high'; const labelId = `settings.cliTools.effort${level.charAt(0).toUpperCase() + level.slice(1)}` as const; return ( ); })}

{formatMessage({ id: 'settings.cliTools.effortHint' })}

)} {/* Action Buttons */}
{!isDefault && config.enabled && ( )}
)}
); } // ========== Response Language Section ========== function ResponseLanguageSection() { const { formatMessage } = useIntl(); const { data: chineseStatus, isLoading: chineseLoading } = useChineseResponseStatus(); const { toggle: toggleChinese, isPending: chineseToggling } = useToggleChineseResponse(); const { data: windowsStatus, isLoading: windowsLoading } = useWindowsPlatformStatus(); const { toggle: toggleWindows, isPending: windowsToggling } = useToggleWindowsPlatform(); const { data: cliEnhStatus, isLoading: cliEnhLoading } = useCodexCliEnhancementStatus(); const { toggle: toggleCliEnh, isPending: cliEnhToggling } = useToggleCodexCliEnhancement(); const { refresh: refreshCliEnh, isPending: refreshing } = useRefreshCodexCliEnhancement(); return (

{formatMessage({ id: 'settings.sections.responseLanguage' })}

{/* Chinese Response - Claude */}
{formatMessage({ id: 'settings.responseLanguage.chineseClaude' })} Claude

{formatMessage({ id: 'settings.responseLanguage.chineseClaudeDesc' })}

{/* Chinese Response - Codex */}
{formatMessage({ id: 'settings.responseLanguage.chineseCodex' })} Codex

{formatMessage({ id: 'settings.responseLanguage.chineseCodexDesc' })}

{chineseStatus?.codexNeedsMigration && (

{formatMessage({ id: 'settings.responseLanguage.migrationWarning' })}

)}
{/* Windows Platform */}
{formatMessage({ id: 'settings.responseLanguage.windowsPlatform' })}

{formatMessage({ id: 'settings.responseLanguage.windowsPlatformDesc' })}

{/* CLI Enhancement - Codex */}
{formatMessage({ id: 'settings.responseLanguage.cliEnhancement' })} Codex
{cliEnhStatus?.enabled && ( )}

{formatMessage({ id: 'settings.responseLanguage.cliEnhancementDesc' })}

{cliEnhStatus?.enabled && (

{formatMessage({ id: 'settings.responseLanguage.cliEnhancementHint' })}

)}
); } // ========== Version Check Section ========== interface VersionData { currentVersion: string; latestVersion: string; hasUpdate: boolean; packageName: string; updateCommand: string; checkedAt: string; } function VersionCheckSection() { const { formatMessage } = useIntl(); const [versionData, setVersionData] = useState(null); const [checking, setChecking] = useState(false); const [error, setError] = useState(null); const [lastChecked, setLastChecked] = useState(null); const [autoCheck, setAutoCheck] = useState(() => { try { const saved = localStorage.getItem('ccw.autoUpdate'); return saved === null ? true : JSON.parse(saved); } catch { return true; } }); const checkVersion = async (silent = false) => { if (!silent) setChecking(true); setError(null); try { const response = await fetch('/api/version-check'); if (!response.ok) throw new Error(`HTTP ${response.status}`); const data: VersionData = await response.json(); if (!data.currentVersion) throw new Error('Invalid response'); setVersionData(data); setLastChecked(new Date()); } catch (err) { if (!silent) setError(err instanceof Error ? err.message : 'Unknown error'); } finally { setChecking(false); } }; useEffect(() => { // Initial check checkVersion(true); if (!autoCheck) return; const interval = setInterval(() => checkVersion(true), 60 * 60 * 1000); return () => clearInterval(interval); }, [autoCheck]); const toggleAutoCheck = (enabled: boolean) => { setAutoCheck(enabled); localStorage.setItem('ccw.autoUpdate', JSON.stringify(enabled)); }; return (

{formatMessage({ id: 'settings.versionCheck.title' })}

{/* Version info */}
{formatMessage({ id: 'settings.versionCheck.currentVersion' })} {versionData?.currentVersion ?? '...'}
{formatMessage({ id: 'settings.versionCheck.latestVersion' })} {versionData?.latestVersion ?? '...'}
{/* Status */} {versionData && (
{versionData.hasUpdate ? formatMessage({ id: 'settings.versionCheck.updateAvailable' }) : formatMessage({ id: 'settings.versionCheck.upToDate' })}
)} {error && (
{formatMessage({ id: 'settings.versionCheck.checkFailed' })}: {error}
)}
{/* Update action */} {versionData?.hasUpdate && (

{formatMessage({ id: 'settings.versionCheck.updateCommand' })}

{versionData.updateCommand}
)} {/* Auto check toggle + last checked */}
{formatMessage({ id: 'settings.versionCheck.lastChecked' })}:{' '} {lastChecked ? lastChecked.toLocaleTimeString() : formatMessage({ id: 'settings.versionCheck.never' })}
); } // ========== System Status Section ========== function SystemStatusSection() { const { formatMessage } = useIntl(); const { installations, isLoading, refetch } = useCcwInstallations(); const { upgrade, isPending: upgrading } = useUpgradeCcwInstallation(); const { data: ccwInstall } = useCcwInstallStatus(); return ( {/* Header */}

{formatMessage({ id: 'settings.systemStatus.title' })} {!isLoading && ( {installations.length} {formatMessage({ id: 'settings.systemStatus.installations' })} )}

{/* Installation cards */} {isLoading ? (
{formatMessage({ id: 'settings.systemStatus.checking' })}
) : installations.length === 0 ? (

{formatMessage({ id: 'settings.systemStatus.noInstallations' })}

ccw install
) : (
{installations.map((inst) => { const isGlobal = inst.installation_mode === 'Global'; const installDate = new Date(inst.installation_date).toLocaleDateString(); const version = inst.application_version !== 'unknown' ? inst.application_version : inst.installer_version; return (
{/* Mode + Version + Upgrade */}
{isGlobal ? : } {isGlobal ? formatMessage({ id: 'settings.systemStatus.global' }) : formatMessage({ id: 'settings.systemStatus.path' })} v{version}
{/* Path */}
{inst.installation_path}
{/* Date + Files */}
{installDate} {inst.files_count} {formatMessage({ id: 'settings.systemStatus.files' })}
); })} {/* Missing files warning */} {ccwInstall && !ccwInstall.installed && ccwInstall.missingFiles.length > 0 && (
{formatMessage({ id: 'settings.systemStatus.incomplete' })} — {ccwInstall.missingFiles.length} {formatMessage({ id: 'settings.systemStatus.missingFiles' }).toLowerCase()}
    {ccwInstall.missingFiles.slice(0, 4).map((f) => (
  • {f}
  • ))} {ccwInstall.missingFiles.length > 4 && (
  • +{ccwInstall.missingFiles.length - 4} more...
  • )}

{formatMessage({ id: 'settings.systemStatus.runToFix' })}:

ccw install
)}
)}
); } // ========== CLI Tools with Status Enhancement ========== interface CliToolsWithStatusProps { cliTools: Record; defaultCliTool: string; expandedTools: Set; savingTools: Set; onToggleExpand: (toolId: string) => void; onToggleEnabled: (toolId: string) => void; onSetDefault: (toolId: string) => void; onUpdateModel: (toolId: string, field: 'primaryModel' | 'secondaryModel', value: string) => void; onUpdateTags: (toolId: string, tags: string[]) => void; onUpdateAvailableModels: (toolId: string, models: string[]) => void; onUpdateEnvFile: (toolId: string, envFile: string | undefined) => void; onUpdateSettingsFile: (toolId: string, settingsFile: string | undefined) => void; onUpdateEffort: (toolId: string, effort: string | undefined) => void; onSaveToBackend: (toolId: string) => void; formatMessage: ReturnType['formatMessage']; } function CliToolsWithStatus({ cliTools, defaultCliTool, expandedTools, savingTools, onToggleExpand, onToggleEnabled, onSetDefault, onUpdateModel, onUpdateTags, onUpdateAvailableModels, onUpdateEnvFile, onUpdateSettingsFile, onUpdateEffort, onSaveToBackend, formatMessage, }: CliToolsWithStatusProps) { const { data: toolStatus } = useCliToolStatus(); return ( <>

{formatMessage({ id: 'settings.cliTools.description' })} {defaultCliTool}

{Object.entries(cliTools).map(([toolId, config]) => { const status = toolStatus?.[toolId]; return ( onToggleExpand(toolId)} onToggleEnabled={() => onToggleEnabled(toolId)} onSetDefault={() => onSetDefault(toolId)} onUpdateModel={(field, value) => onUpdateModel(toolId, field, value)} onUpdateTags={(tags) => onUpdateTags(toolId, tags)} onUpdateAvailableModels={(models) => onUpdateAvailableModels(toolId, models)} onUpdateEnvFile={(envFile) => onUpdateEnvFile(toolId, envFile)} onUpdateSettingsFile={(settingsFile) => onUpdateSettingsFile(toolId, settingsFile)} onUpdateEffort={(effort) => onUpdateEffort(toolId, effort)} onSaveToBackend={() => onSaveToBackend(toolId)} /> ); })}
); } // ========== Main Page Component ========== export function SettingsPage() { const { formatMessage } = useIntl(); const { theme, setTheme } = useTheme(); const cliTools = useConfigStore(selectCliTools); const defaultCliTool = useConfigStore(selectDefaultCliTool); const userPreferences = useConfigStore(selectUserPreferences); const { updateCliTool, setDefaultCliTool, setUserPreferences, resetUserPreferences } = useConfigStore(); const [expandedTools, setExpandedTools] = useState>(new Set()); const [savingTools, setSavingTools] = useState>(new Set()); const toggleToolExpand = (toolId: string) => { setExpandedTools((prev) => { const next = new Set(prev); if (next.has(toolId)) { next.delete(toolId); } else { next.add(toolId); } return next; }); }; const handleToggleToolEnabled = (toolId: string) => { updateCliTool(toolId, { enabled: !cliTools[toolId].enabled }); }; const handleSetDefaultTool = (toolId: string) => { setDefaultCliTool(toolId); }; const handleUpdateModel = (toolId: string, field: 'primaryModel' | 'secondaryModel', value: string) => { updateCliTool(toolId, { [field]: value }); }; const handleUpdateTags = (toolId: string, tags: string[]) => { updateCliTool(toolId, { tags }); }; const handleUpdateAvailableModels = (toolId: string, availableModels: string[]) => { updateCliTool(toolId, { availableModels }); }; const handleUpdateEnvFile = (toolId: string, envFile: string | undefined) => { updateCliTool(toolId, { envFile }); }; const handleUpdateSettingsFile = (toolId: string, settingsFile: string | undefined) => { updateCliTool(toolId, { settingsFile }); }; const handleUpdateEffort = (toolId: string, effort: string | undefined) => { updateCliTool(toolId, { effort }); }; // Save tool config to backend (~/.claude/cli-tools.json) const handleSaveToBackend = useCallback(async (toolId: string) => { const config = cliTools[toolId]; if (!config) return; setSavingTools((prev) => new Set(prev).add(toolId)); try { const body: Record = { enabled: config.enabled, primaryModel: config.primaryModel, secondaryModel: config.secondaryModel, tags: config.tags, availableModels: config.availableModels, }; // Only include the relevant config file field const configFileType = getConfigFileType(toolId); if (configFileType === 'envFile') { body.envFile = config.envFile || null; } else if (configFileType === 'settingsFile') { body.settingsFile = config.settingsFile || null; body.effort = config.effort || null; } const res = await fetch(`/api/cli/config/${toolId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) { throw new Error(`HTTP ${res.status}`); } toast.success(formatMessage({ id: 'settings.cliTools.configSaved' }), { description: toolId, }); } catch { toast.error(formatMessage({ id: 'settings.cliTools.configSaveError' }), { description: toolId, }); } finally { setSavingTools((prev) => { const next = new Set(prev); next.delete(toolId); return next; }); } }, [cliTools, formatMessage]); const handlePreferenceChange = (key: keyof UserPreferences, value: unknown) => { setUserPreferences({ [key]: value }); }; return (
{/* Page Header */}

{formatMessage({ id: 'settings.title' })}

{formatMessage({ id: 'settings.description' })}

{/* Appearance Settings */}

{formatMessage({ id: 'settings.sections.appearance' })}

{/* Multi-Theme Selector */}

{formatMessage({ id: 'settings.appearance.theme' })}

{formatMessage({ id: 'settings.appearance.description' })}

{/* System Theme Toggle (Backward Compatibility) */}

{formatMessage({ id: 'settings.appearance.systemFollow' })}

{formatMessage({ id: 'settings.appearance.systemFollowDesc' })}

{/* Language Settings */}

{formatMessage({ id: 'settings.sections.language' })}

{formatMessage({ id: 'settings.language.displayLanguage' })}

{formatMessage({ id: 'settings.language.chooseLanguage' })}

{/* Response Language Settings */} {/* A2UI Preferences */} {/* System Status */} {/* Version Check */} {/* CLI Tools Configuration */}

{formatMessage({ id: 'settings.sections.cliTools' })}

{/* Data Refresh Settings */}

{formatMessage({ id: 'settings.dataRefresh.title' })}

{formatMessage({ id: 'settings.dataRefresh.autoRefresh' })}

{formatMessage({ id: 'settings.dataRefresh.autoRefreshDesc' })}

{userPreferences.autoRefresh && (

{formatMessage({ id: 'settings.dataRefresh.refreshInterval' })}

{formatMessage({ id: 'settings.dataRefresh.refreshIntervalDesc' })}

{[15000, 30000, 60000, 120000].map((interval) => ( ))}
)}
{/* Notifications */}

{formatMessage({ id: 'settings.notifications.title' })}

{formatMessage({ id: 'settings.notifications.enableNotifications' })}

{formatMessage({ id: 'settings.notifications.enableNotificationsDesc' })}

{formatMessage({ id: 'settings.notifications.soundEffects' })}

{formatMessage({ id: 'settings.notifications.soundEffectsDesc' })}

{/* Remote Notifications */} {/* Reset Settings */}

{formatMessage({ id: 'common.actions.reset' })}

{formatMessage({ id: 'settings.reset.description' })}

); } export default SettingsPage;