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