bulk-actions-bar.tsxfeat: [thread] bulk actionsDec 18, 2025, 10:27 AMemail-tracking-indicator.tsxfix(mail): stop a tracked-email open from white-screening the whole app EmailTrackingIndicator called useState + useTRPC, returned null early when openCount/clickCount were 0, and only then called useQuery and three useMemos. Those counts come off the thread query, so the moment a recipient opened a tracked email the same mounted instance re-rendered past the guards and ran six hooks where React had recorded two — React #310, caught by the route error boundary, which replaced the app with the error screen. Hit <email> on staging (2026-08-17/18) and, with the same fingerprint, <email> and isabelle on production (2026-08-11); one 2026-08-05 session looped the crash 59 times. All six hooks move above the guards; the two early returns collapse into one `if (!isVisible) return null` after the last hook. Behaviour is unchanged — the query was already gated by `enabled: isOpen`, and a disabled TanStack v5 query still reports isLoading === false. ESLint had flagged all four lines the whole time; CI runs oxlint, where rules-of-hooks was off. Turned it on, scoped off for apps/server and packages (server helpers named use* are not hooks), and renamed the CompanyGTMPanel click handler `useTemplate` to `applyTemplate` so the repo lints clean. Verified against a copy of the pre-fix file: the gate reproduces the four errors and would have blocked this. Report: apps/mail/docs/bug-email-tracking-indicator-conditional-hooks.md Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 17, 2026, 11:46 PMinbox-section.tsxfix(mail): stop provider failures blanking threads, losing sends, and hiding mail Five defects traced from the Pirros onboarding (2026-08-03/04) and Isabelle's staging inbox. Full evidence in apps/server/src/docs/bug-blank-thread-and-missing-inbox-rows.md. The shared cause: the list is served from the Postgres mirror while opening a thread is served only from Gmail, and both sides failed silently. - Thread reads discarded the S3 snapshot they had already loaded when the provider errored, so a draft-only thread that Gmail 404s rendered as a blank pane and got tombstoned. 18% of one user's thread reads in a day; 197 of 228 tombstones in 14 days were this shape. Now any provider failure serves the snapshot, read-only, and only tombstones when nothing is stored. - markThreadDeleted matched on thread_id alone, so a 404 in one mailbox could hide a thread in another. Scoped to the connection. - A non-UUID conversationId ('failedCreatingConversation') aborted the mirror write; the caller swallows that, so the thread never reached the inbox while the Gmail cursor advanced. Coerced to null — the row is the inbox, the conversation link is not. - The send path never routed threadId through toProviderThreadId, so the composer's draftSessionId-<uuid> reached Gmail as a thread id and every new compose failed with 400 Invalid thread_id value. - Deleting a draft resolved its id only via the same manager.get that 404s, so no delete was sent and the route still reported success; listThreadsFromDb also ignored deleted_at, which summaries.ts and hash.ts both honour. Resolve from the snapshot, tombstone on an explicit delete, and filter deleted_at. - Stacked inbox containers loaded sequentially, gated on the previous one reporting ready — so a container whose query never settled left every container below it permanently unfetched, with no request, error or log. They now load in parallel. Read and delete are deliberately opposite on a 404: a read serves the snapshot and must not tombstone; an explicit delete must. Both directions are pinned by tests. Includes a dry-run-by-default repair script for the 198 already-tombstoned draft threads — run it only after deploy, or unfixed prod re-creates them. Tests written first and observed failing; two reproduce the exact prod strings. 1025 passed across mail/crm/driver, 159 across the frontend thread suite. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 4, 2026, 5:39 PMInboxLinkedDealsSection.tsxfix(mail): stop the edit-inboxes dialog overflowing the viewport, and constrain the inbox form The dialog width regressed to `sm:max-w-6xl`. DialogContent's base cap is `max-w-[calc(100%-2rem)]` with NO breakpoint prefix, so a prefixed rule overrides it from 640px up: on a 1024px laptop the dialog rendered 1152px wide, off the edge of the screen, taking every popover anchored near its right edge with it. Capped with min() so the template gallery stays wide on large screens without ever exceeding the window. The add/edit form is now a centred max-w-2xl column rather than fields stretched across the full gallery width, and the Linked deals filter list is fixed-width: each row is its own popover trigger and the leaf popovers are a fixed 384px anchored to the row's left edge, so a full-width row put those anchors at the far right. Renames the Pipeline template to "Active Pipeline". Co-Authored-By: Claude Opus 5 <<email>>Aug 4, 2026, 7:57 PMSplitInboxTabs.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 PMSplitSettingsCreator.tsxrefactor(mail): drop the icon from split inboxes An inbox is identified by its name; the icon next to it carried no signal and cost a picker in the create/edit form, an icon per template, and a nullable field threaded through the client, the tRPC schema, and the CLI. Removes it end to end. Stored icons are stripped harmlessly on read — the record schema is non-strict, so existing user_settings rows still parse. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 5, 2026, 9:06 AMSplitTemplateCard.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 PMstacked-inbox-view.tsxfix(mail): stop provider failures blanking threads, losing sends, and hiding mail Five defects traced from the Pirros onboarding (2026-08-03/04) and Isabelle's staging inbox. Full evidence in apps/server/src/docs/bug-blank-thread-and-missing-inbox-rows.md. The shared cause: the list is served from the Postgres mirror while opening a thread is served only from Gmail, and both sides failed silently. - Thread reads discarded the S3 snapshot they had already loaded when the provider errored, so a draft-only thread that Gmail 404s rendered as a blank pane and got tombstoned. 18% of one user's thread reads in a day; 197 of 228 tombstones in 14 days were this shape. Now any provider failure serves the snapshot, read-only, and only tombstones when nothing is stored. - markThreadDeleted matched on thread_id alone, so a 404 in one mailbox could hide a thread in another. Scoped to the connection. - A non-UUID conversationId ('failedCreatingConversation') aborted the mirror write; the caller swallows that, so the thread never reached the inbox while the Gmail cursor advanced. Coerced to null — the row is the inbox, the conversation link is not. - The send path never routed threadId through toProviderThreadId, so the composer's draftSessionId-<uuid> reached Gmail as a thread id and every new compose failed with 400 Invalid thread_id value. - Deleting a draft resolved its id only via the same manager.get that 404s, so no delete was sent and the route still reported success; listThreadsFromDb also ignored deleted_at, which summaries.ts and hash.ts both honour. Resolve from the snapshot, tombstone on an explicit delete, and filter deleted_at. - Stacked inbox containers loaded sequentially, gated on the previous one reporting ready — so a container whose query never settled left every container below it permanently unfetched, with no request, error or log. They now load in parallel. Read and delete are deliberately opposite on a 404: a read serves the snapshot and must not tombstone; an explicit delete must. Both directions are pinned by tests. Includes a dry-run-by-default repair script for the 198 already-tombstoned draft threads — run it only after deploy, or unfixed prod re-creates them. Tests written first and observed failing; two reproduce the exact prod strings. 1025 passed across mail/crm/driver, 159 across the frontend thread suite. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 4, 2026, 5:39 PM