File size: 6,228 Bytes
1dbc34b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | # 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
|