1470 lines
61 KiB
TypeScript
1470 lines
61 KiB
TypeScript
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<DesktopSettings>('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<DesktopSettings['skillCatalogs']> = [];
|
|
const seen = new Set<string>();
|
|
|
|
for (const entry of value) {
|
|
if (!entry || typeof entry !== 'object') continue;
|
|
const candidate = entry as Record<string, unknown>;
|
|
|
|
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<string, string> | undefined => {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
return undefined;
|
|
}
|
|
const result: Record<string, string> = {};
|
|
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<string, string>, right: Record<string, string>): 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<string, string[]> | undefined => {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
const result: Record<string, string[]> = {};
|
|
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<string, string[]>, right: Record<string, string[]>): 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<DesktopSettings['projects']> = [];
|
|
const seenIds = new Set<string>();
|
|
const seenPaths = new Set<string>();
|
|
|
|
for (const entry of value) {
|
|
if (!entry || typeof entry !== 'object') continue;
|
|
const candidate = entry as Record<string, unknown>;
|
|
|
|
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<DesktopSettings['projects']>[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<string, unknown>).iconImage = null;
|
|
} else if (candidate.iconImage && typeof candidate.iconImage === 'object') {
|
|
const iconImage = candidate.iconImage as Record<string, unknown>;
|
|
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<string, unknown>).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<string, unknown>).iconBackground = null;
|
|
} else {
|
|
const iconBackground = normalizeIconBackground(candidate.iconBackground);
|
|
if (iconBackground) {
|
|
(project as unknown as Record<string, unknown>).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<string, unknown>).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<DesktopSettings['managedRemoteTunnelPresets']> = [];
|
|
const seenIds = new Set<string>();
|
|
const seenHostnames = new Set<string>();
|
|
|
|
for (const entry of value) {
|
|
if (!entry || typeof entry !== 'object') continue;
|
|
const candidate = entry as Record<string, unknown>;
|
|
|
|
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<string, unknown>;
|
|
const result: Record<string, string> = {};
|
|
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<string>();
|
|
|
|
for (const entry of value) {
|
|
if (!entry || typeof entry !== 'object') continue;
|
|
const candidate = entry as Record<string, unknown>;
|
|
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<typeof configStore> = {};
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, string[]>)
|
|
if (candidate.usageSelectedModels && typeof candidate.usageSelectedModels === 'object') {
|
|
const selectedModels: Record<string, string[]> = {};
|
|
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<string, string[]>)
|
|
if (candidate.usageCollapsedFamilies && typeof candidate.usageCollapsedFamilies === 'object') {
|
|
const collapsedFamilies: Record<string, string[]> = {};
|
|
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<string, string[]>) - inverted collapsed logic for header dropdown
|
|
if (candidate.usageExpandedFamilies && typeof candidate.usageExpandedFamilies === 'object') {
|
|
const expandedFamilies: Record<string, string[]> = {};
|
|
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<string, {
|
|
customGroups?: Array<{id: string; label: string; models: string[]; order: number}>;
|
|
modelAssignments?: Record<string, string>;
|
|
renamedGroups?: Record<string, string>;
|
|
}> = {};
|
|
for (const [providerId, config] of Object.entries(candidate.usageModelGroups)) {
|
|
if (config && typeof config === 'object') {
|
|
const typedConfig = config as Record<string, unknown>;
|
|
const providerConfig: {
|
|
customGroups?: Array<{id: string; label: string; models: string[]; order: number}>;
|
|
modelAssignments?: Record<string, string>;
|
|
renamedGroups?: Record<string, string>;
|
|
} = {};
|
|
|
|
// Parse customGroups
|
|
if (Array.isArray(typedConfig.customGroups)) {
|
|
providerConfig.customGroups = typedConfig.customGroups
|
|
.filter((g): g is Record<string, unknown> => 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<string, unknown>)
|
|
.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<string, unknown>)
|
|
.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<DesktopSettings | null> | null = null;
|
|
const SETTINGS_CACHE_TTL = 2_000; // 2 seconds — covers the startup burst
|
|
|
|
const fetchWebSettings = async (): Promise<DesktopSettings | null> => {
|
|
// 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<DesktopSettings | null> => {
|
|
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<void> => {
|
|
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<void> => {
|
|
if (!persistApi?.hasHydrated || persistApi.hasHydrated()) {
|
|
return Promise.resolve();
|
|
}
|
|
if (!persistApi.onFinishHydration) {
|
|
return Promise.resolve();
|
|
}
|
|
return new Promise<void>((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<DesktopSettings> | null = null;
|
|
let _settingsFlushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
let _settingsFlushWaiters: Array<() => void> = [];
|
|
const SETTINGS_DEBOUNCE_MS = 200;
|
|
|
|
const _flushSettingsUpdate = async (): Promise<void> => {
|
|
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<DesktopSettings>): Promise<void> => {
|
|
if (typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
_pendingSettingsChanges = { ...(_pendingSettingsChanges ?? {}), ...changes };
|
|
|
|
if (_settingsFlushTimer) {
|
|
clearTimeout(_settingsFlushTimer);
|
|
}
|
|
const flushed = new Promise<void>((resolve) => {
|
|
_settingsFlushWaiters.push(resolve);
|
|
});
|
|
_settingsFlushTimer = setTimeout(() => void _flushSettingsUpdate(), SETTINGS_DEBOUNCE_MS);
|
|
return flushed;
|
|
};
|
|
|
|
export const initializeAppearancePreferences = async (): Promise<void> => {
|
|
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);
|
|
}
|
|
};
|