Uh oh!
There was an error while loading. Please reload this page.
fix(desktop): sync Electron's nativeTheme with the in-app theme preference - #493
Conversation
…rence
Switching the in-app theme (Settings → Appearance) only ever flipped the
`.dark` class on <html> in the renderer -- a pure DOM/CSS operation with
no IPC to the main process. On macOS the session-list sidebar is
deliberately transparent (theme-glass.css) so it can show through the
window's native `vibrancy: 'sidebar'` material, and that material's
light/dark tint is controlled by Electron's own `nativeTheme.themeSource`,
which stays on its default ('system') forever since nothing ever touched
it. Result: if the OS appearance disagrees with the chosen in-app theme
(e.g. OS is Dark, user picks Light), the opaque main content repaints
correctly but the vibrancy-backed sidebar keeps showing the *system*
theme's tint.
Added a `window:setThemeSource` IPC round-trip (main-window.ts, main.ts,
preload.ts, global.d.ts) so `theme.ts`'s `applyTheme()` also calls
`nativeTheme.themeSource = ...` in the main process whenever the
preference changes, keeping the vibrancy material -- and any other
Electron-native chrome -- in sync with the app's own theme choice.
Verified: typecheck clean, full desktop suite (1821/1821) passing.likun666661
commented
Jul 4, 2026
LGTM |
Hi! No blocking issues from me. Non-blocking suggestions only:
Suggested checks: the focused tests plus typecheck. I do not think this needs a slow E2E. |
Addresses non-blocking review feedback on apache#493 (Astro-Han): - P2: createWindow() now syncs nativeTheme.themeSource from the resolved themePref before constructing the BrowserWindow, not only via the renderer's later setThemeSource() IPC call. Previously, on a cold start where the OS appearance disagreed with the persisted in-app preference, the vibrancy-backed sidebar could flash the *system* theme's tint for the first frame or two before the renderer reached applyTheme() and sent the IPC call. - P3: extracted the pref->themeSource mapping and validation into a new apps/desktop/src/main/theme-source.ts (toNativeThemeSource, isThemePreference) -- a single conversion point shared by both the createWindow() startup sync and the setThemeSource() IPC handler, so the two call sites can't drift. The IPC contract itself changed to carry the raw ThemePreference ('auto'|'light'|'dark') instead of a pre-mapped Electron value; the renderer no longer does its own 'auto' -> 'system' mapping (preload.ts / global.d.ts updated to match). - P3: trimmed the duplicated DOM-`.dark`-vs-native-chrome explanation down to one full copy (on toNativeThemeSource's docstring) with a one-line pointer from theme.ts, instead of a paragraph repeated on both sides of the bridge. - P2: added theme-source.test.ts -- direct unit tests for the pure mapping/validation functions (toNativeThemeSource, isThemePreference, including rejecting the old pre-mapped 'system' value now that the contract carries ThemePreference), plus source-contract checks that setThemeSource validates the sender + preference and that the createWindow sync runs before `new BrowserWindow(`. Verified: typecheck clean, full desktop suite (1828/1828, +7 new). Live IPC round-trip check confirms setThemeSource resolves cleanly for all three valid preferences and silently rejects an invalid one (no main-process error). Screenshot capture confirms the app still builds and renders correctly end to end.
GabrielDrapor
commented
Jul 4, 2026
Thanks @Astro-Han — addressed all four. P2 (cold-start flash): P3 (helper): extracted P3 (duplicated comment): now lives once, on P2 (test): added Verified: typecheck clean, full desktop suite (1828/1828, +7 new). Also did a live IPC round-trip check confirming |
Astro-Han
commented
Jul 4, 2026
LGTM, thanks for addressing the review feedback. |
Summary
Reported bug: switching the in-app theme from dark back to light left the left session-list sidebar stuck dark, while the rest of the UI repainted correctly to light.
apps/desktop/src/renderer/theme.ts) only ever toggles a.darkclass on<html>-- a pure renderer/DOM operation with zero IPC to the main process.theme-glass.css) so it can show through the window's nativevibrancy: 'sidebar'material (set once at window creation inmain-window.ts). That material's light/dark tint is controlled by Electron's ownnativeTheme.themeSource, which stays on its default ('system') forever -- nothing ever updated it..dark), but the vibrancy-backed sidebar keeps showing the system theme's tint, since that's a native/OS-level material, not something CSS variables can reach.Fix
Added a
window:setThemeSourceIPC round-trip:main-window.ts: newsetThemeSourcemethod onMainWindowController, setsnativeTheme.themeSource.main.ts: registers thewindow:setThemeSourceIPC handler.preload.ts/global.d.ts: exposeswindow.maka.appWindow.setThemeSource(...).theme.ts:applyTheme()now calls this alongside the existing.darkclass toggle, mapping the app's'auto'|'light'|'dark'preference to Electron's'system'|'light'|'dark'themeSource.This keeps the vibrancy material (and any other Electron-native chrome) in sync with the in-app theme choice, instead of silently following the OS setting forever.
Test plan
npm --workspace @maka/desktop run typecheckNote
A related fix for duplicate app instances (adding
app.requestSingleInstanceLock()) was developed alongside this one but is intentionally not included here -- it'll land in a separate follow-up PR.Co-Authored-By: Claude noreply@anthropic.com