import { useCallback, useEffect, useRef, useState } from 'react'; import { useTheme } from '../../../contexts/ThemeContext'; import { authenticatedFetch } from '../../../utils/api'; import { useProviderAuthStatus } from '../../provider-auth/hooks/useProviderAuthStatus'; import { DEFAULT_CODE_EDITOR_SETTINGS, DEFAULT_CURSOR_PERMISSIONS, } from '../constants/constants'; import type { AgentProvider, ClaudePermissionsState, CodeEditorSettingsState, CodexPermissionMode, CursorPermissionsState, GeminiPermissionMode, NotificationPreferencesState, ProjectSortOrder, SettingsMainTab, } from '../types/types'; type ThemeContextValue = { isDarkMode: boolean; toggleDarkMode: () => void; }; type UseSettingsControllerArgs = { isOpen: boolean; initialTab: string; }; type ClaudeSettingsStorage = { allowedTools?: string[]; disallowedTools?: string[]; skipPermissions?: boolean; projectSortOrder?: ProjectSortOrder; }; type CursorSettingsStorage = { allowedCommands?: string[]; disallowedCommands?: string[]; skipPermissions?: boolean; }; type CodexSettingsStorage = { permissionMode?: CodexPermissionMode; }; type NotificationPreferencesResponse = { success?: boolean; preferences?: NotificationPreferencesState; }; type ActiveLoginProvider = AgentProvider | ''; const KNOWN_MAIN_TABS: SettingsMainTab[] = ['agents', 'appearance', 'git', 'api', 'tasks', 'notifications', 'plugins']; const normalizeMainTab = (tab: string): SettingsMainTab => { // Keep backwards compatibility with older callers that still pass "tools". if (tab === 'tools') { return 'agents'; } return KNOWN_MAIN_TABS.includes(tab as SettingsMainTab) ? (tab as SettingsMainTab) : 'agents'; }; const parseJson = (value: string | null, fallback: T): T => { if (!value) { return fallback; } try { return JSON.parse(value) as T; } catch { return fallback; } }; const toCodexPermissionMode = (value: unknown): CodexPermissionMode => { if (value === 'acceptEdits' || value === 'bypassPermissions') { return value; } return 'default'; }; const readCodeEditorSettings = (): CodeEditorSettingsState => ({ theme: localStorage.getItem('codeEditorTheme') === 'light' ? 'light' : 'dark', wordWrap: localStorage.getItem('codeEditorWordWrap') === 'true', showMinimap: localStorage.getItem('codeEditorShowMinimap') !== 'false', lineNumbers: localStorage.getItem('codeEditorLineNumbers') !== 'false', fontSize: localStorage.getItem('codeEditorFontSize') ?? DEFAULT_CODE_EDITOR_SETTINGS.fontSize, }); const toResponseJson = async (response: Response): Promise => response.json() as Promise; const createEmptyClaudePermissions = (): ClaudePermissionsState => ({ allowedTools: [], disallowedTools: [], skipPermissions: false, }); const createEmptyCursorPermissions = (): CursorPermissionsState => ({ ...DEFAULT_CURSOR_PERMISSIONS, }); const createDefaultNotificationPreferences = (): NotificationPreferencesState => ({ channels: { inApp: true, webPush: false, }, events: { actionRequired: true, stop: true, error: true, }, }); export function useSettingsController({ isOpen, initialTab }: UseSettingsControllerArgs) { const { isDarkMode, toggleDarkMode } = useTheme() as ThemeContextValue; const closeTimerRef = useRef(null); const [activeTab, setActiveTab] = useState(() => normalizeMainTab(initialTab)); const [saveStatus, setSaveStatus] = useState<'success' | 'error' | null>(null); const [projectSortOrder, setProjectSortOrder] = useState('name'); const [codeEditorSettings, setCodeEditorSettings] = useState(() => ( readCodeEditorSettings() )); const [claudePermissions, setClaudePermissions] = useState(() => ( createEmptyClaudePermissions() )); const [cursorPermissions, setCursorPermissions] = useState(() => ( createEmptyCursorPermissions() )); const [notificationPreferences, setNotificationPreferences] = useState(() => ( createDefaultNotificationPreferences() )); const [codexPermissionMode, setCodexPermissionMode] = useState('default'); const [geminiPermissionMode, setGeminiPermissionMode] = useState('default'); const [showLoginModal, setShowLoginModal] = useState(false); const [loginProvider, setLoginProvider] = useState(''); const { providerAuthStatus, checkProviderAuthStatus, refreshProviderAuthStatuses, } = useProviderAuthStatus(); const loadSettings = useCallback(async () => { try { const savedClaudeSettings = parseJson( localStorage.getItem('claude-settings'), {}, ); setClaudePermissions({ allowedTools: savedClaudeSettings.allowedTools || [], disallowedTools: savedClaudeSettings.disallowedTools || [], skipPermissions: Boolean(savedClaudeSettings.skipPermissions), }); setProjectSortOrder(savedClaudeSettings.projectSortOrder === 'date' ? 'date' : 'name'); const savedCursorSettings = parseJson( localStorage.getItem('cursor-tools-settings'), {}, ); setCursorPermissions({ allowedCommands: savedCursorSettings.allowedCommands || [], disallowedCommands: savedCursorSettings.disallowedCommands || [], skipPermissions: Boolean(savedCursorSettings.skipPermissions), }); const savedCodexSettings = parseJson( localStorage.getItem('codex-settings'), {}, ); setCodexPermissionMode(toCodexPermissionMode(savedCodexSettings.permissionMode)); const savedGeminiSettings = parseJson<{ permissionMode?: GeminiPermissionMode }>( localStorage.getItem('gemini-settings'), {}, ); setGeminiPermissionMode(savedGeminiSettings.permissionMode || 'default'); try { const notificationResponse = await authenticatedFetch('/api/settings/notification-preferences'); if (notificationResponse.ok) { const notificationData = await toResponseJson(notificationResponse); if (notificationData.success && notificationData.preferences) { setNotificationPreferences(notificationData.preferences); } else { setNotificationPreferences(createDefaultNotificationPreferences()); } } else { setNotificationPreferences(createDefaultNotificationPreferences()); } } catch { setNotificationPreferences(createDefaultNotificationPreferences()); } } catch (error) { console.error('Error loading settings:', error); setClaudePermissions(createEmptyClaudePermissions()); setCursorPermissions(createEmptyCursorPermissions()); setNotificationPreferences(createDefaultNotificationPreferences()); setCodexPermissionMode('default'); setProjectSortOrder('name'); } }, []); const openLoginForProvider = useCallback((provider: AgentProvider) => { setLoginProvider(provider); setShowLoginModal(true); }, []); const handleLoginComplete = useCallback((exitCode: number) => { if (exitCode !== 0 || !loginProvider) { return; } setSaveStatus('success'); void checkProviderAuthStatus(loginProvider); }, [checkProviderAuthStatus, loginProvider]); const saveSettings = useCallback(async () => { setSaveStatus(null); try { const now = new Date().toISOString(); localStorage.setItem('claude-settings', JSON.stringify({ allowedTools: claudePermissions.allowedTools, disallowedTools: claudePermissions.disallowedTools, skipPermissions: claudePermissions.skipPermissions, projectSortOrder, lastUpdated: now, })); localStorage.setItem('cursor-tools-settings', JSON.stringify({ allowedCommands: cursorPermissions.allowedCommands, disallowedCommands: cursorPermissions.disallowedCommands, skipPermissions: cursorPermissions.skipPermissions, lastUpdated: now, })); localStorage.setItem('codex-settings', JSON.stringify({ permissionMode: codexPermissionMode, lastUpdated: now, })); localStorage.setItem('gemini-settings', JSON.stringify({ permissionMode: geminiPermissionMode, lastUpdated: now, })); const notificationResponse = await authenticatedFetch('/api/settings/notification-preferences', { method: 'PUT', body: JSON.stringify(notificationPreferences), }); if (!notificationResponse.ok) { throw new Error('Failed to save notification preferences'); } setSaveStatus('success'); } catch (error) { console.error('Error saving settings:', error); setSaveStatus('error'); } }, [ claudePermissions.allowedTools, claudePermissions.disallowedTools, claudePermissions.skipPermissions, codexPermissionMode, cursorPermissions.allowedCommands, cursorPermissions.disallowedCommands, cursorPermissions.skipPermissions, notificationPreferences, geminiPermissionMode, projectSortOrder, ]); const updateCodeEditorSetting = useCallback( (key: K, value: CodeEditorSettingsState[K]) => { setCodeEditorSettings((prev) => ({ ...prev, [key]: value })); }, [], ); useEffect(() => { if (!isOpen) { return; } setActiveTab(normalizeMainTab(initialTab)); void loadSettings(); void refreshProviderAuthStatuses(); }, [initialTab, isOpen, loadSettings, refreshProviderAuthStatuses]); useEffect(() => { localStorage.setItem('codeEditorTheme', codeEditorSettings.theme); localStorage.setItem('codeEditorWordWrap', String(codeEditorSettings.wordWrap)); localStorage.setItem('codeEditorShowMinimap', String(codeEditorSettings.showMinimap)); localStorage.setItem('codeEditorLineNumbers', String(codeEditorSettings.lineNumbers)); localStorage.setItem('codeEditorFontSize', codeEditorSettings.fontSize); window.dispatchEvent(new Event('codeEditorSettingsChanged')); }, [codeEditorSettings]); // Auto-save permissions and sort order with debounce const autoSaveTimerRef = useRef(null); const isInitialLoadRef = useRef(true); useEffect(() => { // Skip auto-save on initial load (settings are being loaded from localStorage) if (isInitialLoadRef.current) { isInitialLoadRef.current = false; return; } if (autoSaveTimerRef.current !== null) { window.clearTimeout(autoSaveTimerRef.current); } autoSaveTimerRef.current = window.setTimeout(() => { saveSettings(); }, 500); return () => { if (autoSaveTimerRef.current !== null) { window.clearTimeout(autoSaveTimerRef.current); } }; }, [saveSettings]); // Clear save status after 2 seconds useEffect(() => { if (saveStatus === null) { return; } const timer = window.setTimeout(() => setSaveStatus(null), 2000); return () => window.clearTimeout(timer); }, [saveStatus]); // Reset initial load flag when settings dialog opens useEffect(() => { if (isOpen) { isInitialLoadRef.current = true; } }, [isOpen]); useEffect(() => () => { if (closeTimerRef.current !== null) { window.clearTimeout(closeTimerRef.current); closeTimerRef.current = null; } if (autoSaveTimerRef.current !== null) { window.clearTimeout(autoSaveTimerRef.current); autoSaveTimerRef.current = null; } }, []); return { activeTab, setActiveTab, isDarkMode, toggleDarkMode, saveStatus, projectSortOrder, setProjectSortOrder, codeEditorSettings, updateCodeEditorSetting, claudePermissions, setClaudePermissions, cursorPermissions, setCursorPermissions, notificationPreferences, setNotificationPreferences, codexPermissionMode, setCodexPermissionMode, providerAuthStatus, geminiPermissionMode, setGeminiPermissionMode, openLoginForProvider, showLoginModal, setShowLoginModal, loginProvider, handleLoginComplete, }; }