From 7854298eaecdd8dd45bd75f1d36d72a9d53d6eca Mon Sep 17 00:00:00 2001 From: pewdiepie-archdaemon Date: Tue, 30 Jun 2026 10:48:08 +0000 Subject: [PATCH] Write email replies into open composer --- src/agent_loop.py | 16 ++++++++++++++++ static/index.html | 6 +++--- static/js/chat.js | 13 +++++++++++-- static/js/chatStream.js | 14 ++++++++++++++ static/js/document.js | 20 ++++++++++++++++++-- static/sw.js | 2 +- 6 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/agent_loop.py b/src/agent_loop.py index 22e52aee8..e8bd12f82 100644 --- a/src/agent_loop.py +++ b/src/agent_loop.py @@ -1055,11 +1055,27 @@ def _turn_targets_active_document(intent: Dict[str, object], last_user: str, act """ if active_document is None: return False + raw_doc = getattr(active_document, "current_content", "") or "" + title_l = (getattr(active_document, "title", "") or "").strip().lower() + is_email_doc = ( + getattr(active_document, "language", None) == "email" + or title_l in {"new email", "new mail", "new message"} + or ("To:" in raw_doc[:400] and "Subject:" in raw_doc[:400] and "\n---\n" in raw_doc) + ) if "documents" in (intent.get("domains") or set()): return True text = str(last_user or "").strip().lower() if not text: return False + if is_email_doc and re.search( + r"\b(" + r"email|mail|reply|respond|response|draft|compose|send|" + r"tell them|tell her|tell him|say|write|make it say|" + r"japanese|japan|polite|formal|tone|style" + r")\b", + text, + ): + return True return bool(re.search( r"\b(" r"document|doc|draft|text|poem|story|essay|outline|letter|paragraph|" diff --git a/static/index.html b/static/index.html index d19fffa5f..0ba508d23 100644 --- a/static/index.html +++ b/static/index.html @@ -219,8 +219,8 @@ }, { once: true }); })(); - - + + @@ -2512,7 +2512,7 @@ - + diff --git a/static/js/chat.js b/static/js/chat.js index 09c22a997..542d5ec88 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -780,9 +780,15 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer } // Auto-save document editor content before sending so the AI sees latest text - const activeDocIdForSend = documentModule && typeof documentModule.getCurrentDocId === 'function' + const activeEmailComposerCtx = documentModule && typeof documentModule.getActiveEmailComposerContext === 'function' + ? documentModule.getActiveEmailComposerContext() + : null; + let activeDocIdForSend = documentModule && typeof documentModule.getCurrentDocId === 'function' ? documentModule.getCurrentDocId() : null; + if (!activeDocIdForSend && activeEmailComposerCtx?.docId) { + activeDocIdForSend = activeEmailComposerCtx.docId; + } if (documentModule && activeDocIdForSend) { try { await documentModule.saveDocument(); } catch(e) { console.warn('doc auto-save failed', e); } } @@ -826,7 +832,10 @@ import { wireArrowUpRecall, getLastUserMessageFromChatHistory } from './composer try { const getEmailCtx = window.__odysseusGetActiveEmailContext; const emCtx = typeof getEmailCtx === 'function' ? getEmailCtx() : null; - if (emCtx && emCtx.uid) { + if (activeEmailComposerCtx && activeEmailComposerCtx.sourceUid) { + fd.append('active_email_uid', String(activeEmailComposerCtx.sourceUid)); + fd.append('active_email_folder', String(activeEmailComposerCtx.sourceFolder || 'INBOX')); + } else if (emCtx && emCtx.uid) { fd.append('active_email_uid', String(emCtx.uid)); fd.append('active_email_folder', String(emCtx.folder || 'INBOX')); if (emCtx.account) fd.append('active_email_account', String(emCtx.account)); diff --git a/static/js/chatStream.js b/static/js/chatStream.js index fc62216ad..7f292de25 100644 --- a/static/js/chatStream.js +++ b/static/js/chatStream.js @@ -7,6 +7,7 @@ import Storage from './storage.js'; import themeModule from './theme.js'; import markdownModule from './markdown.js'; import sessionModule from './sessions.js'; +import documentModule from './document.js'; /** * Handle a ui_control SSE event — AI-driven UI manipulation. @@ -183,6 +184,19 @@ export function handleUIControl(uiData) { } } else if (uiEvent === 'open_email_reply' || uiData.ui_event === 'open_email_reply') { + try { + var existingDocId = documentModule && documentModule.findEmailDocId + ? documentModule.findEmailDocId(uiData.uid, uiData.folder || 'INBOX') + : null; + if (existingDocId && documentModule.replaceEmailReplyBody) { + if (documentModule.loadDocument) documentModule.loadDocument(existingDocId); + documentModule.replaceEmailReplyBody(existingDocId, uiData.body || '', { force: true }); + if (uiModule && uiModule.showToast) uiModule.showToast('Wrote reply into the open email'); + return; + } + } catch (e) { + console.warn('open_email_reply existing draft update failed:', e); + } import('./emailInbox.js').then(function(mod) { var fn = mod.openReplyDraft || (mod.default && mod.default.openReplyDraft); if (fn) fn(uiData.uid, uiData.folder || 'INBOX', uiData.mode || 'reply', uiData.body || ''); diff --git a/static/js/document.js b/static/js/document.js index a540b9bc0..7c8be5a68 100644 --- a/static/js/document.js +++ b/static/js/document.js @@ -6539,7 +6539,7 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js'; })); } - export async function replaceEmailReplyBody(docId, replyText) { + export async function replaceEmailReplyBody(docId, replyText, { force = false } = {}) { const doc = docs.get(docId); if (!doc) return; const fields = _parseEmailHeader(doc.content || ''); @@ -6550,7 +6550,7 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js'; ); const quote = quoteIdx >= 0 ? lines.slice(quoteIdx).join('\n') : ''; const ownText = _emailReplyOwnText(fields.body || ''); - if (ownText && !/^(\[AI reply draft will appear here\]|Drafting AI reply)/i.test(ownText)) { + if (!force && ownText && !/^(\[AI reply draft will appear here\]|Drafting AI reply)/i.test(ownText)) { if (uiModule) uiModule.showToast('AI reply ready, but draft was edited'); return; } @@ -10430,6 +10430,21 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js'; return activeDocId; } + export function getActiveEmailComposerContext() { + if (!activeDocId) return null; + const doc = docs.get(activeDocId); + if (!doc || doc.language !== 'email') return null; + const fields = _parseEmailHeader(doc.content || ''); + return { + docId: activeDocId, + sourceUid: fields.sourceUid || '', + sourceFolder: fields.sourceFolder || 'INBOX', + inReplyTo: fields.inReplyTo || '', + to: fields.to || '', + subject: fields.subject || '', + }; + } + /** Find an open email tab by source UID + folder. Returns docId or null. */ export function findEmailDocId(uid, folder) { if (uid == null) return null; @@ -10472,6 +10487,7 @@ const documentModule = { exitDiffMode, isDiffModeActive, getCurrentDocId, + getActiveEmailComposerContext, findEmailDocId, getSelectionContext, clearSelection, diff --git a/static/sw.js b/static/sw.js index 071e6a3bc..16b76265c 100644 --- a/static/sw.js +++ b/static/sw.js @@ -7,7 +7,7 @@ // - Other static assets (images/fonts/libs): cache-first with bg refresh. // - API / non-GET: never cached. // Bump CACHE_NAME whenever the precache list or SW logic changes. -const CACHE_NAME = 'odysseus-v329'; +const CACHE_NAME = 'odysseus-v330'; // Core shell precached on install so repeat opens are instant without any // network wait. Keep this list in sync with the