From 93101b2e837a2f0559a7f00305655e8df746db3d Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Thu, 14 May 2026 14:52:01 +0530 Subject: [PATCH] handle drafts --- .../renderer/src/components/email-view.tsx | 9 ++++-- .../core/src/knowledge/classify_thread.ts | 13 ++++++-- .../packages/core/src/knowledge/sync_gmail.ts | 31 +++++++++++++++---- apps/x/packages/shared/src/blocks.ts | 1 + 4 files changed, 43 insertions(+), 11 deletions(-) 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 9de2715c..415a792f 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), });