describeProposedMutation.test.tsfix(chat): an approval card that names what it is about to delete
The delete-document card read "Delete document" over a bare UUID — the payload
carried a documentId and the renderer never looked at it, so a cleanup that
proposed two hundred deletions was two hundred identical cards. The tool now
reads the document's name before deleting (afterwards the row is gone) and sends
it as documentTitle + documentPath; cards persisted before that resolve the name
from the live document. All eleven proposed-mutation types now name their
subject through one descriptor table, laid out like the calendar proposal.
A card with no pendingMutationId says so rather than offering an Approve that can
only toast — off the Agent-SDK harness nothing persists one, and pending
mutations expire from KV after an hour.
document.delete stays auto-apply, deliberately: it is a tombstone, and this adds
the other half of that promise — purgeExpiredDeletedDocuments hard-deletes
tombstones past a 30-day window, on the nightly cron, with a dry-run-by-default
CLI for operators. It never purges a tombstone that still has a LIVE descendant:
parent_id cascades on a real DELETE, and a "no live direct child" check misses
the grandchild two levels down.
Also gates the conversation a chat thread claims to be about. primaryConversation
is committed automatically from whatever the shell has open, and the thread-row
seed wrote the client's snapshot verbatim — which is how a /home chat ended up
committed to a deal in another org, invisible in the UI and handed to every tool
in the turn as its operating deal.
And email-thread context chips wear the real Gmail mark, from one shared
ContextKindIcon the rail and the chips both draw from instead of each deciding.
Co-Authored-By: Claude Opus 5 <<email>>Sep 7, 2026, 2:08 PMdraft-body.test.tsfix(drafts): give the draft body its spacing back, and settle on one body shape
The chat draft card asked for spacing with `prose prose-sm dark:prose-invert`,
but no typography plugin is installed and globals.css defines no `.prose` rule —
the class did nothing, while Preflight's `* { margin: 0; padding: 0 }` stripped
every paragraph gap and `ol, ul { list-style: none }` took the bullets.
That is why it looked intermittent. A `<p>`-shaped draft lost its blank lines
outright and rendered as one block of text; a `<div>x</div><div><br></div>` one
survived, because a `<br>` is content and Preflight only removes properties. The
div-shaped drafts were not fine either — their `<ul>` had been rendering as
unmarked indented lines the whole time, quietly enough that nobody reported it.
`.draft-body` hands the browser defaults back with `revert` for that subtree
only — the same shape of rule `.ProseMirror` already uses a few lines above. An
email body is authored against a browser's default stylesheet, the one Gmail
renders it with, so reverting is both closer to what the recipient sees and
proof against whatever tag the agent reaches for next. Only the density is
re-tightened: a 2em <h1> and a 40px list indent are out of scale in a preview.
Server side, nothing settled the body shape. Over a week of traces `draft-email`
emitted `<div>` 1594/1594 times while chat `save-draft` emitted `<p>` on ~39% of
drafts, with a tail of whole `<!DOCTYPE html>` documents and of markup escaped
into visible text. `normalizeDraftBodyHtml` converts to Gmail's div form at both
tool boundaries and again in `createDraftInProvider`. Note it converts the tag
AND supplies the spacer: a `<br>` bolted onto a `<p>` would stack a blank line on
top of margins the paragraph already has, and double-space the sent mail.
The HTML sniff was also too loose. `/<[a-z][\s\S]*>/` read `Hi <name>,` and a
bare `<user@host>` as markup, so a plain-text draft was handed to the sanitizer,
which ate the bracketed span and collapsed the newlines. Detection now needs a
real void tag or a matched pair, and copy-to-clipboard and the collapsed preview
leave plain text alone instead of running a tag-strip over it.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 26, 2026, 5:23 PMprovisionalPrimaryContext.test.tsfeat(inbox): unibox keyboard parity, undo-send that puts the draft back, sharper chat context
Unibox now navigates like the email list — j/k, Enter, Escape all walk whichever
list is on screen via a new `activeListSource` on the thread slice, and Escape
clears the full selection (focused row, bulk checkboxes, ambient conversation)
instead of only the keyboard cursor.
Undo send stops round-tripping through localStorage and a URL rewrite: the send
records where the draft came from and undo reopens that surface, restoring the
un-quoted body onto the draft row (recreating it when the send removed it).
Chat context: an auto-committed primary conversation is provisional while the
chat is empty and drops when you move on, the context cards follow the deal
you're actually on, threads with no conversation get a link prompt, and the
conversation surface shows a keyboard legend rather than restating cards already
on screen.
Also: agenda Trash removes the row outright, Next steps renders as one card with
its booked calls beneath, and channel chats focus the composer on open.Jul 30, 2026, 11:54 PMtoolCallDetails.test.tsfeat: the transcript names what it did, and the table it made is reachable
A chat that spent twenty calls building a LinkedIn tracker recorded almost
none of it: four different people got connection requests under four
identical "Executing linkedin-write" lines, the table itself never became a
context item, and re-entering the chat reopened the deal instead of the
table. Traced end to end; this is the set of causes.
The transcript:
- Tool labels resolve from the ARGUMENTS, not just the name. Every tool is an
action family now, so the name alone collapses a dozen verbs into one row.
Rows expand to who / where / whether a message went with it.
- A select column's declared option order IS its sort ladder; the tool
description now says so, and the transcript shows the ladder.
LinkedIn caps:
- A send's provider-id lookup gets its own budget (connect/dm/inmail_lookup),
mirroring the send it serves. Charged to profile_view, a day of browsing
refused invitations that had 80/week of connect headroom left.
- Connection requests go out with NO note unless the user asked for one —
enforced at execute time, not just described.
Getting back to what the agent made:
- The table tool files a context chip on create/import, as write-document has
always done; delete pulls it back off.
- A thread remembers what it was displaying (chat_threads.context), so
re-entry restores the table rather than re-deriving the deal.
- A fan-out opens a conversation-scoped table inside its deal's Files tab.
- 'table' is a valid write type on the document family — the tool was telling
the agent to pass a value its own schema rejected, which is what drove a
delete-and-rebuild that orphaned the open document.
The grid:
- Title, then description, then the commands — the order the questions arrive
in; a URL or address in a cell is a real link; a deleted table says so
instead of rendering on from the local Y.Doc cache.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 20, 2026, 11:43 PM