Skip to content

Speed up the Welcome kickoff and show the team arriving#2066

Open
morgmart wants to merge 9 commits into
mainfrom
remove-kickoff-delay
Open

Speed up the Welcome kickoff and show the team arriving#2066
morgmart wants to merge 9 commits into
mainfrom
remove-kickoff-delay

Conversation

@morgmart

Copy link
Copy Markdown
Contributor

Why

Landing on the Welcome channel meant staring at an empty room. The kickoff had a deliberate delay before it started, the closer dragged, and the whole thing gave no sign it was working — so a new user's first impression of Buzz was silence, and a failed kickoff looked exactly like a slow one.

The new-user sequence

Before: onboarding ends → brief blank → empty Welcome channel → (eventually) Fizz speaks, or doesn't, with no way to tell which you're in.

After: onboarding's last screen stays up as a curtain while the app mounts behind it → it fades to reveal Welcome with the starter team already on the composer banner → Fizz greets you by name → teammates introduce themselves in-thread → Fizz closes with the CTA.

What's in it

  • No kickoff landing delay — the choreography starts immediately; the closer's beat is tightened.
  • Direct handoff from onboarding to Welcome. The onboarding screen holds as an opaque curtain while the app mounts underneath on the Welcome route, then fades once Welcome reports its first settled render (with a safety timeout so a slow load can't strand anyone). No flash of empty app.
  • A loading stage on the composer banner — the starter team characters stand there while setup is genuinely in progress, so the wait reads as activity rather than silence.
  • Fizz greets the new user by name.
  • The opener's thread is watched directly, so teammate intros are seen without the user opening the thread.

Failure states

Situation What the user sees
No AI provider configured Fizz explains, points at Settings ✅
Honey & Bumble never come online Fizz greets solo, self-contained opener ✅
Agent spawns but requirements fail The agent itself posts a setup nudge ✅
Fizz dead / relay down / rate-limited Silent — but degrades to an ordinary, usable empty channel ⚠️

That last row is the honest gap. The stage waits 90s, then the characters exit and the banner reverts to its normal mention hint — so a failed kickoff stops claiming a team is coming, but it can't say why. It's driven purely by "is the timeline empty" plus a timer; it never reads real kickoff state, so it can't tell "Fizz crashed" from "the relay is slow."

Explaining the cause is deliberately out of scope here and tracked in docs/welcome-kickoff-silent-failures.md, which this PR updates to reflect that the perception gap is now handled while the silent paths themselves are not.

Review findings addressed

Three issues found in self-review and fixed in c887e34a:

  1. The timeout didn't resolve anything. timed-out rendered identically to active — the banner kept promising a team was being set up, forever. Now timed-out exits, and a terminal done phase keeps a finished stage from re-entering active off the still-empty timeline and looping the characters.
  2. Teammate intros are thread replies, which never reach the main timeline (non-broadcast replies are filtered out). Extracted mergeKickoffEvents so the closer's classification sees them without the user opening the thread — pinned by a test that fails against the old behavior.
  3. The opener thread query refetched forever on every Welcome revisit (staleTime: 0). Latched off once the closer marker exists.

Merge with main

main moved 13 commits during this work, 5 of them onboarding PRs touching the same screens (#2048, #2055, #2056, #2058). Merged in 07144e95 — four conflicts, resolved in favor of main wherever it had made a deliberate choice this branch hadn't:

  • Main's new WelcomeSetup back button was grafted onto this branch's relocated render, so it survives the restructure.
  • Main relocated the stage-flag declaration to feed its new autofocus effect; the entering stage was grafted onto it. Without that, the step chrome would unmount and the shell would snap alignment the instant the curtain began — a visible jump before the fade.
  • Main's onboarding shell layout taken wholesale (h-dvh, per-stage alignment, pb-36). This branch never intended spacing polish there; main's ramp wins.

Testing

  • Gates green on the merged tree: typecheck, biome lint, px-text guard, 3140/3140 desktop tests (+7 new here), plus rust, mobile, and tauri suites via pre-push.
  • New unit coverage on the stage phase machine (timeout resolves, done is terminal, banner never claims setup once resolved) and on mergeKickoffEvents.

Not verified by hand: the app was not run manually for this change. Verification is automated gates plus code reading. In particular the 90s timeout path has no eyes-on confirmation — it's covered by unit tests on the phase logic, not by watching it happen.

🤖 Generated with Claude Code

morgmart and others added 8 commits July 17, 2026 18:49
… opener thread directly

- Remove the 3s landing beat before Fizz's opener — the opener now sends
  as soon as the welcome team is online.
- Shorten the closer beat from 10s to 3s after teammate intros resolve.
- Watch the opener's thread replies via a dedicated query keyed to the
  opener event, so the closer fires without the user opening the thread
  (previously classification was blind unless the thread panel was open).
While the welcome team is being set up, the Welcome composer banner reads
"Setting up your welcome team…" and the three starter-team characters
(animated APNGs) stand on top of the banner with a staggered rise-in
entrance. The stage crossfades out when the first agent message lands —
one rule covers every resolution (opener, provider fallback, setup nudge,
or a user message).

- useWelcomeKickoffStage: pure local phase machine (hidden/active/
  timed-out/exiting) gated on *visible* timeline rows, so invisible
  events (canvas seed, membership records, welcome setup system
  messages) never suppress the stage; 90s timeout for silent failures.
- useWelcomeKickoffStagePresence: composes gating + rendered stage for
  ChannelScreen (also keeps ChannelScreen under the file-size limit).
- WelcomeKickoffStage: characters perched on the banner (bottom-full,
  pointer-events-none, decorative; banner copy carries status for
  screen readers).
- motion.css: kickoff character enter / stage exit primitives on the
  shared motion tokens, with reduced-motion treatments.
- docs/welcome-kickoff-silent-failures.md: inventory of kickoff message
  fallbacks and the silent failure paths still needing real handling.

Includes a DEV_FORCE_STAGE dev-only switch (off) for iterating on the
choreography without re-running fresh onboarding.
…annel

New users previously rode through the Inbox to reach Welcome: onboarding
cleared, the router mounted on Home, and a Home-route listener redirected
to the Welcome channel once the channels list confirmed it — flashing the
Inbox skeleton, an empty Inbox, and a channel skeleton along the way.

Now the final onboarding screen hands off directly:

- On Enter, the router is pointed at the Welcome channel route *before*
  the app mounts, so Welcome is the app's first-ever route. The pending
  Welcome entry is consumed at that moment (leaving it would yank a later
  Home visit back to Welcome); the Home-route listener stays as a
  fallback for other paths (e.g. relaunch mid-onboarding).
- New "entering" transaction stage: the "Meet your starter team" screen
  stays up as an opaque curtain while the app mounts and loads
  underneath, then fades out (500ms, instant under reduced motion) once
  the Welcome surface reports its first settled render. An 8s safety
  timeout fades anyway so a slow load can never strand the user.
- The onboarding flow keeps one stable position in the element tree
  across every stage — rendering it from a different slot when the stage
  flips to "entering" would remount it and visibly restart the screen
  mid-handoff.
- WELCOME_SURFACE_READY_EVENT: announced by the kickoff stage presence
  hook on the Welcome timeline's first settled render (per channel).

New-user sequence is now: starter team screen → fade → Welcome with the
kickoff loading stage already running.
The kickoff opener now resolves the owner's profile and opens with
"Hi @name, I'm Fizz." — the display name renders as a mention pill and
the owner's pubkey rides in the opener's p tags, so Fizz's first message
files into the user's Inbox mentions feed instead of leaving the Inbox
an empty state on first visit.

- buildWelcomeKickoffOpener takes an optional owner display name for the
  greeting; blank/missing names fall back to the unnamed greeting.
- buildWelcomeKickoffOpenerSendInput takes a WelcomeKickoffOwner
  ({pubkey, displayName}), appends the pubkey to mentionPubkeys without
  duplicating, and covers every opener variant including the degraded
  no-teammates path.
- Profile resolution is best-effort: a failed lookup degrades to an
  ungreeted, untagged opener rather than blocking the kickoff.
- Drive the entering-curtain fade duration from ENTERING_CURTAIN_FADE_MS
  via inline style (matching the boot-splash pattern) instead of a
  hardcoded duration-500 class that had to stay in sync by hand.
- Remove the DEV_FORCE_STAGE dev preview switch — it was scaffolding for
  iterating on the kickoff stage choreography, and that work is done.
- Retire the opener thread-replies query once the closer marker exists:
  the kickoff is resolved, so revisits to Welcome no longer refetch the
  opener's thread subtree forever (staleTime 0 made that a permanent
  extra relay fetch per visit).
- Normalize pubkeys when deduping the owner against the opener's mention
  tags, matching the repo's normalizePubkey convention.
… thread watch

Three fixes from a pre-PR review pass over this branch.

1. The kickoff stage's `timed-out` phase did nothing. It rendered identically
   to `active` (only `data-phase` differed) and `welcomeKickoffSettingUp` stayed
   true, so a failed kickoff left the characters on stage and the banner reading
   "Setting up your welcome team…" indefinitely. Every silent failure path in
   docs/welcome-kickoff-silent-failures.md — including the rate-limiting seen in
   practice — landed there. Now the characters play their exit and the banner
   reverts to the normal hint, degrading to an ordinary empty channel.

   This needs a terminal `done` phase: `hidden` meant both "not yet" and
   "finished", so a stage that exited on timeout would re-enter `active` off the
   still-empty timeline and loop forever.

2. Extract `mergeKickoffEvents` and test it. The opener-thread watch (the fix
   this branch exists for) had no test that the pre-fix code would fail — the
   merge was inline in the hook, so reverting to `channelEvents` alone kept the
   suite green.

3. `kickoffResolved` derived from `channelEvents`, but the closer is a thread
   reply and so never appears there unless the user opened the thread — the
   watch never retired. It's now a per-channel latch, and the closer effect
   respects it: retiring the watch drops the subtree that holds the closer, so
   an events-only guard would classify every teammate as silent and re-run the
   closer on each revisit.

Co-Authored-By: Claude <noreply@anthropic.com>
The doc described the loading animation as "in flight" and claimed it resolved
into a quiet timeout state. That was aspirational when written — the timed-out
phase rendered identically to active and the banner kept claiming setup was in
progress. It's true as of this branch, so record it as landed and keep the doc
open for the part that still isn't: telling the user *why* nothing happened.

Also notes two things the follow-up needs: the stage can't diagnose a cause
(it only knows the timeline is empty), and the empty channel it degrades to
invites an @mention of the very agent that failed. Renames the stale "loading
card" references to the actual `timed-out` phase.

Co-Authored-By: Claude <noreply@anthropic.com>
Resolves four conflicts where main's onboarding polish (#2048, #2055,
#2056, #2058) overlapped this branch's Welcome-kickoff choreography.

- App.tsx: main added a WelcomeSetup back button inside the early-return
  block this branch replaced with the `appContent` assignment. Kept this
  branch's structure and grafted `onBack={onBackToMachineConfig}` onto the
  relocated render, so the new back navigation survives the restructure.
- CommunityOnboardingFlow: main moved the isProfileStage/isTeamStage
  declaration above `if (!transaction) return null` to feed its new
  name-input autofocus effect, and that move auto-merged — leaving a
  duplicate. Dropped this branch's copy and added the `entering` stage to
  main's relocated isTeamStage. Without that graft the step chrome would
  unmount and the shell would snap from items-start to items-stretch the
  instant the curtain stage began, a visible jump before the fade.
- CommunityOnboardingFlow: took main's onboarding shell layout wholesale
  (h-dvh, per-stage alignment, pb-36) and layered this branch's curtain
  fade on as a third cn() argument. This branch never intended spacing
  polish on these screens; main's ramp wins.
- CommunityOnboardingFlow: kept both `isCurtainFading` and `nameInputRef`.

welcomeKickoff.ts auto-merged; main's teammate-presence loop fix (always
poll once before checking the deadline) is compatible with this branch's
timing changes.

Co-Authored-By: Claude <noreply@anthropic.com>
@morgmart
morgmart requested a review from a team as a code owner July 18, 2026 07:35
The two first-run kickoff e2e tests still expected "Hi, I'm Fizz." — the
ungreeted opener. 0487699 made Fizz greet the new user by name, so the
live opener now reads "Hi @morty QA, I'm Fizz." and both assertions were
stale against our own intentional copy change.

Assert the greeted form including the name rather than relaxing to a
name-agnostic substring: both tests already type "Morty QA" during
onboarding, so this pins the greeting feature instead of passing whether
or not the name renders.

The ungreeted path is still covered by welcomeKickoff.test.mjs, which
exercises the no-owner-name fallback.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant