diff --git a/packages/ui/src/lib/persistence.test.ts b/packages/ui/src/lib/persistence.test.ts index ff72c1b8..c698f711 100644 --- a/packages/ui/src/lib/persistence.test.ts +++ b/packages/ui/src/lib/persistence.test.ts @@ -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).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 = { diff --git a/packages/ui/src/lib/persistence.ts b/packages/ui/src/lib/persistence.ts index ea9fef52..898ff502 100644 --- a/packages/ui/src/lib/persistence.ts +++ b/packages/ui/src/lib/persistence.ts @@ -1679,7 +1679,19 @@ class SettingsMutationTracker { record(changes: Partial): 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; }