From adf1fe1858d4f4df6ad1b90d70211adf96ed3c7c Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Fri, 5 Jun 2026 09:48:17 +0200 Subject: [PATCH] add ready prop to autosize hook --- client/ui/frontend/src/hooks/useAutoSizeWindow.ts | 14 ++++++++++++-- .../frontend/src/modules/welcome/WelcomeDialog.tsx | 11 +++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/client/ui/frontend/src/hooks/useAutoSizeWindow.ts b/client/ui/frontend/src/hooks/useAutoSizeWindow.ts index 99e854dc0..2d0c32a28 100644 --- a/client/ui/frontend/src/hooks/useAutoSizeWindow.ts +++ b/client/ui/frontend/src/hooks/useAutoSizeWindow.ts @@ -22,7 +22,16 @@ import i18next from "@/lib/i18n"; // the observer can settle on a stale size before React's commit and the // font's glyph metrics finish updating. An explicit double-rAF after the // language flip guarantees the final layout is the one we measure. -export function useAutoSizeWindow(width: number) { +// +// `ready` (default true) gates Window.SetSize + Window.Show. Pass false +// while the caller is still resolving its initial content (e.g. waiting +// on an async probe) so the window stays Hidden instead of briefly +// rendering placeholder padding at the wrong size — Linux/GNOME in +// particular paints whatever the frame ends up at, and a transient +// half-height frame can leak through. Flip ready=true once the real +// content is in the DOM; the effect re-runs, measures the final size, +// and shows the window. +export function useAutoSizeWindow(width: number, ready: boolean = true) { const ref = useRef(null); useLayoutEffect(() => { const el = ref.current; @@ -31,6 +40,7 @@ export function useAutoSizeWindow(width: number) { let raf1 = 0; let raf2 = 0; const apply = () => { + if (!ready) return; const h = Math.ceil(el.getBoundingClientRect().height); if (h <= 0) return; // Wails Window.SetSize takes the *frame* size on every platform @@ -79,6 +89,6 @@ export function useAutoSizeWindow(width: number) { cancelAnimationFrame(raf2); i18next.off("languageChanged", scheduleApply); }; - }, [width]); + }, [width, ready]); return ref; } diff --git a/client/ui/frontend/src/modules/welcome/WelcomeDialog.tsx b/client/ui/frontend/src/modules/welcome/WelcomeDialog.tsx index e6231c9d9..b1bcaafcd 100644 --- a/client/ui/frontend/src/modules/welcome/WelcomeDialog.tsx +++ b/client/ui/frontend/src/modules/welcome/WelcomeDialog.tsx @@ -51,10 +51,13 @@ type InitialState = { }; export default function WelcomeDialog() { - const contentRef = useAutoSizeWindow(WINDOW_WIDTH); const [step, setStep] = useState("tray"); const [initial, setInitial] = useState(null); const [closing, setClosing] = useState(false); + // ready=false until the daemon probe resolves — keeps the window + // Hidden so neither the empty padding-only frame (Linux/GNOME paints + // through) nor a placeholder div leaks onto screen. + const contentRef = useAutoSizeWindow(WINDOW_WIDTH, initial !== null); // Probe daemon state on mount: who's the active profile, do they // have an email recorded, and what management URL is configured? @@ -171,11 +174,7 @@ export default function WelcomeDialog() { const content = useMemo(() => { if (!initial) { - // Probe in flight — render an empty container so the dialog - // window measures something tiny instead of flashing the - // tray step before we know whether step 2 applies. The probe - // completes within a single tick on a healthy daemon. - return
; + return null; } switch (step) { case "tray":