| ... | @@ -63,6 +63,8 @@ function readPersistedState(): AppState { | ... | @@ -63,6 +63,8 @@ function readPersistedState(): AppState { |
| 63 | } | 63 | } |
| 64 | } | 64 | } |
| 65 | | 65 | |
| | 66 | let legacyKeysCleanedUp = false; |
| | 67 | |
| 66 | function persistState(state: AppState): void { | 68 | function persistState(state: AppState): void { |
| 67 | if (typeof window === "undefined") return; | 69 | if (typeof window === "undefined") return; |
| 68 | try { | 70 | try { |
| ... | @@ -74,14 +76,29 @@ function persistState(state: AppState): void { | ... | @@ -74,14 +76,29 @@ function persistState(state: AppState): void { |
| 74 | .map((project) => project.cwd), | 76 | .map((project) => project.cwd), |
| 75 | }), | 77 | }), |
| 76 | ); | 78 | ); |
| 77 | for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) { | 79 | if (!legacyKeysCleanedUp) { |
| 78 | window.localStorage.removeItem(legacyKey); | 80 | legacyKeysCleanedUp = true; |
| | 81 | for (const legacyKey of LEGACY_PERSISTED_STATE_KEYS) { |
| | 82 | window.localStorage.removeItem(legacyKey); |
| | 83 | } |
| 79 | } | 84 | } |
| 80 | } catch { | 85 | } catch { |
| 81 | // Ignore quota/storage errors to avoid breaking chat UX. | 86 | // Ignore quota/storage errors to avoid breaking chat UX. |
| 82 | } | 87 | } |
| 83 | } | 88 | } |
| 84 | | 89 | |
| | 90 | let persistTimer: ReturnType<typeof setTimeout> | null = null; |
| | 91 | |
| | 92 | function debouncedPersistState(state: AppState): void { |
| | 93 | if (persistTimer !== null) { |
| | 94 | clearTimeout(persistTimer); |
| | 95 | } |
| | 96 | persistTimer = setTimeout(() => { |
| | 97 | persistTimer = null; |
| | 98 | persistState(state); |
| | 99 | }, 500); |
| | 100 | } |
| | 101 | |
| 85 | // ── Pure helpers ────────────────────────────────────────────────────── | 102 | // ── Pure helpers ────────────────────────────────────────────────────── |
| 86 | | 103 | |
| 87 | function updateThread( | 104 | function updateThread( |
| ... | @@ -395,8 +412,19 @@ export const useStore = create<AppStore>((set) => ({ | ... | @@ -395,8 +412,19 @@ export const useStore = create<AppStore>((set) => ({ |
| 395 | set((state) => setThreadBranch(state, threadId, branch, worktreePath)), | 412 | set((state) => setThreadBranch(state, threadId, branch, worktreePath)), |
| 396 | })); | 413 | })); |
| 397 | | 414 | |
| 398 | // Persist on every state change | 415 | // Persist state changes with debouncing to avoid localStorage thrashing |
| 399 | useStore.subscribe((state) => persistState(state)); | 416 | useStore.subscribe((state) => debouncedPersistState(state)); |
| | 417 | |
| | 418 | // Flush pending writes synchronously before page unload to prevent data loss. |
| | 419 | if (typeof window !== "undefined") { |
| | 420 | window.addEventListener("beforeunload", () => { |
| | 421 | if (persistTimer !== null) { |
| | 422 | clearTimeout(persistTimer); |
| | 423 | persistTimer = null; |
| | 424 | persistState(useStore.getState()); |
| | 425 | } |
| | 426 | }); |
| | 427 | } |
| 400 | | 428 | |
| 401 | export function StoreProvider({ children }: { children: ReactNode }) { | 429 | export function StoreProvider({ children }: { children: ReactNode }) { |
| 402 | useEffect(() => { | 430 | useEffect(() => { |