SetupComplete.tsxfeat(onboarding): make /onboarding the whole setup, and retire the modal
The card-and-dots wizard at /onboarding only connected accounts. Everything a
user actually has to decide — inbox layout, sub-inboxes, task groups, pipeline
views — was discoverable only by finding the right dialog afterwards. The other
thing called onboarding, a dev-only modal behind two profile menus, ran on mock
data and configured nothing at all.
So /onboarding is now the full-page rail-driven flow, and the integration steps
survive inside it as the Connect section, spliced in from whatever the org has
installed rather than hard-coded — a team with no CRM never sees a CRM step, and
the rail does not count one toward its progress. Provider selection moved inside
each step, so Back means one thing instead of two.
The modal is deleted along with the CRMTablePreview/SimpleCRM subtree only it
used. Its templates screen is kept, unreferenced, at modules/onboarding/
templates: the AI-variable notation and source-email provenance are the design a
real templates surface should be built on, and re-deriving them would lose that.
Nothing imports it and it is not routed, but it stays inside the project so it
keeps type-checking instead of rotting into a snippet that no longer builds.
Also drops /onboarding-setup, added one commit ago — one flow, one URL.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 11:52 AMSetupPreviewContext.tsxfeat(onboarding): a setup flow that shows you the product while you configure it
Adds `/onboarding-setup` — a sidebar-driven flow over the four surfaces a new
user actually has to configure (Inbox, sub-inboxes, Tasks, Pipeline), each step
paired with a live preview of the real product rather than a screenshot of it.
Reachable from the profile menu as "Set up Cedar".
Two inbox-creation races surfaced by clicking through the template gallery are
fixed on the way, since the flow leans on that path hard:
- Per-mutation `invalidateQueries` lost rows. Create #1's refetch was issued
after #2 had written its optimistic row, so a server list predating #2 landed
last and overwrote it — the row vanished and the template card flipped back to
unselected. Now the last create to settle does the single refetch.
- Two adds fired back to back both read the cache before either had written to
it and claimed the same position, so a run of template clicks piled up at one
index and the tab strip reordered itself on refetch. Positions are now issued
from a carried-forward high-water mark.
A failed create drops only its own row instead of restoring a whole snapshot,
and `removeInbox` no longer swallows the rejection — callers holding their own
selected-state need to hear about it, or the row returns while the card stays
deselected.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 31, 2026, 10:48 PMSetupPreviewFrame.tsxfeat(onboarding): a setup flow that shows you the product while you configure it
Adds `/onboarding-setup` — a sidebar-driven flow over the four surfaces a new
user actually has to configure (Inbox, sub-inboxes, Tasks, Pipeline), each step
paired with a live preview of the real product rather than a screenshot of it.
Reachable from the profile menu as "Set up Cedar".
Two inbox-creation races surfaced by clicking through the template gallery are
fixed on the way, since the flow leans on that path hard:
- Per-mutation `invalidateQueries` lost rows. Create #1's refetch was issued
after #2 had written its optimistic row, so a server list predating #2 landed
last and overwrote it — the row vanished and the template card flipped back to
unselected. Now the last create to settle does the single refetch.
- Two adds fired back to back both read the cache before either had written to
it and claimed the same position, so a run of template clicks piled up at one
index and the tab strip reordered itself on refetch. Positions are now issued
from a carried-forward high-water mark.
A failed create drops only its own row instead of restoring a whole snapshot,
and `removeInbox` no longer swallows the rejection — callers holding their own
selected-state need to hear about it, or the row returns while the card stays
deselected.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 31, 2026, 10:48 PMSetupRail.tsxfeat(calendar,tasks): mint real Zoom meetings, and two fixes for surfaces that lied
Zoom reaches a Google Calendar event through the vendor's Workspace Add-on running
INSIDE Google, never through an external API client. Probed live against two accounts:
all 17 calendars report `allowedConferenceSolutionTypes: ["hangoutsMeet"]`, so the
`addOn` option added in ead6b26a4 was unreachable code — and had it rendered, Google
would have answered "Invalid conference type value". Cedar now holds a Zoom OAuth token
per user, mints the meeting through Zoom's own API, and writes the join details onto the
event. That is what Superhuman does, and there is no Calendar-API shortcut they know
that we do not.
The driver implements the existing OAuth interface, which buys the sealed AES-GCM state,
`initiateOAuth`'s URL builder, and `refreshMcpConnection`. The last is load-bearing
rather than convenient: Zoom rotates the refresh token on every refresh and kills the old
one, so two concurrent refreshes presenting the same stored token leave it dead — exactly
the failure the single-flight coalescing there already exists for. The row is written
`direct_oauth`, never `external_mcp`, because every MCP surface selects on that column and
an agent enumerating tools from api.zoom.us should not be possible.
Attachment goes to `location` (the only field that renders on every Google client),
`description` (a fenced, idempotent dial-in block — recreating the meeting REPLACES it
rather than appending beside it, which is what a "does it already contain this URL?"
check gets wrong), and `conferenceData` best-effort. `addOn` is gone from both
`createRequest` enums, the agent tool, the chat route, and `CalendarEvent.conferenceType`;
describing an existing conference is still allowed, because that is what a Zoom meeting is
by the time it reaches the event body. Inert until ZOOM_CLIENT_ID is set.
Two unrelated fixes, both reported by the same customer:
- "Search a deal…" searched every conversation the user owned. Cedar mints one per
unrecognised correspondent, so on the reporting account 117 conversations held 6 deals
and typing "gmail" returned a column of recruiters' addresses. `dealsOnly` narrows to
the AOPs the user actually works — `isNoOp = false` being their own declaration that
Cedar does work there — and falls back to today's behaviour for the 18 of 109 active
accounts that have no workable AOP.
- Every task surface splits due-now from upcoming at `endOfToday()`, read inside memos
whose dependencies are all data. Nothing in them moves when the day does, so a tab left
open past midnight files everything due TODAY under Upcoming, out of its group column,
which then collapses into the hidden-columns rail for being empty. `useDayKey` is the
missing dependency.
Staged whole-tree, not session-scoped: this branch is shared with a concurrent session
whose in-flight work (the board's Done lane, the calendar drag-settle animation, the
onboarding setup flow, the task-groups CLI) is entangled with these changes both within
files and across them — CalendarView passes props DayColumn only accepts in its
uncommitted form — so a scoped commit would not have compiled. That session likewise
swept this change's crm.ts and NewTaskDialog edits into a5fc7b295.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 10:35 PMSplitTemplateGallery.tsxfeat(calendar,tasks): mint real Zoom meetings, and two fixes for surfaces that lied
Zoom reaches a Google Calendar event through the vendor's Workspace Add-on running
INSIDE Google, never through an external API client. Probed live against two accounts:
all 17 calendars report `allowedConferenceSolutionTypes: ["hangoutsMeet"]`, so the
`addOn` option added in ead6b26a4 was unreachable code — and had it rendered, Google
would have answered "Invalid conference type value". Cedar now holds a Zoom OAuth token
per user, mints the meeting through Zoom's own API, and writes the join details onto the
event. That is what Superhuman does, and there is no Calendar-API shortcut they know
that we do not.
The driver implements the existing OAuth interface, which buys the sealed AES-GCM state,
`initiateOAuth`'s URL builder, and `refreshMcpConnection`. The last is load-bearing
rather than convenient: Zoom rotates the refresh token on every refresh and kills the old
one, so two concurrent refreshes presenting the same stored token leave it dead — exactly
the failure the single-flight coalescing there already exists for. The row is written
`direct_oauth`, never `external_mcp`, because every MCP surface selects on that column and
an agent enumerating tools from api.zoom.us should not be possible.
Attachment goes to `location` (the only field that renders on every Google client),
`description` (a fenced, idempotent dial-in block — recreating the meeting REPLACES it
rather than appending beside it, which is what a "does it already contain this URL?"
check gets wrong), and `conferenceData` best-effort. `addOn` is gone from both
`createRequest` enums, the agent tool, the chat route, and `CalendarEvent.conferenceType`;
describing an existing conference is still allowed, because that is what a Zoom meeting is
by the time it reaches the event body. Inert until ZOOM_CLIENT_ID is set.
Two unrelated fixes, both reported by the same customer:
- "Search a deal…" searched every conversation the user owned. Cedar mints one per
unrecognised correspondent, so on the reporting account 117 conversations held 6 deals
and typing "gmail" returned a column of recruiters' addresses. `dealsOnly` narrows to
the AOPs the user actually works — `isNoOp = false` being their own declaration that
Cedar does work there — and falls back to today's behaviour for the 18 of 109 active
accounts that have no workable AOP.
- Every task surface splits due-now from upcoming at `endOfToday()`, read inside memos
whose dependencies are all data. Nothing in them moves when the day does, so a tab left
open past midnight files everything due TODAY under Upcoming, out of its group column,
which then collapses into the hidden-columns rail for being empty. `useDayKey` is the
missing dependency.
Staged whole-tree, not session-scoped: this branch is shared with a concurrent session
whose in-flight work (the board's Done lane, the calendar drag-settle animation, the
onboarding setup flow, the task-groups CLI) is entangled with these changes both within
files and across them — CalendarView passes props DayColumn only accepts in its
uncommitted form — so a scoped commit would not have compiled. That session likewise
swept this change's crm.ts and NewTaskDialog edits into a5fc7b295.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 10:35 PM