From 2d0aa1b7cbca750a27e38c4cb7d81532afae6324 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Fri, 8 May 2026 07:54:53 -0400 Subject: [PATCH] fix(desktop): mount onboarding from frame 1 to kill the FOUT Default onboarding.configured to null (unknown until the runtime check resolves) and have the onboarding overlay render whenever it's not yet confirmed true. The boot overlay now yields to it, so the very first paint is the Welcome card with a "While we get you set up..." progress strip instead of a flash of the chat shell between boot dismiss and onboarding mount. The picker swaps in cleanly once the gateway opens and the runtime check confirms the user is not configured. Already-configured users see the same prep card briefly while their existing runtime warms up, then the overlay dismisses without touching the chat shell. --- .../src/components/desktop-boot-overlay.tsx | 9 ++++ .../components/desktop-onboarding-overlay.tsx | 48 +++++++++++++++++-- apps/desktop/src/store/onboarding.ts | 6 ++- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/components/desktop-boot-overlay.tsx b/apps/desktop/src/components/desktop-boot-overlay.tsx index 0c0352b909f..e3eae3b664e 100644 --- a/apps/desktop/src/components/desktop-boot-overlay.tsx +++ b/apps/desktop/src/components/desktop-boot-overlay.tsx @@ -3,9 +3,18 @@ import { useStore } from '@nanostores/react' import { Loader } from '@/components/ui/loader' import { cn } from '@/lib/utils' import { $desktopBoot } from '@/store/boot' +import { $desktopOnboarding } from '@/store/onboarding' export function DesktopBootOverlay() { const boot = useStore($desktopBoot) + const onboarding = useStore($desktopOnboarding) + + // Onboarding overlay covers the whole "first run" UX: it mounts from frame 1 + // and renders boot progress inline. Yield to it whenever it has not yet + // confirmed the user is configured. + if (onboarding.configured !== true) { + return null + } if (!boot.visible) { return null diff --git a/apps/desktop/src/components/desktop-onboarding-overlay.tsx b/apps/desktop/src/components/desktop-onboarding-overlay.tsx index b8833356df2..14dbfe724b6 100644 --- a/apps/desktop/src/components/desktop-onboarding-overlay.tsx +++ b/apps/desktop/src/components/desktop-onboarding-overlay.tsx @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Check, ChevronLeft, ChevronRight, ExternalLink, KeyRound, Loader2, Sparkles } from '@/lib/icons' import { cn } from '@/lib/utils' +import { $desktopBoot, type DesktopBootState } from '@/store/boot' import { $desktopOnboarding, cancelOnboardingFlow, @@ -107,7 +108,7 @@ const sortProviders = (providers: OAuthProvider[]) => export function DesktopOnboardingOverlay({ enabled, onCompleted, requestGateway }: DesktopOnboardingOverlayProps) { const onboarding = useStore($desktopOnboarding) - const visible = (enabled || onboarding.requested) && !onboarding.configured + const boot = useStore($desktopBoot) const ctxRef = useRef({ requestGateway, onCompleted }) ctxRef.current = { requestGateway, onCompleted } @@ -125,23 +126,64 @@ export function DesktopOnboardingOverlay({ enabled, onCompleted, requestGateway } }, [ctx, enabled, onboarding.requested]) - if (!visible) { + // Mount from frame 1 so we replace the boot overlay seamlessly. The + // configured field stays null until the runtime check resolves; only then + // do we know whether to dismiss (true) or surface the picker (false). + if (onboarding.configured === true) { return null } const { flow } = onboarding + const ready = enabled && onboarding.configured === false const showPicker = flow.status === 'idle' || flow.status === 'success' return (
-
{showPicker ? : }
+
+ {ready ? ( + showPicker ? ( + + ) : ( + + ) + ) : ( + + )} +
) } +function Preparing({ boot }: { boot: DesktopBootState }) { + const progress = Math.max(2, Math.min(100, Math.round(boot.progress))) + const hasError = Boolean(boot.error) + + return ( +
+

+ While we get you set up — Hermes is finishing install. This usually takes under a minute on first run. +

+
+
+
+
+ {boot.message} + {progress}% +
+ {hasError ?

{boot.error}

: null} +
+ ) +} + function Header() { return (
diff --git a/apps/desktop/src/store/onboarding.ts b/apps/desktop/src/store/onboarding.ts index c2299cbba41..431e488c8c2 100644 --- a/apps/desktop/src/store/onboarding.ts +++ b/apps/desktop/src/store/onboarding.ts @@ -27,7 +27,9 @@ export type OnboardingFlow = | { message: string; provider?: OAuthProvider; start?: OAuthStartResponse; status: 'error' } export interface DesktopOnboardingState { - configured: boolean + /** null until the first runtime check resolves; lets the overlay render + * during boot without flickering for already-configured users. */ + configured: boolean | null flow: OnboardingFlow mode: OnboardingMode providers: null | OAuthProvider[] @@ -41,7 +43,7 @@ export interface OnboardingContext { } const INITIAL: DesktopOnboardingState = { - configured: true, + configured: null, flow: { status: 'idle' }, mode: 'oauth', providers: null,