import type { DesktopSettings } from '@/lib/desktop'; import { useUIStore } from '@/stores/useUIStore'; import { useMessageQueueStore } from '@/stores/messageQueueStore'; import { setDirectoryShowHidden } from '@/lib/directoryShowHidden'; import { setFilesViewShowGitignored } from '@/lib/filesViewShowGitignored'; import { loadAppearancePreferences, applyAppearancePreferences } from '@/lib/appearancePersistence'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; 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); window.__OPENCHAMBER_HOME__ = 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'); } } }; 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 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 id = typeof candidate.id === 'string' ? candidate.id.trim() : ''; const rawPath = typeof candidate.path === 'string' ? candidate.path.trim() : ''; if (!id || !rawPath) continue; const normalizedPath = rawPath === '/' ? rawPath : rawPath.replace(/\\/g, '/').replace(/\/+$/, ''); if (!normalizedPath) 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 queueStore = useMessageQueueStore.getState(); if (typeof settings.showReasoningTraces === 'boolean' && settings.showReasoningTraces !== store.showReasoningTraces) { store.setShowReasoningTraces(settings.showReasoningTraces); } 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); } } if (typeof settings.queueModeEnabled === 'boolean' && settings.queueModeEnabled !== queueStore.queueModeEnabled) { queueStore.setQueueMode(settings.queueModeEnabled); } 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.showToolFileIcons === 'boolean' && settings.showToolFileIcons !== store.showToolFileIcons) { store.setShowToolFileIcons(settings.showToolFileIcons); } 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.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.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 (typeof settings.terminalFontSize === 'number' && Number.isFinite(settings.terminalFontSize) && settings.terminalFontSize !== store.terminalFontSize) { store.setTerminalFontSize(settings.terminalFontSize); } 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 (Array.isArray(settings.favoriteModels)) { const current = store.favoriteModels; const next = settings.favoriteModels; const same = current.length === next.length && current.every((item, idx) => item.providerID === next[idx]?.providerID && item.modelID === next[idx]?.modelID); if (!same) { useUIStore.setState({ favoriteModels: next }); } } if (Array.isArray(settings.recentModels)) { const current = store.recentModels; const next = settings.recentModels; const same = current.length === next.length && current.every((item, idx) => item.providerID === next[idx]?.providerID && item.modelID === next[idx]?.modelID); if (!same) { useUIStore.setState({ recentModels: 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.diffViewMode === 'string' && (settings.diffViewMode === 'single' || settings.diffViewMode === 'stacked')) { if (settings.diffViewMode !== store.diffViewMode) { store.setDiffViewMode(settings.diffViewMode); } } 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; } 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.approvedDirectories)) { result.approvedDirectories = candidate.approvedDirectories.filter( (entry): entry is string => typeof entry === 'string' && entry.length > 0 ); } 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 (typeof candidate.showReasoningTraces === 'boolean') { result.showReasoningTraces = candidate.showReasoningTraces; } 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.autoCreateWorktree === 'boolean') { result.autoCreateWorktree = candidate.autoCreateWorktree; } if (typeof candidate.gitmojiEnabled === 'boolean') { result.gitmojiEnabled = candidate.gitmojiEnabled; } if (typeof candidate.queueModeEnabled === 'boolean') { result.queueModeEnabled = 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 (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.showToolFileIcons === 'boolean') { result.showToolFileIcons = candidate.showToolFileIcons; } 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.stickyUserHeader === 'boolean') { result.stickyUserHeader = candidate.stickyUserHeader; } 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 (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 favoriteModels = sanitizeModelRefs(candidate.favoriteModels, 64); if (favoriteModels) { result.favoriteModels = favoriteModels; } const recentModels = sanitizeModelRefs(candidate.recentModels, 16); if (recentModels) { result.recentModels = recentModels; } if ( typeof candidate.diffLayoutPreference === 'string' && (candidate.diffLayoutPreference === 'dynamic' || candidate.diffLayoutPreference === 'inline' || candidate.diffLayoutPreference === 'side-by-side') ) { result.diffLayoutPreference = candidate.diffLayoutPreference; } if ( typeof candidate.diffViewMode === 'string' && (candidate.diffViewMode === 'single' || candidate.diffViewMode === 'stacked') ) { result.diffViewMode = candidate.diffViewMode; } 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; } 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 fetch('/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(); const applySettings = (settings: DesktopSettings) => { persistToLocalStorage(settings); const apply = () => applyDesktopUiPreferences(settings); if (persistApi?.hasHydrated?.()) { apply(); } else { apply(); if (persistApi?.onFinishHydration) { const unsubscribe = persistApi.onFinishHydration(() => { unsubscribe?.(); apply(); }); } } if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent('openchamber:settings-synced', { detail: settings })); } }; try { const webSettings = await fetchWebSettings(); if (webSettings) { 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; const SETTINGS_DEBOUNCE_MS = 200; const _flushSettingsUpdate = async (): Promise => { const changes = _pendingSettingsChanges; _pendingSettingsChanges = null; _settingsFlushTimer = null; if (!changes || Object.keys(changes).length === 0) return; const runtimeSettings = getRuntimeSettingsAPI(); if (runtimeSettings) { try { const updated = await runtimeSettings.save(changes); if (updated) { persistToLocalStorage(updated); applyDesktopUiPreferences(updated); } return; } catch (error) { console.warn('Failed to update settings via runtime settings API:', error); } } try { const response = await fetch('/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); // Invalidate GET cache so next read sees the fresh data _settingsCache = null; } } catch (error) { console.warn('Failed to update shared settings via API:', error); } }; export const updateDesktopSettings = async (changes: Partial): Promise => { if (typeof window === 'undefined') { return; } _pendingSettingsChanges = { ...(_pendingSettingsChanges ?? {}), ...changes }; if (_settingsFlushTimer) { clearTimeout(_settingsFlushTimer); } _settingsFlushTimer = setTimeout(() => void _flushSettingsUpdate(), SETTINGS_DEBOUNCE_MS); }; 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); } };