From 38871d3e560f7de8837cb367cec8c3007cb282d6 Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Mon, 9 Nov 2026 21:42:27 -0500 Subject: [PATCH] fix(desktop): show the Lotus titlebar only when the native frame is really gone On Linux / Wayland the desktop can't strip the window frame at runtime, so turning on custom window chrome drew the Lotus titlebar inside KDE's frame (two sets of window buttons). The desktop's `set_custom_chrome` now answers whether the frame is stripped right now; the titlebar and layout follow that answer (falling back to the setting for older desktop builds), so there's never two titlebars or none. The setting says it applies on next start on Linux / Wayland. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA --- src/app/features/desktop/TitleBar.tsx | 4 +-- src/app/features/settings/general/General.tsx | 2 +- src/app/hooks/useTauriWindowChrome.ts | 29 +++++++++++++++---- src/app/pages/App.tsx | 4 +-- src/app/state/customWindowChrome.ts | 18 ++++++++++++ 5 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/app/features/desktop/TitleBar.tsx b/src/app/features/desktop/TitleBar.tsx index 41f3d379f..52ac44b7e 100644 --- a/src/app/features/desktop/TitleBar.tsx +++ b/src/app/features/desktop/TitleBar.tsx @@ -1,7 +1,7 @@ import React, { MouseEvent, ReactNode } from 'react'; import { useAtomValue } from 'jotai'; import { Text } from 'folds'; -import { customWindowChromeAtom } from '../../state/customWindowChrome'; +import { customChromeShownAtom } from '../../state/customWindowChrome'; import { invokeTauri, isTauri } from '../../hooks/useTauri'; import * as css from './TitleBar.css'; @@ -75,7 +75,7 @@ function ControlButton({ label, glyph, onClick, close }: ControlButtonProps) { * real traffic lights — are stripped while custom chrome is on. */ export function TitleBar() { - const enabled = useAtomValue(customWindowChromeAtom); + const enabled = useAtomValue(customChromeShownAtom); if (!isTauri() || !enabled) return null; diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index 1767dc246..892321e06 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -160,7 +160,7 @@ function DesktopChromeSetting() { } /> diff --git a/src/app/hooks/useTauriWindowChrome.ts b/src/app/hooks/useTauriWindowChrome.ts index 0f42c6bf1..28ce34361 100644 --- a/src/app/hooks/useTauriWindowChrome.ts +++ b/src/app/hooks/useTauriWindowChrome.ts @@ -1,7 +1,7 @@ import { useEffect } from 'react'; -import { useAtomValue } from 'jotai'; -import { customWindowChromeAtom } from '../state/customWindowChrome'; -import { invokeTauri, isTauri } from './useTauri'; +import { useAtomValue, useSetAtom } from 'jotai'; +import { customWindowChromeAtom, nativeChromeActiveAtom } from '../state/customWindowChrome'; +import { isTauri, tauriInvoke } from './useTauri'; /** * P5-47 — drive the native window frame from the `customWindowChromeAtom`. @@ -11,12 +11,29 @@ import { invokeTauri, isTauri } from './useTauri'; * web `` can take over, `enabled = false` restores the native frame. * No-op in the browser (`isTauri()` guard), so it's safe to call unconditionally * from the app shell. + * + * The command answers whether the native frame is stripped *now* (on Linux / + * Wayland the frame only changes at the next launch); that drives whether the + * Lotus titlebar is shown. Older desktop builds answer nothing, so the setting + * is used as before. */ export function useTauriWindowChrome(): void { const enabled = useAtomValue(customWindowChromeAtom); + const setNativeActive = useSetAtom(nativeChromeActiveAtom); useEffect(() => { - if (!isTauri()) return; - invokeTauri('set_custom_chrome', { enabled }); - }, [enabled]); + const invoke = isTauri() ? tauriInvoke() : undefined; + if (!invoke) return undefined; + let cancelled = false; + invoke('set_custom_chrome', { enabled }) + .then((active) => { + if (!cancelled) setNativeActive(typeof active === 'boolean' ? active : enabled); + }) + .catch(() => { + if (!cancelled) setNativeActive(enabled); + }); + return () => { + cancelled = true; + }; + }, [enabled, setNativeActive]); } diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx index c005066b3..79b519408 100644 --- a/src/app/pages/App.tsx +++ b/src/app/pages/App.tsx @@ -31,7 +31,7 @@ import { useSessionAppeared } from '../hooks/useSessionAppeared'; import { useTauriWindowChrome } from '../hooks/useTauriWindowChrome'; import { isTauri } from '../hooks/useTauri'; import { TitleBar } from '../features/desktop/TitleBar'; -import { customWindowChromeAtom } from '../state/customWindowChrome'; +import { customChromeShownAtom } from '../state/customWindowChrome'; import { SeasonalEffect } from '../components/seasonal/SeasonalEffect'; import { applyCustomAccent, removeCustomAccent } from '../utils/accentColor'; import { zIndices } from '../styles/zIndex'; @@ -110,7 +110,7 @@ function TauriEffects() { // default, and always in the browser) this returns children unchanged, so there // is zero layout impact for everyone else. function DesktopChrome({ children }: { children: ReactNode }) { - const customChrome = useAtomValue(customWindowChromeAtom); + const customChrome = useAtomValue(customChromeShownAtom); useTauriWindowChrome(); const useChrome = isTauri() && customChrome; // Keep the wrapper element structure STABLE across the toggle so flipping the diff --git a/src/app/state/customWindowChrome.ts b/src/app/state/customWindowChrome.ts index ea8d3f7a3..ab0fdb642 100644 --- a/src/app/state/customWindowChrome.ts +++ b/src/app/state/customWindowChrome.ts @@ -1,3 +1,4 @@ +import { atom } from 'jotai'; import { atomWithLocalStorage, getLocalStorageItem, @@ -20,3 +21,20 @@ export const customWindowChromeAtom = atomWithLocalStorage( (key) => getLocalStorageItem(key, false), (key, value) => setLocalStorageItem(key, value), ); + +/** + * [cinny-desktop: Linux custom chrome] Whether the native frame is **actually** + * stripped right now, as reported by the desktop's `set_custom_chrome`. It can + * differ from the setting: on Linux / Wayland the frame only changes when the + * app restarts. `null` until the desktop has answered (or on desktop builds + * that don't report it). + */ +export const nativeChromeActiveAtom = atom(null); + +/** + * Show the Lotus titlebar only when the native frame is really gone (never two + * titlebars, never none): the desktop's report, else the setting. + */ +export const customChromeShownAtom = atom( + (get) => get(nativeChromeActiveAtom) ?? get(customWindowChromeAtom), +);