Add system-wide settings config for administrators (#3498)

Co-authored-by: Jack Wotherspoon <jackwoth@google.com>
This commit is contained in:
christine betts 2025-07-09 21:16:42 +00:00 committed by GitHub
parent 063481faa4
commit da50a1eefb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 292 additions and 31 deletions

View File

@ -9,12 +9,13 @@ Configuration is applied in the following order of precedence (lower numbers are
1. **Default values:** Hardcoded defaults within the application. 1. **Default values:** Hardcoded defaults within the application.
2. **User settings file:** Global settings for the current user. 2. **User settings file:** Global settings for the current user.
3. **Project settings file:** Project-specific settings. 3. **Project settings file:** Project-specific settings.
4. **Environment variables:** System-wide or session-specific variables, potentially loaded from `.env` files. 4. **System settings file:** System-wide settings.
5. **Command-line arguments:** Values passed when launching the CLI. 5. **Environment variables:** System-wide or session-specific variables, potentially loaded from `.env` files.
6. **Command-line arguments:** Values passed when launching the CLI.
## The user settings file and project settings file ## Settings files
Gemini CLI uses `settings.json` files for persistent configuration. There are two locations for these files: Gemini CLI uses `settings.json` files for persistent configuration. There are three locations for these files:
- **User settings file:** - **User settings file:**
- **Location:** `~/.gemini/settings.json` (where `~` is your home directory). - **Location:** `~/.gemini/settings.json` (where `~` is your home directory).
@ -22,6 +23,9 @@ Gemini CLI uses `settings.json` files for persistent configuration. There are tw
- **Project settings file:** - **Project settings file:**
- **Location:** `.gemini/settings.json` within your project's root directory. - **Location:** `.gemini/settings.json` within your project's root directory.
- **Scope:** Applies only when running Gemini CLI from that specific project. Project settings override user settings. - **Scope:** Applies only when running Gemini CLI from that specific project. Project settings override user settings.
- **System settings file:**
- **Location:** `/etc/gemini-cli/settings.json` (Linux), `C:\ProgramData\gemini-cli\settings.json` (Windows) or `/Library/Application Support/GeminiCli/settings.json` (macOS).
- **Scope:** Applies to all Gemini CLI sessions on the system, for all users. System settings override user and project settings. May be useful for system administrators at enterprises to have controls over users' Gemini CLI setups.
**Note on environment variables in settings:** String values within your `settings.json` files can reference environment variables using either `$VAR_NAME` or `${VAR_NAME}` syntax. These variables will be automatically resolved when the settings are loaded. For example, if you have an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like this: `"apiKey": "$MY_API_TOKEN"`. **Note on environment variables in settings:** String values within your `settings.json` files can reference environment variables using either `$VAR_NAME` or `${VAR_NAME}` syntax. These variables will be automatically resolved when the settings are loaded. For example, if you have an environment variable `MY_API_TOKEN`, you could use it in `settings.json` like this: `"apiKey": "$MY_API_TOKEN"`.

View File

@ -168,6 +168,7 @@ async function parseArguments(): Promise<CliArgs> {
type: 'boolean', type: 'boolean',
description: 'List all available extensions and exit.', description: 'List all available extensions and exit.',
}) })
.version(await getCliVersion()) // This will enable the --version flag based on package.json .version(await getCliVersion()) // This will enable the --version flag based on package.json
.alias('v', 'version') .alias('v', 'version')
.help() .help()

View File

