onboarding-setup-flow.md8.2 KBView on GitHub # Onboarding setup flow
A multi-step, sidebar-driven setup flow that walks a new user through the four
surfaces they actually have to configure — Inbox, sub-inboxes, Tasks, Pipeline —
with a live preview of the real product on the right of every step.
## Current state
There are two unrelated things called "onboarding" today:
- `app/(full-width)/onboarding/page.tsx` (518 lines) — the **integrations**
flow. Linear `currentStep` index over a `Step[]` built from whichever
integrations the org has enabled (meeting recorder, CRM, Slack, MCP, LinkedIn,
WhatsApp, Google Drive). Top-centred card, dots + Previous/Next at the bottom.
It connects accounts; it configures nothing about the product itself.
- `modules/onboarding/components/onboarding.tsx` (2021 lines) — a mock-data
playbook/CRM preview dialog exported as `useOnboarding()`. Reachable only from
the avatar dropdown in `modules/conversations/components/LeftSidebarContent.tsx`
behind `import.meta.env.MODE === 'development'` ("Open Onboarding").
Neither teaches the product. Everything a user must actually decide —
inbox layout, which sub-inboxes exist, task groups, pipeline tabs — is
discoverable only by finding the right dialog after the fact.
The pieces the new flow configures all already exist:
| Surface | Where it lives today | Write path |
| --- | --- | --- |
| Inbox layout / important signal | `modules/threads/hooks/use-inboxes.ts` | `setInboxLayout`, `setImportantSignal` → `settings.save` |
| Sub-inboxes | `SplitInboxTabs.tsx` → `EditInboxesDialog` | `addInbox`/`removeInbox` → `mail.createInbox`/`deleteInbox` |
| Sub-inbox library | `SUPERHUMAN_TEMPLATES` in `SplitInboxTabs.tsx` | same |
| Task groups | `CreateTaskGroupPopover.tsx` | `taskGroups.createGroup` |
| Pipeline tabs | `NewCanvasScreen.tsx` (`CONVERSATION_CANVAS_TEMPLATES`) | `canvas.create` |
### The sub-inbox bug
Adding a sub-inbox from the template gallery "doesn't consistently save". Three
concrete causes, all in the create path:
1. **Refetch clobber.** `createInbox`'s `onSettled` invalidates
`mail.listInboxes` unconditionally. Click two templates in quick succession
and create #1's refetch resolves *after* create #2's optimistic insert,
overwriting the cache with a server list that does not yet contain #2. The
card flips back to unselected and the user clicks again.
(`use-inboxes.ts:340`, `:362`, `:377`)
2. **Colliding positions.** `addInbox` sends `position: rawInboxes?.length ?? 0`
— read off the *server* cache, not the optimistic one. Two creates before the
first refetch both claim the same position. (`use-inboxes.ts:459`)
3. **Orphaned optimistic UI.** The gallery's `optimisticTemplateNames` Set is
local to `EditInboxesDialog` and is never reconciled against a failed
`removeInbox` (whose error is swallowed by a `console.warn`), so a failed
deselect leaves the card looking deselected forever.
(`SplitInboxTabs.tsx:1601`, `use-inboxes.ts:566`)
## Proposed changes
A new `modules/onboarding/setup/` module rendering a full-bleed three-region
shell:
```
┌──────────┬───────────────────────────┬──────────────────────────┐
│ step │ step controls │ live preview │
│ rail │ (title, copy, toggles, │ (mock inbox that reacts │
│ │ template cards) │ to the toggles, or the │
│ Home │ │ user's REAL threads) │
│ Inbox ◀ │ │ │
│ Tasks ░ │ │ │
│ Pipeline░│ [ Continue ⏎ ] │ │
└──────────┴───────────────────────────┴──────────────────────────┘
```
The left rail is the progress indicator — it is a facsimile of the app's own
sidebar, so the user learns the real navigation while stepping through it. The
current step is the selected row; completed steps are solid and clickable;
future steps are blurred and inert. No top progress bar.
### Steps
1. **Welcome** (`home`) — what Cedar is, one screen, no controls.
2. **Inbox** (`inbox`) — the real display settings: layout (`Inbox` /
`Important + Other` / `Stacked`), important signal, auto-read, animations.
Every control writes through `useInboxes`/`settings.save` immediately and the
preview re-renders — the preview *is* the explanation.
3. **Sub-inboxes** (`splits`) — the `SUPERHUMAN_TEMPLATES` library as a
selectable gallery. Clicking a card creates the inbox; the preview shows the
resulting tab strip and a mock feed partitioned across it.
4. **Your inbox** (`live`) — same gallery, but the preview renders the user's
**real** threads for the selected inbox via `mail.listThreads({ q })`. Adds
are optimistic and land in the tab strip instantly.
5. **Tasks** (`tasks`) — what a task is and where drafts come from, plus
inline creation of task groups (name + colour + routing criteria), with the
groups rail rendering live on the right.
6. **Pipeline** (`pipeline`) — the `CONVERSATION_CANVAS_TEMPLATES` library;
selecting templates creates canvases, previewed as the pipeline tab strip.
### Entry point
`/onboarding` itself. The card-and-dots integration wizard that used to live
there is replaced by this flow; its integration steps survive as the **Connect**
section, spliced in from whatever the org has installed. One route, one flow, no
menu entry.
The old onboarding MODAL (`modules/onboarding/components/onboarding.tsx`,
`useOnboarding`, `OnboardingDialog`) and both dropdown entries that opened it are
deleted. Its templates screen is preserved, deliberately unreachable, in
`modules/onboarding/templates/` — see the README there.
### Sub-inbox reliability
Fixed in `use-inboxes.ts` so the main app benefits too, not just onboarding:
- Invalidate `listInboxes` on settle only when **no other create is in flight**;
the last create to settle does the single refetch.
- Derive `position` from `max(position) + 1` over the optimistic cache.
- Surface `removeInbox` failures and roll the cache back instead of warning.
## Critical files
| File | Change |
| --- | --- |
| `modules/threads/hooks/use-inboxes.ts` | create/delete reliability fixes |
| `modules/threads/components/SplitInboxTabs.tsx` | export template types/helpers for reuse |
| `modules/onboarding/setup/` | new module (shell, rail, steps, previews) |
| `app/(full-width)/onboarding/page.tsx` | now hosts the flow |
| `modules/conversations/components/LeftSidebarContent.tsx`, `components/ui/nav-user.tsx` | modal + menu entries removed |
| `modules/onboarding/templates/` | salvaged templates screen, unreferenced |
| `messages/en.json` | new Paraglide keys |
## Phased implementation plan
- [x] **Phase 1 — Sub-inbox write reliability.** Fix the three create/delete
races in `use-inboxes.ts`. Tests: two rapid `addInbox` calls both survive a
settle; a failed `removeInbox` restores the row.
- [x] **Phase 2 — Flow shell.** `SetupFlow` + `SetupRail` + dynamic step registry,
hosted at `/onboarding`.
- [x] **Phase 3 — Inbox step + mock preview.** `PreviewInbox` driven by the real
settings; the Inbox step's controls.
- [x] **Phase 4 — Sub-inbox steps.** Shared template gallery component; mock
preview for step 3, real `mail.listThreads` preview for step 4.
- [x] **Phase 5 — Tasks step.** Explainer + group creation + live groups rail.
- [x] **Phase 6 — Pipeline step.** Canvas template library + tab-strip preview.
- [x] **Phase 7 — Retire the old onboarding.** Delete the modal and its consumers;
fold the integration steps into the flow as the Connect section; salvage the
templates screen unreferenced.
## Verification
- `timeout 300 pnpm --filter @zero/mail run types`
- `timeout 300 pnpm --filter @zero/mail exec vitest run tests/modules/threads/useInboxes*.test.ts*`
- Manual: open `/onboarding`, click five sub-inbox templates in a row —
all five stay selected and appear in the tab strip.