feat: enhance global settings update with data loss prevention

- Added safeguards to prevent overwriting non-empty arrays with empty arrays during global settings updates, specifically for the 'projects' field.
- Implemented logging for updates to assist in diagnosing accidental wipes of critical settings.
- Updated tests to verify that projects are preserved during logout transitions and that theme changes are ignored if a project wipe is attempted.
- Enhanced the settings synchronization logic to ensure safe handling during authentication state changes.
This commit is contained in:
webdevcody
2026-01-07 21:38:46 -05:00
parent 8c68c24716
commit d8cdb0bf7a
7 changed files with 190 additions and 13 deletions

View File

@@ -266,25 +266,80 @@ export class SettingsService {
const settingsPath = getGlobalSettingsPath(this.dataDir);
const current = await this.getGlobalSettings();
// Guard against destructive "empty array/object" overwrites.
// During auth transitions, the UI can briefly have default/empty state and accidentally
// sync it, wiping persisted settings (especially `projects`).
const sanitizedUpdates: Partial<GlobalSettings> = { ...updates };
let attemptedProjectWipe = false;
const ignoreEmptyArrayOverwrite = <K extends keyof GlobalSettings>(key: K): void => {
const nextVal = sanitizedUpdates[key] as unknown;
const curVal = current[key] as unknown;
if (
Array.isArray(nextVal) &&
nextVal.length === 0 &&
Array.isArray(curVal) &&
curVal.length > 0
) {
delete sanitizedUpdates[key];
}
};
const currentProjectsLen = Array.isArray(current.projects) ? current.projects.length : 0;
if (
Array.isArray(sanitizedUpdates.projects) &&
sanitizedUpdates.projects.length === 0 &&
currentProjectsLen > 0
) {
attemptedProjectWipe = true;
delete sanitizedUpdates.projects;
}
ignoreEmptyArrayOverwrite('trashedProjects');
ignoreEmptyArrayOverwrite('projectHistory');
ignoreEmptyArrayOverwrite('recentFolders');
ignoreEmptyArrayOverwrite('aiProfiles');
ignoreEmptyArrayOverwrite('mcpServers');
ignoreEmptyArrayOverwrite('enabledCursorModels');
ignoreEmptyArrayOverwrite('enabledCodexModels');
// Empty object overwrite guard
if (
sanitizedUpdates.lastSelectedSessionByProject &&
typeof sanitizedUpdates.lastSelectedSessionByProject === 'object' &&
!Array.isArray(sanitizedUpdates.lastSelectedSessionByProject) &&
Object.keys(sanitizedUpdates.lastSelectedSessionByProject).length === 0 &&
current.lastSelectedSessionByProject &&
Object.keys(current.lastSelectedSessionByProject).length > 0
) {
delete sanitizedUpdates.lastSelectedSessionByProject;
}
// If a request attempted to wipe projects, also ignore theme changes in that same request.
if (attemptedProjectWipe) {
delete sanitizedUpdates.theme;
}
const updated: GlobalSettings = {
...current,
...updates,
...sanitizedUpdates,
version: SETTINGS_VERSION,
};
// Deep merge keyboard shortcuts if provided
if (updates.keyboardShortcuts) {
if (sanitizedUpdates.keyboardShortcuts) {
updated.keyboardShortcuts = {
...current.keyboardShortcuts,
...updates.keyboardShortcuts,
...sanitizedUpdates.keyboardShortcuts,
};
}
// Deep merge phaseModels if provided
if (updates.phaseModels) {
if (sanitizedUpdates.phaseModels) {
updated.phaseModels = {
...current.phaseModels,
...updates.phaseModels,
...sanitizedUpdates.phaseModels,
};
}