diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts
index 7808063d..5bb75384 100644
--- a/apps/x/apps/main/src/ipc.ts
+++ b/apps/x/apps/main/src/ipc.ts
@@ -47,7 +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, listRecentThreadIds, listInboxPage, saveMessageBodyHeight } from '@x/core/dist/knowledge/sync_gmail.js';
+import { fetchThreadSnapshot, listRecentThreadIds, listInboxPage, saveMessageBodyHeight, sendThreadReply } 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';
@@ -510,6 +510,9 @@ export function setupIpcHandlers() {
limit: args.limit,
});
},
+ 'gmail:sendReply': async (_event, args) => {
+ return sendThreadReply(args);
+ },
'gmail:saveMessageHeight': async (_event, args) => {
saveMessageBodyHeight(args.threadId, args.messageId, args.height);
return {};
diff --git a/apps/x/apps/renderer/src/components/email-view.tsx b/apps/x/apps/renderer/src/components/email-view.tsx
index d5229df8..2e3a4592 100644
--- a/apps/x/apps/renderer/src/components/email-view.tsx
+++ b/apps/x/apps/renderer/src/components/email-view.tsx
@@ -466,39 +466,55 @@ function ComposeBox({
if (editor && sel) editor.chain().focus().setTextSelection(sel).run()
}
+ const [sending, setSending] = useState(false)
const sendInGmail = async () => {
- if (!editor) {
- window.open(thread.threadUrl, '_blank')
- return
- }
+ if (!editor || sending) return
const html = editor.getHTML()
const text = editor.getText().trim()
-
- let copied = false
- if (text) {
- try {
- if (typeof ClipboardItem !== 'undefined' && navigator.clipboard?.write) {
- await navigator.clipboard.write([
- new ClipboardItem({
- 'text/html': new Blob([html], { type: 'text/html' }),
- 'text/plain': new Blob([text], { type: 'text/plain' }),
- }),
- ])
- copied = true
- } else if (navigator.clipboard?.writeText) {
- await navigator.clipboard.writeText(text)
- copied = true
- }
- } catch (err) {
- console.warn('[Gmail] clipboard write failed:', err)
- }
+ if (!text) {
+ toast('Draft is empty.', 'error')
+ return
}
- window.open(thread.threadUrl, '_blank')
- if (copied) {
- toast('Draft copied — open the reply in Gmail and paste.', 'info')
- } else if (text) {
- toast('Could not copy draft. Open Gmail and paste manually.', 'error')
+ const recipient = mode === 'reply' ? extractAddress(latest?.from) : ''
+ if (!recipient) {
+ toast('No recipient found for this thread.', 'error')
+ return
+ }
+
+ const rawSubject = thread.subject || ''
+ const subject = mode === 'reply'
+ ? (/^re:/i.test(rawSubject) ? rawSubject : `Re: ${rawSubject}`.trim())
+ : (/^fwd:/i.test(rawSubject) ? rawSubject : `Fwd: ${rawSubject}`.trim())
+
+ // Build References chain from all known message ids (newest last).
+ const messageIds = thread.messages
+ .map((m) => m.messageIdHeader)
+ .filter((v): v is string => Boolean(v))
+ const references = messageIds.join(' ')
+ const inReplyTo = latest?.messageIdHeader
+
+ setSending(true)
+ try {
+ const result = await window.ipc.invoke('gmail:sendReply', {
+ threadId: thread.threadId,
+ to: recipient,
+ subject,
+ bodyHtml: html,
+ bodyText: text,
+ inReplyTo,
+ references: references || undefined,
+ })
+ if (result.error) {
+ toast(`Send failed: ${result.error}`, 'error')
+ return
+ }
+ toast('Sent.', 'success')
+ onClose()
+ } catch (err) {
+ toast(`Send failed: ${err instanceof Error ? err.message : String(err)}`, 'error')
+ } finally {
+ setSending(false)
}
}
@@ -577,10 +593,11 @@ function ComposeBox({
type="button"
className="gmail-send-button"
onClick={() => { void sendInGmail() }}
- title="Copy draft and open this thread in Gmail"
+ disabled={sending}
+ title="Send this reply via Gmail"
>
-
- Send
+ {sending ? : }
+ {sending ? 'Sending…' : 'Send'}