void composer.pasteClipboardImage()}
+ onPasteClipboardImage={opts => composer.pasteClipboardImage(opts)}
onPickFiles={() => void composer.pickContextPaths('file')}
onPickFolders={() => void composer.pickContextPaths('folder')}
onPickImages={() => void composer.pickImages()}
diff --git a/apps/desktop/src/app/shell/app-shell.tsx b/apps/desktop/src/app/shell/app-shell.tsx
index b0981681c6c..56deb913952 100644
--- a/apps/desktop/src/app/shell/app-shell.tsx
+++ b/apps/desktop/src/app/shell/app-shell.tsx
@@ -21,6 +21,7 @@ import { isSecondaryWindow } from '@/store/windows'
import { SIDEBAR_COLLAPSE_MEDIA_QUERY } from '../layout-constants'
+import { useWindowControlsOverlayWidth } from './hooks/use-window-controls-overlay-width'
import { KeybindPanel } from './keybind-panel'
import { StatusbarControls, type StatusbarItem } from './statusbar-controls'
import { TITLEBAR_HEIGHT, titlebarControlsPosition } from './titlebar'
@@ -86,12 +87,26 @@ export function AppShell({
// tool cluster. Gate on isSecondaryWindow, never the narrower new-session flag.
const hideTitlebarControls = isSecondaryWindow()
const titlebarControls = titlebarControlsPosition(connection?.windowButtonPosition, isFullscreen)
- // Width Windows/Linux reserve for the OS-painted min/max/close overlay (zero
- // on macOS, where window controls sit on the left and are reported via
+ // Width Windows/WSLg reserve for the native min/max/close overlay (zero on
+ // macOS, where window controls sit on the left and are reported via
// windowButtonPosition instead). The right tool cluster has to clear them.
- const nativeOverlayWidth = connection?.nativeOverlayWidth ?? 0
+ // Prefer the EXACT width measured from the live Window Controls Overlay
+ // (precise + self-correcting across DPI/host themes); fall back to the static
+ // reservation the main process sends when the WCO API isn't available.
+ const measuredOverlayWidth = useWindowControlsOverlayWidth()
+ const staticOverlayWidth = connection?.nativeOverlayWidth ?? 0
+ const nativeOverlayWidth = measuredOverlayWidth ?? staticOverlayWidth
const titlebarToolsRight = nativeOverlayWidth > 0 ? `${nativeOverlayWidth}px` : '0.75rem'
+ // When the native window controls overlay our titlebar band — Windows and
+ // WSLg both paint Electron's Window Controls Overlay and report
+ // nativeOverlayWidth > 0 — the right rail's editor-style tab strip (which
+ // normally lives IN that band) would render at y=0 under the fixed titlebar
+ // tool cluster and collide with it. Drop the right rail one titlebar-height so
+ // it opens BELOW the band. macOS / plain Linux paint no overlay → 0 inset,
+ // layout byte-for-byte unchanged. Consumed as --right-rail-top-inset.
+ const rightRailTopInset = nativeOverlayWidth > 0 ? 'var(--titlebar-height)' : '0px'
+
// The inset clears the top-left titlebar buttons when nothing covers the
// window's left edge. Default layout: the sessions sidebar sits there.
// Flipped layout: the file browser does instead. Both force-collapse to a
@@ -159,6 +174,9 @@ export function AppShell({
'--titlebar-controls-top': `${titlebarControls.top}px`,
'--titlebar-tools-right': titlebarToolsRight,
'--titlebar-tools-width': titlebarToolsWidth,
+ // Drops the right rail below the titlebar band when the OS/host paints
+ // window controls over it (Windows/WSLg); 0px elsewhere.
+ '--right-rail-top-inset': rightRailTopInset,
// Anchor for the pane-tool cluster's right edge in TitlebarControls.
// Sourced from the layout store rather than the PaneShell-emitted
// --pane-*-width vars because the titlebar is a sibling of PaneShell
@@ -171,6 +189,13 @@ export function AppShell({
)}
+ {nativeOverlayWidth > 0 && (
+
+ )}
+
DOMRect
+ addEventListener: (type: 'geometrychange', cb: () => void) => void
+ removeEventListener: (type: 'geometrychange', cb: () => void) => void
+}
+
+const overlay = () =>
+ (navigator as Navigator & { windowControlsOverlay?: WindowControlsOverlayLike }).windowControlsOverlay ?? null
+
+function measure(wco: WindowControlsOverlayLike | null): number | null {
+ const rect = wco?.visible ? wco.getTitlebarAreaRect() : null
+
+ // No overlay, or it isn't laid out yet.
+ if (!rect?.width) {
+ return null
+ }
+
+ const width = Math.round(window.innerWidth - rect.right)
+
+ return width > 0 ? width : null
+}
+
+/**
+ * Live width (px) of the right-side native window-controls overlay, or null when
+ * the platform/build exposes no overlay (caller should use the static fallback).
+ */
+export function useWindowControlsOverlayWidth(): number | null {
+ const [width, setWidth] = useState
(() => measure(overlay()))
+
+ useEffect(() => {
+ const wco = overlay()
+
+ if (!wco) {
+ return
+ }
+
+ const update = () => setWidth(measure(wco))
+
+ // Re-measure on overlay geometry changes (maximize/restore, DPI) and on
+ // window resize (innerWidth feeds the calc).
+ wco.addEventListener('geometrychange', update)
+ window.addEventListener('resize', update)
+ update()
+
+ return () => {
+ wco.removeEventListener('geometrychange', update)
+ window.removeEventListener('resize', update)
+ }
+ }, [])
+
+ return width
+}
diff --git a/apps/desktop/src/app/shell/titlebar-controls.tsx b/apps/desktop/src/app/shell/titlebar-controls.tsx
index d0ace1c8838..fb0cae4307e 100644
--- a/apps/desktop/src/app/shell/titlebar-controls.tsx
+++ b/apps/desktop/src/app/shell/titlebar-controls.tsx
@@ -175,7 +175,7 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
{visiblePaneTools.length > 0 && (
{visiblePaneTools.map(tool => (
diff --git a/apps/desktop/src/themes/context.tsx b/apps/desktop/src/themes/context.tsx
index 8dec1c9e0a8..575316633fa 100644
--- a/apps/desktop/src/themes/context.tsx
+++ b/apps/desktop/src/themes/context.tsx
@@ -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 => ({
'--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