From e4244a8ce5912002cc5fccce79293f239922485f Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Wed, 13 May 2026 11:57:09 +0530 Subject: [PATCH] direct gmail initial commit --- apps/x/apps/main/src/ipc.ts | 11 + .../renderer/src/extensions/email-block.tsx | 279 ++++++++++++++---- .../assistant/skills/live-note/skill.ts | 3 +- .../core/src/knowledge/ensure_daily_note.ts | 4 +- .../core/src/knowledge/inline_task_agent.ts | 6 +- .../core/src/knowledge/live-note/agent.ts | 12 +- .../packages/core/src/knowledge/sync_gmail.ts | 65 ++++ apps/x/packages/shared/src/blocks.ts | 3 +- apps/x/packages/shared/src/ipc.ts | 10 + 9 files changed, 318 insertions(+), 75 deletions(-) diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index f07a1542..d2ab043a 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -47,6 +47,7 @@ import { summarizeMeeting } from '@x/core/dist/knowledge/summarize_meeting.js'; import { getAccessToken } from '@x/core/dist/auth/tokens.js'; import { getRowboatConfig } from '@x/core/dist/config/rowboat.js'; import { runLiveNoteAgent } from '@x/core/dist/knowledge/live-note/runner.js'; +import { fetchThreadSnapshot } from '@x/core/dist/knowledge/sync_gmail.js'; import { liveNoteBus } from '@x/core/dist/knowledge/live-note/bus.js'; import { getInstallationId } from '@x/core/dist/analytics/installation.js'; import { API_URL } from '@x/core/dist/config/env.js'; @@ -482,6 +483,16 @@ export function setupIpcHandlers() { 'workspace:remove': async (_event, args) => { return workspace.remove(args.path, args.opts); }, + 'gmail:getThread': async (_event, args) => { + try { + return { thread: await fetchThreadSnapshot(args.threadId) }; + } catch (error) { + return { + thread: null, + error: error instanceof Error ? error.message : String(error), + }; + } + }, 'mcp:listTools': async (_event, args) => { return mcpCore.listTools(args.serverName, args.cursor); }, diff --git a/apps/x/apps/renderer/src/extensions/email-block.tsx b/apps/x/apps/renderer/src/extensions/email-block.tsx index d615c212..cbfed013 100644 --- a/apps/x/apps/renderer/src/extensions/email-block.tsx +++ b/apps/x/apps/renderer/src/extensions/email-block.tsx @@ -54,6 +54,137 @@ function avatarColor(from: string): string { return GMAIL_AVATAR_COLORS[hash % GMAIL_AVATAR_COLORS.length] } +function extractThreadId(config: Pick): string | null { + if (config.threadId) return config.threadId + if (!config.threadUrl) return null + const url = config.threadUrl.trim() + const hashId = url.match(/#(?:all|inbox|sent|important|starred|search\/[^/]+)\/([^/?#]+)/) + if (hashId?.[1]) return decodeURIComponent(hashId[1]) + const queryId = url.match(/[?&](?:th|threadId)=([^&]+)/) + if (queryId?.[1]) return decodeURIComponent(queryId[1]) + const tailId = url.match(/\/([a-f0-9]{12,})\/?$/i) + return tailId?.[1] || null +} + +function parseSyncedGmailThread(markdown: string, threadId: string): blocks.EmailBlock | null { + const subject = markdown.match(/^#\s+(.+)$/m)?.[1]?.trim() + const chunks = markdown + .split(/\n---\n/g) + .map(chunk => chunk.trim()) + .filter(chunk => /^### From:/m.test(chunk)) + + const messages = chunks.map((chunk) => { + const from = chunk.match(/^### From:\s*(.+)$/m)?.[1]?.trim() + const date = chunk.match(/^\*\*Date:\*\*\s*(.+)$/m)?.[1]?.trim() + const body = chunk + .replace(/^### From:\s*.+$/m, '') + .replace(/^\*\*Date:\*\*\s*.+$/m, '') + .replace(/\n\*\*Attachments:\*\*[\s\S]*$/m, '') + .trim() + return { from, date, body } + }).filter(message => message.from || message.body) + + const latest = messages[messages.length - 1] + if (!latest) return null + + const earlier = messages.slice(0, -1) + const pastSummary = earlier + .map((message) => { + const date = message.date ? ` (${message.date})` : '' + const body = message.body.replace(/\s+/g, ' ').slice(0, 500).trim() + return `${message.from || 'Unknown'}${date}: ${body}` + }) + .filter(Boolean) + .join('\n\n') + + return { + threadId, + threadUrl: `https://mail.google.com/mail/u/0/#all/${threadId}`, + subject, + from: latest.from, + date: latest.date, + latest_email: latest.body, + past_summary: pastSummary || undefined, + } +} + +function mergeHydratedEmail(base: blocks.EmailBlock, hydrated: blocks.EmailBlock | null): blocks.EmailBlock { + if (!hydrated) return base + return { + ...hydrated, + ...base, + threadId: base.threadId || hydrated.threadId, + threadUrl: base.threadUrl || hydrated.threadUrl, + subject: hydrated.subject || base.subject, + from: hydrated.from || base.from, + to: hydrated.to || base.to, + date: hydrated.date || base.date, + latest_email: hydrated.latest_email || base.latest_email, + past_summary: hydrated.past_summary || base.past_summary, + summary: base.summary || hydrated.summary, + } +} + +function useHydratedEmail(config: blocks.EmailBlock): { email: blocks.EmailBlock; loading: boolean; error: string | null } { + const [email, setEmail] = useState(config) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const threadId = extractThreadId(config) + const configKey = JSON.stringify(config) + + useEffect(() => { + let cancelled = false + const baseConfig = blocks.EmailBlockSchema.parse(JSON.parse(configKey)) + + async function load() { + setEmail(baseConfig) + setError(null) + if (!threadId) { + setLoading(false) + return + } + + setLoading(true) + let hydrated: blocks.EmailBlock | null = null + let loadError: string | null = null + + try { + const result = await window.ipc.invoke('gmail:getThread', { threadId }) + if (result.thread) { + hydrated = result.thread + } else if (result.error) { + loadError = result.error + } + } catch (err) { + loadError = err instanceof Error ? err.message : String(err) + } + + if (!hydrated) { + try { + const result = await window.ipc.invoke('workspace:readFile', { + path: `gmail_sync/${threadId}.md`, + encoding: 'utf8', + }) + hydrated = parseSyncedGmailThread(result.data, threadId) + } catch (err) { + loadError ||= err instanceof Error ? err.message : String(err) + } + } + + if (!cancelled) { + setEmail(mergeHydratedEmail(baseConfig, hydrated)) + setError(hydrated ? null : loadError) + setLoading(false) + } + } + + void load() + return () => { cancelled = true } + }, [configKey, threadId]) + + return { email, loading, error } +} + declare global { interface Window { __pendingEmailDraft?: { prompt: string } @@ -88,8 +219,9 @@ function EmailExpandedBody({ let prompt = draftBody ? `Help me refine this draft response to an email` : `Help me draft a response to this email` - if (config.threadId) { - prompt += `. Read the full thread at gmail_sync/${config.threadId}.md for context` + const threadId = extractThreadId(config) + if (threadId) { + prompt += `. Read the full thread at gmail_sync/${threadId}.md for context` } prompt += `.\n\n**From:** ${config.from || 'Unknown'}\n**Subject:** ${config.subject || 'No subject'}\n` if (draftBody) prompt += `\n**Current draft:**\n${draftBody}\n` @@ -113,9 +245,8 @@ function EmailExpandedBody({ }) }, [draftBody]) - const gmailUrl = config.threadId - ? `https://mail.google.com/mail/u/0/#all/${config.threadId}` - : null + const threadId = extractThreadId(config) + const gmailUrl = config.threadUrl || (threadId ? `https://mail.google.com/mail/u/0/#all/${threadId}` : null) const initial = config.from ? getInitial(config.from) : '?' const color = config.from ? avatarColor(config.from) : '#5f6368' @@ -138,7 +269,7 @@ function EmailExpandedBody({ -
{config.latest_email}
+
{config.latest_email || 'Loading latest message...'}
{config.past_summary && (
@@ -224,6 +355,69 @@ function EmailExpandedBody({ // --- Multi-email inbox block (language-emails) --- +function EmailInboxRow({ + email, + expanded, + onToggle, + resolvedTheme, +}: { + email: blocks.EmailBlock + expanded: boolean + onToggle: () => void + resolvedTheme: string +}) { + const { email: hydratedEmail, loading, error } = useHydratedEmail(email) + const senderName = hydratedEmail.from ? extractName(hydratedEmail.from) : 'Unknown' + const initial = hydratedEmail.from ? getInitial(hydratedEmail.from) : '?' + const color = hydratedEmail.from ? avatarColor(hydratedEmail.from) : '#5f6368' + const snippet = hydratedEmail.summary + || (hydratedEmail.latest_email ? hydratedEmail.latest_email.slice(0, 100).replace(/\s+/g, ' ').trim() : '') + || (loading ? 'Loading latest Gmail thread...' : error || '') + + return ( +
+ {/* Collapsed row */} +
{ e.stopPropagation(); onToggle() }} + onMouseDown={(e) => e.stopPropagation()} + > +
{initial}
+ +
+
+ {senderName} + {hydratedEmail.date && {formatEmailDate(hydratedEmail.date)}} +
+
+ {hydratedEmail.subject && {hydratedEmail.subject}} + {snippet && ( + + {hydratedEmail.subject ? ` — ${snippet}` : snippet} + + )} +
+
+ + +
+ + {/* Expanded content */} + {expanded && ( +
+ +
+ )} +
+ ) +} + function EmailsBlockView({ node, deleteNode }: { node: { attrs: Record } deleteNode: () => void @@ -258,53 +452,14 @@ function EmailsBlockView({ node, deleteNode }: {
{config.emails.map((email, i) => { const isExpanded = expandedIndex === i - const senderName = email.from ? extractName(email.from) : 'Unknown' - const initial = email.from ? getInitial(email.from) : '?' - const color = email.from ? avatarColor(email.from) : '#5f6368' - const snippet = email.summary - || (email.latest_email ? email.latest_email.slice(0, 100).replace(/\s+/g, ' ').trim() : '') - return ( -
- {/* Collapsed row */} -
{ e.stopPropagation(); setExpandedIndex(isExpanded ? null : i) }} - onMouseDown={(e) => e.stopPropagation()} - > -
{initial}
- -
-
- {senderName} - {email.date && {formatEmailDate(email.date)}} -
-
- {email.subject && {email.subject}} - {snippet && ( - - {email.subject ? ` — ${snippet}` : snippet} - - )} -
-
- - -
- - {/* Expanded content */} - {isExpanded && ( -
- -
- )} -
+ setExpandedIndex(isExpanded ? null : i)} + resolvedTheme={resolvedTheme} + /> ) })}
@@ -387,11 +542,13 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { ) } - const senderName = config.from ? extractName(config.from) : 'Unknown' - const initial = config.from ? getInitial(config.from) : '?' - const color = config.from ? avatarColor(config.from) : '#5f6368' - const snippet = config.summary - || (config.latest_email ? config.latest_email.slice(0, 120).replace(/\s+/g, ' ').trim() : '') + const { email, loading, error } = useHydratedEmail(config) + const senderName = email.from ? extractName(email.from) : 'Unknown' + const initial = email.from ? getInitial(email.from) : '?' + const color = email.from ? avatarColor(email.from) : '#5f6368' + const snippet = email.summary + || (email.latest_email ? email.latest_email.slice(0, 120).replace(/\s+/g, ' ').trim() : '') + || (loading ? 'Loading latest Gmail thread...' : error || '') return ( @@ -407,11 +564,11 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: {
{senderName} - {config.date && {formatEmailDate(config.date)}} + {email.date && {formatEmailDate(email.date)}}
- {config.subject && {config.subject}} - {snippet && {config.subject ? ` — ${snippet}` : snippet}} + {email.subject && {email.subject}} + {snippet && {email.subject ? ` — ${snippet}` : snippet}}
@@ -419,7 +576,7 @@ function EmailBlockView({ node, deleteNode, updateAttributes }: { {expanded && ( )} diff --git a/apps/x/packages/core/src/application/assistant/skills/live-note/skill.ts b/apps/x/packages/core/src/application/assistant/skills/live-note/skill.ts index 715c049d..48952e43 100644 --- a/apps/x/packages/core/src/application/assistant/skills/live-note/skill.ts +++ b/apps/x/packages/core/src/application/assistant/skills/live-note/skill.ts @@ -324,7 +324,8 @@ live: Maintain a digest of email threads worth attention today, as a single \`emails\` block. Without an event payload (cron / window / manual runs): scan \`gmail_sync/\` and emit the - full digest from scratch. + full digest from scratch. In each email entry, store only \`threadId\` or \`threadUrl\`, + optional \`summary\`, and optional \`draft_response\`; the block hydrates Gmail metadata. With an event payload (event run): integrate the new thread into the existing digest — add it if new, update its entry if the threadId is already shown — and don't re-list diff --git a/apps/x/packages/core/src/knowledge/ensure_daily_note.ts b/apps/x/packages/core/src/knowledge/ensure_daily_note.ts index 341ddfd9..8d6e960c 100644 --- a/apps/x/packages/core/src/knowledge/ensure_daily_note.ts +++ b/apps/x/packages/core/src/knowledge/ensure_daily_note.ts @@ -14,7 +14,7 @@ const DAILY_NOTE_PATH = path.join(KNOWLEDGE_DIR, 'Today.md'); // on-disk `templateVersion` against this constant — if older or missing, the // existing file is renamed to Today.md.bkp. and replaced with the // new template. v2 is the live-note rewrite (single objective, no `track:`). -const CANONICAL_DAILY_NOTE_VERSION = 2; +const CANONICAL_DAILY_NOTE_VERSION = 3; const TODAY_LIVE_NOTE: z.infer = { objective: @@ -24,7 +24,7 @@ const TODAY_LIVE_NOTE: z.infer = { 2. **Calendar** — today's meetings as a single \`calendar\` block titled "Today's Meetings". Read \`calendar_sync/\` via \`workspace-readdir\` → \`workspace-readFile\` each \`.json\`. Filter to today; after 10am drop meetings that have already ended. Always emit the block (use \`events: []\` when empty). Set \`showJoinButton: true\` if any event has a \`conferenceLink\`. -3. **Emails** — a digest of email threads worth attention today, as a **single** fenced \`emails\` block (plural — never individual \`email\` blocks per thread). Body shape: \`{"title":"Today's Emails","emails":[...]}\`. Each entry: \`threadId\`, \`subject\`, \`from\`, \`date\`, \`summary\`, \`latest_email\`. For threads needing a reply, add \`draft_response\` written in the user's voice — direct, informal, no fluff. For FYI threads, omit \`draft_response\`. Skip marketing, auto-notifications, and closed threads. Without an event payload, scan \`gmail_sync/\` (skip \`sync_state.json\` and \`attachments/\`), prioritising threads where frontmatter \`action = "reply"\` or \`"respond"\`. With an event payload, integrate qualifying new threads into the existing digest (add a new entry for a new threadId; update the existing entry if shown). Don't re-list threads the user has already seen unless their state changed. If nothing qualifies: "No new emails." +3. **Emails** — a digest of email threads worth attention today, as a **single** fenced \`emails\` block (plural — never individual \`email\` blocks per thread). Body shape: \`{"title":"Today's Emails","emails":[...]}\`. Each entry should contain only \`threadId\` and/or \`threadUrl\`, plus optional \`summary\` if useful for why it matters. Do **not** copy \`subject\`, \`from\`, \`date\`, or \`latest_email\` from Gmail into the block — the email block hydrates the latest thread from Gmail directly. For threads needing a reply, add \`draft_response\` written in the user's voice — direct, informal, no fluff. For FYI threads, omit \`draft_response\`. Skip marketing, auto-notifications, and closed threads. Without an event payload, scan \`gmail_sync/\` (skip \`sync_state.json\` and \`attachments/\`), prioritising threads where frontmatter \`action = "reply"\` or \`"respond"\`, and use the filename/Thread ID to set \`threadId\`. With an event payload, integrate qualifying new threads into the existing digest (add a new entry for a new threadId; update the existing entry if shown). Don't re-list threads the user has already seen unless their state changed. If nothing qualifies: "No new emails." 4. **What you missed** — a short markdown summary of yesterday's meetings + emails that matter this morning. Pull decisions / action items from \`knowledge/Meetings///\` (\`workspace-readdir\` recursive on \`knowledge/Meetings\`, filter folders matching yesterday's date, read each file). Skim \`gmail_sync/\` for unresolved threads. Skip recurring/routine events. If nothing notable: "Quiet day yesterday — nothing to flag." diff --git a/apps/x/packages/core/src/knowledge/inline_task_agent.ts b/apps/x/packages/core/src/knowledge/inline_task_agent.ts index cd17a940..a991812c 100644 --- a/apps/x/packages/core/src/knowledge/inline_task_agent.ts +++ b/apps/x/packages/core/src/knowledge/inline_task_agent.ts @@ -163,11 +163,11 @@ If there are events, include them: 1. Use \`workspace-readdir\` with path \`gmail_sync\` to list files (skip \`sync_state.json\` and \`attachments/\`) 2. Use \`workspace-readFile\` to read the email markdown files (e.g. \`gmail_sync/threadid123.md\`) 3. Check the frontmatter \`action\` field — emails with \`action: reply\` or \`action: respond\` need a response -4. Output ALL emails (both action items and FYI) in a single \\\`\\\`\\\`emails block as a JSON array. Emails needing a response get a \`draft_response\`. Write drafts in the user's voice — direct, informal, no fluff. Example: +4. Output ALL emails (both action items and FYI) in a single \\\`\\\`\\\`emails block as a JSON array. Each entry should contain only \`threadId\` and/or \`threadUrl\`, plus optional \`summary\` if useful. Do not copy sender, subject, date, or latest body into the block; the renderer hydrates those from Gmail. Emails needing a response get a \`draft_response\`. Write drafts in the user's voice — direct, informal, no fluff. Example: \`\`\` \\\`\\\`\\\`emails -{"title":"Today's Emails","emails":[{"threadId":"abc123","summary":"Payment confirmation","subject":"Google services payment","from":"Sender ","date":"2026-04-01T11:28:39+05:30","latest_email":"Hi, I've made the payment...","draft_response":"Thanks for confirming. I'll update our records."},{"threadId":"def456","summary":"Security alert","subject":"New sign-in from Chrome","from":"Google ","date":"2026-04-01T09:15:00+05:30","latest_email":"A new sign-in to your account was detected."}]} +{"title":"Today's Emails","emails":[{"threadId":"abc123","summary":"Payment confirmation needs acknowledgement.","draft_response":"Thanks for confirming. I'll update our records."},{"threadId":"def456","summary":"FYI security alert."}]} \\\`\\\`\\\` \`\`\` @@ -221,4 +221,4 @@ When you see a target region associated with your task (during a scheduled run), - Use the existing content as context (e.g., to update rather than regenerate from scratch if appropriate) - Do NOT include the target tags themselves in your response `; -} \ No newline at end of file +} diff --git a/apps/x/packages/core/src/knowledge/live-note/agent.ts b/apps/x/packages/core/src/knowledge/live-note/agent.ts index 66cacbc3..610551ab 100644 --- a/apps/x/packages/core/src/knowledge/live-note/agent.ts +++ b/apps/x/packages/core/src/knowledge/live-note/agent.ts @@ -160,19 +160,17 @@ Required: \`events\` (array). Each event optionally has \`summary\`, \`start\`/\ ## \`email\` — single email or thread digest (JSON) -Use for: surfacing one important thread — latest message body, summary of prior context, optional draft reply. +Use for: surfacing one important Gmail thread. Prefer storing only a Gmail thread reference plus an optional draft reply; the renderer hydrates sender, subject, date, and latest body from Gmail. \`\`\`email { - "subject": "Q3 launch readiness", - "from": "sarah@acme.com", - "date": "2026-04-19T16:42:00Z", - "summary": "Sarah confirms timeline; flagged blocker on infra capacity.", - "latest_email": "Hey — quick update on Q3...\\n\\nThanks,\\nSarah" + "threadId": "18d5a3b2c1e4f567", + "summary": "Needs a reply on the Q3 launch blocker.", + "draft_response": "Thanks for flagging. I'll check with infra and get back to you today." } \`\`\` -Required: \`latest_email\` (string). Optional: \`threadId\`, \`summary\`, \`subject\`, \`from\`, \`to\`, \`date\`, \`past_summary\`, \`draft_response\`, \`response_mode\` ("inline" | "assistant" | "both"). +Required: \`threadId\` or \`threadUrl\`. Optional: \`summary\`, \`draft_response\`, \`response_mode\` ("inline" | "assistant" | "both"). Legacy fields \`subject\`, \`from\`, \`to\`, \`date\`, \`latest_email\`, and \`past_summary\` are supported for older notes, but don't emit them for Gmail threads. For digests of **many** threads, prefer a \`table\` (Subject | From | Snippet) — \`email\` is for one thread at a time. diff --git a/apps/x/packages/core/src/knowledge/sync_gmail.ts b/apps/x/packages/core/src/knowledge/sync_gmail.ts index c8f696eb..d5daae8c 100644 --- a/apps/x/packages/core/src/knowledge/sync_gmail.ts +++ b/apps/x/packages/core/src/knowledge/sync_gmail.ts @@ -21,6 +21,18 @@ interface SyncedThread { markdown: string; } +export interface GmailThreadSnapshot { + threadId: string; + threadUrl: string; + summary?: string; + subject?: string; + from?: string; + to?: string; + date?: string; + latest_email?: string; + past_summary?: string; +} + function summarizeGmailSync(threads: SyncedThread[]): string { const lines: string[] = [ `# Gmail sync update`, @@ -124,6 +136,59 @@ function getBody(payload: gmail.Schema$MessagePart): string { return body; } +function normalizeBody(body: string): string { + return body.replace(/\r\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim(); +} + +function headerValue(headers: gmail.Schema$MessagePartHeader[] | undefined, name: string): string | undefined { + return headers?.find(h => h.name?.toLowerCase() === name.toLowerCase())?.value || undefined; +} + +export async function fetchThreadSnapshot(threadId: string): Promise { + const auth = await GoogleClientFactory.getClient(); + if (!auth) { + throw new Error('Gmail is not connected.'); + } + + const gmailClient = google.gmail({ version: 'v1', auth }); + const res = await gmailClient.users.threads.get({ userId: 'me', id: threadId }); + const messages = res.data.messages; + if (!messages || messages.length === 0) return null; + + const parsed = messages.map((msg) => { + const headers = msg.payload?.headers || []; + return { + from: headerValue(headers, 'From') || 'Unknown', + to: headerValue(headers, 'To'), + date: headerValue(headers, 'Date'), + subject: headerValue(headers, 'Subject') || '(No Subject)', + body: msg.payload ? normalizeBody(getBody(msg.payload)) : '', + }; + }); + + const latest = parsed[parsed.length - 1]!; + const earlier = parsed.slice(0, -1); + const earlierSummary = earlier + .map((msg) => { + const date = msg.date ? ` (${msg.date})` : ''; + const body = msg.body.replace(/\s+/g, ' ').slice(0, 500).trim(); + return `${msg.from}${date}: ${body}`; + }) + .filter(Boolean) + .join('\n\n'); + + return { + threadId, + threadUrl: `https://mail.google.com/mail/u/0/#all/${threadId}`, + subject: latest.subject || parsed[0]?.subject, + from: latest.from, + to: latest.to, + date: latest.date, + latest_email: latest.body, + past_summary: earlierSummary || undefined, + }; +} + async function saveAttachment(gmail: gmail.Gmail, userId: string, msgId: string, part: gmail.Schema$MessagePart, attachmentsDir: string): Promise { const filename = part.filename; const attId = part.body?.attachmentId; diff --git a/apps/x/packages/shared/src/blocks.ts b/apps/x/packages/shared/src/blocks.ts index 69f58f97..e09638ab 100644 --- a/apps/x/packages/shared/src/blocks.ts +++ b/apps/x/packages/shared/src/blocks.ts @@ -88,12 +88,13 @@ export type CalendarBlock = z.infer; export const EmailBlockSchema = z.object({ threadId: z.string().optional(), + threadUrl: z.string().url().optional(), summary: z.string().optional(), subject: z.string().optional(), from: z.string().optional(), to: z.string().optional(), date: z.string().optional(), - latest_email: z.string(), + latest_email: z.string().optional(), past_summary: z.string().optional(), draft_response: z.string().optional(), response_mode: z.enum(['inline', 'assistant', 'both']).optional(), diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 2def4998..1514c598 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -17,6 +17,7 @@ import { UserMessageContent } from './message.js'; import { RowboatApiConfig } from './rowboat-account.js'; import { ZListToolkitsResponse } from './composio.js'; import { BrowserStateSchema } from './browser-control.js'; +import { EmailBlockSchema } from './blocks.js'; // ============================================================================ // Runtime Validation Schemas (Single Source of Truth) @@ -122,6 +123,15 @@ const ipcSchemas = { req: WorkspaceChangeEvent, res: z.null(), }, + 'gmail:getThread': { + req: z.object({ + threadId: z.string().min(1), + }), + res: z.object({ + thread: EmailBlockSchema.nullable(), + error: z.string().optional(), + }), + }, 'mcp:listTools': { req: z.object({ serverName: z.string(),