extensionsfix(drafting): the drafts a send leaves behind, the drafts nobody sees, and the link Space ate
Three A5 items, all in the gap between a draft existing in Gmail and Cedar knowing
about it.
Leftover drafts after a send (Vooma, Horison). Gmail retires the draft that was
actually sent; nothing retired the others, and a send without a draftId only deleted
one when an emailHeaderMessageId/id happened to ride along. So a thread that had
collected a second Cedar draft kept it, and the rep found a stale pre-send copy under
a message they had already sent — 77 of these across 21 accounts in three weeks.
mail.send now sweeps the thread afterwards, deleting only drafts Cedar authored
(checked against analytics_draft_actions, the same authorship record the rewrite path
adopts by). Fire-and-forget: the mail is sent, and nothing here may fail that.
Drafts with no task at all. The chat/headless save-draft path created a real Gmail
draft and attached it to nothing — 90% of emailResponses drafts and 100% of
preMeetingPrep drafts in the week to Aug 30 had no user_tasks row pointing at them,
so the user could not open, close or track them and nothing could find them to clean
up. It now upserts the draft task the on-event path has always created, via the same
idempotent upsertDraftTask, skipping interactive surfaces where the draft card
already carries its own task descriptor.
Link formatting (Concentrate). ExitLinkOnSpace bound Space globally and called
unsetLink() whenever the caret was anywhere inside a link — and unsetLink extends
over the whole mark range on a collapsed selection, so editing a word in the middle
of a hyperlink and pressing space stripped the href off the entire link, with the
next autosave writing the de-linked body to Gmail. It now acts only at the END of a
link, which is the case it was written for; mid-link it declines and the space types
normally. The Link mark is inclusive only when autolink is on, which the composer
turns on, so the extension does still have a job.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 30, 2026, 9:19 PMmarkdown-editor-liveness.test.tsxfix(mail): stop a destroyed editor reaching onEditorCreated and killing the page
Follow-on to 0bcd5cde0, which guarded the effect sites that touch a TipTap editor
but left the handoff itself unguarded. `MarkdownEditor` announces its editor to the
parent from a `useEffect` on `[editor]`, gated by `if (editor)` — the half-guard the
last fix was about. TipTap builds the editor a tick after mount and tears it down on
a 1ms timer after unmount, and a destroyed editor is not null: it keeps its identity
while commandManager/view/state are nulled underneath it. A surface that disappears
in that gap leaves the effect holding a live-LOOKING editor, and every consumer
callback reaches for `.commands` or `.getMarkdown()` first thing — so the throw lands
inside an effect and escapes to the route error boundary, replacing the whole page
with "Something went wrong!".
That is what four users hit today. The deployed bundle (release 7ddb16f8) still
carries two unguarded callbacks in the ConversationView chunk — NextStepsCard's
`editor.commands.setContent(nextSteps, …)` and the Slack composer's
`setContent`/`getMarkdown` pair — and the resolved frames on every event today are
`onEditorCreated` -> TipTap's `get commands`, throwing
"Cannot read properties of null (reading 'commands')" on /agent?conversationId=…
and /home?chat=… URLs.
Guarding the handoff rather than the seven callbacks that consume it means no
consumer can ever be handed a dead editor — including the four outside the
conversation view (AgentsEditor, the share page, the agents demo) that would
otherwise each need the same line. Nothing is lost by skipping the call: a
destroyed editor cannot accept content, and a real remount builds a new editor,
which changes the `[editor]` dep and re-runs the effect to seed normally.
Why this started: the seeding code is unchanged since April, the handoff since
March, and TipTap's destroy machinery is byte-identical between 3.18 and 3.26. What
changed is where the card lives — b6ded45e9 put NextStepsCard on the Overview tab
(2026-07-13) and 47dc63f32 gave that tab doc embeds (2026-07-27), so an artifact now
slides over it and unmounts it mid-creation. First page-replacing occurrence is
2026-07-29 17:00Z, on a /strategicOverview/ URL.
The test drives `useEditor` directly so the destroyed state is exact rather than
timing-dependent; it fails on the old line with the production error text.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 14, 2026, 2:01 PMmarkdown-prose-classes.test.tsstyle(coaching): space the section breaks in the markdown, not the stylesheet
Reverts the hr margin to its original my-4. The breathing room above a section
break is now authored per-document: a paragraph containing only a zero-width
space on the blank line above each `---`. A document that wants the space asks
for it; one that does not is not padded by the stylesheet.
The filler has to be exactly `​`, and the near-misses are the reason this
came with a test. Measured through the real parse/serialize round trip that runs
on every agent rewrite:
​ 4 -> 4 -> 4 -> 4 nodes survives
4 -> 4 -> 3 -> 3 renders once, gone by the third rewrite
U+00A0 4 -> 3 -> 3 -> 3 gone by the second
blank 3 collapses immediately
So the obvious choices fail silently: correct on the first render, and the
spacing quietly evaporates the next time the coaching agent rewrites the file,
with nothing in the diff to explain it. The playbook now names the token and
says not to tidy it away.
42 spacers across the 7 documents that have section rules; the per-call docs use
headings and take none. Verified in the published tree: 42 zero-width spaces
across 43 documents.
Heading and paragraph spacing from the previous commit is kept — only the rule
margin is reverted.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 25, 2026, 6:25 PMmenu-selection-indicator.test.tsxfix(ui): a menu marks the chosen row with a tick on the right, never a black dot
The agent workspace's Folder and Default file submenus put a solid filled disc
beside the current value, in a left gutter — shadcn's `RadioItem` default,
inherited without anyone choosing it. It out-weighed the label it was marking,
it sat in the lane that belongs to the option's own icon, and it looked nothing
like the checkbox rows in the same menu, which were already ticking on the right.
One mark, one side, for every "which one(s)?" surface: `Check` on the right in a
lane every row reserves. Applied to `DropdownMenuRadioItem`, its checkbox
sibling, both `ContextMenu` items and the cedar-os copy of the menu; `SelectItem`
and `OptionPicker` were already correct. `radio-group.tsx` keeps its dot — there
the dot IS the control, not an annotation on one.
Written into the design system (crystallized.md → Counts, ticks and ordinals,
plus the panel checklist and CLAUDE.md), and guarded by a test, because the dot
is a one-line regression and nothing else in the suite would notice.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 7, 2026, 1:43 PM