mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -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;
|
||||
|
||||
@@ -25,14 +25,31 @@ import { render } from 'ink-testing-library';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { SettingsDialog } from './SettingsDialog.js';
|
||||
import { LoadedSettings } from '../../config/settings.js';
|
||||
import { LoadedSettings, SettingScope } from '../../config/settings.js';
|
||||
import { VimModeProvider } from '../contexts/VimModeContext.js';
|
||||
import { KeypressProvider } from '../contexts/KeypressContext.js';
|
||||
import { act } from 'react';
|
||||
import { saveModifiedSettings, TEST_ONLY } from '../../utils/settingsUtils.js';
|
||||
import {
|
||||
getSettingsSchema,
|
||||
type SettingDefinition,
|
||||
type SettingsSchemaType,
|
||||
} from '../../config/settingsSchema.js';
|
||||
|
||||
// Mock the VimModeContext
|
||||
const mockToggleVimEnabled = vi.fn();
|
||||
const mockSetVimMode = vi.fn();
|
||||
|
||||
enum TerminalKeys {
|
||||
ENTER = '\u000D',
|
||||
TAB = '\t',
|
||||
UP_ARROW = '\u001B[A',
|
||||
DOWN_ARROW = '\u001B[B',
|
||||
LEFT_ARROW = '\u001B[D',
|
||||
RIGHT_ARROW = '\u001B[C',
|
||||
ESCAPE = '\u001B',
|
||||
}
|
||||
|
||||
const createMockSettings = (
|
||||
userSettings = {},
|
||||
systemSettings = {},
|
||||
@@ -67,26 +84,12 @@ const createMockSettings = (
|
||||
new Set(),
|
||||
);
|
||||
|
||||
vi.mock('../contexts/SettingsContext.js', async () => {
|
||||
const actual = await vi.importActual('../contexts/SettingsContext.js');
|
||||
let settings = createMockSettings({ 'a.string.setting': 'initial' });
|
||||
vi.mock('../../config/settingsSchema.js', async (importOriginal) => {
|
||||
const original =
|
||||
await importOriginal<typeof import('../../config/settingsSchema.js')>();
|
||||
return {
|
||||
...actual,
|
||||
useSettings: () => ({
|
||||
settings,
|
||||
setSetting: (key: string, value: string) => {
|
||||
settings = createMockSettings({ [key]: value });
|
||||
},
|
||||
getSettingDefinition: (key: string) => {
|
||||
if (key === 'a.string.setting') {
|
||||
return {
|
||||
type: 'string',
|
||||
description: 'A string setting',
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
}),
|
||||
...original,
|
||||
getSettingsSchema: vi.fn(original.getSettingsSchema),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -136,7 +139,6 @@ describe('SettingsDialog', () => {
|
||||
const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset keypress mock state (variables are commented out)
|
||||
// currentKeypressHandler = null;
|
||||
// isKeypressActive = false;
|
||||
@@ -146,6 +148,9 @@ describe('SettingsDialog', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
TEST_ONLY.clearFlattenedSchema();
|
||||
vi.clearAllMocks();
|
||||
vi.resetAllMocks();
|
||||
// Reset keypress mock state (variables are commented out)
|
||||
// currentKeypressHandler = null;
|
||||
// isKeypressActive = false;
|
||||
@@ -153,44 +158,6 @@ describe('SettingsDialog', () => {
|
||||
// console.error = originalConsoleError;
|
||||
});
|
||||
|
||||
const createMockSettings = (
|
||||
userSettings = {},
|
||||
systemSettings = {},
|
||||
workspaceSettings = {},
|
||||
) =>
|
||||
new LoadedSettings(
|
||||
{
|
||||
settings: {
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
...systemSettings,
|
||||
},
|
||||
path: '/system/settings.json',
|
||||
},
|
||||
{
|
||||
settings: {},
|
||||
path: '/system/system-defaults.json',
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
...userSettings,
|
||||
},
|
||||
path: '/user/settings.json',
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
ui: { customThemes: {} },
|
||||
mcpServers: {},
|
||||
...workspaceSettings,
|
||||
},
|
||||
path: '/workspace/settings.json',
|
||||
},
|
||||
true,
|
||||
new Set(),
|
||||
);
|
||||
|
||||
describe('Initial Rendering', () => {
|
||||
it('should render the settings dialog with default state', () => {
|
||||
const settings = createMockSettings();
|
||||
@@ -244,15 +211,18 @@ describe('SettingsDialog', () => {
|
||||
const settings = createMockSettings();
|
||||
const onSelect = vi.fn();
|
||||
|
||||
const { stdin, unmount } = render(
|
||||
const { stdin, unmount, lastFrame } = render(
|
||||
<KeypressProvider kittyProtocolEnabled={false}>
|
||||
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||
</KeypressProvider>,
|
||||
);
|
||||
|
||||
// Press down arrow
|
||||
stdin.write('\u001B[B'); // Down arrow
|
||||
await wait();
|
||||
act(() => {
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string); // Down arrow
|
||||
});
|
||||
|
||||
expect(lastFrame()).toContain('● Disable Auto Update');
|
||||
|
||||
// The active index should have changed (tested indirectly through behavior)
|
||||
unmount();
|
||||
@@ -269,9 +239,9 @@ describe('SettingsDialog', () => {
|
||||
);
|
||||
|
||||
// First go down, then up
|
||||
stdin.write('\u001B[B'); // Down arrow
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string); // Down arrow
|
||||
await wait();
|
||||
stdin.write('\u001B[A'); // Up arrow
|
||||
stdin.write(TerminalKeys.UP_ARROW as string);
|
||||
await wait();
|
||||
|
||||
unmount();
|
||||
@@ -296,21 +266,25 @@ describe('SettingsDialog', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not navigate beyond bounds', async () => {
|
||||
it('wraps around when at the top of the list', async () => {
|
||||
const settings = createMockSettings();
|
||||
const onSelect = vi.fn();
|
||||
|
||||
const { stdin, unmount } = render(
|
||||
const { stdin, unmount, lastFrame } = render(
|
||||
<KeypressProvider kittyProtocolEnabled={false}>
|
||||
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||
</KeypressProvider>,
|
||||
);
|
||||
|
||||
// Try to go up from first item
|
||||
stdin.write('\u001B[A'); // Up arrow
|
||||
act(() => {
|
||||
stdin.write(TerminalKeys.UP_ARROW);
|
||||
});
|
||||
|
||||
await wait();
|
||||
|
||||
// Should still be on first item
|
||||
expect(lastFrame()).toContain('● Folder Trust');
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -319,20 +293,142 @@ describe('SettingsDialog', () => {
|
||||
it('should toggle setting with Enter key', async () => {
|
||||
const settings = createMockSettings();
|
||||
const onSelect = vi.fn();
|
||||
|
||||
const { stdin, unmount } = render(
|
||||
const component = (
|
||||
<KeypressProvider kittyProtocolEnabled={false}>
|
||||
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||
</KeypressProvider>,
|
||||
</KeypressProvider>
|
||||
);
|
||||
|
||||
const { stdin, unmount } = render(component);
|
||||
|
||||
// Press Enter to toggle current setting
|
||||
stdin.write('\u000D'); // Enter key
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string);
|
||||
await wait();
|
||||
stdin.write(TerminalKeys.ENTER as string);
|
||||
await wait();
|
||||
|
||||
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalledWith(
|
||||
new Set<string>(['general.disableAutoUpdate']),
|
||||
{
|
||||
general: {
|
||||
disableAutoUpdate: true,
|
||||
},
|
||||
},
|
||||
expect.any(LoadedSettings),
|
||||
SettingScope.User,
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('enum values', () => {
|
||||
enum StringEnum {
|
||||
FOO = 'foo',
|
||||
BAR = 'bar',
|
||||
BAZ = 'baz',
|
||||
}
|
||||
|
||||
const SETTING: SettingDefinition = {
|
||||
type: 'enum',
|
||||
label: 'Theme',
|
||||
options: [
|
||||
{
|
||||
label: 'Foo',
|
||||
value: StringEnum.FOO,
|
||||
},
|
||||
{
|
||||
label: 'Bar',
|
||||
value: StringEnum.BAR,
|
||||
},
|
||||
{
|
||||
label: 'Baz',
|
||||
value: StringEnum.BAZ,
|
||||
},
|
||||
],
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: StringEnum.BAR,
|
||||
description: 'The color theme for the UI.',
|
||||
showInDialog: true,
|
||||
};
|
||||
|
||||
const FAKE_SCHEMA: SettingsSchemaType = {
|
||||
ui: {
|
||||
showInDialog: false,
|
||||
properties: {
|
||||
theme: {
|
||||
...SETTING,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as SettingsSchemaType;
|
||||
|
||||
it('toggles enum values with the enter key', async () => {
|
||||
vi.mocked(getSettingsSchema).mockReturnValue(FAKE_SCHEMA);
|
||||
const settings = createMockSettings();
|
||||
const onSelect = vi.fn();
|
||||
const component = (
|
||||
<KeypressProvider kittyProtocolEnabled={false}>
|
||||
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||
</KeypressProvider>
|
||||
);
|
||||
|
||||
const { stdin, unmount } = render(component);
|
||||
|
||||
// Press Enter to toggle current setting
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string);
|
||||
await wait();
|
||||
stdin.write(TerminalKeys.ENTER as string);
|
||||
await wait();
|
||||
|
||||
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalledWith(
|
||||
new Set<string>(['ui.theme']),
|
||||
{
|
||||
ui: {
|
||||
theme: StringEnum.BAZ,
|
||||
},
|
||||
},
|
||||
expect.any(LoadedSettings),
|
||||
SettingScope.User,
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('loops back when reaching the end of an enum', async () => {
|
||||
vi.mocked(getSettingsSchema).mockReturnValue(FAKE_SCHEMA);
|
||||
const settings = createMockSettings();
|
||||
settings.setValue(SettingScope.User, 'ui.theme', StringEnum.BAZ);
|
||||
const onSelect = vi.fn();
|
||||
const component = (
|
||||
<KeypressProvider kittyProtocolEnabled={false}>
|
||||
<SettingsDialog settings={settings} onSelect={onSelect} />
|
||||
</KeypressProvider>
|
||||
);
|
||||
|
||||
const { stdin, unmount } = render(component);
|
||||
|
||||
// Press Enter to toggle current setting
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string);
|
||||
await wait();
|
||||
stdin.write(TerminalKeys.ENTER as string);
|
||||
await wait();
|
||||
|
||||
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalledWith(
|
||||
new Set<string>(['ui.theme']),
|
||||
{
|
||||
ui: {
|
||||
theme: StringEnum.FOO,
|
||||
},
|
||||
},
|
||||
expect.any(LoadedSettings),
|
||||
SettingScope.User,
|
||||
);
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('should toggle setting with Space key', async () => {
|
||||
const settings = createMockSettings();
|
||||
const onSelect = vi.fn();
|
||||
@@ -362,7 +458,7 @@ describe('SettingsDialog', () => {
|
||||
|
||||
// Navigate to vim mode setting and toggle it
|
||||
// This would require knowing the exact position, so we'll just test that the mock is called
|
||||
stdin.write('\u000D'); // Enter key
|
||||
stdin.write(TerminalKeys.ENTER as string); // Enter key
|
||||
await wait();
|
||||
|
||||
// The mock should potentially be called if vim mode was toggled
|
||||
@@ -382,7 +478,7 @@ describe('SettingsDialog', () => {
|
||||
);
|
||||
|
||||
// Switch to scope focus
|
||||
stdin.write('\t'); // Tab key
|
||||
stdin.write(TerminalKeys.TAB); // Tab key
|
||||
await wait();
|
||||
|
||||
// Select different scope (numbers 1-3 typically available)
|
||||
@@ -502,7 +598,7 @@ describe('SettingsDialog', () => {
|
||||
);
|
||||
|
||||
// Switch to scope selector
|
||||
stdin.write('\t'); // Tab
|
||||
stdin.write(TerminalKeys.TAB as string); // Tab
|
||||
await wait();
|
||||
|
||||
// Change scope
|
||||
@@ -547,7 +643,7 @@ describe('SettingsDialog', () => {
|
||||
);
|
||||
|
||||
// Try to toggle a setting (this might trigger vim mode toggle)
|
||||
stdin.write('\u000D'); // Enter
|
||||
stdin.write(TerminalKeys.ENTER as string); // Enter
|
||||
await wait();
|
||||
|
||||
// Should not crash
|
||||
@@ -567,13 +663,13 @@ describe('SettingsDialog', () => {
|
||||
);
|
||||
|
||||
// Toggle a setting
|
||||
stdin.write('\u000D'); // Enter
|
||||
stdin.write(TerminalKeys.ENTER as string); // Enter
|
||||
await wait();
|
||||
|
||||
// Toggle another setting
|
||||
stdin.write('\u001B[B'); // Down
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string); // Down
|
||||
await wait();
|
||||
stdin.write('\u000D'); // Enter
|
||||
stdin.write(TerminalKeys.ENTER as string); // Enter
|
||||
await wait();
|
||||
|
||||
// Should track multiple modified settings
|
||||
@@ -592,7 +688,7 @@ describe('SettingsDialog', () => {
|
||||
|
||||
// Navigate down many times to test scrolling
|
||||
for (let i = 0; i < 10; i++) {
|
||||
stdin.write('\u001B[B'); // Down arrow
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string); // Down arrow
|
||||
await wait(10);
|
||||
}
|
||||
|
||||
@@ -615,7 +711,7 @@ describe('SettingsDialog', () => {
|
||||
|
||||
// Navigate to and toggle vim mode setting
|
||||
// This would require knowing the exact position of vim mode setting
|
||||
stdin.write('\u000D'); // Enter
|
||||
stdin.write(TerminalKeys.ENTER as string); // Enter
|
||||
await wait();
|
||||
|
||||
unmount();
|
||||
@@ -653,7 +749,7 @@ describe('SettingsDialog', () => {
|
||||
);
|
||||
|
||||
// Toggle a non-restart-required setting (like hideTips)
|
||||
stdin.write('\u000D'); // Enter - toggle current setting
|
||||
stdin.write(TerminalKeys.ENTER as string); // Enter - toggle current setting
|
||||
await wait();
|
||||
|
||||
// Should save immediately without showing restart prompt
|
||||
@@ -750,8 +846,8 @@ describe('SettingsDialog', () => {
|
||||
|
||||
// Rapid navigation
|
||||
for (let i = 0; i < 5; i++) {
|
||||
stdin.write('\u001B[B'); // Down arrow
|
||||
stdin.write('\u001B[A'); // Up arrow
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string);
|
||||
stdin.write(TerminalKeys.UP_ARROW as string);
|
||||
}
|
||||
await wait(100);
|
||||
|
||||
@@ -806,9 +902,9 @@ describe('SettingsDialog', () => {
|
||||
);
|
||||
|
||||
// Try to navigate when potentially at bounds
|
||||
stdin.write('\u001B[B'); // Down
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string);
|
||||
await wait();
|
||||
stdin.write('\u001B[A'); // Up
|
||||
stdin.write(TerminalKeys.UP_ARROW as string);
|
||||
await wait();
|
||||
|
||||
unmount();
|
||||
@@ -917,19 +1013,19 @@ describe('SettingsDialog', () => {
|
||||
);
|
||||
|
||||
// Toggle first setting (should require restart)
|
||||
stdin.write('\u000D'); // Enter
|
||||
stdin.write(TerminalKeys.ENTER as string); // Enter
|
||||
await wait();
|
||||
|
||||
// Navigate to next setting and toggle it (should not require restart - e.g., vimMode)
|
||||
stdin.write('\u001B[B'); // Down
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string); // Down
|
||||
await wait();
|
||||
stdin.write('\u000D'); // Enter
|
||||
stdin.write(TerminalKeys.ENTER as string); // Enter
|
||||
await wait();
|
||||
|
||||
// Navigate to another setting and toggle it (should also require restart)
|
||||
stdin.write('\u001B[B'); // Down
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string); // Down
|
||||
await wait();
|
||||
stdin.write('\u000D'); // Enter
|
||||
stdin.write(TerminalKeys.ENTER as string); // Enter
|
||||
await wait();
|
||||
|
||||
// The test verifies that all changes are preserved and the dialog still works
|
||||
@@ -948,13 +1044,13 @@ describe('SettingsDialog', () => {
|
||||
);
|
||||
|
||||
// Multiple scope changes
|
||||
stdin.write('\t'); // Tab to scope
|
||||
stdin.write(TerminalKeys.TAB as string); // Tab to scope
|
||||
await wait();
|
||||
stdin.write('2'); // Workspace
|
||||
await wait();
|
||||
stdin.write('\t'); // Tab to settings
|
||||
stdin.write(TerminalKeys.TAB as string); // Tab to settings
|
||||
await wait();
|
||||
stdin.write('\t'); // Tab to scope
|
||||
stdin.write(TerminalKeys.TAB as string); // Tab to scope
|
||||
await wait();
|
||||
stdin.write('1'); // User
|
||||
await wait();
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import {
|
||||
getDialogSettingKeys,
|
||||
getSettingValue,
|
||||
setPendingSettingValue,
|
||||
getDisplayValue,
|
||||
hasRestartRequiredSettings,
|
||||
@@ -28,11 +27,16 @@ import {
|
||||
getDefaultValue,
|
||||
setPendingSettingValueAny,
|
||||
getNestedValue,
|
||||
getEffectiveValue,
|
||||
} from '../../utils/settingsUtils.js';
|
||||
import { useVimMode } from '../contexts/VimModeContext.js';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import chalk from 'chalk';
|
||||
import { cpSlice, cpLen, stripUnsafeCharacters } from '../utils/textUtils.js';
|
||||
import {
|
||||
type SettingsValue,
|
||||
TOGGLE_TYPES,
|
||||
} from '../../config/settingsSchema.js';
|
||||
|
||||
interface SettingsDialogProps {
|
||||
settings: LoadedSettings;
|
||||
@@ -122,15 +126,33 @@ export function SettingsDialog({
|
||||
value: key,
|
||||
type: definition?.type,
|
||||
toggle: () => {
|
||||
if (definition?.type !== 'boolean') {
|
||||
// For non-boolean items, toggle will be handled via edit mode.
|
||||
if (!TOGGLE_TYPES.has(definition?.type)) {
|
||||
return;
|
||||
}
|
||||
const currentValue = getSettingValue(key, pendingSettings, {});
|
||||
const newValue = !currentValue;
|
||||
const currentValue = getEffectiveValue(key, pendingSettings, {});
|
||||
let newValue: SettingsValue;
|
||||
if (definition?.type === 'boolean') {
|
||||
newValue = !(currentValue as boolean);
|
||||
setPendingSettings((prev) =>
|
||||
setPendingSettingValue(key, newValue as boolean, prev),
|
||||
);
|
||||
} else if (definition?.type === 'enum' && definition.options) {
|
||||
const options = definition.options;
|
||||
const currentIndex = options?.findIndex(
|
||||
(opt) => opt.value === currentValue,
|
||||
);
|
||||
if (currentIndex !== -1 && currentIndex < options.length - 1) {
|
||||
newValue = options[currentIndex + 1].value;
|
||||
} else {
|
||||
newValue = options[0].value; // loop back to start.
|
||||
}
|
||||
setPendingSettings((prev) =>
|
||||
setPendingSettingValueAny(key, newValue, prev),
|
||||
);
|
||||
}
|
||||
|
||||
setPendingSettings((prev) =>
|
||||
setPendingSettingValue(key, newValue, prev),
|
||||
setPendingSettingValue(key, newValue as boolean, prev),
|
||||
);
|
||||
|
||||
if (!requiresRestart(key)) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,18 +12,19 @@ import type {
|
||||
import type {
|
||||
SettingDefinition,
|
||||
SettingsSchema,
|
||||
SettingsType,
|
||||
SettingsValue,
|
||||
} from '../config/settingsSchema.js';
|
||||
import { SETTINGS_SCHEMA } from '../config/settingsSchema.js';
|
||||
import { getSettingsSchema } from '../config/settingsSchema.js';
|
||||
|
||||
// The schema is now nested, but many parts of the UI and logic work better
|
||||
// with a flattened structure and dot-notation keys. This section flattens the
|
||||
// schema into a map for easier lookups.
|
||||
|
||||
function flattenSchema(
|
||||
schema: SettingsSchema,
|
||||
prefix = '',
|
||||
): Record<string, SettingDefinition & { key: string }> {
|
||||
let result: Record<string, SettingDefinition & { key: string }> = {};
|
||||
type FlattenedSchema = Record<string, SettingDefinition & { key: string }>;
|
||||
|
||||
function flattenSchema(schema: SettingsSchema, prefix = ''): FlattenedSchema {
|
||||
let result: FlattenedSchema = {};
|
||||
for (const key in schema) {
|
||||
const newKey = prefix ? `${prefix}.${key}` : key;
|
||||
const definition = schema[key];
|
||||
@@ -35,7 +36,19 @@ function flattenSchema(
|
||||
return result;
|
||||
}
|
||||
|
||||
const FLATTENED_SCHEMA = flattenSchema(SETTINGS_SCHEMA);
|
||||
let _FLATTENED_SCHEMA: FlattenedSchema | undefined;
|
||||
|
||||
/** Returns a flattened schema, the first call is memoized for future requests. */
|
||||
export function getFlattenedSchema() {
|
||||
return (
|
||||
_FLATTENED_SCHEMA ??
|
||||
(_FLATTENED_SCHEMA = flattenSchema(getSettingsSchema()))
|
||||
);
|
||||
}
|
||||
|
||||
function clearFlattenedSchema() {
|
||||
_FLATTENED_SCHEMA = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all settings grouped by category
|
||||
@@ -49,7 +62,7 @@ export function getSettingsByCategory(): Record<
|
||||
Array<SettingDefinition & { key: string }>
|
||||
> = {};
|
||||
|
||||
Object.values(FLATTENED_SCHEMA).forEach((definition) => {
|
||||
Object.values(getFlattenedSchema()).forEach((definition) => {
|
||||
const category = definition.category;
|
||||
if (!categories[category]) {
|
||||
categories[category] = [];
|
||||
@@ -66,28 +79,28 @@ export function getSettingsByCategory(): Record<
|
||||
export function getSettingDefinition(
|
||||
key: string,
|
||||
): (SettingDefinition & { key: string }) | undefined {
|
||||
return FLATTENED_SCHEMA[key];
|
||||
return getFlattenedSchema()[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a setting requires restart
|
||||
*/
|
||||
export function requiresRestart(key: string): boolean {
|
||||
return FLATTENED_SCHEMA[key]?.requiresRestart ?? false;
|
||||
return getFlattenedSchema()[key]?.requiresRestart ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default value for a setting
|
||||
*/
|
||||
export function getDefaultValue(key: string): SettingDefinition['default'] {
|
||||
return FLATTENED_SCHEMA[key]?.default;
|
||||
export function getDefaultValue(key: string): SettingsValue {
|
||||
return getFlattenedSchema()[key]?.default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all setting keys that require restart
|
||||
*/
|
||||
export function getRestartRequiredSettings(): string[] {
|
||||
return Object.values(FLATTENED_SCHEMA)
|
||||
return Object.values(getFlattenedSchema())
|
||||
.filter((definition) => definition.requiresRestart)
|
||||
.map((definition) => definition.key);
|
||||
}
|
||||
@@ -121,7 +134,7 @@ export function getEffectiveValue(
|
||||
key: string,
|
||||
settings: Settings,
|
||||
mergedSettings: Settings,
|
||||
): SettingDefinition['default'] {
|
||||
): SettingsValue {
|
||||
const definition = getSettingDefinition(key);
|
||||
if (!definition) {
|
||||
return undefined;
|
||||
@@ -132,13 +145,13 @@ export function getEffectiveValue(
|
||||
// Check the current scope's settings first
|
||||
let value = getNestedValue(settings as Record<string, unknown>, path);
|
||||
if (value !== undefined) {
|
||||
return value as SettingDefinition['default'];
|
||||
return value as SettingsValue;
|
||||
}
|
||||
|
||||
// Check the merged settings for an inherited value
|
||||
value = getNestedValue(mergedSettings as Record<string, unknown>, path);
|
||||
if (value !== undefined) {
|
||||
return value as SettingDefinition['default'];
|
||||
return value as SettingsValue;
|
||||
}
|
||||
|
||||
// Return default value if no value is set anywhere
|
||||
@@ -149,16 +162,16 @@ export function getEffectiveValue(
|
||||
* Get all setting keys from the schema
|
||||
*/
|
||||
export function getAllSettingKeys(): string[] {
|
||||
return Object.keys(FLATTENED_SCHEMA);
|
||||
return Object.keys(getFlattenedSchema());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings by type
|
||||
*/
|
||||
export function getSettingsByType(
|
||||
type: SettingDefinition['type'],
|
||||
type: SettingsType,
|
||||
): Array<SettingDefinition & { key: string }> {
|
||||
return Object.values(FLATTENED_SCHEMA).filter(
|
||||
return Object.values(getFlattenedSchema()).filter(
|
||||
(definition) => definition.type === type,
|
||||
);
|
||||
}
|
||||
@@ -171,7 +184,7 @@ export function getSettingsRequiringRestart(): Array<
|
||||
key: string;
|
||||
}
|
||||
> {
|
||||
return Object.values(FLATTENED_SCHEMA).filter(
|
||||
return Object.values(getFlattenedSchema()).filter(
|
||||
(definition) => definition.requiresRestart,
|
||||
);
|
||||
}
|
||||
@@ -180,21 +193,21 @@ export function getSettingsRequiringRestart(): Array<
|
||||
* Validate if a setting key exists in the schema
|
||||
*/
|
||||
export function isValidSettingKey(key: string): boolean {
|
||||
return key in FLATTENED_SCHEMA;
|
||||
return key in getFlattenedSchema();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the category for a setting
|
||||
*/
|
||||
export function getSettingCategory(key: string): string | undefined {
|
||||
return FLATTENED_SCHEMA[key]?.category;
|
||||
return getFlattenedSchema()[key]?.category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a setting should be shown in the settings dialog
|
||||
*/
|
||||
export function shouldShowInDialog(key: string): boolean {
|
||||
return FLATTENED_SCHEMA[key]?.showInDialog ?? true; // Default to true for backward compatibility
|
||||
return getFlattenedSchema()[key]?.showInDialog ?? true; // Default to true for backward compatibility
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,7 +222,7 @@ export function getDialogSettingsByCategory(): Record<
|
||||
Array<SettingDefinition & { key: string }>
|
||||
> = {};
|
||||
|
||||
Object.values(FLATTENED_SCHEMA)
|
||||
Object.values(getFlattenedSchema())
|
||||
.filter((definition) => definition.showInDialog !== false)
|
||||
.forEach((definition) => {
|
||||
const category = definition.category;
|
||||
@@ -226,9 +239,9 @@ export function getDialogSettingsByCategory(): Record<
|
||||
* Get settings by type that should be shown in the dialog
|
||||
*/
|
||||
export function getDialogSettingsByType(
|
||||
type: SettingDefinition['type'],
|
||||
type: SettingsType,
|
||||
): Array<SettingDefinition & { key: string }> {
|
||||
return Object.values(FLATTENED_SCHEMA).filter(
|
||||
return Object.values(getFlattenedSchema()).filter(
|
||||
(definition) =>
|
||||
definition.type === type && definition.showInDialog !== false,
|
||||
);
|
||||
@@ -238,7 +251,7 @@ export function getDialogSettingsByType(
|
||||
* Get all setting keys that should be shown in the dialog
|
||||
*/
|
||||
export function getDialogSettingKeys(): string[] {
|
||||
return Object.values(FLATTENED_SCHEMA)
|
||||
return Object.values(getFlattenedSchema())
|
||||
.filter((definition) => definition.showInDialog !== false)
|
||||
.map((definition) => definition.key);
|
||||
}
|
||||
@@ -344,7 +357,7 @@ export function setPendingSettingValue(
|
||||
*/
|
||||
export function setPendingSettingValueAny(
|
||||
key: string,
|
||||
value: unknown,
|
||||
value: SettingsValue,
|
||||
pendingSettings: Settings,
|
||||
): Settings {
|
||||
const path = key.split('.');
|
||||
@@ -415,25 +428,30 @@ export function getDisplayValue(
|
||||
pendingSettings?: Settings,
|
||||
): string {
|
||||
// Prioritize pending changes if user has modified this setting
|
||||
let value: boolean;
|
||||
const definition = getSettingDefinition(key);
|
||||
|
||||
let value: SettingsValue;
|
||||
if (pendingSettings && settingExistsInScope(key, pendingSettings)) {
|
||||
// Show the value from the pending (unsaved) edits when it exists
|
||||
value = getSettingValue(key, pendingSettings, {});
|
||||
value = getEffectiveValue(key, pendingSettings, {});
|
||||
} else if (settingExistsInScope(key, settings)) {
|
||||
// Show the value defined at the current scope if present
|
||||
value = getSettingValue(key, settings, {});
|
||||
value = getEffectiveValue(key, settings, {});
|
||||
} else {
|
||||
// Fall back to the schema default when the key is unset in this scope
|
||||
const defaultValue = getDefaultValue(key);
|
||||
value = typeof defaultValue === 'boolean' ? defaultValue : false;
|
||||
value = getDefaultValue(key);
|
||||
}
|
||||
|
||||
const valueString = String(value);
|
||||
let valueString = String(value);
|
||||
|
||||
if (definition?.type === 'enum' && definition.options) {
|
||||
const option = definition.options?.find((option) => option.value === value);
|
||||
valueString = option?.label ?? `${value}`;
|
||||
}
|
||||
|
||||
// Check if value is different from default OR if it's in modified settings OR if there are pending changes
|
||||
const defaultValue = getDefaultValue(key);
|
||||
const isChangedFromDefault =
|
||||
typeof defaultValue === 'boolean' ? value !== defaultValue : value === true;
|
||||
const isChangedFromDefault = value !== defaultValue;
|
||||
const isInModifiedSettings = modifiedSettings.has(key);
|
||||
|
||||
// Mark as modified if setting exists in current scope OR is in modified settings
|
||||
@@ -476,3 +494,5 @@ export function getEffectiveDisplayValue(
|
||||
): boolean {
|
||||
return getSettingValue(key, settings, mergedSettings);
|
||||
}
|
||||
|
||||
export const TEST_ONLY = { clearFlattenedSchema };
|
||||
|
||||
Reference in New Issue
Block a user