2025-12-07 19:32:53 +02:00
|
|
|
import type { DesktopSettings } from '@/lib/desktop';
|
|
|
|
|
import { useUIStore } from '@/stores/useUIStore';
|
2025-12-29 02:16:42 +02:00
|
|
|
import { useMessageQueueStore } from '@/stores/messageQueueStore';
|
2026-01-26 00:24:40 +02:00
|
|
|
import { setDirectoryShowHidden } from '@/lib/directoryShowHidden';
|
|
|
|
|
import { setFilesViewShowGitignored } from '@/lib/filesViewShowGitignored';
|
2025-12-07 19:32:53 +02:00
|
|
|
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;
|
|
|
|
|
}
|
2026-01-06 21:31:04 +02:00
|
|
|
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');
|
|
|
|
|
}
|
2025-12-07 19:32:53 +02:00
|
|
|
if (Array.isArray(settings.pinnedDirectories) && settings.pinnedDirectories.length > 0) {
|
|
|
|
|
localStorage.setItem('pinnedDirectories', JSON.stringify(settings.pinnedDirectories));
|
|
|
|
|
} else {
|
|
|
|
|
localStorage.removeItem('pinnedDirectories');
|
|
|
|
|
}
|
2026-02-05 01:59:49 +02:00
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-17 01:09:21 -08:00
|
|
|
if (typeof settings.gitmojiEnabled === 'boolean') {
|
|
|
|
|
localStorage.setItem('gitmojiEnabled', String(settings.gitmojiEnabled));
|
|
|
|
|
} else {
|
|
|
|
|
localStorage.removeItem('gitmojiEnabled');
|
|
|
|
|
}
|
2026-01-26 00:24:40 +02:00
|
|
|
if (typeof settings.directoryShowHidden === 'boolean') {
|
|
|
|
|
localStorage.setItem('directoryTreeShowHidden', settings.directoryShowHidden ? 'true' : 'false');
|
|
|
|
|
}
|
|
|
|
|
if (typeof settings.filesViewShowGitignored === 'boolean') {
|
|
|
|
|
localStorage.setItem('filesViewShowGitignored', settings.filesViewShowGitignored ? 'true' : 'false');
|
|
|
|
|
}
|
2025-12-07 19:32:53 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type PersistApi = {
|
|
|
|
|
hasHydrated?: () => boolean;
|
2026-01-17 01:09:21 -08:00
|
|
|
onFinishHydration?: (callback: () => void) => (() => void) | undefined;
|
2025-12-07 19:32:53 +02:00
|
|
|
};
|
|
|
|
|
|
2025-12-30 21:03:16 +02:00
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-06 21:31:04 +02:00
|
|
|
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 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<DesktopSettings['projects']>[number] = {
|
|
|
|
|
id,
|
|
|
|
|
path: normalizedPath,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (typeof candidate.label === 'string' && candidate.label.trim().length > 0) {
|
|
|
|
|
project.label = candidate.label.trim();
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
2026-02-05 01:59:49 +02:00
|
|
|
if (typeof candidate.sidebarCollapsed === 'boolean') {
|
|
|
|
|
(project as unknown as Record<string, unknown>).sidebarCollapsed = candidate.sidebarCollapsed;
|
|
|
|
|
}
|
2026-01-06 21:31:04 +02:00
|
|
|
result.push(project);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result.length > 0 ? result : undefined;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-05 01:59:49 +02:00
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
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();
|
2025-12-29 02:16:42 +02:00
|
|
|
const queueStore = useMessageQueueStore.getState();
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
if (typeof settings.showReasoningTraces === 'boolean' && settings.showReasoningTraces !== store.showReasoningTraces) {
|
|
|
|
|
store.setShowReasoningTraces(settings.showReasoningTraces);
|
|
|
|
|
}
|
2025-12-20 02:06:00 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-19 14:56:27 +02:00
|
|
|
|
|
|
|
|
if (typeof settings.memoryLimitHistorical === 'number' && Number.isFinite(settings.memoryLimitHistorical)) {
|
|
|
|
|
store.setMemoryLimitHistorical(settings.memoryLimitHistorical);
|
|
|
|
|
}
|
|
|
|
|
if (typeof settings.memoryLimitViewport === 'number' && Number.isFinite(settings.memoryLimitViewport)) {
|
|
|
|
|
store.setMemoryLimitViewport(settings.memoryLimitViewport);
|
|
|
|
|
}
|
|
|
|
|
if (typeof settings.memoryLimitActiveSession === 'number' && Number.isFinite(settings.memoryLimitActiveSession)) {
|
|
|
|
|
store.setMemoryLimitActiveSession(settings.memoryLimitActiveSession);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 02:16:42 +02:00
|
|
|
if (typeof settings.queueModeEnabled === 'boolean' && settings.queueModeEnabled !== queueStore.queueModeEnabled) {
|
|
|
|
|
queueStore.setQueueMode(settings.queueModeEnabled);
|
|
|
|
|
}
|
2026-01-26 00:24:40 +02:00
|
|
|
|
|
|
|
|
if (typeof settings.showTextJustificationActivity === 'boolean' && settings.showTextJustificationActivity !== store.showTextJustificationActivity) {
|
|
|
|
|
store.setShowTextJustificationActivity(settings.showTextJustificationActivity);
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-30 20:15:00 +08:00
|
|
|
if (typeof settings.notifyOnSubtasks === 'boolean' && settings.notifyOnSubtasks !== store.notifyOnSubtasks) {
|
|
|
|
|
store.setNotifyOnSubtasks(settings.notifyOnSubtasks);
|
|
|
|
|
}
|
2026-01-26 00:24:40 +02:00
|
|
|
if (typeof settings.toolCallExpansion === 'string'
|
|
|
|
|
&& (settings.toolCallExpansion === 'collapsed' || settings.toolCallExpansion === 'activity' || settings.toolCallExpansion === 'detailed')) {
|
|
|
|
|
if (settings.toolCallExpansion !== store.toolCallExpansion) {
|
|
|
|
|
store.setToolCallExpansion(settings.toolCallExpansion);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (typeof settings.fontSize === 'number' && Number.isFinite(settings.fontSize) && settings.fontSize !== store.fontSize) {
|
|
|
|
|
store.setFontSize(settings.fontSize);
|
|
|
|
|
}
|
2026-02-04 16:32:25 +02:00
|
|
|
if (typeof settings.terminalFontSize === 'number' && Number.isFinite(settings.terminalFontSize) && settings.terminalFontSize !== store.terminalFontSize) {
|
|
|
|
|
store.setTerminalFontSize(settings.terminalFontSize);
|
|
|
|
|
}
|
2026-01-26 00:24:40 +02:00
|
|
|
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);
|
|
|
|
|
}
|
2026-02-05 01:59:49 +02:00
|
|
|
|
|
|
|
|
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 });
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-26 00:24:40 +02:00
|
|
|
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.directoryShowHidden === 'boolean') {
|
|
|
|
|
setDirectoryShowHidden(settings.directoryShowHidden, { persist: false });
|
|
|
|
|
}
|
|
|
|
|
if (typeof settings.filesViewShowGitignored === 'boolean') {
|
|
|
|
|
setFilesViewShowGitignored(settings.filesViewShowGitignored, { persist: false });
|
|
|
|
|
}
|
2025-12-07 19:32:53 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2026-01-06 21:31:04 +02:00
|
|
|
|
2026-02-06 01:32:26 +02:00
|
|
|
if (typeof candidate.opencodeBinary === 'string') {
|
|
|
|
|
const trimmed = candidate.opencodeBinary.trim();
|
|
|
|
|
result.opencodeBinary = trimmed.length > 0 ? trimmed : undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 21:31:04 +02:00
|
|
|
const projects = sanitizeProjects(candidate.projects);
|
|
|
|
|
if (projects) {
|
|
|
|
|
result.projects = projects;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.activeProjectId === 'string' && candidate.activeProjectId.length > 0) {
|
|
|
|
|
result.activeProjectId = candidate.activeProjectId;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
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;
|
|
|
|
|
}
|
2025-12-20 02:06:00 +02:00
|
|
|
if (typeof candidate.autoDeleteEnabled === 'boolean') {
|
|
|
|
|
result.autoDeleteEnabled = candidate.autoDeleteEnabled;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.autoDeleteAfterDays === 'number' && Number.isFinite(candidate.autoDeleteAfterDays)) {
|
|
|
|
|
result.autoDeleteAfterDays = candidate.autoDeleteAfterDays;
|
|
|
|
|
}
|
2025-12-26 04:38:38 +02:00
|
|
|
if (typeof candidate.defaultModel === 'string' && candidate.defaultModel.length > 0) {
|
|
|
|
|
result.defaultModel = candidate.defaultModel;
|
|
|
|
|
}
|
2026-01-08 16:08:51 +02:00
|
|
|
if (typeof candidate.defaultVariant === 'string' && candidate.defaultVariant.length > 0) {
|
|
|
|
|
result.defaultVariant = candidate.defaultVariant;
|
|
|
|
|
}
|
2025-12-26 04:38:38 +02:00
|
|
|
if (typeof candidate.defaultAgent === 'string' && candidate.defaultAgent.length > 0) {
|
|
|
|
|
result.defaultAgent = candidate.defaultAgent;
|
|
|
|
|
}
|
2026-01-07 23:37:32 +02:00
|
|
|
if (typeof candidate.autoCreateWorktree === 'boolean') {
|
|
|
|
|
result.autoCreateWorktree = candidate.autoCreateWorktree;
|
|
|
|
|
}
|
2026-01-17 01:09:21 -08:00
|
|
|
if (typeof candidate.gitmojiEnabled === 'boolean') {
|
|
|
|
|
result.gitmojiEnabled = candidate.gitmojiEnabled;
|
|
|
|
|
}
|
2025-12-29 02:16:42 +02:00
|
|
|
if (typeof candidate.queueModeEnabled === 'boolean') {
|
|
|
|
|
result.queueModeEnabled = candidate.queueModeEnabled;
|
|
|
|
|
}
|
2026-01-26 00:24:40 +02:00
|
|
|
if (typeof candidate.showTextJustificationActivity === 'boolean') {
|
|
|
|
|
result.showTextJustificationActivity = candidate.showTextJustificationActivity;
|
|
|
|
|
}
|
|
|
|
|
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;
|
|
|
|
|
}
|
2026-01-30 20:15:00 +08:00
|
|
|
if (typeof candidate.notifyOnSubtasks === 'boolean') {
|
|
|
|
|
result.notifyOnSubtasks = candidate.notifyOnSubtasks;
|
|
|
|
|
}
|
2026-02-01 13:30:30 -03:00
|
|
|
if (typeof candidate.usageAutoRefresh === 'boolean') {
|
|
|
|
|
result.usageAutoRefresh = candidate.usageAutoRefresh;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.usageRefreshIntervalMs === 'number' && Number.isFinite(candidate.usageRefreshIntervalMs)) {
|
|
|
|
|
result.usageRefreshIntervalMs = candidate.usageRefreshIntervalMs;
|
|
|
|
|
}
|
2026-02-04 01:13:23 -08:00
|
|
|
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
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-01-26 00:24:40 +02:00
|
|
|
if (
|
|
|
|
|
typeof candidate.toolCallExpansion === 'string'
|
|
|
|
|
&& (candidate.toolCallExpansion === 'collapsed'
|
|
|
|
|
|| candidate.toolCallExpansion === 'activity'
|
|
|
|
|
|| candidate.toolCallExpansion === 'detailed')
|
|
|
|
|
) {
|
|
|
|
|
result.toolCallExpansion = candidate.toolCallExpansion;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.fontSize === 'number' && Number.isFinite(candidate.fontSize)) {
|
|
|
|
|
result.fontSize = candidate.fontSize;
|
|
|
|
|
}
|
2026-02-04 16:32:25 +02:00
|
|
|
if (typeof candidate.terminalFontSize === 'number' && Number.isFinite(candidate.terminalFontSize)) {
|
|
|
|
|
result.terminalFontSize = candidate.terminalFontSize;
|
|
|
|
|
}
|
2026-01-26 00:24:40 +02:00
|
|
|
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;
|
|
|
|
|
}
|
2026-02-05 01:59:49 +02:00
|
|
|
|
|
|
|
|
const favoriteModels = sanitizeModelRefs(candidate.favoriteModels, 64);
|
|
|
|
|
if (favoriteModels) {
|
|
|
|
|
result.favoriteModels = favoriteModels;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const recentModels = sanitizeModelRefs(candidate.recentModels, 16);
|
|
|
|
|
if (recentModels) {
|
|
|
|
|
result.recentModels = recentModels;
|
|
|
|
|
}
|
2026-01-26 00:24:40 +02:00
|
|
|
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.directoryShowHidden === 'boolean') {
|
|
|
|
|
result.directoryShowHidden = candidate.directoryShowHidden;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.filesViewShowGitignored === 'boolean') {
|
|
|
|
|
result.filesViewShowGitignored = candidate.filesViewShowGitignored;
|
|
|
|
|
}
|
2025-12-07 19:32:53 +02:00
|
|
|
|
2026-01-19 14:56:27 +02:00
|
|
|
if (typeof candidate.memoryLimitHistorical === 'number' && Number.isFinite(candidate.memoryLimitHistorical)) {
|
|
|
|
|
result.memoryLimitHistorical = candidate.memoryLimitHistorical;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.memoryLimitViewport === 'number' && Number.isFinite(candidate.memoryLimitViewport)) {
|
|
|
|
|
result.memoryLimitViewport = candidate.memoryLimitViewport;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.memoryLimitActiveSession === 'number' && Number.isFinite(candidate.memoryLimitActiveSession)) {
|
|
|
|
|
result.memoryLimitActiveSession = candidate.memoryLimitActiveSession;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-30 21:03:16 +02:00
|
|
|
const skillCatalogs = sanitizeSkillCatalogs(candidate.skillCatalogs);
|
|
|
|
|
if (skillCatalogs) {
|
|
|
|
|
result.skillCatalogs = skillCatalogs;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-07 19:32:53 +02:00
|
|
|
return result;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const fetchWebSettings = async (): Promise<DesktopSettings | null> => {
|
|
|
|
|
const runtimeSettings = getRuntimeSettingsAPI();
|
|
|
|
|
if (runtimeSettings) {
|
|
|
|
|
try {
|
|
|
|
|
const result = await runtimeSettings.load();
|
|
|
|
|
return sanitizeWebSettings(result.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);
|
|
|
|
|
return sanitizeWebSettings(data);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('Failed to load shared settings from server:', error);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const syncDesktopSettings = async (): Promise<void> => {
|
|
|
|
|
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<DesktopSettings>('openchamber:settings-synced', { detail: settings }));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
2026-02-05 01:59:49 +02:00
|
|
|
const webSettings = await fetchWebSettings();
|
|
|
|
|
if (webSettings) {
|
|
|
|
|
applySettings(webSettings);
|
2025-12-07 19:32:53 +02:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('Failed to synchronise settings:', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const updateDesktopSettings = async (changes: Partial<DesktopSettings>): Promise<void> => {
|
|
|
|
|
if (typeof window === 'undefined') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 01:59:49 +02:00
|
|
|
// Desktop shell uses the same HTTP settings API as web.
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.warn('Failed to update shared settings via API:', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|