diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index e5b86f0c..757d99b4 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -665,6 +665,7 @@ function setGlobalTheme(t: Theme): void { let currentThemeName: string | undefined; let themeWatcher: fs.FSWatcher | undefined; +let themeReloadTimer: NodeJS.Timeout | undefined; let onThemeChangeCallback: (() => void) | undefined; const registeredThemes = new Map(); @@ -730,11 +731,7 @@ export function onThemeChange(callback: () => void): void { } function startThemeWatcher(): void { - // Stop existing watcher if any - if (themeWatcher) { - themeWatcher.close(); - themeWatcher = undefined; - } + stopThemeWatcher(); // Only watch if it's a custom theme (not built-in) if (!currentThemeName || currentThemeName === "dark" || currentThemeName === "light") { @@ -742,45 +739,61 @@ function startThemeWatcher(): void { } const customThemesDir = getCustomThemesDir(); - const themeFile = path.join(customThemesDir, `${currentThemeName}.json`); + const watchedThemeName = currentThemeName; + const watchedFileName = `${watchedThemeName}.json`; + const themeFile = path.join(customThemesDir, watchedFileName); // Only watch if the file exists if (!fs.existsSync(themeFile)) { return; } - try { - themeWatcher = fs.watch(themeFile, (eventType) => { - if (eventType === "change") { - // Debounce rapid changes - setTimeout(() => { - try { - // Reload the theme - setGlobalTheme(loadTheme(currentThemeName!)); - // Notify callback (to invalidate UI) - if (onThemeChangeCallback) { - onThemeChangeCallback(); - } - } catch (_error) { - // Ignore errors (file might be in invalid state while being edited) - } - }, 100); - } else if (eventType === "rename") { - // File was deleted or renamed - fall back to default theme - setTimeout(() => { - if (!fs.existsSync(themeFile)) { - currentThemeName = "dark"; - setGlobalTheme(loadTheme("dark")); - if (themeWatcher) { - themeWatcher.close(); - themeWatcher = undefined; - } - if (onThemeChangeCallback) { - onThemeChangeCallback(); - } - } - }, 100); + const scheduleReload = () => { + if (themeReloadTimer) { + clearTimeout(themeReloadTimer); + } + themeReloadTimer = setTimeout(() => { + themeReloadTimer = undefined; + + // Ignore stale timers after switching themes or stopping the watcher + if (currentThemeName !== watchedThemeName) { + return; } + + // Keep the last successfully loaded theme active if the file is temporarily missing + if (!fs.existsSync(themeFile)) { + return; + } + + try { + // Reload the theme from disk and refresh the registry cache + const reloadedTheme = loadThemeFromPath(themeFile); + registeredThemes.set(watchedThemeName, reloadedTheme); + setGlobalTheme(reloadedTheme); + // Notify callback (to invalidate UI) + if (onThemeChangeCallback) { + onThemeChangeCallback(); + } + } catch (_error) { + // Ignore errors (file might be in invalid state while being edited) + } + }, 100); + }; + + try { + themeWatcher = fs.watch(customThemesDir, (_eventType, filename) => { + if (currentThemeName !== watchedThemeName) { + return; + } + if (!filename) { + scheduleReload(); + return; + } + const changedFile = String(filename); + if (changedFile !== watchedFileName) { + return; + } + scheduleReload(); }); } catch (_error) { // Ignore errors starting watcher @@ -788,6 +801,10 @@ function startThemeWatcher(): void { } export function stopThemeWatcher(): void { + if (themeReloadTimer) { + clearTimeout(themeReloadTimer); + themeReloadTimer = undefined; + } if (themeWatcher) { themeWatcher.close(); themeWatcher = undefined;