+
diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts
index 8ca25b81d..cd881650c 100644
--- a/desktop/playwright.config.ts
+++ b/desktop/playwright.config.ts
@@ -60,6 +60,7 @@ export default defineConfig({
"**/top-chrome-zoom-clearance.spec.ts",
"**/thread-unread.spec.ts",
"**/workspace-rail.spec.ts",
+ "**/boot-splash.spec.ts",
"**/thread-reply-anchor-roleplay.spec.ts",
"**/threadpane-ultrawide.spec.ts",
"**/animated-avatar.spec.ts",
diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json
index c33f3443b..7bcc434f7 100644
--- a/desktop/src-tauri/tauri.conf.json
+++ b/desktop/src-tauri/tauri.conf.json
@@ -20,7 +20,7 @@
"width": 800,
"height": 600,
"maximized": true,
- "visible": false,
+ "visible": true,
"backgroundColor": "#000000",
"titleBarStyle": "Overlay",
"hiddenTitle": true,
diff --git a/desktop/src/app/App.tsx b/desktop/src/app/App.tsx
index c3624af24..a830cfd2e 100644
--- a/desktop/src/app/App.tsx
+++ b/desktop/src/app/App.tsx
@@ -12,6 +12,7 @@ import {
} from "react";
import { router } from "@/app/router";
+import { ThemeGrainientBackground } from "@/app/ThemeGrainientBackground";
import { useReloadShortcut } from "@/app/useReloadShortcut";
import { useAppOnboardingState } from "@/features/onboarding/hooks";
import { OnboardingSlideTransition } from "@/features/onboarding/ui/OnboardingSlideTransition";
@@ -25,27 +26,108 @@ import { createBuzzQueryClient } from "@/shared/api/queryClient";
import { isSharedIdentity as isSharedIdentityCmd } from "@/shared/api/tauri";
import { listenForDeepLinks } from "@/shared/deep-link";
import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme";
+import { cn } from "@/shared/lib/cn";
import { Button } from "@/shared/ui/button";
import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark";
-import { Spinner } from "@/shared/ui/spinner";
+import { FlappingBee } from "@/shared/ui/buzz-logo/FlappingBee";
+import { FuzzyLogo } from "@/shared/ui/buzz-logo/FuzzyLogo";
import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
import { StepProgress } from "@/shared/ui/step-progress";
const LOADING_TEXT = "Setting up your workspace...";
-// Cold boot gate: a plain static Buzz mark (#D7D72E) on solid black. No
-// animation machinery, no gradient — the mark must paint complete on the very
-// first frame, even in webviews that render before scripting or SMIL start.
+// Minimum time the cold-boot splash stays on screen. A real boot resolves the
+// workspace in well under 100ms, and the hidden Tauri window (visible:false
+// until getCurrentWindow().show()) takes longer than that to put its first
+// frame on screen — without a hold, the bee is unmounted before it is ever
+// visible. The hold runs as an overlay above the already-mounted app, so
+// time-to-interactive is unchanged; only the reveal waits.
+const BOOT_SPLASH_MIN_VISIBLE_MS = 1_200;
+const BOOT_SPLASH_FADE_MS = 200;
+
+type BootSplashPhase = "holding" | "fading" | "done";
+
+// E2E runs skip the hold (it would slow every spec's boot and block pointer
+// actionability); a spec can opt back in via __BUZZ_E2E__.bootSplashHoldMs.
+function bootSplashHoldMs(): number {
+ const e2e = (
+ window as Window & {
+ __BUZZ_E2E__?: { bootSplashHoldMs?: number };
+ }
+ ).__BUZZ_E2E__;
+ if (e2e) {
+ return e2e.bootSplashHoldMs ?? 0;
+ }
+ return BOOT_SPLASH_MIN_VISIBLE_MS;
+}
+
+function useBootSplashHold(): BootSplashPhase {
+ const [phase, setPhase] = useState(() =>
+ bootSplashHoldMs() > 0 ? "holding" : "done",
+ );
+
+ useEffect(() => {
+ const holdMs = bootSplashHoldMs();
+ if (holdMs <= 0) {
+ return;
+ }
+ const fadeTimer = window.setTimeout(() => setPhase("fading"), holdMs);
+ const doneTimer = window.setTimeout(
+ () => setPhase("done"),
+ holdMs + BOOT_SPLASH_FADE_MS,
+ );
+ return () => {
+ window.clearTimeout(fadeTimer);
+ window.clearTimeout(doneTimer);
+ };
+ }, []);
+
+ return phase;
+}
+
+// Animated Buzz mark for the loading gates. The static BuzzMark renders in
+// normal flow and sizes the box — it's plain SVG (no JS/SMIL), so it paints on
+// the very first frame even before scripting starts, avoiding a blank flash on
+// hard reload. The animated FuzzyLogo is layered on top and takes over once it
+// begins playing.
+function BeeLoader({
+ ariaLabel,
+ className,
+ tintClassName = "text-foreground",
+}: {
+ ariaLabel: string;
+ className?: string;
+ tintClassName?: string;
+}) {
+ return (
+
+
+
+
+ );
+}
+
+// Cold boot gate: the theme-adaptive grainient background with a single
+// centered Buzz bee flying over it — the same static mark as before, now with
+// its wings flapping (ported from the Buzz website's wing-flap). Replaces the
+// old "Setting up your workspace" text, which stays as an sr-only caption.
function AppLoadingGate() {
return (
+ {LOADING_TEXT}
-
+
);
}
@@ -69,7 +151,11 @@ function WorkspaceSwitchGate() {
Switching workspace…
{showSpinner ? (
-
+
) : null}
);
@@ -313,6 +399,8 @@ export function App() {
setIsCompletingFirstRunWorkspace(false);
}, []);
+ const bootSplashPhase = useBootSplashHold();
+
// Wait for the shared-identity IPC call to resolve before rendering
// anything that depends on it. Without this gate, children briefly see
// isSharedIdentity=false and may flash WelcomeSetup or the onboarding flow.
@@ -343,6 +431,14 @@ export function App() {
return isWorkspaceSwitch ? : ;
}
+ // The app mounts (and starts loading data) beneath the splash overlay; the
+ // overlay just keeps the bee on screen long enough to be seen, then fades.
+ // Workspace switches and first-run completion keep their quiet gates.
+ const showBootSplashOverlay =
+ bootSplashPhase !== "done" &&
+ !isWorkspaceSwitch &&
+ !isCompletingFirstRunWorkspace;
+
return (
+ {showBootSplashOverlay ? (
+