use-attendee-availability.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 PMuse-calendar-canvas-overlay.tsfeat(home): the calendar pill opens the calendar, and ⇧Q is on its face
The /agent hero's third pill asked the agent to write about the day; the hero
already SHOWS the day right below it. It now opens the week grid instead — the
same overlay ⇧Q has always opened, which nothing on the home screen taught.
The open/close logic moves out of GlobalHotkeys into useCalendarCanvasOverlay so
the key and the button are one action rather than two that can drift.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 26, 2026, 10:56 PMuse-calendar-favicon.tsfix(chat,drive,calendar): close the gaps the review found
The draft chat only rendered its tab when no other chat was open, so the +
button left the strip selecting a value with no trigger behind it — nothing
looked selected and no "New chat" appeared. And since opening a conversation
still needs a thread to hang its display pointer on, every home → open a deal →
home round trip pinned another untouched chat: the pile-up the draft chat was
meant to stop, arriving the long way round. Stepping off an untouched chat now
closes it.
Withholding drive.readonly meant a re-link could take it away from someone who
already had it: Google's response can only speak to scopes we asked for, and
persisting it verbatim read that silence as a revocation — with no re-auth
button to recover. That one scope now survives while we are not asking.
Also carries the in-flight move of "New chat" out of the chat header and into
the composer toolbar.
Co-Authored-By: Claude Opus 5 <<email>>Aug 30, 2026, 9:04 PMuse-calendar-hotkey-items.tsfeat(board): board and card document types (design: customer-feedback-loop phase 1)
A board is a file and a card is a file, end to end through the save pipeline,
with no UI and no agent yet.
A board joins `table` as a document type whose content lives outside the
`prosemirror` fragment: a `boardSchema` Y.Map holds lanes and field
definitions. Unlike a table it holds no items — its cards are separate
`documents` rows parented to it, which is what gives a ticket its own path,
share link, `[[doc: id]]` address and a real foreign key. A card is
behaviourally an ordinary markdown document whose body is the description.
The load-bearing property is that the schema is OPEN: unknown keys at every
level round-trip verbatim, because `writeBoardSchema` merges into the Y.Map
rather than replacing it. A writer on an older deploy therefore cannot delete
a key it has not heard of. Exactly three things may refuse a schema write —
a reserved key whose shape changed, the 64KB cap, and nothing else; an
unknown field `type` is kept and rendered as `text`, and an unrecognised lane
`role` is kept and ignored.
Cards adopt rather than reject: a card naming an undeclared lane or field
appends it to the board's schema instead of failing the write.
Migration is two indexes, applied by hand (never `pnpm db:push`): a
jsonb_path_ops GIN on `documents.metadata` and a partial btree on
`(parent_id)` for live cards, both CONCURRENTLY.
53 new tests; `pnpm --filter @zero/server run types` green and the full
document-saving + documents suites (655 tests) pass.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 30, 2026, 8:20 PMuse-calendar-tasks.tsfix(types): close 205 typecheck errors by repairing real client/server drift
apps/mail typechecked against a stale apps/server/dist d.ts (the server's
package.json points "types" at dist), so a chunk of the reported drift was
phantom. Rebuilt it and fixed the four script errors that blocked `tsc -b`
in server, which is what regenerates it.
The rest was genuine contract drift, fixed at the source rather than cast
away — routes that dropped fields their callers still read (agentExecutions,
getUpcomingCalendarEvents' conversationId, createCanvas' description),
schema splits the UI never followed (conversationUsers vs
conversationContacts), and enums that grew server-side but not client-side
(TASK_TYPES, ActionStatus, DateFilterOperator).
Also deletes code that was already dead: an unreferenced sort popover whose
store API is gone, a panel importing a deleted module, the system-skill
metadata UI whose mutation was removed deliberately, and a test asserting a
store method that no longer exists.
apps/server: 4 -> 0. apps/mail: 228 -> 23, all remaining errors being
dependency resolution (zod v3/v4, react-router dev/runtime skew, tiptap
v2 via novel) rather than code.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 2, 2026, 7:45 PMuse-calendar-timezones.tsfeat(mail): rebuild the left rail around chats, and fix chat auto-titling
The sidebar's "Agents" section becomes "Chats": the wordmark and the Chats nav
item always land on a fresh chat (reusing the current one when it is already
untouched), open chats close from a hover x, the list scrolls and backfills the
4 most recent chats on reload, and both the section and per-chat collapse
chevrons are gone. Inbox moves up to second.
Auto-titling never fired on a single-turn chat: the thread row is only written
at the END of a turn, so the parallel title summariser found no row and bailed.
It now seeds the row itself, and the placeholder-name set collapses to one
"New chat".
Also renames the default "Responses" task group to "Responses needed" (with a
migration for existing users), states task placeholders as "No active tasks",
and quiets the thread delete button until hover.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 1, 2026, 12:43 PMuse-calendar.tsfix(meetings): the widget rolled to tomorrow before today's meetings had loaded
`todayExhausted` read straight off the events array with no notion of whether
that array had been fetched. `useAllCalendarEvents` returns `[]` while pending,
so on the first paint of every session the day read as spent, and the host's
auto-advance — which fires once and latches — moved you to tomorrow with four
meetings still to come.
Gate it on the data actually being there: the hook now reports `hasLoaded`
(`!isLoading && isSuccess`, because an errored query is also neither loading nor
answered while `events` stays `[]`), and the agenda additionally waits on the
own-event filter having an identity to match against — it fails open only while
BOTH the addresses and the calendar list are empty, so the window in between can
legitimately drop the user's own meetings.
The trim rule moves to liveDay.ts so the test drives the shipped functions. It
used to re-implement them beside itself, which is exactly why this shipped
green: a copy of a rule cannot fail when the rule does.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 1, 2026, 11:40 AMuse-conferencing.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 PMuse-event-manipulation.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 PMuse-event-rsvp.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 AM