From 597164d089e449c04cd7ae65fc7fe82a22dbc4a0 Mon Sep 17 00:00:00 2001
From: kenny lopez
Date: Thu, 27 Aug 2026 07:32:08 +0100
Subject: [PATCH] Complete onboarding V3 workshop preview
Signed-off-by: kenny lopez
---
.../onboarding/ui/OnboardingPreviewApp.tsx | 242 ++++--
.../ui/OnboardingPreviewJourney.tsx | 705 ++++++++++++++++++
.../onboarding/ui/OnboardingPreviewShell.tsx | 38 +
3 files changed, 934 insertions(+), 51 deletions(-)
create mode 100644 desktop/src/features/onboarding/ui/OnboardingPreviewJourney.tsx
create mode 100644 desktop/src/features/onboarding/ui/OnboardingPreviewShell.tsx
diff --git a/desktop/src/features/onboarding/ui/OnboardingPreviewApp.tsx b/desktop/src/features/onboarding/ui/OnboardingPreviewApp.tsx
index 9ba05bc61a..d528149a2b 100644
--- a/desktop/src/features/onboarding/ui/OnboardingPreviewApp.tsx
+++ b/desktop/src/features/onboarding/ui/OnboardingPreviewApp.tsx
@@ -15,9 +15,21 @@ import {
ONBOARDING_LANDING_CTA_CLASS,
ONBOARDING_PRIMARY_CTA_CLASS,
ONBOARDING_SECONDARY_CTA_CLASS,
- OnboardingChrome,
} from "./OnboardingChrome";
import { OnboardingFooter, OnboardingFooterProvider } from "./OnboardingFooter";
+import {
+ BackupPasswordPreview,
+ CommunityChoicePreview,
+ CommunityConnectingPreview,
+ CommunityEntryPreview,
+ CommunityHomePreview,
+ CommunityProfilePreview,
+ type CommunityPreviewRoute,
+ DefaultConfigPreview,
+ StarterTeamPreview,
+ WelcomeChannelPreview,
+} from "./OnboardingPreviewJourney";
+import { OnboardingPreviewStep } from "./OnboardingPreviewShell";
import { OnboardingSlideTransition } from "./OnboardingSlideTransition";
import { SetupStepPreview } from "./SetupStepPreview";
@@ -28,7 +40,17 @@ type PreviewPage =
| "sign-in"
| "sign-in-key"
| "forgot-password"
- | "setup";
+ | "backup-options"
+ | "backup-password"
+ | "setup"
+ | "config"
+ | "community-choice"
+ | "community-entry"
+ | "community-connecting"
+ | "community-profile"
+ | "starter-team"
+ | "welcome-channel"
+ | "community-home";
const EMAIL_SIGNUP_POLICY: JoinPolicy = {
ageAttestationRequired: true,
@@ -123,33 +145,6 @@ function Landing({ onNavigate }: { onNavigate: (page: PreviewPage) => void }) {
);
}
-function PreviewStep({
- children,
- current = 2,
- onBack,
- testId,
-}: {
- children: React.ReactNode;
- current?: number;
- onBack: () => void;
- testId: string;
-}) {
- return (
-
-
-
-
-
- {children}
-
-
-
- );
-}
-
type PreviewPasswordStrength = "empty" | "weak" | "moderate" | "strong";
const strengthLabelPhraseVariants = {
@@ -446,12 +441,18 @@ function EmailSignup({
const [confirmPassword, setConfirmPassword] = React.useState("");
const [ageConfirmed, setAgeConfirmed] = React.useState(false);
const [agreementConfirmed, setAgreementConfirmed] = React.useState(false);
- // This workshop never creates an account, so it deliberately permits the
- // next preview step without collecting real credentials or consent.
- const canContinue = true;
+ const [openPolicyDocument, setOpenPolicyDocument] = React.useState<
+ "terms" | "privacy" | null
+ >(null);
+ const canContinue =
+ email.trim().includes("@") &&
+ password.length > 0 &&
+ confirmPassword === password &&
+ ageConfirmed &&
+ agreementConfirmed;
return (
-
+
undefined}
+ onOpenDocument={setOpenPolicyDocument}
policy={EMAIL_SIGNUP_POLICY}
relayWsUrl="wss://preview.invalid"
textTone="foreground"
@@ -504,8 +505,35 @@ function EmailSignup({
+ {openPolicyDocument ? (
+
+
+
+ {openPolicyDocument === "terms"
+ ? "Terms of Service"
+ : "Privacy Policy"}
+
+
+ {openPolicyDocument === "terms"
+ ? "Review the terms that apply when you use Buzz."
+ : "Review how information is handled when you use Buzz."}
+
+
setOpenPolicyDocument(null)}
+ type="button"
+ >
+ Close
+
+
+
+ ) : null}
-
+
);
}
@@ -517,9 +545,13 @@ function AgentHarnessStep({
onNext: () => void;
}) {
return (
-
+
-
+
);
}
@@ -542,7 +574,7 @@ function EmailSignIn({
const canContinue = email.trim().length > 0 && password.length > 0;
return (
-
+
-
+
);
}
@@ -623,7 +655,7 @@ function PasswordReset({
if (submitted) {
return (
-
@@ -638,12 +670,15 @@ function PasswordReset({
Use the link we sent to {email.trim()} to reset your password.
-
+
);
}
return (
-
+
-
+
);
}
@@ -701,7 +736,10 @@ function KeySignIn({
onContinue: () => void;
}) {
return (
-
+
-
+
);
}
@@ -735,6 +773,11 @@ export function OnboardingPreviewApp() {
const [passwordResetEmail, setPasswordResetEmail] = React.useState("");
const [setupBackPage, setSetupBackPage] =
React.useState("landing");
+ const [communityRoute, setCommunityRoute] =
+ React.useState("join");
+ const [communityName, setCommunityName] = React.useState("Block Community");
+ const [displayName, setDisplayName] = React.useState("");
+ const [hasAvatar, setHasAvatar] = React.useState(false);
React.useEffect(() => {
if (page === "landing") {
@@ -750,6 +793,10 @@ export function OnboardingPreviewApp() {
setRun((current) => current + 1);
setSignInEmail("");
setSignupPassword("");
+ setCommunityRoute("join");
+ setCommunityName("Block Community");
+ setDisplayName("");
+ setHasAvatar(false);
}, []);
let content: React.ReactNode;
@@ -769,7 +816,7 @@ export function OnboardingPreviewApp() {
);
} else if (page === "identity-key") {
content = (
- setPage("landing")}
testId="onboarding-preview-identity-key"
>
@@ -779,13 +826,37 @@ export function OnboardingPreviewApp() {
setSetupBackPage("identity-key");
setPage("setup");
}}
- onOpenPasswordBackup={() => undefined}
- onShowOptions={() => undefined}
+ onOpenPasswordBackup={() => setPage("backup-password")}
+ onShowOptions={() => setPage("backup-options")}
optionsExpanded={false}
previewMode
returningFromSecurity={false}
/>
-
+
+ );
+ } else if (page === "backup-options") {
+ content = (
+ setPage("identity-key")}
+ testId="onboarding-preview-backup-options"
+ >
+ setPage("setup")}
+ onOpenPasswordBackup={() => setPage("backup-password")}
+ onShowOptions={() => setPage("backup-options")}
+ optionsExpanded
+ previewMode
+ returningFromSecurity={false}
+ />
+
+ );
+ } else if (page === "backup-password") {
+ content = (
+ setPage("backup-options")}
+ onDone={() => setPage("backup-options")}
+ />
);
} else if (page === "sign-in") {
content = (
@@ -821,11 +892,80 @@ export function OnboardingPreviewApp() {
}}
/>
);
- } else {
+ } else if (page === "setup") {
content = (
setPage(setupBackPage)}
- onNext={() => undefined}
+ onNext={() => setPage("config")}
+ />
+ );
+ } else if (page === "config") {
+ content = (
+ setPage("setup")}
+ onNext={() => setPage("community-choice")}
+ />
+ );
+ } else if (page === "community-choice") {
+ content = (
+ setPage("config")}
+ onChoose={(route) => {
+ setCommunityRoute(route);
+ setPage("community-entry");
+ }}
+ />
+ );
+ } else if (page === "community-entry") {
+ content = (
+ setPage("community-choice")}
+ onContinue={(name) => {
+ setCommunityName(name);
+ setPage("community-connecting");
+ }}
+ route={communityRoute}
+ />
+ );
+ } else if (page === "community-connecting") {
+ content = (
+ setPage("community-entry")}
+ onContinue={() => setPage("community-profile")}
+ />
+ );
+ } else if (page === "community-profile") {
+ content = (
+ setHasAvatar((current) => !current)}
+ onBack={() => setPage("community-entry")}
+ onDisplayNameChange={setDisplayName}
+ onNext={() => setPage("starter-team")}
+ />
+ );
+ } else if (page === "starter-team") {
+ content = (
+ setPage("community-profile")}
+ onNext={() => setPage("welcome-channel")}
+ />
+ );
+ } else if (page === "welcome-channel") {
+ content = (
+ setPage("starter-team")}
+ onNext={() => setPage("community-home")}
+ />
+ );
+ } else {
+ content = (
+ setPage("welcome-channel")}
/>
);
}
diff --git a/desktop/src/features/onboarding/ui/OnboardingPreviewJourney.tsx b/desktop/src/features/onboarding/ui/OnboardingPreviewJourney.tsx
new file mode 100644
index 0000000000..04ba4503a1
--- /dev/null
+++ b/desktop/src/features/onboarding/ui/OnboardingPreviewJourney.tsx
@@ -0,0 +1,705 @@
+import { Check, Plus, Users } from "lucide-react";
+import * as React from "react";
+
+import { Button } from "@/shared/ui/button";
+import { Card } from "@/shared/ui/card";
+import { Input } from "@/shared/ui/input";
+import {
+ ONBOARDING_PRIMARY_CTA_CLASS,
+ ONBOARDING_SECONDARY_CTA_CLASS,
+} from "./OnboardingChrome";
+import { OnboardingFooter } from "./OnboardingFooter";
+import { OnboardingPreviewStep } from "./OnboardingPreviewShell";
+import { OnboardingSlideTransition } from "./OnboardingSlideTransition";
+
+const FIELD_CLASS =
+ "h-12 rounded-2xl border-foreground/15 bg-white px-4 text-sm shadow-none";
+const COMMUNITY_OPTION_CLASS =
+ "w-full max-w-[320px] items-center px-6 py-4 text-center text-sm font-normal leading-6 text-foreground [--buzz-card-textured-min-height:88px] transition-[filter] duration-150 ease-out hover:brightness-[0.98] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-foreground/35";
+
+function LabeledField({
+ id,
+ label,
+ onChange,
+ placeholder,
+ type = "text",
+ value,
+}: {
+ id: string;
+ label: string;
+ onChange: (value: string) => void;
+ placeholder: string;
+ type?: "password" | "text" | "url";
+ value: string;
+}) {
+ return (
+
+
+ {label}
+
+ onChange(event.target.value)}
+ placeholder={placeholder}
+ type={type}
+ value={value}
+ />
+
+ );
+}
+
+type BackupPhase = "password" | "saved" | "verify" | "verified";
+
+export function BackupPasswordPreview({
+ onBack,
+ onDone,
+}: {
+ onBack: () => void;
+ onDone: () => void;
+}) {
+ const [phase, setPhase] = React.useState("password");
+ const [password, setPassword] = React.useState("");
+ const [verificationPassword, setVerificationPassword] = React.useState("");
+ const heading =
+ phase === "verified"
+ ? "Your backup is verified"
+ : phase === "verify"
+ ? "That’s your backup file"
+ : phase === "saved"
+ ? "Optionally, test your backup"
+ : "Backup your key with a password";
+ const description =
+ phase === "verified"
+ ? "Your file and password can restore your identity."
+ : phase === "verify"
+ ? "Now enter your password to prove you can unlock it."
+ : phase === "saved"
+ ? "Learn how your backup works. Choose the file you just saved and unlock it with your password."
+ : "Keep the downloaded file private — you need both it and your password to restore your identity. Save the backup password somewhere safe; Buzz cannot reset it if lost.";
+
+ return (
+ setPhase("password")}
+ security
+ testId="onboarding-preview-backup-password"
+ >
+
+
+
{heading}
+
+ {description}
+
+
+
+ {phase === "password" ? (
+
+
+
+ Buzz identity backup
+
+ Protected with your backup password and ready to save.
+
+
+
+ ) : phase === "saved" ? (
+
+
+
+
+
+
+ Buzz identity backup saved
+
+
+ Keep the file and password somewhere safe.
+
+
+
+ ) : phase === "verify" ? (
+
+
+ Buzz identity backup
+
+ Selected and ready to unlock.
+
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {phase === "password" ? (
+ setPhase("saved")}
+ >
+ Create locked backup
+
+ ) : phase === "saved" ? (
+ <>
+ setPhase("verify")}
+ >
+ Test backup
+
+
+ Skip for now
+
+ >
+ ) : phase === "verify" ? (
+ setPhase("verified")}
+ >
+ Verify backup
+
+ ) : (
+
+ Finish
+
+ )}
+
+
+
+ );
+}
+
+export function DefaultConfigPreview({
+ onBack,
+ onNext,
+}: {
+ onBack: () => void;
+ onNext: () => void;
+}) {
+ const [harness, setHarness] = React.useState("buzz-agent");
+ const [provider, setProvider] = React.useState("");
+ const [model, setModel] = React.useState("");
+
+ return (
+
+
+
+
+ Configure your default model settings
+
+
+ This will be set as your default model configuration across Buzz.
+ You can always change this in your Settings or give specific agents
+ a different configuration.
+
+
+
+
+
+ Default harness
+ setHarness(event.target.value)}
+ value={harness}
+ >
+ Buzz Agent
+ Claude Code
+ Codex
+ Goose
+
+
+
+ Provider
+ setProvider(event.target.value)}
+ value={provider}
+ >
+ Select a provider
+ Anthropic
+ OpenAI
+ Ollama
+
+
+
+ Model
+ setModel(event.target.value)}
+ value={model}
+ >
+ Select a model
+ Provider default
+
+
+
+
+
+
+ Next
+
+
+ Skip for now
+
+
+
+
+ );
+}
+
+export type CommunityPreviewRoute = "join" | "create" | "existing";
+
+export function CommunityChoicePreview({
+ onBack,
+ onChoose,
+}: {
+ onBack: () => void;
+ onChoose: (route: CommunityPreviewRoute) => void;
+}) {
+ return (
+
+
+
+
Join or create a community
+
+ Join with an invite, create your own community, or reconnect one you
+ already have.
+
+
+
+ {(
+ [
+ ["join", "Join a community"],
+ ["create", "Create a community"],
+ ["existing", "I already have a community"],
+ ] as const
+ ).map(([route, label]) => (
+
+ onChoose(route)}
+ type="button"
+ >
+ {label}
+
+
+ ))}
+
+
+
+ );
+}
+
+export function CommunityEntryPreview({
+ onBack,
+ onContinue,
+ route,
+}: {
+ onBack: () => void;
+ onContinue: (communityName: string) => void;
+ route: CommunityPreviewRoute;
+}) {
+ const [value, setValue] = React.useState("");
+ const isCreate = route === "create";
+ const heading = isCreate
+ ? "Create a community"
+ : route === "existing"
+ ? "Reconnect to your community"
+ : "Join a community";
+
+ return (
+
+
+ {heading}
+
+ {isCreate
+ ? "Choose a name for the space where your people and agents will work together."
+ : route === "existing"
+ ? "Enter the community URL or an invite link. Your role will be restored when you connect."
+ : "Enter the invite link or community URL you received."}
+
+
+
+
+ );
+}
+
+export function CommunityConnectingPreview({
+ communityName,
+ onBack,
+ onContinue,
+}: {
+ communityName: string;
+ onBack: () => void;
+ onContinue: () => void;
+}) {
+ return (
+
+
+
+ Joining {communityName}
+ Connecting securely…
+
+
+ Continue
+
+
+
+
+ );
+}
+
+export function CommunityProfilePreview({
+ displayName,
+ hasAvatar,
+ onAvatarChange,
+ onBack,
+ onDisplayNameChange,
+ onNext,
+}: {
+ displayName: string;
+ hasAvatar: boolean;
+ onAvatarChange: () => void;
+ onBack: () => void;
+ onDisplayNameChange: (value: string) => void;
+ onNext: () => void;
+}) {
+ return (
+
+
+
+
Build your profile
+
+ Add a name and avatar. They’ll show up on your messages, reactions,
+ and agent handoffs.
+
+
+
+
+ {hasAvatar ? (
+
+ 🐝
+
+ ) : (
+
+ )}
+
+
+
+ Your username
+
+ onDisplayNameChange(event.target.value)}
+ placeholder="Enter your username here"
+ value={displayName}
+ />
+
+
+
+
+ Next
+
+
+
+
+ );
+}
+
+const STARTER_TEAM = ["Fizz", "Honey", "Pollen"] as const;
+
+export function StarterTeamPreview({
+ onBack,
+ onNext,
+}: {
+ onBack: () => void;
+ onNext: () => void;
+}) {
+ return (
+
+
+ Meet your starter team
+
+ Buzz lets you bring multiple agents into the same workspace. Your team
+ will help you get started using Buzz.
+
+
+
+ {STARTER_TEAM.map((name) => (
+
+
+
+ {name}
+
+
+ ))}
+
+
+
+
+ Take me to Buzz
+
+
+
+
+ );
+}
+
+export function WelcomeChannelPreview({
+ communityName,
+ onBack,
+ onNext,
+}: {
+ communityName: string;
+ onBack: () => void;
+ onNext: () => void;
+}) {
+ return (
+
+
+
+ Your Welcome channel is ready
+
+
+ This is your starting place in {communityName}. Your starter team can
+ help you learn the space and decide what to do next.
+
+
+ # Welcome
+
+ Meet your team, start a conversation, or explore your community.
+
+
+ {STARTER_TEAM.map((name) => (
+
+ {name[0]}
+
+ ))}
+
+
+
+
+ Open Welcome
+
+
+
+
+ );
+}
+
+export function CommunityHomePreview({
+ communityName,
+ onBack,
+}: {
+ communityName: string;
+ onBack: () => void;
+}) {
+ return (
+
+
+
+ {communityName}
+
+ Welcome to Buzz
+
+ You’re in the Welcome channel. Start with a message, meet the people
+ and agents here, or explore the rest of the community.
+
+
+ Fizz
+
+ Welcome! I can show you around and help you get started.
+
+
+
+
+ );
+}
diff --git a/desktop/src/features/onboarding/ui/OnboardingPreviewShell.tsx b/desktop/src/features/onboarding/ui/OnboardingPreviewShell.tsx
new file mode 100644
index 0000000000..384bf1e414
--- /dev/null
+++ b/desktop/src/features/onboarding/ui/OnboardingPreviewShell.tsx
@@ -0,0 +1,38 @@
+import type * as React from "react";
+
+import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
+import { OnboardingChrome } from "./OnboardingChrome";
+import { OnboardingFooterProvider } from "./OnboardingFooter";
+
+export function OnboardingPreviewStep({
+ children,
+ current = 2,
+ onBack,
+ security = false,
+ testId,
+}: {
+ children: React.ReactNode;
+ current?: number;
+ onBack?: () => void;
+ security?: boolean;
+ testId: string;
+}) {
+ return (
+
+
+ {!security ?
: null}
+
+
+ {children}
+
+
+
+ );
+}