/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import * as fs from 'node:fs'; import * as path from 'node:path'; import { platform } from 'node:os'; import * as dotenv from 'dotenv'; import process from 'node:process'; import { CoreEvent, FatalConfigError, GEMINI_DIR, getErrorMessage, Storage, coreEvents, homedir, createCache, } from '@google/gemini-cli-core'; import stripJsonComments from 'strip-json-comments'; import { DefaultLight } from '../ui/themes/builtin/light/default-light.js'; import { DefaultDark } from '../ui/themes/builtin/dark/default-dark.js'; import { isWorkspaceTrusted } from './trustedFolders.js'; import { getSettingsSchema, } from './settingsSchema.js'; export { getSettingsSchema, }; import { resolveEnvVarsInObject } from '../utils/envVarResolver.js'; import { customDeepMerge } from '../utils/deepMerge.js'; import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js'; import { validateSettings, formatValidationError, } from './settings-validation.js'; export function getMergeStrategyForPath(path) { let current = undefined; let currentSchema = getSettingsSchema(); let parent = undefined; for (const key of path) { if (!currentSchema || !currentSchema[key]) { // Key not found in schema - check if parent has additionalProperties if (parent?.additionalProperties?.mergeStrategy) { return parent.additionalProperties.mergeStrategy; } return undefined; } parent = current; current = currentSchema[key]; currentSchema = current.properties; } return current?.mergeStrategy; } export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath(); export const USER_SETTINGS_DIR = path.dirname(USER_SETTINGS_PATH); export const DEFAULT_EXCLUDED_ENV_VARS = ['DEBUG', 'DEBUG_MODE']; const AUTH_ENV_VAR_WHITELIST = [ 'GEMINI_API_KEY', 'GOOGLE_API_KEY', 'GOOGLE_CLOUD_PROJECT', 'GOOGLE_CLOUD_LOCATION', 'GOOGLE_GEMINI_BASE_URL', 'GOOGLE_VERTEX_BASE_URL', ]; /** * Sanitizes an environment variable value to prevent shell injection. * Restricts values to a safe character set: alphanumeric, -, _, ., / */ export function sanitizeEnvVar(value) { return value.replace(/[^a-zA-Z0-9\-_./:@]/g, ''); } export function getSystemSettingsPath() { if (process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']) { return process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']; } if (platform() === 'darwin') { return '/Library/Application Support/GeminiCli/settings.json'; } else if (platform() === 'win32') { return 'C:\\ProgramData\\gemini-cli\\settings.json'; } else { return '/etc/gemini-cli/settings.json'; } } export function getSystemDefaultsPath() { if (process.env['GEMINI_CLI_SYSTEM_DEFAULTS_PATH']) { return process.env['GEMINI_CLI_SYSTEM_DEFAULTS_PATH']; } return path.join(path.dirname(getSystemSettingsPath()), 'system-defaults.json'); } export var SettingScope; (function (SettingScope) { SettingScope["User"] = "User"; SettingScope["Workspace"] = "Workspace"; SettingScope["System"] = "System"; SettingScope["SystemDefaults"] = "SystemDefaults"; // Note that this scope is not supported in the settings dialog at this time, // it is only supported for extensions. SettingScope["Session"] = "Session"; })(SettingScope || (SettingScope = {})); /** * The actual values of the loadable settings scopes. */ const _loadableSettingScopes = [ SettingScope.User, SettingScope.Workspace, SettingScope.System, SettingScope.SystemDefaults, ]; /** * A type guard function that checks if `scope` is a loadable settings scope, * and allows promotion to the `LoadableSettingsScope` type based on the result. */ export function isLoadableSettingScope(scope) { return _loadableSettingScopes.includes(scope); } function setNestedProperty(obj, path, value) { const keys = path.split('.'); const lastKey = keys.pop(); if (!lastKey) return; let current = obj; for (const key of keys) { if (current[key] === undefined) { current[key] = {}; } const next = current[key]; if (typeof next === 'object' && next !== null) { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion current = next; } else { // This path is invalid, so we stop. return; } } current[lastKey] = value; } export function getDefaultsFromSchema(schema = getSettingsSchema()) { const defaults = {}; for (const key in schema) { const definition = schema[key]; if (definition.properties) { defaults[key] = getDefaultsFromSchema(definition.properties); } else if (definition.default !== undefined) { defaults[key] = definition.default; } } return defaults; } export function mergeSettings(system, systemDefaults, user, workspace, isTrusted) { const safeWorkspace = isTrusted ? workspace : {}; const schemaDefaults = getDefaultsFromSchema(); // Settings are merged with the following precedence (last one wins for // single values): // 1. Schema Defaults (Built-in) // 2. System Defaults // 3. User Settings // 4. Workspace Settings // 5. System Settings (as overrides) // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return customDeepMerge(getMergeStrategyForPath, schemaDefaults, systemDefaults, user, safeWorkspace, system); } /** * Creates a fully populated MergedSettings object for testing purposes. * It merges the provided overrides with the default settings from the schema. * * @param overrides Partial settings to override the defaults. * @returns A complete MergedSettings object. */ export function createTestMergedSettings(overrides = {}) { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion return customDeepMerge(getMergeStrategyForPath, getDefaultsFromSchema(), overrides); } export class LoadedSettings { constructor(system, systemDefaults, user, workspace, isTrusted, errors = []) { this.system = system; this.systemDefaults = systemDefaults; this.user = user; this._workspaceFile = workspace; this.isTrusted = isTrusted; this.workspace = isTrusted ? workspace : this.createEmptyWorkspace(workspace); this.errors = errors; this._merged = this.computeMergedSettings(); this._snapshot = this.computeSnapshot(); } system; systemDefaults; user; workspace; isTrusted; errors; _workspaceFile; _merged; _snapshot; _remoteAdminSettings; get merged() { return this._merged; } setTrusted(isTrusted) { if (this.isTrusted === isTrusted) { return; } this.isTrusted = isTrusted; this.workspace = isTrusted ? this._workspaceFile : this.createEmptyWorkspace(this._workspaceFile); this._merged = this.computeMergedSettings(); coreEvents.emitSettingsChanged(); } createEmptyWorkspace(workspace) { return { ...workspace, settings: {}, originalSettings: {}, }; } computeMergedSettings() { const merged = mergeSettings(this.system.settings, this.systemDefaults.settings, this.user.settings, this.workspace.settings, this.isTrusted); // Remote admin settings always take precedence and file-based admin settings // are ignored. const adminSettingSchema = getSettingsSchema().admin; if (adminSettingSchema?.properties) { const adminSchema = adminSettingSchema.properties; const adminDefaults = getDefaultsFromSchema(adminSchema); // The final admin settings are the defaults overridden by remote settings. // Any admin settings from files are ignored. // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion merged.admin = customDeepMerge((path) => getMergeStrategyForPath(['admin', ...path]), adminDefaults, this._remoteAdminSettings?.admin ?? {}); } return merged; } computeSnapshot() { const cloneSettingsFile = (file) => ({ path: file.path, rawJson: file.rawJson, settings: structuredClone(file.settings), originalSettings: structuredClone(file.originalSettings), }); return { system: cloneSettingsFile(this.system), systemDefaults: cloneSettingsFile(this.systemDefaults), user: cloneSettingsFile(this.user), workspace: cloneSettingsFile(this.workspace), isTrusted: this.isTrusted, errors: [...this.errors], merged: structuredClone(this._merged), }; } // Passing this along with getSnapshot to useSyncExternalStore allows for idiomatic reactivity on settings changes // React will pass a listener fn into this subscribe fn // that listener fn will perform an object identity check on the snapshot and trigger a React re render if the snapshot has changed subscribe(listener) { coreEvents.on(CoreEvent.SettingsChanged, listener); return () => coreEvents.off(CoreEvent.SettingsChanged, listener); } getSnapshot() { return this._snapshot; } forScope(scope) { switch (scope) { case SettingScope.User: return this.user; case SettingScope.Workspace: return this.workspace; case SettingScope.System: return this.system; case SettingScope.SystemDefaults: return this.systemDefaults; default: throw new Error(`Invalid scope: ${scope}`); } } isPersistable(settingsFile) { return !settingsFile.readOnly; } setValue(scope, key, value) { const settingsFile = this.forScope(scope); // Clone value to prevent reference sharing const valueToSet = typeof value === 'object' && value !== null ? structuredClone(value) : value; setNestedProperty(settingsFile.settings, key, valueToSet); if (this.isPersistable(settingsFile)) { // Use a fresh clone for originalSettings to ensure total independence setNestedProperty(settingsFile.originalSettings, key, structuredClone(valueToSet)); saveSettings(settingsFile); } this._merged = this.computeMergedSettings(); this._snapshot = this.computeSnapshot(); coreEvents.emitSettingsChanged(); } setRemoteAdminSettings(remoteSettings) { const admin = {}; const { strictModeDisabled, mcpSetting, cliFeatureSetting } = remoteSettings; if (Object.keys(remoteSettings).length === 0) { this._remoteAdminSettings = { admin }; this._merged = this.computeMergedSettings(); return; } admin.secureModeEnabled = !strictModeDisabled; admin.mcp = { enabled: mcpSetting?.mcpEnabled, config: mcpSetting?.mcpConfig?.mcpServers, }; admin.extensions = { enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled, }; admin.skills = { enabled: cliFeatureSetting?.unmanagedCapabilitiesEnabled, }; this._remoteAdminSettings = { admin }; this._merged = this.computeMergedSettings(); } } function findEnvFile(startDir) { let currentDir = path.resolve(startDir); while (true) { // prefer gemini-specific .env under GEMINI_DIR const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env'); if (fs.existsSync(geminiEnvPath)) { return geminiEnvPath; } const envPath = path.join(currentDir, '.env'); if (fs.existsSync(envPath)) { return envPath; } const parentDir = path.dirname(currentDir); if (parentDir === currentDir || !parentDir) { // check .env under home as fallback, again preferring gemini-specific .env const homeGeminiEnvPath = path.join(homedir(), GEMINI_DIR, '.env'); if (fs.existsSync(homeGeminiEnvPath)) { return homeGeminiEnvPath; } const homeEnvPath = path.join(homedir(), '.env'); if (fs.existsSync(homeEnvPath)) { return homeEnvPath; } return null; } currentDir = parentDir; } } export function setUpCloudShellEnvironment(envFilePath, isTrusted, isSandboxed) { // Special handling for GOOGLE_CLOUD_PROJECT in Cloud Shell: // Because GOOGLE_CLOUD_PROJECT in Cloud Shell tracks the project // set by the user using "gcloud config set project" we do not want to // use its value. So, unless the user overrides GOOGLE_CLOUD_PROJECT in // one of the .env files, we set the Cloud Shell-specific default here. let value = 'cloudshell-gca'; if (envFilePath && fs.existsSync(envFilePath)) { const envFileContent = fs.readFileSync(envFilePath); const parsedEnv = dotenv.parse(envFileContent); if (parsedEnv['GOOGLE_CLOUD_PROJECT']) { // .env file takes precedence in Cloud Shell value = parsedEnv['GOOGLE_CLOUD_PROJECT']; if (!isTrusted && isSandboxed) { value = sanitizeEnvVar(value); } } } process.env['GOOGLE_CLOUD_PROJECT'] = value; } export function loadEnvironment(settings, workspaceDir, isWorkspaceTrustedFn = isWorkspaceTrusted) { const envFilePath = findEnvFile(workspaceDir); const trustResult = isWorkspaceTrustedFn(settings, workspaceDir); const isTrusted = trustResult.isTrusted ?? false; // Check settings OR check process.argv directly since this might be called // before arguments are fully parsed. This is a best-effort sniffing approach // that happens early in the CLI lifecycle. It is designed to detect the // sandbox flag before the full command-line parser is initialized to ensure // security constraints are applied when loading environment variables. const args = process.argv.slice(2); const doubleDashIndex = args.indexOf('--'); const relevantArgs = doubleDashIndex === -1 ? args : args.slice(0, doubleDashIndex); const isSandboxed = !!settings.tools?.sandbox || relevantArgs.includes('-s') || relevantArgs.includes('--sandbox'); // Cloud Shell environment variable handling if (process.env['CLOUD_SHELL'] === 'true') { setUpCloudShellEnvironment(envFilePath, isTrusted, isSandboxed); } if (envFilePath) { // Manually parse and load environment variables to handle exclusions correctly. // This avoids modifying environment variables that were already set from the shell. try { const envFileContent = fs.readFileSync(envFilePath, 'utf-8'); const parsedEnv = dotenv.parse(envFileContent); const excludedVars = settings?.advanced?.excludedEnvVars || DEFAULT_EXCLUDED_ENV_VARS; const isProjectEnvFile = !envFilePath.includes(GEMINI_DIR); for (const key in parsedEnv) { if (Object.hasOwn(parsedEnv, key)) { let value = parsedEnv[key]; // If the workspace is untrusted but we are sandboxed, only allow whitelisted variables. if (!isTrusted && isSandboxed) { if (!AUTH_ENV_VAR_WHITELIST.includes(key)) { continue; } // Sanitize the value for untrusted sources value = sanitizeEnvVar(value); } // If it's a project .env file, skip loading excluded variables. if (isProjectEnvFile && excludedVars.includes(key)) { continue; } // Load variable only if it's not already set in the environment. if (!Object.hasOwn(process.env, key)) { process.env[key] = value; } } } } catch (_e) { // Errors are ignored to match the behavior of `dotenv.config({ quiet: true })`. } } } // Cache to store the results of loadSettings to avoid redundant disk I/O. const settingsCache = createCache({ storage: 'map', defaultTtl: 10000, // 10 seconds }); /** * Resets the settings cache. Used exclusively for test isolation. * @internal */ export function resetSettingsCacheForTesting() { settingsCache.clear(); } /** * Loads settings from user and workspace directories. * Project settings override user settings. */ export function loadSettings(workspaceDir = process.cwd()) { const normalizedWorkspaceDir = path.resolve(workspaceDir); return settingsCache.getOrCreate(normalizedWorkspaceDir, () => _doLoadSettings(normalizedWorkspaceDir)); } /** * Internal implementation of the settings loading logic. */ function _doLoadSettings(workspaceDir) { let systemSettings = {}; let systemDefaultSettings = {}; let userSettings = {}; let workspaceSettings = {}; const settingsErrors = []; const systemSettingsPath = getSystemSettingsPath(); const systemDefaultsPath = getSystemDefaultsPath(); const storage = new Storage(workspaceDir); const workspaceSettingsPath = storage.getWorkspaceSettingsPath(); const load = (filePath) => { try { if (fs.existsSync(filePath)) { const content = fs.readFileSync(filePath, 'utf-8'); const rawSettings = JSON.parse(stripJsonComments(content)); if (typeof rawSettings !== 'object' || rawSettings === null || Array.isArray(rawSettings)) { settingsErrors.push({ message: 'Settings file is not a valid JSON object.', path: filePath, severity: 'error', }); return { settings: {} }; } // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const settingsObject = rawSettings; // Validate settings structure with Zod const validationResult = validateSettings(settingsObject); if (!validationResult.success && validationResult.error) { const errorMessage = formatValidationError(validationResult.error, filePath); settingsErrors.push({ message: errorMessage, path: filePath, severity: 'warning', }); } return { settings: settingsObject, rawJson: content }; } } catch (error) { settingsErrors.push({ message: getErrorMessage(error), path: filePath, severity: 'error', }); } return { settings: {} }; }; const systemResult = load(systemSettingsPath); const systemDefaultsResult = load(systemDefaultsPath); const userResult = load(USER_SETTINGS_PATH); let workspaceResult = { settings: {}, rawJson: undefined, }; if (!storage.isWorkspaceHomeDir()) { workspaceResult = load(workspaceSettingsPath); } const systemOriginalSettings = structuredClone(systemResult.settings); const systemDefaultsOriginalSettings = structuredClone(systemDefaultsResult.settings); const userOriginalSettings = structuredClone(userResult.settings); const workspaceOriginalSettings = structuredClone(workspaceResult.settings); // Environment variables for runtime use systemSettings = resolveEnvVarsInObject(systemResult.settings); systemDefaultSettings = resolveEnvVarsInObject(systemDefaultsResult.settings); userSettings = resolveEnvVarsInObject(userResult.settings); workspaceSettings = resolveEnvVarsInObject(workspaceResult.settings); // Support legacy theme names if (userSettings.ui?.theme === 'VS') { userSettings.ui.theme = DefaultLight.name; } else if (userSettings.ui?.theme === 'VS2015') { userSettings.ui.theme = DefaultDark.name; } if (workspaceSettings.ui?.theme === 'VS') { workspaceSettings.ui.theme = DefaultLight.name; } else if (workspaceSettings.ui?.theme === 'VS2015') { workspaceSettings.ui.theme = DefaultDark.name; } // For the initial trust check, we can only use user and system settings. const initialTrustCheckSettings = customDeepMerge(getMergeStrategyForPath, getDefaultsFromSchema(), systemDefaultSettings, userSettings, systemSettings); const isTrusted = isWorkspaceTrusted(initialTrustCheckSettings, workspaceDir) .isTrusted ?? false; // Create a temporary merged settings object to pass to loadEnvironment. const tempMergedSettings = mergeSettings(systemSettings, systemDefaultSettings, userSettings, workspaceSettings, isTrusted); // loadEnvironment depends on settings so we have to create a temp version of // the settings to avoid a cycle loadEnvironment(tempMergedSettings, workspaceDir); // Check for any fatal errors before proceeding const fatalErrors = settingsErrors.filter((e) => e.severity === 'error'); if (fatalErrors.length > 0) { const errorMessages = fatalErrors.map((error) => `Error in ${error.path}: ${error.message}`); throw new FatalConfigError(`${errorMessages.join('\n')}\nPlease fix the configuration file(s) and try again.`); } const loadedSettings = new LoadedSettings({ path: systemSettingsPath, settings: systemSettings, originalSettings: systemOriginalSettings, rawJson: systemResult.rawJson, readOnly: true, }, { path: systemDefaultsPath, settings: systemDefaultSettings, originalSettings: systemDefaultsOriginalSettings, rawJson: systemDefaultsResult.rawJson, readOnly: true, }, { path: USER_SETTINGS_PATH, settings: userSettings, originalSettings: userOriginalSettings, rawJson: userResult.rawJson, readOnly: false, }, { path: storage.isWorkspaceHomeDir() ? '' : workspaceSettingsPath, settings: workspaceSettings, originalSettings: workspaceOriginalSettings, rawJson: workspaceResult.rawJson, readOnly: storage.isWorkspaceHomeDir(), }, isTrusted, settingsErrors); // Automatically migrate deprecated settings when loading. migrateDeprecatedSettings(loadedSettings); return loadedSettings; } /** * Migrates deprecated settings to their new counterparts. * * Deprecated settings are removed from settings files by default. * * @returns true if any changes were made and need to be saved. */ export function migrateDeprecatedSettings(loadedSettings, removeDeprecated = true) { let anyModified = false; const systemWarnings = new Map(); /** * Helper to migrate a boolean setting and track it if it's deprecated. */ const migrateBoolean = (settings, oldKey, newKey, prefix, foundDeprecated) => { let modified = false; const oldValue = settings[oldKey]; const newValue = settings[newKey]; if (typeof oldValue === 'boolean') { if (foundDeprecated) { foundDeprecated.push(prefix ? `${prefix}.${oldKey}` : oldKey); } if (typeof newValue === 'boolean') { // Both exist, trust the new one if (removeDeprecated) { delete settings[oldKey]; modified = true; } } else { // Only old exists, migrate to new (inverted) settings[newKey] = !oldValue; if (removeDeprecated) { delete settings[oldKey]; } modified = true; } } return modified; }; const processScope = (scope) => { const settingsFile = loadedSettings.forScope(scope); const settings = settingsFile.settings; const foundDeprecated = []; // Migrate general settings const generalSettings = settings.general; if (generalSettings) { const newGeneral = { ...generalSettings }; let modified = false; modified = migrateBoolean(newGeneral, 'disableAutoUpdate', 'enableAutoUpdate', 'general', foundDeprecated) || modified; modified = migrateBoolean(newGeneral, 'disableUpdateNag', 'enableAutoUpdateNotification', 'general', foundDeprecated) || modified; if (modified) { loadedSettings.setValue(scope, 'general', newGeneral); if (!settingsFile.readOnly) { anyModified = true; } } } // Migrate ui settings const uiSettings = settings.ui; if (uiSettings) { const newUi = { ...uiSettings }; // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const accessibilitySettings = newUi['accessibility']; if (accessibilitySettings) { const newAccessibility = { ...accessibilitySettings }; if (migrateBoolean(newAccessibility, 'disableLoadingPhrases', 'enableLoadingPhrases', 'ui.accessibility', foundDeprecated)) { newUi['accessibility'] = newAccessibility; loadedSettings.setValue(scope, 'ui', newUi); if (!settingsFile.readOnly) { anyModified = true; } } // Migrate enableLoadingPhrases: false → loadingPhrases: 'off' const enableLP = newAccessibility['enableLoadingPhrases']; if (typeof enableLP === 'boolean' && newUi['loadingPhrases'] === undefined) { if (!enableLP) { newUi['loadingPhrases'] = 'off'; loadedSettings.setValue(scope, 'ui', newUi); if (!settingsFile.readOnly) { anyModified = true; } } foundDeprecated.push('ui.accessibility.enableLoadingPhrases'); } } } // Migrate context settings const contextSettings = settings.context; if (contextSettings) { const newContext = { ...contextSettings }; // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const fileFilteringSettings = newContext['fileFiltering']; if (fileFilteringSettings) { const newFileFiltering = { ...fileFilteringSettings }; if (migrateBoolean(newFileFiltering, 'disableFuzzySearch', 'enableFuzzySearch', 'context.fileFiltering', foundDeprecated)) { newContext['fileFiltering'] = newFileFiltering; loadedSettings.setValue(scope, 'context', newContext); if (!settingsFile.readOnly) { anyModified = true; } } } } // Migrate tools settings const toolsSettings = settings.tools; if (toolsSettings) { if (toolsSettings['approvalMode'] !== undefined) { foundDeprecated.push('tools.approvalMode'); const generalSettings = settings.general || {}; const newGeneral = { ...generalSettings }; // Only set defaultApprovalMode if it's not already set if (newGeneral['defaultApprovalMode'] === undefined) { newGeneral['defaultApprovalMode'] = toolsSettings['approvalMode']; loadedSettings.setValue(scope, 'general', newGeneral); if (!settingsFile.readOnly) { anyModified = true; } } if (removeDeprecated) { const newTools = { ...toolsSettings }; delete newTools['approvalMode']; loadedSettings.setValue(scope, 'tools', newTools); if (!settingsFile.readOnly) { anyModified = true; } } } } // Migrate experimental agent settings const experimentalModified = migrateExperimentalSettings(settings, loadedSettings, scope, removeDeprecated, foundDeprecated); if (experimentalModified) { if (!settingsFile.readOnly) { anyModified = true; } } if (settingsFile.readOnly && foundDeprecated.length > 0) { systemWarnings.set(scope, foundDeprecated); } }; processScope(SettingScope.User); processScope(SettingScope.Workspace); processScope(SettingScope.System); processScope(SettingScope.SystemDefaults); if (systemWarnings.size > 0) { for (const [scope, flags] of systemWarnings) { const scopeName = scope === SettingScope.SystemDefaults ? 'system default' : scope.toLowerCase(); coreEvents.emitFeedback('warning', `The ${scopeName} configuration contains deprecated settings: [${flags.join(', ')}]. These could not be migrated automatically as system settings are read-only. Please update the system configuration manually.`); } } return anyModified; } export function saveSettings(settingsFile) { // Clear the entire cache on any save. settingsCache.clear(); try { // Ensure the directory exists const dirPath = path.dirname(settingsFile.path); if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } const settingsToSave = settingsFile.originalSettings; // Use the format-preserving update function updateSettingsFilePreservingFormat(settingsFile.path, settingsToSave); } catch (error) { coreEvents.emitFeedback('error', 'There was an error saving your latest settings changes.', error); } } export function saveModelChange(loadedSettings, model) { try { loadedSettings.setValue(SettingScope.User, 'model.name', model); } catch (error) { coreEvents.emitFeedback('error', 'There was an error saving your preferred model.', error); } } function migrateExperimentalSettings(settings, loadedSettings, scope, removeDeprecated, foundDeprecated) { const experimentalSettings = settings.experimental; if (experimentalSettings) { const agentsSettings = { ...settings.agents, }; const agentsOverrides = { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion ...(agentsSettings['overrides'] || {}), }; let modified = false; const migrateExperimental = (oldKey, migrateFn) => { const old = experimentalSettings[oldKey]; if (old) { foundDeprecated?.push(`experimental.${oldKey}`); // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion migrateFn(old); modified = true; } }; // Migrate codebaseInvestigatorSettings -> agents.overrides.codebase_investigator migrateExperimental('codebaseInvestigatorSettings', (old) => { const override = { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion ...agentsOverrides['codebase_investigator'], }; if (old['enabled'] !== undefined) override['enabled'] = old['enabled']; const runConfig = { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion ...override['runConfig'], }; if (old['maxNumTurns'] !== undefined) runConfig['maxTurns'] = old['maxNumTurns']; if (old['maxTimeMinutes'] !== undefined) runConfig['maxTimeMinutes'] = old['maxTimeMinutes']; if (Object.keys(runConfig).length > 0) override['runConfig'] = runConfig; if (old['model'] !== undefined || old['thinkingBudget'] !== undefined) { const modelConfig = { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion ...override['modelConfig'], }; if (old['model'] !== undefined) modelConfig['model'] = old['model']; if (old['thinkingBudget'] !== undefined) { const generateContentConfig = { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion ...modelConfig['generateContentConfig'], }; const thinkingConfig = { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion ...generateContentConfig['thinkingConfig'], }; thinkingConfig['thinkingBudget'] = old['thinkingBudget']; generateContentConfig['thinkingConfig'] = thinkingConfig; modelConfig['generateContentConfig'] = generateContentConfig; } override['modelConfig'] = modelConfig; } agentsOverrides['codebase_investigator'] = override; }); // Migrate cliHelpAgentSettings -> agents.overrides.cli_help migrateExperimental('cliHelpAgentSettings', (old) => { const override = { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion ...agentsOverrides['cli_help'], }; if (old['enabled'] !== undefined) override['enabled'] = old['enabled']; agentsOverrides['cli_help'] = override; }); if (modified) { agentsSettings['overrides'] = agentsOverrides; loadedSettings.setValue(scope, 'agents', agentsSettings); if (removeDeprecated) { const newExperimental = { ...experimentalSettings }; delete newExperimental['codebaseInvestigatorSettings']; delete newExperimental['cliHelpAgentSettings']; loadedSettings.setValue(scope, 'experimental', newExperimental); } return true; } } return false; } //# sourceMappingURL=settings.js.map