@ -13,6 +13,7 @@ vi.mock('os', async (importOriginal) => {
return { return {
...actualOs, ...actualOs,
homedir: vi.fn(() => '/mock/home/user'), homedir: vi.fn(() => '/mock/home/user'),
platform: vi.fn(() => 'linux'),
}; };
}); });
@ -45,6 +46,7 @@ import stripJsonComments from 'strip-json-comments'; // Will be mocked separatel
import { import {
loadSettings, loadSettings,
USER_SETTINGS_PATH, // This IS the mocked path. USER_SETTINGS_PATH, // This IS the mocked path.
SYSTEM_SETTINGS_PATH,
SETTINGS_DIRECTORY_NAME, // This is from the original module, but used by the mock. SETTINGS_DIRECTORY_NAME, // This is from the original module, but used by the mock.
SettingScope, SettingScope,
} from './settings.js'; } from './settings.js';
@ -90,12 +92,41 @@ describe('Settings Loading and Merging', () => {
describe('loadSettings', () => { describe('loadSettings', () => {
it('should load empty settings if no files exist', () => { it('should load empty settings if no files exist', () => {
const settings = loadSettings(MOCK_WORKSPACE_DIR); const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.system.settings).toEqual({});
expect(settings.user.settings).toEqual({}); expect(settings.user.settings).toEqual({});
expect(settings.workspace.settings).toEqual({}); expect(settings.workspace.settings).toEqual({});
expect(settings.merged).toEqual({}); expect(settings.merged).toEqual({});
expect(settings.errors.length).toBe(0); expect(settings.errors.length).toBe(0);
}); });
it('should load system settings if only system file exists', () => {
(mockFsExistsSync as Mock).mockImplementation(
(p: fs.PathLike) => p === SYSTEM_SETTINGS_PATH,
);
const systemSettingsContent = {
theme: 'system-default',
sandbox: false,
};
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === SYSTEM_SETTINGS_PATH)
return JSON.stringify(systemSettingsContent);
return '{}';
},
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(fs.readFileSync).toHaveBeenCalledWith(
SYSTEM_SETTINGS_PATH,
'utf-8',
);
expect(settings.system.settings).toEqual(systemSettingsContent);
expect(settings.user.settings).toEqual({});
expect(settings.workspace.settings).toEqual({});
expect(settings.merged).toEqual(systemSettingsContent);
});
it('should load user settings if only user file exists', () => { it('should load user settings if only user file exists', () => {
const expectedUserSettingsPath = USER_SETTINGS_PATH; // Use the path actually resolved by the (mocked) module const expectedUserSettingsPath = USER_SETTINGS_PATH; // Use the path actually resolved by the (mocked) module
@ -187,6 +218,50 @@ describe('Settings Loading and Merging', () => {
}); });
}); });
it('should merge system, user and workspace settings, with system taking precedence over workspace, and workspace over user', () => {
(mockFsExistsSync as Mock).mockReturnValue(true);
const systemSettingsContent = {
theme: 'system-theme',
sandbox: false,
telemetry: { enabled: false },
};
const userSettingsContent = {
theme: 'dark',
sandbox: true,
contextFileName: 'USER_CONTEXT.md',
};
const workspaceSettingsContent = {
sandbox: false,
coreTools: ['tool1'],
contextFileName: 'WORKSPACE_CONTEXT.md',
};
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === SYSTEM_SETTINGS_PATH)
return JSON.stringify(systemSettingsContent);
if (p === USER_SETTINGS_PATH)
return JSON.stringify(userSettingsContent);
if (p === MOCK_WORKSPACE_SETTINGS_PATH)
return JSON.stringify(workspaceSettingsContent);
return '';
},
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.system.settings).toEqual(systemSettingsContent);
expect(settings.user.settings).toEqual(userSettingsContent);
expect(settings.workspace.settings).toEqual(workspaceSettingsContent);
expect(settings.merged).toEqual({
theme: 'system-theme',
sandbox: false,
telemetry: { enabled: false },
coreTools: ['tool1'],
contextFileName: 'WORKSPACE_CONTEXT.md',
});
});
it('should handle contextFileName correctly when only in user settings', () => { it('should handle contextFileName correctly when only in user settings', () => {
(mockFsExistsSync as Mock).mockImplementation( (mockFsExistsSync as Mock).mockImplementation(
(p: fs.PathLike) => p === USER_SETTINGS_PATH, (p: fs.PathLike) => p === USER_SETTINGS_PATH,
@ -409,6 +484,50 @@ describe('Settings Loading and Merging', () => {
delete process.env.WORKSPACE_ENDPOINT; delete process.env.WORKSPACE_ENDPOINT;
}); });
it('should prioritize user env variables over workspace env variables if keys clash after resolution', () => {
const userSettingsContent = { configValue: '$SHARED_VAR' };
const workspaceSettingsContent = { configValue: '$SHARED_VAR' };
(mockFsExistsSync as Mock).mockReturnValue(true);
const originalSharedVar = process.env.SHARED_VAR;
// Temporarily delete to ensure a clean slate for the test's specific manipulations
delete process.env.SHARED_VAR;
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === USER_SETTINGS_PATH) {
process.env.SHARED_VAR = 'user_value_for_user_read'; // Set for user settings read
return JSON.stringify(userSettingsContent);
}
if (p === MOCK_WORKSPACE_SETTINGS_PATH) {
process.env.SHARED_VAR = 'workspace_value_for_workspace_read'; // Set for workspace settings read
return JSON.stringify(workspaceSettingsContent);
}
return '{}';
},
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.user.settings.configValue).toBe(
'user_value_for_user_read',
);
expect(settings.workspace.settings.configValue).toBe(
'workspace_value_for_workspace_read',
);
// Merged should take workspace's resolved value
expect(settings.merged.configValue).toBe(
'workspace_value_for_workspace_read',
);
// Restore original environment variable state
if (originalSharedVar !== undefined) {
process.env.SHARED_VAR = originalSharedVar;
} else {
delete process.env.SHARED_VAR; // Ensure it's deleted if it wasn't there before
}
});
it('should prioritize workspace env variables over user env variables if keys clash after resolution', () => { it('should prioritize workspace env variables over user env variables if keys clash after resolution', () => {
const userSettingsContent = { configValue: '$SHARED_VAR' }; const userSettingsContent = { configValue: '$SHARED_VAR' };
const workspaceSettingsContent = { configValue: '$SHARED_VAR' }; const workspaceSettingsContent = { configValue: '$SHARED_VAR' };
@ -453,6 +572,48 @@ describe('Settings Loading and Merging', () => {
} }
}); });
it('should prioritize system env variables over workspace env variables if keys clash after resolution', () => {
const workspaceSettingsContent = { configValue: '$SHARED_VAR' };
const systemSettingsContent = { configValue: '$SHARED_VAR' };
(mockFsExistsSync as Mock).mockReturnValue(true);
const originalSharedVar = process.env.SHARED_VAR;
// Temporarily delete to ensure a clean slate for the test's specific manipulations
delete process.env.SHARED_VAR;
(fs.readFileSync as Mock).mockImplementation(
(p: fs.PathOrFileDescriptor) => {
if (p === SYSTEM_SETTINGS_PATH) {
process.env.SHARED_VAR = 'system_value_for_system_read'; // Set for system settings read
return JSON.stringify(systemSettingsContent);
}
if (p === MOCK_WORKSPACE_SETTINGS_PATH) {
process.env.SHARED_VAR = 'workspace_value_for_workspace_read'; // Set for workspace settings read
return JSON.stringify(workspaceSettingsContent);
}
return '{}';
},
);
const settings = loadSettings(MOCK_WORKSPACE_DIR);
expect(settings.system.settings.configValue).toBe(
'system_value_for_system_read',
);
expect(settings.workspace.settings.configValue).toBe(
'workspace_value_for_workspace_read',
);
// Merged should take workspace's resolved value
expect(settings.merged.configValue).toBe('system_value_for_system_read');
// Restore original environment variable state
if (originalSharedVar !== undefined) {
process.env.SHARED_VAR = originalSharedVar;
} else {
delete process.env.SHARED_VAR; // Ensure it's deleted if it wasn't there before
}
});
it('should leave unresolved environment variables as is', () => { it('should leave unresolved environment variables as is', () => {
const userSettingsContent = { apiKey: '$UNDEFINED_VAR' }; const userSettingsContent = { apiKey: '$UNDEFINED_VAR' };
(mockFsExistsSync as Mock).mockImplementation( (mockFsExistsSync as Mock).mockImplementation(
@ -624,10 +785,10 @@ describe('Settings Loading and Merging', () => {
'utf-8', 'utf-8',
); );
// Workspace theme overrides user theme // System theme overrides user and workspace themes
loadedSettings.setValue(SettingScope.Workspace, 'theme', 'ocean'); loadedSettings.setValue(SettingScope.System, 'theme', 'ocean');
expect(loadedSettings.workspace.settings.theme).toBe('ocean'); expect(loadedSettings.system.settings.theme).toBe('ocean');
expect(loadedSettings.merged.theme).toBe('ocean'); expect(loadedSettings.merged.theme).toBe('ocean');
}); });
}); });

