import type { DesktopSettings } from '@/lib/desktop'; import { sanitizeWorkStatusHiddenSections } from '@/components/chat/work-status/sections'; import { createProjectIdFromPath } from '@/lib/projectId'; import { useUIStore } from '@/stores/useUIStore'; import { isMonoFontOption, isUiFontOption } from '@/lib/fontOptions'; import { DEFAULT_FOLLOW_UP_BEHAVIOR, isFollowUpBehavior, normalizeFollowUpBehavior, useMessageQueueStore, type FollowUpBehavior, } from '@/stores/messageQueueStore'; import { setDirectoryShowHidden } from '@/lib/directoryShowHidden'; import { setFilesViewShowGitignored } from '@/lib/filesViewShowGitignored'; import { loadAppearancePreferences, applyAppearancePreferences } from '@/lib/appearancePersistence'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { sanitizeStarterRefs } from '@/lib/draftStarters'; import { normalizeMobileKeyboardMode, setStoredMobileKeyboardMode } from '@/lib/mobileKeyboardMode'; import { runtimeFetch } from '@/lib/runtime-fetch'; import { isTerminalShell } from '@/lib/terminalShell'; import { getRuntimeKey, subscribeRuntimeEndpointChanged, subscribeRuntimeEndpointWillChange } from '@/lib/runtime-switch'; import { DEFAULT_DARK_THEME_ID, DEFAULT_LIGHT_THEME_ID } from '@/lib/theme/themes'; import { DEFAULT_OPEN_IN_APP_ID } from '@/lib/openInApps'; import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore'; export const applyPersistedHomeDirectoryToWindow = (homeDirectory: string): void => { if (typeof window === 'undefined') { return; } if (typeof window.__OPENCHAMBER_HOME__ === 'string' && window.__OPENCHAMBER_HOME__.length > 0) { return; } try { window.__OPENCHAMBER_HOME__ = homeDirectory; } catch { /* read-only contextBridge property — leave preload-seeded value */ } }; const SETTINGS_MIRROR_INDEX_KEY = 'openchamber.settingsMirror.v2.index'; const SETTINGS_MIRROR_KEY_PREFIX = 'openchamber.settingsMirror.v2:'; const MAX_SETTINGS_MIRROR_RUNTIMES = 5; export const getRuntimeSettingsMirrorStorageKey = (runtimeKey: string): string => `${SETTINGS_MIRROR_KEY_PREFIX}${encodeURIComponent(runtimeKey)}`; const setOrRemoveLocalStorage = (key: string, value: string | null): void => { if (value === null) { localStorage.removeItem(key); } else { localStorage.setItem(key, value); } }; const persistRuntimeSettingsMirror = (settings: DesktopSettings, runtimeKey: string): void => { const mirror = { themeId: settings.themeId, themeVariant: settings.themeVariant, lightThemeId: settings.lightThemeId, darkThemeId: settings.darkThemeId, useSystemTheme: settings.useSystemTheme, lastDirectory: settings.lastDirectory, homeDirectory: settings.homeDirectory, projects: settings.projects, activeProjectId: settings.activeProjectId, sidebarProjectDisplayMode: settings.sidebarProjectDisplayMode, sidebarSessionGroupingMode: settings.sidebarSessionGroupingMode, sidebarProjectSortOrder: settings.sidebarProjectSortOrder, sidebarShowRecentSection: settings.sidebarShowRecentSection, pinnedDirectories: settings.pinnedDirectories, gitmojiEnabled: settings.gitmojiEnabled, directoryShowHidden: settings.directoryShowHidden, filesViewShowGitignored: settings.filesViewShowGitignored, openInAppId: settings.openInAppId, pwaAppName: settings.pwaAppName, mobileKeyboardMode: settings.mobileKeyboardMode, openCodeUpdateToastDismissedVersion: settings.openCodeUpdateToastDismissedVersion, dictationEnabled: settings.dictationEnabled, sttProvider: settings.sttProvider, sttServerUrl: settings.sttServerUrl, sttModel: settings.sttModel, sttLocalModel: settings.sttLocalModel, sttLanguage: settings.sttLanguage, }; localStorage.setItem(getRuntimeSettingsMirrorStorageKey(runtimeKey), JSON.stringify(mirror)); let previous: string[] = []; try { const parsed = JSON.parse(localStorage.getItem(SETTINGS_MIRROR_INDEX_KEY) ?? '[]') as unknown; if (Array.isArray(parsed)) previous = parsed.filter((entry): entry is string => typeof entry === 'string'); } catch { previous = []; } const runtimes = [runtimeKey, ...previous.filter((entry) => entry !== runtimeKey)].slice(0, MAX_SETTINGS_MIRROR_RUNTIMES); for (const staleRuntime of previous) { if (!runtimes.includes(staleRuntime)) localStorage.removeItem(getRuntimeSettingsMirrorStorageKey(staleRuntime)); } localStorage.setItem(SETTINGS_MIRROR_INDEX_KEY, JSON.stringify(runtimes)); }; const persistToLocalStorage = (settings: DesktopSettings) => { if (typeof window === 'undefined') { return; } persistRuntimeSettingsMirror(settings, getRuntimeKey()); setOrRemoveLocalStorage('selectedThemeId', settings.themeId || null); setOrRemoveLocalStorage('selectedThemeVariant', settings.themeVariant || null); setOrRemoveLocalStorage('lightThemeId', settings.lightThemeId || null); setOrRemoveLocalStorage('darkThemeId', settings.darkThemeId || null); setOrRemoveLocalStorage('useSystemTheme', typeof settings.useSystemTheme === 'boolean' ? String(settings.useSystemTheme) : null); setOrRemoveLocalStorage('lastDirectory', settings.lastDirectory || null); if (settings.homeDirectory) { localStorage.setItem('homeDirectory', settings.homeDirectory); applyPersistedHomeDirectoryToWindow(settings.homeDirectory); } else { localStorage.removeItem('homeDirectory'); } if (Array.isArray(settings.projects) && settings.projects.length > 0) { localStorage.setItem('projects', JSON.stringify(settings.projects)); } else { localStorage.removeItem('projects'); } if (settings.activeProjectId) { localStorage.setItem('activeProjectId', settings.activeProjectId); } else { localStorage.removeItem('activeProjectId'); } if (Array.isArray(settings.pinnedDirectories) && settings.pinnedDirectories.length > 0) { localStorage.setItem('pinnedDirectories', JSON.stringify(settings.pinnedDirectories)); } else { localStorage.removeItem('pinnedDirectories'); } if (Array.isArray(settings.projects) && settings.projects.length > 0) { const collapsed = settings.projects .filter((project) => project.sidebarCollapsed === true) .map((project) => project.id) .filter((id): id is string => typeof id === 'string' && id.length > 0); if (collapsed.length > 0) { localStorage.setItem('oc.sessions.projectCollapse', JSON.stringify(collapsed)); } else { localStorage.removeItem('oc.sessions.projectCollapse'); } } else { localStorage.removeItem('oc.sessions.projectCollapse'); } if (typeof settings.gitmojiEnabled === 'boolean') { localStorage.setItem('gitmojiEnabled', String(settings.gitmojiEnabled)); } else { localStorage.removeItem('gitmojiEnabled'); } if (typeof settings.directoryShowHidden === 'boolean') { localStorage.setItem('directoryTreeShowHidden', settings.directoryShowHidden ? 'true' : 'false'); } else { localStorage.removeItem('directoryTreeShowHidden'); } if (typeof settings.filesViewShowGitignored === 'boolean') { localStorage.setItem('filesViewShowGitignored', settings.filesViewShowGitignored ? 'true' : 'false'); } else { localStorage.removeItem('filesViewShowGitignored'); } setOrRemoveLocalStorage('openInAppId', typeof settings.openInAppId === 'string' && settings.openInAppId.length > 0 ? settings.openInAppId : null); if (typeof settings.pwaAppName === 'string') { const normalized = settings.pwaAppName.trim().replace(/\s+/g, ' ').slice(0, 64); if (normalized.length > 0) { localStorage.setItem('openchamber.pwaName', normalized); } else { localStorage.removeItem('openchamber.pwaName'); } } else { localStorage.removeItem('openchamber.pwaName'); } setStoredMobileKeyboardMode(settings.mobileKeyboardMode); if (typeof settings.openCodeUpdateToastDismissedVersion === 'string') { const version = settings.openCodeUpdateToastDismissedVersion.trim(); if (version) { localStorage.setItem('opencode-update-toast-dismissed-version', version); } else { localStorage.removeItem('opencode-update-toast-dismissed-version'); } } else { localStorage.removeItem('opencode-update-toast-dismissed-version'); } if (typeof settings.dictationEnabled === 'boolean') { localStorage.setItem('dictationEnabled', String(settings.dictationEnabled)); } else { localStorage.removeItem('dictationEnabled'); } if (settings.sttProvider === 'local' || settings.sttProvider === 'openai-compatible') { localStorage.setItem('sttProvider', settings.sttProvider); } else { localStorage.removeItem('sttProvider'); } setOrRemoveLocalStorage('sttServerUrl', typeof settings.sttServerUrl === 'string' ? settings.sttServerUrl : null); setOrRemoveLocalStorage('sttModel', typeof settings.sttModel === 'string' ? settings.sttModel : null); setOrRemoveLocalStorage('sttLocalModel', typeof settings.sttLocalModel === 'string' ? settings.sttLocalModel : null); setOrRemoveLocalStorage('sttLanguage', typeof settings.sttLanguage === 'string' ? settings.sttLanguage : null); }; const dispatchSettingsSynced = (settings: DesktopSettings): void => { if (typeof window === 'undefined') { return; } window.dispatchEvent(new CustomEvent('openchamber:settings-synced', { detail: settings })); }; type SettingsSaveState = 'idle' | 'saving' | 'error'; let _settingsSaveState: SettingsSaveState = 'idle'; let _settingsSaveStateResetTimer: ReturnType | null = null; const _settingsSaveStateListeners = new Set<() => void>(); export const getSettingsSaveState = (): SettingsSaveState => _settingsSaveState; export const subscribeToSettingsSaveState = (listener: () => void): (() => void) => { _settingsSaveStateListeners.add(listener); return () => _settingsSaveStateListeners.delete(listener); }; /** * Drive the shared settings save indicator from pages that persist through * their own APIs instead of updateDesktopSettings. 'error' resets to idle. */ export const reportSettingsSaveState = (state: 'saving' | 'saved' | 'error'): void => { dispatchSettingsSaveState(state); }; const dispatchSettingsSaveState = (state: 'saving' | 'saved' | 'error'): void => { if (_settingsSaveStateResetTimer) { clearTimeout(_settingsSaveStateResetTimer); _settingsSaveStateResetTimer = null; } // Quiet indicator: success is the normal case and renders nothing ('saved' → idle); // only in-flight saves and failures surface in the UI. const nextState: SettingsSaveState = state === 'saved' ? 'idle' : state; if (nextState !== _settingsSaveState) { _settingsSaveState = nextState; _settingsSaveStateListeners.forEach((listener) => listener()); } if (nextState === 'error') { _settingsSaveStateResetTimer = setTimeout(() => dispatchSettingsSaveState('saved'), 6000); } if (typeof window === 'undefined') { return; } window.dispatchEvent(new CustomEvent<'saving' | 'saved' | 'error'>('openchamber:settings-save-state', { detail: state })); }; type PersistApi = { hasHydrated?: () => boolean; onFinishHydration?: (callback: () => void) => (() => void) | undefined; }; const sanitizeSkillCatalogs = (value: unknown): DesktopSettings['skillCatalogs'] | undefined => { if (!Array.isArray(value)) { return undefined; } const result: NonNullable = []; const seen = new Set(); for (const entry of value) { if (!entry || typeof entry !== 'object') continue; const candidate = entry as Record; const id = typeof candidate.id === 'string' ? candidate.id.trim() : ''; const label = typeof candidate.label === 'string' ? candidate.label.trim() : ''; const source = typeof candidate.source === 'string' ? candidate.source.trim() : ''; const subpath = typeof candidate.subpath === 'string' ? candidate.subpath.trim() : ''; const gitIdentityId = typeof candidate.gitIdentityId === 'string' ? candidate.gitIdentityId.trim() : ''; if (!id || !label || !source) continue; if (seen.has(id)) continue; seen.add(id); const catalog: NonNullable[number] = { id, label, source, }; if (subpath) catalog.subpath = subpath; if (gitIdentityId) catalog.gitIdentityId = gitIdentityId; result.push(catalog); } return result; }; const sanitizeShortcutOverrides = (value: unknown): Record | undefined => { if (!value || typeof value !== 'object' || Array.isArray(value)) { return undefined; } const result: Record = {}; for (const [key, combo] of Object.entries(value)) { const normalizedKey = typeof key === 'string' ? key.trim() : ''; const normalizedCombo = typeof combo === 'string' ? combo.trim() : ''; if (!normalizedKey || !normalizedCombo) continue; result[normalizedKey] = normalizedCombo; } return result; }; const areStringRecordsEqual = (left: Record, right: Record): boolean => { const leftEntries = Object.entries(left); const rightEntries = Object.entries(right); if (leftEntries.length !== rightEntries.length) return false; return leftEntries.every(([key, value]) => right[key] === value); }; const areModelRefsEqual = ( left: Array<{ providerID: string; modelID: string }>, right: Array<{ providerID: string; modelID: string }>, ): boolean => ( left.length === right.length && left.every((item, idx) => item.providerID === right[idx]?.providerID && item.modelID === right[idx]?.modelID) ); const areStringArraysEqual = (left: string[], right: string[]): boolean => ( left.length === right.length && left.every((value, idx) => value === right[idx]) ); const sanitizeStringArray = (value: unknown): string[] | undefined => { if (!Array.isArray(value)) return undefined; return Array.from(new Set(value.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0))); }; const sanitizeRecentEfforts = (value: unknown): Record | undefined => { if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined; const result: Record = {}; for (const [key, variants] of Object.entries(value)) { if (!key || !Array.isArray(variants)) continue; const sanitized = sanitizeStringArray(variants); if (sanitized && sanitized.length > 0) { result[key] = sanitized.slice(0, 5); } } return Object.keys(result).length > 0 ? result : undefined; }; const areRecentEffortsEqual = (left: Record, right: Record): boolean => { const leftKeys = Object.keys(left); if (leftKeys.length !== Object.keys(right).length) return false; return leftKeys.every((key) => Array.isArray(right[key]) && areStringArraysEqual(left[key], right[key])); }; const HEX_COLOR_PATTERN = /^#(?:[\da-fA-F]{3}|[\da-fA-F]{6})$/; const normalizeIconBackground = (value: unknown): string | null => { if (typeof value !== 'string') { return null; } const trimmed = value.trim(); if (!trimmed) { return null; } return HEX_COLOR_PATTERN.test(trimmed) ? trimmed.toLowerCase() : null; }; const sanitizeProjects = (value: unknown): DesktopSettings['projects'] | undefined => { if (!Array.isArray(value)) { return undefined; } const result: NonNullable = []; const seenIds = new Set(); const seenPaths = new Set(); for (const entry of value) { if (!entry || typeof entry !== 'object') continue; const candidate = entry as Record; const rawPath = typeof candidate.path === 'string' ? candidate.path.trim() : ''; if (!rawPath) continue; const normalizedPath = rawPath === '/' ? rawPath : rawPath.replace(/\\/g, '/').replace(/\/+$/, ''); if (!normalizedPath) continue; const id = createProjectIdFromPath(normalizedPath); if (!id) continue; if (seenIds.has(id) || seenPaths.has(normalizedPath)) continue; seenIds.add(id); seenPaths.add(normalizedPath); const project: NonNullable[number] = { id, path: normalizedPath, }; if (typeof candidate.label === 'string' && candidate.label.trim().length > 0) { project.label = candidate.label.trim(); } if (typeof candidate.icon === 'string' && candidate.icon.trim().length > 0) { project.icon = candidate.icon.trim(); } if (candidate.iconImage === null) { project.iconImage = null; } else if (candidate.iconImage && typeof candidate.iconImage === 'object') { const iconImage = candidate.iconImage as Record; const mime = typeof iconImage.mime === 'string' ? iconImage.mime.trim() : ''; const updatedAt = typeof iconImage.updatedAt === 'number' && Number.isFinite(iconImage.updatedAt) ? Math.max(0, Math.round(iconImage.updatedAt)) : 0; const source = iconImage.source === 'custom' || iconImage.source === 'auto' ? iconImage.source : null; if (mime && updatedAt > 0 && source) { project.iconImage = { mime, updatedAt, source }; } } if (typeof candidate.color === 'string' && candidate.color.trim().length > 0) { project.color = candidate.color.trim(); } if (candidate.iconBackground === null) { project.iconBackground = null; } else { const iconBackground = normalizeIconBackground(candidate.iconBackground); if (iconBackground) { project.iconBackground = iconBackground; } } if (typeof candidate.addedAt === 'number' && Number.isFinite(candidate.addedAt) && candidate.addedAt >= 0) { project.addedAt = candidate.addedAt; } if ( typeof candidate.lastOpenedAt === 'number' && Number.isFinite(candidate.lastOpenedAt) && candidate.lastOpenedAt >= 0 ) { project.lastOpenedAt = candidate.lastOpenedAt; } if (typeof candidate.sidebarCollapsed === 'boolean') { project.sidebarCollapsed = candidate.sidebarCollapsed; } result.push(project); } return result.length > 0 ? result : undefined; }; const sanitizeManagedRemoteTunnelPresets = (value: unknown): DesktopSettings['managedRemoteTunnelPresets'] | undefined => { if (!Array.isArray(value)) { return undefined; } const result: NonNullable = []; const seenIds = new Set(); const seenHostnames = new Set(); for (const entry of value) { if (!entry || typeof entry !== 'object') continue; const candidate = entry as Record; const id = typeof candidate.id === 'string' ? candidate.id.trim() : ''; const name = typeof candidate.name === 'string' ? candidate.name.trim() : ''; const hostname = typeof candidate.hostname === 'string' ? candidate.hostname.trim().toLowerCase() : ''; if (!id || !name || !hostname) continue; if (seenIds.has(id) || seenHostnames.has(hostname)) continue; seenIds.add(id); seenHostnames.add(hostname); result.push({ id, name, hostname }); } return result; }; const sanitizeManagedRemoteTunnelPresetTokens = (value: unknown): DesktopSettings['managedRemoteTunnelPresetTokens'] | undefined => { if (!value || typeof value !== 'object' || Array.isArray(value)) { return undefined; } const candidate = value as Record; const result: Record = {}; for (const [key, tokenValue] of Object.entries(candidate)) { const id = key.trim(); const token = typeof tokenValue === 'string' ? tokenValue.trim() : ''; if (!id || !token) continue; result[id] = token; } return Object.keys(result).length > 0 ? result : undefined; }; const sanitizeModelRefs = (value: unknown, limit: number): Array<{ providerID: string; modelID: string }> | undefined => { if (!Array.isArray(value)) { return undefined; } const result: Array<{ providerID: string; modelID: string }> = []; const seen = new Set(); for (const entry of value) { if (!entry || typeof entry !== 'object') continue; const candidate = entry as Record; const providerID = typeof candidate.providerID === 'string' ? candidate.providerID.trim() : ''; const modelID = typeof candidate.modelID === 'string' ? candidate.modelID.trim() : ''; if (!providerID || !modelID) continue; const key = `${providerID}/${modelID}`; if (seen.has(key)) continue; seen.add(key); result.push({ providerID, modelID }); if (result.length >= limit) break; } return result; }; const getPersistApi = (): PersistApi | undefined => { const candidate = useUIStore.persist; if (candidate && typeof candidate === 'object') { return candidate; } return undefined; }; const getRuntimeSettingsAPI = () => getRegisteredRuntimeAPIs()?.settings ?? null; const materializeAuthoritativeUiSettings = (settings: DesktopSettings): DesktopSettings => { const defaults = useUIStore.getInitialState(); return { useSystemTheme: true, lightThemeId: DEFAULT_LIGHT_THEME_ID, darkThemeId: DEFAULT_DARK_THEME_ID, openInAppId: DEFAULT_OPEN_IN_APP_ID, showReasoningTraces: defaults.showReasoningTraces, streamingAutoFollowEnabled: defaults.streamingAutoFollowEnabled, workStatusPanelEnabled: defaults.workStatusPanelEnabled, workStatusHiddenSections: defaults.workStatusHiddenSections, sessionRecapEnabled: defaults.sessionRecapEnabled, sessionSuggestionEnabled: defaults.sessionSuggestionEnabled, sessionGoalEnabled: defaults.sessionGoalEnabled, sessionGoalDefaultBudgetEnabled: defaults.sessionGoalDefaultBudgetEnabled, sessionGoalDefaultBudget: defaults.sessionGoalDefaultBudget, collapsibleThinkingBlocks: defaults.collapsibleThinkingBlocks, autoDeleteEnabled: defaults.autoDeleteEnabled, autoSaveEnabled: defaults.autoSaveEnabled, autoDeleteAfterDays: defaults.autoDeleteAfterDays, sessionRetentionAction: defaults.sessionRetentionAction, followUpBehavior: DEFAULT_FOLLOW_UP_BEHAVIOR, showDeletionDialog: defaults.showDeletionDialog, nativeNotificationsEnabled: defaults.nativeNotificationsEnabled, notificationMode: defaults.notificationMode, notifyOnSubtasks: defaults.notifyOnSubtasks, notifyOnCompletion: defaults.notifyOnCompletion, notifyOnError: defaults.notifyOnError, notifyOnQuestion: defaults.notifyOnQuestion, notificationTemplates: defaults.notificationTemplates, summarizeLastMessage: defaults.summarizeLastMessage, summaryThreshold: defaults.summaryThreshold, summaryLength: defaults.summaryLength, maxLastMessageLength: defaults.maxLastMessageLength, inputSpellcheckEnabled: defaults.inputSpellcheckEnabled, showOpenCodeUpdateNotifications: defaults.showOpenCodeUpdateNotifications, agentControlToolEnabled: defaults.agentControlToolEnabled, agentWebToolEnabled: defaults.agentWebToolEnabled, agentMemoryToolEnabled: defaults.agentMemoryToolEnabled, showToolFileIcons: defaults.showToolFileIcons, codeBlockLineWrap: defaults.codeBlockLineWrap, showTurnChangedFiles: defaults.showTurnChangedFiles, showExpandedBashTools: defaults.showExpandedBashTools, showExpandedEditTools: defaults.showExpandedEditTools, timeFormatPreference: defaults.timeFormatPreference, weekStartPreference: defaults.weekStartPreference, desktopWindowControlsPosition: defaults.desktopWindowControlsPosition, desktopWindowControlsStyle: defaults.desktopWindowControlsStyle, chatRenderMode: defaults.chatRenderMode, activityRenderMode: defaults.activityRenderMode, mermaidRenderingMode: defaults.mermaidRenderingMode, userMessageRenderingMode: defaults.userMessageRenderingMode, collapsibleUserMessages: defaults.collapsibleUserMessages, messageStreamTransport: 'auto', stickyUserHeader: defaults.stickyUserHeader, promptNavigatorEnabled: defaults.promptNavigatorEnabled, wideChatLayoutEnabled: defaults.wideChatLayoutEnabled, showSplitAssistantMessageActions: defaults.showSplitAssistantMessageActions, draftStartersVisible: defaults.draftStartersVisible, reportUsage: defaults.reportUsage, fontSize: defaults.fontSize, terminalFontSize: defaults.terminalFontSize, terminalShell: defaults.terminalShell, terminalLoginShells: defaults.terminalLoginShells, editorFontSize: defaults.editorFontSize, uiFont: defaults.uiFont, monoFont: defaults.monoFont, padding: defaults.padding, cornerRadius: defaults.cornerRadius, inputBarOffset: defaults.inputBarOffset, shortcutOverrides: defaults.shortcutOverrides, mobileKeyboardMode: 'resize-content', favoriteModels: defaults.favoriteModels, hiddenModels: defaults.hiddenModels, collapsedModelProviders: defaults.collapsedModelProviders, recentModels: defaults.recentModels, recentAgents: defaults.recentAgents, recentEfforts: defaults.recentEfforts, diffLayoutPreference: defaults.diffLayoutPreference, gitChangesViewMode: defaults.gitChangesViewMode, directoryShowHidden: true, filesViewShowGitignored: false, dictationEnabled: true, sttProvider: 'local', sttServerUrl: 'http://localhost:8001/v1', sttModel: 'deepdml/faster-whisper-large-v3-turbo-ct2', sttLocalModel: 'parakeet-tdt-0.6b-v2-int8', sttLanguage: '', ...settings, }; }; const applyDesktopUiPreferences = (settings: DesktopSettings) => { const store = useUIStore.getState(); const configStore = typeof window !== 'undefined' ? window.__zustand_config_store__?.getState?.() ?? null : null; const configStoreApi = typeof window !== 'undefined' ? window.__zustand_config_store__ ?? null : null; const queueStore = useMessageQueueStore.getState(); if (typeof settings.workStatusPanelEnabled === 'boolean' && settings.workStatusPanelEnabled !== store.workStatusPanelEnabled) { store.setWorkStatusPanelEnabled(settings.workStatusPanelEnabled); } if (Array.isArray(settings.workStatusHiddenSections)) { const next = sanitizeWorkStatusHiddenSections(settings.workStatusHiddenSections); if (next.join('\u0000') !== store.workStatusHiddenSections.join('\u0000')) { store.setWorkStatusHiddenSections(next); } } if (typeof settings.showReasoningTraces === 'boolean' && settings.showReasoningTraces !== store.showReasoningTraces) { store.setShowReasoningTraces(settings.showReasoningTraces); } if (typeof settings.streamingAutoFollowEnabled === 'boolean' && settings.streamingAutoFollowEnabled !== store.streamingAutoFollowEnabled) { store.setStreamingAutoFollowEnabled(settings.streamingAutoFollowEnabled); } if (typeof settings.sessionRecapEnabled === 'boolean' && settings.sessionRecapEnabled !== store.sessionRecapEnabled) { store.setSessionRecapEnabled(settings.sessionRecapEnabled); } if (typeof settings.sessionSuggestionEnabled === 'boolean' && settings.sessionSuggestionEnabled !== store.sessionSuggestionEnabled) { store.setSessionSuggestionEnabled(settings.sessionSuggestionEnabled); } if (typeof settings.sessionGoalEnabled === 'boolean' && settings.sessionGoalEnabled !== store.sessionGoalEnabled) { store.setSessionGoalEnabled(settings.sessionGoalEnabled); } if (typeof settings.sessionGoalDefaultBudgetEnabled === 'boolean' && settings.sessionGoalDefaultBudgetEnabled !== store.sessionGoalDefaultBudgetEnabled) { store.setSessionGoalDefaultBudgetEnabled(settings.sessionGoalDefaultBudgetEnabled); } if (typeof settings.sessionGoalDefaultBudget === 'number' && Number.isFinite(settings.sessionGoalDefaultBudget) && settings.sessionGoalDefaultBudget !== store.sessionGoalDefaultBudget) { store.setSessionGoalDefaultBudget(settings.sessionGoalDefaultBudget); } if (typeof settings.collapsibleThinkingBlocks === 'boolean' && settings.collapsibleThinkingBlocks !== store.collapsibleThinkingBlocks) { store.setCollapsibleThinkingBlocks(settings.collapsibleThinkingBlocks); } if (typeof settings.autoDeleteEnabled === 'boolean' && settings.autoDeleteEnabled !== store.autoDeleteEnabled) { store.setAutoDeleteEnabled(settings.autoDeleteEnabled); } if (typeof settings.autoSaveEnabled === 'boolean' && settings.autoSaveEnabled !== store.autoSaveEnabled) { store.setAutoSaveEnabled(settings.autoSaveEnabled); } if (typeof settings.autoDeleteAfterDays === 'number' && Number.isFinite(settings.autoDeleteAfterDays)) { const normalized = Math.max(1, Math.min(365, settings.autoDeleteAfterDays)); if (normalized !== store.autoDeleteAfterDays) { store.setAutoDeleteAfterDays(normalized); } } if (settings.sessionRetentionAction === 'archive' || settings.sessionRetentionAction === 'delete') { if (settings.sessionRetentionAction !== store.sessionRetentionAction) { store.setSessionRetentionAction(settings.sessionRetentionAction); } } let nextFollowUpBehavior: FollowUpBehavior | null = null; if (isFollowUpBehavior(settings.followUpBehavior)) { nextFollowUpBehavior = settings.followUpBehavior; } else if (typeof settings.queueModeEnabled === 'boolean') { nextFollowUpBehavior = normalizeFollowUpBehavior(undefined, settings.queueModeEnabled); } if (nextFollowUpBehavior && nextFollowUpBehavior !== queueStore.followUpBehavior) { queueStore.setFollowUpBehavior(nextFollowUpBehavior); } if (typeof settings.showDeletionDialog === 'boolean' && settings.showDeletionDialog !== store.showDeletionDialog) { store.setShowDeletionDialog(settings.showDeletionDialog); } if (typeof settings.nativeNotificationsEnabled === 'boolean' && settings.nativeNotificationsEnabled !== store.nativeNotificationsEnabled) { store.setNativeNotificationsEnabled(settings.nativeNotificationsEnabled); } if (typeof settings.notificationMode === 'string' && (settings.notificationMode === 'always' || settings.notificationMode === 'hidden-only')) { if (settings.notificationMode !== store.notificationMode) { store.setNotificationMode(settings.notificationMode); } } if (typeof settings.notifyOnSubtasks === 'boolean' && settings.notifyOnSubtasks !== store.notifyOnSubtasks) { store.setNotifyOnSubtasks(settings.notifyOnSubtasks); } if (typeof settings.notifyOnCompletion === 'boolean' && settings.notifyOnCompletion !== store.notifyOnCompletion) { store.setNotifyOnCompletion(settings.notifyOnCompletion); } if (typeof settings.notifyOnError === 'boolean' && settings.notifyOnError !== store.notifyOnError) { store.setNotifyOnError(settings.notifyOnError); } if (typeof settings.notifyOnQuestion === 'boolean' && settings.notifyOnQuestion !== store.notifyOnQuestion) { store.setNotifyOnQuestion(settings.notifyOnQuestion); } if (settings.notificationTemplates && typeof settings.notificationTemplates === 'object') { store.setNotificationTemplates(settings.notificationTemplates); } if (typeof settings.summarizeLastMessage === 'boolean' && settings.summarizeLastMessage !== store.summarizeLastMessage) { store.setSummarizeLastMessage(settings.summarizeLastMessage); } if (typeof settings.summaryThreshold === 'number' && Number.isFinite(settings.summaryThreshold)) { store.setSummaryThreshold(settings.summaryThreshold); } if (typeof settings.summaryLength === 'number' && Number.isFinite(settings.summaryLength)) { store.setSummaryLength(settings.summaryLength); } if (typeof settings.maxLastMessageLength === 'number' && Number.isFinite(settings.maxLastMessageLength)) { store.setMaxLastMessageLength(settings.maxLastMessageLength); } if (typeof settings.inputSpellcheckEnabled === 'boolean' && settings.inputSpellcheckEnabled !== store.inputSpellcheckEnabled) { store.setInputSpellcheckEnabled(settings.inputSpellcheckEnabled); } if ( typeof settings.showOpenCodeUpdateNotifications === 'boolean' && settings.showOpenCodeUpdateNotifications !== store.showOpenCodeUpdateNotifications ) { store.setShowOpenCodeUpdateNotifications(settings.showOpenCodeUpdateNotifications); } if ( typeof settings.agentControlToolEnabled === 'boolean' && settings.agentControlToolEnabled !== store.agentControlToolEnabled ) { store.setAgentControlToolEnabled(settings.agentControlToolEnabled); } if ( typeof settings.agentWebToolEnabled === 'boolean' && settings.agentWebToolEnabled !== store.agentWebToolEnabled ) { store.setAgentWebToolEnabled(settings.agentWebToolEnabled); } if ( typeof settings.agentMemoryToolEnabled === 'boolean' && settings.agentMemoryToolEnabled !== store.agentMemoryToolEnabled ) { store.setAgentMemoryToolEnabled(settings.agentMemoryToolEnabled); } // Server-owned: it says whether this build has the feature at all. if ( typeof settings.agentMemoryFeatureAvailable === 'boolean' && settings.agentMemoryFeatureAvailable !== store.agentMemoryFeatureAvailable ) { store.setAgentMemoryFeatureAvailable(settings.agentMemoryFeatureAvailable); } if (typeof settings.showToolFileIcons === 'boolean' && settings.showToolFileIcons !== store.showToolFileIcons) { store.setShowToolFileIcons(settings.showToolFileIcons); } if (typeof settings.codeBlockLineWrap === 'boolean' && settings.codeBlockLineWrap !== store.codeBlockLineWrap) { store.setCodeBlockLineWrap(settings.codeBlockLineWrap); } if (typeof settings.showTurnChangedFiles === 'boolean' && settings.showTurnChangedFiles !== store.showTurnChangedFiles) { store.setShowTurnChangedFiles(settings.showTurnChangedFiles); } if (typeof settings.showExpandedBashTools === 'boolean' && settings.showExpandedBashTools !== store.showExpandedBashTools) { store.setShowExpandedBashTools(settings.showExpandedBashTools); } if (typeof settings.showExpandedEditTools === 'boolean' && settings.showExpandedEditTools !== store.showExpandedEditTools) { store.setShowExpandedEditTools(settings.showExpandedEditTools); } if (typeof settings.timeFormatPreference === 'string' && (settings.timeFormatPreference === 'auto' || settings.timeFormatPreference === '12h' || settings.timeFormatPreference === '24h')) { if (settings.timeFormatPreference !== store.timeFormatPreference) { store.setTimeFormatPreference(settings.timeFormatPreference); } } if (typeof settings.weekStartPreference === 'string' && (settings.weekStartPreference === 'auto' || settings.weekStartPreference === 'sunday' || settings.weekStartPreference === 'monday')) { if (settings.weekStartPreference !== store.weekStartPreference) { store.setWeekStartPreference(settings.weekStartPreference); } } if (typeof settings.desktopWindowControlsPosition === 'string') { const nextPosition = settings.desktopWindowControlsPosition === 'left' ? 'left' : (settings.desktopWindowControlsPosition === 'right' || settings.desktopWindowControlsPosition === 'auto') ? 'right' : null; if (nextPosition && nextPosition !== store.desktopWindowControlsPosition) { store.setDesktopWindowControlsPosition(nextPosition); } } if (typeof settings.desktopWindowControlsStyle === 'string') { const nextStyle = settings.desktopWindowControlsStyle === 'traffic-lights' ? 'traffic-lights' : settings.desktopWindowControlsStyle === 'classic' ? 'classic' : null; if (nextStyle && nextStyle !== store.desktopWindowControlsStyle) { store.setDesktopWindowControlsStyle(nextStyle); } } if (typeof settings.chatRenderMode === 'string' && (settings.chatRenderMode === 'sorted' || settings.chatRenderMode === 'live')) { if (settings.chatRenderMode !== store.chatRenderMode) { store.setChatRenderMode(settings.chatRenderMode); } } if (typeof settings.activityRenderMode === 'string' && (settings.activityRenderMode === 'collapsed' || settings.activityRenderMode === 'summary')) { if (settings.activityRenderMode !== store.activityRenderMode) { store.setActivityRenderMode(settings.activityRenderMode); } } if (typeof settings.mermaidRenderingMode === 'string' && (settings.mermaidRenderingMode === 'svg' || settings.mermaidRenderingMode === 'ascii')) { if (settings.mermaidRenderingMode !== store.mermaidRenderingMode) { store.setMermaidRenderingMode(settings.mermaidRenderingMode); } } if (typeof settings.userMessageRenderingMode === 'string' && (settings.userMessageRenderingMode === 'markdown' || settings.userMessageRenderingMode === 'plain')) { if (settings.userMessageRenderingMode !== store.userMessageRenderingMode) { store.setUserMessageRenderingMode(settings.userMessageRenderingMode); } } if (typeof settings.collapsibleUserMessages === 'boolean' && settings.collapsibleUserMessages !== store.collapsibleUserMessages) { store.setCollapsibleUserMessages(settings.collapsibleUserMessages); } if (typeof settings.messageStreamTransport === 'string' && (settings.messageStreamTransport === 'auto' || settings.messageStreamTransport === 'ws' || settings.messageStreamTransport === 'sse')) { if (configStore && settings.messageStreamTransport !== configStore.settingsMessageStreamTransport) { configStore.setSettingsMessageStreamTransport(settings.messageStreamTransport); } } if (typeof settings.stickyUserHeader === 'boolean' && settings.stickyUserHeader !== store.stickyUserHeader) { store.setStickyUserHeader(settings.stickyUserHeader); } if (typeof settings.promptNavigatorEnabled === 'boolean' && settings.promptNavigatorEnabled !== store.promptNavigatorEnabled) { store.setPromptNavigatorEnabled(settings.promptNavigatorEnabled); } if (typeof settings.wideChatLayoutEnabled === 'boolean' && settings.wideChatLayoutEnabled !== store.wideChatLayoutEnabled) { store.setWideChatLayoutEnabled(settings.wideChatLayoutEnabled); } if ( typeof settings.showSplitAssistantMessageActions === 'boolean' && settings.showSplitAssistantMessageActions !== store.showSplitAssistantMessageActions ) { store.setShowSplitAssistantMessageActions(settings.showSplitAssistantMessageActions); } if (typeof settings.reportUsage === 'boolean' && settings.reportUsage !== store.reportUsage) { store.setReportUsage(settings.reportUsage); } if (typeof settings.fontSize === 'number' && Number.isFinite(settings.fontSize) && settings.fontSize !== store.fontSize) { store.setFontSize(settings.fontSize); } if (Array.isArray(settings.draftStarters)) { let nextStarters = sanitizeStarterRefs(settings.draftStarters); if (settings.draftStartersCraftGoalAdded !== true && !nextStarters.some((starter) => starter.type === 'command' && starter.name === 'craft-goal')) { const planIndex = nextStarters.findIndex((starter) => starter.type === 'command' && starter.name === 'plan-feature'); const insertAt = planIndex >= 0 ? planIndex + 1 : nextStarters.length; nextStarters = [ ...nextStarters.slice(0, insertAt), { type: 'command', name: 'craft-goal' }, ...nextStarters.slice(insertAt), ]; } if (settings.draftStartersScheduleTaskAdded !== true && !nextStarters.some((starter) => starter.type === 'command' && starter.name === 'schedule-task')) { const goalIndex = nextStarters.findIndex((starter) => starter.type === 'command' && starter.name === 'craft-goal'); const insertAt = goalIndex >= 0 ? goalIndex + 1 : nextStarters.length; nextStarters = [ ...nextStarters.slice(0, insertAt), { type: 'command', name: 'schedule-task' }, ...nextStarters.slice(insertAt), ]; } if (JSON.stringify(store.globalDraftStarters) !== JSON.stringify(nextStarters)) { store.setGlobalDraftStarters(nextStarters); } if (settings.draftStartersCraftGoalAdded !== true || settings.draftStartersScheduleTaskAdded !== true) { settings.draftStarters = nextStarters; settings.draftStartersCraftGoalAdded = true; settings.draftStartersScheduleTaskAdded = true; } } else { // The built-in default already contains Craft a Goal and Schedule a Task; // only persist the markers so removing them later remains a durable user // choice. if (settings.draftStartersCraftGoalAdded !== true) { settings.draftStartersCraftGoalAdded = true; } if (settings.draftStartersScheduleTaskAdded !== true) { settings.draftStartersScheduleTaskAdded = true; } } if (typeof settings.draftStartersVisible === 'boolean' && settings.draftStartersVisible !== store.draftStartersVisible) { store.setDraftStartersVisible(settings.draftStartersVisible); } if (typeof settings.terminalFontSize === 'number' && Number.isFinite(settings.terminalFontSize) && settings.terminalFontSize !== store.terminalFontSize) { store.setTerminalFontSize(settings.terminalFontSize); } if (isTerminalShell(settings.terminalShell) && settings.terminalShell !== store.terminalShell) { store.setTerminalShell(settings.terminalShell); } if ( Array.isArray(settings.terminalLoginShells) && ( settings.terminalLoginShells.length !== store.terminalLoginShells.length || settings.terminalLoginShells.some((shell, index) => shell !== store.terminalLoginShells[index]) ) ) { store.setTerminalLoginShells(settings.terminalLoginShells); } if (typeof settings.editorFontSize === 'number' && Number.isFinite(settings.editorFontSize) && settings.editorFontSize !== store.editorFontSize) { store.setEditorFontSize(settings.editorFontSize); } if (isUiFontOption(settings.uiFont) && settings.uiFont !== store.uiFont) { store.setUiFont(settings.uiFont); } if (isMonoFontOption(settings.monoFont) && settings.monoFont !== store.monoFont) { store.setMonoFont(settings.monoFont); } if (typeof settings.padding === 'number' && Number.isFinite(settings.padding) && settings.padding !== store.padding) { store.setPadding(settings.padding); } if (typeof settings.cornerRadius === 'number' && Number.isFinite(settings.cornerRadius) && settings.cornerRadius !== store.cornerRadius) { store.setCornerRadius(settings.cornerRadius); } if (typeof settings.inputBarOffset === 'number' && Number.isFinite(settings.inputBarOffset) && settings.inputBarOffset !== store.inputBarOffset) { store.setInputBarOffset(settings.inputBarOffset); } if (settings.shortcutOverrides && !areStringRecordsEqual(settings.shortcutOverrides, store.shortcutOverrides)) { useUIStore.setState({ shortcutOverrides: settings.shortcutOverrides }); } if (typeof settings.mobileKeyboardMode === 'string') { const mode = normalizeMobileKeyboardMode(settings.mobileKeyboardMode, store.mobileKeyboardMode); if (mode !== store.mobileKeyboardMode) { store.setMobileKeyboardMode(mode); } } if (configStoreApi && configStore) { const nextConfigState: Partial = {}; if (typeof settings.dictationEnabled === 'boolean' && settings.dictationEnabled !== configStore.dictationEnabled) { nextConfigState.dictationEnabled = settings.dictationEnabled; } if ((settings.sttProvider === 'local' || settings.sttProvider === 'openai-compatible') && settings.sttProvider !== configStore.sttProvider) { nextConfigState.sttProvider = settings.sttProvider; } if (typeof settings.sttServerUrl === 'string' && settings.sttServerUrl !== configStore.sttServerUrl) { nextConfigState.sttServerUrl = settings.sttServerUrl; } if (typeof settings.sttModel === 'string' && settings.sttModel !== configStore.sttModel) { nextConfigState.sttModel = settings.sttModel; } if (typeof settings.sttLocalModel === 'string' && settings.sttLocalModel !== configStore.sttLocalModel) { nextConfigState.sttLocalModel = settings.sttLocalModel; } if (typeof settings.sttLanguage === 'string' && settings.sttLanguage !== configStore.sttLanguage) { nextConfigState.sttLanguage = settings.sttLanguage; } if (Object.keys(nextConfigState).length > 0) { configStoreApi.setState(nextConfigState); } } if (Array.isArray(settings.favoriteModels)) { const current = store.favoriteModels; const next = settings.favoriteModels; if (!areModelRefsEqual(current, next)) { useUIStore.setState({ favoriteModels: next }); } } if (Array.isArray(settings.hiddenModels)) { const current = store.hiddenModels; const next = settings.hiddenModels; if (!areModelRefsEqual(current, next)) { useUIStore.setState({ hiddenModels: next }); } } if (Array.isArray(settings.collapsedModelProviders)) { const current = store.collapsedModelProviders; const next = settings.collapsedModelProviders; if (!areStringArraysEqual(current, next)) { useUIStore.setState({ collapsedModelProviders: next }); } } if (Array.isArray(settings.recentModels)) { const current = store.recentModels; const next = settings.recentModels; if (!areModelRefsEqual(current, next)) { useUIStore.setState({ recentModels: next }); } } if (Array.isArray(settings.recentAgents)) { const current = store.recentAgents; const next = settings.recentAgents; if (!areStringArraysEqual(current, next)) { useUIStore.setState({ recentAgents: next }); } } if (settings.recentEfforts && typeof settings.recentEfforts === 'object') { const current = store.recentEfforts; const next = settings.recentEfforts; if (!areRecentEffortsEqual(current, next)) { useUIStore.setState({ recentEfforts: next }); } } if (typeof settings.diffLayoutPreference === 'string' && (settings.diffLayoutPreference === 'dynamic' || settings.diffLayoutPreference === 'inline' || settings.diffLayoutPreference === 'side-by-side')) { if (settings.diffLayoutPreference !== store.diffLayoutPreference) { store.setDiffLayoutPreference(settings.diffLayoutPreference); } } if (typeof settings.gitChangesViewMode === 'string' && (settings.gitChangesViewMode === 'flat' || settings.gitChangesViewMode === 'tree')) { if (settings.gitChangesViewMode !== store.gitChangesViewMode) { store.setGitChangesViewMode(settings.gitChangesViewMode); } } if (typeof settings.directoryShowHidden === 'boolean') { setDirectoryShowHidden(settings.directoryShowHidden, { persist: false }); } if (typeof settings.filesViewShowGitignored === 'boolean') { setFilesViewShowGitignored(settings.filesViewShowGitignored, { persist: false }); } const sessionDisplayChanges: Partial> = {}; if (settings.sidebarProjectDisplayMode === 'all' || settings.sidebarProjectDisplayMode === 'single') { sessionDisplayChanges.projectDisplayMode = settings.sidebarProjectDisplayMode; } if (settings.sidebarSessionGroupingMode === 'by-worktree' || settings.sidebarSessionGroupingMode === 'flat') { sessionDisplayChanges.sessionGroupingMode = settings.sidebarSessionGroupingMode; } if (settings.sidebarProjectSortOrder === 'manual' || settings.sidebarProjectSortOrder === 'a-z' || settings.sidebarProjectSortOrder === 'z-a' || settings.sidebarProjectSortOrder === 'date-added' || settings.sidebarProjectSortOrder === 'recent') { sessionDisplayChanges.projectSortOrder = settings.sidebarProjectSortOrder; } if (typeof settings.sidebarShowRecentSection === 'boolean') { sessionDisplayChanges.showRecentSection = settings.sidebarShowRecentSection; } if (Object.keys(sessionDisplayChanges).length > 0) { useSessionDisplayStore.setState(sessionDisplayChanges); } }; const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => { if (!payload || typeof payload !== 'object') { return null; } const candidate = payload as Record; const result: DesktopSettings = {}; if (typeof candidate.themeId === 'string' && candidate.themeId.length > 0) { result.themeId = candidate.themeId; } if (candidate.useSystemTheme === true || candidate.useSystemTheme === false) { result.useSystemTheme = candidate.useSystemTheme; } if (typeof candidate.themeVariant === 'string' && (candidate.themeVariant === 'light' || candidate.themeVariant === 'dark')) { result.themeVariant = candidate.themeVariant; } if (typeof candidate.lightThemeId === 'string' && candidate.lightThemeId.length > 0) { result.lightThemeId = candidate.lightThemeId; } if (typeof candidate.darkThemeId === 'string' && candidate.darkThemeId.length > 0) { result.darkThemeId = candidate.darkThemeId; } if (typeof candidate.lastDirectory === 'string' && candidate.lastDirectory.length > 0) { result.lastDirectory = candidate.lastDirectory; } if (typeof candidate.homeDirectory === 'string' && candidate.homeDirectory.length > 0) { result.homeDirectory = candidate.homeDirectory; } if (typeof candidate.opencodeBinary === 'string') { const trimmed = candidate.opencodeBinary.trim(); result.opencodeBinary = trimmed.length > 0 ? trimmed : undefined; } if (typeof candidate.desktopLanAccessEnabled === 'boolean') { result.desktopLanAccessEnabled = candidate.desktopLanAccessEnabled; } if (typeof candidate.desktopKeepAwakeEnabled === 'boolean') { result.desktopKeepAwakeEnabled = candidate.desktopKeepAwakeEnabled; } if (typeof candidate.desktopMinimizeToTrayEnabled === 'boolean') { result.desktopMinimizeToTrayEnabled = candidate.desktopMinimizeToTrayEnabled; } if (typeof candidate.desktopMacMenuBarEnabled === 'boolean') { result.desktopMacMenuBarEnabled = candidate.desktopMacMenuBarEnabled; } const projects = sanitizeProjects(candidate.projects); if (projects) { result.projects = projects; } if (typeof candidate.activeProjectId === 'string' && candidate.activeProjectId.length > 0) { result.activeProjectId = candidate.activeProjectId; } if (candidate.sidebarProjectDisplayMode === 'all' || candidate.sidebarProjectDisplayMode === 'single') { result.sidebarProjectDisplayMode = candidate.sidebarProjectDisplayMode; } if (candidate.sidebarSessionGroupingMode === 'by-worktree' || candidate.sidebarSessionGroupingMode === 'flat') { result.sidebarSessionGroupingMode = candidate.sidebarSessionGroupingMode; } if (candidate.sidebarProjectSortOrder === 'manual' || candidate.sidebarProjectSortOrder === 'a-z' || candidate.sidebarProjectSortOrder === 'z-a' || candidate.sidebarProjectSortOrder === 'date-added' || candidate.sidebarProjectSortOrder === 'recent') { result.sidebarProjectSortOrder = candidate.sidebarProjectSortOrder; } if (typeof candidate.sidebarShowRecentSection === 'boolean') { result.sidebarShowRecentSection = candidate.sidebarShowRecentSection; } if (Array.isArray(candidate.securityScopedBookmarks)) { result.securityScopedBookmarks = candidate.securityScopedBookmarks.filter( (entry): entry is string => typeof entry === 'string' && entry.length > 0 ); } if (Array.isArray(candidate.pinnedDirectories)) { result.pinnedDirectories = Array.from( new Set( candidate.pinnedDirectories.filter((entry): entry is string => typeof entry === 'string' && entry.length > 0) ) ); } if (Array.isArray(candidate.draftStarters)) { result.draftStarters = sanitizeStarterRefs(candidate.draftStarters); } if (typeof candidate.draftStartersVisible === 'boolean') { result.draftStartersVisible = candidate.draftStartersVisible; } if (typeof candidate.draftStartersCraftGoalAdded === 'boolean') { result.draftStartersCraftGoalAdded = candidate.draftStartersCraftGoalAdded; } if (typeof candidate.draftStartersScheduleTaskAdded === 'boolean') { result.draftStartersScheduleTaskAdded = candidate.draftStartersScheduleTaskAdded; } if (typeof candidate.workStatusPanelEnabled === 'boolean') { result.workStatusPanelEnabled = candidate.workStatusPanelEnabled; } if (Array.isArray(candidate.workStatusHiddenSections)) { // Unknown ids are dropped rather than kept: they would hide nothing and // accumulate forever as sections get renamed. result.workStatusHiddenSections = sanitizeWorkStatusHiddenSections(candidate.workStatusHiddenSections); } if (typeof candidate.showReasoningTraces === 'boolean') { result.showReasoningTraces = candidate.showReasoningTraces; } if (typeof candidate.streamingAutoFollowEnabled === 'boolean') { result.streamingAutoFollowEnabled = candidate.streamingAutoFollowEnabled; } if (typeof candidate.sessionRecapEnabled === 'boolean') { result.sessionRecapEnabled = candidate.sessionRecapEnabled; } if (typeof candidate.sessionSuggestionEnabled === 'boolean') { result.sessionSuggestionEnabled = candidate.sessionSuggestionEnabled; } if (typeof candidate.sessionGoalEnabled === 'boolean') { result.sessionGoalEnabled = candidate.sessionGoalEnabled; } if (typeof candidate.sessionGoalDefaultBudgetEnabled === 'boolean') { result.sessionGoalDefaultBudgetEnabled = candidate.sessionGoalDefaultBudgetEnabled; } if (typeof candidate.sessionGoalDefaultBudget === 'number' && Number.isFinite(candidate.sessionGoalDefaultBudget) && candidate.sessionGoalDefaultBudget > 0) { result.sessionGoalDefaultBudget = Math.floor(candidate.sessionGoalDefaultBudget); } if (typeof candidate.collapsibleThinkingBlocks === 'boolean') { result.collapsibleThinkingBlocks = candidate.collapsibleThinkingBlocks; } if (typeof candidate.autoDeleteEnabled === 'boolean') { result.autoDeleteEnabled = candidate.autoDeleteEnabled; } if (typeof candidate.autoSaveEnabled === 'boolean') { result.autoSaveEnabled = candidate.autoSaveEnabled; } if (typeof candidate.autoDeleteAfterDays === 'number' && Number.isFinite(candidate.autoDeleteAfterDays)) { result.autoDeleteAfterDays = candidate.autoDeleteAfterDays; } if (candidate.sessionRetentionAction === 'archive' || candidate.sessionRetentionAction === 'delete') { result.sessionRetentionAction = candidate.sessionRetentionAction; } if (typeof candidate.tunnelProvider === 'string') { const provider = candidate.tunnelProvider.trim().toLowerCase(); if (provider.length > 0) { result.tunnelProvider = provider; } } if (typeof candidate.tunnelMode === 'string') { const mode = candidate.tunnelMode.trim().toLowerCase(); if (mode === 'quick' || mode === 'managed-remote' || mode === 'managed-local') { result.tunnelMode = mode; } } if (candidate.tunnelBootstrapTtlMs === null) { result.tunnelBootstrapTtlMs = null; } else if (typeof candidate.tunnelBootstrapTtlMs === 'number' && Number.isFinite(candidate.tunnelBootstrapTtlMs)) { result.tunnelBootstrapTtlMs = candidate.tunnelBootstrapTtlMs; } if (typeof candidate.tunnelSessionTtlMs === 'number' && Number.isFinite(candidate.tunnelSessionTtlMs)) { result.tunnelSessionTtlMs = candidate.tunnelSessionTtlMs; } if (candidate.managedLocalTunnelConfigPath === null) { result.managedLocalTunnelConfigPath = null; } else if (typeof candidate.managedLocalTunnelConfigPath === 'string') { const trimmed = candidate.managedLocalTunnelConfigPath.trim(); result.managedLocalTunnelConfigPath = trimmed.length > 0 ? trimmed : null; } if (typeof candidate.managedRemoteTunnelHostname === 'string') { result.managedRemoteTunnelHostname = candidate.managedRemoteTunnelHostname.trim(); } if (candidate.managedRemoteTunnelToken === null) { result.managedRemoteTunnelToken = null; } else if (typeof candidate.managedRemoteTunnelToken === 'string') { result.managedRemoteTunnelToken = candidate.managedRemoteTunnelToken.trim(); } const managedRemoteTunnelPresets = sanitizeManagedRemoteTunnelPresets(candidate.managedRemoteTunnelPresets); if (managedRemoteTunnelPresets) { result.managedRemoteTunnelPresets = managedRemoteTunnelPresets; } if (typeof candidate.managedRemoteTunnelSelectedPresetId === 'string') { const trimmed = candidate.managedRemoteTunnelSelectedPresetId.trim(); result.managedRemoteTunnelSelectedPresetId = trimmed.length > 0 ? trimmed : undefined; } const managedRemoteTunnelPresetTokens = sanitizeManagedRemoteTunnelPresetTokens(candidate.managedRemoteTunnelPresetTokens); if (managedRemoteTunnelPresetTokens) { result.managedRemoteTunnelPresetTokens = managedRemoteTunnelPresetTokens; } if (typeof candidate.defaultModel === 'string' && candidate.defaultModel.length > 0) { result.defaultModel = candidate.defaultModel; } if (typeof candidate.defaultVariant === 'string' && candidate.defaultVariant.length > 0) { result.defaultVariant = candidate.defaultVariant; } if (typeof candidate.defaultAgent === 'string' && candidate.defaultAgent.length > 0) { result.defaultAgent = candidate.defaultAgent; } if (typeof candidate.smallModelUseDefault === 'boolean') { result.smallModelUseDefault = candidate.smallModelUseDefault; } if (typeof candidate.smallModelOverride === 'string' && candidate.smallModelOverride.length > 0) { result.smallModelOverride = candidate.smallModelOverride; } if (typeof candidate.walkthroughModelOverride === 'string' && candidate.walkthroughModelOverride.length > 0) { result.walkthroughModelOverride = candidate.walkthroughModelOverride; } if (typeof candidate.autoCreateWorktree === 'boolean') { result.autoCreateWorktree = candidate.autoCreateWorktree; } if (typeof candidate.gitmojiEnabled === 'boolean') { result.gitmojiEnabled = candidate.gitmojiEnabled; } if (isFollowUpBehavior(candidate.followUpBehavior)) { result.followUpBehavior = candidate.followUpBehavior; } else if (typeof candidate.queueModeEnabled === 'boolean') { result.followUpBehavior = normalizeFollowUpBehavior(undefined, candidate.queueModeEnabled); } if (typeof candidate.showDeletionDialog === 'boolean') { result.showDeletionDialog = candidate.showDeletionDialog; } if (typeof candidate.nativeNotificationsEnabled === 'boolean') { result.nativeNotificationsEnabled = candidate.nativeNotificationsEnabled; } if (typeof candidate.notificationMode === 'string' && (candidate.notificationMode === 'always' || candidate.notificationMode === 'hidden-only')) { result.notificationMode = candidate.notificationMode; } if (typeof candidate.notifyOnSubtasks === 'boolean') { result.notifyOnSubtasks = candidate.notifyOnSubtasks; } if (typeof candidate.notifyOnCompletion === 'boolean') { result.notifyOnCompletion = candidate.notifyOnCompletion; } if (typeof candidate.notifyOnError === 'boolean') { result.notifyOnError = candidate.notifyOnError; } if (typeof candidate.notifyOnQuestion === 'boolean') { result.notifyOnQuestion = candidate.notifyOnQuestion; } if (candidate.notificationTemplates && typeof candidate.notificationTemplates === 'object') { const templates = candidate.notificationTemplates as Record; const validateTemplate = (key: string): { title: string; message: string } | undefined => { const value = templates[key]; if (!value || typeof value !== 'object') return undefined; const obj = value as Record; const title = typeof obj.title === 'string' ? obj.title : ''; const message = typeof obj.message === 'string' ? obj.message : ''; return { title, message }; }; const completion = validateTemplate('completion'); const error = validateTemplate('error'); const question = validateTemplate('question'); const subtask = validateTemplate('subtask'); if (completion || error || question || subtask) { result.notificationTemplates = { completion: completion ?? { title: 'Task Complete', message: 'Your task has finished.' }, error: error ?? { title: 'Error Occurred', message: 'An error occurred while processing your task.' }, question: question ?? { title: 'Input Needed', message: 'Please provide input to continue.' }, subtask: subtask ?? { title: 'Subtask Complete', message: 'A subtask has finished.' }, }; } } if (typeof candidate.summarizeLastMessage === 'boolean') { result.summarizeLastMessage = candidate.summarizeLastMessage; } if (typeof candidate.summaryThreshold === 'number' && Number.isFinite(candidate.summaryThreshold)) { result.summaryThreshold = Math.max(0, Math.round(candidate.summaryThreshold)); } if (typeof candidate.summaryLength === 'number' && Number.isFinite(candidate.summaryLength)) { result.summaryLength = Math.max(10, Math.round(candidate.summaryLength)); } if (typeof candidate.maxLastMessageLength === 'number' && Number.isFinite(candidate.maxLastMessageLength)) { result.maxLastMessageLength = Math.max(10, Math.round(candidate.maxLastMessageLength)); } if (candidate.usageDisplayMode === 'usage' || candidate.usageDisplayMode === 'remaining') { result.usageDisplayMode = candidate.usageDisplayMode; } if (Array.isArray(candidate.usageDropdownProviders)) { result.usageDropdownProviders = candidate.usageDropdownProviders.filter( (entry): entry is string => typeof entry === 'string' && entry.length > 0 ); } // Parse usageSelectedModels (Record) if (candidate.usageSelectedModels && typeof candidate.usageSelectedModels === 'object') { const selectedModels: Record = {}; for (const [providerId, models] of Object.entries(candidate.usageSelectedModels)) { if (Array.isArray(models)) { selectedModels[providerId] = models.filter((m): m is string => typeof m === 'string'); } } if (Object.keys(selectedModels).length > 0) { result.usageSelectedModels = selectedModels; } } // Parse usageCollapsedFamilies (Record) if (candidate.usageCollapsedFamilies && typeof candidate.usageCollapsedFamilies === 'object') { const collapsedFamilies: Record = {}; for (const [providerId, families] of Object.entries(candidate.usageCollapsedFamilies)) { if (Array.isArray(families)) { collapsedFamilies[providerId] = families.filter((f): f is string => typeof f === 'string'); } } if (Object.keys(collapsedFamilies).length > 0) { result.usageCollapsedFamilies = collapsedFamilies; } } // Parse usageExpandedFamilies (Record) - inverted collapsed logic for header dropdown if (candidate.usageExpandedFamilies && typeof candidate.usageExpandedFamilies === 'object') { const expandedFamilies: Record = {}; for (const [providerId, families] of Object.entries(candidate.usageExpandedFamilies)) { if (Array.isArray(families)) { expandedFamilies[providerId] = families.filter((f): f is string => typeof f === 'string'); } } if (Object.keys(expandedFamilies).length > 0) { result.usageExpandedFamilies = expandedFamilies; } } // Parse usageModelGroups - custom model groups configuration per provider if (candidate.usageModelGroups && typeof candidate.usageModelGroups === 'object') { const modelGroups: Record; modelAssignments?: Record; renamedGroups?: Record; }> = {}; for (const [providerId, config] of Object.entries(candidate.usageModelGroups)) { if (config && typeof config === 'object') { const typedConfig = config as Record; const providerConfig: NonNullable[string] = {}; // Parse customGroups if (Array.isArray(typedConfig.customGroups)) { providerConfig.customGroups = typedConfig.customGroups .filter((g): g is Record => g && typeof g === 'object') .map((g) => ({ id: String(g.id ?? ''), label: String(g.label ?? ''), models: Array.isArray(g.models) ? g.models.filter((m): m is string => typeof m === 'string') : [], order: typeof g.order === 'number' ? g.order : 0, })); } // Parse modelAssignments if (typedConfig.modelAssignments && typeof typedConfig.modelAssignments === 'object') { providerConfig.modelAssignments = Object.fromEntries( Object.entries(typedConfig.modelAssignments as Record) .filter(([, v]) => typeof v === 'string') .map(([k, v]) => [k, String(v)]) ); } // Parse renamedGroups if (typedConfig.renamedGroups && typeof typedConfig.renamedGroups === 'object') { providerConfig.renamedGroups = Object.fromEntries( Object.entries(typedConfig.renamedGroups as Record) .filter(([, v]) => typeof v === 'string') .map(([k, v]) => [k, String(v)]) ); } if (Object.keys(providerConfig).length > 0) { modelGroups[providerId] = providerConfig; } } } if (Object.keys(modelGroups).length > 0) { result.usageModelGroups = modelGroups; } } if (typeof candidate.inputSpellcheckEnabled === 'boolean') { result.inputSpellcheckEnabled = candidate.inputSpellcheckEnabled; } if (typeof candidate.showOpenCodeUpdateNotifications === 'boolean') { result.showOpenCodeUpdateNotifications = candidate.showOpenCodeUpdateNotifications; } if (typeof candidate.agentControlToolEnabled === 'boolean') { result.agentControlToolEnabled = candidate.agentControlToolEnabled; } if (typeof candidate.agentWebToolEnabled === 'boolean') { result.agentWebToolEnabled = candidate.agentWebToolEnabled; } if (typeof candidate.agentMemoryToolEnabled === 'boolean') { result.agentMemoryToolEnabled = candidate.agentMemoryToolEnabled; } if (typeof candidate.openCodeUpdateToastDismissedVersion === 'string') { result.openCodeUpdateToastDismissedVersion = candidate.openCodeUpdateToastDismissedVersion.trim().slice(0, 128); } if (typeof candidate.showToolFileIcons === 'boolean') { result.showToolFileIcons = candidate.showToolFileIcons; } if (typeof candidate.codeBlockLineWrap === 'boolean') { result.codeBlockLineWrap = candidate.codeBlockLineWrap; } if (typeof candidate.showTurnChangedFiles === 'boolean') { result.showTurnChangedFiles = candidate.showTurnChangedFiles; } if (typeof candidate.showExpandedBashTools === 'boolean') { result.showExpandedBashTools = candidate.showExpandedBashTools; } if (typeof candidate.showExpandedEditTools === 'boolean') { result.showExpandedEditTools = candidate.showExpandedEditTools; } if (typeof candidate.timeFormatPreference === 'string' && (candidate.timeFormatPreference === 'auto' || candidate.timeFormatPreference === '12h' || candidate.timeFormatPreference === '24h')) { result.timeFormatPreference = candidate.timeFormatPreference; } if (typeof candidate.weekStartPreference === 'string' && (candidate.weekStartPreference === 'auto' || candidate.weekStartPreference === 'sunday' || candidate.weekStartPreference === 'monday')) { result.weekStartPreference = candidate.weekStartPreference; } if (typeof candidate.desktopWindowControlsPosition === 'string') { if (candidate.desktopWindowControlsPosition === 'left') { result.desktopWindowControlsPosition = 'left'; } else if ( candidate.desktopWindowControlsPosition === 'right' || candidate.desktopWindowControlsPosition === 'auto' ) { // Legacy "auto" never read OS chrome config; treat as right. result.desktopWindowControlsPosition = 'right'; } } if (typeof candidate.desktopWindowControlsStyle === 'string') { if (candidate.desktopWindowControlsStyle === 'classic' || candidate.desktopWindowControlsStyle === 'traffic-lights') { result.desktopWindowControlsStyle = candidate.desktopWindowControlsStyle; } } if (typeof candidate.chatRenderMode === 'string' && (candidate.chatRenderMode === 'sorted' || candidate.chatRenderMode === 'live')) { result.chatRenderMode = candidate.chatRenderMode; } if (typeof candidate.messageStreamTransport === 'string' && (candidate.messageStreamTransport === 'auto' || candidate.messageStreamTransport === 'ws' || candidate.messageStreamTransport === 'sse')) { result.messageStreamTransport = candidate.messageStreamTransport; } if (typeof candidate.activityRenderMode === 'string' && (candidate.activityRenderMode === 'collapsed' || candidate.activityRenderMode === 'summary')) { result.activityRenderMode = candidate.activityRenderMode; } if (typeof candidate.mermaidRenderingMode === 'string' && (candidate.mermaidRenderingMode === 'svg' || candidate.mermaidRenderingMode === 'ascii')) { result.mermaidRenderingMode = candidate.mermaidRenderingMode; } if (typeof candidate.userMessageRenderingMode === 'string' && (candidate.userMessageRenderingMode === 'markdown' || candidate.userMessageRenderingMode === 'plain')) { result.userMessageRenderingMode = candidate.userMessageRenderingMode; } if (typeof candidate.collapsibleUserMessages === 'boolean') { result.collapsibleUserMessages = candidate.collapsibleUserMessages; } if (typeof candidate.stickyUserHeader === 'boolean') { result.stickyUserHeader = candidate.stickyUserHeader; } if (typeof candidate.promptNavigatorEnabled === 'boolean') { result.promptNavigatorEnabled = candidate.promptNavigatorEnabled; } if (typeof candidate.wideChatLayoutEnabled === 'boolean') { result.wideChatLayoutEnabled = candidate.wideChatLayoutEnabled; } if (typeof candidate.showSplitAssistantMessageActions === 'boolean') { result.showSplitAssistantMessageActions = candidate.showSplitAssistantMessageActions; } if (typeof candidate.fontSize === 'number' && Number.isFinite(candidate.fontSize)) { result.fontSize = candidate.fontSize; } if (typeof candidate.terminalFontSize === 'number' && Number.isFinite(candidate.terminalFontSize)) { result.terminalFontSize = candidate.terminalFontSize; } if (isTerminalShell(candidate.terminalShell)) { result.terminalShell = candidate.terminalShell; } if (Array.isArray(candidate.terminalLoginShells)) { result.terminalLoginShells = [...new Set(candidate.terminalLoginShells.filter(isTerminalShell))]; } if (typeof candidate.editorFontSize === 'number' && Number.isFinite(candidate.editorFontSize)) { result.editorFontSize = candidate.editorFontSize; } if (isUiFontOption(candidate.uiFont)) { result.uiFont = candidate.uiFont; } if (isMonoFontOption(candidate.monoFont)) { result.monoFont = candidate.monoFont; } if (typeof candidate.padding === 'number' && Number.isFinite(candidate.padding)) { result.padding = candidate.padding; } if (typeof candidate.cornerRadius === 'number' && Number.isFinite(candidate.cornerRadius)) { result.cornerRadius = candidate.cornerRadius; } if (typeof candidate.inputBarOffset === 'number' && Number.isFinite(candidate.inputBarOffset)) { result.inputBarOffset = candidate.inputBarOffset; } const shortcutOverrides = sanitizeShortcutOverrides(candidate.shortcutOverrides); if (shortcutOverrides) { result.shortcutOverrides = shortcutOverrides; } if (typeof candidate.mobileKeyboardMode === 'string') { if (candidate.mobileKeyboardMode === 'native' || candidate.mobileKeyboardMode === 'resize-content') { result.mobileKeyboardMode = candidate.mobileKeyboardMode; } } const favoriteModels = sanitizeModelRefs(candidate.favoriteModels, 64); if (favoriteModels) { result.favoriteModels = favoriteModels; } const hiddenModels = sanitizeModelRefs(candidate.hiddenModels, 1024); if (hiddenModels) { result.hiddenModels = hiddenModels; } const collapsedModelProviders = sanitizeStringArray(candidate.collapsedModelProviders); if (collapsedModelProviders) { result.collapsedModelProviders = collapsedModelProviders; } const recentModels = sanitizeModelRefs(candidate.recentModels, 16); if (recentModels) { result.recentModels = recentModels; } const recentAgents = sanitizeStringArray(candidate.recentAgents); if (recentAgents) { result.recentAgents = recentAgents; } const recentEfforts = sanitizeRecentEfforts(candidate.recentEfforts); if (recentEfforts) { result.recentEfforts = recentEfforts; } if ( typeof candidate.diffLayoutPreference === 'string' && (candidate.diffLayoutPreference === 'dynamic' || candidate.diffLayoutPreference === 'inline' || candidate.diffLayoutPreference === 'side-by-side') ) { result.diffLayoutPreference = candidate.diffLayoutPreference; } if ( typeof candidate.gitChangesViewMode === 'string' && (candidate.gitChangesViewMode === 'flat' || candidate.gitChangesViewMode === 'tree') ) { result.gitChangesViewMode = candidate.gitChangesViewMode; } if (typeof candidate.directoryShowHidden === 'boolean') { result.directoryShowHidden = candidate.directoryShowHidden; } if (typeof candidate.filesViewShowGitignored === 'boolean') { result.filesViewShowGitignored = candidate.filesViewShowGitignored; } if (typeof candidate.openInAppId === 'string' && candidate.openInAppId.length > 0) { result.openInAppId = candidate.openInAppId; } if (typeof candidate.pwaAppName === 'string') { const normalized = candidate.pwaAppName.trim().replace(/\s+/g, ' ').slice(0, 64); result.pwaAppName = normalized.length > 0 ? normalized : ''; } const skillCatalogs = sanitizeSkillCatalogs(candidate.skillCatalogs); if (skillCatalogs) { result.skillCatalogs = skillCatalogs; } if (typeof candidate.reportUsage === 'boolean') { result.reportUsage = candidate.reportUsage; } if (typeof candidate.globalBehaviorPrompt === 'string') { result.globalBehaviorPrompt = candidate.globalBehaviorPrompt; } if (typeof candidate.responseStyleEnabled === 'boolean') { result.responseStyleEnabled = candidate.responseStyleEnabled; } if ( typeof candidate.responseStylePreset === 'string' && (candidate.responseStylePreset === 'concise' || candidate.responseStylePreset === 'detailed' || candidate.responseStylePreset === 'mentor' || candidate.responseStylePreset === 'pushback' || candidate.responseStylePreset === 'noFiller' || candidate.responseStylePreset === 'matchEnergy' || candidate.responseStylePreset === 'warmPeer' || candidate.responseStylePreset === 'custom') ) { result.responseStylePreset = candidate.responseStylePreset; } if (typeof candidate.responseStyleCustomInstructions === 'string') { result.responseStyleCustomInstructions = candidate.responseStyleCustomInstructions; } if (typeof candidate.dictationEnabled === 'boolean') { result.dictationEnabled = candidate.dictationEnabled; } if (candidate.sttProvider === 'local' || candidate.sttProvider === 'openai-compatible') { result.sttProvider = candidate.sttProvider; } else if (candidate.sttProvider === 'server') { // Legacy provider migration: 'server' was the OpenAI-compatible endpoint. result.sttProvider = 'openai-compatible'; } else if (candidate.sttProvider === 'browser' || candidate.sttProvider === 'wasm') { result.sttProvider = 'local'; } if (typeof candidate.sttServerUrl === 'string') { result.sttServerUrl = candidate.sttServerUrl.trim(); } if (typeof candidate.sttModel === 'string') { result.sttModel = candidate.sttModel.trim(); } if (typeof candidate.sttLocalModel === 'string') { result.sttLocalModel = candidate.sttLocalModel.trim(); } if (typeof candidate.sttLanguage === 'string') { result.sttLanguage = candidate.sttLanguage.trim(); } return result; }; type SettingsRuntimeContext = { runtimeKey: string; generation: number }; // Short-lived cache + in-flight dedup for settings fetches to avoid repeated GET calls during startup let _settingsRuntimeGeneration = 0; let _settingsCache: { value: DesktopSettings | null; at: number; context: SettingsRuntimeContext } | null = null; let _settingsInflight: { promise: Promise; context: SettingsRuntimeContext } | null = null; let _pendingSettingsChanges: Partial | null = null; let _pendingSettingsContext: SettingsRuntimeContext | null = null; let _settingsFlushTimer: ReturnType | null = null; let _settingsFlushWaiters: Array<() => void> = []; let _settingsLifecycleInitialized = false; const SETTINGS_CACHE_TTL = 2_000; // 2 seconds — covers the startup burst const SETTINGS_DEBOUNCE_MS = 200; const captureSettingsRuntimeContext = (): SettingsRuntimeContext => ({ runtimeKey: getRuntimeKey(), generation: _settingsRuntimeGeneration, }); const isSameSettingsRuntimeContext = (left: SettingsRuntimeContext, right: SettingsRuntimeContext): boolean => ( left.runtimeKey === right.runtimeKey && left.generation === right.generation ); const isSettingsRuntimeContextCurrent = (context: SettingsRuntimeContext): boolean => ( context.generation === _settingsRuntimeGeneration && context.runtimeKey === getRuntimeKey() ); const ensureSettingsRuntimeLifecycle = (): void => { if (_settingsLifecycleInitialized || typeof window === 'undefined') return; _settingsLifecycleInitialized = true; subscribeRuntimeEndpointWillChange((detail) => { if (detail.runtimeKey === detail.previousRuntimeKey) return; if (_settingsFlushTimer) clearTimeout(_settingsFlushTimer); if (_pendingSettingsChanges) void _flushSettingsUpdate(); }); subscribeRuntimeEndpointChanged((detail) => { if (detail.runtimeKey === detail.previousRuntimeKey) return; _settingsRuntimeGeneration += 1; _settingsCache = null; _settingsInflight = null; }); }; const fetchWebSettings = async (context = captureSettingsRuntimeContext()): Promise => { ensureSettingsRuntimeLifecycle(); // Return cached if fresh if (_settingsCache && isSameSettingsRuntimeContext(_settingsCache.context, context) && Date.now() - _settingsCache.at < SETTINGS_CACHE_TTL) { return _settingsCache.value; } // Dedup concurrent calls if (_settingsInflight && isSameSettingsRuntimeContext(_settingsInflight.context, context)) return _settingsInflight.promise; const inflight = { context, promise: (async (): Promise => { const runtimeSettings = getRuntimeSettingsAPI(); if (runtimeSettings) { try { const result = await runtimeSettings.load(); if (!isSettingsRuntimeContextCurrent(context)) return null; const settings = sanitizeWebSettings(result.settings); _settingsCache = { value: settings, at: Date.now(), context }; return settings; } catch (error) { if (!isSettingsRuntimeContextCurrent(context)) return null; console.warn('Failed to load shared settings from runtime settings API:', error); } } if (!isSettingsRuntimeContextCurrent(context)) return null; try { const response = await runtimeFetch('/api/config/settings', { method: 'GET', headers: { Accept: 'application/json' }, }); if (!isSettingsRuntimeContextCurrent(context)) return null; if (!response.ok) { return null; } const data = await response.json().catch(() => null); if (!isSettingsRuntimeContextCurrent(context)) return null; const settings = sanitizeWebSettings(data); _settingsCache = { value: settings, at: Date.now(), context }; return settings; } catch (error) { if (!isSettingsRuntimeContextCurrent(context)) return null; console.warn('Failed to load shared settings from server:', error); return null; } })(), }; _settingsInflight = inflight; void inflight.promise.finally(() => { if (_settingsInflight === inflight) _settingsInflight = null; }); return inflight.promise; }; /** Invalidate cached settings (call after a successful PUT) */ export const invalidateSettingsCache = (): void => { _settingsCache = null; }; export const syncDesktopSettings = async (): Promise => { if (typeof window === 'undefined') { return; } ensureSettingsRuntimeLifecycle(); const context = captureSettingsRuntimeContext(); const persistApis = [getPersistApi(), useSessionDisplayStore.persist]; // Wait for Zustand persist hydration before applying server settings. // Otherwise `set()`-calls race with hydration: we set X, then hydration // reads localStorage and overwrites back to the persisted value. const waitForPersistHydration = (persistApi: PersistApi | undefined): Promise => { if (!persistApi?.hasHydrated || persistApi.hasHydrated()) { return Promise.resolve(); } if (!persistApi.onFinishHydration) { return Promise.resolve(); } return new Promise((resolve) => { let settled = false; const finish = () => { if (settled) return; settled = true; resolve(); }; const unsubscribe = persistApi.onFinishHydration!(() => { unsubscribe?.(); finish(); }); // Guard: hydration may have flipped to true between the hasHydrated // check and the onFinishHydration subscription — resolve immediately. if (persistApi.hasHydrated?.()) finish(); }); }; const waitForHydration = (): Promise => Promise.all( persistApis.map(waitForPersistHydration), ).then(() => undefined); // Each step is wrapped in try/catch so a failure in one side-effect (e.g. // a TypeError from writing to a contextBridge-protected global) doesn't // prevent server settings from reaching the Zustand store. const applySettings = async (settings: DesktopSettings) => { if (!isSettingsRuntimeContextCurrent(context)) return; const shouldPersistCraftGoalMigration = settings.draftStartersCraftGoalAdded !== true || settings.draftStartersScheduleTaskAdded !== true; // `autoSaveEnabled` is new to the settings backend. Until the server has a // value, materialize would invent the client default (true) and overwrite a // deliberate legacy "off" preference migrated from // `openchamber:files:auto-save-enabled`. Prefer the hydrated store value and // seed the backend once so later omitted→default authority is correct. const shouldSeedAutoSaveEnabled = typeof settings.autoSaveEnabled !== 'boolean'; const shouldSeedSidebarProjectDisplayMode = settings.sidebarProjectDisplayMode === undefined; const shouldSeedSidebarSessionGroupingMode = settings.sidebarSessionGroupingMode === undefined; const shouldSeedSidebarProjectSortOrder = settings.sidebarProjectSortOrder === undefined; const shouldSeedSidebarShowRecentSection = settings.sidebarShowRecentSection === undefined; const authoritativeSettings = materializeAuthoritativeUiSettings(settings); try { persistToLocalStorage(settings); } catch (error) { console.warn('persistToLocalStorage failed:', error); } await waitForHydration(); if (!isSettingsRuntimeContextCurrent(context)) return; if (shouldSeedAutoSaveEnabled) { authoritativeSettings.autoSaveEnabled = useUIStore.getState().autoSaveEnabled; } const sessionDisplayState = useSessionDisplayStore.getState(); if (shouldSeedSidebarProjectDisplayMode) { authoritativeSettings.sidebarProjectDisplayMode = sessionDisplayState.projectDisplayMode; } if (shouldSeedSidebarSessionGroupingMode) { authoritativeSettings.sidebarSessionGroupingMode = sessionDisplayState.sessionGroupingMode; } if (shouldSeedSidebarProjectSortOrder) { authoritativeSettings.sidebarProjectSortOrder = sessionDisplayState.projectSortOrder; } if (shouldSeedSidebarShowRecentSection) { authoritativeSettings.sidebarShowRecentSection = sessionDisplayState.showRecentSection; } if (settings.draftStarters === undefined) { useUIStore.setState({ globalDraftStarters: null }); } try { applyDesktopUiPreferences(authoritativeSettings); } catch (error) { console.warn('applyDesktopUiPreferences failed:', error); } const migrationPatch: Partial = {}; if (shouldPersistCraftGoalMigration) { if (authoritativeSettings.draftStarters) { migrationPatch.draftStarters = authoritativeSettings.draftStarters; } migrationPatch.draftStartersCraftGoalAdded = true; migrationPatch.draftStartersScheduleTaskAdded = true; } if (shouldSeedAutoSaveEnabled) { migrationPatch.autoSaveEnabled = authoritativeSettings.autoSaveEnabled; } if (shouldSeedSidebarProjectDisplayMode) { migrationPatch.sidebarProjectDisplayMode = authoritativeSettings.sidebarProjectDisplayMode; } if (shouldSeedSidebarSessionGroupingMode) { migrationPatch.sidebarSessionGroupingMode = authoritativeSettings.sidebarSessionGroupingMode; } if (shouldSeedSidebarProjectSortOrder) { migrationPatch.sidebarProjectSortOrder = authoritativeSettings.sidebarProjectSortOrder; } if (shouldSeedSidebarShowRecentSection) { migrationPatch.sidebarShowRecentSection = authoritativeSettings.sidebarShowRecentSection; } if (Object.keys(migrationPatch).length > 0) { await updateDesktopSettings(migrationPatch); if (!isSettingsRuntimeContextCurrent(context)) return; } dispatchSettingsSynced(authoritativeSettings); }; try { const webSettings = await fetchWebSettings(context); if (webSettings && isSettingsRuntimeContextCurrent(context)) { await applySettings(webSettings); } } catch (error) { console.warn('Failed to synchronise settings:', error); } }; // Coalesce rapid updateDesktopSettings calls into a single PUT async function _flushSettingsUpdate(): Promise { const changes = _pendingSettingsChanges; const context = _pendingSettingsContext; const waiters = _settingsFlushWaiters; _pendingSettingsChanges = null; _pendingSettingsContext = null; _settingsFlushTimer = null; _settingsFlushWaiters = []; try { if (!changes || !context || Object.keys(changes).length === 0 || !isSettingsRuntimeContextCurrent(context)) { // Nothing will be written — clear any pending "Saving…" indicator. dispatchSettingsSaveState('saved'); return; } const runtimeSettings = getRuntimeSettingsAPI(); if (runtimeSettings) { try { const updated = await runtimeSettings.save(changes); if (!isSettingsRuntimeContextCurrent(context)) return; if (updated) { applyDesktopUiPreferences(updated); dispatchSettingsSynced(updated); _settingsCache = null; } dispatchSettingsSaveState(updated ? 'saved' : 'error'); return; } catch (error) { if (!isSettingsRuntimeContextCurrent(context)) return; console.warn('Failed to update settings via runtime settings API:', error); } } if (!isSettingsRuntimeContextCurrent(context)) return; try { const response = await runtimeFetch('/api/config/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify(changes), }); if (!isSettingsRuntimeContextCurrent(context)) return; if (!response.ok) { console.warn('Failed to update shared settings via API:', response.status, response.statusText); dispatchSettingsSaveState('error'); return; } const updated = sanitizeWebSettings(await response.json().catch(() => null)); if (!isSettingsRuntimeContextCurrent(context)) return; if (updated) { applyDesktopUiPreferences(updated); dispatchSettingsSynced(updated); dispatchSettingsSaveState('saved'); // Invalidate GET cache so next read sees the fresh data _settingsCache = null; } else { dispatchSettingsSaveState('error'); } } catch (error) { if (isSettingsRuntimeContextCurrent(context)) { console.warn('Failed to update shared settings via API:', error); dispatchSettingsSaveState('error'); } } } finally { waiters.forEach((resolve) => resolve()); } } export const updateDesktopSettings = async (changes: Partial): Promise => { if (typeof window === 'undefined') { return; } ensureSettingsRuntimeLifecycle(); const context = captureSettingsRuntimeContext(); if (_pendingSettingsContext && !isSameSettingsRuntimeContext(_pendingSettingsContext, context)) { if (_settingsFlushTimer) clearTimeout(_settingsFlushTimer); void _flushSettingsUpdate(); } _pendingSettingsChanges = { ...(_pendingSettingsChanges ?? {}), ...changes }; _pendingSettingsContext = context; dispatchSettingsSaveState('saving'); if (_settingsFlushTimer) { clearTimeout(_settingsFlushTimer); } const flushed = new Promise((resolve) => { _settingsFlushWaiters.push(resolve); }); _settingsFlushTimer = setTimeout(() => void _flushSettingsUpdate(), SETTINGS_DEBOUNCE_MS); return flushed; }; export const initializeAppearancePreferences = async (): Promise => { if (typeof window === 'undefined') { return; } const persistApi = getPersistApi(); try { const appearance = await loadAppearancePreferences(); if (!appearance) { return; } const applyAppearance = () => applyAppearancePreferences(appearance); if (persistApi?.hasHydrated?.()) { applyAppearance(); return; } applyAppearance(); if (persistApi?.onFinishHydration) { const unsubscribe = persistApi.onFinishHydration(() => { unsubscribe?.(); applyAppearance(); }); } } catch (error) { console.warn('Failed to load appearance preferences:', error); } };