attendee-availability.test.tsfeat(calendar): compose an event on its own block, and show who is free
Creating an event took over the chat column, which put Cancel a column away from
the block it cancels and swapped out the chat to show a form about something
already on screen. The composer is now pinned to the draft's own block. The panel
survives only as the fallback for a draft raised with no block to pin to — a
follow-up from a thread, an event the agent proposes in chat — and both render the
same form, so the fields and the Enter-to-next flow cannot drift.
Clicking outside deliberately does not dismiss: the grid stays live so the block
can be dragged to another time while the form is up, and losing a half-written
event to a stray click is the worse failure. Escape has to be answered by the
popover itself — this repo's PopoverContent stops Escape propagation and Radix
listens in the capture phase, so a window listener inside the form never runs.
Adding a colleague to an invite is already the user saying they care when that
person is free, so they no longer have to say it twice in the separate "Meet with"
input. Internal guests — same email domain, which is also the boundary Google's
free/busy respects — get their busy time laid out on the grid beside the draft and
a free/busy verdict on their row. A calendar Google cannot read comes back as a
per-calendar errors array rather than a failed request, so an unreadable colleague
would otherwise look exactly like a free one; that reports no access instead.
Separately: the create call never sent sendUpdates, and Google's default for an
insert is to notify nobody. An event created with guests appeared on the
organiser's calendar and no guest was ever emailed — an invite that invites no
one, which is how it was reported.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 29, 2026, 4:08 PMcalendar-visibility.test.tsfix(mail): clear 309 typecheck errors and 5 live ReferenceErrors
apps/mail 792 -> 483. Jest stays green at 111 suites / 1071 tests.
The single biggest cluster was one missing line: tsconfig pins `types` to
["node","vite/client","jest"] and jest.setup.js is a .js file outside `include`, so the
@testing-library/jest-dom matcher augmentation never entered the program — 170 errors from
`expect(...).toBeInTheDocument()` and friends. A 4-line jest-dom.d.ts clears all of them.
Stripping @jest/globals from 22 test files was a prerequisite, not a tidy-up: only the
globally-injected `expect` picks up that augmentation. The other 145 test files already
used injected globals, so this also converges on the dominant convention. Do not install
@jest/globals.
The rest: CRM store test fixtures still extending state that moved to conversationsSlice in
Dec 2025 and importing a deleted CRMTypes module; barrel files re-exporting modules that no
longer exist (each one an import-time throw); a locally-declared SelectorItem whose module
was deleted; sanitizeJson taking `object` when its body explicitly handles primitives and
both callers already cast.
Five of these were live runtime bugs, not type noise:
- setForcePropagate(false) in the AOP-save onSuccess threw on EVERY successful playbook
save (cedarAdmin/page.tsx, admin/page.tsx)
- backendResponsiveness referenced in a useMemo dependency array, evaluated every render,
while the memo body no longer mentions it
- "Force re-sync" called handleForceSync() with no argument against a z.object() input
that rejects undefined, so it always failed validation
- an empty-string icon key produced `Icon === ''`, i.e. <'' />
- showOverlay leaked an unknown attribute onto a Radix DOM node
Worth knowing separately: the 792 were invisible. Plain tsc reported 3, because a syntax
error elsewhere in the tree makes TypeScript skip ALL semantic checking. One stray comma
blinds the entire typecheck.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 2, 2026, 2:11 PMcalendarEventLayout.test.tsfeat(calendar): lay out overlapping events the way Google Calendar does
Events were packed into a hard equal-width grid, so one busy moment shrank
every event in its collision group — even ones with empty space beside them —
and chips never overlapped, leaving titles truncated to nothing in a stack.
Grouping was also wrong: events joined the first group they touched and groups
were never merged, so transitively-linked events could collide on screen.
Layout is now three passes: connected-component collision groups, greedy lane
packing, then rightward expansion into lanes holding nothing beside the event.
Chips render wider than their lane and layer by depth, so each one keeps its
title strip exposed while the stack still shows every event.
Chips also had to stop being see-through: element opacity let the chip
underneath, colour ribbon and all, read through the one on top. Every recede
state now blends the event colour toward the grid canvas instead, with the
amounts declared per theme in globals.css.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Jul 30, 2026, 11:45 PMcalendarSlice.test.tsfix(mail): /calendar showed stale times for rescheduled events
useAllCalendarEvents memoised its merged event list on the number of events
each query returned. A reschedule leaves that count untouched, so the memo kept
handing back the previous array and its useEffect never fired — the correct
time sat in query.data, refetched every 30s, and was discarded. It only righted
itself once the event count in the window happened to change, or on remount.
Key the memo on dataUpdatedAt instead, and only treat a calendar as synced when
its query actually succeeded, so a failed fetch is never read as "empty".
mergeCalendarEvents was also upsert-only, so an event the response no longer
carries at all — deleted, declined, or moved out of the fetched range — had
nothing to overwrite it and lingered at its old slot. It now accepts the window
the fetch covered and evicts within it, leaving other date ranges, calendars
that did not answer, and unparseable starts alone. The single-event upserts in
NextEventWidget pass no window and so still cannot evict.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 19, 2026, 10:17 AMclient-only-events.test.tsfix(calendar,files): stop the 404s, and let a Drive folder open
Google answered 404 Not Found for every calendar mutation the UI sent, from two
independent causes.
`next-meeting-preview` is the client-side ghost CalendarView paints for an
unsaved draft, but only `new-…` ids counted as unsaved — so adding a guest to
the preview, and then trying to delete the block, both went to Google. Prod logs
for the reported session are sixteen straight 404s, update then delete. The
popover half of this landed in ead6b26a4 without `isClientOnlyEvent` itself,
leaving that commit unable to typecheck; the predicate is here, with the same
guard on the right-click menu.
Every mutation also omitted `calendarId`, so the server addressed `'primary'`
while the grid fetches each visible calendar separately and stamps its real id
on each event. Anything on a secondary or shared calendar 404'd on edit, drag,
RSVP or delete.
Separately: a Drive folder reaches the Files tab in the same flat list as the
files, and fell through to the file row — a Drive icon and no way in, which on
screen is a Google Doc that refuses to open. Folders now expand in place, and
the browse branch no longer unions the deal's pins into every folder it lists.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 29, 2026, 2:21 PMcomposerFocusRequest.test.tsfeat(calendar): the composer becomes the event panel, and the date field stops eating what you typed
Creating an event and editing one were two different panels — labelled boxed
inputs versus a column of icon rows — for the same fields. They now share one
set of primitives (eventFieldStyles), so the row rhythm and field chrome can
only change in both at once: plain text until you point at it, a wash on hover,
a blue rule on the field you are in, and the day above its two ends in one
block. Repeat moved out of the details popover into a control both use, and
location is not offered where there is no place picker behind it.
Three bugs, all the same shape: committing a date moves the event's block to
another column, so the popover pinned to it unmounts and a new one mounts.
- The draft is the only state that survives that, so a date change now carries
the whole form onto it (draftWithComposerFields) instead of just the times —
picking "next tuesday" used to clear the title.
- React's `autoFocus` re-fires on every remount, which is what kept snapping the
caret back to the title. Removed; the title is focused once per draft.
- "Enter on the date lands on Add guests" is now a request on the store, not a
call on a ref that the remount has already detached. The orders differ within
a week and across weeks, so nothing read only at mount could serve both.
Also: a day-only input no longer resets the clock — suggestions carry
`hasExplicitTime`, and chrono's implied hour is read with `isCertain`, which is
why the "at 9am" alternative almost never appeared. A created event is seeded
into the listEvents cache the grid actually reads, so it no longer blinks out
between the draft dropping and the refetch landing.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 30, 2026, 11:41 PMconferencing.test.tsfix(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 PMdraftEvent.test.tsfix(calendar): let an untouched event composer be dismissed by clicking away
Clicking Create opened a composer whose block then followed the cursor: every
click on the grid re-timed the blank event instead of abandoning it, and Cancel
was the only way out. An empty composer now behaves like a modal with a
transparent backdrop — the first click anywhere else means "not this" and
nothing else, the way an existing event's details popover already reads. The
moment anything is typed the backdrop goes and the calendar is live again, so a
half-written event is still never lost to a stray click.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 10:03 PMeventComposerPopoverDismiss.test.tsxfix(calendar): let an untouched event composer be dismissed by clicking away
Clicking Create opened a composer whose block then followed the cursor: every
click on the grid re-timed the blank event instead of abandoning it, and Cancel
was the only way out. An empty composer now behaves like a modal with a
transparent backdrop — the first click anywhere else means "not this" and
nothing else, the way an existing event's details popover already reads. The
moment anything is typed the backdrop goes and the calendar is live again, so a
half-written event is still never lost to a stray click.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 10:03 PMis-own-event.test.tsfeat(home): the day is one card, and the agenda only shows meetings that are mine
The home hero stacked a next-meeting card over a collapsible agenda, which fitted
exactly one meeting on screen and pushed the tasks under the fold. The day is now a
single "Daily agenda" card: today's tasks beside every one of today's meetings, with
the title row itself as the expand control and a clamp (not a fold) when collapsed.
Meetings render as bare rows — the calendar colour survives as a left rule and nothing
else — because a list of bordered tiles beside the tasks read as a second set of tasks.
Everything the row drops comes back in the event popover, now opened by HOVER and
anchored right. Hover, not click, because the row's click belongs to the conversation:
that needs a PopoverAnchor rather than a Trigger, and the portaled panel has to keep its
own hover alive. A meeting with no conversation stays pressable and says so.
The popover gains a bottom action row — Reschedule (No show while the meeting is
running) and Schedule Follow-Up, moved down from the top controls — both wearing the
composer's Send pill, since each one drafts and sends a mail.
Agents become cards under a per-folder toggle, each with one of the twenty animated
faces (resolution extracted from AgentsEditor so both surfaces draw the same face for
the same agent, hashed from the id rather than list position). Search overrides the
fold: a search that only looks in the open folder reports "no matches" for an agent
that is right there.
The filter that took the longest to get right is is-own-event: the daily agenda shows
PARTICIPATION, not access. Two traps, both of which leaked before being pinned in
tests — attendees[].self is relative to the calendar the event was READ from, so on a
teammate's calendar it flags the teammate; and "a calendar you own" is not "your
calendar", so accessRole 'owner' re-admitted every meeting on any secondary calendar.
Only the primary calendar counts, and the literal fetch alias 'primary' counts as it.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 26, 2026, 10:42 PMpendingMovePreview.test.tsfeat(calendar): the date expands where you clicked it, and the grid answers before you save
Clicking the date on an existing event opened a second panel over the first: a
month grid beside a natural-language field, which was a third way in that nobody
reached for and made an existing event's "when" a different shape from a new
one's. The row now expands in place into the SAME two rows the composer uses —
what you type on top, what it resolved to underneath — with the caret already in
the field. The month grid is gone; all-day, time zone and repeat came with it.
A time typed there is now answered on the grid immediately rather than only after
Save: the block keeps its saved slot, faded, and a ghost paints the slot it would
move to. Same pair of states a drag leaves while the pointer is down, and for the
same reason — where it was and where it is going are both things you need to see.
The panel stays anchored to the ORIGINAL block, which is what keeps it from
unmounting mid-edit, so the grid never navigates for an unsaved change.
Two bugs behind the two features:
- Committing a natural-language date went through `handleStartTimeChange`, which
derives the end DATE from the start date it closed over — the old day. So
"next tuesday" left the event ending on the day it used to be on, and Save
failed on "end must be after start". It now moves both ends itself, keeping the
duration, and honours `hasExplicitTime` so a day-only input stops resetting the
clock to 9am.
- The hovered panel (the meetings widget) closes when the pointer leaves it, and
the time picker and date suggestions are portaled OUT of it — so reaching for
the control that made an edit is what threw the edit away. It now pins itself
while it holds one, and closes the way every other panel does. Its X did
nothing at all: nine close controls called an `onOpenChange` a hover-driven
panel does not have, and now route through `requestOpenChange`.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 31, 2026, 12:38 AMuse-event-rsvp.test.tsxfeat(meetings): frontend for the Meetings tab (design: meeting-tab phases 2-10)
The deal's Meetings tab mounts the SAME `AgentWorkspace` component
`/agents/:agentId` mounts — conversation-scoped, embedded chrome — so a meeting
looks like an agent because it IS one. Anything rebuilt here instead of reused
would drift the first time either surface was touched.
Above it, a strip naming the meeting and stating whether prep has run for it.
Inside its Output tab, in order: the rep's NOTES for that meeting (the surface
you use during the call), the prep BRIEF (folded away once the call starts),
"Past Meeting Preps" as date chips over the agent's archives/, and a paged
"Previous meetings" log — then the agent's ordinary file tree.
The notes editor carries note markers (`- [pain] … <!--m:1f0c-->`), real
AgendaTaskNode task lines bound to the deal, @-mentions of the attendees, and a
slash command per marker kind plus /template, /ask-cedar, /draft-followup.
The prep agent is RELOCATED, not hidden: dropped from the conversation's agent
rows and from the Files tab's Agents folder, but still built and still
resolvable, because `resolveOpenDoc` resolves the `__meeting_prep__` sentinel
through that list. Legacy `{convId}/files/__meeting_prep__` links now route to
the Meetings tab instead of a dead end.
Post-meeting triage is wired into the real pipeline, two ways: the notes go into
the orchestrator's prompt as context (with the rule that they outrank the
transcript on what was committed to), and `triageMeetingNotes` runs
deterministically after the meeting so a marker becomes a task whether or not the
model cooperated. Idempotent, so a replay and the manual button cannot double-create.
Six corrections the build forced, each recorded in the doc:
- The prep pane must NOT resolve the brief with `documents.getDoc` —
find-or-create would mint an empty brief and make "prep never ran"
permanently unobservable, killing the Run-prep affordance.
- `agent.getOutputs` returned no `metadata`, so the archive stamp was landing
and nothing could read it; every archive fell back to date-matching.
- `documents.list` hard-filters userId, so org templates were invisible —
hence `meetings.listNoteTemplates`.
- A marker inside a bullet list serialized as `- - [pain] …`, which the
contract regex rejects: the rep's line vanished from triage silently.
- Pasted markers must be re-minted at PASTE time, not de-duped afterwards, or
pasting above the original re-mints the ORIGINAL and strands a task's
source_marker_id.
- `follow-up` and `next-meeting` were the same action rendered twice.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 27, 2026, 1:26 AMzoomConnectDialog.test.tsxfix(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 PM