View File

@ -6,7 +6,7 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { homedir } from 'os'; import { homedir, platform } from 'os';
import * as dotenv from 'dotenv'; import * as dotenv from 'dotenv';
import { import {
MCPServerConfig, MCPServerConfig,
@ -24,9 +24,22 @@ export const SETTINGS_DIRECTORY_NAME = '.gemini';
export const USER_SETTINGS_DIR = path.join(homedir(), SETTINGS_DIRECTORY_NAME); export const USER_SETTINGS_DIR = path.join(homedir(), SETTINGS_DIRECTORY_NAME);
export const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, 'settings.json'); export const USER_SETTINGS_PATH = path.join(USER_SETTINGS_DIR, 'settings.json');
function getSystemSettingsPath(): string {
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 const SYSTEM_SETTINGS_PATH = getSystemSettingsPath();
export enum SettingScope { export enum SettingScope {
User = 'User', User = 'User',
Workspace = 'Workspace', Workspace = 'Workspace',
System = 'System',
} }
export interface CheckpointingSettings { export interface CheckpointingSettings {
@ -81,16 +94,19 @@ export interface SettingsFile {
} }
export class LoadedSettings { export class LoadedSettings {
constructor( constructor(
system: SettingsFile,
user: SettingsFile, user: SettingsFile,
workspace: SettingsFile, workspace: SettingsFile,
errors: SettingsError[], errors: SettingsError[],
) { ) {
this.system = system;
this.user = user; this.user = user;
this.workspace = workspace; this.workspace = workspace;
this.errors = errors; this.errors = errors;
this._merged = this.computeMergedSettings(); this._merged = this.computeMergedSettings();
} }
readonly system: SettingsFile;
readonly user: SettingsFile; readonly user: SettingsFile;
readonly workspace: SettingsFile; readonly workspace: SettingsFile;
readonly errors: SettingsError[]; readonly errors: SettingsError[];
@ -105,6 +121,7 @@ export class LoadedSettings {
return { return {
...this.user.settings, ...this.user.settings,
...this.workspace.settings, ...this.workspace.settings,
...this.system.settings,
}; };
} }
@ -114,6 +131,8 @@ export class LoadedSettings {
return this.user; return this.user;
case SettingScope.Workspace: case SettingScope.Workspace:
return this.workspace; return this.workspace;
case SettingScope.System:
return this.system;
default: default:
throw new Error(`Invalid scope: ${scope}`); throw new Error(`Invalid scope: ${scope}`);
} }
@ -243,10 +262,27 @@ export function loadEnvironment(): void {
*/ */
export function loadSettings(workspaceDir: string): LoadedSettings { export function loadSettings(workspaceDir: string): LoadedSettings {
loadEnvironment(); loadEnvironment();
let systemSettings: Settings = {};
let userSettings: Settings = {}; let userSettings: Settings = {};
let workspaceSettings: Settings = {}; let workspaceSettings: Settings = {};
const settingsErrors: SettingsError[] = []; const settingsErrors: SettingsError[] = [];
// Load system settings
try {
if (fs.existsSync(SYSTEM_SETTINGS_PATH)) {
const systemContent = fs.readFileSync(SYSTEM_SETTINGS_PATH, 'utf-8');
const parsedSystemSettings = JSON.parse(
stripJsonComments(systemContent),
) as Settings;
systemSettings = resolveEnvVarsInObject(parsedSystemSettings);
}
} catch (error: unknown) {
settingsErrors.push({
message: getErrorMessage(error),
path: SYSTEM_SETTINGS_PATH,
});
}
// Load user settings // Load user settings
try { try {
if (fs.existsSync(USER_SETTINGS_PATH)) { if (fs.existsSync(USER_SETTINGS_PATH)) {
@ -300,6 +336,10 @@ export function loadSettings(workspaceDir: string): LoadedSettings {
} }
return new LoadedSettings( return new LoadedSettings(
{
path: SYSTEM_SETTINGS_PATH,
settings: systemSettings,
},
{ {
path: USER_SETTINGS_PATH, path: USER_SETTINGS_PATH,
settings: userSettings, settings: userSettings,

View File

@ -112,7 +112,12 @@ describe('gemini.tsx main function', () => {
path: '/workspace/.gemini/settings.json', path: '/workspace/.gemini/settings.json',
settings: {}, settings: {},
}; };
const systemSettingsFile: SettingsFile = {
path: '/system/settings.json',
settings: {},
};
const mockLoadedSettings = new LoadedSettings( const mockLoadedSettings = new LoadedSettings(
systemSettingsFile,
userSettingsFile, userSettingsFile,
workspaceSettingsFile, workspaceSettingsFile,
[settingsError], [settingsError],

View File

@ -185,19 +185,30 @@ describe('App UI', () => {
let currentUnmount: (() => void) | undefined; let currentUnmount: (() => void) | undefined;
const createMockSettings = ( const createMockSettings = (
settings: Partial<Settings> = {}, settings: {
system?: Partial<Settings>;
user?: Partial<Settings>;
workspace?: Partial<Settings>;
} = {},
): LoadedSettings => { ): LoadedSettings => {
const systemSettingsFile: SettingsFile = {
path: '/system/settings.json',
settings: settings.system || {},
};
const userSettingsFile: SettingsFile = { const userSettingsFile: SettingsFile = {
path: '/user/settings.json', path: '/user/settings.json',
settings: {}, settings: settings.user || {},
}; };
const workspaceSettingsFile: SettingsFile = { const workspaceSettingsFile: SettingsFile = {
path: '/workspace/.gemini/settings.json', path: '/workspace/.gemini/settings.json',
settings: { settings: settings.workspace || {},
...settings,
},
}; };
return new LoadedSettings(userSettingsFile, workspaceSettingsFile, []); return new LoadedSettings(
systemSettingsFile,
userSettingsFile,
workspaceSettingsFile,
[],
);
}; };
beforeEach(() => { beforeEach(() => {
@ -222,7 +233,7 @@ describe('App UI', () => {
mockConfig.getShowMemoryUsage.mockReturnValue(false); // Default for most tests mockConfig.getShowMemoryUsage.mockReturnValue(false); // Default for most tests
// Ensure a theme is set so the theme dialog does not appear. // Ensure a theme is set so the theme dialog does not appear.
mockSettings = createMockSettings({ theme: 'Default' }); mockSettings = createMockSettings({ workspace: { theme: 'Default' } });
}); });
afterEach(() => { afterEach(() => {
@ -268,8 +279,7 @@ describe('App UI', () => {
it('should display custom contextFileName in footer when set and count is 1', async () => { it('should display custom contextFileName in footer when set and count is 1', async () => {
mockSettings = createMockSettings({ mockSettings = createMockSettings({
contextFileName: 'AGENTS.md', workspace: { contextFileName: 'AGENTS.md', theme: 'Default' },
theme: 'Default',
}); });
mockConfig.getGeminiMdFileCount.mockReturnValue(1); mockConfig.getGeminiMdFileCount.mockReturnValue(1);
mockConfig.getDebugMode.mockReturnValue(false); mockConfig.getDebugMode.mockReturnValue(false);
@ -288,8 +298,10 @@ describe('App UI', () => {
it('should display a generic message when multiple context files with different names are provided', async () => { it('should display a generic message when multiple context files with different names are provided', async () => {
mockSettings = createMockSettings({ mockSettings = createMockSettings({
contextFileName: ['AGENTS.md', 'CONTEXT.md'], workspace: {
theme: 'Default', contextFileName: ['AGENTS.md', 'CONTEXT.md'],
theme: 'Default',
},
}); });
mockConfig.getGeminiMdFileCount.mockReturnValue(2); mockConfig.getGeminiMdFileCount.mockReturnValue(2);
mockConfig.getDebugMode.mockReturnValue(false); mockConfig.getDebugMode.mockReturnValue(false);
@ -308,8 +320,7 @@ describe('App UI', () => {
it('should display custom contextFileName with plural when set and count is > 1', async () => { it('should display custom contextFileName with plural when set and count is > 1', async () => {
mockSettings = createMockSettings({ mockSettings = createMockSettings({
contextFileName: 'MY_NOTES.TXT', workspace: { contextFileName: 'MY_NOTES.TXT', theme: 'Default' },
theme: 'Default',
}); });
mockConfig.getGeminiMdFileCount.mockReturnValue(3); mockConfig.getGeminiMdFileCount.mockReturnValue(3);
mockConfig.getDebugMode.mockReturnValue(false); mockConfig.getDebugMode.mockReturnValue(false);
@ -328,8 +339,7 @@ describe('App UI', () => {
it('should not display context file message if count is 0, even if contextFileName is set', async () => { it('should not display context file message if count is 0, even if contextFileName is set', async () => {
mockSettings = createMockSettings({ mockSettings = createMockSettings({
contextFileName: 'ANY_FILE.MD', workspace: { contextFileName: 'ANY_FILE.MD', theme: 'Default' },
theme: 'Default',
}); });
mockConfig.getGeminiMdFileCount.mockReturnValue(0); mockConfig.getGeminiMdFileCount.mockReturnValue(0);
mockConfig.getDebugMode.mockReturnValue(false); mockConfig.getDebugMode.mockReturnValue(false);
@ -399,7 +409,9 @@ describe('App UI', () => {
it('should not display Tips component when hideTips is true', async () => { it('should not display Tips component when hideTips is true', async () => {
mockSettings = createMockSettings({ mockSettings = createMockSettings({
hideTips: true, workspace: {
hideTips: true,
},
}); });
const { unmount } = render( const { unmount } = render(
@ -413,6 +425,24 @@ describe('App UI', () => {
expect(vi.mocked(Tips)).not.toHaveBeenCalled(); expect(vi.mocked(Tips)).not.toHaveBeenCalled();
}); });
it('should show tips if system says show, but workspace and user settings say hide', async () => {
mockSettings = createMockSettings({
system: { hideTips: false },
user: { hideTips: true },
workspace: { hideTips: true },
});
const { unmount } = render(
<App
config={mockConfig as unknown as ServerConfig}
settings={mockSettings}
/>,
);
currentUnmount = unmount;
await Promise.resolve();
expect(vi.mocked(Tips)).toHaveBeenCalled();
});
describe('when no theme is set', () => { describe('when no theme is set', () => {
let originalNoColor: string | undefined; let originalNoColor: string | undefined;

View File

@ -29,6 +29,10 @@ describe('AuthDialog', () => {
process.env.GEMINI_API_KEY = ''; process.env.GEMINI_API_KEY = '';
const settings: LoadedSettings = new LoadedSettings( const settings: LoadedSettings = new LoadedSettings(
{
settings: {},
path: '',
},
{ {
settings: { settings: {
selectedAuthType: AuthType.USE_GEMINI, selectedAuthType: AuthType.USE_GEMINI,
@ -86,6 +90,12 @@ describe('AuthDialog', () => {
settings: {}, settings: {},
path: '', path: '',
}, },
{
settings: {
selectedAuthType: undefined,
},
path: '',
},
{ {
settings: {}, settings: {},
path: '', path: '',
@ -147,6 +157,10 @@ describe('AuthDialog', () => {
it('should allow exiting when auth method is already selected', async () => { it('should allow exiting when auth method is already selected', async () => {
const onSelect = vi.fn(); const onSelect = vi.fn();
const settings: LoadedSettings = new LoadedSettings( const settings: LoadedSettings = new LoadedSettings(
{
settings: {},
path: '',
},
{ {
settings: { settings: {
selectedAuthType: AuthType.USE_GEMINI, selectedAuthType: AuthType.USE_GEMINI,

View File

@ -57,6 +57,7 @@ export function ThemeDialog({
const scopeItems = [ const scopeItems = [
{ label: 'User Settings', value: SettingScope.User }, { label: 'User Settings', value: SettingScope.User },
{ label: 'Workspace Settings', value: SettingScope.Workspace }, { label: 'Workspace Settings', value: SettingScope.Workspace },
{ label: 'System Settings', value: SettingScope.System },
]; ];
const handleThemeSelect = (themeName: string) => { const handleThemeSelect = (themeName: string) => {
@ -86,16 +87,21 @@ export function ThemeDialog({
} }
}); });
const otherScopes = Object.values(SettingScope).filter(
(scope) => scope !== selectedScope,
);
const modifiedInOtherScopes = otherScopes.filter(
(scope) => settings.forScope(scope).settings.theme !== undefined,
);
let otherScopeModifiedMessage = ''; let otherScopeModifiedMessage = '';
const otherScope = if (modifiedInOtherScopes.length > 0) {
selectedScope === SettingScope.User const modifiedScopesStr = modifiedInOtherScopes.join(', ');
? SettingScope.Workspace
: SettingScope.User;
if (settings.forScope(otherScope).settings.theme !== undefined) {
otherScopeModifiedMessage = otherScopeModifiedMessage =
settings.forScope(selectedScope).settings.theme !== undefined settings.forScope(selectedScope).settings.theme !== undefined
? `(Also modified in ${otherScope})` ? `(Also modified in ${modifiedScopesStr})`
: `(Modified in ${otherScope})`; : `(Modified in ${modifiedScopesStr})`;
} }
// Constants for calculating preview pane layout. // Constants for calculating preview pane layout.

View File