| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { useEffect, useRef, useCallback, useState } from 'react'; |
| import { createLogger } from '@automaker/utils/logger'; |
| import { getHttpApiClient, waitForApiKeyInit } from '@/lib/http-api-client'; |
| import { setItem } from '@/lib/storage'; |
| import { useAppStore, type ThemeMode, THEME_STORAGE_KEY } from '@/store/app-store'; |
| import { useSetupStore } from '@/store/setup-store'; |
| import { useAuthStore } from '@/store/auth-store'; |
| import { waitForMigrationComplete, resetMigrationState } from './use-settings-migration'; |
| import { sanitizeWorktreeByProject } from '@/lib/settings-utils'; |
| import { |
| DEFAULT_OPENCODE_MODEL, |
| DEFAULT_GEMINI_MODEL, |
| DEFAULT_COPILOT_MODEL, |
| DEFAULT_MAX_CONCURRENCY, |
| DEFAULT_PHASE_MODELS, |
| getAllOpencodeModelIds, |
| getAllCursorModelIds, |
| getAllGeminiModelIds, |
| getAllCopilotModelIds, |
| migrateCursorModelIds, |
| migrateOpencodeModelIds, |
| migratePhaseModelEntry, |
| type GlobalSettings, |
| type CursorModelId, |
| type GeminiModelId, |
| type CopilotModelId, |
| type PhaseModelEntry, |
| } from '@automaker/types'; |
|
|
| const logger = createLogger('SettingsSync'); |
|
|
| |
| const SYNC_DEBOUNCE_MS = 1000; |
|
|
| |
| const SETTINGS_FIELDS_TO_SYNC = [ |
| 'theme', |
| 'fontFamilySans', |
| 'fontFamilyMono', |
| 'terminalFontFamily', |
| 'openTerminalMode', |
| 'terminalCustomBackgroundColor', |
| 'terminalCustomForegroundColor', |
| 'sidebarOpen', |
| 'sidebarStyle', |
| 'collapsedNavSections', |
| 'chatHistoryOpen', |
| 'maxConcurrency', |
| 'autoModeByWorktree', |
| 'defaultSkipTests', |
| 'enableDependencyBlocking', |
| 'skipVerificationInAutoMode', |
| 'mergePostAction', |
| 'useWorktrees', |
| 'defaultPlanningMode', |
| 'defaultRequirePlanApproval', |
| 'defaultFeatureModel', |
| 'muteDoneSound', |
| 'disableSplashScreen', |
| 'defaultSortNewestCardOnTop', |
| 'serverLogLevel', |
| 'enableRequestLogging', |
| 'showQueryDevtools', |
| 'enhancementModel', |
| 'validationModel', |
| 'phaseModels', |
| 'defaultThinkingLevel', |
| 'defaultReasoningEffort', |
| 'enabledCursorModels', |
| 'cursorDefaultModel', |
| 'enabledOpencodeModels', |
| 'opencodeDefaultModel', |
| 'enabledGeminiModels', |
| 'geminiDefaultModel', |
| 'enabledCopilotModels', |
| 'copilotDefaultModel', |
| 'enabledDynamicModelIds', |
| 'knownDynamicModelIds', |
| 'disabledProviders', |
| 'autoLoadClaudeMd', |
| 'useClaudeCodeSystemPrompt', |
| 'keyboardShortcuts', |
| 'mcpServers', |
| 'defaultEditorCommand', |
| 'editorFontSize', |
| 'editorFontFamily', |
| 'editorAutoSave', |
| 'editorAutoSaveDelay', |
| 'defaultTerminalId', |
| 'enableAiCommitMessages', |
| 'enableSkills', |
| 'skillsSources', |
| 'enableSubagents', |
| 'subagentsSources', |
| 'promptCustomization', |
| 'eventHooks', |
| 'ntfyEndpoints', |
| 'featureTemplates', |
| 'claudeCompatibleProviders', |
| 'claudeApiProfiles', |
| 'activeClaudeApiProfileId', |
| 'projects', |
| 'trashedProjects', |
| 'currentProjectId', |
| 'projectHistory', |
| 'projectHistoryIndex', |
| 'lastSelectedSessionByProject', |
| 'agentModelBySession', |
| 'currentWorktreeByProject', |
| |
| 'codexAutoLoadAgents', |
| 'codexSandboxMode', |
| 'codexApprovalPolicy', |
| 'codexEnableWebSearch', |
| 'codexEnableImages', |
| 'codexAdditionalDirs', |
| 'codexThreadId', |
| |
| 'defaultMaxTurns', |
| |
| 'worktreePanelCollapsed', |
| 'lastProjectDir', |
| 'recentFolders', |
| ] as const; |
|
|
| |
| const SETUP_FIELDS_TO_SYNC = ['isFirstRun', 'setupComplete', 'skipClaudeSetup'] as const; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function getSettingsFieldValue( |
| field: (typeof SETTINGS_FIELDS_TO_SYNC)[number], |
| appState: ReturnType<typeof useAppStore.getState> |
| ): unknown { |
| if (field === 'currentProjectId') { |
| return appState.currentProject?.id ?? null; |
| } |
| if (field === 'terminalFontFamily') { |
| return appState.terminalState.fontFamily; |
| } |
| if (field === 'openTerminalMode') { |
| return appState.terminalState.openTerminalMode; |
| } |
| if (field === 'terminalCustomBackgroundColor') { |
| return appState.terminalState.customBackgroundColor; |
| } |
| if (field === 'terminalCustomForegroundColor') { |
| return appState.terminalState.customForegroundColor; |
| } |
| if (field === 'autoModeByWorktree') { |
| |
| const autoModeByWorktree = appState.autoModeByWorktree; |
| const persistedSettings: Record<string, { maxConcurrency: number; branchName: string | null }> = |
| {}; |
| for (const [key, value] of Object.entries(autoModeByWorktree)) { |
| persistedSettings[key] = { |
| maxConcurrency: value.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY, |
| branchName: value.branchName, |
| }; |
| } |
| return persistedSettings; |
| } |
| if (field === 'agentModelBySession') { |
| |
| |
| |
| const map = appState.agentModelBySession as Record<string, unknown>; |
| const MAX_ENTRIES = 50; |
| const entries = Object.entries(map); |
| if (entries.length <= MAX_ENTRIES) return map; |
| |
| return Object.fromEntries(entries.slice(-MAX_ENTRIES)); |
| } |
| return appState[field as keyof typeof appState]; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function hasSettingsFieldChanged( |
| field: (typeof SETTINGS_FIELDS_TO_SYNC)[number], |
| newState: ReturnType<typeof useAppStore.getState>, |
| prevState: ReturnType<typeof useAppStore.getState> |
| ): boolean { |
| if (field === 'currentProjectId') { |
| return newState.currentProject?.id !== prevState.currentProject?.id; |
| } |
| if (field === 'terminalFontFamily') { |
| return newState.terminalState.fontFamily !== prevState.terminalState.fontFamily; |
| } |
| if (field === 'openTerminalMode') { |
| return newState.terminalState.openTerminalMode !== prevState.terminalState.openTerminalMode; |
| } |
| if (field === 'terminalCustomBackgroundColor') { |
| return ( |
| newState.terminalState.customBackgroundColor !== prevState.terminalState.customBackgroundColor |
| ); |
| } |
| if (field === 'terminalCustomForegroundColor') { |
| return ( |
| newState.terminalState.customForegroundColor !== prevState.terminalState.customForegroundColor |
| ); |
| } |
| const key = field as keyof typeof newState; |
| return newState[key] !== prevState[key]; |
| } |
|
|
| interface SettingsSyncState { |
| |
| loaded: boolean; |
| |
| error: string | null; |
| |
| syncing: boolean; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function useSettingsSync(): SettingsSyncState { |
| const [state, setState] = useState<SettingsSyncState>({ |
| loaded: false, |
| error: null, |
| syncing: false, |
| }); |
|
|
| const isAuthenticated = useAuthStore((s) => s.isAuthenticated); |
| const authChecked = useAuthStore((s) => s.authChecked); |
| const settingsLoaded = useAuthStore((s) => s.settingsLoaded); |
|
|
| const syncTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| const lastSyncedRef = useRef<string>(''); |
| const isInitializedRef = useRef(false); |
|
|
| |
| |
| useEffect(() => { |
| if (!authChecked) return; |
|
|
| if (!isAuthenticated) { |
| if (syncTimeoutRef.current) { |
| clearTimeout(syncTimeoutRef.current); |
| syncTimeoutRef.current = null; |
| } |
| lastSyncedRef.current = ''; |
| isInitializedRef.current = false; |
|
|
| |
| resetMigrationState(); |
|
|
| setState({ loaded: false, error: null, syncing: false }); |
| } |
| }, [authChecked, isAuthenticated]); |
|
|
| |
| const syncToServer = useCallback(async () => { |
| try { |
| |
| |
| const auth = useAuthStore.getState(); |
| logger.debug('[SYNC_CHECK] Auth state:', { |
| authChecked: auth.authChecked, |
| isAuthenticated: auth.isAuthenticated, |
| settingsLoaded: auth.settingsLoaded, |
| projectsCount: useAppStore.getState().projects?.length ?? 0, |
| }); |
| if (!auth.authChecked || !auth.isAuthenticated || !auth.settingsLoaded) { |
| logger.warn('[SYNC_SKIPPED] Not ready:', { |
| authChecked: auth.authChecked, |
| isAuthenticated: auth.isAuthenticated, |
| settingsLoaded: auth.settingsLoaded, |
| }); |
| return; |
| } |
|
|
| setState((s) => ({ ...s, syncing: true })); |
| const api = getHttpApiClient(); |
| const appState = useAppStore.getState(); |
|
|
| logger.info('[SYNC_START] Syncing to server:', { |
| projectsCount: appState.projects?.length ?? 0, |
| }); |
|
|
| |
| const updates: Record<string, unknown> = {}; |
| for (const field of SETTINGS_FIELDS_TO_SYNC) { |
| updates[field] = getSettingsFieldValue(field, appState); |
| } |
|
|
| |
| const setupState = useSetupStore.getState(); |
| for (const field of SETUP_FIELDS_TO_SYNC) { |
| updates[field] = setupState[field as keyof typeof setupState]; |
| } |
|
|
| |
| const updateHash = JSON.stringify(updates); |
| if (updateHash === lastSyncedRef.current) { |
| logger.debug('[SYNC_SKIP_IDENTICAL] No changes from last sync'); |
| setState((s) => ({ ...s, syncing: false })); |
| return; |
| } |
|
|
| logger.info('[SYNC_SEND] Sending settings update to server:', { |
| projects: Array.isArray(updates.projects) ? updates.projects.length : 0, |
| trashedProjects: Array.isArray(updates.trashedProjects) |
| ? updates.trashedProjects.length |
| : 0, |
| }); |
|
|
| const result = await api.settings.updateGlobal(updates); |
| logger.info('[SYNC_RESPONSE] Server response:', { success: result.success }); |
| if (result.success) { |
| lastSyncedRef.current = updateHash; |
| logger.debug('Settings synced to server'); |
|
|
| |
| |
| try { |
| setItem('automaker-settings-cache', JSON.stringify(updates)); |
| logger.debug('Updated localStorage cache after sync'); |
| } catch (storageError) { |
| logger.warn('Failed to update localStorage cache after sync:', storageError); |
| } |
| } else { |
| logger.error('Failed to sync settings:', result.error); |
| } |
| } catch (error) { |
| logger.error('Failed to sync settings to server:', error); |
| } finally { |
| setState((s) => ({ ...s, syncing: false })); |
| } |
| }, []); |
|
|
| |
| const scheduleSyncToServer = useCallback(() => { |
| if (syncTimeoutRef.current) { |
| clearTimeout(syncTimeoutRef.current); |
| } |
| syncTimeoutRef.current = setTimeout(() => { |
| syncToServer(); |
| }, SYNC_DEBOUNCE_MS); |
| }, [syncToServer]); |
|
|
| |
| const syncNow = useCallback(() => { |
| if (syncTimeoutRef.current) { |
| clearTimeout(syncTimeoutRef.current); |
| syncTimeoutRef.current = null; |
| } |
| void syncToServer(); |
| }, [syncToServer]); |
|
|
| |
| useEffect(() => { |
| |
| |
| |
| |
| |
| logger.debug('useSettingsSync initialization check:', { |
| authChecked, |
| isAuthenticated, |
| settingsLoaded, |
| stateLoaded: state.loaded, |
| }); |
| if (!authChecked || !isAuthenticated || !settingsLoaded) return; |
| if (isInitializedRef.current) return; |
| isInitializedRef.current = true; |
|
|
| async function initializeSync() { |
| try { |
| |
| await waitForApiKeyInit(); |
|
|
| |
| |
| logger.info('Waiting for migration to complete before starting sync...'); |
| await waitForMigrationComplete(); |
|
|
| |
| |
| |
| |
| await new Promise((resolve) => setTimeout(resolve, 50)); |
|
|
| logger.info('Migration complete, initializing sync'); |
|
|
| |
| const appState = useAppStore.getState(); |
| const setupState = useSetupStore.getState(); |
|
|
| logger.info('Initial state read:', { projectsCount: appState.projects?.length ?? 0 }); |
|
|
| |
| |
| const updates: Record<string, unknown> = {}; |
| for (const field of SETTINGS_FIELDS_TO_SYNC) { |
| updates[field] = getSettingsFieldValue(field, appState); |
| } |
| for (const field of SETUP_FIELDS_TO_SYNC) { |
| updates[field] = setupState[field as keyof typeof setupState]; |
| } |
| lastSyncedRef.current = JSON.stringify(updates); |
|
|
| logger.info('Settings sync initialized'); |
| setState({ loaded: true, error: null, syncing: false }); |
| } catch (error) { |
| logger.error('Failed to initialize settings sync:', error); |
| setState({ |
| loaded: true, |
| error: error instanceof Error ? error.message : 'Unknown error', |
| syncing: false, |
| }); |
| } |
| } |
|
|
| initializeSync(); |
| |
| }, [authChecked, isAuthenticated, settingsLoaded]); |
|
|
| |
| useEffect(() => { |
| if (!state.loaded || !authChecked || !isAuthenticated || !settingsLoaded) return; |
|
|
| |
| const unsubscribeApp = useAppStore.subscribe((newState, prevState) => { |
| const auth = useAuthStore.getState(); |
| logger.debug('Store subscription fired:', { |
| prevProjects: prevState.projects?.length ?? 0, |
| newProjects: newState.projects?.length ?? 0, |
| authChecked: auth.authChecked, |
| isAuthenticated: auth.isAuthenticated, |
| settingsLoaded: auth.settingsLoaded, |
| loaded: state.loaded, |
| }); |
|
|
| |
| if (!auth.settingsLoaded) { |
| logger.debug('Store changed but settings not loaded, skipping sync'); |
| return; |
| } |
|
|
| |
| if (newState.currentProject?.id !== prevState.currentProject?.id) { |
| logger.debug('Current project changed, syncing immediately'); |
| syncNow(); |
| return; |
| } |
|
|
| |
| |
| if (newState.defaultSortNewestCardOnTop !== prevState.defaultSortNewestCardOnTop) { |
| logger.debug('defaultSortNewestCardOnTop changed, syncing immediately'); |
| syncNow(); |
| return; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if (newState.projects !== prevState.projects) { |
| const prevIds = prevState.projects |
| ?.map((p) => JSON.stringify([p.id, p.name, p.path])) |
| .join(','); |
| const newIds = newState.projects |
| ?.map((p) => JSON.stringify([p.id, p.name, p.path])) |
| .join(','); |
| if (prevIds !== newIds) { |
| logger.info('[PROJECTS_CHANGED] Projects array changed, syncing immediately', { |
| prevCount: prevState.projects?.length ?? 0, |
| newCount: newState.projects?.length ?? 0, |
| }); |
| syncNow(); |
| |
| |
| |
| } else { |
| |
| |
| |
| logger.debug('[PROJECTS_NESTED_CHANGE] Projects nested fields changed, scheduling sync'); |
| scheduleSyncToServer(); |
| } |
| } |
|
|
| |
| let changed = false; |
| for (const field of SETTINGS_FIELDS_TO_SYNC) { |
| if (field === 'projects') continue; |
| if (hasSettingsFieldChanged(field, newState, prevState)) { |
| changed = true; |
| break; |
| } |
| } |
|
|
| if (changed) { |
| logger.debug('Store changed, scheduling sync'); |
| scheduleSyncToServer(); |
| } |
| }); |
|
|
| |
| const unsubscribeSetup = useSetupStore.subscribe((newState, prevState) => { |
| let changed = false; |
| for (const field of SETUP_FIELDS_TO_SYNC) { |
| const key = field as keyof typeof newState; |
| if (newState[key] !== prevState[key]) { |
| changed = true; |
| break; |
| } |
| } |
|
|
| if (changed) { |
| |
| scheduleSyncToServer(); |
| } |
| }); |
|
|
| return () => { |
| unsubscribeApp(); |
| unsubscribeSetup(); |
| if (syncTimeoutRef.current) { |
| clearTimeout(syncTimeoutRef.current); |
| } |
| }; |
| }, [state.loaded, authChecked, isAuthenticated, settingsLoaded, scheduleSyncToServer, syncNow]); |
|
|
| |
| useEffect(() => { |
| if (!state.loaded || !authChecked || !isAuthenticated || !settingsLoaded) return; |
|
|
| const handleBeforeUnload = () => { |
| |
| syncNow(); |
| }; |
|
|
| const handleVisibilityChange = () => { |
| if (document.visibilityState === 'hidden') { |
| syncNow(); |
| } |
| }; |
|
|
| window.addEventListener('beforeunload', handleBeforeUnload); |
| document.addEventListener('visibilitychange', handleVisibilityChange); |
|
|
| return () => { |
| window.removeEventListener('beforeunload', handleBeforeUnload); |
| document.removeEventListener('visibilitychange', handleVisibilityChange); |
| }; |
| }, [state.loaded, authChecked, isAuthenticated, settingsLoaded, syncNow]); |
|
|
| return state; |
| } |
|
|
| |
| |
| |
| |
| export async function forceSyncSettingsToServer(): Promise<boolean> { |
| try { |
| const api = getHttpApiClient(); |
| const appState = useAppStore.getState(); |
|
|
| const updates: Record<string, unknown> = {}; |
| for (const field of SETTINGS_FIELDS_TO_SYNC) { |
| updates[field] = getSettingsFieldValue(field, appState); |
| } |
| const setupState = useSetupStore.getState(); |
| for (const field of SETUP_FIELDS_TO_SYNC) { |
| updates[field] = setupState[field as keyof typeof setupState]; |
| } |
|
|
| |
| |
| |
| try { |
| setItem('automaker-settings-cache', JSON.stringify(updates)); |
| } catch (storageError) { |
| logger.warn('Failed to update localStorage cache during force sync:', storageError); |
| } |
|
|
| const result = await api.settings.updateGlobal(updates); |
| return result.success; |
| } catch (error) { |
| logger.error('Failed to force sync settings:', error); |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| export async function refreshSettingsFromServer(): Promise<boolean> { |
| try { |
| const api = getHttpApiClient(); |
| const result = await api.settings.getGlobal(); |
|
|
| if (!result.success || !result.settings) { |
| return false; |
| } |
|
|
| const serverSettings = result.settings as unknown as GlobalSettings; |
| const currentAppState = useAppStore.getState(); |
|
|
| |
| const allCursorModels = getAllCursorModelIds(); |
| const validCursorModelIds = new Set(allCursorModels); |
|
|
| |
| const migratedCursorDefault = migrateCursorModelIds([ |
| serverSettings.cursorDefaultModel ?? 'cursor-auto', |
| ])[0]; |
| const sanitizedCursorDefault = validCursorModelIds.has(migratedCursorDefault) |
| ? migratedCursorDefault |
| : ('cursor-auto' as CursorModelId); |
|
|
| |
| const migratedOpencodeModels = migrateOpencodeModelIds( |
| serverSettings.enabledOpencodeModels ?? [] |
| ); |
| const validOpencodeModelIds = new Set(getAllOpencodeModelIds()); |
| const sanitizedEnabledOpencodeModels = migratedOpencodeModels.filter((id) => |
| validOpencodeModelIds.has(id) |
| ); |
|
|
| |
| const migratedOpencodeDefault = migrateOpencodeModelIds([ |
| serverSettings.opencodeDefaultModel ?? DEFAULT_OPENCODE_MODEL, |
| ])[0]; |
| const sanitizedOpencodeDefaultModel = validOpencodeModelIds.has(migratedOpencodeDefault) |
| ? migratedOpencodeDefault |
| : DEFAULT_OPENCODE_MODEL; |
|
|
| if (!sanitizedEnabledOpencodeModels.includes(sanitizedOpencodeDefaultModel)) { |
| sanitizedEnabledOpencodeModels.push(sanitizedOpencodeDefaultModel); |
| } |
|
|
| |
| const validGeminiModelIds = new Set(getAllGeminiModelIds()); |
| const sanitizedEnabledGeminiModels = (serverSettings.enabledGeminiModels ?? []).filter( |
| (id): id is GeminiModelId => validGeminiModelIds.has(id as GeminiModelId) |
| ); |
| const sanitizedGeminiDefaultModel = validGeminiModelIds.has( |
| serverSettings.geminiDefaultModel as GeminiModelId |
| ) |
| ? (serverSettings.geminiDefaultModel as GeminiModelId) |
| : DEFAULT_GEMINI_MODEL; |
|
|
| if (!sanitizedEnabledGeminiModels.includes(sanitizedGeminiDefaultModel)) { |
| sanitizedEnabledGeminiModels.push(sanitizedGeminiDefaultModel); |
| } |
|
|
| |
| const validCopilotModelIds = new Set(getAllCopilotModelIds()); |
| const sanitizedEnabledCopilotModels = (serverSettings.enabledCopilotModels ?? []).filter( |
| (id): id is CopilotModelId => validCopilotModelIds.has(id as CopilotModelId) |
| ); |
| const sanitizedCopilotDefaultModel = validCopilotModelIds.has( |
| serverSettings.copilotDefaultModel as CopilotModelId |
| ) |
| ? (serverSettings.copilotDefaultModel as CopilotModelId) |
| : DEFAULT_COPILOT_MODEL; |
|
|
| if (!sanitizedEnabledCopilotModels.includes(sanitizedCopilotDefaultModel)) { |
| sanitizedEnabledCopilotModels.push(sanitizedCopilotDefaultModel); |
| } |
|
|
| const persistedDynamicModelIds = |
| serverSettings.enabledDynamicModelIds ?? currentAppState.enabledDynamicModelIds; |
| const sanitizedDynamicModelIds = persistedDynamicModelIds.filter( |
| (modelId) => !modelId.startsWith('amazon-bedrock/') |
| ); |
|
|
| const persistedKnownDynamicModelIds = |
| serverSettings.knownDynamicModelIds ?? currentAppState.knownDynamicModelIds; |
| const sanitizedKnownDynamicModelIds = persistedKnownDynamicModelIds.filter( |
| (modelId) => !modelId.startsWith('amazon-bedrock/') |
| ); |
|
|
| |
| const migratedPhaseModels = serverSettings.phaseModels |
| ? { |
| enhancementModel: migratePhaseModelEntry(serverSettings.phaseModels.enhancementModel), |
| fileDescriptionModel: migratePhaseModelEntry( |
| serverSettings.phaseModels.fileDescriptionModel |
| ), |
| imageDescriptionModel: migratePhaseModelEntry( |
| serverSettings.phaseModels.imageDescriptionModel |
| ), |
| validationModel: migratePhaseModelEntry(serverSettings.phaseModels.validationModel), |
| specGenerationModel: migratePhaseModelEntry( |
| serverSettings.phaseModels.specGenerationModel |
| ), |
| featureGenerationModel: migratePhaseModelEntry( |
| serverSettings.phaseModels.featureGenerationModel |
| ), |
| backlogPlanningModel: migratePhaseModelEntry( |
| serverSettings.phaseModels.backlogPlanningModel |
| ), |
| projectAnalysisModel: migratePhaseModelEntry( |
| serverSettings.phaseModels.projectAnalysisModel |
| ), |
| ideationModel: migratePhaseModelEntry(serverSettings.phaseModels.ideationModel), |
| memoryExtractionModel: migratePhaseModelEntry( |
| serverSettings.phaseModels.memoryExtractionModel |
| ), |
| commitMessageModel: migratePhaseModelEntry(serverSettings.phaseModels.commitMessageModel), |
| prDescriptionModel: migratePhaseModelEntry(serverSettings.phaseModels.prDescriptionModel), |
| } |
| : undefined; |
|
|
| |
| if (serverSettings.theme) { |
| setItem(THEME_STORAGE_KEY, serverSettings.theme); |
| } |
|
|
| |
| const restoredAutoModeByWorktree: Record< |
| string, |
| { |
| isRunning: boolean; |
| runningTasks: string[]; |
| branchName: string | null; |
| maxConcurrency: number; |
| } |
| > = {}; |
| if (serverSettings.autoModeByWorktree) { |
| const persistedSettings = serverSettings.autoModeByWorktree as Record< |
| string, |
| { maxConcurrency?: number; branchName?: string | null } |
| >; |
| for (const [key, value] of Object.entries(persistedSettings)) { |
| restoredAutoModeByWorktree[key] = { |
| isRunning: false, |
| runningTasks: [], |
| branchName: value.branchName ?? null, |
| maxConcurrency: value.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY, |
| }; |
| } |
| } |
|
|
| useAppStore.setState({ |
| theme: serverSettings.theme as unknown as ThemeMode, |
| sidebarOpen: serverSettings.sidebarOpen, |
| sidebarStyle: serverSettings.sidebarStyle ?? 'unified', |
| collapsedNavSections: serverSettings.collapsedNavSections ?? {}, |
| chatHistoryOpen: serverSettings.chatHistoryOpen, |
| maxConcurrency: serverSettings.maxConcurrency, |
| autoModeByWorktree: restoredAutoModeByWorktree, |
| defaultSkipTests: serverSettings.defaultSkipTests, |
| enableDependencyBlocking: serverSettings.enableDependencyBlocking, |
| skipVerificationInAutoMode: serverSettings.skipVerificationInAutoMode, |
| mergePostAction: serverSettings.mergePostAction ?? null, |
| useWorktrees: serverSettings.useWorktrees, |
| defaultPlanningMode: serverSettings.defaultPlanningMode, |
| defaultRequirePlanApproval: serverSettings.defaultRequirePlanApproval, |
| defaultFeatureModel: serverSettings.defaultFeatureModel |
| ? migratePhaseModelEntry(serverSettings.defaultFeatureModel) |
| : { model: 'claude-opus', thinkingLevel: 'adaptive' }, |
| muteDoneSound: serverSettings.muteDoneSound, |
| defaultMaxTurns: serverSettings.defaultMaxTurns ?? 10000, |
| disableSplashScreen: serverSettings.disableSplashScreen ?? false, |
| defaultSortNewestCardOnTop: serverSettings.defaultSortNewestCardOnTop ?? false, |
| serverLogLevel: serverSettings.serverLogLevel ?? 'info', |
| enableRequestLogging: serverSettings.enableRequestLogging ?? true, |
| enhancementModel: serverSettings.enhancementModel, |
| validationModel: serverSettings.validationModel, |
| phaseModels: { |
| ...DEFAULT_PHASE_MODELS, |
| ...(migratedPhaseModels ?? serverSettings.phaseModels), |
| }, |
| defaultThinkingLevel: serverSettings.defaultThinkingLevel ?? 'adaptive', |
| defaultReasoningEffort: serverSettings.defaultReasoningEffort ?? 'none', |
| enabledCursorModels: allCursorModels, |
| cursorDefaultModel: sanitizedCursorDefault, |
| enabledOpencodeModels: sanitizedEnabledOpencodeModels, |
| opencodeDefaultModel: sanitizedOpencodeDefaultModel, |
| enabledGeminiModels: sanitizedEnabledGeminiModels, |
| geminiDefaultModel: sanitizedGeminiDefaultModel, |
| enabledCopilotModels: sanitizedEnabledCopilotModels, |
| copilotDefaultModel: sanitizedCopilotDefaultModel, |
| enabledDynamicModelIds: sanitizedDynamicModelIds, |
| knownDynamicModelIds: sanitizedKnownDynamicModelIds, |
| disabledProviders: serverSettings.disabledProviders ?? [], |
| autoLoadClaudeMd: serverSettings.autoLoadClaudeMd ?? true, |
| useClaudeCodeSystemPrompt: serverSettings.useClaudeCodeSystemPrompt ?? true, |
| keyboardShortcuts: { |
| ...currentAppState.keyboardShortcuts, |
| ...(serverSettings.keyboardShortcuts as unknown as Partial< |
| typeof currentAppState.keyboardShortcuts |
| >), |
| }, |
| mcpServers: serverSettings.mcpServers, |
| defaultEditorCommand: serverSettings.defaultEditorCommand ?? null, |
| editorFontSize: serverSettings.editorFontSize ?? 13, |
| editorFontFamily: serverSettings.editorFontFamily ?? 'default', |
| editorAutoSave: serverSettings.editorAutoSave ?? false, |
| editorAutoSaveDelay: serverSettings.editorAutoSaveDelay ?? 1000, |
| defaultTerminalId: serverSettings.defaultTerminalId ?? null, |
| promptCustomization: serverSettings.promptCustomization ?? {}, |
| |
| claudeCompatibleProviders: serverSettings.claudeCompatibleProviders ?? [], |
| |
| claudeApiProfiles: serverSettings.claudeApiProfiles ?? [], |
| activeClaudeApiProfileId: serverSettings.activeClaudeApiProfileId ?? null, |
| projects: serverSettings.projects, |
| trashedProjects: serverSettings.trashedProjects, |
| projectHistory: serverSettings.projectHistory, |
| projectHistoryIndex: serverSettings.projectHistoryIndex, |
| lastSelectedSessionByProject: serverSettings.lastSelectedSessionByProject, |
| agentModelBySession: serverSettings.agentModelBySession |
| ? Object.fromEntries( |
| Object.entries(serverSettings.agentModelBySession as Record<string, unknown>).map( |
| ([sessionId, entry]) => [ |
| sessionId, |
| migratePhaseModelEntry(entry as string | PhaseModelEntry | null | undefined), |
| ] |
| ) |
| ) |
| : currentAppState.agentModelBySession, |
| |
| |
| currentWorktreeByProject: sanitizeWorktreeByProject( |
| serverSettings.currentWorktreeByProject ?? currentAppState.currentWorktreeByProject |
| ), |
| |
| worktreePanelCollapsed: serverSettings.worktreePanelCollapsed ?? false, |
| lastProjectDir: serverSettings.lastProjectDir ?? '', |
| recentFolders: serverSettings.recentFolders ?? [], |
| |
| eventHooks: serverSettings.eventHooks ?? [], |
| |
| ntfyEndpoints: serverSettings.ntfyEndpoints ?? [], |
| |
| featureTemplates: serverSettings.featureTemplates ?? [], |
| |
| codexAutoLoadAgents: serverSettings.codexAutoLoadAgents ?? false, |
| codexSandboxMode: serverSettings.codexSandboxMode ?? 'workspace-write', |
| codexApprovalPolicy: serverSettings.codexApprovalPolicy ?? 'on-request', |
| codexEnableWebSearch: serverSettings.codexEnableWebSearch ?? false, |
| codexEnableImages: serverSettings.codexEnableImages ?? true, |
| codexAdditionalDirs: serverSettings.codexAdditionalDirs ?? [], |
| codexThreadId: serverSettings.codexThreadId, |
| |
| ...((serverSettings.terminalFontFamily || |
| serverSettings.openTerminalMode || |
| (serverSettings as unknown as Record<string, unknown>).terminalCustomBackgroundColor !== |
| undefined || |
| (serverSettings as unknown as Record<string, unknown>).terminalCustomForegroundColor !== |
| undefined) && { |
| terminalState: { |
| ...currentAppState.terminalState, |
| ...(serverSettings.terminalFontFamily && { |
| fontFamily: serverSettings.terminalFontFamily, |
| }), |
| ...(serverSettings.openTerminalMode && { |
| openTerminalMode: serverSettings.openTerminalMode, |
| }), |
| ...((serverSettings as unknown as Record<string, unknown>) |
| .terminalCustomBackgroundColor !== undefined && { |
| customBackgroundColor: (serverSettings as unknown as Record<string, unknown>) |
| .terminalCustomBackgroundColor as string | null, |
| }), |
| ...((serverSettings as unknown as Record<string, unknown>) |
| .terminalCustomForegroundColor !== undefined && { |
| customForegroundColor: (serverSettings as unknown as Record<string, unknown>) |
| .terminalCustomForegroundColor as string | null, |
| }), |
| }, |
| }), |
| }); |
|
|
| |
| useSetupStore.setState({ |
| setupComplete: serverSettings.setupComplete ?? false, |
| isFirstRun: serverSettings.isFirstRun ?? true, |
| skipClaudeSetup: serverSettings.skipClaudeSetup ?? false, |
| currentStep: serverSettings.setupComplete ? 'complete' : 'welcome', |
| }); |
|
|
| logger.info('Settings refreshed from server'); |
| return true; |
| } catch (error) { |
| logger.error('Failed to refresh settings from server:', error); |
| return false; |
| } |
| } |
|
|