import React, { useState, useEffect } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { ArrowLeft, Plus, Trash2, Save, AlertCircle, Loader2, BarChart3, Shield, Trash, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Card } from "@/components/ui/card"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { api, type ClaudeSettings, type ClaudeInstallation } from "@/lib/api"; import { cn } from "@/lib/utils"; import { Toast, ToastContainer } from "@/components/ui/toast"; import { ClaudeVersionSelector } from "./ClaudeVersionSelector"; import { StorageTab } from "./StorageTab"; import { HooksEditor } from "./HooksEditor"; import { SlashCommandsManager } from "./SlashCommandsManager"; import { ProxySettings } from "./ProxySettings"; import { AnalyticsConsent } from "./AnalyticsConsent"; import { useTheme, useTrackEvent, useTranslation } from "@/hooks"; import { analytics } from "@/lib/analytics"; interface SettingsProps { /** * Callback to go back to the main view */ onBack: () => void; /** * Optional className for styling */ className?: string; } interface PermissionRule { id: string; value: string; } interface EnvironmentVariable { id: string; key: string; value: string; } /** * Comprehensive Settings UI for managing Claude Code settings * Provides a no-code interface for editing the settings.json file */ export const Settings: React.FC = ({ onBack, className, }) => { const { t } = useTranslation(); const [settings, setSettings] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState("general"); const [currentBinaryPath, setCurrentBinaryPath] = useState(null); const [selectedInstallation, setSelectedInstallation] = useState(null); const [binaryPathChanged, setBinaryPathChanged] = useState(false); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); // Permission rules state const [allowRules, setAllowRules] = useState([]); const [denyRules, setDenyRules] = useState([]); // Environment variables state const [envVars, setEnvVars] = useState([]); // Hooks state const [userHooksChanged, setUserHooksChanged] = useState(false); const getUserHooks = React.useRef<(() => any) | null>(null); // Theme hook const { theme, setTheme, customColors, setCustomColors } = useTheme(); // Proxy state const [proxySettingsChanged, setProxySettingsChanged] = useState(false); const saveProxySettings = React.useRef<(() => Promise) | null>(null); // Analytics state const [analyticsEnabled, setAnalyticsEnabled] = useState(false); const [analyticsConsented, setAnalyticsConsented] = useState(false); const [showAnalyticsConsent, setShowAnalyticsConsent] = useState(false); const trackEvent = useTrackEvent(); // Load settings on mount useEffect(() => { loadSettings(); loadClaudeBinaryPath(); loadAnalyticsSettings(); }, []); /** * Loads analytics settings */ const loadAnalyticsSettings = async () => { const settings = analytics.getSettings(); if (settings) { setAnalyticsEnabled(settings.enabled); setAnalyticsConsented(settings.hasConsented); } }; /** * Loads the current Claude binary path */ const loadClaudeBinaryPath = async () => { try { const path = await api.getClaudeBinaryPath(); setCurrentBinaryPath(path); } catch (err) { console.error("Failed to load Claude binary path:", err); } }; /** * Loads the current Claude settings */ const loadSettings = async () => { try { setLoading(true); setError(null); const loadedSettings = await api.getClaudeSettings(); // Ensure loadedSettings is an object if (!loadedSettings || typeof loadedSettings !== 'object') { console.warn("Loaded settings is not an object:", loadedSettings); setSettings({}); return; } setSettings(loadedSettings); // Parse permissions if (loadedSettings.permissions && typeof loadedSettings.permissions === 'object') { if (Array.isArray(loadedSettings.permissions.allow)) { setAllowRules( loadedSettings.permissions.allow.map((rule: string, index: number) => ({ id: `allow-${index}`, value: rule, })) ); } if (Array.isArray(loadedSettings.permissions.deny)) { setDenyRules( loadedSettings.permissions.deny.map((rule: string, index: number) => ({ id: `deny-${index}`, value: rule, })) ); } } // Parse environment variables if (loadedSettings.env && typeof loadedSettings.env === 'object' && !Array.isArray(loadedSettings.env)) { setEnvVars( Object.entries(loadedSettings.env).map(([key, value], index) => ({ id: `env-${index}`, key, value: value as string, })) ); } } catch (err) { console.error("Failed to load settings:", err); setError(t('settings.messages.loadFailed')); setSettings({}); } finally { setLoading(false); } }; /** * Saves the current settings */ const saveSettings = async () => { try { setSaving(true); setError(null); setToast(null); // Build the settings object const updatedSettings: ClaudeSettings = { ...settings, permissions: { allow: allowRules.map(rule => rule.value).filter(v => v && String(v).trim()), deny: denyRules.map(rule => rule.value).filter(v => v && String(v).trim()), }, env: envVars.reduce((acc, { key, value }) => { if (key && String(key).trim() && value && String(value).trim()) { acc[key] = String(value); } return acc; }, {} as Record), }; await api.saveClaudeSettings(updatedSettings); setSettings(updatedSettings); // Save Claude binary path if changed if (binaryPathChanged && selectedInstallation) { await api.setClaudeBinaryPath(selectedInstallation.path); setCurrentBinaryPath(selectedInstallation.path); setBinaryPathChanged(false); } // Save user hooks if changed if (userHooksChanged && getUserHooks.current) { const hooks = getUserHooks.current(); await api.updateHooksConfig('user', hooks); setUserHooksChanged(false); } // Save proxy settings if changed if (proxySettingsChanged && saveProxySettings.current) { await saveProxySettings.current(); setProxySettingsChanged(false); } setToast({ message: t('settings.saveButton.settingsSavedSuccess'), type: "success" }); } catch (err) { console.error("Failed to save settings:", err); setError(t('settings.messages.saveFailed')); setToast({ message: t('settings.saveButton.settingsSaveFailed'), type: "error" }); } finally { setSaving(false); } }; /** * Updates a simple setting value */ const updateSetting = (key: string, value: any) => { setSettings(prev => ({ ...prev, [key]: value })); }; /** * Adds a new permission rule */ const addPermissionRule = (type: "allow" | "deny") => { const newRule: PermissionRule = { id: `${type}-${Date.now()}`, value: "", }; if (type === "allow") { setAllowRules(prev => [...prev, newRule]); } else { setDenyRules(prev => [...prev, newRule]); } }; /** * Updates a permission rule */ const updatePermissionRule = (type: "allow" | "deny", id: string, value: string) => { if (type === "allow") { setAllowRules(prev => prev.map(rule => rule.id === id ? { ...rule, value } : rule )); } else { setDenyRules(prev => prev.map(rule => rule.id === id ? { ...rule, value } : rule )); } }; /** * Removes a permission rule */ const removePermissionRule = (type: "allow" | "deny", id: string) => { if (type === "allow") { setAllowRules(prev => prev.filter(rule => rule.id !== id)); } else { setDenyRules(prev => prev.filter(rule => rule.id !== id)); } }; /** * Adds a new environment variable */ const addEnvVar = () => { const newVar: EnvironmentVariable = { id: `env-${Date.now()}`, key: "", value: "", }; setEnvVars(prev => [...prev, newVar]); }; /** * Updates an environment variable */ const updateEnvVar = (id: string, field: "key" | "value", value: string) => { setEnvVars(prev => prev.map(envVar => envVar.id === id ? { ...envVar, [field]: value } : envVar )); }; /** * Removes an environment variable */ const removeEnvVar = (id: string) => { setEnvVars(prev => prev.filter(envVar => envVar.id !== id)); }; /** * Handle Claude installation selection */ const handleClaudeInstallationSelect = (installation: ClaudeInstallation) => { setSelectedInstallation(installation); setBinaryPathChanged(installation.path !== currentBinaryPath); }; return (
{/* Header */}

