Email Thread
An email conversation from the user's mailbox, with an optional reply the agent drafted for review before anything is sent.
Experience · Shares patterns with Draft Options, Citations
≈ 2,139 tokens per use (tool 1,375 · guidance 297 · typical payload 467)How token costs are estimated
Example: Email Thread
What did Dana say about the contract?
Northwind MSA renewal: redlines
Openin your mail app (opens in a new tab)Dana Levi, Sam Ortiz, you · Work inbox
When to use
- The user asks about an email conversation: what someone said, where a thread stands, or to catch up on it.
- The user asks the agent to reply in a thread. Show the thread with the draft so they can review and edit it before anything is sent.
When not to use
- The agent couldn't read the thread. Fetch it first; never reconstruct messages from memory.
- The user wants a list of many emails, like a search or an inbox view. Use a Data Table.
- The user is writing a new email, not replying in a thread. Use Draft Options.
How the agent should use it
- Pass the messages verbatim from the mailbox, oldest first, as plain text. Split the quoted history into
quoted. - Mark the mailbox owner in
participantswithself: true. They're shown as "you". - Keep
summaryto one or two sentences that only state what the messages say. - Set
highlightto a sentence copied verbatim from that message's body. If it doesn't match exactly, nothing is highlighted. - Never send until the result is
send, then send exactly the returnedbody. Onrevise, rewrite the draft from the feedback and show the thread again. Ondiscard, send nothing.
Reading and replying
The summary, when given, leads the card as the answer. Every message is shown in full, with its sender, time, attachments and "Unread" or "Important" tags; the header carries one line of participants. Each message's recipients ("to Sam, cc Priya") and the sender's address are read to screen readers (the address is also the name's tooltip) but kept off screen. Only the quoted older text is collapsed, behind Show quoted, as in any mail client. Bodies keep their line breaks, and http(s) URLs become links that open in a new tab. Nothing is ever rendered as HTML.
Each message is an article named by its sender and time ("Dana Levi, Oct 6, 10:42 AM"). Times are relative ("Yesterday, 4:10 PM") with the full date in a tooltip, and are computed after mount so server and client markup match.
The highlighted sentence is wrapped in <mark> with a screen-reader note, "(answers your question)".
With draft, a Draft reply block follows the last message: recipients as plain text (name and address), and the body in an editable text area that grows with its content (its character count is read to screen readers). The footer offers Send, Discard, and Request changes (the message icon, which opens a feedback box). The component never sends anything: it returns the result, and the host sends, after its own confirmation if it needs one. Without a draft, the thread is display-only.
The helpers (participant lists, the autolinker, highlight splitting and relative time) are pure functions in email-thread-utils.ts.
Installation
npx shadcn@latest add https://your-site.com/r/email-thread.json
Usage
import { tool } from "ai"
import { emailThreadTool } from "@/components/genui/email-thread.schema"
const tools = {
// With a draft, no execute(): the call is completed by the user's decision.
// Without one, return immediately, e.g. execute: async () => ({ shown: true }).
[emailThreadTool.name]: tool({
description: emailThreadTool.description,
inputSchema: emailThreadTool.inputSchema,
}),
}<EmailThread
{...part.input}
status={part.state === "input-streaming" ? "streaming" : undefined}
result={part.state === "output-available" ? part.output : undefined}
onResult={(result) =>
addToolResult({ tool: "show_email_thread", toolCallId: part.toolCallId, output: result })
}
/>When there is a draft, the agent receives one of:
{ action: "send", body: "Hi Sam,\n\nThursday at 10 AM ET works…", edited: false }
{ action: "revise", feedback: "Mention that Dana will join too" }
{ action: "discard" }If the user changed the body, edited is true and body holds their version. Send exactly that text. While the host sends, pass status="submitting".
With LangGraph
Push it from a graph node with ui.push({ name: "show_email_thread", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.
Restoring from history
Pass the stored result back as result. A sent reply appears as the last message with a "Sent" label, a discarded draft is removed with a quiet note, and requested changes show the feedback. The component never calls onResult again.
<EmailThread
{...part.input}
result={part.state === "output-available" ? part.output : undefined}
/>Schema
| Prop | Type | Notes |
|---|---|---|
subject | string | Data-bound. The subject line |
mailbox | { label }? | Data-bound. e.g. "Work inbox". Read to screen readers only |
participants | { name, email, self? }[]? | Data-bound. self marks the mailbox owner, shown as "you". Derived from the messages when omitted |
messages | EmailMessage[] | Data-bound. 1–50, oldest first |
summary | string? | One or two sentences that reflect the messages |
highlight | { messageId, text }? | A sentence copied verbatim from that message's body |
draft | { to, cc?, body, replyToId }? | A reply for review. Adds the actions |
url | string? | Data-bound. Opens the thread in the mail app. http(s) only |
EmailMessage
| Field | Type | Notes |
|---|---|---|
id | string | The mailbox's message id |
from | { name, email } | |
to / cc | { name, email }[] | cc is optional |
sentAt | string | ISO 8601 with offset |
body | string | Plain text, never HTML, without the quoted history |
quoted | string? | The quoted older text, collapsed by default |
attachments | { name, sizeBytes?, kind?, url? }[]? | kind: pdf, image, doc, sheet or other |
unread / important | boolean? | Shown as tags |
Host props: status (streaming, ready, submitting, sent, revised, discarded), onResult, result and className.