searchSlice.tsrefactor: delete bridge & merge mailStore into cedarStoreFeb 26, 2026, 9:45 PMuse-inbox-compiled-queries.tsfeat(inbox): retire the server-merged feed, and make one email read serve both lists (design: inbox-triage phases 8 + 0) Finishes Phase 8 and lands Phase 0. RETIRED — `inbox.listItems` and `assembleInboxFeed`, with `fetchEmailItems`, `inboxCompiledQuery`, the folder→query fallbacks and `resolveEmailLabelTerms`. feed.ts drops 875→500 lines and no longer reads mail at all. Its 4 test files are RETARGETED, not deleted: scope onto `participatingChannels` and the three sources' SQL, and the real-DB files onto the live `inbox.listChannelItems` route. TWO PAGING BUGS the server-merged feed was hiding, both found by retargeting those tests onto a single source — three others had been covering for each one: * `mergeAndPage` truncated a single-source feed at page two: the sources resume at `<= cursorSortedAt`, so every page re-read its boundary row and spent the `limit + 1` that proves "more remains". It now takes a REQUIRED `sourcesSaturated` — required so a new caller cannot silently reintroduce it. * The sources ordered by `last_message_at` alone while the pager tiebreaks by id, so a LIMIT landing mid-tie-band took an arbitrary subset and the skipped rows sorted ABOVE the minted cursor — gone for good. All six source queries now order `(last_message_at DESC, <itemId> COLLATE "C" DESC)`; the collation is load-bearing, since the default one folds case and ignores punctuation and would disagree with `compareItemsDesc`. Measured: three rows vanished from a paged walk of a real feed. PHASE 0 — one email read, one route resolver: * The `headChanged` latch is per-surface. Phase 8 gave `mail.listThreads` a second caller, so a single per-connection flag meant whichever surface read first consumed the signal. `markPendingHeadChange` fans out to every surface, because the reconcile dedup key carries none and the second reader routinely joins the first's in-flight run. * `use-route-inbox.ts` resolves the active inbox from the URL for BOTH lists; the persisted `settings.activeInboxId` fallback is gone. Closes the important/other and system-folder divergences as well. * `drafts.create` awaits `surfaceDraftInInbox`, and the label join row is born with its ordering key instead of being backfilled by a second statement. compose-feed becomes self-verifying now that its diff target is gone: ordered, unique, stable-across-paging, and watermark-respecting, checked over real paging rounds and each probed against a deliberately broken history. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 31, 2026, 9:56 PMuse-inboxes.tsfeat(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 PMuse-prefetch-thread.tsfix: revert viraat mail changesApr 14, 2026, 2:00 PMuse-route-inbox.tsfeat(inbox): retire the server-merged feed, and make one email read serve both lists (design: inbox-triage phases 8 + 0) Finishes Phase 8 and lands Phase 0. RETIRED — `inbox.listItems` and `assembleInboxFeed`, with `fetchEmailItems`, `inboxCompiledQuery`, the folder→query fallbacks and `resolveEmailLabelTerms`. feed.ts drops 875→500 lines and no longer reads mail at all. Its 4 test files are RETARGETED, not deleted: scope onto `participatingChannels` and the three sources' SQL, and the real-DB files onto the live `inbox.listChannelItems` route. TWO PAGING BUGS the server-merged feed was hiding, both found by retargeting those tests onto a single source — three others had been covering for each one: * `mergeAndPage` truncated a single-source feed at page two: the sources resume at `<= cursorSortedAt`, so every page re-read its boundary row and spent the `limit + 1` that proves "more remains". It now takes a REQUIRED `sourcesSaturated` — required so a new caller cannot silently reintroduce it. * The sources ordered by `last_message_at` alone while the pager tiebreaks by id, so a LIMIT landing mid-tie-band took an arbitrary subset and the skipped rows sorted ABOVE the minted cursor — gone for good. All six source queries now order `(last_message_at DESC, <itemId> COLLATE "C" DESC)`; the collation is load-bearing, since the default one folds case and ignores punctuation and would disagree with `compareItemsDesc`. Measured: three rows vanished from a paged walk of a real feed. PHASE 0 — one email read, one route resolver: * The `headChanged` latch is per-surface. Phase 8 gave `mail.listThreads` a second caller, so a single per-connection flag meant whichever surface read first consumed the signal. `markPendingHeadChange` fans out to every surface, because the reconcile dedup key carries none and the second reader routinely joins the first's in-flight run. * `use-route-inbox.ts` resolves the active inbox from the URL for BOTH lists; the persisted `settings.activeInboxId` fallback is gone. Closes the important/other and system-folder divergences as well. * `drafts.create` awaits `surfaceDraftInInbox`, and the label join row is born with its ordering key instead of being backfilled by a second statement. compose-feed becomes self-verifying now that its diff target is gone: ordered, unique, stable-across-paging, and watermark-respecting, checked over real paging rounds and each probed against a deliberately broken history. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 31, 2026, 9:56 PMuse-search-query-sync.tsfix(mail): a search that leaves the screen stops filtering the list Isabelle's Email tab showed a full page of older mail with almost nothing from the past week, while the same screen's All-channels badge showed her whole inbox. It was not missing mail: the list was still filtered by a `thiago` search she had run three hours earlier, and mail.listThreads re-ran it 52 times on the 5-minute refetch interval with no search box on screen to say so. SearchInput held the only writer that resets searchState.value, and mail.tsx mounts it only while `isSearchOpen || !!searchQuery`. isSearchOpen resets to false on remount and ?search= is dropped by navigation, so the input unmounts in the same commit as the param change and its effect never runs for the new value — while useCedarStore, a module singleton, keeps the compiled query. The mirror moves into useSearchQuerySync(), called unconditionally next to the list it filters: the URL owns the filter, the input only edits the URL. Second defect from the same logs: her Starred split returned 0 rows on every read (over-fetch-cap truncation) and stayed there. The server hands back a short page plus a usable cursor for exactly that case, but MailList renders the inbox-zero state when the list is empty, so the VList never mounts and the auto-load effect bailed on its null-ref guard before it could page. An empty list with hasNextPage now pages ahead of that guard, and spins rather than announcing an inbox zero it cannot yet claim. Report: apps/mail/docs/bug-email-tab-shows-few-threads.md Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 8, 2026, 12:47 PMuse-search-value.tsrefactor: delete bridge & merge mailStore into cedarStoreFeb 26, 2026, 9:45 PMuse-unread-filter.tsfeat(mail): ⇧U filters the inbox you are on to unread, on every channel Superhuman's split-inbox filter, with the difference that matters here: Cedar's inbox is omni-channel, so the filter has to hold for email, LinkedIn, WhatsApp and Slack at once, or the one merged list would say "Unread" while carrying read chats. Email narrows through an `is:unread` clause on the compiled query; the chat channels through their own read state, with the `cedar_inbox_item_state` override included — the SQL twin of the overlay applied after the read, so a chat the rep marked unread by hand survives the filter instead of vanishing while its badge says unread. The filtered list is a new cache key, so it is seeded with the unread subset of what is already cached: the filter lands on the next frame rather than a round trip, and the server's page — which reaches further back than the loaded window — only adds to it. That placeholder may only under-report (a row whose read state cannot be determined is dropped, never kept), and an empty one spins rather than claiming an inbox zero the server is about to contradict. A filtered inbox looks exactly like an empty one, so the tab wears a clickable "Unread" badge, the counts follow the filtered query under their own hash, and both empty states name the filter instead of congratulating the user on mail they have merely hidden. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 20, 2026, 11:25 PM