Message Draft

Review messages before sending.
npx tool-agent "integrate the message draft component to review and approve messages before sending"
npx shadcn@latest add @tool-ui/message-draft

An AI sending a message on your behalf without asking is a trust violation. MessageDraft shows the full email or Slack message for review, gives you a Send button with an undo grace period, and collapses into a compact receipt once sent or cancelled. The agent never sends silently.

Role: Decision. For choices that return to the assistant. See Design Guidelines for how component roles work.

Getting Started

Run this once from your project root.

npx tool-agent "integrate the message draft component to review and approve messages before sending"
npx shadcn@latest add @tool-ui/message-draft

Render MessageDraft in your UI with tool-compatible props.

import { MessageDraft } from "@/components/tool-ui/message-draft";export function Example() {  return (    <MessageDraft      id="message-draft-example"      channel="email"      subject="Q4 Planning Follow-up"      to={["sarah.chen@company.com"]}      body={`Hi Sarah,Thanks for joining the planning meeting today. I've attached the updated timeline.Best,Alex`}      onSend={() => console.log("Sent")}      onCancel={() => console.log("Cancelled")}    />  );}

Register this renderer so tool results display as MessageDraft.

"use client";import { type Toolkit } from "@assistant-ui/react";import { MessageDraft } from "@/components/tool-ui/message-draft";import {  safeParseSerializableMessageDraft,  SerializableMessageDraftSchema,} from "@/components/tool-ui/message-draft/schema";export const toolkit: Toolkit = {  draftMessage: {    description: "Draft a message for the user to review before sending.",    parameters: SerializableMessageDraftSchema,    render: ({ args, toolCallId, result, addResult }) => {      const parsedArgs = safeParseSerializableMessageDraft({        ...args,        id: args?.id ?? `message-draft-${toolCallId}`,      });      if (!parsedArgs) {        return null;      }      return result ? (        <MessageDraft {...parsedArgs} outcome={result} />      ) : (        <MessageDraft          {...parsedArgs}          onSend={async () => {            // Perform actual send logic here            await sendMessage(parsedArgs);            await addResult?.("sent");          }}          onCancel={() => addResult?.("cancelled")}        />      );    },  },};

Key Features

Human-in-the-loop

Nothing sends until the user clicks Send

Undo grace period

Configurable countdown after Send where the user can still cancel

Channel skins

Email shows subject/recipients, Slack shows channel or DM target

Receipt states

Collapses to a one-line confirmation after send or cancel

Channel Skins

Two channel types, each with its own metadata layout.

Email

Shows subject line, To/CC/BCC recipients, and message body.

Slack

Shows channel or DM target with the Slack icon.

Send Flow

Clicking Send starts a countdown. The user can undo before it completes:

  1. Review - User reviews the draft with Send/Cancel buttons
  2. Sending - Countdown with Undo button (default 5 seconds)
  3. Sent - Compact receipt confirming the message was sent

Configure the countdown length with undoGracePeriod (in milliseconds).

Receipt States

Pass outcome="sent" to render a one-line confirmation receipt. outcome="cancelled" hides the component entirely.

Props

Prop

Type

Email-specific Props

Prop

Type

Slack-specific Props

Prop

Type

Accessibility

  • Uses article with aria-labelledby for the draft content
  • Keyboard navigation:
    • Tab to move between buttons
    • Enter or Space to activate focused button
    • Escape triggers cancel while in review state
  • Sending state announces countdown via aria-live="polite"
  • Receipt state uses role="status" for screen reader announcements
  • Focus moves to Undo button when entering sending state
  • Approval Card: binary approve/deny for other consequential actions