explore(desktop): anonymous app-shell home on first open

A fresh install now opens straight into a full-bleed mock of the app
frame — ghost community rail and sidebar skeleton on the left, a
Discord-style community-discovery home in the main pane — instead of
a centered onboarding card stack. The user is presented with options
(join a featured community, set up identity & agents, import a key)
rather than being put in front of an agent as the first thing.

Joining stays one click: identity is persisted silently and a
first-community onboarding transaction connects to the chosen relay.
The classic corridor and key import remain reachable below the fold.
Replaces the centered DiscoveryLanding from the previous exploration
with AnonymousShellLanding; the discover page escapes the onboarding
shell (no chartreuse welcome chrome) and renders the app frame.

Exploration for the community-first onboarding flip (Track 2), built
on top of explore/discord-style-landing.

Co-authored-by: Thomas Petersen <thomasp@squareup.com>
Signed-off-by: Thomas Petersen <thomasp@squareup.com>
This commit is contained in:
Wintermute
2026-08-15 16:20:41 -04:00
co-authored by Thomas Petersen
parent 3ddaa0d59d
commit a737d62e4d
5 changed files with 231 additions and 159 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ export default defineConfig({
"**/sidebar-offcanvas-rail.spec.ts",
"**/search-scope-screenshots.spec.ts",
"**/onboarding-docked-cta-screenshots.spec.ts",
"**/discovery-landing-shot.spec.ts",
"**/anonymous-shell-shot.spec.ts",
"**/identity-key-help.spec.ts",
"**/key-import-reveal.spec.ts",
"**/navigation.spec.ts",
@@ -0,0 +1,185 @@
import { Compass, Plus } from "lucide-react";
import {
FEATURED_COMMUNITIES,
type FeaturedCommunity,
} from "@/features/onboarding/featuredCommunities";
import { Button } from "@/shared/ui/button";
import { Card } from "@/shared/ui/card";
import { Skeleton } from "@/shared/ui/skeleton";
import { ONBOARDING_SECONDARY_CTA_CLASS } from "./OnboardingChrome";
/**
* EXPLORATION — anonymous-first app shell (Discord model).
*
* A fresh install opens straight into something that looks like the app:
* a ghost sidebar on the left and a community-discovery home in the main
* pane. No identity ceremony and no agent greeting the user — joining a
* community creates the key silently, and agents become something you
* discover later. The classic corridor and key import stay reachable as
* quiet options below the fold.
*/
export function AnonymousShellLanding({
error,
isPending,
onAdvancedSetup,
onImportKey,
onJoin,
}: {
error: string | null;
isPending: boolean;
/** Classic corridor: identity → backup → harness → config. */
onAdvancedSetup: () => void;
onImportKey: () => void;
onJoin: (community: FeaturedCommunity) => void;
}) {
return (
<div className="flex h-full w-full" data-testid="discovery-landing">
<aside
aria-hidden
className="flex w-14 shrink-0 flex-col items-center gap-2.5 border-r border-sidebar-border/60 bg-sidebar px-2.5 pb-5 pt-12"
data-testid="anon-shell-rail"
>
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-primary text-primary-foreground">
<Compass className="h-4 w-4" />
</span>
<span className="my-0.5 h-px w-7 bg-sidebar-border" />
<Skeleton className="h-9 w-9 rounded-2xl" pulsing={false} />
<Skeleton className="h-9 w-9 rounded-2xl" pulsing={false} />
<span className="flex h-9 w-9 items-center justify-center rounded-2xl bg-sidebar-accent/60 text-sidebar-foreground/70">
<Plus className="h-4 w-4" />
</span>
</aside>
<aside
aria-hidden
className="flex w-60 shrink-0 flex-col border-r border-sidebar-border bg-sidebar px-3 pb-3 pt-12"
data-testid="anon-shell-sidebar"
>
<div className="rounded-lg bg-foreground/8 px-3 py-2 text-xs text-foreground/45">
Search everything
</div>
<div className="mt-4 flex flex-col gap-0.5">
{["Inbox", "Pulse", "Projects", "Agents"].map((label) => (
<div
className="rounded-md px-2 py-1.5 text-sm text-foreground/40"
key={label}
>
{label}
</div>
))}
</div>
<div className="mt-7 px-2 text-xs font-medium uppercase tracking-wide text-foreground/35">
Channels
</div>
<div className="mt-3 flex flex-col gap-3 px-2">
<Skeleton className="h-3.5 w-32" pulsing={false} />
<Skeleton className="h-3.5 w-24" pulsing={false} />
<Skeleton className="h-3.5 w-28" pulsing={false} />
</div>
<div className="mt-8 px-2 text-xs font-medium uppercase tracking-wide text-foreground/35">
Direct messages
</div>
<div className="mt-3 flex flex-col gap-3 px-2">
<Skeleton className="h-3.5 w-28" pulsing={false} />
<Skeleton className="h-3.5 w-20" pulsing={false} />
</div>
<div className="mt-auto rounded-xl bg-foreground/5 px-3 py-2.5 text-left">
<p className="text-sm font-medium text-foreground/80">Guest</p>
<p className="mt-0.5 text-xs leading-4 text-foreground/50">
Join a community to claim your identity
</p>
</div>
</aside>
<main className="flex min-w-0 flex-1 flex-col overflow-y-auto">
<div className="mx-auto w-full max-w-[880px] px-8 pb-12 pt-14">
<div className="rounded-2xl bg-foreground/5 px-10 py-12 text-center">
<h1 className="text-3xl font-semibold leading-tight text-foreground">
Find your community on Buzz
</h1>
<p className="mx-auto mt-3 max-w-[480px] text-sm leading-6 text-foreground/70">
From building agents to protocol talk, theres a place for you.
Jump in your identity is created as you go.
</p>
</div>
{error ? (
<p className="mt-6 text-center text-sm text-destructive">{error}</p>
) : null}
<h2 className="mt-10 text-left text-base font-medium text-foreground/85">
Featured communities
</h2>
<div className="mt-4 grid w-full grid-cols-1 gap-x-8 gap-y-10 sm:grid-cols-2">
{FEATURED_COMMUNITIES.map((community) => (
<Card
className="items-stretch px-7 py-5 text-left [--buzz-card-textured-min-height:132px]"
key={community.id}
variant="textured"
>
<div className="flex items-start gap-4">
<span
aria-hidden
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-foreground/8 text-2xl"
>
{community.emoji}
</span>
<div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-3">
<span className="truncate text-base font-medium text-foreground">
{community.name}
</span>
<span className="shrink-0 text-xs text-foreground/60">
{community.members.toLocaleString()} members
</span>
</div>
<p className="mt-1 line-clamp-2 text-sm leading-5 text-foreground/75">
{community.tagline}
</p>
</div>
</div>
<div className="mt-4 flex justify-end">
<Button
className="h-8 rounded-full px-5"
data-testid={`discovery-join-${community.id}`}
disabled={isPending}
onClick={() => onJoin(community)}
size="sm"
type="button"
>
Join
</Button>
</div>
</Card>
))}
</div>
<div className="mt-12 flex flex-col items-center gap-3">
<p className="text-sm text-foreground/70">
Want to set things up yourself first?
</p>
<div className="flex flex-wrap items-center justify-center gap-3">
<Button
className={ONBOARDING_SECONDARY_CTA_CLASS}
data-testid="discovery-advanced-setup"
disabled={isPending}
onClick={onAdvancedSetup}
type="button"
variant="ghost"
>
Set up identity &amp; agents first
</Button>
<Button
className={ONBOARDING_SECONDARY_CTA_CLASS}
data-testid="discovery-import-key"
disabled={isPending}
onClick={onImportKey}
type="button"
variant="ghost"
>
I already have a key
</Button>
</div>
</div>
</div>
</main>
</div>
);
}
@@ -1,123 +0,0 @@
import {
FEATURED_COMMUNITIES,
type FeaturedCommunity,
} from "@/features/onboarding/featuredCommunities";
import { Button } from "@/shared/ui/button";
import { Card } from "@/shared/ui/card";
import { ONBOARDING_SECONDARY_CTA_CLASS } from "./OnboardingChrome";
/**
* EXPLORATION — Discord-style first-open landing.
*
* The very first screen a fresh install shows: a directory of communities
* the user can join right away. No identity ceremony, no agent corridor —
* clicking Join creates the key silently and connects. Agent setup and key
* backup become one avenue off this screen (`onAdvancedSetup`) instead of
* the mandatory path.
*/
export function DiscoveryLanding({
error,
isPending,
onAdvancedSetup,
onImportKey,
onJoin,
}: {
error: string | null;
isPending: boolean;
/** Classic corridor: identity → backup → harness → config. */
onAdvancedSetup: () => void;
onImportKey: () => void;
onJoin: (community: FeaturedCommunity) => void;
}) {
return (
<div
className="flex w-full max-w-[860px] flex-col items-center text-center"
data-testid="discovery-landing"
>
<img
alt="Buzz"
className="w-full max-w-[420px]"
src="/landing/buzz-wordmark.png"
/>
<h1 className="mt-4 text-2xl font-normal leading-tight text-foreground">
Find your people
</h1>
<p className="mt-2 max-w-[520px] text-sm leading-6 text-foreground/80">
Jump into a community well set up your identity as you go. Your
agents, backups, and settings are one click away once youre in.
</p>
{error ? <p className="mt-4 text-sm text-destructive">{error}</p> : null}
<div className="mt-10 grid w-full grid-cols-1 gap-x-10 gap-y-12 sm:grid-cols-2">
{FEATURED_COMMUNITIES.map((community) => (
<Card
className="items-stretch px-7 py-5 text-left [--buzz-card-textured-min-height:132px]"
key={community.id}
variant="textured"
>
<div className="flex items-start gap-4">
<span
aria-hidden
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-foreground/8 text-2xl"
>
{community.emoji}
</span>
<div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-3">
<span className="truncate text-base font-medium text-foreground">
{community.name}
</span>
<span className="shrink-0 text-xs text-foreground/60">
{community.members.toLocaleString()} members
</span>
</div>
<p className="mt-1 line-clamp-2 text-sm leading-5 text-foreground/75">
{community.tagline}
</p>
</div>
</div>
<div className="mt-4 flex justify-end">
<Button
className="h-8 rounded-full px-5"
data-testid={`discovery-join-${community.id}`}
disabled={isPending}
onClick={() => onJoin(community)}
size="sm"
type="button"
>
Join
</Button>
</div>
</Card>
))}
</div>
<div className="mt-12 flex flex-col items-center gap-3 pb-10">
<p className="text-sm text-foreground/70">
Have an invite link, or want to run your own?
</p>
<div className="flex flex-wrap items-center justify-center gap-3">
<Button
className={ONBOARDING_SECONDARY_CTA_CLASS}
data-testid="discovery-advanced-setup"
disabled={isPending}
onClick={onAdvancedSetup}
type="button"
variant="ghost"
>
Set up identity &amp; agents first
</Button>
<Button
className={ONBOARDING_SECONDARY_CTA_CLASS}
data-testid="discovery-import-key"
disabled={isPending}
onClick={onImportKey}
type="button"
variant="ghost"
>
I already have a key
</Button>
</div>
</div>
</div>
);
}
@@ -18,9 +18,9 @@ import {
import { StartupWindowDragRegion } from "@/shared/ui/StartupWindowDragRegion";
import type { FeaturedCommunity } from "@/features/onboarding/featuredCommunities";
import { useCommunityOnboarding } from "@/features/onboarding/communityOnboarding";
import { AnonymousShellLanding } from "./AnonymousShellLanding";
import { BackupStep } from "./BackupStep";
import { DefaultConfigStep } from "./DefaultConfigStep";
import { DiscoveryLanding } from "./DiscoveryLanding";
import { DownloadKeyStep } from "./DownloadKeyStep";
import {
backupSessionToPasswordEntry,
@@ -327,12 +327,44 @@ export function MachineOnboardingFlow({
}
: undefined;
// EXPLORATION — the discover page escapes the onboarding shell entirely.
// A fresh install opens straight into a full-bleed mock of the app frame
// (rail + sidebar + main pane) instead of a centered onboarding corridor,
// so the first thing a user meets is the product, not a setup ceremony.
if (page === "discover") {
return (
<div
className="flex h-dvh w-full overflow-hidden bg-background text-foreground"
data-testid="machine-onboarding-gate"
>
<StartupWindowDragRegion />
<AnonymousShellLanding
error={error}
isPending={isPending}
onAdvancedSetup={() => {
setError(null);
setTransitionDirection("forward");
setPage("identity");
}}
onImportKey={() => {
setError(null);
setKeyImportDialog(null);
setKeyImportStage("key-entry");
setTransitionDirection("forward");
setPage("key-import");
}}
onJoin={(community) => void quickJoinCommunity(community)}
/>
</div>
);
}
return (
<div
className={`buzz-onboarding-neutral-theme buzz-startup-shell flex max-h-dvh items-start justify-center overflow-x-hidden overflow-y-auto px-4 text-foreground ${
isSecuritySubview ? "buzz-onboarding-security-theme" : ""
} ${
page === "discover" || page === "identity"
page === "identity"
? "buzz-onboarding-welcome py-8"
: "pb-28 pt-[106px]"
}`}
@@ -340,7 +372,7 @@ export function MachineOnboardingFlow({
>
<StartupWindowDragRegion />
{page === "identity" ? <LandingBees /> : null}
{page !== "discover" && page !== "identity" && !isSecuritySubview ? (
{page !== "identity" && !isSecuritySubview ? (
<OnboardingChrome
current={page === "config" ? 4 : page === "setup" ? 3 : 2}
/>
@@ -348,36 +380,10 @@ export function MachineOnboardingFlow({
<OnboardingFooterProvider backAction={chromeBackAction}>
<div
className={`relative flex w-full max-w-[1040px] flex-col items-center text-center ${
page === "discover" || page === "identity"
? "my-auto"
: "buzz-onboarding-step-frame"
page === "identity" ? "my-auto" : "buzz-onboarding-step-frame"
}`}
>
{page === "discover" ? (
<OnboardingSlideTransition
className="flex w-full flex-col items-center text-center"
direction={transitionDirection}
transitionKey={`machine-discover-${transitionDirection}`}
>
<DiscoveryLanding
error={error}
isPending={isPending}
onAdvancedSetup={() => {
setError(null);
setTransitionDirection("forward");
setPage("identity");
}}
onImportKey={() => {
setError(null);
setKeyImportDialog(null);
setKeyImportStage("key-entry");
setTransitionDirection("forward");
setPage("key-import");
}}
onJoin={(community) => void quickJoinCommunity(community)}
/>
</OnboardingSlideTransition>
) : page === "identity" ? (
{page === "identity" ? (
<OnboardingSlideTransition
className="flex w-full max-w-[720px] flex-col items-center text-center"
direction={transitionDirection}
@@ -3,22 +3,26 @@ import { expect, test } from "@playwright/test";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge } from "../helpers/bridge";
const SHOT_DIR = "test-results/discovery-landing";
const SHOT_DIR = "test-results/anonymous-shell";
test.use({ viewport: { width: 1280, height: 960 } });
test("discord-style discovery landing on first open", async ({ page }) => {
test("anonymous app-shell home on first open", async ({ page }) => {
await installMockBridge(page, undefined, {
skipCommunitySeed: true,
skipOnboardingSeed: true,
});
await page.goto("/");
// Fresh install → straight to the discovery landing. No identity ceremony.
// Fresh install → straight into the full-bleed app shell: ghost rail +
// sidebar on the left, community discovery in the main pane. No identity
// ceremony and no agent greeting.
await expect(page.getByTestId("discovery-landing")).toBeVisible();
await expect(page.getByTestId("anon-shell-rail")).toBeVisible();
await expect(page.getByTestId("anon-shell-sidebar")).toBeVisible();
await expect(page.getByTestId("discovery-join-buzz-hq")).toBeVisible();
await waitForAnimations(page);
await page.screenshot({ path: `${SHOT_DIR}/01-discovery-landing.png` });
await page.screenshot({ path: `${SHOT_DIR}/01-anonymous-shell-home.png` });
// Advanced setup remains one click away (the classic corridor).
await page.getByTestId("discovery-advanced-setup").click();