import type { DesktopSettings } from '@/lib/desktop'; import { createProjectIdFromPath } from '@/lib/projectId'; import { useUIStore } from '@/stores/useUIStore'; import { isMonoFontOption, isUiFontOption } from '@/lib/fontOptions'; import { 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'; 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 persistToLocalStorage = (settings: DesktopSettings) => { if (typeof window === 'undefined') { return; } if (settings.themeId) { localStorage.setItem('selectedThemeId', settings.themeId); } if (settings.themeVariant) { localStorage.setItem('selectedThemeVariant', settings.themeVariant); } if (settings.lightThemeId) { localStorage.setItem('lightThemeId', settings.lightThemeId); } if (settings.darkThemeId) { localStorage.setItem('darkThemeId', settings.darkThemeId); } if (typeof settings.useSystemTheme === 'boolean') { localStorage.setItem('useSystemTheme', String(settings.useSystemTheme)); } if (settings.lastDirectory) { localStorage.setItem('lastDirectory', settings.lastDirectory); } if (settings.homeDirectory) { localStorage.setItem('homeDirectory', settings.homeDirectory); applyPersistedHomeDirectoryToWindow(settings.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 as unknown as { sidebarCollapsed?: boolean }).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'); } } 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'); } if (typeof settings.filesViewShowGitignored === 'boolean') { localStorage.setItem('filesViewShowGitignored', settings.filesViewShowGitignored ? 'true' : 'false'); } if (typeof settings.openInAppId === 'string' && settings.openInAppId.length > 0) { localStorage.setItem('openInAppId', settings.openInAppId); } 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'); } } if (typeof settings.mobileKeyboardMode === 'string') { 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'); } } if (typeof settings.dictationEnabled === 'boolean') { localStorage.setItem('dictationEnabled', String(settings.dictationEnabled)); } if (settings.sttProvider === 'local' || settings.sttProvider === 'openai-compatible') { localStorage.setItem('sttProvider', settings.sttProvider); } if (typeof settings.sttServerUrl === 'string') { localStorage.setItem('sttServerUrl', settings.sttServerUrl); } if (typeof settings.sttModel === 'string') { localStorage.setItem('sttModel', settings.sttModel); } if (typeof settings.sttLocalModel === 'string') { localStorage.setItem('sttLocalModel', settings.sttLocalModel); } if (typeof settings.sttLanguage === 'string') { localStorage.setItem('sttLanguage', settings.sttLanguage); } }; const dispatchSettingsSynced = (settings: DesktopSettings): void => { if (typeof window === 'undefined') { return; } window.dispatchEvent(new CustomEvent('openchamber:settings-synced', { detail: settings })); }; 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); result.push({ id, label, source, ...(subpath ? { subpath } : {}), ...(gitIdentityId ? { gitIdentityId } : {}), }); } 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 as unknown as Record).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 as unknown as Record).iconImage = { mime, updatedAt, source }; } } if (typeof candidate.color === 'string' && candidate.color.trim().length > 0) { project.color = candidate.color.trim(); } if (candidate.iconBackground === null) { (project as unknown as Record).iconBackground = null; } else { const iconBackground = normalizeIconBackground(candidate.iconBackground); if (iconBackground) { (project as unknown as Record).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 as unknown as Record).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 as unknown as { persist?: PersistApi }).persist; if (candidate && typeof candidate === 'object') { return candidate; } return undefined; }; const getRuntimeSettingsAPI = () => getRegisteredRuntimeAPIs()?.settings ?? null; 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.showReasoningTraces === 'boolean' && settings.showReasoningTraces !== store.showReasoningTraces) { store.setShowReasoningTraces(settings.showReasoningTraces); } 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.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.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.showToolFileIcons === 'boolean' && settings.showToolFileIcons !== store.showToolFileIcons) { store.setShowToolFileIcons(settings.showToolFileIcons); } 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.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.expandedEditorToolbar === 'boolean' && settings.expandedEditorToolbar !== store.expandedEditorToolbar) { store.setExpandedEditorToolbar(settings.expandedEditorToolbar); } 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)) { const nextStarters = sanitizeStarterRefs(settings.draftStarters); if (JSON.stringify(store.globalDraftStarters) !== JSON.stringify(nextStarters)) { store.setGlobalDraftStarters(nextStarters); } } if (typeof settings.terminalFontSize === 'number' && Number.isFinite(settings.terminalFontSize) && settings.terminalFontSize !== store.terminalFontSize) { store.setTerminalFontSize(settings.terminalFontSize); } 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 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; } const projects = sanitizeProjects(candidate.projects); if (projects) { result.projects = projects; } if (typeof candidate.activeProjectId === 'string' && candidate.activeProjectId.length > 0) { result.activeProjectId = candidate.activeProjectId; } 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.showReasoningTraces === 'boolean') { result.showReasoningTraces = candidate.showReasoningTraces; } if (typeof candidate.sessionRecapEnabled === 'boolean') { result.sessionRecapEnabled = candidate.sessionRecapEnabled; } if (typeof candidate.sessionSuggestionEnabled === 'boolean') { result.sessionSuggestionEnabled = candidate.sessionSuggestionEnabled; } if (typeof candidate.collapsibleThinkingBlocks === 'boolean') { result.collapsibleThinkingBlocks = candidate.collapsibleThinkingBlocks; } if (typeof candidate.autoDeleteEnabled === 'boolean') { result.autoDeleteEnabled = candidate.autoDeleteEnabled; } 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.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 (typeof candidate.usageAutoRefresh === 'boolean') { result.usageAutoRefresh = candidate.usageAutoRefresh; } if (typeof candidate.usageRefreshIntervalMs === 'number' && Number.isFinite(candidate.usageRefreshIntervalMs)) { result.usageRefreshIntervalMs = candidate.usageRefreshIntervalMs; } if (candidate.usageDisplayMode === 'usage' || candidate.usageDisplayMode === 'remaining') { result.usageDisplayMode = candidate.usageDisplayMode; } if (typeof candidate.usageShowPredValues === 'boolean') { result.usageShowPredValues = candidate.usageShowPredValues; } 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: { customGroups?: Array<{id: string; label: string; models: string[]; order: number}>; modelAssignments?: Record; renamedGroups?: Record; } = {}; // 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.openCodeUpdateToastDismissedVersion === 'string') { result.openCodeUpdateToastDismissedVersion = candidate.openCodeUpdateToastDismissedVersion.trim().slice(0, 128); } if (typeof candidate.showToolFileIcons === 'boolean') { result.showToolFileIcons = candidate.showToolFileIcons; } 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.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.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 (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; }; // Short-lived cache + in-flight dedup for settings fetches to avoid repeated GET calls during startup let _settingsCache: { value: DesktopSettings | null; at: number } | null = null; let _settingsInflight: Promise | null = null; const SETTINGS_CACHE_TTL = 2_000; // 2 seconds — covers the startup burst const fetchWebSettings = async (): Promise => { // Return cached if fresh if (_settingsCache && Date.now() - _settingsCache.at < SETTINGS_CACHE_TTL) { return _settingsCache.value; } // Dedup concurrent calls if (_settingsInflight) return _settingsInflight; _settingsInflight = (async (): Promise => { const runtimeSettings = getRuntimeSettingsAPI(); if (runtimeSettings) { try { const result = await runtimeSettings.load(); const settings = sanitizeWebSettings(result.settings); _settingsCache = { value: settings, at: Date.now() }; return settings; } catch (error) { console.warn('Failed to load shared settings from runtime settings API:', error); } } try { const response = await runtimeFetch('/api/config/settings', { method: 'GET', headers: { Accept: 'application/json' }, }); if (!response.ok) { return null; } const data = await response.json().catch(() => null); const settings = sanitizeWebSettings(data); _settingsCache = { value: settings, at: Date.now() }; return settings; } catch (error) { console.warn('Failed to load shared settings from server:', error); return null; } })().finally(() => { _settingsInflight = null; }); return _settingsInflight; }; /** Invalidate cached settings (call after a successful PUT) */ export const invalidateSettingsCache = (): void => { _settingsCache = null; }; export const syncDesktopSettings = async (): Promise => { if (typeof window === 'undefined') { return; } const persistApi = getPersistApi(); // 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 waitForHydration = (): 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(); }); }; // 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) => { try { persistToLocalStorage(settings); } catch (error) { console.warn('persistToLocalStorage failed:', error); } await waitForHydration(); try { applyDesktopUiPreferences(settings); } catch (error) { console.warn('applyDesktopUiPreferences failed:', error); } dispatchSettingsSynced(settings); }; try { const webSettings = await fetchWebSettings(); if (webSettings) { await applySettings(webSettings); } } catch (error) { console.warn('Failed to synchronise settings:', error); } }; // Coalesce rapid updateDesktopSettings calls into a single PUT let _pendingSettingsChanges: Partial | null = null; let _settingsFlushTimer: ReturnType | null = null; let _settingsFlushWaiters: Array<() => void> = []; const SETTINGS_DEBOUNCE_MS = 200; const _flushSettingsUpdate = async (): Promise => { const changes = _pendingSettingsChanges; const waiters = _settingsFlushWaiters; _pendingSettingsChanges = null; _settingsFlushTimer = null; _settingsFlushWaiters = []; if (!changes || Object.keys(changes).length === 0) { waiters.forEach((resolve) => resolve()); return; } const runtimeSettings = getRuntimeSettingsAPI(); if (runtimeSettings) { try { const updated = await runtimeSettings.save(changes); if (updated) { persistToLocalStorage(updated); applyDesktopUiPreferences(updated); dispatchSettingsSynced(updated); _settingsCache = null; } waiters.forEach((resolve) => resolve()); return; } catch (error) { console.warn('Failed to update settings via runtime settings API:', error); } } try { const response = await runtimeFetch('/api/config/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, body: JSON.stringify(changes), }); if (!response.ok) { console.warn('Failed to update shared settings via API:', response.status, response.statusText); return; } const updated = (await response.json().catch(() => null)) as DesktopSettings | null; if (updated) { persistToLocalStorage(updated); applyDesktopUiPreferences(updated); dispatchSettingsSynced(updated); // Invalidate GET cache so next read sees the fresh data _settingsCache = null; } } catch (error) { console.warn('Failed to update shared settings via API:', error); } finally { waiters.forEach((resolve) => resolve()); } }; export const updateDesktopSettings = async (changes: Partial): Promise => { if (typeof window === 'undefined') { return; } _pendingSettingsChanges = { ...(_pendingSettingsChanges ?? {}), ...changes }; 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); } };