{t('settings.title')}

{t('settings.configurePreferences')}

{/* Error message */} {error && ( {error} )} {/* Content */} {loading ? (
) : (
{t('settings.general')} {t('settings.permissionsTab')} {t('settings.environmentTab')} {t('settings.advancedTab')} {t('settings.hooksTab')} {t('settings.commands')} {t('settings.storage')} {t('settings.proxy')} {t('settings.analyticsTab')} {/* General Settings */}

{t('settings.generalSettings')}

{/* Theme Selector */}

{t('settings.themeSelector.choosePreferredTheme')}

{/* Custom Color Editor */} {theme === 'custom' && (

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

{/* Background Color */}
setCustomColors({ background: e.target.value })} placeholder="oklch(0.12 0.01 240)" className="font-mono text-xs" />
{/* Foreground Color */}
setCustomColors({ foreground: e.target.value })} placeholder="oklch(0.98 0.01 240)" className="font-mono text-xs" />
{/* Primary Color */}
setCustomColors({ primary: e.target.value })} placeholder="oklch(0.98 0.01 240)" className="font-mono text-xs" />
{/* Card Color */}
setCustomColors({ card: e.target.value })} placeholder="oklch(0.14 0.01 240)" className="font-mono text-xs" />
{/* Accent Color */}
setCustomColors({ accent: e.target.value })} placeholder="oklch(0.16 0.01 240)" className="font-mono text-xs" />
{/* Destructive Color */}
setCustomColors({ destructive: e.target.value })} placeholder="oklch(0.6 0.2 25)" className="font-mono text-xs" />

{t('settings.customTheme.colorValuesDesc')}

)} {/* Include Co-authored By */}

{t('settings.generalOptions.includeCoAuthorDesc')}

updateSetting("includeCoAuthoredBy", checked)} />
{/* Verbose Output */}

