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