mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 18:14:29 -07:00
feat(settings): Add support for settings enum options (#7719)
This commit is contained in:
@@ -22,17 +22,17 @@ import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
import {
|
||||
type Settings,
|
||||
type MemoryImportFormat,
|
||||
SETTINGS_SCHEMA,
|
||||
type MergeStrategy,
|
||||
type SettingsSchema,
|
||||
type SettingDefinition,
|
||||
getSettingsSchema,
|
||||
} from './settingsSchema.js';
|
||||
import { resolveEnvVarsInObject } from '../utils/envVarResolver.js';
|
||||
import { customDeepMerge } from '../utils/deepMerge.js';
|
||||
|
||||
function getMergeStrategyForPath(path: string[]): MergeStrategy | undefined {
|
||||
let current: SettingDefinition | undefined = undefined;
|
||||
let currentSchema: SettingsSchema | undefined = SETTINGS_SCHEMA;
|
||||
let currentSchema: SettingsSchema | undefined = getSettingsSchema();
|
||||
|
||||
for (const key of path) {
|
||||
if (!currentSchema || !currentSchema[key]) {
|
||||
|
||||
@@ -5,13 +5,17 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { Settings } from './settingsSchema.js';
|
||||
import { SETTINGS_SCHEMA } from './settingsSchema.js';
|
||||
import {
|
||||
getSettingsSchema,
|
||||
type SettingDefinition,
|
||||
type Settings,
|
||||
type SettingsSchema,
|
||||
} from './settingsSchema.js';
|
||||
|
||||
describe('SettingsSchema', () => {
|
||||
describe('SETTINGS_SCHEMA', () => {
|
||||
describe('getSettingsSchema', () => {
|
||||
it('should contain all expected top-level settings', () => {
|
||||
const expectedSettings = [
|
||||
const expectedSettings: Array<keyof Settings> = [
|
||||
'mcpServers',
|
||||
'general',
|
||||
'ui',
|
||||
@@ -27,14 +31,12 @@ describe('SettingsSchema', () => {
|
||||
];
|
||||
|
||||
expectedSettings.forEach((setting) => {
|
||||
expect(
|
||||
SETTINGS_SCHEMA[setting as keyof typeof SETTINGS_SCHEMA],
|
||||
).toBeDefined();
|
||||
expect(getSettingsSchema()[setting as keyof Settings]).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have correct structure for each setting', () => {
|
||||
Object.entries(SETTINGS_SCHEMA).forEach(([_key, definition]) => {
|
||||
Object.entries(getSettingsSchema()).forEach(([_key, definition]) => {
|
||||
expect(definition).toHaveProperty('type');
|
||||
expect(definition).toHaveProperty('label');
|
||||
expect(definition).toHaveProperty('category');
|
||||
@@ -48,7 +50,7 @@ describe('SettingsSchema', () => {
|
||||
});
|
||||
|
||||
it('should have correct nested setting structure', () => {
|
||||
const nestedSettings = [
|
||||
const nestedSettings: Array<keyof Settings> = [
|
||||
'general',
|
||||
'ui',
|
||||
'ide',
|
||||
@@ -62,11 +64,9 @@ describe('SettingsSchema', () => {
|
||||
];
|
||||
|
||||
nestedSettings.forEach((setting) => {
|
||||
const definition = SETTINGS_SCHEMA[
|
||||
setting as keyof typeof SETTINGS_SCHEMA
|
||||
] as (typeof SETTINGS_SCHEMA)[keyof typeof SETTINGS_SCHEMA] & {
|
||||
properties: unknown;
|
||||
};
|
||||
const definition = getSettingsSchema()[
|
||||
setting as keyof Settings
|
||||
] as SettingDefinition;
|
||||
expect(definition.type).toBe('object');
|
||||
expect(definition.properties).toBeDefined();
|
||||
expect(typeof definition.properties).toBe('object');
|
||||
@@ -75,35 +75,36 @@ describe('SettingsSchema', () => {
|
||||
|
||||
it('should have accessibility nested properties', () => {
|
||||
expect(
|
||||
SETTINGS_SCHEMA.ui?.properties?.accessibility?.properties,
|
||||
getSettingsSchema().ui?.properties?.accessibility?.properties,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
SETTINGS_SCHEMA.ui?.properties?.accessibility.properties
|
||||
getSettingsSchema().ui?.properties?.accessibility.properties
|
||||
?.disableLoadingPhrases.type,
|
||||
).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should have checkpointing nested properties', () => {
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general?.properties?.checkpointing.properties?.enabled,
|
||||
getSettingsSchema().general?.properties?.checkpointing.properties
|
||||
?.enabled,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general?.properties?.checkpointing.properties?.enabled
|
||||
.type,
|
||||
getSettingsSchema().general?.properties?.checkpointing.properties
|
||||
?.enabled.type,
|
||||
).toBe('boolean');
|
||||
});
|
||||
|
||||
it('should have fileFiltering nested properties', () => {
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context.properties.fileFiltering.properties
|
||||
getSettingsSchema().context.properties.fileFiltering.properties
|
||||
?.respectGitIgnore,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context.properties.fileFiltering.properties
|
||||
getSettingsSchema().context.properties.fileFiltering.properties
|
||||
?.respectGeminiIgnore,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context.properties.fileFiltering.properties
|
||||
getSettingsSchema().context.properties.fileFiltering.properties
|
||||
?.enableRecursiveFileSearch,
|
||||
).toBeDefined();
|
||||
});
|
||||
@@ -112,7 +113,7 @@ describe('SettingsSchema', () => {
|
||||
const categories = new Set();
|
||||
|
||||
// Collect categories from top-level settings
|
||||
Object.values(SETTINGS_SCHEMA).forEach((definition) => {
|
||||
Object.values(getSettingsSchema()).forEach((definition) => {
|
||||
categories.add(definition.category);
|
||||
// Also collect from nested properties
|
||||
const defWithProps = definition as typeof definition & {
|
||||
@@ -137,74 +138,80 @@ describe('SettingsSchema', () => {
|
||||
});
|
||||
|
||||
it('should have consistent default values for boolean settings', () => {
|
||||
const checkBooleanDefaults = (schema: Record<string, unknown>) => {
|
||||
Object.entries(schema).forEach(
|
||||
([_key, definition]: [string, unknown]) => {
|
||||
const def = definition as {
|
||||
type?: string;
|
||||
default?: unknown;
|
||||
properties?: Record<string, unknown>;
|
||||
};
|
||||
if (def.type === 'boolean') {
|
||||
// Boolean settings can have boolean or undefined defaults (for optional settings)
|
||||
expect(['boolean', 'undefined']).toContain(typeof def.default);
|
||||
}
|
||||
if (def.properties) {
|
||||
checkBooleanDefaults(def.properties);
|
||||
}
|
||||
},
|
||||
);
|
||||
const checkBooleanDefaults = (schema: SettingsSchema) => {
|
||||
Object.entries(schema).forEach(([, definition]) => {
|
||||
const def = definition as SettingDefinition;
|
||||
if (def.type === 'boolean') {
|
||||
// Boolean settings can have boolean or undefined defaults (for optional settings)
|
||||
expect(['boolean', 'undefined']).toContain(typeof def.default);
|
||||
}
|
||||
if (def.properties) {
|
||||
checkBooleanDefaults(def.properties);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
checkBooleanDefaults(SETTINGS_SCHEMA as Record<string, unknown>);
|
||||
checkBooleanDefaults(getSettingsSchema() as SettingsSchema);
|
||||
});
|
||||
|
||||
it('should have showInDialog property configured', () => {
|
||||
// Check that user-facing settings are marked for dialog display
|
||||
expect(SETTINGS_SCHEMA.ui.properties.showMemoryUsage.showInDialog).toBe(
|
||||
true,
|
||||
);
|
||||
expect(SETTINGS_SCHEMA.general.properties.vimMode.showInDialog).toBe(
|
||||
true,
|
||||
);
|
||||
expect(SETTINGS_SCHEMA.ide.properties.enabled.showInDialog).toBe(true);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.disableAutoUpdate.showInDialog,
|
||||
getSettingsSchema().ui.properties.showMemoryUsage.showInDialog,
|
||||
).toBe(true);
|
||||
expect(SETTINGS_SCHEMA.ui.properties.hideWindowTitle.showInDialog).toBe(
|
||||
expect(getSettingsSchema().general.properties.vimMode.showInDialog).toBe(
|
||||
true,
|
||||
);
|
||||
expect(getSettingsSchema().ide.properties.enabled.showInDialog).toBe(
|
||||
true,
|
||||
);
|
||||
expect(SETTINGS_SCHEMA.ui.properties.hideTips.showInDialog).toBe(true);
|
||||
expect(SETTINGS_SCHEMA.ui.properties.hideBanner.showInDialog).toBe(true);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.privacy.properties.usageStatisticsEnabled.showInDialog,
|
||||
getSettingsSchema().general.properties.disableAutoUpdate.showInDialog,
|
||||
).toBe(true);
|
||||
expect(
|
||||
getSettingsSchema().ui.properties.hideWindowTitle.showInDialog,
|
||||
).toBe(true);
|
||||
expect(getSettingsSchema().ui.properties.hideTips.showInDialog).toBe(
|
||||
true,
|
||||
);
|
||||
expect(getSettingsSchema().ui.properties.hideBanner.showInDialog).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
getSettingsSchema().privacy.properties.usageStatisticsEnabled
|
||||
.showInDialog,
|
||||
).toBe(false);
|
||||
|
||||
// Check that advanced settings are hidden from dialog
|
||||
expect(SETTINGS_SCHEMA.security.properties.auth.showInDialog).toBe(false);
|
||||
expect(SETTINGS_SCHEMA.tools.properties.core.showInDialog).toBe(false);
|
||||
expect(SETTINGS_SCHEMA.mcpServers.showInDialog).toBe(false);
|
||||
expect(SETTINGS_SCHEMA.telemetry.showInDialog).toBe(false);
|
||||
expect(getSettingsSchema().security.properties.auth.showInDialog).toBe(
|
||||
false,
|
||||
);
|
||||
expect(getSettingsSchema().tools.properties.core.showInDialog).toBe(
|
||||
false,
|
||||
);
|
||||
expect(getSettingsSchema().mcpServers.showInDialog).toBe(false);
|
||||
expect(getSettingsSchema().telemetry.showInDialog).toBe(false);
|
||||
|
||||
// Check that some settings are appropriately hidden
|
||||
expect(SETTINGS_SCHEMA.ui.properties.theme.showInDialog).toBe(false); // Changed to false
|
||||
expect(SETTINGS_SCHEMA.ui.properties.customThemes.showInDialog).toBe(
|
||||
expect(getSettingsSchema().ui.properties.theme.showInDialog).toBe(false); // Changed to false
|
||||
expect(getSettingsSchema().ui.properties.customThemes.showInDialog).toBe(
|
||||
false,
|
||||
); // Managed via theme editor
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.checkpointing.showInDialog,
|
||||
getSettingsSchema().general.properties.checkpointing.showInDialog,
|
||||
).toBe(false); // Experimental feature
|
||||
expect(SETTINGS_SCHEMA.ui.properties.accessibility.showInDialog).toBe(
|
||||
expect(getSettingsSchema().ui.properties.accessibility.showInDialog).toBe(
|
||||
false,
|
||||
); // Changed to false
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context.properties.fileFiltering.showInDialog,
|
||||
getSettingsSchema().context.properties.fileFiltering.showInDialog,
|
||||
).toBe(false); // Changed to false
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.preferredEditor.showInDialog,
|
||||
getSettingsSchema().general.properties.preferredEditor.showInDialog,
|
||||
).toBe(false); // Changed to false
|
||||
expect(
|
||||
SETTINGS_SCHEMA.advanced.properties.autoConfigureMemory.showInDialog,
|
||||
getSettingsSchema().advanced.properties.autoConfigureMemory
|
||||
.showInDialog,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
@@ -228,80 +235,84 @@ describe('SettingsSchema', () => {
|
||||
|
||||
it('should have includeDirectories setting in schema', () => {
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context?.properties.includeDirectories,
|
||||
getSettingsSchema().context?.properties.includeDirectories,
|
||||
).toBeDefined();
|
||||
expect(SETTINGS_SCHEMA.context?.properties.includeDirectories.type).toBe(
|
||||
'array',
|
||||
);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context?.properties.includeDirectories.category,
|
||||
getSettingsSchema().context?.properties.includeDirectories.type,
|
||||
).toBe('array');
|
||||
expect(
|
||||
getSettingsSchema().context?.properties.includeDirectories.category,
|
||||
).toBe('Context');
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context?.properties.includeDirectories.default,
|
||||
getSettingsSchema().context?.properties.includeDirectories.default,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('should have loadMemoryFromIncludeDirectories setting in schema', () => {
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories,
|
||||
getSettingsSchema().context?.properties
|
||||
.loadMemoryFromIncludeDirectories,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories
|
||||
getSettingsSchema().context?.properties.loadMemoryFromIncludeDirectories
|
||||
.type,
|
||||
).toBe('boolean');
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories
|
||||
getSettingsSchema().context?.properties.loadMemoryFromIncludeDirectories
|
||||
.category,
|
||||
).toBe('Context');
|
||||
expect(
|
||||
SETTINGS_SCHEMA.context?.properties.loadMemoryFromIncludeDirectories
|
||||
getSettingsSchema().context?.properties.loadMemoryFromIncludeDirectories
|
||||
.default,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should have folderTrustFeature setting in schema', () => {
|
||||
expect(
|
||||
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled,
|
||||
getSettingsSchema().security.properties.folderTrust.properties.enabled,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled.type,
|
||||
getSettingsSchema().security.properties.folderTrust.properties.enabled
|
||||
.type,
|
||||
).toBe('boolean');
|
||||
expect(
|
||||
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled
|
||||
getSettingsSchema().security.properties.folderTrust.properties.enabled
|
||||
.category,
|
||||
).toBe('Security');
|
||||
expect(
|
||||
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled
|
||||
getSettingsSchema().security.properties.folderTrust.properties.enabled
|
||||
.default,
|
||||
).toBe(false);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.security.properties.folderTrust.properties.enabled
|
||||
getSettingsSchema().security.properties.folderTrust.properties.enabled
|
||||
.showInDialog,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should have debugKeystrokeLogging setting in schema', () => {
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging,
|
||||
getSettingsSchema().general.properties.debugKeystrokeLogging,
|
||||
).toBeDefined();
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.type,
|
||||
getSettingsSchema().general.properties.debugKeystrokeLogging.type,
|
||||
).toBe('boolean');
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.category,
|
||||
getSettingsSchema().general.properties.debugKeystrokeLogging.category,
|
||||
).toBe('General');
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.default,
|
||||
getSettingsSchema().general.properties.debugKeystrokeLogging.default,
|
||||
).toBe(false);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging
|
||||
getSettingsSchema().general.properties.debugKeystrokeLogging
|
||||
.requiresRestart,
|
||||
).toBe(false);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.showInDialog,
|
||||
getSettingsSchema().general.properties.debugKeystrokeLogging
|
||||
.showInDialog,
|
||||
).toBe(true);
|
||||
expect(
|
||||
SETTINGS_SCHEMA.general.properties.debugKeystrokeLogging.description,
|
||||
getSettingsSchema().general.properties.debugKeystrokeLogging
|
||||
.description,
|
||||
).toBe('Enable debug logging of keystrokes to the console.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,37 @@ import {
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { CustomTheme } from '../ui/themes/theme.js';
|
||||
|
||||
export type SettingsType =
|
||||
| 'boolean'
|
||||
| 'string'
|
||||
| 'number'
|
||||
| 'array'
|
||||
| 'object'
|
||||
| 'enum';
|
||||
|
||||
export type SettingsValue =
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
| string[]
|
||||
| object
|
||||
| undefined;
|
||||
|
||||
/**
|
||||
* Setting datatypes that "toggle" through a fixed list of options
|
||||
* (e.g. an enum or true/false) rather than allowing for free form input
|
||||
* (like a number or string).
|
||||
*/
|
||||
export const TOGGLE_TYPES: ReadonlySet<SettingsType | undefined> = new Set([
|
||||
'boolean',
|
||||
'enum',
|
||||
]);
|
||||
|
||||
interface SettingEnumOption {
|
||||
value: string | number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export enum MergeStrategy {
|
||||
// Replace the old value with the new value. This is the default.
|
||||
REPLACE = 'replace',
|
||||
@@ -29,11 +60,11 @@ export enum MergeStrategy {
|
||||
}
|
||||
|
||||
export interface SettingDefinition {
|
||||
type: 'boolean' | 'string' | 'number' | 'array' | 'object';
|
||||
type: SettingsType;
|
||||
label: string;
|
||||
category: string;
|
||||
requiresRestart: boolean;
|
||||
default: boolean | string | number | string[] | object | undefined;
|
||||
default: SettingsValue;
|
||||
description?: string;
|
||||
parentKey?: string;
|
||||
childKey?: string;
|
||||
@@ -41,6 +72,8 @@ export interface SettingDefinition {
|
||||
properties?: SettingsSchema;
|
||||
showInDialog?: boolean;
|
||||
mergeStrategy?: MergeStrategy;
|
||||
/** Enum type options */
|
||||
options?: readonly SettingEnumOption[];
|
||||
}
|
||||
|
||||
export interface SettingsSchema {
|
||||
@@ -55,7 +88,7 @@ export type DnsResolutionOrder = 'ipv4first' | 'verbatim';
|
||||
* The structure of this object defines the structure of the `Settings` type.
|
||||
* `as const` is crucial for TypeScript to infer the most specific types possible.
|
||||
*/
|
||||
export const SETTINGS_SCHEMA = {
|
||||
const SETTINGS_SCHEMA = {
|
||||
// Maintained for compatibility/criticality
|
||||
mcpServers: {
|
||||
type: 'object',
|
||||
@@ -900,7 +933,13 @@ export const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
} as const satisfies SettingsSchema;
|
||||
|
||||
export type SettingsSchemaType = typeof SETTINGS_SCHEMA;
|
||||
|
||||
export function getSettingsSchema(): SettingsSchemaType {
|
||||
return SETTINGS_SCHEMA;
|
||||
}
|
||||
|
||||
type InferSettings<T extends SettingsSchema> = {
|
||||
-readonly [K in keyof T]?: T[K] extends { properties: SettingsSchema }
|
||||
@@ -910,7 +949,7 @@ type InferSettings<T extends SettingsSchema> = {
|
||||
: T[K]['default'];
|
||||
};
|
||||
|
||||
export type Settings = InferSettings<typeof SETTINGS_SCHEMA>;
|
||||
export type Settings = InferSettings<SettingsSchemaType>;
|
||||
|
||||
export interface FooterSettings {
|
||||
hideCWD?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user