2025-05-01 10:34:07 -07:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-08-25 22:11:27 +02:00
import * as fs from 'node:fs' ;
import * as path from 'node:path' ;
2026-01-06 20:09:39 -08:00
import { platform } from 'node:os' ;
2025-07-07 01:13:13 -04:00
import * as dotenv from 'dotenv' ;
2025-08-29 19:53:39 +02:00
import process from 'node:process' ;
2025-06-14 00:00:24 -07:00
import {
2026-02-11 15:40:27 -08:00
CoreEvent ,
2025-09-03 10:41:53 -07:00
FatalConfigError ,
2025-10-14 02:31:39 +09:00
GEMINI_DIR ,
2025-06-14 00:00:24 -07:00
getErrorMessage ,
2025-08-20 10:55:47 +09:00
Storage ,
2025-10-23 14:14:14 -04:00
coreEvents ,
2026-01-06 20:09:39 -08:00
homedir ,
2026-02-03 16:19:14 -05:00
type AdminControlsSettings ,
2025-06-25 05:41:11 -07:00
} from '@google/gemini-cli-core' ;
2025-05-18 10:47:57 -07:00
import stripJsonComments from 'strip-json-comments' ;
2025-05-31 11:10:52 -07:00
import { DefaultLight } from '../ui/themes/default-light.js' ;
import { DefaultDark } from '../ui/themes/default.js' ;
2025-08-21 00:38:12 -07:00
import { isWorkspaceTrusted } from './trustedFolders.js' ;
2025-09-03 19:23:25 -07:00
import {
type Settings ,
2026-01-15 09:26:10 -08:00
type MergedSettings ,
2025-09-03 19:23:25 -07:00
type MemoryImportFormat ,
type MergeStrategy ,
type SettingsSchema ,
type SettingDefinition ,
2025-09-08 10:01:18 -04:00
getSettingsSchema ,
2025-09-03 19:23:25 -07:00
} from './settingsSchema.js' ;
2026-01-15 09:26:10 -08:00
export {
type Settings ,
type MergedSettings ,
type MemoryImportFormat ,
type MergeStrategy ,
type SettingsSchema ,
type SettingDefinition ,
getSettingsSchema ,
} ;
2025-08-29 19:53:39 +02:00
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js' ;
2026-01-09 14:34:23 -08:00
import { customDeepMerge } from '../utils/deepMerge.js' ;
2025-09-13 14:31:15 +08:00
import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js' ;
2025-12-12 14:30:34 +08:00
import {
validateSettings ,
formatValidationError ,
} from './settings-validation.js' ;
2025-09-03 19:23:25 -07:00
2026-02-03 00:54:10 -05:00
export function getMergeStrategyForPath (
path : string [ ] ,
) : MergeStrategy | undefined {
2025-09-03 19:23:25 -07:00
let current : SettingDefinition | undefined = undefined ;
2025-09-08 10:01:18 -04:00
let currentSchema : SettingsSchema | undefined = getSettingsSchema ( ) ;
2025-12-03 10:01:57 -08:00
let parent : SettingDefinition | undefined = undefined ;
2025-09-03 19:23:25 -07:00
for ( const key of path ) {
if ( ! currentSchema || ! currentSchema [ key ] ) {
2025-12-03 10:01:57 -08:00
// Key not found in schema - check if parent has additionalProperties
if ( parent ? . additionalProperties ? . mergeStrategy ) {
return parent . additionalProperties . mergeStrategy ;
}
2025-09-03 19:23:25 -07:00
return undefined ;
}
2025-12-03 10:01:57 -08:00
parent = current ;
2025-09-03 19:23:25 -07:00
current = currentSchema [ key ] ;
currentSchema = current . properties ;
}
return current ? . mergeStrategy ;
}
2025-08-10 09:04:52 +09:00
2025-08-20 10:55:47 +09:00
export const USER_SETTINGS_PATH = Storage . getGlobalSettingsPath ( ) ;
export const USER_SETTINGS_DIR = path . dirname ( USER_SETTINGS_PATH ) ;
2025-08-03 20:44:15 +02:00
export const DEFAULT_EXCLUDED_ENV_VARS = [ 'DEBUG' , 'DEBUG_MODE' ] ;
2025-05-01 10:34:07 -07:00
2026-02-03 17:08:10 -08:00
const AUTH_ENV_VAR_WHITELIST = [
'GEMINI_API_KEY' ,
'GOOGLE_API_KEY' ,
'GOOGLE_CLOUD_PROJECT' ,
'GOOGLE_CLOUD_LOCATION' ,
] ;
/ * *
* Sanitizes an environment variable value to prevent shell injection .
* Restricts values to a safe character set : alphanumeric , - , _ , . , /
* /
export function sanitizeEnvVar ( value : string ) : string {
return value . replace ( /[^a-zA-Z0-9\-_./]/g , '' ) ;
}
2025-07-21 20:14:07 +00:00
export function getSystemSettingsPath ( ) : string {
2025-08-17 12:43:21 -04:00
if ( process . env [ 'GEMINI_CLI_SYSTEM_SETTINGS_PATH' ] ) {
return process . env [ 'GEMINI_CLI_SYSTEM_SETTINGS_PATH' ] ;
2025-07-21 20:14:07 +00:00
}
2025-07-09 21:16:42 +00:00
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' ;
}
}
2025-08-24 21:21:22 -07:00
export function getSystemDefaultsPath ( ) : string {
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' ,
) ;
}
2025-08-10 09:04:52 +09:00
export type { DnsResolutionOrder } from './settingsSchema.js' ;
2025-08-01 12:30:39 -07:00
2025-05-01 10:34:07 -07:00
export enum SettingScope {
User = 'User' ,
Workspace = 'Workspace' ,
2025-07-09 21:16:42 +00:00
System = 'System' ,
2025-08-24 21:21:22 -07:00
SystemDefaults = 'SystemDefaults' ,
2025-11-05 11:36:07 -08:00
// Note that this scope is not supported in the settings dialog at this time,
// it is only supported for extensions.
Session = 'Session' ,
}
/ * *
* A type representing the settings scopes that are supported for LoadedSettings .
* /
export type LoadableSettingScope =
| SettingScope . User
| SettingScope . Workspace
| SettingScope . System
| SettingScope . SystemDefaults ;
/ * *
* 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 : SettingScope ,
) : scope is LoadableSettingScope {
return _loadableSettingScopes . includes ( scope ) ;
2025-05-01 10:34:07 -07:00
}
2025-06-20 00:39:15 -04:00
export interface CheckpointingSettings {
enabled? : boolean ;
}
2025-07-15 10:22:31 -07:00
export interface SummarizeToolOutputSettings {
tokenBudget? : number ;
}
2025-06-04 00:46:57 -07:00
export interface AccessibilitySettings {
2026-01-16 23:33:49 +01:00
enableLoadingPhrases? : boolean ;
2025-08-21 22:29:15 +00:00
screenReader? : boolean ;
2025-06-04 00:46:57 -07:00
}
2025-10-06 13:34:00 -06:00
export interface SessionRetentionSettings {
/** Enable automatic session cleanup */
enabled? : boolean ;
/** Maximum age of sessions to keep (e.g., "30d", "7d", "24h", "1w") */
maxAge? : string ;
/** Alternative: Maximum number of sessions to keep (most recent) */
maxCount? : number ;
/** Minimum retention period (safety limit, defaults to "1d") */
minRetention? : string ;
}
2025-06-06 09:56:45 -07:00
export interface SettingsError {
message : string ;
path : string ;
2025-12-23 10:26:37 -08:00
severity : 'error' | 'warning' ;
2025-06-06 09:56:45 -07:00
}
2025-05-01 10:34:07 -07:00
export interface SettingsFile {
settings : Settings ;
2025-09-13 14:31:15 +08:00
originalSettings : Settings ;
2025-05-01 10:34:07 -07:00
path : string ;
2025-09-13 14:31:15 +08:00
rawJson? : string ;
2026-02-06 14:35:58 -05:00
readOnly? : boolean ;
2025-05-01 10:34:07 -07:00
}
2025-08-19 13:07:42 -06:00
2025-08-27 18:39:45 -07:00
function setNestedProperty (
obj : Record < string , unknown > ,
path : string ,
value : unknown ,
) {
const keys = path . split ( '.' ) ;
const lastKey = keys . pop ( ) ;
if ( ! lastKey ) return ;
let current : Record < string , unknown > = obj ;
for ( const key of keys ) {
if ( current [ key ] === undefined ) {
current [ key ] = { } ;
}
const next = current [ key ] ;
if ( typeof next === 'object' && next !== null ) {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2025-08-27 18:39:45 -07:00
current = next as Record < string , unknown > ;
} else {
// This path is invalid, so we stop.
return ;
}
}
current [ lastKey ] = value ;
}
2026-01-05 15:00:20 -05:00
export function getDefaultsFromSchema (
schema : SettingsSchema = getSettingsSchema ( ) ,
) : Settings {
const defaults : Record < string , unknown > = { } ;
for ( const key in schema ) {
const definition = schema [ key ] ;
if ( definition . properties ) {
2026-01-15 09:26:10 -08:00
defaults [ key ] = getDefaultsFromSchema ( definition . properties ) ;
2026-01-05 15:00:20 -05:00
} else if ( definition . default !== undefined ) {
defaults [ key ] = definition . default ;
}
}
return defaults as Settings ;
}
2026-01-15 09:26:10 -08:00
export function mergeSettings (
2025-08-19 13:07:42 -06:00
system : Settings ,
2025-08-24 21:21:22 -07:00
systemDefaults : Settings ,
2025-08-19 13:07:42 -06:00
user : Settings ,
workspace : Settings ,
2025-08-21 00:38:12 -07:00
isTrusted : boolean ,
2026-01-15 09:26:10 -08:00
) : MergedSettings {
2025-08-21 00:38:12 -07:00
const safeWorkspace = isTrusted ? workspace : ( { } as Settings ) ;
2026-01-05 15:00:20 -05:00
const schemaDefaults = getDefaultsFromSchema ( ) ;
2025-08-21 00:38:12 -07:00
2025-08-24 21:21:22 -07:00
// Settings are merged with the following precedence (last one wins for
// single values):
2026-01-05 15:00:20 -05:00
// 1. Schema Defaults (Built-in)
// 2. System Defaults
// 3. User Settings
// 4. Workspace Settings
// 5. System Settings (as overrides)
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2025-09-03 19:23:25 -07:00
return customDeepMerge (
getMergeStrategyForPath ,
2026-01-05 15:00:20 -05:00
schemaDefaults ,
2025-09-03 19:23:25 -07:00
systemDefaults ,
user ,
2025-09-05 09:10:15 -07:00
safeWorkspace ,
2025-09-03 19:23:25 -07:00
system ,
2026-01-15 09:26:10 -08:00
) as MergedSettings ;
}
/ * *
* 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 : Partial < Settings > = { } ,
) : MergedSettings {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-15 09:26:10 -08:00
return customDeepMerge (
getMergeStrategyForPath ,
getDefaultsFromSchema ( ) ,
overrides ,
) as MergedSettings ;
2025-08-19 13:07:42 -06:00
}
2026-02-11 15:40:27 -08:00
/ * *
* An immutable snapshot of settings state .
* Used with useSyncExternalStore for reactive updates .
* /
export interface LoadedSettingsSnapshot {
system : SettingsFile ;
systemDefaults : SettingsFile ;
user : SettingsFile ;
workspace : SettingsFile ;
isTrusted : boolean ;
errors : SettingsError [ ] ;
merged : MergedSettings ;
}
2025-05-01 10:34:07 -07:00
export class LoadedSettings {
2025-06-06 09:56:45 -07:00
constructor (
2025-07-09 21:16:42 +00:00
system : SettingsFile ,
2025-08-24 21:21:22 -07:00
systemDefaults : SettingsFile ,
2025-06-06 09:56:45 -07:00
user : SettingsFile ,
workspace : SettingsFile ,
2025-08-21 00:38:12 -07:00
isTrusted : boolean ,
2025-12-23 10:26:37 -08:00
errors : SettingsError [ ] = [ ] ,
2025-06-06 09:56:45 -07:00
) {
2025-07-09 21:16:42 +00:00
this . system = system ;
2025-08-24 21:21:22 -07:00
this . systemDefaults = systemDefaults ;
2025-05-01 10:34:07 -07:00
this . user = user ;
2026-02-03 14:53:31 -08:00
this . _workspaceFile = workspace ;
2025-08-21 00:38:12 -07:00
this . isTrusted = isTrusted ;
2026-02-03 14:53:31 -08:00
this . workspace = isTrusted
? workspace
: this . createEmptyWorkspace ( workspace ) ;
2025-12-23 10:26:37 -08:00
this . errors = errors ;
2025-05-02 08:15:46 -07:00
this . _merged = this . computeMergedSettings ( ) ;
2026-02-11 15:40:27 -08:00
this . _snapshot = this . computeSnapshot ( ) ;
2025-05-01 10:34:07 -07:00
}
2025-07-09 21:16:42 +00:00
readonly system : SettingsFile ;
2025-08-24 21:21:22 -07:00
readonly systemDefaults : SettingsFile ;
2025-05-01 10:34:07 -07:00
readonly user : SettingsFile ;
2026-02-03 14:53:31 -08:00
workspace : SettingsFile ;
isTrusted : boolean ;
2025-12-23 10:26:37 -08:00
readonly errors : SettingsError [ ] ;
2025-05-01 10:34:07 -07:00
2026-02-03 14:53:31 -08:00
private _workspaceFile : SettingsFile ;
2026-01-15 09:26:10 -08:00
private _merged : MergedSettings ;
2026-02-11 15:40:27 -08:00
private _snapshot : LoadedSettingsSnapshot ;
2026-01-09 17:04:57 -05:00
private _remoteAdminSettings : Partial < Settings > | undefined ;
2025-05-01 10:34:07 -07:00
2026-01-15 09:26:10 -08:00
get merged ( ) : MergedSettings {
2025-05-02 08:15:46 -07:00
return this . _merged ;
2025-05-01 10:34:07 -07:00
}
2026-02-03 14:53:31 -08:00
setTrusted ( isTrusted : boolean ) : void {
if ( this . isTrusted === isTrusted ) {
return ;
}
this . isTrusted = isTrusted ;
this . workspace = isTrusted
? this . _workspaceFile
: this . createEmptyWorkspace ( this . _workspaceFile ) ;
this . _merged = this . computeMergedSettings ( ) ;
coreEvents . emitSettingsChanged ( ) ;
}
private createEmptyWorkspace ( workspace : SettingsFile ) : SettingsFile {
return {
. . . workspace ,
settings : { } ,
originalSettings : { } ,
} ;
}
2026-01-15 09:26:10 -08:00
private computeMergedSettings ( ) : MergedSettings {
2026-01-09 17:04:57 -05:00
const merged = mergeSettings (
2025-08-19 13:07:42 -06:00
this . system . settings ,
2025-08-24 21:21:22 -07:00
this . systemDefaults . settings ,
2025-08-19 13:07:42 -06:00
this . user . settings ,
this . workspace . settings ,
2025-08-21 00:38:12 -07:00
this . isTrusted ,
2025-08-19 13:07:42 -06:00
) ;
2026-01-09 17:04:57 -05:00
// 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.
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-09 17:04:57 -05:00
merged . admin = customDeepMerge (
( path : string [ ] ) = > getMergeStrategyForPath ( [ 'admin' , . . . path ] ) ,
adminDefaults ,
this . _remoteAdminSettings ? . admin ? ? { } ,
2026-01-15 09:26:10 -08:00
) as MergedSettings [ 'admin' ] ;
2026-01-09 17:04:57 -05:00
}
return merged ;
2025-05-01 10:34:07 -07:00
}
2026-02-11 15:40:27 -08:00
private computeSnapshot ( ) : LoadedSettingsSnapshot {
const cloneSettingsFile = ( file : SettingsFile ) : SettingsFile = > ( {
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 : ( ) = > void ) : ( ) = > void {
coreEvents . on ( CoreEvent . SettingsChanged , listener ) ;
return ( ) = > coreEvents . off ( CoreEvent . SettingsChanged , listener ) ;
}
getSnapshot ( ) : LoadedSettingsSnapshot {
return this . _snapshot ;
}
2025-11-05 11:36:07 -08:00
forScope ( scope : LoadableSettingScope ) : SettingsFile {
2025-05-01 10:34:07 -07:00
switch ( scope ) {
case SettingScope . User :
return this . user ;
case SettingScope . Workspace :
return this . workspace ;
2025-07-09 21:16:42 +00:00
case SettingScope . System :
return this . system ;
2025-08-24 21:21:22 -07:00
case SettingScope . SystemDefaults :
return this . systemDefaults ;
2025-05-01 10:34:07 -07:00
default :
throw new Error ( ` Invalid scope: ${ scope } ` ) ;
}
}
2026-02-06 14:35:58 -05:00
private isPersistable ( settingsFile : SettingsFile ) : boolean {
return ! settingsFile . readOnly ;
}
2025-11-05 11:36:07 -08:00
setValue ( scope : LoadableSettingScope , key : string , value : unknown ) : void {
2025-05-01 10:34:07 -07:00
const settingsFile = this . forScope ( scope ) ;
2026-02-03 14:53:31 -08:00
2026-02-06 14:35:58 -05:00
// Clone value to prevent reference sharing
2026-02-03 14:53:31 -08:00
const valueToSet =
typeof value === 'object' && value !== null
? structuredClone ( value )
: value ;
setNestedProperty ( settingsFile . settings , key , valueToSet ) ;
2026-02-06 14:35:58 -05:00
if ( this . isPersistable ( settingsFile ) ) {
// Use a fresh clone for originalSettings to ensure total independence
setNestedProperty (
settingsFile . originalSettings ,
key ,
structuredClone ( valueToSet ) ,
) ;
saveSettings ( settingsFile ) ;
}
2026-02-03 14:53:31 -08:00
2025-05-02 08:15:46 -07:00
this . _merged = this . computeMergedSettings ( ) ;
2026-02-11 15:40:27 -08:00
this . _snapshot = this . computeSnapshot ( ) ;
2026-01-05 15:12:51 -08:00
coreEvents . emitSettingsChanged ( ) ;
2025-05-01 10:34:07 -07:00
}
2026-01-09 17:04:57 -05:00
2026-02-03 16:19:14 -05:00
setRemoteAdminSettings ( remoteSettings : AdminControlsSettings ) : void {
2026-01-09 17:04:57 -05:00
const admin : Settings [ 'admin' ] = { } ;
2026-02-03 16:19:14 -05:00
const { strictModeDisabled , mcpSetting , cliFeatureSetting } =
remoteSettings ;
2026-01-09 17:04:57 -05:00
2026-01-27 12:14:11 -05:00
if ( Object . keys ( remoteSettings ) . length === 0 ) {
this . _remoteAdminSettings = { admin } ;
this . _merged = this . computeMergedSettings ( ) ;
return ;
2026-01-09 17:04:57 -05:00
}
2026-02-03 16:19:14 -05:00
admin . secureModeEnabled = ! strictModeDisabled ;
2026-02-05 08:46:01 -05:00
admin . mcp = {
enabled : mcpSetting?.mcpEnabled ,
config : mcpSetting?.mcpConfig?.mcpServers ,
} ;
2026-01-27 12:14:11 -05:00
admin . extensions = {
2026-02-03 16:19:14 -05:00
enabled : cliFeatureSetting?.extensionsSetting?.extensionsEnabled ,
2026-01-27 12:14:11 -05:00
} ;
2026-01-28 10:53:05 -05:00
admin . skills = {
2026-02-03 16:19:14 -05:00
enabled : cliFeatureSetting?.unmanagedCapabilitiesEnabled ,
2026-01-28 10:53:05 -05:00
} ;
2026-01-20 11:57:47 -05:00
2026-01-09 17:04:57 -05:00
this . _remoteAdminSettings = { admin } ;
this . _merged = this . computeMergedSettings ( ) ;
}
2025-05-01 10:34:07 -07:00
}
2025-07-07 01:13:13 -04:00
function findEnvFile ( startDir : string ) : string | null {
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 ;
}
}
2026-02-03 17:08:10 -08:00
export function setUpCloudShellEnvironment (
envFilePath : string | null ,
isTrusted : boolean ,
isSandboxed : boolean ,
) : void {
2025-07-07 15:02:13 -07:00
// 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.
2026-02-03 17:08:10 -08:00
let value = 'cloudshell-gca' ;
2025-07-07 15:02:13 -07:00
if ( envFilePath && fs . existsSync ( envFilePath ) ) {
const envFileContent = fs . readFileSync ( envFilePath ) ;
const parsedEnv = dotenv . parse ( envFileContent ) ;
2025-08-17 12:43:21 -04:00
if ( parsedEnv [ 'GOOGLE_CLOUD_PROJECT' ] ) {
2025-07-07 15:02:13 -07:00
// .env file takes precedence in Cloud Shell
2026-02-03 17:08:10 -08:00
value = parsedEnv [ 'GOOGLE_CLOUD_PROJECT' ] ;
if ( ! isTrusted && isSandboxed ) {
value = sanitizeEnvVar ( value ) ;
}
2025-07-07 15:02:13 -07:00
}
}
2026-02-03 17:08:10 -08:00
process . env [ 'GOOGLE_CLOUD_PROJECT' ] = value ;
2025-07-07 15:02:13 -07:00
}
2026-02-03 00:54:10 -05:00
export function loadEnvironment (
settings : Settings ,
workspaceDir : string ,
isWorkspaceTrustedFn = isWorkspaceTrusted ,
) : void {
const envFilePath = findEnvFile ( workspaceDir ) ;
const trustResult = isWorkspaceTrustedFn ( settings , workspaceDir ) ;
2025-07-07 15:02:13 -07:00
2026-02-03 17:08:10 -08:00
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' ) ;
if ( trustResult . isTrusted !== true && ! isSandboxed ) {
2025-08-28 15:16:07 -04:00
return ;
}
2025-08-03 20:44:15 +02:00
// Cloud Shell environment variable handling
2025-08-17 12:43:21 -04:00
if ( process . env [ 'CLOUD_SHELL' ] === 'true' ) {
2026-02-03 17:08:10 -08:00
setUpCloudShellEnvironment ( envFilePath , isTrusted , isSandboxed ) ;
2025-07-07 15:02:13 -07:00
}
2025-07-07 01:13:13 -04:00
if ( envFilePath ) {
2025-08-03 20:44:15 +02:00
// 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 =
2025-08-28 13:52:25 -04:00
settings ? . advanced ? . excludedEnvVars || DEFAULT_EXCLUDED_ENV_VARS ;
2025-08-03 20:44:15 +02:00
const isProjectEnvFile = ! envFilePath . includes ( GEMINI_DIR ) ;
for ( const key in parsedEnv ) {
if ( Object . hasOwn ( parsedEnv , key ) ) {
2026-02-03 17:08:10 -08:00
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 ) ;
}
2025-08-03 20:44:15 +02:00
// 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 ) ) {
2026-02-03 17:08:10 -08:00
process . env [ key ] = value ;
2025-08-03 20:44:15 +02:00
}
}
}
} catch ( _e ) {
// Errors are ignored to match the behavior of `dotenv.config({ quiet: true })`.
}
2025-07-07 01:13:13 -04:00
}
}
2025-05-01 10:34:07 -07:00
/ * *
2025-05-02 08:15:46 -07:00
* Loads settings from user and workspace directories .
2025-05-01 10:34:07 -07:00
* Project settings override user settings .
* /
2025-09-03 10:41:53 -07:00
export function loadSettings (
workspaceDir : string = process . cwd ( ) ,
) : LoadedSettings {
2025-07-09 21:16:42 +00:00
let systemSettings : Settings = { } ;
2025-08-24 21:21:22 -07:00
let systemDefaultSettings : Settings = { } ;
2025-05-01 10:34:07 -07:00
let userSettings : Settings = { } ;
2025-05-31 11:10:52 -07:00
let workspaceSettings : Settings = { } ;
2025-06-06 09:56:45 -07:00
const settingsErrors : SettingsError [ ] = [ ] ;
2025-07-21 20:14:07 +00:00
const systemSettingsPath = getSystemSettingsPath ( ) ;
2025-08-24 21:21:22 -07:00
const systemDefaultsPath = getSystemDefaultsPath ( ) ;
2025-08-02 03:52:17 +05:30
2025-08-05 21:10:16 +02:00
// Resolve paths to their canonical representation to handle symlinks
2025-08-02 03:52:17 +05:30
const resolvedWorkspaceDir = path . resolve ( workspaceDir ) ;
const resolvedHomeDir = path . resolve ( homedir ( ) ) ;
let realWorkspaceDir = resolvedWorkspaceDir ;
try {
// fs.realpathSync gets the "true" path, resolving any symlinks
realWorkspaceDir = fs . realpathSync ( resolvedWorkspaceDir ) ;
} catch ( _e ) {
// This is okay. The path might not exist yet, and that's a valid state.
}
// We expect homedir to always exist and be resolvable.
const realHomeDir = fs . realpathSync ( resolvedHomeDir ) ;
2025-08-20 10:55:47 +09:00
const workspaceSettingsPath = new Storage (
workspaceDir ,
) . getWorkspaceSettingsPath ( ) ;
2025-08-03 20:44:15 +02:00
2026-01-09 14:34:23 -08:00
const load = ( filePath : string ) : { settings : Settings ; rawJson? : string } = > {
2025-08-02 03:52:17 +05:30
try {
2025-08-27 18:39:45 -07:00
if ( fs . existsSync ( filePath ) ) {
const content = fs . readFileSync ( filePath , 'utf-8' ) ;
const rawSettings : unknown = JSON . parse ( stripJsonComments ( content ) ) ;
if (
typeof rawSettings !== 'object' ||
rawSettings === null ||
Array . isArray ( rawSettings )
2025-08-02 03:52:17 +05:30
) {
2025-08-27 18:39:45 -07:00
settingsErrors . push ( {
message : 'Settings file is not a valid JSON object.' ,
path : filePath ,
2025-12-23 10:26:37 -08:00
severity : 'error' ,
2025-08-27 18:39:45 -07:00
} ) ;
2025-09-13 14:31:15 +08:00
return { settings : { } } ;
2025-08-27 18:39:45 -07:00
}
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-09 14:34:23 -08:00
const settingsObject = rawSettings as Record < string , unknown > ;
2025-12-12 14:30:34 +08:00
2026-01-09 14:34:23 -08:00
// Validate settings structure with Zod
2025-12-12 14:30:34 +08:00
const validationResult = validateSettings ( settingsObject ) ;
if ( ! validationResult . success && validationResult . error ) {
const errorMessage = formatValidationError (
validationResult . error ,
filePath ,
) ;
2025-12-23 10:26:37 -08:00
settingsErrors . push ( {
message : errorMessage ,
path : filePath ,
severity : 'warning' ,
} ) ;
2025-12-12 14:30:34 +08:00
}
2025-09-13 14:31:15 +08:00
return { settings : settingsObject as Settings , rawJson : content } ;
2025-05-31 11:10:52 -07:00
}
2025-08-02 03:52:17 +05:30
} catch ( error : unknown ) {
settingsErrors . push ( {
message : getErrorMessage ( error ) ,
2025-08-27 18:39:45 -07:00
path : filePath ,
2025-12-23 10:26:37 -08:00
severity : 'error' ,
2025-08-02 03:52:17 +05:30
} ) ;
2025-05-01 10:34:07 -07:00
}
2025-09-13 14:31:15 +08:00
return { settings : { } } ;
2025-08-27 18:39:45 -07:00
} ;
2026-01-09 14:34:23 -08:00
const systemResult = load ( systemSettingsPath ) ;
const systemDefaultsResult = load ( systemDefaultsPath ) ;
const userResult = load ( USER_SETTINGS_PATH ) ;
2025-08-27 18:39:45 -07:00
2025-09-13 14:31:15 +08:00
let workspaceResult : { settings : Settings ; rawJson? : string } = {
settings : { } as Settings ,
rawJson : undefined ,
} ;
2025-08-27 18:39:45 -07:00
if ( realWorkspaceDir !== realHomeDir ) {
2026-01-09 14:34:23 -08:00
workspaceResult = load ( workspaceSettingsPath ) ;
2025-08-27 18:39:45 -07:00
}
2025-09-13 14:31:15 +08:00
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 ) ;
2025-08-27 18:39:45 -07:00
// 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 ;
2025-05-01 10:34:07 -07:00
}
2025-08-21 00:38:12 -07:00
// For the initial trust check, we can only use user and system settings.
2025-09-03 19:23:25 -07:00
const initialTrustCheckSettings = customDeepMerge (
getMergeStrategyForPath ,
2026-02-03 14:53:31 -08:00
getDefaultsFromSchema ( ) ,
systemDefaultSettings ,
2025-09-03 19:23:25 -07:00
userSettings ,
2026-02-03 14:53:31 -08:00
systemSettings ,
2025-09-03 19:23:25 -07:00
) ;
2025-08-27 18:39:45 -07:00
const isTrusted =
2026-02-03 00:54:10 -05:00
isWorkspaceTrusted ( initialTrustCheckSettings as Settings , workspaceDir )
. isTrusted ? ? false ;
2025-08-21 00:38:12 -07:00
2025-08-19 13:07:42 -06:00
// Create a temporary merged settings object to pass to loadEnvironment.
const tempMergedSettings = mergeSettings (
systemSettings ,
2025-08-24 21:21:22 -07:00
systemDefaultSettings ,
2025-08-19 13:07:42 -06:00
userSettings ,
workspaceSettings ,
2025-08-21 00:38:12 -07:00
isTrusted ,
2025-08-19 13:07:42 -06:00
) ;
2025-11-05 11:36:07 -08:00
// loadEnvironment depends on settings so we have to create a temp version of
2025-08-19 13:07:42 -06:00
// the settings to avoid a cycle
2026-02-03 00:54:10 -05:00
loadEnvironment ( tempMergedSettings , workspaceDir ) ;
2025-08-19 13:07:42 -06:00
2025-12-23 10:26:37 -08:00
// Check for any fatal errors before proceeding
const fatalErrors = settingsErrors . filter ( ( e ) = > e . severity === 'error' ) ;
if ( fatalErrors . length > 0 ) {
const errorMessages = fatalErrors . map (
2025-09-03 10:41:53 -07:00
( error ) = > ` Error in ${ error . path } : ${ error . message } ` ,
) ;
throw new FatalConfigError (
` ${ errorMessages . join ( '\n' ) } \ nPlease fix the configuration file(s) and try again. ` ,
) ;
}
2026-01-16 23:33:49 +01:00
const loadedSettings = new LoadedSettings (
2025-07-09 21:16:42 +00:00
{
2025-07-21 20:14:07 +00:00
path : systemSettingsPath ,
2025-07-09 21:16:42 +00:00
settings : systemSettings ,
2025-09-13 14:31:15 +08:00
originalSettings : systemOriginalSettings ,
rawJson : systemResult.rawJson ,
2026-02-06 14:35:58 -05:00
readOnly : true ,
2025-07-09 21:16:42 +00:00
} ,
2025-08-24 21:21:22 -07:00
{
path : systemDefaultsPath ,
settings : systemDefaultSettings ,
2025-09-13 14:31:15 +08:00
originalSettings : systemDefaultsOriginalSettings ,
rawJson : systemDefaultsResult.rawJson ,
2026-02-06 14:35:58 -05:00
readOnly : true ,
2025-08-24 21:21:22 -07:00
} ,
2025-06-06 09:56:45 -07:00
{
path : USER_SETTINGS_PATH ,
settings : userSettings ,
2025-09-13 14:31:15 +08:00
originalSettings : userOriginalSettings ,
rawJson : userResult.rawJson ,
2026-02-06 14:35:58 -05:00
readOnly : false ,
2025-06-06 09:56:45 -07:00
} ,
{
path : workspaceSettingsPath ,
settings : workspaceSettings ,
2025-09-13 14:31:15 +08:00
originalSettings : workspaceOriginalSettings ,
rawJson : workspaceResult.rawJson ,
2026-02-06 14:35:58 -05:00
readOnly : false ,
2025-06-06 09:56:45 -07:00
} ,
2025-08-21 00:38:12 -07:00
isTrusted ,
2025-12-23 10:26:37 -08:00
settingsErrors ,
2025-05-01 10:34:07 -07:00
) ;
2026-01-16 23:33:49 +01:00
// Automatically migrate deprecated settings when loading.
migrateDeprecatedSettings ( loadedSettings ) ;
return loadedSettings ;
}
/ * *
* Migrates deprecated settings to their new counterparts .
*
* TODO : After a couple of weeks ( around early Feb 2026 ) , we should start removing
* the deprecated settings from the settings files by default .
*
* @returns true if any changes were made and need to be saved .
* /
export function migrateDeprecatedSettings (
loadedSettings : LoadedSettings ,
removeDeprecated = false ,
) : boolean {
let anyModified = false ;
2026-02-06 14:35:58 -05:00
const systemWarnings : Map < LoadableSettingScope , string [ ] > = new Map ( ) ;
2026-02-03 14:53:31 -08:00
2026-02-06 14:35:58 -05:00
/ * *
* Helper to migrate a boolean setting and track it if it ' s deprecated .
* /
2026-02-03 14:53:31 -08:00
const migrateBoolean = (
settings : Record < string , unknown > ,
oldKey : string ,
newKey : string ,
2026-02-06 14:35:58 -05:00
prefix : string ,
foundDeprecated? : string [ ] ,
2026-02-03 14:53:31 -08:00
) : boolean = > {
let modified = false ;
const oldValue = settings [ oldKey ] ;
const newValue = settings [ newKey ] ;
if ( typeof oldValue === 'boolean' ) {
2026-02-06 14:35:58 -05:00
if ( foundDeprecated ) {
foundDeprecated . push ( prefix ? ` ${ prefix } . ${ oldKey } ` : oldKey ) ;
}
2026-02-03 14:53:31 -08:00
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 ;
} ;
2026-01-16 23:33:49 +01:00
const processScope = ( scope : LoadableSettingScope ) = > {
2026-02-06 14:35:58 -05:00
const settingsFile = loadedSettings . forScope ( scope ) ;
const settings = settingsFile . settings ;
const foundDeprecated : string [ ] = [ ] ;
2026-01-16 23:33:49 +01:00
2026-02-03 14:53:31 -08:00
// Migrate general settings
2026-01-16 23:33:49 +01:00
const generalSettings = settings . general as
| Record < string , unknown >
| undefined ;
if ( generalSettings ) {
2026-02-03 14:53:31 -08:00
const newGeneral = { . . . generalSettings } ;
2026-01-16 23:33:49 +01:00
let modified = false ;
2026-02-03 14:53:31 -08:00
modified =
2026-02-06 14:35:58 -05:00
migrateBoolean (
newGeneral ,
'disableAutoUpdate' ,
'enableAutoUpdate' ,
'general' ,
foundDeprecated ,
) || modified ;
2026-02-03 14:53:31 -08:00
modified =
migrateBoolean (
newGeneral ,
'disableUpdateNag' ,
'enableAutoUpdateNotification' ,
2026-02-06 14:35:58 -05:00
'general' ,
foundDeprecated ,
2026-02-03 14:53:31 -08:00
) || modified ;
2026-01-16 23:33:49 +01:00
if ( modified ) {
loadedSettings . setValue ( scope , 'general' , newGeneral ) ;
2026-02-06 14:35:58 -05:00
if ( ! settingsFile . readOnly ) {
anyModified = true ;
}
2026-01-16 23:33:49 +01:00
}
}
// Migrate ui settings
2026-02-03 14:53:31 -08:00
const uiSettings = settings . ui as Record < string , unknown > | undefined ;
2026-01-16 23:33:49 +01:00
if ( uiSettings ) {
2026-02-03 14:53:31 -08:00
const newUi = { . . . uiSettings } ;
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-16 23:33:49 +01:00
const accessibilitySettings = newUi [ 'accessibility' ] as
| Record < string , unknown >
| undefined ;
2026-02-03 14:53:31 -08:00
if ( accessibilitySettings ) {
const newAccessibility = { . . . accessibilitySettings } ;
2026-01-16 23:33:49 +01:00
if (
2026-02-03 14:53:31 -08:00
migrateBoolean (
newAccessibility ,
'disableLoadingPhrases' ,
'enableLoadingPhrases' ,
2026-02-06 14:35:58 -05:00
'ui.accessibility' ,
foundDeprecated ,
2026-02-03 14:53:31 -08:00
)
2026-01-16 23:33:49 +01:00
) {
newUi [ 'accessibility' ] = newAccessibility ;
2026-02-03 14:53:31 -08:00
loadedSettings . setValue ( scope , 'ui' , newUi ) ;
2026-02-06 14:35:58 -05:00
if ( ! settingsFile . readOnly ) {
anyModified = true ;
}
2026-01-16 23:33:49 +01:00
}
}
}
// Migrate context settings
2026-02-03 14:53:31 -08:00
const contextSettings = settings . context as
| Record < string , unknown >
| undefined ;
2026-01-16 23:33:49 +01:00
if ( contextSettings ) {
2026-02-03 14:53:31 -08:00
const newContext = { . . . contextSettings } ;
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-16 23:33:49 +01:00
const fileFilteringSettings = newContext [ 'fileFiltering' ] as
| Record < string , unknown >
| undefined ;
2026-02-03 14:53:31 -08:00
if ( fileFilteringSettings ) {
const newFileFiltering = { . . . fileFilteringSettings } ;
if (
migrateBoolean (
newFileFiltering ,
'disableFuzzySearch' ,
'enableFuzzySearch' ,
2026-02-06 14:35:58 -05:00
'context.fileFiltering' ,
foundDeprecated ,
2026-02-03 14:53:31 -08:00
)
) {
2026-01-16 23:33:49 +01:00
newContext [ 'fileFiltering' ] = newFileFiltering ;
2026-02-03 14:53:31 -08:00
loadedSettings . setValue ( scope , 'context' , newContext ) ;
2026-02-06 14:35:58 -05:00
if ( ! settingsFile . readOnly ) {
anyModified = true ;
}
2026-01-16 23:33:49 +01:00
}
}
}
2026-01-22 12:59:47 -08:00
2026-02-10 08:07:04 -05:00
// Migrate tools settings
const toolsSettings = settings . tools as Record < string , unknown > | undefined ;
if ( toolsSettings ) {
if ( toolsSettings [ 'approvalMode' ] !== undefined ) {
foundDeprecated . push ( 'tools.approvalMode' ) ;
const generalSettings =
( settings . general as Record < string , unknown > | undefined ) || { } ;
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 ;
}
}
}
}
2026-01-22 12:59:47 -08:00
// Migrate experimental agent settings
2026-02-06 14:35:58 -05:00
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 ) ;
}
2026-01-16 23:33:49 +01:00
} ;
processScope ( SettingScope . User ) ;
processScope ( SettingScope . Workspace ) ;
2026-01-20 20:07:17 -08:00
processScope ( SettingScope . System ) ;
processScope ( SettingScope . SystemDefaults ) ;
2026-01-16 23:33:49 +01:00
2026-02-06 14:35:58 -05:00
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. ` ,
) ;
}
}
2026-01-16 23:33:49 +01:00
return anyModified ;
2025-05-01 10:34:07 -07:00
}
export function saveSettings ( settingsFile : SettingsFile ) : void {
try {
// Ensure the directory exists
const dirPath = path . dirname ( settingsFile . path ) ;
if ( ! fs . existsSync ( dirPath ) ) {
fs . mkdirSync ( dirPath , { recursive : true } ) ;
}
2026-01-09 14:34:23 -08:00
const settingsToSave = settingsFile . originalSettings ;
2025-08-27 18:39:45 -07:00
2025-09-13 14:31:15 +08:00
// Use the format-preserving update function
updateSettingsFilePreservingFormat (
2025-05-01 10:34:07 -07:00
settingsFile . path ,
2025-09-13 14:31:15 +08:00
settingsToSave as Record < string , unknown > ,
2025-05-01 10:34:07 -07:00
) ;
} catch ( error ) {
2025-10-23 14:14:14 -04:00
coreEvents . emitFeedback (
'error' ,
'There was an error saving your latest settings changes.' ,
error ,
) ;
2025-05-01 10:34:07 -07:00
}
}
2025-12-23 19:53:43 +05:30
export function saveModelChange (
loadedSettings : LoadedSettings ,
model : string ,
) : void {
try {
loadedSettings . setValue ( SettingScope . User , 'model.name' , model ) ;
} catch ( error ) {
coreEvents . emitFeedback (
'error' ,
'There was an error saving your preferred model.' ,
error ,
) ;
}
}
2026-01-22 12:59:47 -08:00
function migrateExperimentalSettings (
settings : Settings ,
loadedSettings : LoadedSettings ,
scope : LoadableSettingScope ,
removeDeprecated : boolean ,
2026-02-06 14:35:58 -05:00
foundDeprecated? : string [ ] ,
2026-01-22 12:59:47 -08:00
) : boolean {
const experimentalSettings = settings . experimental as
| Record < string , unknown >
| undefined ;
2026-02-06 14:35:58 -05:00
2026-01-22 12:59:47 -08:00
if ( experimentalSettings ) {
const agentsSettings = {
. . . ( settings . agents as Record < string , unknown > | undefined ) ,
} ;
const agentsOverrides = {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-22 12:59:47 -08:00
. . . ( ( agentsSettings [ 'overrides' ] as Record < string , unknown > ) || { } ) ,
} ;
let modified = false ;
2026-02-06 14:35:58 -05:00
const migrateExperimental = (
oldKey : string ,
migrateFn : ( oldValue : Record < string , unknown > ) = > void ,
) = > {
const old = experimentalSettings [ oldKey ] ;
if ( old ) {
foundDeprecated ? . push ( ` experimental. ${ oldKey } ` ) ;
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-02-06 14:35:58 -05:00
migrateFn ( old as Record < string , unknown > ) ;
modified = true ;
}
} ;
2026-01-22 12:59:47 -08:00
// Migrate codebaseInvestigatorSettings -> agents.overrides.codebase_investigator
2026-02-06 14:35:58 -05:00
migrateExperimental ( 'codebaseInvestigatorSettings' , ( old ) = > {
2026-01-22 12:59:47 -08:00
const override = {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-22 12:59:47 -08:00
. . . ( agentsOverrides [ 'codebase_investigator' ] as
| Record < string , unknown >
| undefined ) ,
} ;
if ( old [ 'enabled' ] !== undefined ) override [ 'enabled' ] = old [ 'enabled' ] ;
const runConfig = {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-22 12:59:47 -08:00
. . . ( override [ 'runConfig' ] as Record < string , unknown > | undefined ) ,
} ;
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 = {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-22 12:59:47 -08:00
. . . ( override [ 'modelConfig' ] as Record < string , unknown > | undefined ) ,
} ;
if ( old [ 'model' ] !== undefined ) modelConfig [ 'model' ] = old [ 'model' ] ;
if ( old [ 'thinkingBudget' ] !== undefined ) {
const generateContentConfig = {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-22 12:59:47 -08:00
. . . ( modelConfig [ 'generateContentConfig' ] as
| Record < string , unknown >
| undefined ) ,
} ;
const thinkingConfig = {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-22 12:59:47 -08:00
. . . ( generateContentConfig [ 'thinkingConfig' ] as
| Record < string , unknown >
| undefined ) ,
} ;
thinkingConfig [ 'thinkingBudget' ] = old [ 'thinkingBudget' ] ;
generateContentConfig [ 'thinkingConfig' ] = thinkingConfig ;
modelConfig [ 'generateContentConfig' ] = generateContentConfig ;
}
override [ 'modelConfig' ] = modelConfig ;
}
agentsOverrides [ 'codebase_investigator' ] = override ;
2026-02-06 14:35:58 -05:00
} ) ;
2026-01-22 12:59:47 -08:00
// Migrate cliHelpAgentSettings -> agents.overrides.cli_help
2026-02-06 14:35:58 -05:00
migrateExperimental ( 'cliHelpAgentSettings' , ( old ) = > {
2026-01-22 12:59:47 -08:00
const override = {
2026-02-10 00:10:15 +00:00
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
2026-01-22 12:59:47 -08:00
. . . ( agentsOverrides [ 'cli_help' ] as Record < string , unknown > | undefined ) ,
} ;
if ( old [ 'enabled' ] !== undefined ) override [ 'enabled' ] = old [ 'enabled' ] ;
agentsOverrides [ 'cli_help' ] = override ;
2026-02-06 14:35:58 -05:00
} ) ;
2026-01-22 12:59:47 -08:00
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 ;
}