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