diff --git a/.dev.vars.example b/.dev.vars.example
index f0c881b3..730d8b5d 100644
--- a/.dev.vars.example
+++ b/.dev.vars.example
@@ -1,3 +1,19 @@
# Cloudflare Access (required in production). TEAM_DOMAIN may be the base Access URL or the full /cdn-cgi/access/certs URL.
POLICY_AUD=your-access-policy-audience-tag
TEAM_DOMAIN=https://your-team.cloudflareaccess.com
+# Optional Google Drive import. Share selected files with this service account.
+GOOGLE_SERVICE_ACCOUNT_EMAIL=service-account@example.iam.gserviceaccount.com
+GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----\\n"
+# Optional Microsoft Graph app-only OneDrive import.
+MICROSOFT_TENANT_ID=your-tenant-id
+MICROSOFT_CLIENT_ID=your-app-client-id
+MICROSOFT_CLIENT_SECRET=your-app-client-secret
+ONEDRIVE_USER_ID=person@example.com
+# Microsoft OAuth (required for the Outlook connector)
+MICROSOFT_TENANT_ID=common
+MICROSOFT_CLIENT_ID=
+MICROSOFT_CLIENT_SECRET=
+MICROSOFT_REDIRECT_URI=http://localhost:5173/auth/microsoft/callback
+APP_ORIGIN=http://localhost:5173
+# Generate a separate random value for production token encryption.
+TOKEN_ENCRYPTION_KEY=
diff --git a/README.md b/README.md
index e1e2eb72..065b1424 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,7 @@ https://github.com/cloudflare/agentic-inbox/issues/4#issuecomment-4269118513
- **Built-in AI agent** — Side panel with 9 email tools for reading, searching, drafting, and sending
- **Auto-draft on new email** — Agent automatically reads inbound emails and generates draft replies, always requiring explicit confirmation before sending
- **Configurable and persistent** — Custom system prompts per mailbox, persistent chat history, streaming markdown responses, and tool call visibility
+- **Evidence-backed memory** — Markdown/file memory with keyword and optional semantic retrieval, source provenance, confirmed facts, Google Drive imports, and operator-visible drafting context
## Stack
@@ -62,6 +63,55 @@ npm run dev
1. Set your domain in `wrangler.jsonc`
2. Create an R2 bucket named `agentic-inbox`: `wrangler r2 bucket create agentic-inbox`
+### Optional cloud-drive memory imports
+
+Configure `GOOGLE_SERVICE_ACCOUNT_EMAIL` and `GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY` as Worker secrets, then share selected Drive files with that service account. In Memory, choose **Google Drive** and paste the selected file IDs. Imports are mailbox-scoped, deduplicated by Drive file ID, converted to Markdown-compatible text, chunked, and indexed through the same pipeline as uploaded files.
+
+For Microsoft OneDrive, configure `MICROSOFT_TENANT_ID`, `MICROSOFT_CLIENT_ID`, `MICROSOFT_CLIENT_SECRET`, and `ONEDRIVE_USER_ID` as Worker secrets. Grant the Microsoft Entra app the least-privilege Microsoft Graph application permission needed to read the configured user drive, then choose **Microsoft OneDrive** in Memory and paste selected file IDs. OneDrive imports use the same normalization, chunking, provenance, deduplication, and fact-review pipeline.
+
+### Outlook + Power Automate workflows
+
+The recommended no-Premium workflow uses standard Outlook email actions and the existing Cloudflare Email Routing receiver:
+
+```
+Outlook: When a new email arrives (V3)
+ -> Forward an email (V2) to the Agentic Inbox address
+Cloudflare Email Routing
+ -> Agentic Inbox Worker receives and stores the message
+ -> EmailAgent + Workers AI create an Agentic Inbox draft
+ -> Operator reviews and sends from Agentic Inbox
+```
+
+This path does not require Microsoft OAuth tokens in Cloudflare, OneDrive, or the Premium HTTP connector. Configure the forwarding destination as a mailbox already created in Agentic Inbox, for example `ai@example.com`, and ensure Cloudflare Email Routing forwards that address to this Worker.
+
+#### Optional Premium HTTP bridge
+
+The separately deployed bridge is available at:
+
+```text
+https://outlook-ai-bridge.shorlol.workers.dev/integrations/power-automate/outlook
+```
+
+It accepts an authenticated JSON POST and returns parsed `classification`, `priority`, `draftSubject`, `draftBodyHtml`, and `confidence` fields. It is intended for Power Automate's Premium **HTTP** action and does not persist messages or send mail:
+
+```text
+Outlook trigger -> Premium HTTP POST -> Workers AI JSON -> Outlook Create draft
+```
+
+Configure its secret with `npx wrangler secret put BRIDGE_SECRET --name outlook-ai-bridge`. Do not use the Office 365 Users or Office 365 Outlook **Send an HTTP request** actions as substitutes: those actions are Microsoft Graph-specific, not general external HTTP clients.
+
+#### Outlook Drafts without Premium HTTP
+
+Putting the AI result into the Outlook Drafts folder without Premium HTTP requires a second email relay that is not yet part of the Worker:
+
+```text
+Outlook -> standard Forward action -> Agentic Inbox + Workers AI
+ -> Worker sends a structured [Agentic Draft] result email
+ -> Outlook trigger -> Parse JSON -> Draft an email message
+```
+
+The required backend addition is a Worker result-email step containing the original recipient, generated subject, and generated HTML body. The second standard Outlook flow can then create the real Outlook draft and optionally delete the temporary result email. Until that relay is implemented, AI drafts are available in Agentic Inbox only.
+
### Deploy
```bash
diff --git a/add-in/manifest.xml b/add-in/manifest.xml
new file mode 100644
index 00000000..411f508a
--- /dev/null
+++ b/add-in/manifest.xml
@@ -0,0 +1,132 @@
+
+
+
+ c4ae0c81-89f5-40d3-b8f8-e1eaaef82f66
+ 1.0.0.0
+ Cloudflare
+ en-US
+
+
+
+
+
+
+ https://YOUR-ADDIN-HOST.example.com
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ReadWriteItem
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/add-in/taskpane.css b/add-in/taskpane.css
new file mode 100644
index 00000000..5f7fda09
--- /dev/null
+++ b/add-in/taskpane.css
@@ -0,0 +1,211 @@
+:root {
+ color-scheme: light;
+ font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+ --bg: #f3f7fb;
+ --panel: #ffffff;
+ --panel-border: #d8e2ee;
+ --text: #16324a;
+ --muted: #5a7188;
+ --accent: #0f6cbd;
+ --accent-strong: #0c5aa0;
+ --accent-soft: #e8f2fc;
+ --warning: #8a4b0f;
+ --warning-soft: #fff5e8;
+ --success: #0f6a46;
+ --success-soft: #e8f6f0;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+ min-height: 100%;
+ background: radial-gradient(circle at top, #ffffff 0%, var(--bg) 58%);
+ color: var(--text);
+}
+
+body {
+ font: 14px/1.5 "Segoe UI", "Helvetica Neue", Arial, sans-serif;
+}
+
+.shell {
+ min-height: 100vh;
+ padding: 16px;
+}
+
+.panel {
+ max-width: 480px;
+ margin: 0 auto;
+ padding: 18px;
+ background: var(--panel);
+ border: 1px solid var(--panel-border);
+ border-radius: 18px;
+ box-shadow: 0 18px 38px rgba(15, 49, 85, 0.08);
+}
+
+.eyebrow-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.eyebrow {
+ margin: 0;
+ color: var(--muted);
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.status-badge {
+ display: inline-flex;
+ align-items: center;
+ min-height: 28px;
+ padding: 0 10px;
+ border-radius: 999px;
+ font-size: 12px;
+ font-weight: 700;
+}
+
+.status-pending {
+ color: var(--warning);
+ background: var(--warning-soft);
+}
+
+.status-ready {
+ color: var(--success);
+ background: var(--success-soft);
+}
+
+.status-warning {
+ color: var(--warning);
+ background: var(--warning-soft);
+}
+
+h1 {
+ margin: 10px 0 4px;
+ font-size: 24px;
+ line-height: 1.2;
+}
+
+.lede {
+ margin: 0 0 18px;
+ color: var(--muted);
+}
+
+.context-list {
+ margin: 0;
+ padding: 0;
+}
+
+.context-row {
+ display: grid;
+ grid-template-columns: 72px 1fr;
+ gap: 10px;
+ padding: 11px 0;
+ border-top: 1px solid #edf2f7;
+}
+
+.context-row:first-child {
+ border-top: 0;
+}
+
+.context-row dt {
+ color: var(--muted);
+ font-weight: 600;
+}
+
+.context-row dd {
+ margin: 0;
+ font-weight: 600;
+ overflow-wrap: anywhere;
+}
+
+.actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-top: 18px;
+}
+
+.primary-action,
+.secondary-action {
+ appearance: none;
+ border-radius: 12px;
+ font: inherit;
+ font-weight: 700;
+ text-decoration: none;
+ cursor: pointer;
+ transition: transform 120ms ease, background-color 120ms ease, border-color 120ms ease;
+}
+
+.primary-action {
+ padding: 11px 14px;
+ color: #ffffff;
+ background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%);
+ border: 0;
+}
+
+.secondary-action {
+ padding: 10px 14px;
+ color: var(--text);
+ background: var(--accent-soft);
+ border: 1px solid #cfe1f5;
+}
+
+.primary-action:hover,
+.secondary-action:hover {
+ transform: translateY(-1px);
+}
+
+.destination {
+ margin-top: 16px;
+ padding: 12px;
+ border-radius: 14px;
+ background: #f7fafc;
+ border: 1px solid #e6eef6;
+}
+
+.destination-label {
+ display: block;
+ margin-bottom: 6px;
+ color: var(--muted);
+ font-size: 12px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+code {
+ display: block;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ font-size: 12px;
+ overflow-wrap: anywhere;
+}
+
+.note {
+ margin: 14px 0 0;
+ color: var(--muted);
+ font-size: 12px;
+}
+
+@media (max-width: 480px) {
+ .shell {
+ padding: 12px;
+ }
+
+ .panel {
+ padding: 16px;
+ border-radius: 16px;
+ }
+
+ .context-row {
+ grid-template-columns: 1fr;
+ gap: 4px;
+ }
+}
diff --git a/add-in/taskpane.html b/add-in/taskpane.html
new file mode 100644
index 00000000..a1b5b1a0
--- /dev/null
+++ b/add-in/taskpane.html
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+ Agentic Inbox for Outlook
+
+
+
+
+
+
+
+
+
Outlook context
+
Loading
+
+ Agentic Inbox
+ Open the hosted app with the current message context when Outlook makes it available.
+
+
+
+
- Mode
+ - Detecting
+
+
+
- Subject
+ - Waiting for Outlook
+
+
+
- Sender
+ - Waiting for Outlook
+
+
+
+
+
+
+ Hosted app
+ Resolving…
+
+
+ The link uses the current origin by default. Override it with appRoot and appPath query parameters if this pane is hosted separately.
+
+
+
+
diff --git a/add-in/taskpane.js b/add-in/taskpane.js
new file mode 100644
index 00000000..fa8f09a2
--- /dev/null
+++ b/add-in/taskpane.js
@@ -0,0 +1,273 @@
+(function () {
+ "use strict";
+
+ var SUBJECT_FALLBACK = "No subject available";
+ var SENDER_FALLBACK = "Sender unavailable";
+ var MODE_FALLBACK = "General";
+
+ var elements = {};
+
+ document.addEventListener("DOMContentLoaded", function () {
+ elements.statusBadge = document.getElementById("status-badge");
+ elements.mode = document.getElementById("context-mode");
+ elements.subject = document.getElementById("context-subject");
+ elements.sender = document.getElementById("context-sender");
+ elements.destination = document.getElementById("destination-url");
+ elements.openLink = document.getElementById("open-app-link");
+ elements.refreshButton = document.getElementById("refresh-button");
+ elements.contextNote = document.getElementById("context-note");
+
+ elements.refreshButton.addEventListener("click", function () {
+ refreshContext();
+ });
+
+ elements.openLink.addEventListener("click", function (event) {
+ var targetUrl = elements.openLink.getAttribute("href");
+ if (!targetUrl) {
+ return;
+ }
+
+ if (window.Office && Office.context && Office.context.ui && typeof Office.context.ui.openBrowserWindow === "function") {
+ event.preventDefault();
+ Office.context.ui.openBrowserWindow(targetUrl);
+ }
+ });
+
+ setStatus("Loading Outlook", "pending");
+ renderContext({
+ modeLabel: inferHostContextLabel(),
+ subject: SUBJECT_FALLBACK,
+ senderLabel: SENDER_FALLBACK
+ });
+
+ initializeOffice();
+ });
+
+ function initializeOffice() {
+ if (!window.Office || typeof Office.onReady !== "function") {
+ setStatus("Office.js unavailable", "warning");
+ elements.contextNote.textContent = "Office.js did not initialize. Open this pane from Outlook to load live item context.";
+ refreshContext();
+ return;
+ }
+
+ Office.onReady()
+ .then(function (info) {
+ if (!info || info.host !== Office.HostType.Outlook) {
+ setStatus("Not running in Outlook", "warning");
+ elements.contextNote.textContent = "This shell is designed for Outlook. Context fields will stay in fallback mode outside that host.";
+ refreshContext();
+ return;
+ }
+
+ setStatus("Connected to Outlook", "ready");
+ registerItemChangedHandler();
+ refreshContext();
+ })
+ .catch(function () {
+ setStatus("Outlook unavailable", "warning");
+ elements.contextNote.textContent = "Outlook did not finish initializing, so only the hosted-app link is available.";
+ refreshContext();
+ });
+ }
+
+ function registerItemChangedHandler() {
+ if (!Office.context || !Office.context.mailbox || typeof Office.context.mailbox.addHandlerAsync !== "function") {
+ return;
+ }
+
+ if (!Office.EventType || !Office.EventType.ItemChanged) {
+ return;
+ }
+
+ Office.context.mailbox.addHandlerAsync(Office.EventType.ItemChanged, function () {
+ refreshContext();
+ });
+ }
+
+ function refreshContext() {
+ var item = getCurrentItem();
+ var context = {
+ modeLabel: inferModeLabel(item),
+ subject: readSubject(item),
+ sender: readSender(item)
+ };
+ context.senderLabel = formatSender(context.sender);
+ context.appUrl = buildHostedAppUrl(context);
+ renderContext(context);
+ }
+
+ function getCurrentItem() {
+ if (!window.Office || !Office.context || !Office.context.mailbox) {
+ return null;
+ }
+
+ return Office.context.mailbox.item || null;
+ }
+
+ function inferModeLabel(item) {
+ var fromQuery = inferHostContextLabel();
+ if (fromQuery !== MODE_FALLBACK) {
+ return fromQuery;
+ }
+
+ if (!item) {
+ return MODE_FALLBACK;
+ }
+
+ if (typeof item.itemId === "string" && item.itemId) {
+ return "Message read";
+ }
+
+ return "Message compose";
+ }
+
+ function inferHostContextLabel() {
+ var params = new URLSearchParams(window.location.search);
+ var hostContext = (params.get("hostContext") || "").toLowerCase();
+
+ if (hostContext === "read") {
+ return "Message read";
+ }
+
+ if (hostContext === "compose") {
+ return "Message compose";
+ }
+
+ return MODE_FALLBACK;
+ }
+
+ function readSubject(item) {
+ if (!item || typeof item.subject !== "string") {
+ return SUBJECT_FALLBACK;
+ }
+
+ var subject = item.subject.trim();
+ return subject || SUBJECT_FALLBACK;
+ }
+
+ function readSender(item) {
+ if (!item) {
+ return null;
+ }
+
+ var sender = item.from || item.sender || item.organizer || null;
+ if (sender && (sender.displayName || sender.emailAddress)) {
+ return sender;
+ }
+
+ return null;
+ }
+
+ function formatSender(sender) {
+ if (!sender) {
+ return SENDER_FALLBACK;
+ }
+
+ var displayName = normalizeText(sender.displayName);
+ var emailAddress = normalizeText(sender.emailAddress);
+
+ if (displayName && emailAddress) {
+ return displayName + " <" + emailAddress + ">";
+ }
+
+ return displayName || emailAddress || SENDER_FALLBACK;
+ }
+
+ function buildHostedAppUrl(context) {
+ var config = readConfig();
+ var baseUrl = resolveBaseUrl(config.appRoot);
+ var url = new URL(normalizeAppPath(config.appPath), baseUrl);
+
+ url.searchParams.set("source", "outlook-addin");
+ url.searchParams.set("hostContext", (new URLSearchParams(window.location.search).get("hostContext") || "general").toLowerCase());
+
+ if (context.subject && context.subject !== SUBJECT_FALLBACK) {
+ url.searchParams.set("subject", context.subject);
+ }
+
+ if (context.sender) {
+ if (normalizeText(context.sender.displayName)) {
+ url.searchParams.set("senderName", normalizeText(context.sender.displayName));
+ }
+
+ if (normalizeText(context.sender.emailAddress)) {
+ url.searchParams.set("senderEmail", normalizeText(context.sender.emailAddress));
+ }
+ }
+
+ return url;
+ }
+
+ function readConfig() {
+ var params = new URLSearchParams(window.location.search);
+ return {
+ appRoot: params.get("appRoot") || readMeta("addin-app-root", "current"),
+ appPath: params.get("appPath") || readMeta("addin-app-path", "/")
+ };
+ }
+
+ function readMeta(name, fallback) {
+ var meta = document.querySelector('meta[name="' + name + '"]');
+ if (!meta) {
+ return fallback;
+ }
+
+ return meta.getAttribute("content") || fallback;
+ }
+
+ function resolveBaseUrl(appRoot) {
+ if (!appRoot || appRoot === "current") {
+ return new URL(window.location.origin + "/");
+ }
+
+ try {
+ var resolved = new URL(appRoot, window.location.origin + "/");
+ return new URL(ensureTrailingSlash(resolved.href));
+ } catch (_error) {
+ return new URL(window.location.origin + "/");
+ }
+ }
+
+ function ensureTrailingSlash(value) {
+ return value.endsWith("/") ? value : value + "/";
+ }
+
+ function normalizeText(value) {
+ if (typeof value !== "string") {
+ return "";
+ }
+
+ return value.trim();
+ }
+
+ function normalizeAppPath(value) {
+ var appPath = normalizeText(value);
+ if (!appPath || appPath === "/") {
+ return "";
+ }
+
+ if (/^https?:\/\//i.test(appPath)) {
+ return appPath;
+ }
+
+ return appPath.replace(/^\/+/, "");
+ }
+
+ function renderContext(context) {
+ elements.mode.textContent = context.modeLabel || MODE_FALLBACK;
+ elements.subject.textContent = context.subject || SUBJECT_FALLBACK;
+ elements.sender.textContent = context.senderLabel || SENDER_FALLBACK;
+
+ if (context.appUrl) {
+ var href = context.appUrl.toString();
+ elements.openLink.setAttribute("href", href);
+ elements.destination.textContent = href;
+ }
+ }
+
+ function setStatus(text, tone) {
+ elements.statusBadge.textContent = text;
+ elements.statusBadge.className = "status-badge status-" + tone;
+ }
+})();
diff --git a/app/CLAUDE.md b/app/CLAUDE.md
new file mode 100644
index 00000000..adfdcb11
--- /dev/null
+++ b/app/CLAUDE.md
@@ -0,0 +1,7 @@
+
+# Recent Activity
+
+
+
+*No recent activity*
+
\ No newline at end of file
diff --git a/app/components/CLAUDE.md b/app/components/CLAUDE.md
new file mode 100644
index 00000000..adfdcb11
--- /dev/null
+++ b/app/components/CLAUDE.md
@@ -0,0 +1,7 @@
+
+# Recent Activity
+
+
+
+*No recent activity*
+
\ No newline at end of file
diff --git a/app/components/ComposeAIBar.tsx b/app/components/ComposeAIBar.tsx
new file mode 100644
index 00000000..6630c4dd
--- /dev/null
+++ b/app/components/ComposeAIBar.tsx
@@ -0,0 +1,156 @@
+// Copyright (c) 2026 Cloudflare, Inc.
+// Licensed under the Apache 2.0 license found in the LICENSE file or at:
+// https://opensource.org/licenses/Apache-2.0
+
+import { Badge, Button, Input, Loader, Popover } from "@cloudflare/kumo";
+import { NoteIcon, SparkleIcon } from "@phosphor-icons/react";
+import { useState } from "react";
+import { textToHtml } from "~/lib/utils";
+import { useTemplateList } from "~/queries/templates";
+import { useMemoryContext } from "~/queries/memory";
+import api from "~/services/api";
+
+interface ComposeAIBarProps {
+ mailboxId: string;
+ body: string;
+ subject?: string;
+ to?: string;
+ onRewrite: (newBody: string) => void;
+}
+
+const QUICK_ACTIONS = [
+ { action: "polish", label: "Polish" },
+ { action: "formalize", label: "Formalize" },
+ { action: "friendly", label: "Friendly" },
+ { action: "shorten", label: "Shorten" },
+] as const;
+
+/** Whether the current body is empty or just an empty paragraph placeholder. */
+function isBodyEmpty(body: string): boolean {
+ return !body || body.trim() === "" || body.trim() === "
";
+}
+
+/** AI compose bar with custom instruction input, quick-action buttons, and a template inserter. */
+export function ComposeAIBar({ mailboxId, body, subject = "", to = "", onRewrite }: ComposeAIBarProps) {
+ const [instruction, setInstruction] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [activeAction, setActiveAction] = useState(null);
+ const [isTemplatesOpen, setIsTemplatesOpen] = useState(false);
+ const { data: templates = [] } = useTemplateList(mailboxId);
+ const contextQuery = [subject, to, body.replace(/<[^>]*>/g, " ")].join(" ").trim();
+ const { data: context, isFetching: isContextLoading } = useMemoryContext(mailboxId, contextQuery);
+
+ const handleRewrite = async (action: string, customInstruction?: string) => {
+ if (!body.trim() || isLoading) return;
+ setIsLoading(true);
+ setActiveAction(action);
+ try {
+ const result = await api.rewriteEmailBody(
+ mailboxId,
+ body,
+ action,
+ customInstruction,
+ );
+ onRewrite(result.body);
+ if (action === "custom") setInstruction("");
+ } finally {
+ setIsLoading(false);
+ setActiveAction(null);
+ }
+ };
+
+ const handleCustomSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!instruction.trim()) return;
+ handleRewrite("custom", instruction.trim());
+ };
+
+ const handleInsertTemplate = (templateBody: string) => {
+ const html = textToHtml(templateBody);
+ onRewrite(isBodyEmpty(body) ? html : `${body}${html}`);
+ setIsTemplatesOpen(false);
+ };
+
+ return (
+
+
+
+
+
+ }
+ className="shrink-0"
+ >
+ Templates
+
+
+
+ {templates.length === 0 ? (
+ No templates yet.
+ ) : (
+
+ {templates.map((template) => (
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+
+ Memory context
+ {isContextLoading && }
+
+ {context?.warnings.map((warning) => {warning}
)}
+ {context?.sources.length ? context.sources.map((source) => (
+
+
+ {source.citation}
+ {source.source}
+
+
{source.excerpt}
+
+ )) : No relevant memory found yet.
}
+
+
+
+ );
+}
diff --git a/app/components/ComposePanel.tsx b/app/components/ComposePanel.tsx
index c7196a2b..ff66ea74 100644
--- a/app/components/ComposePanel.tsx
+++ b/app/components/ComposePanel.tsx
@@ -3,10 +3,11 @@
// https://opensource.org/licenses/Apache-2.0
import { Banner, Button, Input } from "@cloudflare/kumo";
-import { FloppyDiskIcon, PaperPlaneTiltIcon, XIcon } from "@phosphor-icons/react";
+import { ArrowCounterClockwiseIcon, FloppyDiskIcon, PaperPlaneTiltIcon, XIcon } from "@phosphor-icons/react";
import { useParams } from "react-router";
import { useComposeForm } from "~/hooks/useComposeForm";
import RichTextEditor from "./RichTextEditor";
+import { ComposeAIBar } from "./ComposeAIBar";
export default function ComposePanel() {
const { mailboxId, folder } = useParams<{
@@ -35,6 +36,11 @@ export default function ComposePanel() {
handleSend,
closeCompose,
closePanel,
+ applyAiRewrite,
+ undoAiEdit,
+ redoAiEdit,
+ canUndoAi,
+ canRedoAi,
} = useComposeForm(mailboxId, folder);
return (
@@ -145,7 +151,40 @@ export default function ComposePanel() {
value={body}
onChange={setBody}
/>
+ {mailboxId && (
+
+ )}
+ {(canUndoAi || canRedoAi) && (
+
+ }
+ disabled={!canUndoAi}
+ onClick={undoAiEdit}
+ >
+ Undo AI
+
+ }
+ disabled={!canRedoAi}
+ onClick={redoAiEdit}
+ >
+ Redo AI
+
+
+ )}
{/* Footer actions */}
diff --git a/app/components/MCPPanel.tsx b/app/components/MCPPanel.tsx
index 7caf8586..ce155d44 100644
--- a/app/components/MCPPanel.tsx
+++ b/app/components/MCPPanel.tsx
@@ -51,6 +51,7 @@ const TOOLS = [
{ name: "get_email", desc: "Read a full email with body" },
{ name: "get_thread", desc: "Load a conversation thread" },
{ name: "search_emails", desc: "Search emails by query" },
+ { name: "search_memory", desc: "Search stored memory notes" },
{ name: "draft_reply", desc: "Draft a reply to an email" },
{ name: "send_reply", desc: "Send a reply" },
{ name: "send_email", desc: "Send a new email" },
diff --git a/app/components/Sidebar.tsx b/app/components/Sidebar.tsx
index ff6ed5c5..5956bfdb 100644
--- a/app/components/Sidebar.tsx
+++ b/app/components/Sidebar.tsx
@@ -5,14 +5,19 @@
import { Badge, Button, Dialog, Input, Tooltip } from "@cloudflare/kumo";
import {
ArchiveIcon,
+ BrainIcon,
+ CalendarDotsIcon,
CaretLeftIcon,
FileIcon,
FolderIcon,
+ LightningIcon,
+ NoteIcon,
PaperPlaneTiltIcon,
PencilSimpleIcon,
PlusIcon,
TrashIcon,
TrayIcon,
+ UsersIcon,
} from "@phosphor-icons/react";
import { useMemo, useState } from "react";
import { NavLink, useNavigate, useParams } from "react-router";
@@ -159,6 +164,13 @@ export default function Sidebar() {
{/* Navigation */}