fix(ui): bound settings mutation history

Skip mutation history when no settings operation is in flight and merge updates that share the same operation visibility window. This keeps stale-response reconciliation unchanged while preventing history from growing with repeated debounced updates.

Add a repeated-update regression covering an older pending load and latest-value reconciliation.

Tests: bun test src/lib/persistence.test.ts
This commit is contained in:
c_w_xiaohei
2026-08-26 00:42:37 +08:00
parent 4454bf7e14
commit df924aa532
2 changed files with 50 additions and 1 deletions
+37
View File
@@ -641,6 +641,43 @@ describe('updateDesktopSettings', () => {
}
});
test('preserves only the latest settings values across repeated pending updates', async () => {
const loadedSettings = deferred<{ settings: SettingsPayload; source: 'web' | 'vscode' }>();
registerSettingsApi(async (changes) => changes as SettingsPayload, () => loadedSettings.promise);
invalidateSettingsCache();
const syncedSettings: SettingsPayload[] = [];
const handleSettingsSynced = (event: Event) => {
syncedSettings.push((event as CustomEvent<SettingsPayload>).detail);
};
getWindow().addEventListener('openchamber:settings-synced', handleSettingsSynced);
try {
const sync = syncDesktopSettings();
const updates = Array.from({ length: 100 }, (_, index) => updateDesktopSettings({
activeProjectId: `project-${index}`,
showReasoningTraces: index % 2 === 0,
}));
loadedSettings.resolve({
settings: {
activeProjectId: 'stale-project',
showReasoningTraces: true,
draftStartersCraftGoalAdded: true,
draftStartersScheduleTaskAdded: true,
},
source: 'web',
});
await sync;
expect(syncedSettings.at(-1)?.activeProjectId).toBe('project-99');
expect(syncedSettings.at(-1)?.showReasoningTraces).toBe(false);
await Promise.all(updates);
} finally {
getWindow().removeEventListener('openchamber:settings-synced', handleSettingsSynced);
}
});
test('applies model selector settings from server settings', async () => {
getWindow();
const settings = {
+13 -1
View File
@@ -1679,7 +1679,19 @@ class SettingsMutationTracker {
record(changes: Partial<DesktopSettings>): number {
this.revision += 1;
this.mutations.push({ revision: this.revision, changes });
if (this.operations.size > 0) {
const latest = this.mutations.at(-1);
// A new segment is only needed when an operation started after the last one.
const crossedOperationBoundary = latest
? [...this.operations].some((operation) => operation.revision >= latest.revision)
: true;
if (latest && !crossedOperationBoundary) {
latest.revision = this.revision;
latest.changes = { ...latest.changes, ...changes };
} else {
this.mutations.push({ revision: this.revision, changes });
}
}
return this.revision;
}