componentsfix(mail): a link in a chat thread is a link
LinkedIn and WhatsApp send their words as plain text, so a pasted URL
rendered as characters. Slack linkified only its own <url|label> tokens —
which its composer emits and a bot, an unfurl field or pasted text do not.
Bare URLs are now linkified in both, before the mrkdwn formatting pass so
the parser cannot read a URL's underscores as italics. The splitter is
lifted out of the calendar's LinkifiedEventText into lib/linkify.
Links are painted with the action ramp, not primary: primary is near-black
on light and near-white on dark, the colour of the words around them.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 18, 2026, 9:38 AM1 defecthooksMerge remote-tracking branch 'origin/staging' into fix/dm-container-kind
# Conflicts:
# apps/mail/modules/home/widgets/HomeAgentsWidget.tsxSep 2, 2026, 12:32 AMstorerefactor(agenda): a type that says "this event has a start and an end", so nothing has to assert it
Greptile, on the merged head: the agenda casts a filtered array and then asserts its way
through the dates — `ev.start!.dateTime!` at a dozen sites — which is the one thing CLAUDE.md
says never to do, and for the usual reason. `CalendarEvent.start`/`end` are optional and
nullable because Google allows an all-day event and a malformed one; every agenda surface
filters those out and then does arithmetic on what is left, and each `!` is a claim the
compiler cannot check, repeated until one of them is wrong.
Named the post-filter shape instead. `TimedCalendarEvent` is an event with both ends present
and timed, reachable only through `isTimedCalendarEvent` — a real predicate, checked at
runtime. The filters that already performed that check now SAY so in their signature, and
`visibleEventsForDay` being generic carries the narrowing all the way to the row renderer, so
every assertion downstream simply became legal code.
`endOfEvent`'s `POSITIVE_INFINITY` fallback is gone with them: its own comment said the branch
should never be reached, and now the type is what guarantees it.
The `event as CalendarEvent` casts at the render seams were the same mistake wearing a
different hat — the callbacks declared the NARROWER type while the data always carried its
`calendarId`, so every call site cast back. That hid a real consumer: `AttendeeIndicator`
needs `CalendarEventWithCalendarId`, which `AgendaEventBlock` was reaching by way of
`as unknown as`. The callbacks now take what the lists actually hold, and all eleven casts and
assertions are gone rather than relocated.
Not taken: Greptile also asked for `./contextKinds` to become an absolute import. Every sibling
in that directory imports its neighbours the same way, and `cedar-os` is a vendored module with
its own `@/` root — the convention is about crossing module boundaries, which this does not.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 7, 2026, 9:59 PMtypesfeat(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 PMutilsfix(calendar,ui): a Zoom connect dialog, and four things the review found
The Zoom work lands its last piece: picking Zoom on an unconnected account used
to open a bare browser window with no explanation and no way back. ZoomConnectDialog
owns everything around the consent screen — why Cedar is asking, the wait, and the
four different reasons a popup ends without a connection — and resolves back to the
composer so one click on "Change to Zoom" still ends with Zoom on the event. The
OAuth callback URL now falls back to the server's own origin, so an unset env var
is no longer the difference between a working integration and a message telling a
user to edit a file they have never heard of.
Agent avatars move to components/icons/agent-avatar.tsx as one component, and the
conversation/agent reading column narrows from 100ch to 80ch across all four places
that hold the measure.
Review fixes:
- Breadcrumb decided "you are here" positionally, so it rendered the last crumb as
an inert span and silently DISCARDED any href it was handed. A task's bar is
`Tasks / <lane>` above the task's own title — the h1 below is the page, and the
lane is somewhere to go — so its link had become dead markup. The current crumb is
now the last one with nowhere to go, which every other call site already is.
- modifyCalendarEventTool passed a start time but no duration to Zoom, which
silently books 60 minutes: an AI-updated 25-minute event held an hour on the
host's own account.
- schema.ts declared two_factor_required without .notNull(), drifting from the
NOT NULL the migration actually applied.
- HomeAgentsWidget cast a tRPC result that was already typed — the redundant-cast
shape that took down the home rail once before.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 11:54 PMcalendarUtils.tsfix(calendar): stop event chips at the column edge, so a busy hour stays clickable
Chips ran the full width of their day column, leaving nowhere to click or
drag to book over an existing event. Lay them out inside the column minus an
8px gutter — the strip Google leaves for exactly that — including the
rightmost lane and the drag previews.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 26, 2026, 12:54 PMindex.tsfeat: superior /mail & pipeline designApr 19, 2026, 9:00 PM