{t('settings.generalOptions.verboseOutputDesc')}

updateSetting("verbose", checked)} />
{/* Cleanup Period */}
{ const value = e.target.value ? parseInt(e.target.value) : undefined; updateSetting("cleanupPeriodDays", value); }} />

{t('settings.generalOptions.chatRetentionDesc')}

{/* Claude Binary Path Selector */}

{t('settings.generalOptions.claudeCodeInstallationDesc')}

{binaryPathChanged && (

{t('settings.generalOptions.binaryPathChanged')}

)}
{/* Permissions Settings */}

{t('settings.permissions.permissionRules')}

{t('settings.permissions.permissionRulesDesc')}

{/* Allow Rules */}
{allowRules.length === 0 ? (

{t('settings.permissions.noAllowRules')}

) : ( allowRules.map((rule) => ( updatePermissionRule("allow", rule.id, e.target.value)} className="flex-1" /> )) )}
{/* Deny Rules */}
{denyRules.length === 0 ? (

{t('settings.permissions.noDenyRules')}

) : ( denyRules.map((rule) => ( updatePermissionRule("deny", rule.id, e.target.value)} className="flex-1" /> )) )}

{t('settings.permissions.examples')}

  • Bash - {t('settings.permissions.exampleBash')}
  • Bash(npm run build) - {t('settings.permissions.exampleExactCommand')}
  • Bash(npm run test:*) - {t('settings.permissions.examplePrefix')}
  • Read(~/.zshrc) - {t('settings.permissions.exampleReadFile')}
  • Edit(docs/**) - {t('settings.permissions.exampleEditDir')}
{/* Environment Variables */}

{t('settings.environment.environmentVariables')}

{t('settings.environment.environmentVariablesDesc')}

{envVars.length === 0 ? (

{t('settings.environment.noEnvironmentVariables')}

) : ( envVars.map((envVar) => ( updateEnvVar(envVar.id, "key", e.target.value)} className="flex-1 font-mono text-sm" /> = updateEnvVar(envVar.id, "value", e.target.value)} className="flex-1 font-mono text-sm" /> )) )}

{t('settings.environment.commonVariables')}

  • CLAUDE_CODE_ENABLE_TELEMETRY - {t('settings.environment.telemetryDesc')}
  • ANTHROPIC_MODEL - {t('settings.environment.modelDesc')}
  • DISABLE_COST_WARNINGS - {t('settings.environment.costWarningsDesc')}
{/* Advanced Settings */}

{t('settings.advanced.advancedSettings')}

{t('settings.advanced.advancedSettingsDesc')}

{/* API Key Helper */}
updateSetting("apiKeyHelper", e.target.value || undefined)} />

{t('settings.advanced.apiKeyHelperDesc')}

{/* Raw JSON Editor */}
{JSON.stringify(settings, null, 2)}

{t('settings.advanced.rawSettingsDesc')}

{/* Hooks Settings */}

{t('settings.hooks.userHooks')}

{t('settings.hooks.userHooksDesc')}

{ setUserHooksChanged(hasChanges); getUserHooks.current = getHooks; }} />
{/* Commands Tab */} {/* Storage Tab */} {/* Proxy Settings */} { setProxySettingsChanged(hasChanges); saveProxySettings.current = save; }} /> {/* Analytics Settings */}

{t('settings.analytics.analyticsSettings')}

{/* Analytics Toggle */}

{t('settings.analytics.enableAnalyticsDesc')}

{ if (checked && !analyticsConsented) { setShowAnalyticsConsent(true); } else if (checked) { await analytics.enable(); setAnalyticsEnabled(true); trackEvent.settingsChanged('analytics_enabled', true); setToast({ message: t('settings.analytics.analyticsEnabled'), type: "success" }); } else { await analytics.disable(); setAnalyticsEnabled(false); trackEvent.settingsChanged('analytics_enabled', false); setToast({ message: t('settings.analytics.analyticsDisabled'), type: "success" }); } }} />
{/* Privacy Info */}

{t('settings.analytics.privacyProtected')}

  • • {t('settings.analytics.noPersonalInfo')}
  • • {t('settings.analytics.noFileContents')}
  • • {t('settings.analytics.anonymousData')}
  • • {t('settings.analytics.canDisable')}
{/* Data Collection Info */} {analyticsEnabled && (

{t('settings.analytics.whatWeCollect')}

  • • {t('settings.analytics.featureUsage')}
  • • {t('settings.analytics.performanceMetrics')}
  • • {t('settings.analytics.errorReports')}
  • • {t('settings.analytics.sessionFrequency')}
{/* Delete Data Button */}
)}
)}
{/* Toast Notification */} {toast && ( setToast(null)} /> )} {/* Analytics Consent Dialog */} { await loadAnalyticsSettings(); setShowAnalyticsConsent(false); }} />
); };