fix(desktop): transparent WCO titlebar chrome on Windows/WSLg

Use a transparent native overlay so renderer chrome shows through the min/max/close
band. Sync window pre-paint bg to the computed chrome mix.
This commit is contained in:
Brooklyn Nicholson 2026-06-25 23:50:59 -05:00
parent 3b1344c18c
commit 76074b2145
3 changed files with 30 additions and 19 deletions

View file

@ -527,34 +527,30 @@ function getWindowBackgroundColor() {
return nativeTheme.shouldUseDarkColors ? '#111111' : '#f7f7f7'
}
// Transparent WCO — renderer chrome shows through. rgba(0,0,0,0) can fall back
// to GetFrameColor() on some Electron builds; rgba(1,0,0,0) is the escape hatch.
const TITLEBAR_OVERLAY_COLOR = 'rgba(1, 0, 0, 0)'
function getTitleBarOverlayOptions() {
if (IS_MAC) {
return { height: TITLEBAR_HEIGHT }
}
// Window Controls Overlay: Windows paints it natively, and WSLg honors it too
// (it renders through a real compositor), so keep it enabled there — disabling
// it removes the min/max/close buttons entirely. Only plain (non-WSL) Linux,
// where some WMs/builds don't support WCO, falls through to no overlay; the
// frameless titleBarStyle 'hidden' still applies.
// Windows + WSLg paint WCO natively; plain Linux disables it (frameless hidden
// titlebar still applies).
if (!IS_WINDOWS && !IS_WSL) {
return false
}
if (rendererTitleBarTheme) {
return {
color: rendererTitleBarTheme.background,
height: TITLEBAR_HEIGHT,
symbolColor: rendererTitleBarTheme.foreground
}
}
const useDarkColors = nativeTheme.shouldUseDarkColors
return {
color: useDarkColors ? '#111111' : '#f7f7f7',
color: TITLEBAR_OVERLAY_COLOR,
height: TITLEBAR_HEIGHT,
symbolColor: useDarkColors ? '#f7f7f7' : '#242424'
symbolColor:
rendererTitleBarTheme && isHexColor(rendererTitleBarTheme.foreground)
? rendererTitleBarTheme.foreground
: nativeTheme.shouldUseDarkColors
? '#f7f7f7'
: '#242424'
}
}

View file

@ -189,6 +189,13 @@ export function AppShell({
<TitlebarControls leftTools={leftTitlebarTools} onOpenSettings={onOpenSettings} tools={titlebarTools} />
)}
{nativeOverlayWidth > 0 && (
<div
aria-hidden
className="pointer-events-none fixed inset-x-0 top-0 z-[4] h-(--titlebar-height) border-b border-(--ui-stroke-tertiary) bg-(--ui-chat-surface-background)"
/>
)}
<main className="relative z-3 flex min-h-0 w-full flex-1 flex-col overflow-hidden transition-none">
<PaneShell className="min-h-0 flex-1">
<div

View file

@ -157,6 +157,12 @@ function renderedModeFor(colors: DesktopThemeColors, mode: 'light' | 'dark'): 'l
// Per-mode mix knobs. Light/dark fallbacks live in styles.css `:root` /
// `:root.dark`; setting them inline keeps active-skin overrides surviving
// the boot-time paint.
// styles.css --theme-neutral-chrome — keep in sync.
const NEUTRAL_CHROME = { light: '#f3f3f3', dark: '#0d0d0e' } as const
const chromeBackground = (background: string, isDark: boolean) =>
mix(background, NEUTRAL_CHROME[isDark ? 'dark' : 'light'], isDark ? 0.26 : 0.08)
const mixesFor = (isDark: boolean): Record<string, string> => ({
'--theme-mix-chrome': isDark ? '74%' : '92%',
'--theme-mix-sidebar': '100%',
@ -222,8 +228,10 @@ function applyTheme(theme: DesktopTheme, mode: 'light' | 'dark') {
root.style.setProperty(k, v)
}
const chromeBg = chromeBackground(c.background, isDark)
window.hermesDesktop?.setTitleBarTheme?.({
background: c.background,
background: chromeBg,
foreground: c.foreground
})
@ -231,7 +239,7 @@ function applyTheme(theme: DesktopTheme, mode: 'light' | 'dark') {
// they let a brand-new window paint the themed background on its very first
// frame, before this module has even loaded.
try {
window.localStorage.setItem('hermes-boot-background', c.background)
window.localStorage.setItem('hermes-boot-background', chromeBg)
window.localStorage.setItem('hermes-boot-color-scheme', rendered)
} catch {
// Storage may be unavailable (private mode / quota); the inline script