diff --git a/apps/x/apps/renderer/src/components/email-view.tsx b/apps/x/apps/renderer/src/components/email-view.tsx
index 895c31e0..959c5c71 100644
--- a/apps/x/apps/renderer/src/components/email-view.tsx
+++ b/apps/x/apps/renderer/src/components/email-view.tsx
@@ -399,12 +399,15 @@ function ComposeBox({
const to = mode === 'reply' ? extractAddress(latest?.from) : ''
const initialContent = useMemo(() => {
- if (mode !== 'reply' || !thread.draft_response) return ''
- return thread.draft_response
+ if (mode !== 'reply') return ''
+ // Gmail-side draft (user's own work) wins over the AI-generated draft.
+ const source = thread.gmail_draft || thread.draft_response
+ if (!source) return ''
+ return source
.split(/\n{2,}/)
.map((para) => `
${escapeHtml(para).replace(/\n/g, '
')}
`)
.join('')
- }, [mode, thread.draft_response])
+ }, [mode, thread.gmail_draft, thread.draft_response])
const editor = useEditor({
extensions: [
diff --git a/apps/x/packages/core/src/knowledge/classify_thread.ts b/apps/x/packages/core/src/knowledge/classify_thread.ts
index 1a742ecc..69109e34 100644
--- a/apps/x/packages/core/src/knowledge/classify_thread.ts
+++ b/apps/x/packages/core/src/knowledge/classify_thread.ts
@@ -196,9 +196,14 @@ function buildPrompt(
return lines.join('\n');
}
+export interface ClassifyOptions {
+ skipDraft?: boolean;
+}
+
export async function classifyThread(
snapshot: GmailThreadSnapshot,
userEmail: string | null,
+ options: ClassifyOptions = {},
): Promise {
if (userReplied(snapshot, userEmail)) {
return { importance: 'important' };
@@ -213,9 +218,13 @@ export async function classifyThread(
const config = await resolveProviderConfig(provider);
const model = createProvider(config).languageModel(modelId);
+ const systemPrompt = options.skipDraft
+ ? `${SYSTEM_PROMPT}\n\n# Skip the draft\n\nThe user already has their own draft in progress for this thread — DO NOT generate a draftResponse. Always omit the draftResponse field.`
+ : SYSTEM_PROMPT;
+
const result = await generateObject({
model,
- system: SYSTEM_PROMPT,
+ system: systemPrompt,
prompt: buildPrompt(snapshot, userEmail, styleGuide, calendar),
schema: ClassificationSchema,
});
@@ -231,7 +240,7 @@ export async function classifyThread(
const out: Classification = { importance: result.object.importance };
if (result.object.importance === 'important') {
if (result.object.summary) out.summary = result.object.summary;
- if (result.object.draftResponse) out.draftResponse = result.object.draftResponse;
+ if (!options.skipDraft && result.object.draftResponse) out.draftResponse = result.object.draftResponse;
}
return out;
} catch (err) {
diff --git a/apps/x/packages/core/src/knowledge/sync_gmail.ts b/apps/x/packages/core/src/knowledge/sync_gmail.ts
index eec5fd33..a5aacfe2 100644
--- a/apps/x/packages/core/src/knowledge/sync_gmail.ts
+++ b/apps/x/packages/core/src/knowledge/sync_gmail.ts
@@ -95,6 +95,7 @@ export interface GmailThreadSnapshot {
unread?: boolean;
importance?: 'important' | 'other';
draft_response?: string;
+ gmail_draft?: string;
messages: Array<{
id?: string;
from?: string;
@@ -452,6 +453,7 @@ export async function fetchThreadSnapshot(threadId: string, expectedHistoryId?:
bodyHtml = parts.html;
}
}
+ const isDraft = msg.labelIds?.includes('DRAFT') ?? false;
return {
id: msg.id || undefined,
from: headerValue(headers, 'From') || 'Unknown',
@@ -463,11 +465,24 @@ export async function fetchThreadSnapshot(threadId: string, expectedHistoryId?:
bodyHtml,
unread: msg.labelIds?.includes('UNREAD') ?? false,
bodyHeight: msg.id ? heightCarryover.get(msg.id) : undefined,
+ isDraft,
};
}));
- const latest = parsed[parsed.length - 1]!;
- const earlier = parsed.slice(0, -1);
+ const sentMessages = parsed.filter((m) => !m.isDraft);
+ const draftMessages = parsed.filter((m) => m.isDraft);
+ // Drop the isDraft helper field from outgoing messages — it's internal.
+ const visibleMessages = sentMessages.map(({ isDraft: _isDraft, ...rest }) => rest);
+ const latestDraftBody = draftMessages.length > 0
+ ? draftMessages[draftMessages.length - 1]!.body.trim()
+ : '';
+
+ // A thread with no sent messages (only a draft) shouldn't show up in the inbox —
+ // skip caching it. Once the user actually sends, the thread reappears with a real message.
+ if (visibleMessages.length === 0) return null;
+
+ const latest = visibleMessages[visibleMessages.length - 1]!;
+ const earlier = visibleMessages.slice(0, -1);
const earlierSummary = earlier
.map((msg) => {
const date = msg.date ? ` (${msg.date})` : '';
@@ -480,19 +495,23 @@ export async function fetchThreadSnapshot(threadId: string, expectedHistoryId?:
const snapshot: GmailThreadSnapshot = {
threadId,
threadUrl: `https://mail.google.com/mail/u/0/#all/${threadId}`,
- subject: latest.subject || parsed[0]?.subject,
+ subject: latest.subject || visibleMessages[0]?.subject,
from: latest.from,
to: latest.to,
date: latest.date,
latest_email: latest.body,
past_summary: earlierSummary || undefined,
- unread: parsed.some((m) => m.unread),
- messages: parsed,
+ unread: visibleMessages.some((m) => m.unread),
+ messages: visibleMessages,
+ gmail_draft: latestDraftBody || undefined,
};
try {
const userEmail = await getUserEmail(auth);
- const classification = await classifyThread(snapshot, userEmail);
+ // If the user already has a Gmail-side draft going, skip the AI draft generation —
+ // the renderer will prefer the Gmail draft anyway, and we save an LLM call.
+ const skipDraft = latestDraftBody.length > 0;
+ const classification = await classifyThread(snapshot, userEmail, { skipDraft });
snapshot.importance = classification.importance;
if (classification.summary) snapshot.summary = classification.summary;
if (classification.draftResponse) snapshot.draft_response = classification.draftResponse;
diff --git a/apps/x/packages/shared/src/blocks.ts b/apps/x/packages/shared/src/blocks.ts
index c7b48011..b5552c28 100644
--- a/apps/x/packages/shared/src/blocks.ts
+++ b/apps/x/packages/shared/src/blocks.ts
@@ -122,6 +122,7 @@ export const GmailThreadSchema = EmailBlockSchema.extend({
threadUrl: z.string().url(),
unread: z.boolean().optional(),
importance: z.enum(['important', 'other']).optional(),
+ gmail_draft: z.string().optional(),
messages: z.array(GmailThreadMessageSchema),
});