calendarFollowUpUtils.tsfeat: v0 commandBarApr 9, 2026, 3:22 PMconferencing.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.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 PMdragDebug.tsfeat(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 PMis-own-event.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 PMjoinMeeting.tsfeat(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 AMmeetingEmailPrompts.tsfeat(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 AMpendingMovePreview.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 AMrecurrence.tsfeat(board): the board agent tool, the kanban, and the typed ticket (phases 3-4) Answers "can a board be used exactly like a table" with yes on all three surfaces: headless, agent, and UI. **The extraction that makes the rest honest.** Every board operation moved to `services/documents/board/board-ops.ts`. Three callers now wrap it — `admin.boards.*` (the CLI), a new session-scoped `boards.*` (the browser), and `boardTool` (the agent) — differing in exactly one thing: how they answer "who is asking". A kanban drag and `cedar-cli board card move --before` are the same function call, so the headless proof is evidence about what the UI does. **Phase 3 — the agent tool.** `boardTool` with all fourteen actions, a flat object with an `action` enum (a top-level union advertises `{properties: {}}` to the MCP SDK). `boards:` grant enforcement at the CALL SITE: a read-only grant refuses set_schema, an ungranted board is refused by name, and `list_boards`/cross-board `cards` are CLIPPED rather than errored. `grantsBoard` is deliberately not `grantsFile` — a prefix rule would make a grant on `organisation/boards` silently grant every board in the org. **Phase 4 — the UI.** A kanban (`BoardDocumentView`) drawn from one SQL query with dnd-kit drag ordering, and a ticket (`CardDocumentView`) in the same body+rail geometry `TaskTicketView` already uses. The rail renders every field the BOARD declares, typed — select pills, checkboxes, dates, URLs, `[[type: id]]` chips — by importing the table's chip renderer, option colours and type vocabulary rather than forking them. A field whose type Cedar has never heard of renders as text and says so. Two things the UI does that keep the schema open: it renders values whose keys the board does not declare under their own heading, and `BoardSchemaMenu` never serialises a fresh object from form state — every action reads the current schema out of the Y.Doc and merges. Those were the two remaining ways to silently close an open schema; the other two are on the server and were already closed. The tool description had to be cut from 2467 to 2044 chars — the tool-description-length guard catches exactly this, and text past 2048 is silently dropped for every caller. 1551 server tests and 217 frontend tests pass; both packages typecheck; oxlint clean. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 30, 2026, 9:33 PM