Spaces:
Paused
Paused
| # Settings API-First Migration | |
| ## Overview | |
| This document summarizes the migration from localStorage-based settings persistence to an API-first approach. The goal was to ensure settings are consistent between Electron and web modes by using the server's `settings.json` as the single source of truth. | |
| ## Problem | |
| Previously, settings were stored in two places: | |
| 1. **Browser localStorage** (via Zustand persist middleware) - isolated per browser/Electron instance | |
| 2. **Server files** (`{DATA_DIR}/settings.json`) | |
| This caused settings drift between Electron and web modes since each had its own localStorage. | |
| ## Solution | |
| All settings are now: | |
| 1. **Fetched from the server API** on app startup | |
| 2. **Synced back to the server API** when changed (with debouncing) | |
| 3. **No longer cached in localStorage** (persist middleware removed) | |
| ## Files Changed | |
| ### New Files | |
| #### `apps/ui/src/hooks/use-settings-sync.ts` | |
| New hook that: | |
| - Waits for migration to complete before starting | |
| - Subscribes to Zustand store changes | |
| - Debounces sync to server (1000ms delay) | |
| - Handles special case for `currentProjectId` (extracted from `currentProject` object) | |
| ### Modified Files | |
| #### `apps/ui/src/store/app-store.ts` | |
| - Removed `persist` middleware from Zustand store | |
| - Added new state fields: | |
| - `worktreePanelCollapsed: boolean` | |
| - `lastProjectDir: string` | |
| - `recentFolders: string[]` | |
| - Added corresponding setter actions | |
| #### `apps/ui/src/store/setup-store.ts` | |
| - Removed `persist` middleware from Zustand store | |
| #### `apps/ui/src/hooks/use-settings-migration.ts` | |
| Complete rewrite to: | |
| - Run in both Electron and web modes (not just Electron) | |
| - Parse localStorage data and merge with server data | |
| - Prefer server data, but use localStorage for missing arrays (projects, profiles, etc.) | |
| - Export `waitForMigrationComplete()` for coordination with sync hook | |
| - Handle `currentProjectId` to restore the currently open project | |
| #### `apps/ui/src/App.tsx` | |
| - Added `useSettingsSync` hook | |
| - Wait for migration to complete before rendering router (prevents race condition) | |
| - Show loading state while settings are being fetched | |
| #### `apps/ui/src/routes/__root.tsx` | |
| - Removed persist middleware hydration checks (no longer needed) | |
| - Set `setupHydrated` to `true` by default | |
| #### `apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx` | |
| - Changed from localStorage to app store for `worktreePanelCollapsed` | |
| #### `apps/ui/src/components/dialogs/file-browser-dialog.tsx` | |
| - Changed from localStorage to app store for `recentFolders` | |
| #### `apps/ui/src/lib/workspace-config.ts` | |
| - Changed from localStorage to app store for `lastProjectDir` | |
| #### `libs/types/src/settings.ts` | |
| - Added `currentProjectId: string | null` to `GlobalSettings` interface | |
| - Added to `DEFAULT_GLOBAL_SETTINGS` | |
| ## Settings Synced to Server | |
| The following fields are synced to the server when they change: | |
| ```typescript | |
| const SETTINGS_FIELDS_TO_SYNC = [ | |
| 'theme', | |
| 'sidebarOpen', | |
| 'chatHistoryOpen', | |
| 'maxConcurrency', | |
| 'defaultSkipTests', | |
| 'enableDependencyBlocking', | |
| 'skipVerificationInAutoMode', | |
| 'useWorktrees', | |
| 'defaultPlanningMode', | |
| 'defaultRequirePlanApproval', | |
| 'muteDoneSound', | |
| 'enhancementModel', | |
| 'validationModel', | |
| 'phaseModels', | |
| 'enabledCursorModels', | |
| 'cursorDefaultModel', | |
| 'autoLoadClaudeMd', | |
| 'keyboardShortcuts', | |
| 'mcpServers', | |
| 'promptCustomization', | |
| 'projects', | |
| 'trashedProjects', | |
| 'currentProjectId', | |
| 'projectHistory', | |
| 'projectHistoryIndex', | |
| 'lastSelectedSessionByProject', | |
| 'worktreePanelCollapsed', | |
| 'lastProjectDir', | |
| 'recentFolders', | |
| ]; | |
| ``` | |
| ## Data Flow | |
| ### On App Startup | |
| ``` | |
| 1. App mounts | |
| βββ Shows "Loading settings..." screen | |
| 2. useSettingsMigration runs | |
| βββ Waits for API key initialization | |
| βββ Reads localStorage data (if any) | |
| βββ Fetches settings from server API | |
| βββ Merges data (prefers server, uses localStorage for missing arrays) | |
| βββ Hydrates Zustand store (including currentProject from currentProjectId) | |
| βββ Syncs merged data back to server (if needed) | |
| βββ Signals completion via waitForMigrationComplete() | |
| 3. useSettingsSync initializes | |
| βββ Waits for migration to complete | |
| βββ Stores initial state hash | |
| βββ Starts subscribing to store changes | |
| 4. Router renders | |
| βββ Root layout reads currentProject (now properly set) | |
| βββ Navigates to /board if project was open | |
| ``` | |
| ### On Settings Change | |
| ``` | |
| 1. User changes a setting | |
| βββ Zustand store updates | |
| 2. useSettingsSync detects change | |
| βββ Debounces for 1000ms | |
| βββ Syncs to server via API | |
| 3. Server writes to settings.json | |
| ``` | |
| ## Migration Logic | |
| When merging localStorage with server data: | |
| 1. **Server has data** β Use server data as base | |
| 2. **Server missing arrays** (projects, mcpServers, etc.) β Use localStorage arrays | |
| 3. **Server missing objects** (lastSelectedSessionByProject) β Use localStorage objects | |
| 4. **Simple values** (lastProjectDir, currentProjectId) β Use localStorage if server is empty | |
| ## Exported Functions | |
| ### `useSettingsMigration()` | |
| Hook that handles initial settings hydration. Returns: | |
| - `checked: boolean` - Whether hydration is complete | |
| - `migrated: boolean` - Whether data was migrated from localStorage | |
| - `error: string | null` - Error message if failed | |
| ### `useSettingsSync()` | |
| Hook that handles ongoing sync. Returns: | |
| - `loaded: boolean` - Whether sync is initialized | |
| - `syncing: boolean` - Whether currently syncing | |
| - `error: string | null` - Error message if failed | |
| ### `waitForMigrationComplete()` | |
| Returns a Promise that resolves when migration is complete. Used for coordination. | |
| ### `forceSyncSettingsToServer()` | |
| Manually triggers an immediate sync to server. | |
| ### `refreshSettingsFromServer()` | |
| Fetches latest settings from server and updates store. | |
| ## Testing | |
| All 1001 server tests pass after these changes. | |
| ## Notes | |
| - **sessionStorage** is still used for session-specific state (splash screen shown, auto-mode state) | |
| - **Terminal layouts** are stored in the app store per-project (not synced to API - considered transient UI state) | |
| - The server's `{DATA_DIR}/settings.json` is the single source of truth | |