2026-03-31 18:47:00 +03:00
|
|
|
export const createSettingsHelpers = (dependencies) => {
|
|
|
|
|
const {
|
|
|
|
|
normalizePathForPersistence,
|
|
|
|
|
normalizeDirectoryPath,
|
|
|
|
|
normalizeTunnelBootstrapTtlMs,
|
|
|
|
|
normalizeTunnelSessionTtlMs,
|
|
|
|
|
normalizeTunnelProvider,
|
|
|
|
|
normalizeTunnelMode,
|
|
|
|
|
normalizeOptionalPath,
|
|
|
|
|
normalizeManagedRemoteTunnelHostname,
|
|
|
|
|
normalizeManagedRemoteTunnelPresets,
|
|
|
|
|
normalizeManagedRemoteTunnelPresetTokens,
|
|
|
|
|
sanitizeTypographySizesPartial,
|
|
|
|
|
normalizeStringArray,
|
|
|
|
|
sanitizeModelRefs,
|
|
|
|
|
sanitizeSkillCatalogs,
|
|
|
|
|
sanitizeProjects,
|
|
|
|
|
} = dependencies;
|
|
|
|
|
|
|
|
|
|
const PWA_APP_NAME_MAX_LENGTH = 64;
|
2026-05-13 19:45:03 +07:00
|
|
|
const STT_SERVER_URL_MAX_LENGTH = 2048;
|
|
|
|
|
const STT_MODEL_MAX_LENGTH = 256;
|
|
|
|
|
const STT_LANGUAGE_MAX_LENGTH = 64;
|
2026-06-03 16:00:32 +03:00
|
|
|
const VERSION_STRING_MAX_LENGTH = 128;
|
|
|
|
|
const SHORTCUT_OVERRIDE_KEY_MAX_LENGTH = 128;
|
|
|
|
|
const SHORTCUT_OVERRIDE_VALUE_MAX_LENGTH = 128;
|
2026-04-22 03:17:34 +07:00
|
|
|
const PWA_ORIENTATION_VALUES = new Set(['system', 'portrait', 'landscape']);
|
2026-05-05 16:26:34 +07:00
|
|
|
const MOBILE_KEYBOARD_MODE_VALUES = new Set(['native', 'resize-content']);
|
2026-06-23 21:46:23 +03:00
|
|
|
const HIDDEN_MODELS_MAX = 1024;
|
|
|
|
|
const RECENT_EFFORTS_MAX_KEYS = 128;
|
|
|
|
|
const RECENT_EFFORTS_MAX_VARIANTS_PER_KEY = 5;
|
2026-03-31 18:47:00 +03:00
|
|
|
|
2026-06-03 16:00:32 +03:00
|
|
|
const sanitizeShortcutOverrides = (value) => {
|
|
|
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const result = {};
|
|
|
|
|
for (const [rawKey, rawValue] of Object.entries(value)) {
|
|
|
|
|
const key = typeof rawKey === 'string' ? rawKey.trim() : '';
|
|
|
|
|
const combo = typeof rawValue === 'string' ? rawValue.trim() : '';
|
|
|
|
|
if (!key || !combo) continue;
|
|
|
|
|
result[key.slice(0, SHORTCUT_OVERRIDE_KEY_MAX_LENGTH)] = combo.slice(0, SHORTCUT_OVERRIDE_VALUE_MAX_LENGTH);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-23 21:46:23 +03:00
|
|
|
const sanitizeRecentEfforts = (value) => {
|
|
|
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const result = {};
|
|
|
|
|
const seenKeys = new Set();
|
|
|
|
|
let count = 0;
|
|
|
|
|
for (const [rawKey, rawVariants] of Object.entries(value)) {
|
|
|
|
|
const key = typeof rawKey === 'string' ? rawKey.trim() : '';
|
|
|
|
|
if (!key || seenKeys.has(key)) continue;
|
|
|
|
|
if (!Array.isArray(rawVariants)) continue;
|
|
|
|
|
const variants = [];
|
|
|
|
|
const seenVariants = new Set();
|
|
|
|
|
for (const rawVariant of rawVariants) {
|
|
|
|
|
const variant = typeof rawVariant === 'string' ? rawVariant.trim() : '';
|
|
|
|
|
if (!variant || seenVariants.has(variant)) continue;
|
|
|
|
|
seenVariants.add(variant);
|
|
|
|
|
variants.push(variant);
|
|
|
|
|
if (variants.length >= RECENT_EFFORTS_MAX_VARIANTS_PER_KEY) break;
|
|
|
|
|
}
|
|
|
|
|
if (variants.length === 0) continue;
|
|
|
|
|
seenKeys.add(key);
|
|
|
|
|
result[key] = variants;
|
|
|
|
|
count += 1;
|
|
|
|
|
if (count >= RECENT_EFFORTS_MAX_KEYS) break;
|
|
|
|
|
}
|
|
|
|
|
return count > 0 ? result : null;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const normalizePwaAppName = (value, fallback = '') => {
|
|
|
|
|
if (typeof value !== 'string') {
|
|
|
|
|
return fallback;
|
|
|
|
|
}
|
|
|
|
|
const normalized = value.trim().replace(/\s+/g, ' ');
|
|
|
|
|
if (!normalized) {
|
|
|
|
|
return fallback;
|
|
|
|
|
}
|
|
|
|
|
return normalized.slice(0, PWA_APP_NAME_MAX_LENGTH);
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-22 03:17:34 +07:00
|
|
|
const normalizePwaOrientation = (value, fallback = 'system') => {
|
|
|
|
|
if (typeof value !== 'string') {
|
|
|
|
|
return fallback;
|
|
|
|
|
}
|
|
|
|
|
const normalized = value.trim();
|
|
|
|
|
if (PWA_ORIENTATION_VALUES.has(normalized)) {
|
|
|
|
|
return normalized;
|
|
|
|
|
}
|
|
|
|
|
return fallback;
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-05 16:26:34 +07:00
|
|
|
const normalizeMobileKeyboardMode = (value, fallback = 'native') => {
|
|
|
|
|
if (typeof value !== 'string') {
|
|
|
|
|
return fallback;
|
|
|
|
|
}
|
|
|
|
|
const normalized = value.trim();
|
|
|
|
|
if (MOBILE_KEYBOARD_MODE_VALUES.has(normalized)) {
|
|
|
|
|
return normalized;
|
|
|
|
|
}
|
|
|
|
|
return fallback;
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-29 09:28:20 +11:00
|
|
|
const normalizeFollowUpBehavior = (value, legacyQueueModeEnabled = null) => {
|
|
|
|
|
// "immediate" was removed (it was wire-identical to "steer"); collapse it.
|
|
|
|
|
if (value === 'immediate') {
|
|
|
|
|
return 'steer';
|
|
|
|
|
}
|
|
|
|
|
if (value === 'steer' || value === 'queue') {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
if (legacyQueueModeEnabled === false) {
|
|
|
|
|
return 'steer';
|
|
|
|
|
}
|
|
|
|
|
return 'queue';
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const sanitizeSettingsUpdate = (payload) => {
|
|
|
|
|
if (!payload || typeof payload !== 'object') {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const candidate = payload;
|
|
|
|
|
const result = {};
|
|
|
|
|
|
|
|
|
|
if (typeof candidate.themeId === 'string' && candidate.themeId.length > 0) {
|
|
|
|
|
result.themeId = candidate.themeId;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.themeVariant === 'string' && (candidate.themeVariant === 'light' || candidate.themeVariant === 'dark')) {
|
|
|
|
|
result.themeVariant = candidate.themeVariant;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.useSystemTheme === 'boolean') {
|
|
|
|
|
result.useSystemTheme = candidate.useSystemTheme;
|
|
|
|
|
}
|
|
|
|
|
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.splashBgLight === 'string' && candidate.splashBgLight.trim().length > 0) {
|
|
|
|
|
result.splashBgLight = candidate.splashBgLight.trim();
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.splashFgLight === 'string' && candidate.splashFgLight.trim().length > 0) {
|
|
|
|
|
result.splashFgLight = candidate.splashFgLight.trim();
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.splashBgDark === 'string' && candidate.splashBgDark.trim().length > 0) {
|
|
|
|
|
result.splashBgDark = candidate.splashBgDark.trim();
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.splashFgDark === 'string' && candidate.splashFgDark.trim().length > 0) {
|
|
|
|
|
result.splashFgDark = candidate.splashFgDark.trim();
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.lastDirectory === 'string' && candidate.lastDirectory.length > 0) {
|
|
|
|
|
const normalized = normalizePathForPersistence(candidate.lastDirectory);
|
|
|
|
|
if (typeof normalized === 'string' && normalized.length > 0) {
|
|
|
|
|
result.lastDirectory = normalized;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.homeDirectory === 'string' && candidate.homeDirectory.length > 0) {
|
|
|
|
|
const normalized = normalizePathForPersistence(candidate.homeDirectory);
|
|
|
|
|
if (typeof normalized === 'string' && normalized.length > 0) {
|
|
|
|
|
result.homeDirectory = normalized;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Absolute path to the opencode CLI binary (optional override).
|
|
|
|
|
// Accept empty-string to clear (we persist an empty string sentinel so the running
|
|
|
|
|
// process can reliably drop a previously applied OPENCODE_BINARY override).
|
|
|
|
|
if (typeof candidate.opencodeBinary === 'string') {
|
|
|
|
|
const normalized = normalizeDirectoryPath(candidate.opencodeBinary).trim();
|
|
|
|
|
result.opencodeBinary = normalized;
|
|
|
|
|
}
|
2026-04-17 18:05:53 +03:00
|
|
|
if (typeof candidate.desktopLanAccessEnabled === 'boolean') {
|
|
|
|
|
result.desktopLanAccessEnabled = candidate.desktopLanAccessEnabled;
|
|
|
|
|
}
|
2026-07-01 19:07:20 +03:00
|
|
|
if (typeof candidate.desktopKeepAwakeEnabled === 'boolean') {
|
|
|
|
|
result.desktopKeepAwakeEnabled = candidate.desktopKeepAwakeEnabled;
|
|
|
|
|
}
|
2026-07-10 17:40:48 +08:00
|
|
|
if (typeof candidate.desktopMinimizeToTrayEnabled === 'boolean') {
|
|
|
|
|
result.desktopMinimizeToTrayEnabled = candidate.desktopMinimizeToTrayEnabled;
|
|
|
|
|
}
|
2026-07-13 08:59:31 +03:00
|
|
|
if (typeof candidate.desktopWindowControlsPosition === 'string') {
|
|
|
|
|
const mode = candidate.desktopWindowControlsPosition.trim();
|
|
|
|
|
if (mode === 'auto' || mode === 'left' || mode === 'right') {
|
|
|
|
|
result.desktopWindowControlsPosition = mode;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-12 15:03:16 +03:00
|
|
|
if (candidate.permissionAutoAccept && typeof candidate.permissionAutoAccept === 'object' && !Array.isArray(candidate.permissionAutoAccept)) {
|
|
|
|
|
const sessions = {};
|
|
|
|
|
const sourceSessions = candidate.permissionAutoAccept.sessions;
|
|
|
|
|
if (sourceSessions && typeof sourceSessions === 'object' && !Array.isArray(sourceSessions)) {
|
|
|
|
|
for (const [sessionId, enabled] of Object.entries(sourceSessions)) {
|
|
|
|
|
if (sessionId && typeof enabled === 'boolean') sessions[sessionId] = enabled;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
result.permissionAutoAccept = {
|
|
|
|
|
sessions,
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-05-25 15:20:56 +03:00
|
|
|
if (typeof candidate.desktopUiPassword === 'string') {
|
|
|
|
|
result.desktopUiPassword = candidate.desktopUiPassword.trim();
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
if (Array.isArray(candidate.projects)) {
|
|
|
|
|
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 = normalizeStringArray(candidate.securityScopedBookmarks);
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(candidate.pinnedDirectories)) {
|
|
|
|
|
result.pinnedDirectories = normalizeStringArray(
|
|
|
|
|
candidate.pinnedDirectories
|
|
|
|
|
.map((entry) => (typeof entry === 'string' ? normalizePathForPersistence(entry) : entry))
|
|
|
|
|
.filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-30 00:05:49 +03:00
|
|
|
if (Array.isArray(candidate.draftStarters)) {
|
|
|
|
|
const seenStarters = new Set();
|
|
|
|
|
const starters = [];
|
|
|
|
|
for (const entry of candidate.draftStarters) {
|
|
|
|
|
if (!entry || typeof entry !== 'object') continue;
|
|
|
|
|
const type = entry.type === 'command' || entry.type === 'skill' ? entry.type : null;
|
|
|
|
|
const name = typeof entry.name === 'string' ? entry.name.trim() : '';
|
|
|
|
|
if (!type || !name) continue;
|
|
|
|
|
const key = `${type}:${name}`;
|
|
|
|
|
if (seenStarters.has(key)) continue;
|
|
|
|
|
seenStarters.add(key);
|
|
|
|
|
starters.push({ type, name });
|
|
|
|
|
}
|
|
|
|
|
result.draftStarters = starters;
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if (typeof candidate.uiFont === 'string' && candidate.uiFont.length > 0) {
|
|
|
|
|
result.uiFont = candidate.uiFont;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.monoFont === 'string' && candidate.monoFont.length > 0) {
|
|
|
|
|
result.monoFont = candidate.monoFont;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.markdownDisplayMode === 'string' && candidate.markdownDisplayMode.length > 0) {
|
|
|
|
|
result.markdownDisplayMode = candidate.markdownDisplayMode;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.githubClientId === 'string') {
|
|
|
|
|
const trimmed = candidate.githubClientId.trim();
|
|
|
|
|
if (trimmed.length > 0) {
|
|
|
|
|
result.githubClientId = trimmed;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.githubScopes === 'string') {
|
|
|
|
|
const trimmed = candidate.githubScopes.trim();
|
|
|
|
|
if (trimmed.length > 0) {
|
|
|
|
|
result.githubScopes = trimmed;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.showReasoningTraces === 'boolean') {
|
|
|
|
|
result.showReasoningTraces = candidate.showReasoningTraces;
|
|
|
|
|
}
|
2026-07-06 22:54:31 +03:00
|
|
|
if (typeof candidate.sessionRecapEnabled === 'boolean') {
|
|
|
|
|
result.sessionRecapEnabled = candidate.sessionRecapEnabled;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.sessionSuggestionEnabled === 'boolean') {
|
|
|
|
|
result.sessionSuggestionEnabled = candidate.sessionSuggestionEnabled;
|
2026-07-05 23:19:10 +03:00
|
|
|
}
|
2026-07-12 01:23:22 +03:00
|
|
|
if (typeof candidate.sessionGoalEnabled === 'boolean') {
|
|
|
|
|
result.sessionGoalEnabled = candidate.sessionGoalEnabled;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.sessionGoalDefaultBudgetEnabled === 'boolean') {
|
|
|
|
|
result.sessionGoalDefaultBudgetEnabled = candidate.sessionGoalDefaultBudgetEnabled;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.sessionGoalDefaultBudget === 'number' && Number.isFinite(candidate.sessionGoalDefaultBudget) && candidate.sessionGoalDefaultBudget > 0) {
|
|
|
|
|
result.sessionGoalDefaultBudget = Math.floor(candidate.sessionGoalDefaultBudget);
|
|
|
|
|
}
|
2026-05-16 17:20:16 +03:00
|
|
|
if (typeof candidate.collapsibleThinkingBlocks === 'boolean') {
|
|
|
|
|
result.collapsibleThinkingBlocks = candidate.collapsibleThinkingBlocks;
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
if (typeof candidate.showTextJustificationActivity === 'boolean') {
|
|
|
|
|
result.showTextJustificationActivity = candidate.showTextJustificationActivity;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.showDeletionDialog === 'boolean') {
|
|
|
|
|
result.showDeletionDialog = candidate.showDeletionDialog;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.nativeNotificationsEnabled === 'boolean') {
|
|
|
|
|
result.nativeNotificationsEnabled = candidate.nativeNotificationsEnabled;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.notificationMode === 'string') {
|
|
|
|
|
const mode = candidate.notificationMode.trim();
|
|
|
|
|
if (mode === 'always' || mode === 'hidden-only') {
|
|
|
|
|
result.notificationMode = mode;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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') {
|
|
|
|
|
result.notificationTemplates = candidate.notificationTemplates;
|
|
|
|
|
}
|
|
|
|
|
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 = Math.max(30000, Math.min(300000, Math.round(candidate.usageRefreshIntervalMs)));
|
|
|
|
|
}
|
|
|
|
|
if (candidate.usageDisplayMode === 'usage' || candidate.usageDisplayMode === 'remaining') {
|
|
|
|
|
result.usageDisplayMode = candidate.usageDisplayMode;
|
|
|
|
|
}
|
2026-05-26 01:54:24 +03:00
|
|
|
if (typeof candidate.usageShowPredValues === 'boolean') {
|
|
|
|
|
result.usageShowPredValues = candidate.usageShowPredValues;
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
if (Array.isArray(candidate.usageDropdownProviders)) {
|
|
|
|
|
result.usageDropdownProviders = normalizeStringArray(candidate.usageDropdownProviders);
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.autoDeleteEnabled === 'boolean') {
|
|
|
|
|
result.autoDeleteEnabled = candidate.autoDeleteEnabled;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.autoDeleteAfterDays === 'number' && Number.isFinite(candidate.autoDeleteAfterDays)) {
|
|
|
|
|
const normalizedDays = Math.max(1, Math.min(365, Math.round(candidate.autoDeleteAfterDays)));
|
|
|
|
|
result.autoDeleteAfterDays = normalizedDays;
|
|
|
|
|
}
|
|
|
|
|
if (candidate.tunnelBootstrapTtlMs === null) {
|
|
|
|
|
result.tunnelBootstrapTtlMs = null;
|
|
|
|
|
} else if (typeof candidate.tunnelBootstrapTtlMs === 'number' && Number.isFinite(candidate.tunnelBootstrapTtlMs)) {
|
|
|
|
|
result.tunnelBootstrapTtlMs = normalizeTunnelBootstrapTtlMs(candidate.tunnelBootstrapTtlMs);
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.tunnelSessionTtlMs === 'number' && Number.isFinite(candidate.tunnelSessionTtlMs)) {
|
|
|
|
|
result.tunnelSessionTtlMs = normalizeTunnelSessionTtlMs(candidate.tunnelSessionTtlMs);
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.tunnelProvider === 'string') {
|
|
|
|
|
const provider = normalizeTunnelProvider(candidate.tunnelProvider);
|
|
|
|
|
if (provider) {
|
|
|
|
|
result.tunnelProvider = provider;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.tunnelMode === 'string') {
|
|
|
|
|
result.tunnelMode = normalizeTunnelMode(candidate.tunnelMode);
|
|
|
|
|
}
|
|
|
|
|
if (candidate.managedLocalTunnelConfigPath === null) {
|
|
|
|
|
result.managedLocalTunnelConfigPath = null;
|
|
|
|
|
} else if (typeof candidate.managedLocalTunnelConfigPath === 'string') {
|
|
|
|
|
const trimmed = candidate.managedLocalTunnelConfigPath.trim();
|
|
|
|
|
result.managedLocalTunnelConfigPath = trimmed.length > 0 ? normalizeOptionalPath(trimmed) : null;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.managedRemoteTunnelHostname === 'string') {
|
|
|
|
|
const hostname = normalizeManagedRemoteTunnelHostname(candidate.managedRemoteTunnelHostname);
|
|
|
|
|
result.managedRemoteTunnelHostname = hostname;
|
|
|
|
|
}
|
|
|
|
|
if (candidate.managedRemoteTunnelToken === null) {
|
|
|
|
|
result.managedRemoteTunnelToken = null;
|
|
|
|
|
} else if (typeof candidate.managedRemoteTunnelToken === 'string') {
|
|
|
|
|
result.managedRemoteTunnelToken = candidate.managedRemoteTunnelToken.trim();
|
|
|
|
|
}
|
|
|
|
|
const managedRemoteTunnelPresets = normalizeManagedRemoteTunnelPresets(candidate.managedRemoteTunnelPresets);
|
|
|
|
|
if (managedRemoteTunnelPresets) {
|
|
|
|
|
result.managedRemoteTunnelPresets = managedRemoteTunnelPresets;
|
|
|
|
|
}
|
|
|
|
|
const managedRemoteTunnelPresetTokens = normalizeManagedRemoteTunnelPresetTokens(candidate.managedRemoteTunnelPresetTokens);
|
|
|
|
|
if (managedRemoteTunnelPresetTokens) {
|
|
|
|
|
result.managedRemoteTunnelPresetTokens = managedRemoteTunnelPresetTokens;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.managedRemoteTunnelSelectedPresetId === 'string') {
|
|
|
|
|
const id = candidate.managedRemoteTunnelSelectedPresetId.trim();
|
|
|
|
|
result.managedRemoteTunnelSelectedPresetId = id || undefined;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const typography = sanitizeTypographySizesPartial(candidate.typographySizes);
|
|
|
|
|
if (typography) {
|
|
|
|
|
result.typographySizes = typography;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof candidate.defaultModel === 'string') {
|
|
|
|
|
const trimmed = candidate.defaultModel.trim();
|
|
|
|
|
result.defaultModel = trimmed.length > 0 ? trimmed : undefined;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.defaultVariant === 'string') {
|
|
|
|
|
const trimmed = candidate.defaultVariant.trim();
|
|
|
|
|
result.defaultVariant = trimmed.length > 0 ? trimmed : undefined;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.defaultAgent === 'string') {
|
|
|
|
|
const trimmed = candidate.defaultAgent.trim();
|
|
|
|
|
result.defaultAgent = trimmed.length > 0 ? trimmed : undefined;
|
|
|
|
|
}
|
2026-07-05 23:19:10 +03:00
|
|
|
if (typeof candidate.smallModelUseDefault === 'boolean') {
|
|
|
|
|
result.smallModelUseDefault = candidate.smallModelUseDefault;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.smallModelOverride === 'string') {
|
|
|
|
|
const trimmed = candidate.smallModelOverride.trim();
|
|
|
|
|
result.smallModelOverride = trimmed.length > 0 ? trimmed : undefined;
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
if (typeof candidate.defaultGitIdentityId === 'string') {
|
|
|
|
|
const trimmed = candidate.defaultGitIdentityId.trim();
|
|
|
|
|
result.defaultGitIdentityId = trimmed.length > 0 ? trimmed : undefined;
|
|
|
|
|
}
|
2026-06-29 09:28:20 +11:00
|
|
|
if (typeof candidate.followUpBehavior === 'string') {
|
|
|
|
|
result.followUpBehavior = normalizeFollowUpBehavior(candidate.followUpBehavior);
|
|
|
|
|
} else if (typeof candidate.queueModeEnabled === 'boolean') {
|
|
|
|
|
result.followUpBehavior = normalizeFollowUpBehavior(undefined, candidate.queueModeEnabled);
|
2026-03-31 18:47:00 +03:00
|
|
|
}
|
|
|
|
|
if (typeof candidate.autoCreateWorktree === 'boolean') {
|
|
|
|
|
result.autoCreateWorktree = candidate.autoCreateWorktree;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.gitmojiEnabled === 'boolean') {
|
|
|
|
|
result.gitmojiEnabled = candidate.gitmojiEnabled;
|
|
|
|
|
}
|
2026-04-23 07:14:14 -06:00
|
|
|
if (typeof candidate.defaultFileViewerPreview === 'boolean') {
|
|
|
|
|
result.defaultFileViewerPreview = candidate.defaultFileViewerPreview;
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
if (typeof candidate.zenModel === 'string') {
|
|
|
|
|
const trimmed = candidate.zenModel.trim();
|
|
|
|
|
result.zenModel = trimmed.length > 0 ? trimmed : undefined;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.gitProviderId === 'string') {
|
|
|
|
|
const trimmed = candidate.gitProviderId.trim();
|
|
|
|
|
result.gitProviderId = trimmed.length > 0 ? trimmed : undefined;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.gitModelId === 'string') {
|
|
|
|
|
const trimmed = candidate.gitModelId.trim();
|
|
|
|
|
result.gitModelId = trimmed.length > 0 ? trimmed : undefined;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.pwaAppName === 'string') {
|
|
|
|
|
result.pwaAppName = normalizePwaAppName(candidate.pwaAppName, undefined);
|
|
|
|
|
}
|
2026-04-22 03:17:34 +07:00
|
|
|
if (typeof candidate.pwaOrientation === 'string') {
|
|
|
|
|
result.pwaOrientation = normalizePwaOrientation(candidate.pwaOrientation, undefined);
|
|
|
|
|
}
|
2026-05-05 16:26:34 +07:00
|
|
|
if (typeof candidate.mobileKeyboardMode === 'string') {
|
2026-06-03 16:00:32 +03:00
|
|
|
const mode = normalizeMobileKeyboardMode(candidate.mobileKeyboardMode, null);
|
2026-05-05 16:26:34 +07:00
|
|
|
if (mode) {
|
|
|
|
|
result.mobileKeyboardMode = mode;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
if (typeof candidate.toolCallExpansion === 'string') {
|
|
|
|
|
const mode = candidate.toolCallExpansion.trim();
|
|
|
|
|
if (mode === 'collapsed' || mode === 'activity' || mode === 'detailed' || mode === 'changes') {
|
|
|
|
|
result.toolCallExpansion = mode;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.inputSpellcheckEnabled === 'boolean') {
|
|
|
|
|
result.inputSpellcheckEnabled = candidate.inputSpellcheckEnabled;
|
|
|
|
|
}
|
2026-06-03 16:00:32 +03:00
|
|
|
if (typeof candidate.showOpenCodeUpdateNotifications === 'boolean') {
|
|
|
|
|
result.showOpenCodeUpdateNotifications = candidate.showOpenCodeUpdateNotifications;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.openCodeUpdateToastDismissedVersion === 'string') {
|
|
|
|
|
const version = candidate.openCodeUpdateToastDismissedVersion.trim();
|
|
|
|
|
result.openCodeUpdateToastDismissedVersion = version.slice(0, VERSION_STRING_MAX_LENGTH);
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
if (typeof candidate.showToolFileIcons === 'boolean') {
|
|
|
|
|
result.showToolFileIcons = candidate.showToolFileIcons;
|
|
|
|
|
}
|
2026-06-03 22:11:37 +03:00
|
|
|
if (typeof candidate.showTurnChangedFiles === 'boolean') {
|
|
|
|
|
result.showTurnChangedFiles = candidate.showTurnChangedFiles;
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
if (typeof candidate.showExpandedBashTools === 'boolean') {
|
|
|
|
|
result.showExpandedBashTools = candidate.showExpandedBashTools;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.showExpandedEditTools === 'boolean') {
|
|
|
|
|
result.showExpandedEditTools = candidate.showExpandedEditTools;
|
|
|
|
|
}
|
2026-04-16 15:55:08 +03:00
|
|
|
if (typeof candidate.timeFormatPreference === 'string') {
|
|
|
|
|
const mode = candidate.timeFormatPreference.trim();
|
|
|
|
|
if (mode === 'auto' || mode === '12h' || mode === '24h') {
|
|
|
|
|
result.timeFormatPreference = mode;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.weekStartPreference === 'string') {
|
|
|
|
|
const mode = candidate.weekStartPreference.trim();
|
|
|
|
|
if (mode === 'auto' || mode === 'sunday' || mode === 'monday') {
|
|
|
|
|
result.weekStartPreference = mode;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
if (typeof candidate.chatRenderMode === 'string') {
|
|
|
|
|
const mode = candidate.chatRenderMode.trim();
|
|
|
|
|
if (mode === 'sorted' || mode === 'live') {
|
|
|
|
|
result.chatRenderMode = mode;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-17 16:07:26 +08:00
|
|
|
if (typeof candidate.messageStreamTransport === 'string') {
|
|
|
|
|
const mode = candidate.messageStreamTransport.trim();
|
|
|
|
|
if (mode === 'auto' || mode === 'ws' || mode === 'sse') {
|
|
|
|
|
result.messageStreamTransport = mode;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
if (typeof candidate.activityRenderMode === 'string') {
|
|
|
|
|
const mode = candidate.activityRenderMode.trim();
|
|
|
|
|
if (mode === 'collapsed' || mode === 'summary') {
|
|
|
|
|
result.activityRenderMode = mode;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.mermaidRenderingMode === 'string') {
|
|
|
|
|
const mode = candidate.mermaidRenderingMode.trim();
|
|
|
|
|
if (mode === 'svg' || mode === 'ascii') {
|
|
|
|
|
result.mermaidRenderingMode = mode;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.userMessageRenderingMode === 'string') {
|
|
|
|
|
const mode = candidate.userMessageRenderingMode.trim();
|
|
|
|
|
if (mode === 'markdown' || mode === 'plain') {
|
|
|
|
|
result.userMessageRenderingMode = mode;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.stickyUserHeader === 'boolean') {
|
|
|
|
|
result.stickyUserHeader = candidate.stickyUserHeader;
|
|
|
|
|
}
|
2026-06-12 18:01:07 -03:00
|
|
|
if (typeof candidate.expandedEditorToolbar === 'boolean') {
|
|
|
|
|
result.expandedEditorToolbar = candidate.expandedEditorToolbar;
|
|
|
|
|
}
|
2026-04-27 15:41:14 +07:00
|
|
|
if (typeof candidate.showSplitAssistantMessageActions === 'boolean') {
|
|
|
|
|
result.showSplitAssistantMessageActions = candidate.showSplitAssistantMessageActions;
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
if (typeof candidate.fontSize === 'number' && Number.isFinite(candidate.fontSize)) {
|
|
|
|
|
result.fontSize = Math.max(50, Math.min(200, Math.round(candidate.fontSize)));
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.terminalFontSize === 'number' && Number.isFinite(candidate.terminalFontSize)) {
|
|
|
|
|
result.terminalFontSize = Math.max(9, Math.min(52, Math.round(candidate.terminalFontSize)));
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.padding === 'number' && Number.isFinite(candidate.padding)) {
|
|
|
|
|
result.padding = Math.max(50, Math.min(200, Math.round(candidate.padding)));
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.cornerRadius === 'number' && Number.isFinite(candidate.cornerRadius)) {
|
|
|
|
|
result.cornerRadius = Math.max(0, Math.min(32, Math.round(candidate.cornerRadius)));
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.inputBarOffset === 'number' && Number.isFinite(candidate.inputBarOffset)) {
|
|
|
|
|
result.inputBarOffset = Math.max(0, Math.min(100, Math.round(candidate.inputBarOffset)));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 16:00:32 +03:00
|
|
|
const shortcutOverrides = sanitizeShortcutOverrides(candidate.shortcutOverrides);
|
|
|
|
|
if (shortcutOverrides) {
|
|
|
|
|
result.shortcutOverrides = shortcutOverrides;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
const favoriteModels = sanitizeModelRefs(candidate.favoriteModels, 64);
|
|
|
|
|
if (favoriteModels) {
|
|
|
|
|
result.favoriteModels = favoriteModels;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const recentModels = sanitizeModelRefs(candidate.recentModels, 16);
|
|
|
|
|
if (recentModels) {
|
|
|
|
|
result.recentModels = recentModels;
|
|
|
|
|
}
|
2026-06-23 21:46:23 +03:00
|
|
|
|
|
|
|
|
// Cap at 1024: users with several providers (anthropic, openai, google,
|
|
|
|
|
// bedrock, azure, etc.) each exposing dozens-to-hundreds of models can
|
|
|
|
|
// exceed 256 hidden entries quickly. 1024 covers dense multi-provider
|
|
|
|
|
// setups while still bounding persistence/memory.
|
|
|
|
|
const hiddenModels = sanitizeModelRefs(candidate.hiddenModels, HIDDEN_MODELS_MAX);
|
|
|
|
|
if (hiddenModels) {
|
|
|
|
|
result.hiddenModels = hiddenModels;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(candidate.collapsedModelProviders)) {
|
|
|
|
|
result.collapsedModelProviders = normalizeStringArray(candidate.collapsedModelProviders);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(candidate.recentAgents)) {
|
|
|
|
|
result.recentAgents = normalizeStringArray(candidate.recentAgents);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const recentEfforts = sanitizeRecentEfforts(candidate.recentEfforts);
|
|
|
|
|
if (recentEfforts) {
|
|
|
|
|
result.recentEfforts = recentEfforts;
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
if (typeof candidate.diffLayoutPreference === 'string') {
|
|
|
|
|
const mode = candidate.diffLayoutPreference.trim();
|
|
|
|
|
if (mode === 'dynamic' || mode === 'inline' || mode === 'side-by-side') {
|
|
|
|
|
result.diffLayoutPreference = mode;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-15 01:08:59 +08:00
|
|
|
if (typeof candidate.gitChangesViewMode === 'string') {
|
|
|
|
|
const mode = candidate.gitChangesViewMode.trim();
|
|
|
|
|
if (mode === 'flat' || mode === 'tree') {
|
|
|
|
|
result.gitChangesViewMode = mode;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 18:47:00 +03:00
|
|
|
if (typeof candidate.directoryShowHidden === 'boolean') {
|
|
|
|
|
result.directoryShowHidden = candidate.directoryShowHidden;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.filesViewShowGitignored === 'boolean') {
|
|
|
|
|
result.filesViewShowGitignored = candidate.filesViewShowGitignored;
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.openInAppId === 'string') {
|
|
|
|
|
const trimmed = candidate.openInAppId.trim();
|
|
|
|
|
if (trimmed.length > 0) {
|
|
|
|
|
result.openInAppId = trimmed;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Message limit — single setting for fetch / trim / Load More chunk
|
|
|
|
|
if (typeof candidate.messageLimit === 'number' && Number.isFinite(candidate.messageLimit)) {
|
|
|
|
|
result.messageLimit = Math.max(10, Math.min(500, Math.round(candidate.messageLimit)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const skillCatalogs = sanitizeSkillCatalogs(candidate.skillCatalogs);
|
|
|
|
|
if (skillCatalogs) {
|
|
|
|
|
result.skillCatalogs = skillCatalogs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Usage model selections - which models appear in dropdown
|
|
|
|
|
if (candidate.usageSelectedModels && typeof candidate.usageSelectedModels === 'object') {
|
|
|
|
|
const sanitized = {};
|
|
|
|
|
for (const [providerId, models] of Object.entries(candidate.usageSelectedModels)) {
|
|
|
|
|
if (typeof providerId === 'string' && Array.isArray(models)) {
|
|
|
|
|
const validModels = models.filter((m) => typeof m === 'string' && m.length > 0);
|
|
|
|
|
if (validModels.length > 0) {
|
|
|
|
|
sanitized[providerId] = validModels;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (Object.keys(sanitized).length > 0) {
|
|
|
|
|
result.usageSelectedModels = sanitized;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Usage page collapsed families - for "Other Models" section
|
|
|
|
|
if (candidate.usageCollapsedFamilies && typeof candidate.usageCollapsedFamilies === 'object') {
|
|
|
|
|
const sanitized = {};
|
|
|
|
|
for (const [providerId, families] of Object.entries(candidate.usageCollapsedFamilies)) {
|
|
|
|
|
if (typeof providerId === 'string' && Array.isArray(families)) {
|
|
|
|
|
const validFamilies = families.filter((f) => typeof f === 'string' && f.length > 0);
|
|
|
|
|
if (validFamilies.length > 0) {
|
|
|
|
|
sanitized[providerId] = validFamilies;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (Object.keys(sanitized).length > 0) {
|
|
|
|
|
result.usageCollapsedFamilies = sanitized;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Header dropdown expanded families (inverted - stores EXPANDED, default all collapsed)
|
|
|
|
|
if (candidate.usageExpandedFamilies && typeof candidate.usageExpandedFamilies === 'object') {
|
|
|
|
|
const sanitized = {};
|
|
|
|
|
for (const [providerId, families] of Object.entries(candidate.usageExpandedFamilies)) {
|
|
|
|
|
if (typeof providerId === 'string' && Array.isArray(families)) {
|
|
|
|
|
const validFamilies = families.filter((f) => typeof f === 'string' && f.length > 0);
|
|
|
|
|
if (validFamilies.length > 0) {
|
|
|
|
|
sanitized[providerId] = validFamilies;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (Object.keys(sanitized).length > 0) {
|
|
|
|
|
result.usageExpandedFamilies = sanitized;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Custom model groups configuration
|
|
|
|
|
if (candidate.usageModelGroups && typeof candidate.usageModelGroups === 'object') {
|
|
|
|
|
const sanitized = {};
|
|
|
|
|
for (const [providerId, config] of Object.entries(candidate.usageModelGroups)) {
|
|
|
|
|
if (typeof providerId !== 'string') continue;
|
|
|
|
|
|
|
|
|
|
const providerConfig = {};
|
|
|
|
|
|
|
|
|
|
// customGroups: array of {id, label, models, order}
|
|
|
|
|
if (Array.isArray(config.customGroups)) {
|
|
|
|
|
const validGroups = config.customGroups
|
|
|
|
|
.filter((g) => g && typeof g.id === 'string' && typeof g.label === 'string')
|
|
|
|
|
.map((g) => ({
|
|
|
|
|
id: g.id.slice(0, 64),
|
|
|
|
|
label: g.label.slice(0, 128),
|
|
|
|
|
models: Array.isArray(g.models)
|
|
|
|
|
? g.models.filter((m) => typeof m === 'string').slice(0, 500)
|
|
|
|
|
: [],
|
|
|
|
|
order: typeof g.order === 'number' ? g.order : 0,
|
|
|
|
|
}));
|
|
|
|
|
if (validGroups.length > 0) {
|
|
|
|
|
providerConfig.customGroups = validGroups;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// modelAssignments: Record<modelName, groupId>
|
|
|
|
|
if (config.modelAssignments && typeof config.modelAssignments === 'object') {
|
|
|
|
|
const assignments = {};
|
|
|
|
|
for (const [model, groupId] of Object.entries(config.modelAssignments)) {
|
|
|
|
|
if (typeof model === 'string' && typeof groupId === 'string') {
|
|
|
|
|
assignments[model] = groupId;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (Object.keys(assignments).length > 0) {
|
|
|
|
|
providerConfig.modelAssignments = assignments;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// renamedGroups: Record<groupId, label>
|
|
|
|
|
if (config.renamedGroups && typeof config.renamedGroups === 'object') {
|
|
|
|
|
const renamed = {};
|
|
|
|
|
for (const [groupId, label] of Object.entries(config.renamedGroups)) {
|
|
|
|
|
if (typeof groupId === 'string' && typeof label === 'string') {
|
|
|
|
|
renamed[groupId] = label.slice(0, 128);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (Object.keys(renamed).length > 0) {
|
|
|
|
|
providerConfig.renamedGroups = renamed;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Object.keys(providerConfig).length > 0) {
|
|
|
|
|
sanitized[providerId] = providerConfig;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (Object.keys(sanitized).length > 0) {
|
|
|
|
|
result.usageModelGroups = sanitized;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Usage reporting opt-out (default: true/enabled)
|
|
|
|
|
if (typeof candidate.reportUsage === 'boolean') {
|
|
|
|
|
result.reportUsage = candidate.reportUsage;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 14:39:55 -07:00
|
|
|
// Global behavior prompt — synced to ~/.config/opencode/AGENTS.md
|
|
|
|
|
if (typeof candidate.globalBehaviorPrompt === 'string') {
|
|
|
|
|
const value = candidate.globalBehaviorPrompt;
|
|
|
|
|
if (value.length <= 1024 * 1024) {
|
|
|
|
|
result.globalBehaviorPrompt = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 01:27:31 +03:00
|
|
|
if (typeof candidate.responseStyleEnabled === 'boolean') {
|
|
|
|
|
result.responseStyleEnabled = candidate.responseStyleEnabled;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
typeof candidate.responseStylePreset === 'string' &&
|
|
|
|
|
['concise', 'detailed', 'mentor', 'pushback', 'noFiller', 'matchEnergy', 'warmPeer', 'custom'].includes(candidate.responseStylePreset)
|
|
|
|
|
) {
|
|
|
|
|
result.responseStylePreset = candidate.responseStylePreset;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof candidate.responseStyleCustomInstructions === 'string') {
|
|
|
|
|
const value = candidate.responseStyleCustomInstructions;
|
|
|
|
|
if (value.length <= 50_000) {
|
|
|
|
|
result.responseStyleCustomInstructions = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-04 02:48:07 +03:00
|
|
|
if (typeof candidate.dictationEnabled === 'boolean') {
|
|
|
|
|
result.dictationEnabled = candidate.dictationEnabled;
|
|
|
|
|
}
|
2026-05-13 19:45:03 +07:00
|
|
|
if (typeof candidate.sttProvider === 'string') {
|
|
|
|
|
const provider = candidate.sttProvider.trim();
|
2026-07-04 02:48:07 +03:00
|
|
|
if (provider === 'local' || provider === 'openai-compatible') {
|
2026-05-13 19:45:03 +07:00
|
|
|
result.sttProvider = provider;
|
2026-07-04 02:48:07 +03:00
|
|
|
} else if (provider === 'server') {
|
|
|
|
|
// Legacy provider migration: 'server' was the OpenAI-compatible endpoint.
|
|
|
|
|
result.sttProvider = 'openai-compatible';
|
|
|
|
|
} else if (provider === 'browser' || provider === 'wasm') {
|
|
|
|
|
result.sttProvider = 'local';
|
2026-05-13 19:45:03 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.sttServerUrl === 'string') {
|
|
|
|
|
const trimmed = candidate.sttServerUrl.trim();
|
|
|
|
|
if (trimmed.length <= STT_SERVER_URL_MAX_LENGTH) {
|
|
|
|
|
result.sttServerUrl = trimmed;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (typeof candidate.sttModel === 'string') {
|
|
|
|
|
const trimmed = candidate.sttModel.trim();
|
|
|
|
|
if (trimmed.length <= STT_MODEL_MAX_LENGTH) {
|
|
|
|
|
result.sttModel = trimmed;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-04 02:48:07 +03:00
|
|
|
if (typeof candidate.sttLocalModel === 'string') {
|
|
|
|
|
const trimmed = candidate.sttLocalModel.trim();
|
|
|
|
|
if (trimmed.length <= STT_MODEL_MAX_LENGTH) {
|
|
|
|
|
result.sttLocalModel = trimmed;
|
2026-05-14 01:19:52 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-13 19:45:03 +07:00
|
|
|
if (typeof candidate.sttLanguage === 'string') {
|
|
|
|
|
const trimmed = candidate.sttLanguage.trim();
|
|
|
|
|
if (trimmed.length <= STT_LANGUAGE_MAX_LENGTH) {
|
|
|
|
|
result.sttLanguage = trimmed;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 18:47:00 +03:00
|
|
|
return result;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mergePersistedSettings = (current, changes) => {
|
|
|
|
|
const baseBookmarks = Array.isArray(changes.securityScopedBookmarks)
|
|
|
|
|
? changes.securityScopedBookmarks
|
|
|
|
|
: Array.isArray(current.securityScopedBookmarks)
|
|
|
|
|
? current.securityScopedBookmarks
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
const nextTypographySizes = changes.typographySizes
|
|
|
|
|
? {
|
|
|
|
|
...(current.typographySizes || {}),
|
|
|
|
|
...changes.typographySizes
|
|
|
|
|
}
|
|
|
|
|
: current.typographySizes;
|
|
|
|
|
|
|
|
|
|
const next = {
|
|
|
|
|
...current,
|
|
|
|
|
...changes,
|
|
|
|
|
securityScopedBookmarks: Array.from(
|
|
|
|
|
new Set(
|
|
|
|
|
baseBookmarks.filter((entry) => typeof entry === 'string' && entry.length > 0)
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
typographySizes: nextTypographySizes
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return next;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatSettingsResponse = (settings) => {
|
|
|
|
|
const sanitized = sanitizeSettingsUpdate(settings);
|
|
|
|
|
delete sanitized.managedRemoteTunnelToken;
|
|
|
|
|
const bookmarks = normalizeStringArray(settings.securityScopedBookmarks);
|
|
|
|
|
const hasManagedRemoteTunnelToken = typeof settings?.managedRemoteTunnelToken === 'string' && settings.managedRemoteTunnelToken.trim().length > 0;
|
|
|
|
|
const pwaAppName = normalizePwaAppName(settings?.pwaAppName, '');
|
2026-04-22 03:17:34 +07:00
|
|
|
const pwaOrientation = normalizePwaOrientation(settings?.pwaOrientation, 'system');
|
2026-05-05 16:26:34 +07:00
|
|
|
const mobileKeyboardMode = normalizeMobileKeyboardMode(settings?.mobileKeyboardMode, 'native');
|
2026-03-31 18:47:00 +03:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...sanitized,
|
|
|
|
|
hasManagedRemoteTunnelToken,
|
|
|
|
|
...(pwaAppName ? { pwaAppName } : {}),
|
2026-04-22 03:17:34 +07:00
|
|
|
pwaOrientation,
|
2026-05-05 16:26:34 +07:00
|
|
|
mobileKeyboardMode,
|
2026-03-31 18:47:00 +03:00
|
|
|
securityScopedBookmarks: bookmarks,
|
|
|
|
|
pinnedDirectories: normalizeStringArray(settings.pinnedDirectories),
|
|
|
|
|
typographySizes: sanitizeTypographySizesPartial(settings.typographySizes),
|
2026-06-12 18:24:07 +03:00
|
|
|
...(process.env.OPENCHAMBER_RUNTIME === 'desktop'
|
|
|
|
|
? {
|
|
|
|
|
desktopLanAccessActive: process.env.OPENCHAMBER_DESKTOP_LAN_ACCESS_ACTIVE === 'true',
|
|
|
|
|
desktopLanAccessBlockedReason:
|
|
|
|
|
process.env.OPENCHAMBER_DESKTOP_LAN_ACCESS_BLOCKED_REASON === 'missing-password'
|
|
|
|
|
? 'missing-password'
|
|
|
|
|
: null,
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
2026-03-31 18:47:00 +03:00
|
|
|
showReasoningTraces:
|
|
|
|
|
typeof settings.showReasoningTraces === 'boolean'
|
|
|
|
|
? settings.showReasoningTraces
|
|
|
|
|
: typeof sanitized.showReasoningTraces === 'boolean'
|
|
|
|
|
? sanitized.showReasoningTraces
|
2026-05-16 17:20:16 +03:00
|
|
|
: false,
|
|
|
|
|
collapsibleThinkingBlocks:
|
|
|
|
|
typeof settings.collapsibleThinkingBlocks === 'boolean'
|
|
|
|
|
? settings.collapsibleThinkingBlocks
|
|
|
|
|
: typeof sanitized.collapsibleThinkingBlocks === 'boolean'
|
|
|
|
|
? sanitized.collapsibleThinkingBlocks
|
|
|
|
|
: true,
|
2026-03-31 18:47:00 +03:00
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
normalizePwaAppName,
|
2026-04-22 03:17:34 +07:00
|
|
|
normalizePwaOrientation,
|
2026-05-05 16:26:34 +07:00
|
|
|
normalizeMobileKeyboardMode,
|
2026-03-31 18:47:00 +03:00
|
|
|
sanitizeSettingsUpdate,
|
|
|
|
|
mergePersistedSettings,
|
|
|
|
|
formatSettingsResponse,
|
|
|
|
|
};
|
|
|
|
|
};
|