diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts index 711e993e0fb8a73fe9f4a7b24e365899f5b3acc3..6e816804ccc74f32b124ca4cfaabc7749d19a8b6 100644 --- a/apps/web/src/store.ts +++ b/apps/web/src/store.ts @@ -63,6 +63,8 @@ function readPersistedState(): AppState { } } +let legacyKeysCleanedUp = false; + function persistState(state: AppState): void { if (typeof window === "undefined") return; try { @@ -74,14 +76,29 @@ function persistState(state: AppState): void { .map((project) => project.cwd), }), ); - for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) { - window.localStorage.removeItem(legacyKey); + if (!legacyKeysCleanedUp) { + legacyKeysCleanedUp = true; + for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) { + window.localStorage.removeItem(legacyKey); + } } } catch { // Ignore quota/storage errors to avoid breaking chat UX. } } +let persistTimer: ReturnType | null = null; + +function debouncedPersistState(state: AppState): void { + if (persistTimer !== null) { + clearTimeout(persistTimer); + } + persistTimer = setTimeout(() => { + persistTimer = null; + persistState(state); + }, 500); +} + // ── Pure helpers ────────────────────────────────────────────────────── function updateThread( @@ -395,8 +412,19 @@ export const useStore = create((set) => ({ set((state) => setThreadBranch(state, threadId, branch, worktreePath)), })); -// Persist on every state change -useStore.subscribe((state) => persistState(state)); +// Persist state changes with debouncing to avoid localStorage thrashing +useStore.subscribe((state) => debouncedPersistState(state)); + +// Flush pending writes synchronously before page unload to prevent data loss. +if (typeof window !== "undefined") { + window.addEventListener("beforeunload", () => { + if (persistTimer !== null) { + clearTimeout(persistTimer); + persistTimer = null; + persistState(useStore.getState()); + } + }); +} export function StoreProvider({ children }: { children: ReactNode }) { useEffect(() => {