Skip to content

Commit dd8aedb

Browse files
Merge branch 'fix-flashing-1gw9'
2 parents 852a2d1 + 9e36458 commit dd8aedb

3 files changed

Lines changed: 47 additions & 22 deletions

File tree

server/lib/settings.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -627,12 +627,21 @@ export function getClaudeConfig(): Record<string, unknown> {
627627
}
628628
}
629629

630+
// Promise-based lock to serialize writes to ~/.claude.json
631+
// Prevents race conditions when multiple tabs trigger concurrent updates
632+
let claudeConfigLock: Promise<void> = Promise.resolve()
633+
630634
// Update Claude Code config (merges with existing)
635+
// Uses promise chaining to ensure sequential writes and prevent corruption
631636
export function updateClaudeConfig(updates: Record<string, unknown>): void {
632-
const configPath = getClaudeConfigPath()
633-
const current = getClaudeConfig()
634-
const merged = { ...current, ...updates }
635-
fs.writeFileSync(configPath, JSON.stringify(merged, null, 2), 'utf-8')
637+
claudeConfigLock = claudeConfigLock.then(() => {
638+
const configPath = getClaudeConfigPath()
639+
const current = getClaudeConfig()
640+
const merged = { ...current, ...updates }
641+
fs.writeFileSync(configPath, JSON.stringify(merged, null, 2), 'utf-8')
642+
}).catch((err) => {
643+
log.settings.error('Failed to update Claude config', { error: String(err) })
644+
})
636645
}
637646

638647
// Update Claude Code theme if sync is enabled

server/routes/config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,10 @@ app.post('/restart', (c) => {
237237
})
238238

239239
// POST /api/config/sync-theme - Sync theme to Claude Code config
240+
// Debounce to prevent rapid repeated syncs from multiple tabs
241+
let lastSyncedTheme: { theme: 'light' | 'dark'; timestamp: number } | null = null
242+
const SYNC_DEBOUNCE_MS = 1000
243+
240244
app.post('/sync-claude-theme', async (c) => {
241245
try {
242246
const body = await c.req.json<{ resolvedTheme: 'light' | 'dark' }>()
@@ -246,7 +250,17 @@ app.post('/sync-claude-theme', async (c) => {
246250
return c.json({ error: 'resolvedTheme must be "light" or "dark"' }, 400)
247251
}
248252

253+
// Skip if same theme was synced recently (defense against multiple tabs)
254+
const now = Date.now()
255+
if (lastSyncedTheme &&
256+
lastSyncedTheme.theme === resolvedTheme &&
257+
now - lastSyncedTheme.timestamp < SYNC_DEBOUNCE_MS) {
258+
return c.json({ success: true, resolvedTheme, skipped: true })
259+
}
260+
249261
syncClaudeCodeTheme(resolvedTheme)
262+
lastSyncedTheme = { theme: resolvedTheme, timestamp: now }
263+
250264
return c.json({ success: true, resolvedTheme })
251265
} catch (err) {
252266
return c.json({ error: err instanceof Error ? err.message : 'Failed to sync theme' }, 400)

src/hooks/use-theme-sync.ts

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,13 @@ import { fetchJSON } from '@/lib/api'
77
* Hook to sync theme between next-themes and backend settings.
88
* - On mount: applies saved theme preference from backend
99
* - Provides changeTheme function to update both next-themes and backend
10-
* - Optionally syncs theme to Claude Code config when enabled
10+
* - Optionally syncs theme to Claude Code config when enabled (only on explicit user action)
1111
*/
1212
export function useThemeSync() {
1313
const { setTheme, resolvedTheme, theme: currentTheme } = useNextTheme()
1414
const { data: savedTheme, isSuccess } = useTheme()
1515
const { data: syncClaudeCode } = useSyncClaudeCodeTheme()
1616
const updateConfig = useUpdateConfig()
17-
const prevResolvedTheme = useRef<string | undefined>(undefined)
1817
const prevSyncClaudeCode = useRef<boolean | undefined>(undefined)
1918
const hasInitialized = useRef(false)
2019

@@ -42,31 +41,23 @@ export function useThemeSync() {
4241
link.href = favicon
4342
}, [resolvedTheme])
4443

45-
// Sync to Claude Code when:
46-
// 1. Resolved theme changes (if sync is enabled)
47-
// 2. Sync setting is toggled on after initial load (immediate sync with current theme)
44+
// Sync to Claude Code when sync setting is toggled on (immediate sync with current theme)
45+
// NOTE: We intentionally do NOT sync on resolvedTheme changes here to avoid a feedback loop
46+
// when multiple tabs are open. Cross-tab theme sync is handled by next-themes via localStorage.
47+
// Claude sync only happens on explicit user action (changeTheme) or when enabling the toggle.
4848
useEffect(() => {
49-
const themeChanged = resolvedTheme && resolvedTheme !== prevResolvedTheme.current
50-
51-
// Only detect "just enabled" after initial render to avoid syncing on page load
5249
const syncJustEnabled = hasInitialized.current && syncClaudeCode && prevSyncClaudeCode.current === false
5350

54-
// Update refs
55-
prevResolvedTheme.current = resolvedTheme
5651
prevSyncClaudeCode.current = syncClaudeCode
5752
hasInitialized.current = true
5853

59-
// Sync if theme changed while sync is enabled, or if sync was just enabled
60-
if (resolvedTheme && syncClaudeCode && (themeChanged || syncJustEnabled)) {
61-
// Fire and forget - no need to await
54+
if (resolvedTheme && syncJustEnabled) {
6255
fetchJSON('/api/config/sync-claude-theme', {
6356
method: 'POST',
6457
body: JSON.stringify({ resolvedTheme }),
65-
}).catch(() => {
66-
// Silently ignore sync errors
67-
})
58+
}).catch(() => {})
6859
}
69-
}, [resolvedTheme, syncClaudeCode])
60+
}, [syncClaudeCode, resolvedTheme])
7061

7162
// Function to change theme and persist to backend
7263
const changeTheme = useCallback(
@@ -77,8 +68,19 @@ export function useThemeSync() {
7768
key: CONFIG_KEYS.THEME,
7869
value: theme === 'system' ? '' : theme,
7970
})
71+
72+
// Sync to Claude Code if enabled (only on explicit user action, not cross-tab sync)
73+
if (syncClaudeCode) {
74+
const effectiveTheme = theme === 'system'
75+
? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
76+
: theme
77+
fetchJSON('/api/config/sync-claude-theme', {
78+
method: 'POST',
79+
body: JSON.stringify({ resolvedTheme: effectiveTheme }),
80+
}).catch(() => {})
81+
}
8082
},
81-
[setTheme, updateConfig]
83+
[setTheme, updateConfig, syncClaudeCode]
8284
)
8385

8486
return {

0 commit comments

Comments
 (0)