| import { isThinkingLevel, type ThinkingLevel } from './model-thinking.js'; |
| import type { OnboardingMilestone } from './onboarding.js'; |
| import { sanitizeOnboardingMilestones } from './onboarding.js'; |
| import type { WebSearchSettingsPatch, WebSearchSettings } from './web-search.js'; |
| import type { BotChatSettings, BotChatSettingsPatch } from './bot-chat-settings.js'; |
| import { |
| createDefaultBotChatSettings, |
| mergeBotChatSettings, |
| normalizeBotChatSettings, |
| } from './bot-chat-settings.js'; |
| import type { LocalMemorySettings } from './local-memory.js'; |
| import { |
| defaultWebSearchSettings, |
| mergeWebSearchSettings, |
| normalizeWebSearchSettings, |
| } from './web-search.js'; |
| import { defaultLocalMemorySettings, normalizeLocalMemorySettings } from './local-memory.js'; |
| import type { PermissionMode } from './permission.js'; |
| import { |
| UI_LOCALE_PREFERENCES, |
| isUiLocalePreference, |
| type UiLocalePreference, |
| } from './ui-locale.js'; |
| import { normalizeSubagentSettings, type SubagentSettings } from './subagent-settings.js'; |
| import { isPetPackId } from './pet.js'; |
| |
| export { UI_LOCALE_PREFERENCES, isUiLocalePreference } from './ui-locale.js'; |
| export type { UiLocalePreference } from './ui-locale.js'; |
| export type { |
| BotChannelSettings, |
| BotChatSettings, |
| BotDeliveryProvider, |
| BotProvider, |
| BotReadinessState, |
| } from './bot-chat-settings.js'; |
| export { |
| BOT_DELIVERY_PROVIDERS, |
| BOT_PROVIDERS, |
| BOT_READINESS_STATES, |
| MAX_ALLOWED_USER_IDS, |
| createDefaultBotChannel, |
| hasBotChannelCredentials, |
| isBotDeliveryProvider, |
| isBotReadinessState, |
| normalizeAllowedUserIds, |
| parseAllowedUserIdsFromText, |
| } from './bot-chat-settings.js'; |
| |
| /** |
| * PR-SETTINGS-IA-CONSOLIDATE-0 + PR-SETTINGS-REVIEW-0 (WAWQAQ msg |
| * `886f6406`): the memory+review merge had too much density and got |
| * split back out. Other merges (network→general, personalization+ |
| * theme→appearance) held. |
| * |
| * Final mapping: |
| * - `network` → `general` |
| * - `personalization` + `theme` → `appearance` |
| * - `daily-review` is its own section again |
| * - `memory` is its own section again |
| * |
| * See docs/archive/reference-settings.md §7 for historical provenance. |
| */ |
| export const SETTINGS_SECTIONS = [ |
| 'general', |
| 'appearance', |
| 'projects', |
| 'memory', |
| 'daily-review', |
| 'models', |
| 'subagents', |
| 'usage', |
| 'bot-chat', |
| 'search', |
| 'data', |
| 'permissions', |
| 'health', |
| 'about', |
| ] as const; |
| |
| export type SettingsSection = (typeof SETTINGS_SECTIONS)[number]; |
| |
| export type ProxyProtocol = 'http' | 'https' | 'socks5'; |
| |
| export interface NetworkProxySettings { |
| enabled: boolean; |
| protocol: ProxyProtocol; |
| host: string; |
| port: number; |
| authEnabled: boolean; |
| username: string; |
| password: string; |
| bypassList: string[]; |
| autoBypassDomains: string[]; |
| } |
| |
| /** |
| * Persisted application network settings. Runtime proxy execution uses the |
| * separate contract in `settings/network-settings.ts`. |
| */ |
| export interface AppNetworkSettings { |
| proxy: NetworkProxySettings; |
| } |
| |
| /** @deprecated Use AppNetworkSettings for the persisted application settings shape. */ |
| export type NetworkSettings = AppNetworkSettings; |
| |
| export type UsageRange = '24h' | '7d' | '30d' | 'all'; |
| export type UsageStatus = 'all' | 'success' | 'error'; |
| export type UsageTab = 'requests' | 'providers' | 'models' | 'tools' | 'pricing'; |
| |
| export interface UsageSettings { |
| range: UsageRange; |
| status: UsageStatus; |
| modelFilter: string; |
| showDetails: boolean; |
| activeTab: UsageTab; |
| } |
| |
| export type ThemePreference = 'light' | 'dark' | 'auto'; |
| |
| /** |
| * PR-UI-2 (@yuejing 2026-05-22): base46 palette catalog. Each value |
| * maps to a CSS `[data-maka-theme="..."]` selector in maka-tokens.css |
| * that overrides the 6 base color tokens (background / foreground / |
| * accent / info / success / destructive). `default` keeps the |
| * current Maka palette unchanged. |
| * |
| * Adding a new palette = add `<id>` here + add the matching |
| * `[data-maka-theme="<id>"]` block (light + dark) in maka-tokens.css. |
| */ |
| export const THEME_PALETTES = [ |
| 'default', |
| 'onedark', |
| 'catppuccin-mocha', |
| 'tokyo-night', |
| 'nord', |
| // Product accent palettes named by color family. `coral` warm pink, |
| // `azure` cool blue; `forest` deep moss, `dusk` violet twilight, |
| // `sand` warm amber on cream, `mono` distraction-free grayscale. |
| 'coral', |
| 'azure', |
| 'forest', |
| 'dusk', |
| 'sand', |
| 'mono', |
| ] as const; |
| |
| export type ThemePalette = (typeof THEME_PALETTES)[number]; |
| |
| export function isThemePalette(value: unknown): value is ThemePalette { |
| return typeof value === 'string' && (THEME_PALETTES as readonly string[]).includes(value); |
| } |
| |
| export interface AppearanceSettings { |
| theme: ThemePreference; |
| /** |
| * PR-UI-2: optional base46 palette override. When omitted or `default`, |
| * Maka renders the original purple-accent palette. Older settings.json |
| * files without this field continue to work — `normalizeSettings()` |
| * defaults missing values to `default`. |
| */ |
| palette?: ThemePalette; |
| } |
| |
| /** |
| * PR-LANG-PREF-0 (WAWQAQ msg `edc9cb41` + xuan `b4f4f2a8`/`54b56858` |
| * + kenji `7e532892`): closed UI-locale preference. |
| * |
| * `'auto'` — temporarily resolve to Chinese-first UI copy. |
| * `'zh'` / `'en'` — user explicit override; takes precedence over |
| * the temporary fallback but is itself overridden by the e2e-fixture |
| * fixture locale (fixtures stay deterministic regardless of the |
| * persisted user preference). |
| * |
| * Closed union so adding a third locale is a deliberate |
| * contract-level decision. |
| */ |
| export interface PersonalizationSettings { |
| /** How the assistant addresses the user. Empty falls back to "你". */ |
| displayName: string; |
| /** Inline tone preference shown to the model in its system prompt. */ |
| assistantTone: string; |
| /** |
| * PR-LANG-PREF-0: UI locale preference (kenji `7e532892` acceptance): |
| * user explicit choice > temporary auto-to-Chinese fallback; e2e-fixture override |
| * stays for fixture tests. Defaults to `'auto'`. |
| */ |
| uiLocale: UiLocalePreference; |
| /** User-selected custom PetPack. `null` keeps the pet surface disabled. */ |
| selectedPetId: string | null; |
| } |
| |
| /** |
| * PR110b: persisted onboarding state. Only `milestones` lives in |
| * settings.json — `OnboardingState` is a runtime projection and is |
| * never persisted. The milestone list is sanitized via |
| * `sanitizeOnboardingMilestones()` (closed enum + at-most-one |
| * terminal + strict field set) on every read and write. |
| */ |
| export interface OnboardingSettings { |
| milestones: OnboardingMilestone[]; |
| } |
| |
| export interface WorkspaceInstructionsSettings { |
| enabled: boolean; |
| } |
| |
| /** |
| * Which project a new conversation opens in. |
| * |
| * `defaultProjectId` absent means "no preference", and the app keeps its |
| * existing behaviour of reopening whatever project was used last. Set, it wins |
| * every time — a default that only applied when nothing else was remembered |
| * would be a default in name only, and the composer picker is what overrides |
| * it for a single conversation. |
| * |
| * A project id rather than a path: the catalog owns identity (a project can be |
| * relinked to a new location and keep its id), so storing the path would make |
| * this preference silently point at the wrong project after a move. |
| */ |
| export interface ProjectPreferencesSettings { |
| defaultProjectId?: string; |
| } |
| |
| export interface PrivacySettings { |
| incognitoActive: boolean; |
| } |
| |
| /** |
| * `explore` is excluded — it's reserved for Deep Research sessions and |
| * Bot-incoming guards and is never a mode the user picks, in the composer |
| * dropdown or here. Derived from the canonical PERMISSION_MODES (not a |
| * hand-copied literal) so adding a future mode updates every consumer — |
| * the Settings picker, the composer picker (@maka/ui re-exports this |
| * list as PERMISSION_MODE_ORDER), and the settings validation — in one |
| * place. |
| */ |
| export type ChatDefaultPermissionMode = Extract<PermissionMode, 'ask' | 'bypass'>; |
| |
| export const CHAT_DEFAULT_PERMISSION_MODES: readonly ChatDefaultPermissionMode[] = [ |
| 'ask', |
| 'bypass', |
| ]; |
| |
| export function isChatDefaultPermissionMode(value: unknown): value is ChatDefaultPermissionMode { |
| return ( |
| typeof value === 'string' && |
| (CHAT_DEFAULT_PERMISSION_MODES as readonly string[]).includes(value) |
| ); |
| } |
| |
| /** Seeds new sessions' starting permission mode (Settings → 通用 → 默认权限模式). */ |
| export interface ChatDefaultsSettings { |
| permissionMode: ChatDefaultPermissionMode; |
| /** |
| * Seeds new sessions' thinking level. `undefined` means "whatever the model |
| * does on its own" — the absence of a preference, not a level. |
| * |
| * A chosen level is a wish, not a guarantee: models expose different ladders, |
| * so one that does not offer the chosen rung falls back to its own default |
| * for that session rather than being forced to the nearest neighbour. The |
| * composer already resolves it that way for the per-session picker. |
| */ |
| thinkingLevel?: ThinkingLevel; |
| } |
| |
| /** |
| * Desktop OS notifications (Settings → 通用 → 通知). The runtime only |
| * knows a turn ended from the renderer; the main process owns the focus |
| * gate + native `Notification`, so this is a pure product on/off toggle. |
| */ |
| export interface NotificationSettings { |
| /** |
| * When enabled, the desktop app raises a native notification once an |
| * agent turn finishes (completed or errored) **while its window is not |
| * focused**. Focus + OS-permission gating live in the main process. |
| */ |
| runComplete: boolean; |
| } |
| |
| /** |
| * System-level power behavior (Settings surface: the 定时任务 page's |
| * capability row). Scheduled tasks are driven by an in-process timer; when |
| * the machine sleeps, that timer is frozen and reminders silently never |
| * fire. `keepSystemAwake` lets the user hold a power-save blocker so |
| * background scheduled work keeps running. |
| * |
| * The main process owns the actual Electron `powerSaveBlocker` |
| * (`prevent-app-suspension`, which keeps the system awake WITHOUT forcing |
| * the display on). This flag is the pure product on/off toggle, mirroring |
| * `notifications.runComplete`. |
| */ |
| export interface SystemSettings { |
| keepSystemAwake: boolean; |
| } |
| |
| export interface AppSettings { |
| schemaVersion: 1; |
| network: AppNetworkSettings; |
| botChat: BotChatSettings; |
| usage: UsageSettings; |
| appearance: AppearanceSettings; |
| personalization: PersonalizationSettings; |
| onboarding: OnboardingSettings; |
| webSearch: WebSearchSettings; |
| localMemory: LocalMemorySettings; |
| workspaceInstructions: WorkspaceInstructionsSettings; |
| privacy: PrivacySettings; |
| chatDefaults: ChatDefaultsSettings; |
| projects: ProjectPreferencesSettings; |
| notifications: NotificationSettings; |
| system: SystemSettings; |
| subagents: SubagentSettings; |
| } |
| |
| export interface UsageRequestLog { |
| id: string; |
| ts: number; |
| kind: 'model' | 'tool'; |
| sessionId: string; |
| turnId: string; |
| provider: string; |
| model: string; |
| toolName?: string; |
| inputTokens: number; |
| outputTokens: number; |
| cacheMiss?: number; |
| cacheRead?: number; |
| cacheCreation?: number; |
| reasoning?: number; |
| costUsd?: number; |
| latencyMs?: number; |
| status: 'success' | 'error'; |
| } |
| |
| export interface UsageSummary { |
| totalRequests: number; |
| totalCostUsd: number; |
| totalTokens: number; |
| inputTokens: number; |
| outputTokens: number; |
| cacheTokens: number; |
| cacheMiss: number; |
| cacheRead: number; |
| cacheCreation: number; |
| reasoning: number; |
| } |
| |
| export interface UsageStats { |
| summary: UsageSummary; |
| logs: UsageRequestLog[]; |
| byProvider: Array<{ |
| provider: string; |
| requests: number; |
| tokens: number; |
| costUsd: number; |
| }>; |
| byModel: Array<{ |
| model: string; |
| requests: number; |
| tokens: number; |
| costUsd: number; |
| }>; |
| byTool: Array<{ |
| tool: string; |
| calls: number; |
| success: number; |
| errors: number; |
| avgDurationMs: number; |
| }>; |
| pricing: Array<{ |
| provider: string; |
| model: string; |
| inputPerMTokUsd: number; |
| outputPerMTokUsd: number; |
| }>; |
| } |
| |
| export interface SettingsTestResult { |
| ok: boolean; |
| code?: SettingsTestResultCode; |
| message: string; |
| latencyMs?: number; |
| details?: Record<string, unknown>; |
| } |
| |
| export type SettingsTestResultCode = |
| | 'proxy_reachable' |
| | 'proxy_disabled' |
| | 'proxy_configuration_missing' |
| | 'proxy_timeout' |
| | 'proxy_http_error' |
| | 'proxy_unreachable' |
| | 'bot_credentials_valid' |
| | 'bot_token_missing' |
| | 'bot_token_invalid' |
| | 'bot_app_credentials_missing' |
| | 'bot_connection_failed'; |
| |
| export type UpdateAppSettingsInput = Partial<{ |
| network: Partial<{ |
| proxy: Partial<NetworkProxySettings>; |
| }>; |
| botChat: BotChatSettingsPatch; |
| usage: Partial<UsageSettings>; |
| appearance: Partial<AppearanceSettings>; |
| personalization: Partial<PersonalizationSettings>; |
| localMemory: Partial<LocalMemorySettings>; |
| workspaceInstructions: Partial<WorkspaceInstructionsSettings>; |
| privacy: Partial<PrivacySettings>; |
| chatDefaults: Partial<ChatDefaultsSettings>; |
| projects: Partial<ProjectPreferencesSettings>; |
| notifications: Partial<NotificationSettings>; |
| system: Partial<SystemSettings>; |
| webSearch: WebSearchSettingsPatch; |
| subagents: SubagentSettings; |
| }>; |
| |
| export type PersonalizationSettingsWarning = |
| | 'override-attempt' |
| | 'sensitive-pattern' |
| | 'control-chars'; |
| |
| export interface UpdateAppSettingsWarnings { |
| personalization?: PersonalizationSettingsWarning[]; |
| } |
| |
| export interface UpdateAppSettingsResult { |
| settings: AppSettings; |
| warnings?: UpdateAppSettingsWarnings; |
| } |
| |
| export const DEFAULT_PROXY_BYPASS_DOMAINS = [ |
| 'localhost', |
| '127.0.0.1', |
| '::1', |
| '192.168.*', |
| '10.*', |
| '*.local', |
| ]; |
| |
| export function createDefaultSettings(): AppSettings { |
| return { |
| schemaVersion: 1, |
| network: { |
| proxy: { |
| enabled: false, |
| protocol: 'http', |
| host: '127.0.0.1', |
| port: 7890, |
| authEnabled: false, |
| username: '', |
| password: '', |
| bypassList: ['metaso.cn', 'baidu.com'], |
| autoBypassDomains: DEFAULT_PROXY_BYPASS_DOMAINS, |
| }, |
| }, |
| botChat: createDefaultBotChatSettings(), |
| usage: { |
| range: '24h', |
| status: 'all', |
| modelFilter: '', |
| showDetails: false, |
| activeTab: 'requests', |
| }, |
| appearance: { |
| theme: 'auto', |
| palette: 'default', |
| }, |
| personalization: { |
| displayName: '', |
| assistantTone: '', |
| uiLocale: 'auto', |
| selectedPetId: null, |
| }, |
| onboarding: { |
| milestones: [], |
| }, |
| webSearch: defaultWebSearchSettings(), |
| localMemory: defaultLocalMemorySettings(), |
| workspaceInstructions: { |
| enabled: true, |
| }, |
| privacy: defaultPrivacySettings(), |
| projects: defaultProjectPreferencesSettings(), |
| chatDefaults: defaultChatDefaultsSettings(), |
| notifications: { |
| runComplete: true, |
| }, |
| system: { |
| // Off by default: holding a power-save blocker is an explicit, |
| // battery-affecting opt-in, not a silent default. |
| keepSystemAwake: false, |
| }, |
| subagents: { presets: [] }, |
| }; |
| } |
| |
| export function mergeSettings(current: AppSettings, patch: UpdateAppSettingsInput): AppSettings { |
| return { |
| ...current, |
| network: { |
| ...current.network, |
| ...(patch.network ?? {}), |
| proxy: { |
| ...current.network.proxy, |
| ...(patch.network?.proxy ?? {}), |
| }, |
| }, |
| botChat: mergeBotChatSettings(current.botChat, patch.botChat), |
| usage: { |
| ...current.usage, |
| ...(patch.usage ?? {}), |
| }, |
| appearance: { |
| ...current.appearance, |
| ...(patch.appearance ?? {}), |
| }, |
| personalization: { |
| ...current.personalization, |
| ...(patch.personalization ?? {}), |
| selectedPetId: normalizeSelectedPetId( |
| patch.personalization?.selectedPetId === undefined |
| ? current.personalization.selectedPetId |
| : patch.personalization.selectedPetId, |
| ), |
| }, |
| onboarding: { |
| ...current.onboarding, |
| // PR110b: milestones flow through a dedicated setMilestone IPC |
| // rather than the generic UpdateAppSettingsInput patch surface. |
| // Keep the existing list intact when callers patch other sections. |
| }, |
| localMemory: patch.localMemory |
| ? normalizeLocalMemorySettings({ |
| ...current.localMemory, |
| ...patch.localMemory, |
| }) |
| : current.localMemory, |
| workspaceInstructions: patch.workspaceInstructions |
| ? normalizeWorkspaceInstructionsSettings({ |
| ...current.workspaceInstructions, |
| ...patch.workspaceInstructions, |
| }) |
| : current.workspaceInstructions, |
| privacy: patch.privacy |
| ? normalizePrivacySettings({ ...current.privacy, ...patch.privacy }) |
| : current.privacy, |
| projects: patch.projects |
| ? normalizeProjectPreferencesSettings({ ...current.projects, ...patch.projects }) |
| : current.projects, |
| chatDefaults: patch.chatDefaults |
| ? normalizeChatDefaultsSettings({ |
| ...current.chatDefaults, |
| ...patch.chatDefaults, |
| }) |
| : current.chatDefaults, |
| notifications: { |
| ...current.notifications, |
| ...(patch.notifications ?? {}), |
| }, |
| system: { |
| ...current.system, |
| ...(patch.system ?? {}), |
| }, |
| webSearch: mergeWebSearchSettings(current.webSearch, patch.webSearch), |
| subagents: |
| patch.subagents === undefined |
| ? current.subagents |
| : normalizeSubagentSettings(patch.subagents), |
| }; |
| } |
| |
| export function normalizeSettings(input: unknown): AppSettings { |
| const defaults = createDefaultSettings(); |
| if (!input || typeof input !== 'object') return defaults; |
| const value = input as Partial<AppSettings>; |
| const base = mergeSettings(defaults, { |
| network: value.network, |
| botChat: value.botChat, |
| usage: value.usage, |
| appearance: value.appearance, |
| personalization: value.personalization, |
| webSearch: value.webSearch, |
| localMemory: value.localMemory, |
| workspaceInstructions: value.workspaceInstructions, |
| privacy: value.privacy, |
| chatDefaults: value.chatDefaults, |
| projects: value.projects, |
| notifications: value.notifications, |
| system: value.system, |
| subagents: value.subagents, |
| }); |
| // PR110b: milestones bypass the generic patch surface so we can |
| // sanitize them with the closed-enum + at-most-one validator on |
| // every read. The settings → onboarding dependency is one-way; there |
| // is no cycle. |
| const rawOnboarding = (value as { onboarding?: unknown }).onboarding; |
| const rawMilestones = |
| rawOnboarding && typeof rawOnboarding === 'object' |
| ? (rawOnboarding as { milestones?: unknown }).milestones |
| : undefined; |
| const { |
| toastPosition: _legacyToastPosition, |
| density: _legacyDensity, |
| ...appearanceWithoutLegacyFields |
| } = base.appearance as AppearanceSettings & Record<string, unknown>; |
| return { |
| ...base, |
| // PR-UI-D1 (@kenji msg 68bf2b13): closed-enum fail-closed for |
| // appearance.palette. mergeSettings spreads the raw user value |
| // straight in, so an unknown/garbage palette string would |
| // otherwise survive the normalize pass and end up driving |
| // `[data-maka-theme="evil-unknown"]` on the renderer with no |
| // matching CSS block. Validate against the closed `THEME_PALETTES` |
| // allowlist and fall back to `'default'` on any miss (undefined, |
| // non-string, unknown string). |
| // |
| // Critical: this MUST NOT silently reset other appearance fields |
| // (theme). We only override palette when it fails the type guard; |
| // everything else keeps mergeSettings's behavior. |
| // Legacy `appearance.toastPosition` and `appearance.density` are |
| // intentionally stripped here. Toasts are fixed to one app-wide |
| // position; UI density is no longer a product setting. |
| appearance: { |
| ...appearanceWithoutLegacyFields, |
| palette: isThemePalette(base.appearance.palette) ? base.appearance.palette : 'default', |
| }, |
| // PR-LANG-PREF-0: closed-enum fail-closed for the new |
| // `personalization.uiLocale` preference. mergeSettings spreads |
| // raw user values, so an unknown value would otherwise reach the |
| // renderer outside the closed reactive-locale contract. Fall back to |
| // 'auto' on any miss. |
| personalization: { |
| ...base.personalization, |
| uiLocale: isUiLocalePreference(base.personalization.uiLocale) |
| ? base.personalization.uiLocale |
| : 'auto', |
| selectedPetId: normalizeSelectedPetId(base.personalization.selectedPetId), |
| }, |
| botChat: normalizeBotChatSettings(base.botChat, value.botChat), |
| onboarding: { |
| milestones: sanitizeOnboardingMilestones(rawMilestones), |
| }, |
| webSearch: normalizeWebSearchSettings(base.webSearch), |
| localMemory: normalizeLocalMemorySettings(base.localMemory), |
| workspaceInstructions: normalizeWorkspaceInstructionsSettings(base.workspaceInstructions), |
| privacy: normalizePrivacySettings(base.privacy), |
| projects: normalizeProjectPreferencesSettings(base.projects), |
| chatDefaults: normalizeChatDefaultsSettings(base.chatDefaults), |
| // Fail-closed boolean coercion: mergeSettings spreads the raw user |
| // value, so a non-boolean `runComplete` (from a hand-edited or |
| // legacy settings.json) would otherwise reach the main-process gate |
| // as a truthy/falsy non-boolean. Default a missing/garbage value to |
| // the enabled default rather than silently disabling notifications. |
| notifications: { |
| runComplete: |
| typeof base.notifications.runComplete === 'boolean' ? base.notifications.runComplete : true, |
| }, |
| // Fail-closed boolean coercion, same reasoning as |
| // `notifications.runComplete`: a non-boolean `keepSystemAwake` (from a |
| // hand-edited or legacy settings.json) must not reach the main-process |
| // power-save-blocker gate as a truthy/falsy non-boolean. Default a |
| // missing/garbage value to `false` — never silently hold a power |
| // blocker the user did not opt into. |
| system: { |
| keepSystemAwake: |
| typeof base.system.keepSystemAwake === 'boolean' ? base.system.keepSystemAwake : false, |
| }, |
| subagents: normalizeSubagentSettings(base.subagents), |
| }; |
| } |
| |
| function normalizeSelectedPetId(value: unknown): string | null { |
| return isPetPackId(value) ? value : null; |
| } |
| |
| function normalizeWorkspaceInstructionsSettings( |
| settings: WorkspaceInstructionsSettings, |
| ): WorkspaceInstructionsSettings { |
| return { |
| enabled: settings.enabled !== false, |
| }; |
| } |
| |
| function defaultPrivacySettings(): PrivacySettings { |
| return { incognitoActive: false }; |
| } |
| |
| function defaultProjectPreferencesSettings(): ProjectPreferencesSettings { |
| return {}; |
| } |
| |
| function defaultChatDefaultsSettings(): ChatDefaultsSettings { |
| return { permissionMode: 'ask' }; |
| } |
| |
| // Closed-enum fail-closed, same reasoning as appearance.palette / |
| // personalization.uiLocale above: an unknown/garbage persisted value |
| // (corrupted settings.json, a downgraded build reading a newer schema) |
| // must not reach session-creation code as a `PermissionMode` the picker |
| // doesn't recognize -- fall back to the safest default instead. |
| function normalizeChatDefaultsSettings(settings: ChatDefaultsSettings): ChatDefaultsSettings { |
| return { |
| // Same fail-closed reasoning as the mode below: a garbage persisted level |
| // drops to "no preference" (the model's own default) rather than reaching |
| // session creation as a rung no picker recognizes. |
| thinkingLevel: isThinkingLevel(settings.thinkingLevel) ? settings.thinkingLevel : undefined, |
| permissionMode: |
| (settings.permissionMode as unknown) === 'execute' |
| ? 'ask' |
| : isChatDefaultPermissionMode(settings.permissionMode) |
| ? settings.permissionMode |
| : 'ask', |
| }; |
| } |
| |
| function normalizePrivacySettings(settings: PrivacySettings): PrivacySettings { |
| return { |
| incognitoActive: settings.incognitoActive === true, |
| }; |
| } |
| |
| // A blank or non-string id is dropped rather than carried: it cannot name a |
| // project, and letting it through would make "has a default" true while |
| // nothing resolves. Whether the id still names a LIVE project is decided at |
| // read time by the catalog, not here -- a project can be archived or its folder |
| // removed long after this value was written, so normalization is the wrong |
| // place to answer that. |
| function normalizeProjectPreferencesSettings( |
| settings: ProjectPreferencesSettings | undefined, |
| ): ProjectPreferencesSettings { |
| const id = settings?.defaultProjectId; |
| return typeof id === 'string' && id.trim() !== '' ? { defaultProjectId: id } : {}; |
| } |