Skip to content

fix(desktop): sync Electron's nativeTheme with the in-app theme preference - #493

Merged
Astro-Han merged 2 commits into
apache:mainfrom
GabrielDrapor:fix/nativetheme-sync-sidebar-vibrancy
Jul 4, 2026
Merged

fix(desktop): sync Electron's nativeTheme with the in-app theme preference#493
Astro-Han merged 2 commits into
apache:mainfrom
GabrielDrapor:fix/nativetheme-sync-sidebar-vibrancy

Conversation

@GabrielDrapor

@GabrielDraporGabrielDrapor commented Jul 4, 2026

Copy link
Copy Markdown
Contributor
image

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.

  • Theme switching (apps/desktop/src/renderer/theme.ts) only ever toggles a .dark class on <html> -- a pure renderer/DOM operation with zero IPC to the main process.
  • On macOS the sidebar is deliberately transparent (theme-glass.css) so it can show through the window's native vibrancy: 'sidebar' material (set once at window creation in main-window.ts). That material's light/dark tint is controlled by Electron's own nativeTheme.themeSource, which stays on its default ('system') forever -- nothing ever updated it.
  • So when the OS appearance disagrees with the chosen in-app theme (OS is Dark, user picks Light in-app), the opaque main content repaints correctly (it uses CSS vars that flip with .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:setThemeSource IPC round-trip:

  • main-window.ts: new setThemeSource method on MainWindowController, sets nativeTheme.themeSource.
  • main.ts: registers the window:setThemeSource IPC handler.
  • preload.ts / global.d.ts: exposes window.maka.appWindow.setThemeSource(...).
  • theme.ts: applyTheme() now calls this alongside the existing .dark class 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 typecheck
  • Full desktop test suite: 1821/1821 passing
  • Manually verified: launched with OS in Dark mode, switched in-app theme to Light -- sidebar now repaints to light along with the rest of the UI

Note

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

…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

Copy link
Copy Markdown
Member

LGTM

@Astro-Han

Astro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Hi! No blocking issues from me.

Non-blocking suggestions only:

  • P2 non-blocking: the startup first frame still has a small gap. createWindow() reads themePref before new BrowserWindow, and the sidebar uses macOS vibrancy. Right now nativeTheme.themeSource is only synced later, after the renderer reaches applyTheme() and sends the IPC call. On macOS, with the OS in dark mode and the app preference set to light, the sidebar can still flash dark on cold start.

    I would set nativeTheme.themeSource = themePref === 'auto' ? 'system' : themePref right after reading themePref and before creating the BrowserWindow. The runtime IPC path can still handle later theme changes.

  • P2 non-blocking: this bridge could use a small regression test. The useful contract seems to be: auto/light/dark map to system/light/dark; the main handler accepts valid values, rejects invalid values, and rejects senders that are not the main window.

  • P3 non-blocking: the public window.maka.appWindow.setThemeSource bridge spreads theme ownership across renderer, preload, main-window, and settings/localStorage. A tiny helper like toNativeThemeSource(pref) would make the main-process side easier to keep correct, especially for startup and command-palette paths.

  • P3 non-blocking: the long explanation about DOM .dark vs Electron nativeTheme appears on both sides of the bridge. I would trim it to one short comment, or let the helper name carry most of it.

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

Copy link
Copy Markdown
ContributorAuthor

Thanks @Astro-Han — addressed all four.

P2 (cold-start flash): createWindow() now syncs nativeTheme.themeSource from the resolved themePref right after it's computed, before new BrowserWindow(...) — not only via the later setThemeSource() IPC call. Added a source-contract test asserting the sync happens before the BrowserWindow construction.

P3 (helper): extracted toNativeThemeSource(pref) + isThemePreference(value) into a new theme-source.ts — the one conversion point shared by the startup sync and the IPC handler. As part of this the IPC contract itself changed to carry the raw ThemePreference instead of a pre-mapped value, so the renderer no longer does its own 'auto' → 'system' mapping (preload.ts/global.d.ts updated to match).

P3 (duplicated comment): now lives once, on toNativeThemeSource's docstring; theme.ts just points at it.

P2 (test): added theme-source.test.ts — direct unit tests for the pure mapping/validation (including rejecting the old pre-mapped 'system' value, since the contract changed), plus the source-contract checks mentioned above for setThemeSource's sender/value validation.

Verified: typecheck clean, full desktop suite (1828/1828, +7 new). Also did a live IPC round-trip check confirming setThemeSource resolves cleanly for auto/light/dark and silently rejects garbage input with no main-process error.

@Astro-Han

Copy link
Copy Markdown
Contributor

LGTM, thanks for addressing the review feedback.

@Astro-Han
Astro-Han merged commit a153d31 into apache:mainJul 4, 2026
@GabrielDrapor
GabrielDrapor deleted the fix/nativetheme-sync-sidebar-vibrancy branch July 6, 2026 09:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@GabrielDrapor@likun666661@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(desktop): sync Electron's nativeTheme with the in-app theme preference by GabrielDrapor · Pull Request #493 · apache/maka · GitHub
Skip to content

fix(desktop): sync Electron's nativeTheme with the in-app theme preference - #493

Merged
Astro-Han merged 2 commits into
apache:mainfrom
GabrielDrapor:fix/nativetheme-sync-sidebar-vibrancy
Jul 4, 2026
Merged

fix(desktop): sync Electron's nativeTheme with the in-app theme preference#493
Astro-Han merged 2 commits into
apache:mainfrom
GabrielDrapor:fix/nativetheme-sync-sidebar-vibrancy

Conversation

@GabrielDrapor

@GabrielDraporGabrielDrapor commented Jul 4, 2026

Copy link
Copy Markdown
Contributor
image

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.

  • Theme switching (apps/desktop/src/renderer/theme.ts) only ever toggles a .dark class on <html> -- a pure renderer/DOM operation with zero IPC to the main process.
  • On macOS the sidebar is deliberately transparent (theme-glass.css) so it can show through the window's native vibrancy: 'sidebar' material (set once at window creation in main-window.ts). That material's light/dark tint is controlled by Electron's own nativeTheme.themeSource, which stays on its default ('system') forever -- nothing ever updated it.
  • So when the OS appearance disagrees with the chosen in-app theme (OS is Dark, user picks Light in-app), the opaque main content repaints correctly (it uses CSS vars that flip with .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:setThemeSource IPC round-trip:

  • main-window.ts: new setThemeSource method on MainWindowController, sets nativeTheme.themeSource.
  • main.ts: registers the window:setThemeSource IPC handler.
  • preload.ts / global.d.ts: exposes window.maka.appWindow.setThemeSource(...).
  • theme.ts: applyTheme() now calls this alongside the existing .dark class 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 typecheck
  • Full desktop test suite: 1821/1821 passing
  • Manually verified: launched with OS in Dark mode, switched in-app theme to Light -- sidebar now repaints to light along with the rest of the UI

Note

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

…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

Copy link
Copy Markdown
Member

LGTM

@Astro-Han

Astro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Hi! No blocking issues from me.

Non-blocking suggestions only:

  • P2 non-blocking: the startup first frame still has a small gap. createWindow() reads themePref before new BrowserWindow, and the sidebar uses macOS vibrancy. Right now nativeTheme.themeSource is only synced later, after the renderer reaches applyTheme() and sends the IPC call. On macOS, with the OS in dark mode and the app preference set to light, the sidebar can still flash dark on cold start.

    I would set nativeTheme.themeSource = themePref === 'auto' ? 'system' : themePref right after reading themePref and before creating the BrowserWindow. The runtime IPC path can still handle later theme changes.

  • P2 non-blocking: this bridge could use a small regression test. The useful contract seems to be: auto/light/dark map to system/light/dark; the main handler accepts valid values, rejects invalid values, and rejects senders that are not the main window.

  • P3 non-blocking: the public window.maka.appWindow.setThemeSource bridge spreads theme ownership across renderer, preload, main-window, and settings/localStorage. A tiny helper like toNativeThemeSource(pref) would make the main-process side easier to keep correct, especially for startup and command-palette paths.

  • P3 non-blocking: the long explanation about DOM .dark vs Electron nativeTheme appears on both sides of the bridge. I would trim it to one short comment, or let the helper name carry most of it.

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

Copy link
Copy Markdown
ContributorAuthor

Thanks @Astro-Han — addressed all four.

P2 (cold-start flash): createWindow() now syncs nativeTheme.themeSource from the resolved themePref right after it's computed, before new BrowserWindow(...) — not only via the later setThemeSource() IPC call. Added a source-contract test asserting the sync happens before the BrowserWindow construction.

P3 (helper): extracted toNativeThemeSource(pref) + isThemePreference(value) into a new theme-source.ts — the one conversion point shared by the startup sync and the IPC handler. As part of this the IPC contract itself changed to carry the raw ThemePreference instead of a pre-mapped value, so the renderer no longer does its own 'auto' → 'system' mapping (preload.ts/global.d.ts updated to match).

P3 (duplicated comment): now lives once, on toNativeThemeSource's docstring; theme.ts just points at it.

P2 (test): added theme-source.test.ts — direct unit tests for the pure mapping/validation (including rejecting the old pre-mapped 'system' value, since the contract changed), plus the source-contract checks mentioned above for setThemeSource's sender/value validation.

Verified: typecheck clean, full desktop suite (1828/1828, +7 new). Also did a live IPC round-trip check confirming setThemeSource resolves cleanly for auto/light/dark and silently rejects garbage input with no main-process error.

@Astro-Han

Copy link
Copy Markdown
Contributor

LGTM, thanks for addressing the review feedback.

@Astro-Han
Astro-Han merged commit a153d31 into apache:mainJul 4, 2026
@GabrielDrapor
GabrielDrapor deleted the fix/nativetheme-sync-sidebar-vibrancy branch July 6, 2026 09:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@GabrielDrapor@likun666661@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(desktop): sync Electron's nativeTheme with the in-app theme preference by GabrielDrapor · Pull Request #493 · apache/maka · GitHub
Skip to content

fix(desktop): sync Electron's nativeTheme with the in-app theme preference - #493

Merged
Astro-Han merged 2 commits into
apache:mainfrom
GabrielDrapor:fix/nativetheme-sync-sidebar-vibrancy
Jul 4, 2026
Merged

fix(desktop): sync Electron's nativeTheme with the in-app theme preference#493
Astro-Han merged 2 commits into
apache:mainfrom
GabrielDrapor:fix/nativetheme-sync-sidebar-vibrancy

Conversation

@GabrielDrapor

@GabrielDraporGabrielDrapor commented Jul 4, 2026

Copy link
Copy Markdown
Contributor
image

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.

  • Theme switching (apps/desktop/src/renderer/theme.ts) only ever toggles a .dark class on <html> -- a pure renderer/DOM operation with zero IPC to the main process.
  • On macOS the sidebar is deliberately transparent (theme-glass.css) so it can show through the window's native vibrancy: 'sidebar' material (set once at window creation in main-window.ts). That material's light/dark tint is controlled by Electron's own nativeTheme.themeSource, which stays on its default ('system') forever -- nothing ever updated it.
  • So when the OS appearance disagrees with the chosen in-app theme (OS is Dark, user picks Light in-app), the opaque main content repaints correctly (it uses CSS vars that flip with .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:setThemeSource IPC round-trip:

  • main-window.ts: new setThemeSource method on MainWindowController, sets nativeTheme.themeSource.
  • main.ts: registers the window:setThemeSource IPC handler.
  • preload.ts / global.d.ts: exposes window.maka.appWindow.setThemeSource(...).
  • theme.ts: applyTheme() now calls this alongside the existing .dark class 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 typecheck
  • Full desktop test suite: 1821/1821 passing
  • Manually verified: launched with OS in Dark mode, switched in-app theme to Light -- sidebar now repaints to light along with the rest of the UI

Note

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

…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

Copy link
Copy Markdown
Member

LGTM

@Astro-Han

Astro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Hi! No blocking issues from me.

Non-blocking suggestions only:

  • P2 non-blocking: the startup first frame still has a small gap. createWindow() reads themePref before new BrowserWindow, and the sidebar uses macOS vibrancy. Right now nativeTheme.themeSource is only synced later, after the renderer reaches applyTheme() and sends the IPC call. On macOS, with the OS in dark mode and the app preference set to light, the sidebar can still flash dark on cold start.

    I would set nativeTheme.themeSource = themePref === 'auto' ? 'system' : themePref right after reading themePref and before creating the BrowserWindow. The runtime IPC path can still handle later theme changes.

  • P2 non-blocking: this bridge could use a small regression test. The useful contract seems to be: auto/light/dark map to system/light/dark; the main handler accepts valid values, rejects invalid values, and rejects senders that are not the main window.

  • P3 non-blocking: the public window.maka.appWindow.setThemeSource bridge spreads theme ownership across renderer, preload, main-window, and settings/localStorage. A tiny helper like toNativeThemeSource(pref) would make the main-process side easier to keep correct, especially for startup and command-palette paths.

  • P3 non-blocking: the long explanation about DOM .dark vs Electron nativeTheme appears on both sides of the bridge. I would trim it to one short comment, or let the helper name carry most of it.

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

Copy link
Copy Markdown
ContributorAuthor

Thanks @Astro-Han — addressed all four.

P2 (cold-start flash): createWindow() now syncs nativeTheme.themeSource from the resolved themePref right after it's computed, before new BrowserWindow(...) — not only via the later setThemeSource() IPC call. Added a source-contract test asserting the sync happens before the BrowserWindow construction.

P3 (helper): extracted toNativeThemeSource(pref) + isThemePreference(value) into a new theme-source.ts — the one conversion point shared by the startup sync and the IPC handler. As part of this the IPC contract itself changed to carry the raw ThemePreference instead of a pre-mapped value, so the renderer no longer does its own 'auto' → 'system' mapping (preload.ts/global.d.ts updated to match).

P3 (duplicated comment): now lives once, on toNativeThemeSource's docstring; theme.ts just points at it.

P2 (test): added theme-source.test.ts — direct unit tests for the pure mapping/validation (including rejecting the old pre-mapped 'system' value, since the contract changed), plus the source-contract checks mentioned above for setThemeSource's sender/value validation.

Verified: typecheck clean, full desktop suite (1828/1828, +7 new). Also did a live IPC round-trip check confirming setThemeSource resolves cleanly for auto/light/dark and silently rejects garbage input with no main-process error.

@Astro-Han

Copy link
Copy Markdown
Contributor

LGTM, thanks for addressing the review feedback.

@Astro-Han
Astro-Han merged commit a153d31 into apache:mainJul 4, 2026
@GabrielDrapor
GabrielDrapor deleted the fix/nativetheme-sync-sidebar-vibrancy branch July 6, 2026 09:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@GabrielDrapor@likun666661@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(desktop): sync Electron's nativeTheme with the in-app theme preference by GabrielDrapor · Pull Request #493 · apache/maka · GitHub
Skip to content

fix(desktop): sync Electron's nativeTheme with the in-app theme preference - #493

Merged
Astro-Han merged 2 commits into
apache:mainfrom
GabrielDrapor:fix/nativetheme-sync-sidebar-vibrancy
Jul 4, 2026
Merged

fix(desktop): sync Electron's nativeTheme with the in-app theme preference#493
Astro-Han merged 2 commits into
apache:mainfrom
GabrielDrapor:fix/nativetheme-sync-sidebar-vibrancy

Conversation

@GabrielDrapor

@GabrielDraporGabrielDrapor commented Jul 4, 2026

Copy link
Copy Markdown
Contributor
image

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.

  • Theme switching (apps/desktop/src/renderer/theme.ts) only ever toggles a .dark class on <html> -- a pure renderer/DOM operation with zero IPC to the main process.
  • On macOS the sidebar is deliberately transparent (theme-glass.css) so it can show through the window's native vibrancy: 'sidebar' material (set once at window creation in main-window.ts). That material's light/dark tint is controlled by Electron's own nativeTheme.themeSource, which stays on its default ('system') forever -- nothing ever updated it.
  • So when the OS appearance disagrees with the chosen in-app theme (OS is Dark, user picks Light in-app), the opaque main content repaints correctly (it uses CSS vars that flip with .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:setThemeSource IPC round-trip:

  • main-window.ts: new setThemeSource method on MainWindowController, sets nativeTheme.themeSource.
  • main.ts: registers the window:setThemeSource IPC handler.
  • preload.ts / global.d.ts: exposes window.maka.appWindow.setThemeSource(...).
  • theme.ts: applyTheme() now calls this alongside the existing .dark class 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 typecheck
  • Full desktop test suite: 1821/1821 passing
  • Manually verified: launched with OS in Dark mode, switched in-app theme to Light -- sidebar now repaints to light along with the rest of the UI

Note

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

…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

Copy link
Copy Markdown
Member

LGTM

@Astro-Han

Astro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Hi! No blocking issues from me.

Non-blocking suggestions only:

  • P2 non-blocking: the startup first frame still has a small gap. createWindow() reads themePref before new BrowserWindow, and the sidebar uses macOS vibrancy. Right now nativeTheme.themeSource is only synced later, after the renderer reaches applyTheme() and sends the IPC call. On macOS, with the OS in dark mode and the app preference set to light, the sidebar can still flash dark on cold start.

    I would set nativeTheme.themeSource = themePref === 'auto' ? 'system' : themePref right after reading themePref and before creating the BrowserWindow. The runtime IPC path can still handle later theme changes.

  • P2 non-blocking: this bridge could use a small regression test. The useful contract seems to be: auto/light/dark map to system/light/dark; the main handler accepts valid values, rejects invalid values, and rejects senders that are not the main window.

  • P3 non-blocking: the public window.maka.appWindow.setThemeSource bridge spreads theme ownership across renderer, preload, main-window, and settings/localStorage. A tiny helper like toNativeThemeSource(pref) would make the main-process side easier to keep correct, especially for startup and command-palette paths.

  • P3 non-blocking: the long explanation about DOM .dark vs Electron nativeTheme appears on both sides of the bridge. I would trim it to one short comment, or let the helper name carry most of it.

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

Copy link
Copy Markdown
ContributorAuthor

Thanks @Astro-Han — addressed all four.

P2 (cold-start flash): createWindow() now syncs nativeTheme.themeSource from the resolved themePref right after it's computed, before new BrowserWindow(...) — not only via the later setThemeSource() IPC call. Added a source-contract test asserting the sync happens before the BrowserWindow construction.

P3 (helper): extracted toNativeThemeSource(pref) + isThemePreference(value) into a new theme-source.ts — the one conversion point shared by the startup sync and the IPC handler. As part of this the IPC contract itself changed to carry the raw ThemePreference instead of a pre-mapped value, so the renderer no longer does its own 'auto' → 'system' mapping (preload.ts/global.d.ts updated to match).

P3 (duplicated comment): now lives once, on toNativeThemeSource's docstring; theme.ts just points at it.

P2 (test): added theme-source.test.ts — direct unit tests for the pure mapping/validation (including rejecting the old pre-mapped 'system' value, since the contract changed), plus the source-contract checks mentioned above for setThemeSource's sender/value validation.

Verified: typecheck clean, full desktop suite (1828/1828, +7 new). Also did a live IPC round-trip check confirming setThemeSource resolves cleanly for auto/light/dark and silently rejects garbage input with no main-process error.

@Astro-Han

Copy link
Copy Markdown
Contributor

LGTM, thanks for addressing the review feedback.

@Astro-Han
Astro-Han merged commit a153d31 into apache:mainJul 4, 2026
@GabrielDrapor
GabrielDrapor deleted the fix/nativetheme-sync-sidebar-vibrancy branch July 6, 2026 09:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@GabrielDrapor@likun666661@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(desktop): sync Electron's nativeTheme with the in-app theme preference by GabrielDrapor · Pull Request #493 · apache/maka · GitHub
Skip to content

fix(desktop): sync Electron's nativeTheme with the in-app theme preference - #493

Merged
Astro-Han merged 2 commits into
apache:mainfrom
GabrielDrapor:fix/nativetheme-sync-sidebar-vibrancy
Jul 4, 2026
Merged

fix(desktop): sync Electron's nativeTheme with the in-app theme preference#493
Astro-Han merged 2 commits into
apache:mainfrom
GabrielDrapor:fix/nativetheme-sync-sidebar-vibrancy

Conversation

@GabrielDrapor

@GabrielDraporGabrielDrapor commented Jul 4, 2026

Copy link
Copy Markdown
Contributor
image

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.

  • Theme switching (apps/desktop/src/renderer/theme.ts) only ever toggles a .dark class on <html> -- a pure renderer/DOM operation with zero IPC to the main process.
  • On macOS the sidebar is deliberately transparent (theme-glass.css) so it can show through the window's native vibrancy: 'sidebar' material (set once at window creation in main-window.ts). That material's light/dark tint is controlled by Electron's own nativeTheme.themeSource, which stays on its default ('system') forever -- nothing ever updated it.
  • So when the OS appearance disagrees with the chosen in-app theme (OS is Dark, user picks Light in-app), the opaque main content repaints correctly (it uses CSS vars that flip with .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:setThemeSource IPC round-trip:

  • main-window.ts: new setThemeSource method on MainWindowController, sets nativeTheme.themeSource.
  • main.ts: registers the window:setThemeSource IPC handler.
  • preload.ts / global.d.ts: exposes window.maka.appWindow.setThemeSource(...).
  • theme.ts: applyTheme() now calls this alongside the existing .dark class 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 typecheck
  • Full desktop test suite: 1821/1821 passing
  • Manually verified: launched with OS in Dark mode, switched in-app theme to Light -- sidebar now repaints to light along with the rest of the UI

Note

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

…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

Copy link
Copy Markdown
Member

LGTM

@Astro-Han

Astro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Hi! No blocking issues from me.

Non-blocking suggestions only:

  • P2 non-blocking: the startup first frame still has a small gap. createWindow() reads themePref before new BrowserWindow, and the sidebar uses macOS vibrancy. Right now nativeTheme.themeSource is only synced later, after the renderer reaches applyTheme() and sends the IPC call. On macOS, with the OS in dark mode and the app preference set to light, the sidebar can still flash dark on cold start.

    I would set nativeTheme.themeSource = themePref === 'auto' ? 'system' : themePref right after reading themePref and before creating the BrowserWindow. The runtime IPC path can still handle later theme changes.

  • P2 non-blocking: this bridge could use a small regression test. The useful contract seems to be: auto/light/dark map to system/light/dark; the main handler accepts valid values, rejects invalid values, and rejects senders that are not the main window.

  • P3 non-blocking: the public window.maka.appWindow.setThemeSource bridge spreads theme ownership across renderer, preload, main-window, and settings/localStorage. A tiny helper like toNativeThemeSource(pref) would make the main-process side easier to keep correct, especially for startup and command-palette paths.

  • P3 non-blocking: the long explanation about DOM .dark vs Electron nativeTheme appears on both sides of the bridge. I would trim it to one short comment, or let the helper name carry most of it.

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

Copy link
Copy Markdown
ContributorAuthor

Thanks @Astro-Han — addressed all four.

P2 (cold-start flash): createWindow() now syncs nativeTheme.themeSource from the resolved themePref right after it's computed, before new BrowserWindow(...) — not only via the later setThemeSource() IPC call. Added a source-contract test asserting the sync happens before the BrowserWindow construction.

P3 (helper): extracted toNativeThemeSource(pref) + isThemePreference(value) into a new theme-source.ts — the one conversion point shared by the startup sync and the IPC handler. As part of this the IPC contract itself changed to carry the raw ThemePreference instead of a pre-mapped value, so the renderer no longer does its own 'auto' → 'system' mapping (preload.ts/global.d.ts updated to match).

P3 (duplicated comment): now lives once, on toNativeThemeSource's docstring; theme.ts just points at it.

P2 (test): added theme-source.test.ts — direct unit tests for the pure mapping/validation (including rejecting the old pre-mapped 'system' value, since the contract changed), plus the source-contract checks mentioned above for setThemeSource's sender/value validation.

Verified: typecheck clean, full desktop suite (1828/1828, +7 new). Also did a live IPC round-trip check confirming setThemeSource resolves cleanly for auto/light/dark and silently rejects garbage input with no main-process error.

@Astro-Han

Copy link
Copy Markdown
Contributor

LGTM, thanks for addressing the review feedback.

@Astro-Han
Astro-Han merged commit a153d31 into apache:mainJul 4, 2026
@GabrielDrapor
GabrielDrapor deleted the fix/nativetheme-sync-sidebar-vibrancy branch July 6, 2026 09:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@GabrielDrapor@likun666661@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(desktop): sync Electron's nativeTheme with the in-app theme preference by GabrielDrapor · Pull Request #493 · apache/maka · GitHub
Skip to content

fix(desktop): sync Electron's nativeTheme with the in-app theme preference - #493

Merged
Astro-Han merged 2 commits into
apache:mainfrom
GabrielDrapor:fix/nativetheme-sync-sidebar-vibrancy
Jul 4, 2026
Merged

fix(desktop): sync Electron's nativeTheme with the in-app theme preference#493
Astro-Han merged 2 commits into
apache:mainfrom
GabrielDrapor:fix/nativetheme-sync-sidebar-vibrancy

Conversation

@GabrielDrapor

@GabrielDraporGabrielDrapor commented Jul 4, 2026

Copy link
Copy Markdown
Contributor
image

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.

  • Theme switching (apps/desktop/src/renderer/theme.ts) only ever toggles a .dark class on <html> -- a pure renderer/DOM operation with zero IPC to the main process.
  • On macOS the sidebar is deliberately transparent (theme-glass.css) so it can show through the window's native vibrancy: 'sidebar' material (set once at window creation in main-window.ts). That material's light/dark tint is controlled by Electron's own nativeTheme.themeSource, which stays on its default ('system') forever -- nothing ever updated it.
  • So when the OS appearance disagrees with the chosen in-app theme (OS is Dark, user picks Light in-app), the opaque main content repaints correctly (it uses CSS vars that flip with .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:setThemeSource IPC round-trip:

  • main-window.ts: new setThemeSource method on MainWindowController, sets nativeTheme.themeSource.
  • main.ts: registers the window:setThemeSource IPC handler.
  • preload.ts / global.d.ts: exposes window.maka.appWindow.setThemeSource(...).
  • theme.ts: applyTheme() now calls this alongside the existing .dark class 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 typecheck
  • Full desktop test suite: 1821/1821 passing
  • Manually verified: launched with OS in Dark mode, switched in-app theme to Light -- sidebar now repaints to light along with the rest of the UI

Note

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

…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

Copy link
Copy Markdown
Member

LGTM

@Astro-Han

Astro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Hi! No blocking issues from me.

Non-blocking suggestions only:

  • P2 non-blocking: the startup first frame still has a small gap. createWindow() reads themePref before new BrowserWindow, and the sidebar uses macOS vibrancy. Right now nativeTheme.themeSource is only synced later, after the renderer reaches applyTheme() and sends the IPC call. On macOS, with the OS in dark mode and the app preference set to light, the sidebar can still flash dark on cold start.

    I would set nativeTheme.themeSource = themePref === 'auto' ? 'system' : themePref right after reading themePref and before creating the BrowserWindow. The runtime IPC path can still handle later theme changes.

  • P2 non-blocking: this bridge could use a small regression test. The useful contract seems to be: auto/light/dark map to system/light/dark; the main handler accepts valid values, rejects invalid values, and rejects senders that are not the main window.

  • P3 non-blocking: the public window.maka.appWindow.setThemeSource bridge spreads theme ownership across renderer, preload, main-window, and settings/localStorage. A tiny helper like toNativeThemeSource(pref) would make the main-process side easier to keep correct, especially for startup and command-palette paths.

  • P3 non-blocking: the long explanation about DOM .dark vs Electron nativeTheme appears on both sides of the bridge. I would trim it to one short comment, or let the helper name carry most of it.

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

Copy link
Copy Markdown
ContributorAuthor

Thanks @Astro-Han — addressed all four.

P2 (cold-start flash): createWindow() now syncs nativeTheme.themeSource from the resolved themePref right after it's computed, before new BrowserWindow(...) — not only via the later setThemeSource() IPC call. Added a source-contract test asserting the sync happens before the BrowserWindow construction.

P3 (helper): extracted toNativeThemeSource(pref) + isThemePreference(value) into a new theme-source.ts — the one conversion point shared by the startup sync and the IPC handler. As part of this the IPC contract itself changed to carry the raw ThemePreference instead of a pre-mapped value, so the renderer no longer does its own 'auto' → 'system' mapping (preload.ts/global.d.ts updated to match).

P3 (duplicated comment): now lives once, on toNativeThemeSource's docstring; theme.ts just points at it.

P2 (test): added theme-source.test.ts — direct unit tests for the pure mapping/validation (including rejecting the old pre-mapped 'system' value, since the contract changed), plus the source-contract checks mentioned above for setThemeSource's sender/value validation.

Verified: typecheck clean, full desktop suite (1828/1828, +7 new). Also did a live IPC round-trip check confirming setThemeSource resolves cleanly for auto/light/dark and silently rejects garbage input with no main-process error.

@Astro-Han

Copy link
Copy Markdown
Contributor

LGTM, thanks for addressing the review feedback.

@Astro-Han
Astro-Han merged commit a153d31 into apache:mainJul 4, 2026
@GabrielDrapor
GabrielDrapor deleted the fix/nativetheme-sync-sidebar-vibrancy branch July 6, 2026 09:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@GabrielDrapor@likun666661@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); fix(desktop): sync Electron's nativeTheme with the in-app theme preference by GabrielDrapor · Pull Request #493 · apache/maka · GitHub
Skip to content

fix(desktop): sync Electron's nativeTheme with the in-app theme preference - #493

Merged
Astro-Han merged 2 commits into
apache:mainfrom
GabrielDrapor:fix/nativetheme-sync-sidebar-vibrancy
Jul 4, 2026
Merged

fix(desktop): sync Electron's nativeTheme with the in-app theme preference#493
Astro-Han merged 2 commits into
apache:mainfrom
GabrielDrapor:fix/nativetheme-sync-sidebar-vibrancy

Conversation

@GabrielDrapor

@GabrielDraporGabrielDrapor commented Jul 4, 2026

Copy link
Copy Markdown
Contributor
image

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.

  • Theme switching (apps/desktop/src/renderer/theme.ts) only ever toggles a .dark class on <html> -- a pure renderer/DOM operation with zero IPC to the main process.
  • On macOS the sidebar is deliberately transparent (theme-glass.css) so it can show through the window's native vibrancy: 'sidebar' material (set once at window creation in main-window.ts). That material's light/dark tint is controlled by Electron's own nativeTheme.themeSource, which stays on its default ('system') forever -- nothing ever updated it.
  • So when the OS appearance disagrees with the chosen in-app theme (OS is Dark, user picks Light in-app), the opaque main content repaints correctly (it uses CSS vars that flip with .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:setThemeSource IPC round-trip:

  • main-window.ts: new setThemeSource method on MainWindowController, sets nativeTheme.themeSource.
  • main.ts: registers the window:setThemeSource IPC handler.
  • preload.ts / global.d.ts: exposes window.maka.appWindow.setThemeSource(...).
  • theme.ts: applyTheme() now calls this alongside the existing .dark class 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 typecheck
  • Full desktop test suite: 1821/1821 passing
  • Manually verified: launched with OS in Dark mode, switched in-app theme to Light -- sidebar now repaints to light along with the rest of the UI

Note

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

…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

Copy link
Copy Markdown
Member

LGTM

@Astro-Han

Astro-Han commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Hi! No blocking issues from me.

Non-blocking suggestions only:

  • P2 non-blocking: the startup first frame still has a small gap. createWindow() reads themePref before new BrowserWindow, and the sidebar uses macOS vibrancy. Right now nativeTheme.themeSource is only synced later, after the renderer reaches applyTheme() and sends the IPC call. On macOS, with the OS in dark mode and the app preference set to light, the sidebar can still flash dark on cold start.

    I would set nativeTheme.themeSource = themePref === 'auto' ? 'system' : themePref right after reading themePref and before creating the BrowserWindow. The runtime IPC path can still handle later theme changes.

  • P2 non-blocking: this bridge could use a small regression test. The useful contract seems to be: auto/light/dark map to system/light/dark; the main handler accepts valid values, rejects invalid values, and rejects senders that are not the main window.

  • P3 non-blocking: the public window.maka.appWindow.setThemeSource bridge spreads theme ownership across renderer, preload, main-window, and settings/localStorage. A tiny helper like toNativeThemeSource(pref) would make the main-process side easier to keep correct, especially for startup and command-palette paths.

  • P3 non-blocking: the long explanation about DOM .dark vs Electron nativeTheme appears on both sides of the bridge. I would trim it to one short comment, or let the helper name carry most of it.

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

Copy link
Copy Markdown
ContributorAuthor

Thanks @Astro-Han — addressed all four.

P2 (cold-start flash): createWindow() now syncs nativeTheme.themeSource from the resolved themePref right after it's computed, before new BrowserWindow(...) — not only via the later setThemeSource() IPC call. Added a source-contract test asserting the sync happens before the BrowserWindow construction.

P3 (helper): extracted toNativeThemeSource(pref) + isThemePreference(value) into a new theme-source.ts — the one conversion point shared by the startup sync and the IPC handler. As part of this the IPC contract itself changed to carry the raw ThemePreference instead of a pre-mapped value, so the renderer no longer does its own 'auto' → 'system' mapping (preload.ts/global.d.ts updated to match).

P3 (duplicated comment): now lives once, on toNativeThemeSource's docstring; theme.ts just points at it.

P2 (test): added theme-source.test.ts — direct unit tests for the pure mapping/validation (including rejecting the old pre-mapped 'system' value, since the contract changed), plus the source-contract checks mentioned above for setThemeSource's sender/value validation.

Verified: typecheck clean, full desktop suite (1828/1828, +7 new). Also did a live IPC round-trip check confirming setThemeSource resolves cleanly for auto/light/dark and silently rejects garbage input with no main-process error.

@Astro-Han

Copy link
Copy Markdown
Contributor

LGTM, thanks for addressing the review feedback.

@Astro-Han
Astro-Han merged commit a153d31 into apache:mainJul 4, 2026
@GabrielDrapor
GabrielDrapor deleted the fix/nativetheme-sync-sidebar-vibrancy branch July 6, 2026 09:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@GabrielDrapor@likun666661@Astro-Han