Repository navigation
Conversation
|
Here's a visual recap of what changed: Open the full interactive recap |
[Medium impact] The changes affect Mail’s agent-draft card and the actions available inside it. Restore draft save and delete warnings and satisfy the density rule before merging. FindingsSummaryAdds a compact
Acknowledged choices from
|
When an outside agent drafts with manage-draft, MCP Apps hosts now embed a new /draft route instead of the whole inbox. A reply shows the thread's earlier messages folded to one line each, the latest message open, and the editable draft with Send; a new email shows just the draft. "Open in Mail" keeps the full-inbox deep link. The card reuses the inbox's message rows and the real composer in a new "card" presentation: no window chrome, no autofocus, copied recipients shown, and send progress reported to the card instead of toasts. The card shows sending, sent, scheduled, deleted, failed-send, missing-draft and unreadable-thread states, and tells the chat's model when the user sends, schedules or deletes the draft.
The card swallowed a failed model-context update into false, which the no-silent-coercion guard rejects. It now logs a rejected or refused update; the card itself already shows the outcome to the person.
50c447f to
dd63f3a
Compare
- discard() now resolves to whether the draft and its saved mailbox copy are really gone. The card shows "Draft deleted." at once but tells the model only once the delete sticks, and shows a failure state if not. - Scheduling a draft that was edited mid-request keeps the edited draft open, reports no outcome and says what was scheduled in a toast. - Only a known or looked-up thread ID reaches the draft's replyToThreadId; a guessed one is used only to show the conversation. - The card shows save and delete failures itself, since it renders without AppLayout, and its not-found notice drops the description.
The card adds a reply's thread ID once its lookup returns. If that happened while a schedule request was in flight, isSameScheduledDraft saw an edit, so the draft stayed open with no outcome reported and a second send could duplicate the email. The thread ID is filled in, not edited, so the comparison now leaves it out.
A reply drafted with only the message it answers gets its thread ID from a lookup. Scheduling before that lookup returned sent the job without a thread ID, and the scheduled-send path only threads a reply when the payload has one. The card now holds Send and Send later (a new sendDisabled prop on ComposeModal) until the lookup settles, and keeps the lookup's first answer, so the thread can't change under a schedule request. That makes the earlier exclusion in isSameScheduledDraft unnecessary, so it's gone: an agent moving the draft to another thread while it's being scheduled counts as an edit again and keeps the draft open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Builder reviewed your changes and has a few items to flag 🟡
Review Details
Incremental review summary
I reviewed PR #8018 at 8a7a30da against the previous reviewed head 2869ba10. The update restores comparison of replyToThreadId so explicit retargets during scheduling remain open, and holds sending/scheduling while a reply's thread lookup is pending. I verified and resolved the prior explicit-thread-change finding. The pending-lookup safeguard addresses the normal in-flight case; the existing thread-ID finding remains open for cases where lookup settles as an error or cannot run, so I did not repost it.
This remains a standard-risk Mail lifecycle change. I found one additional low-severity issue: the lookup error is stored as a settled result for that key, so a later successful refetch is ignored and the draft never receives its resolved thread ID. Because the error state also clears lookupPending, send and schedule are enabled in that state. No further confirmed issues were found in the incremental diff.
🧪 Browser testing: Skipped — browser-test-planner is unavailable in this environment; PR touches UI code.
|
Thanks for the update. The loading safeguard covers an in-flight lookup, but a lookup error is cached as a settled result; |

