use-conversations-sidebar-conversations.tsfix(sidebar): the latest event can be absent, and the type now says so
`latestEvent: events[0] ?? null` read as ALWAYS PRESENT.
`noUncheckedIndexedAccess` is off, so TypeScript believes `events[0]` on an array
always returns an event — it does not model "slot 0 of an empty list". It then
reasons that the left side of `??` can never be nullish, treats the `null` branch
as dead, and discards it. So every consumer was told `latestEvent` is a thing
that is always there, while the value is null on every deal with no events yet.
`filterConversationsByQuery` guarding it with `?.` is the tell: the author knew,
the type did not.
Annotating the const does NOT fix this — TypeScript keeps the declared type on
paper but narrows the variable to the non-null version at every use, so the
object literal still picks up the narrowed one. `Array.prototype.at` is typed
`T | undefined` unconditionally, so `events.at(0) ?? null` leaves nothing to
narrow and the union survives.
That unblocked the two sidebar test fixtures, which were building the DETAIL
projection and handing it to functions that take the LIST one — the exact
conflation the hook's own note says "took four casts, and their effect was that a
reader reaching for one of those fields compiled clean and read `undefined` at
runtime". Rebuilt against the real wire row: FLAT (the route spreads
`hydrated.conversation` and hangs the joins beside it), `occurredAt` an ISO
string rather than a `Date`, no `conversationScope`/`overviewItems`/`lastReviewedAt`,
and no `emailEvent`/`summary` on the event — which is why subject search goes
through `title`. Built once and referenced twice, since the second copy is what
drifts.
Three casts gone; the one left is the 43-column company row the haystack reads a
single field off, and it says so.
apps/mail types:test: 67 -> 52. All 40 conversationsPage tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 9, 2026, 9:11 PM1 defectuse-sidebar-all-columns.tsfeat(tasks): a CRM updates lane, meeting-aware routing, and narrow-panel polish
field-approval was the single largest Misc population (374 of a 677-task
backlog) with nowhere to go — work that finishes inside the CRM rather than
in someone's inbox now has its own default lane. The router additionally
counts meetings already held on a deal, which is what makes "first meeting"
vs "subsequent meeting" lanes decidable; countMeetingsSoFar is shared with
the opt-in post-meeting split script so a task lands in the same place
whichever path filed it. Backfill window/limit/confidence are env-overridable
because a newly-added lane leaves a deeper backlog than the routine 100/user.
Also in this commit (whole-tree — the session had no edit history to scope by):
- Back gutters collapsed a breakpoint too early; ~760px/75ch columns keep
usable margin past @max-4xl, so the swap moved to @max-3xl (wide-column
hosts 5xl -> 4xl) across threads, conversations, playbooks and Brain.
- Calendar event popover: larger title, single-line date row, and locations
and descriptions run through LinkifiedEventText so meeting links are
clickable.
- Snippets shrinks to an icon-only Braces button; the inbox shortcut legend
drops the channel switch and folds range/multi-select into one "s" hint.
- AOP custom-field column building extracted to crm/utils/aop-columns, and
InboxLinkedDealsSection split out (its test already imported the path).
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 4, 2026, 7:45 PM1 defect