feat(settings): Add support for settings enum options (#7719)

This commit is contained in:
Richie Foreman
2025-09-08 10:01:18 -04:00
committed by GitHub
parent 4693137b82
commit 009c24a4b8
7 changed files with 887 additions and 399 deletions
+2 -2
View File
@@ -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]) {
+97 -86
View File
@@ -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.');
});
});
+44 -5
View File
@@ -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;