Speed up the Welcome kickoff and show the team arriving#2066
Open
morgmart wants to merge 9 commits into
Open
Conversation
… 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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Failure states
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:timed-outrendered identically toactive— the banner kept promising a team was being set up, forever. Nowtimed-outexits, and a terminaldonephase keeps a finished stage from re-enteringactiveoff the still-empty timeline and looping the characters.mergeKickoffEventsso the closer's classification sees them without the user opening the thread — pinned by a test that fails against the old behavior.staleTime: 0). Latched off once the closer marker exists.Merge with main
mainmoved 13 commits during this work, 5 of them onboarding PRs touching the same screens (#2048, #2055, #2056, #2058). Merged in07144e95— four conflicts, resolved in favor of main wherever it had made a deliberate choice this branch hadn't:enteringstage 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.h-dvh, per-stage alignment,pb-36). This branch never intended spacing polish there; main's ramp wins.Testing
doneis terminal, banner never claims setup once resolved) and onmergeKickoffEvents.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