Problem
Outside agents, such as Claude, ChatGPT or Cursor connected to Mail over Model Context Protocol (MCP), write emails with Mail's
manage-draftaction. Chat apps that support MCP Apps then embed the result in the conversation, so the person can check the draft and send it. Until now that embed was the whole Mail inbox with the compose window open, in a 900px-tall frame. Most of it went to the sidebar and message list, the draft sat in a corner, and at phone width it was barely usable.Approach
The embed now points at a new
/draft?id=…route that shows only what the person needs to review and send:The draft is Mail's real composer, not a copy: recipients, Cc and Bcc, the From picker (when more than one account is connected), formatting, attachments, delete, Send and scheduling work as in the inbox, and edits save to the same draft. The host's "Open in Mail" button still opens the full inbox with this draft.
After Send, the card shows the usual undo window, then "Message sent." It also covers a scheduled send, a deleted draft, a failed send or delete, a missing draft and a thread that couldn't load. When the person sends, schedules or deletes, the card tells the chat's model, so the agent doesn't keep editing a draft that's gone. It reports only what really happened: a delete once the draft and its saved copy in Gmail are both gone, and nothing when the draft was edited while it was being scheduled. That edited draft stays open, with a toast saying the earlier version is scheduled. When an agent drafted a reply without naming its thread, Send and scheduling wait the moment it takes the card to look the thread up, so a scheduled reply lands in its conversation.
This pairs with #8005, which adds
manage-draftto the tools Mail offers outside agents. This PR only changes what they see.Screenshots
Rendered as an MCP App in MCPJam's ChatGPT emulator, from a production build with fake mail.
At 380px, the width of Claude's phone app, the thread gives up more height so the draft stays readable:
Other states
What changed
All in
templates/mail:actions/manage-draft.ts: create and update return the/draftpath as the embed, with the inbox link asdeepLinkUrlfor "Open in Mail". The frame height drops from 900 to 604.app/routes/draft.tsxandapp/components/email/DraftCard.tsx: the new route and card.AppLayoutrenders/draftwithout the app shell, as it already does for/email. The inbox's toasts for a failed save or delete live in that shell, so the card shows them itself. A reply's draft gets a thread ID only when it already has one or Mail looks it up from the message it answers. Send and Send later wait for that lookup, and its first answer sticks, so a reply's thread can't change while it's being scheduled. When the lookup fails, the card may still try the message's own ID to show the conversation, but never puts that guess on the draft.ComposeModal.tsx: apresentation="card"mode, anonSendProgresscallback and asendDisabledprop. The card mode drops the window's title bar, autofocus and Escape-to-close, shows Cc and Bcc when the draft already has them, and hides the subject on replies. When a caller passesonSendProgress, it gets the send states and the send toasts are skipped.sendDisableddisables Send and Send later, including their keyboard shortcuts. If the draft changes while a schedule request is in flight, including a move to another thread, the edited draft stays open and the "Scheduled for…" toast shows in both modes.EmailThread.tsxexports the folded and open message rows for the card to reuse; their reply and forward buttons hide when no handler is passed.use-compose-state.tsexposesisErrorandrefetch, so the card can tell a missing draft from one that couldn't load. Itsdiscardnow resolves to whether the draft and its saved copy in Gmail were both deleted; existing callers ignore the result.mail.draftCardstrings in all 11 locales, and a changelog entry.Safety and operations
AGENT_NATIVE_MCP_APPS_INLINE(or its per-email allowlist) is on. Elsewhere, agents get the same text result and link as before. This PR adds no flag.POST /api/emails/send. Reverting the PR restores the full-inbox embed.MCP_CHAT_BRIDGE_VIEWPORT_HEIGHTinpackages/core/src/client/embed-auth.ts), plus the host bar's 44px. A shorter frame for new emails would need core to accept a height per embed, so both cases use 604 and the card fits itself to it.Verification
DraftCardtests (16) cover the reply and new-email layouts, folding, every outcome state, the message to the model, a delete that doesn't stick, a failed save, a reply whose Send waits for its thread lookup, one whose lookup fails, and two connected accounts reaching the From picker. FiveComposeModaltests cover the card mode (send states replace toasts; copied Cc shows without a click; a draft edited while scheduling stays open; so does one moved to another thread;sendDisabledholds Send and scheduling), andmanage-draft.spec.tschecks the embed path and deep link. The full Mail suite (1,941 tests),agent-native typecheckand all 93pnpm guardspass at the current head.{draftId, status: "sent"}. Undo returned to the editable draft.frame-src 'self'), so Claude's layout is inferred from the 380px render. Beta, once this and Expose Mail draft review to connected agents #8005 merge, is the first real-host check.Review focus
ComposeModal.tsxwraps the title bar in{!isCard && …}, so most of that file's diff is re-indentation. Hide whitespace to see the logic.onSendProgress, the send toasts are skipped and the card shows each state instead. Is there a path where a failure shows neither? The schedule-failure toast still shows in both modes on purpose, as does "Scheduled for…" when the draft was edited mid-schedule.{draftId, status}, and reports a delete only once it has stuck. Is that the right shape for agents to act on?Follow-ups