1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
|
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { Settings, SettingScope, LoadedSettings } from '../config/settings.js';
import {
SETTINGS_SCHEMA,
SettingDefinition,
SettingsSchema,
} 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 }> = {};
for (const key in schema) {
const newKey = prefix ? `${prefix}.${key}` : key;
const definition = schema[key];
result[newKey] = { ...definition, key: newKey };
if (definition.properties) {
result = { ...result, ...flattenSchema(definition.properties, newKey) };
}
}
return result;
}
const FLATTENED_SCHEMA = flattenSchema(SETTINGS_SCHEMA);
/**
* Get all settings grouped by category
*/
export function getSettingsByCategory(): Record<
string,
Array<SettingDefinition & { key: string }>
> {
const categories: Record<
string,
Array<SettingDefinition & { key: string }>
> = {};
Object.values(FLATTENED_SCHEMA).forEach((definition) => {
const category = definition.category;
if (!categories[category]) {
categories[category] = [];
}
categories[category].push(definition);
});
return categories;
}
/**
* Get a setting definition by key
*/
export function getSettingDefinition(
key: string,
): (SettingDefinition & { key: string }) | undefined {
return FLATTENED_SCHEMA[key];
}
/**
* Check if a setting requires restart
*/
export function requiresRestart(key: string): boolean {
return FLATTENED_SCHEMA[key]?.requiresRestart ?? false;
}
/**
* Get the default value for a setting
*/
export function getDefaultValue(key: string): SettingDefinition['default'] {
return FLATTENED_SCHEMA[key]?.default;
}
/**
* Get all setting keys that require restart
*/
export function getRestartRequiredSettings(): string[] {
return Object.values(FLATTENED_SCHEMA)
.filter((definition) => definition.requiresRestart)
.map((definition) => definition.key);
}
/**
* Recursively gets a value from a nested object using a key path array.
*/
export function getNestedValue(
obj: Record<string, unknown>,
path: string[],
): unknown {
const [first, ...rest] = path;
if (!first || !(first in obj)) {
return undefined;
}
const value = obj[first];
if (rest.length === 0) {
return value;
}
if (value && typeof value === 'object' && value !== null) {
return getNestedValue(value as Record<string, unknown>, rest);
}
return undefined;
}
/**
* Get the effective value for a setting, considering inheritance from higher scopes
* Always returns a value (never undefined) - falls back to default if not set anywhere
*/
export function getEffectiveValue(
key: string,
settings: Settings,
mergedSettings: Settings,
): SettingDefinition['default'] {
const definition = getSettingDefinition(key);
if (!definition) {
return undefined;
}
const path = key.split('.');
// Check the current scope's settings first
let value = getNestedValue(settings as Record<string, unknown>, path);
if (value !== undefined) {
return value as SettingDefinition['default'];
}
// 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 default value if no value is set anywhere
return definition.default;
}
/**
* Get all setting keys from the schema
*/
export function getAllSettingKeys(): string[] {
return Object.keys(FLATTENED_SCHEMA);
}
/**
* Get settings by type
*/
export function getSettingsByType(
type: SettingDefinition['type'],
): Array<SettingDefinition & { key: string }> {
return Object.values(FLATTENED_SCHEMA).filter(
(definition) => definition.type === type,
);
}
/**
* Get settings that require restart
*/
export function getSettingsRequiringRestart(): Array<
SettingDefinition & {
key: string;
}
> {
return Object.values(FLATTENED_SCHEMA).filter(
(definition) => definition.requiresRestart,
);
}
/**
* Validate if a setting key exists in the schema
*/
export function isValidSettingKey(key: string): boolean {
return key in FLATTENED_SCHEMA;
}
/**
* Get the category for a setting
*/
export function getSettingCategory(key: string): string | undefined {
return FLATTENED_SCHEMA[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
}
/**
* Get all settings that should be shown in the dialog, grouped by category
*/
export function getDialogSettingsByCategory(): Record<
string,
Array<SettingDefinition & { key: string }>
> {
const categories: Record<
string,
Array<SettingDefinition & { key: string }>
> = {};
Object.values(FLATTENED_SCHEMA)
.filter((definition) => definition.showInDialog !== false)
.forEach((definition) => {
const category = definition.category;
if (!categories[category]) {
categories[category] = [];
}
categories[category].push(definition);
});
return categories;
}
/**
* Get settings by type that should be shown in the dialog
*/
export function getDialogSettingsByType(
type: SettingDefinition['type'],
): Array<SettingDefinition & { key: string }> {
return Object.values(FLATTENED_SCHEMA).filter(
(definition) =>
definition.type === type && definition.showInDialog !== false,
);
}
/**
* Get all setting keys that should be shown in the dialog
*/
export function getDialogSettingKeys(): string[] {
return Object.values(FLATTENED_SCHEMA)
.filter((definition) => definition.showInDialog !== false)
.map((definition) => definition.key);
}
// ============================================================================
// BUSINESS LOGIC UTILITIES (Higher-level utilities for setting operations)
// ============================================================================
/**
* Get the current value for a setting in a specific scope
* Always returns a value (never undefined) - falls back to default if not set anywhere
*/
export function getSettingValue(
key: string,
settings: Settings,
mergedSettings: Settings,
): boolean {
const definition = getSettingDefinition(key);
if (!definition) {
return false; // Default fallback for invalid settings
}
const value = getEffectiveValue(key, settings, mergedSettings);
// Ensure we return a boolean value, converting from the more general type
if (typeof value === 'boolean') {
return value;
}
// Fall back to default value, ensuring it's a boolean
const defaultValue = definition.default;
if (typeof defaultValue === 'boolean') {
return defaultValue;
}
return false; // Final fallback
}
/**
* Check if a setting value is modified from its default
*/
export function isSettingModified(key: string, value: boolean): boolean {
const defaultValue = getDefaultValue(key);
// Handle type comparison properly
if (typeof defaultValue === 'boolean') {
return value !== defaultValue;
}
// If default is not a boolean, consider it modified if value is true
return value === true;
}
/**
* Check if a setting exists in the original settings file for a scope
*/
export function settingExistsInScope(
key: string,
scopeSettings: Settings,
): boolean {
const path = key.split('.');
const value = getNestedValue(scopeSettings as Record<string, unknown>, path);
return value !== undefined;
}
/**
* Recursively sets a value in a nested object using a key path array.
*/
function setNestedValue(
obj: Record<string, unknown>,
path: string[],
value: unknown,
): Record<string, unknown> {
const [first, ...rest] = path;
if (!first) {
return obj;
}
if (rest.length === 0) {
obj[first] = value;
return obj;
}
if (!obj[first] || typeof obj[first] !== 'object') {
obj[first] = {};
}
setNestedValue(obj[first] as Record<string, unknown>, rest, value);
return obj;
}
/**
* Set a setting value in the pending settings
*/
export function setPendingSettingValue(
key: string,
value: boolean,
pendingSettings: Settings,
): Settings {
const path = key.split('.');
const newSettings = JSON.parse(JSON.stringify(pendingSettings));
setNestedValue(newSettings, path, value);
return newSettings;
}
/**
* Generic setter: Set a setting value (boolean, number, string, etc.) in the pending settings
*/
export function setPendingSettingValueAny(
key: string,
value: unknown,
pendingSettings: Settings,
): Settings {
const path = key.split('.');
const newSettings = structuredClone(pendingSettings);
setNestedValue(newSettings, path, value);
return newSettings;
}
/**
* Check if any modified settings require a restart
*/
export function hasRestartRequiredSettings(
modifiedSettings: Set<string>,
): boolean {
return Array.from(modifiedSettings).some((key) => requiresRestart(key));
}
/**
* Get the restart required settings from a set of modified settings
*/
export function getRestartRequiredFromModified(
modifiedSettings: Set<string>,
): string[] {
return Array.from(modifiedSettings).filter((key) => requiresRestart(key));
}
/**
* Save modified settings to the appropriate scope
*/
export function saveModifiedSettings(
modifiedSettings: Set<string>,
pendingSettings: Settings,
loadedSettings: LoadedSettings,
scope: SettingScope,
): void {
modifiedSettings.forEach((settingKey) => {
const path = settingKey.split('.');
const value = getNestedValue(
pendingSettings as Record<string, unknown>,
path,
);
if (value === undefined) {
return;
}
const existsInOriginalFile = settingExistsInScope(
settingKey,
loadedSettings.forScope(scope).settings,
);
const isDefaultValue = value === getDefaultValue(settingKey);
if (existsInOriginalFile || !isDefaultValue) {
// This is tricky because setValue only works on top-level keys.
// We need to set the whole parent object.
const [parentKey] = path;
if (parentKey) {
const newParentValue = setPendingSettingValueAny(
settingKey,
value,
loadedSettings.forScope(scope).settings,
)[parentKey as keyof Settings];
loadedSettings.setValue(
scope,
parentKey as keyof Settings,
newParentValue,
);
}
}
});
}
/**
* Get the display value for a setting, showing current scope value with default change indicator
*/
export function getDisplayValue(
key: string,
settings: Settings,
_mergedSettings: Settings,
modifiedSettings: Set<string>,
pendingSettings?: Settings,
): string {
// Prioritize pending changes if user has modified this setting
let value: boolean;
if (pendingSettings && settingExistsInScope(key, pendingSettings)) {
// Show the value from the pending (unsaved) edits when it exists
value = getSettingValue(key, pendingSettings, {});
} else if (settingExistsInScope(key, settings)) {
// Show the value defined at the current scope if present
value = getSettingValue(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;
}
const valueString = String(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 isInModifiedSettings = modifiedSettings.has(key);
// Mark as modified if setting exists in current scope OR is in modified settings
if (settingExistsInScope(key, settings) || isInModifiedSettings) {
return `${valueString}*`; // * indicates setting is set in current scope
}
if (isChangedFromDefault || isInModifiedSettings) {
return `${valueString}*`; // * indicates changed from default value
}
return valueString;
}
/**
* Check if a setting doesn't exist in current scope (should be greyed out)
*/
export function isDefaultValue(key: string, settings: Settings): boolean {
return !settingExistsInScope(key, settings);
}
/**
* Check if a setting value is inherited (not set at current scope)
*/
export function isValueInherited(
key: string,
settings: Settings,
_mergedSettings: Settings,
): boolean {
return !settingExistsInScope(key, settings);
}
/**
* Get the effective value for display, considering inheritance
* Always returns a boolean value (never undefined)
*/
export function getEffectiveDisplayValue(
key: string,
settings: Settings,
mergedSettings: Settings,
): boolean {
return getSettingValue(key, settings, mergedSettings);
}
|