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,