The panel stored notes, todos and plans inside one shared JSON file that six unrelated domains also wrote to, synchronised itself through window CustomEvents, and could only read plans. It is now Project knowledge: server-owned storage with explicit routes, a store with rollback, a section sidebar, plans that open and edit in place, and search across all of it. Notes and plans the user pins travel with every message sent in that project. Pinning is project state, not an attachment to one message, so it holds until unpinned and the work status panel names what is riding along and can detach it. Agent memory is added alongside, in two scopes: what is true about the user, and what is true about this codebase. The split is not cosmetic — a wrong project fact costs one project and is noticed, while a wrong global fact quietly shapes every session everywhere and the user has no code to check it against. It stays separate from notes so an agent mistake cannot land in what the user wrote. Sessions receive an index of titles only; bodies are read on demand, because an index carrying full text grows until it crowds out the conversation. Deciding what a session must be told, and whether it has been told, now lives on the server. The client owned it before, which meant sessions started without a UI — scheduled tasks, sessions the agent dispatches — received nothing at all, and a tab's record of what it had sent outlived the conversation: after compaction the agent no longer held the block while the tab went on believing it did. What was delivered is recorded in the session's own metadata, and compaction restores it through the runtime that already restores pinned messages, in the same turn. Agent memory ships dark behind OPENCHAMBER_MEMORY_ENABLE: unset, there is no tool, no routes, no session index, no settings row and no panel tab. Absent rather than switched off, so nothing invites turning on a feature that has not been announced. Pinned notes and plans are unaffected and ship as normal.
958 lines
40 KiB
JavaScript
958 lines
40 KiB
JavaScript
import { isAgentMemoryFeatureAvailable } from '../agent-memory/feature-flag.js';
|
|
|
|
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;
|
|
const STT_SERVER_URL_MAX_LENGTH = 2048;
|
|
const STT_MODEL_MAX_LENGTH = 256;
|
|
const STT_LANGUAGE_MAX_LENGTH = 64;
|
|
const VERSION_STRING_MAX_LENGTH = 128;
|
|
const SHORTCUT_OVERRIDE_KEY_MAX_LENGTH = 128;
|
|
const SHORTCUT_OVERRIDE_VALUE_MAX_LENGTH = 128;
|
|
const PWA_ORIENTATION_VALUES = new Set(['system', 'portrait', 'landscape']);
|
|
const MOBILE_KEYBOARD_MODE_VALUES = new Set(['native', 'resize-content']);
|
|
const TERMINAL_SHELL_VALUES = new Set(['auto', 'bash', 'zsh', 'sh', 'fish', 'pwsh', 'powershell', 'cmd', 'dash', 'ksh', 'nu']);
|
|
const HIDDEN_MODELS_MAX = 1024;
|
|
const RECENT_EFFORTS_MAX_KEYS = 128;
|
|
const RECENT_EFFORTS_MAX_VARIANTS_PER_KEY = 5;
|
|
|
|
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;
|
|
};
|
|
|
|
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;
|
|
};
|
|
|
|
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);
|
|
};
|
|
|
|
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;
|
|
};
|
|
|
|
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;
|
|
};
|
|
|
|
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';
|
|
};
|
|
|
|
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;
|
|
}
|
|
if (typeof candidate.workStatusPanelEnabled === 'boolean') {
|
|
result.workStatusPanelEnabled = candidate.workStatusPanelEnabled;
|
|
}
|
|
if (Array.isArray(candidate.workStatusHiddenSections)) {
|
|
// Ids are validated on the client, which owns the section list; here we
|
|
// only guarantee the shape, so a malformed payload cannot land on disk.
|
|
result.workStatusHiddenSections = [
|
|
...new Set(candidate.workStatusHiddenSections.filter((entry) => typeof entry === 'string' && entry.length > 0)),
|
|
];
|
|
}
|
|
if (typeof candidate.desktopLanAccessEnabled === 'boolean') {
|
|
result.desktopLanAccessEnabled = candidate.desktopLanAccessEnabled;
|
|
}
|
|
if (typeof candidate.desktopKeepAwakeEnabled === 'boolean') {
|
|
result.desktopKeepAwakeEnabled = candidate.desktopKeepAwakeEnabled;
|
|
}
|
|
if (typeof candidate.desktopMinimizeToTrayEnabled === 'boolean') {
|
|
result.desktopMinimizeToTrayEnabled = candidate.desktopMinimizeToTrayEnabled;
|
|
}
|
|
if (typeof candidate.desktopMacMenuBarEnabled === 'boolean') {
|
|
result.desktopMacMenuBarEnabled = candidate.desktopMacMenuBarEnabled;
|
|
}
|
|
if (typeof candidate.desktopWindowControlsPosition === 'string') {
|
|
const mode = candidate.desktopWindowControlsPosition.trim();
|
|
// Legacy "auto" never read OS chrome config; persist as the right default.
|
|
if (mode === 'auto' || mode === 'right') {
|
|
result.desktopWindowControlsPosition = 'right';
|
|
} else if (mode === 'left') {
|
|
result.desktopWindowControlsPosition = 'left';
|
|
}
|
|
}
|
|
if (typeof candidate.desktopWindowControlsStyle === 'string') {
|
|
const style = candidate.desktopWindowControlsStyle.trim();
|
|
if (style === 'classic' || style === 'traffic-lights') {
|
|
result.desktopWindowControlsStyle = style;
|
|
}
|
|
}
|
|
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,
|
|
revision: Number.isSafeInteger(candidate.permissionAutoAccept.revision)
|
|
&& candidate.permissionAutoAccept.revision >= 0
|
|
? candidate.permissionAutoAccept.revision
|
|
: 0,
|
|
};
|
|
}
|
|
if (typeof candidate.desktopUiPassword === 'string') {
|
|
result.desktopUiPassword = candidate.desktopUiPassword.trim();
|
|
}
|
|
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)
|
|
);
|
|
}
|
|
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;
|
|
}
|
|
if (typeof candidate.draftStartersVisible === 'boolean') {
|
|
result.draftStartersVisible = candidate.draftStartersVisible;
|
|
}
|
|
if (typeof candidate.draftStartersCraftGoalAdded === 'boolean') {
|
|
result.draftStartersCraftGoalAdded = candidate.draftStartersCraftGoalAdded;
|
|
}
|
|
if (typeof candidate.draftStartersScheduleTaskAdded === 'boolean') {
|
|
result.draftStartersScheduleTaskAdded = candidate.draftStartersScheduleTaskAdded;
|
|
}
|
|
|
|
|
|
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;
|
|
}
|
|
if (typeof candidate.sessionRecapEnabled === 'boolean') {
|
|
result.sessionRecapEnabled = candidate.sessionRecapEnabled;
|
|
}
|
|
if (typeof candidate.sessionSuggestionEnabled === 'boolean') {
|
|
result.sessionSuggestionEnabled = candidate.sessionSuggestionEnabled;
|
|
}
|
|
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);
|
|
}
|
|
if (typeof candidate.collapsibleThinkingBlocks === 'boolean') {
|
|
result.collapsibleThinkingBlocks = candidate.collapsibleThinkingBlocks;
|
|
}
|
|
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 (candidate.usageDisplayMode === 'usage' || candidate.usageDisplayMode === 'remaining') {
|
|
result.usageDisplayMode = candidate.usageDisplayMode;
|
|
}
|
|
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.sessionRetentionAction === 'archive' || candidate.sessionRetentionAction === 'delete') {
|
|
result.sessionRetentionAction = candidate.sessionRetentionAction;
|
|
}
|
|
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;
|
|
}
|
|
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;
|
|
}
|
|
if (typeof candidate.walkthroughModelOverride === 'string') {
|
|
const trimmed = candidate.walkthroughModelOverride.trim();
|
|
result.walkthroughModelOverride = trimmed.length > 0 ? trimmed : undefined;
|
|
}
|
|
if (typeof candidate.defaultGitIdentityId === 'string') {
|
|
const trimmed = candidate.defaultGitIdentityId.trim();
|
|
result.defaultGitIdentityId = trimmed.length > 0 ? trimmed : undefined;
|
|
}
|
|
if (typeof candidate.followUpBehavior === 'string') {
|
|
result.followUpBehavior = normalizeFollowUpBehavior(candidate.followUpBehavior);
|
|
} else if (typeof candidate.queueModeEnabled === 'boolean') {
|
|
result.followUpBehavior = normalizeFollowUpBehavior(undefined, candidate.queueModeEnabled);
|
|
}
|
|
if (typeof candidate.autoCreateWorktree === 'boolean') {
|
|
result.autoCreateWorktree = candidate.autoCreateWorktree;
|
|
}
|
|
if (typeof candidate.gitmojiEnabled === 'boolean') {
|
|
result.gitmojiEnabled = candidate.gitmojiEnabled;
|
|
}
|
|
if (typeof candidate.defaultFileViewerPreview === 'boolean') {
|
|
result.defaultFileViewerPreview = candidate.defaultFileViewerPreview;
|
|
}
|
|
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);
|
|
}
|
|
if (typeof candidate.pwaOrientation === 'string') {
|
|
result.pwaOrientation = normalizePwaOrientation(candidate.pwaOrientation, undefined);
|
|
}
|
|
if (typeof candidate.mobileKeyboardMode === 'string') {
|
|
const mode = normalizeMobileKeyboardMode(candidate.mobileKeyboardMode, null);
|
|
if (mode) {
|
|
result.mobileKeyboardMode = mode;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
if (typeof candidate.showOpenCodeUpdateNotifications === 'boolean') {
|
|
result.showOpenCodeUpdateNotifications = candidate.showOpenCodeUpdateNotifications;
|
|
}
|
|
if (typeof candidate.agentWebToolEnabled === 'boolean') {
|
|
result.agentWebToolEnabled = candidate.agentWebToolEnabled;
|
|
}
|
|
if (typeof candidate.agentControlToolEnabled === 'boolean') {
|
|
result.agentControlToolEnabled = candidate.agentControlToolEnabled;
|
|
}
|
|
if (typeof candidate.agentMemoryToolEnabled === 'boolean') {
|
|
result.agentMemoryToolEnabled = candidate.agentMemoryToolEnabled;
|
|
}
|
|
if (typeof candidate.optimizeSystemPrompt === 'boolean') {
|
|
result.optimizeSystemPrompt = candidate.optimizeSystemPrompt;
|
|
}
|
|
if (typeof candidate.openCodeUpdateToastDismissedVersion === 'string') {
|
|
const version = candidate.openCodeUpdateToastDismissedVersion.trim();
|
|
result.openCodeUpdateToastDismissedVersion = version.slice(0, VERSION_STRING_MAX_LENGTH);
|
|
}
|
|
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') {
|
|
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;
|
|
}
|
|
}
|
|
if (typeof candidate.chatRenderMode === 'string') {
|
|
const mode = candidate.chatRenderMode.trim();
|
|
if (mode === 'sorted' || mode === 'live') {
|
|
result.chatRenderMode = mode;
|
|
}
|
|
}
|
|
if (typeof candidate.messageStreamTransport === 'string') {
|
|
const mode = candidate.messageStreamTransport.trim();
|
|
if (mode === 'auto' || mode === 'ws' || mode === 'sse') {
|
|
result.messageStreamTransport = mode;
|
|
}
|
|
}
|
|
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.collapsibleUserMessages === 'boolean') {
|
|
result.collapsibleUserMessages = candidate.collapsibleUserMessages;
|
|
}
|
|
if (typeof candidate.stickyUserHeader === 'boolean') {
|
|
result.stickyUserHeader = candidate.stickyUserHeader;
|
|
}
|
|
if (typeof candidate.promptNavigatorEnabled === 'boolean') {
|
|
result.promptNavigatorEnabled = candidate.promptNavigatorEnabled;
|
|
}
|
|
if (typeof candidate.expandedEditorToolbar === 'boolean') {
|
|
result.expandedEditorToolbar = candidate.expandedEditorToolbar;
|
|
}
|
|
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 = 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.terminalShell === 'string') {
|
|
const shell = candidate.terminalShell.trim().toLowerCase();
|
|
if (TERMINAL_SHELL_VALUES.has(shell)) result.terminalShell = shell;
|
|
}
|
|
if (Array.isArray(candidate.terminalLoginShells)) {
|
|
result.terminalLoginShells = [...new Set(candidate.terminalLoginShells
|
|
.filter((shell) => typeof shell === 'string')
|
|
.map((shell) => shell.trim().toLowerCase())
|
|
.filter((shell) => TERMINAL_SHELL_VALUES.has(shell)))];
|
|
}
|
|
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)));
|
|
}
|
|
|
|
const shortcutOverrides = sanitizeShortcutOverrides(candidate.shortcutOverrides);
|
|
if (shortcutOverrides) {
|
|
result.shortcutOverrides = shortcutOverrides;
|
|
}
|
|
|
|
const favoriteModels = sanitizeModelRefs(candidate.favoriteModels, 64);
|
|
if (favoriteModels) {
|
|
result.favoriteModels = favoriteModels;
|
|
}
|
|
|
|
const recentModels = sanitizeModelRefs(candidate.recentModels, 16);
|
|
if (recentModels) {
|
|
result.recentModels = recentModels;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
if (typeof candidate.diffLayoutPreference === 'string') {
|
|
const mode = candidate.diffLayoutPreference.trim();
|
|
if (mode === 'dynamic' || mode === 'inline' || mode === 'side-by-side') {
|
|
result.diffLayoutPreference = mode;
|
|
}
|
|
}
|
|
if (typeof candidate.gitChangesViewMode === 'string') {
|
|
const mode = candidate.gitChangesViewMode.trim();
|
|
if (mode === 'flat' || mode === 'tree') {
|
|
result.gitChangesViewMode = mode;
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
if (typeof candidate.dictationEnabled === 'boolean') {
|
|
result.dictationEnabled = candidate.dictationEnabled;
|
|
}
|
|
if (typeof candidate.sttProvider === 'string') {
|
|
const provider = candidate.sttProvider.trim();
|
|
if (provider === 'local' || provider === 'openai-compatible') {
|
|
result.sttProvider = provider;
|
|
} 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';
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
if (typeof candidate.sttLocalModel === 'string') {
|
|
const trimmed = candidate.sttLocalModel.trim();
|
|
if (trimmed.length <= STT_MODEL_MAX_LENGTH) {
|
|
result.sttLocalModel = trimmed;
|
|
}
|
|
}
|
|
if (typeof candidate.sttLanguage === 'string') {
|
|
const trimmed = candidate.sttLanguage.trim();
|
|
if (trimmed.length <= STT_LANGUAGE_MAX_LENGTH) {
|
|
result.sttLanguage = trimmed;
|
|
}
|
|
}
|
|
|
|
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, '');
|
|
const pwaOrientation = normalizePwaOrientation(settings?.pwaOrientation, 'system');
|
|
const mobileKeyboardMode = normalizeMobileKeyboardMode(settings?.mobileKeyboardMode, 'native');
|
|
|
|
return {
|
|
...sanitized,
|
|
hasManagedRemoteTunnelToken,
|
|
// Tells the client whether agent memory exists in this build at all, so
|
|
// its settings row and panel tab can be absent rather than merely off.
|
|
agentMemoryFeatureAvailable: isAgentMemoryFeatureAvailable(),
|
|
...(pwaAppName ? { pwaAppName } : {}),
|
|
pwaOrientation,
|
|
mobileKeyboardMode,
|
|
securityScopedBookmarks: bookmarks,
|
|
pinnedDirectories: normalizeStringArray(settings.pinnedDirectories),
|
|
typographySizes: sanitizeTypographySizesPartial(settings.typographySizes),
|
|
...(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,
|
|
}
|
|
: {}),
|
|
showReasoningTraces:
|
|
typeof settings.showReasoningTraces === 'boolean'
|
|
? settings.showReasoningTraces
|
|
: typeof sanitized.showReasoningTraces === 'boolean'
|
|
? sanitized.showReasoningTraces
|
|
: false,
|
|
collapsibleThinkingBlocks:
|
|
typeof settings.collapsibleThinkingBlocks === 'boolean'
|
|
? settings.collapsibleThinkingBlocks
|
|
: typeof sanitized.collapsibleThinkingBlocks === 'boolean'
|
|
? sanitized.collapsibleThinkingBlocks
|
|
: true,
|
|
};
|
|
};
|
|
|
|
return {
|
|
normalizePwaAppName,
|
|
normalizePwaOrientation,
|
|
normalizeMobileKeyboardMode,
|
|
sanitizeSettingsUpdate,
|
|
mergePersistedSettings,
|
|
formatSettingsResponse,
|
|
};
|
|
};
|