diff --git a/apps/x/apps/renderer/src/App.css b/apps/x/apps/renderer/src/App.css index 5c1eabb2..a1a937d0 100644 --- a/apps/x/apps/renderer/src/App.css +++ b/apps/x/apps/renderer/src/App.css @@ -58,6 +58,492 @@ background-image: radial-gradient(circle, oklch(0.7 0 0 / 0.06) 1px, transparent 1px); } +.gmail-shell { + display: flex; + height: 100%; + min-height: 0; + width: 100%; + overflow: hidden; + background: #f6f8fc; + color: #202124; + font-family: "Google Sans", Roboto, Arial, sans-serif; +} + +.gmail-left-nav { + width: 236px; + flex-shrink: 0; + padding: 16px 12px; + background: #f6f8fc; + border-right: 1px solid #e4e7ee; +} + +.gmail-compose-main { + display: flex; + align-items: center; + justify-content: center; + min-width: 132px; + height: 56px; + margin-bottom: 18px; + border: none; + border-radius: 16px; + background: #c2e7ff; + color: #001d35; + font-size: 14px; + font-weight: 500; + box-shadow: 0 1px 2px rgba(60, 64, 67, 0.3); + cursor: pointer; +} + +.gmail-nav-item { + display: flex; + align-items: center; + gap: 14px; + height: 32px; + padding: 0 14px; + border-radius: 0 16px 16px 0; + color: #444746; + font-size: 14px; +} + +.gmail-nav-item-active { + background: #d3e3fd; + color: #001d35; + font-weight: 700; +} + +.gmail-main { + display: flex; + min-width: 0; + min-height: 0; + flex: 1; + flex-direction: column; + padding: 8px 12px 12px 0; +} + +.gmail-topbar { + display: flex; + align-items: center; + gap: 12px; + height: 56px; + padding: 0 12px; +} + +.gmail-search { + display: flex; + align-items: center; + gap: 12px; + width: min(720px, 100%); + height: 46px; + padding: 0 16px; + border-radius: 24px; + background: #eaf1fb; + color: #5f6368; +} + +.gmail-search input { + flex: 1; + border: none; + outline: none; + background: transparent; + color: #202124; + font-size: 15px; +} + +.gmail-icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border: none; + border-radius: 50%; + background: transparent; + color: #5f6368; + cursor: pointer; +} + +.gmail-icon-button:hover { + background: rgba(60, 64, 67, 0.08); +} + +.gmail-layout { + display: grid; + grid-template-columns: minmax(420px, 0.45fr) minmax(480px, 0.55fr); + min-height: 0; + flex: 1; + overflow: hidden; + border-radius: 16px; + background: #fff; + border: 1px solid #e4e7ee; +} + +.gmail-list { + min-width: 0; + min-height: 0; + overflow: auto; + border-right: 1px solid #e4e7ee; +} + +.gmail-list-header { + position: sticky; + top: 0; + z-index: 1; + display: flex; + justify-content: space-between; + height: 44px; + padding: 0 16px; + align-items: center; + background: #fff; + border-bottom: 1px solid #e8eaed; + color: #5f6368; + font-size: 12px; +} + +.gmail-row { + display: grid; + grid-template-columns: 28px 28px minmax(132px, 0.28fr) minmax(0, 1fr) 72px; + align-items: center; + width: 100%; + min-height: 44px; + padding: 0 12px; + border: none; + border-bottom: 1px solid #f1f3f4; + background: #fff; + color: #202124; + text-align: left; + cursor: pointer; + font-family: inherit; +} + +.gmail-row:hover, +.gmail-row-selected { + box-shadow: inset 3px 0 0 #1a73e8; + background: #f2f6fc; +} + +.gmail-row-check { + width: 14px; + height: 14px; + border: 2px solid #bdc1c6; + border-radius: 2px; +} + +.gmail-row-star { + color: #bdc1c6; + line-height: 0; +} + +.gmail-row-sender, +.gmail-row-content strong, +.gmail-row-date { + font-size: 13px; + font-weight: 700; +} + +.gmail-row-sender, +.gmail-row-content, +.gmail-row-content span { + min-width: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.gmail-row-content { + display: flex; + gap: 6px; + color: #5f6368; + font-size: 13px; +} + +.gmail-row-content strong { + flex-shrink: 0; + color: #202124; +} + +.gmail-row-date { + justify-self: end; + color: #5f6368; + white-space: nowrap; +} + +.gmail-detail { + display: flex; + min-width: 0; + min-height: 0; + flex-direction: column; + background: #fff; +} + +.gmail-detail-toolbar { + display: flex; + align-items: center; + gap: 4px; + height: 44px; + padding: 0 12px; + border-bottom: 1px solid #e8eaed; +} + +.gmail-back-button { + display: none; +} + +.gmail-thread-scroll { + min-height: 0; + overflow: auto; + padding: 24px 32px 40px; +} + +.gmail-thread-subject-row { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 18px; +} + +.gmail-thread-subject-row h1 { + margin: 0; + color: #202124; + font-size: 22px; + font-weight: 400; + line-height: 1.35; +} + +.gmail-thread-subject-row span { + border-radius: 4px; + background: #e8eaed; + padding: 2px 6px; + color: #5f6368; + font-size: 12px; +} + +.gmail-message-stack { + display: flex; + flex-direction: column; + gap: 12px; +} + +.gmail-message { + display: grid; + grid-template-columns: 40px minmax(0, 1fr); + gap: 12px; + padding: 12px 0; +} + +.gmail-message-avatar { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 50%; + color: #fff; + font-weight: 500; +} + +.gmail-message-main { + min-width: 0; +} + +.gmail-message-meta { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; +} + +.gmail-message-from { + display: flex; + align-items: baseline; + gap: 8px; + min-width: 0; +} + +.gmail-message-from strong, +.gmail-message-from span { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.gmail-message-from strong { + font-size: 14px; +} + +.gmail-message-from span, +.gmail-message-to, +.gmail-message-date { + color: #5f6368; + font-size: 12px; +} + +.gmail-message-date { + flex-shrink: 0; +} + +.gmail-message-body { + margin-top: 14px; + max-width: 820px; + color: #202124; + font-family: Arial, sans-serif; + font-size: 14px; + line-height: 1.6; + white-space: pre-wrap; +} + +.gmail-thread-actions { + display: flex; + gap: 8px; + margin: 28px 0 16px 52px; +} + +.gmail-thread-actions button { + display: inline-flex; + align-items: center; + gap: 8px; + height: 36px; + padding: 0 18px; + border: 1px solid #dadce0; + border-radius: 18px; + background: #fff; + color: #3c4043; + font: inherit; + font-size: 14px; + cursor: pointer; +} + +.gmail-thread-actions button:hover { + background: #f8fafd; +} + +.gmail-compose-card { + max-width: 720px; + margin-left: 52px; + border: 1px solid #dadce0; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 2px 6px rgba(60, 64, 67, 0.18); +} + +.gmail-compose-header { + display: flex; + align-items: center; + justify-content: space-between; + height: 36px; + padding: 0 12px; + background: #f2f6fc; + font-size: 13px; + font-weight: 500; +} + +.gmail-compose-header button, +.gmail-compose-link { + border: none; + background: transparent; + color: #5f6368; + cursor: pointer; +} + +.gmail-compose-line { + display: flex; + align-items: center; + gap: 8px; + min-height: 36px; + padding: 0 12px; + border-bottom: 1px solid #f1f3f4; + color: #5f6368; + font-size: 13px; +} + +.gmail-compose-line input { + min-width: 0; + flex: 1; + border: none; + outline: none; + background: transparent; + color: #202124; + font: inherit; +} + +.gmail-compose-card textarea { + display: block; + width: 100%; + min-height: 120px; + border: none; + outline: none; + resize: none; + padding: 12px; + color: #202124; + font: 14px/1.5 Arial, sans-serif; +} + +.gmail-compose-actions { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + border-top: 1px solid #f1f3f4; +} + +.gmail-send-button { + display: inline-flex; + align-items: center; + gap: 8px; + height: 36px; + padding: 0 18px; + border: none; + border-radius: 18px; + background: #0b57d0; + color: #fff; + font: inherit; + font-size: 14px; + font-weight: 500; + cursor: pointer; +} + +.gmail-empty-state { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + min-height: 0; + border-radius: 16px; + background: #fff; + color: #5f6368; + font-size: 14px; +} + +@media (max-width: 980px) { + .gmail-left-nav { + display: none; + } + + .gmail-main { + padding-left: 8px; + } + + .gmail-layout { + grid-template-columns: 1fr; + } + + .gmail-list:has(+ .gmail-detail) { + display: none; + } + + .gmail-back-button { + display: inline-flex; + } + + .gmail-thread-scroll { + padding: 18px; + } + + .gmail-compose-card, + .gmail-thread-actions { + margin-left: 0; + } +} + @theme inline { --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index b3297ccd..017f9b2f 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -24,6 +24,7 @@ import { SidebarContentPanel } from '@/components/sidebar-content'; import { SuggestedTopicsView } from '@/components/suggested-topics-view'; import { LiveNotesView } from '@/components/live-notes-view'; import { BgTasksView } from '@/components/bg-tasks-view'; +import { EmailView } from '@/components/email-view'; import { SidebarSectionProvider } from '@/contexts/sidebar-context'; import { Conversation, @@ -178,6 +179,7 @@ const GRAPH_TAB_PATH = '__rowboat_graph_view__' const SUGGESTED_TOPICS_TAB_PATH = '__rowboat_suggested_topics__' const LIVE_NOTES_TAB_PATH = '__rowboat_live_notes__' const BG_TASKS_TAB_PATH = '__rowboat_bg_tasks__' +const EMAIL_TAB_PATH = '__rowboat_email__' const BASES_DEFAULT_TAB_PATH = '__rowboat_bases_default__' const clampNumber = (value: number, min: number, max: number) => @@ -309,6 +311,7 @@ const isGraphTabPath = (path: string) => path === GRAPH_TAB_PATH const isSuggestedTopicsTabPath = (path: string) => path === SUGGESTED_TOPICS_TAB_PATH const isLiveNotesTabPath = (path: string) => path === LIVE_NOTES_TAB_PATH const isBgTasksTabPath = (path: string) => path === BG_TASKS_TAB_PATH +const isEmailTabPath = (path: string) => path === EMAIL_TAB_PATH const isBaseFilePath = (path: string) => path.endsWith('.base') || path === BASES_DEFAULT_TAB_PATH const getSuggestedTopicTargetFolder = (category?: string) => { @@ -557,6 +560,7 @@ type ViewState = | { type: 'task'; name: string } | { type: 'suggested-topics' } | { type: 'live-notes' } + | { type: 'email' } function viewStatesEqual(a: ViewState, b: ViewState): boolean { if (a.type !== b.type) return false @@ -710,11 +714,13 @@ function App() { const [isSuggestedTopicsOpen, setIsSuggestedTopicsOpen] = useState(false) const [isLiveNotesOpen, setIsLiveNotesOpen] = useState(false) const [isBgTasksOpen, setIsBgTasksOpen] = useState(false) + const [isEmailOpen, setIsEmailOpen] = useState(false) const [expandedFrom, setExpandedFrom] = useState<{ path: string | null graph: boolean suggestedTopics: boolean liveNotes: boolean + email: boolean } | null>(null) const [baseConfigByPath, setBaseConfigByPath] = useState>({}) const [graphData, setGraphData] = useState<{ nodes: GraphNode[]; edges: GraphEdge[] }>({ @@ -1041,6 +1047,7 @@ function App() { if (isSuggestedTopicsTabPath(tab.path)) return 'Suggested Topics' if (isLiveNotesTabPath(tab.path)) return 'Live notes' if (isBgTasksTabPath(tab.path)) return 'Background tasks' + if (isEmailTabPath(tab.path)) return 'Email' if (tab.path === BASES_DEFAULT_TAB_PATH) return 'Bases' if (tab.path.endsWith('.base')) return tab.path.split('/').pop()?.replace(/\.base$/i, '') || 'Base' return tab.path.split('/').pop()?.replace(/\.md$/i, '') || tab.path @@ -2753,7 +2760,7 @@ function App() { setActiveFileTabId(existingTab.id) setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) setSelectedPath(path) return } @@ -2762,7 +2769,7 @@ function App() { setActiveFileTabId(id) setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) setSelectedPath(path) }, [fileTabs, dismissBrowserOverlay]) @@ -2781,32 +2788,43 @@ function App() { setSelectedPath(null) setIsGraphOpen(true) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) return } if (isSuggestedTopicsTabPath(tab.path)) { setSelectedPath(null) setIsGraphOpen(false) setIsSuggestedTopicsOpen(true) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) return } if (isLiveNotesTabPath(tab.path)) { setSelectedPath(null) setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) + setIsBgTasksOpen(false) + setIsEmailOpen(false) setIsLiveNotesOpen(true) return } + if (isEmailTabPath(tab.path)) { + setSelectedPath(null) + setIsGraphOpen(false) + setIsSuggestedTopicsOpen(false) + setIsLiveNotesOpen(false) + setIsBgTasksOpen(false) + setIsEmailOpen(true) + return + } setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) setSelectedPath(tab.path) }, [fileTabs, isRightPaneMaximized, dismissBrowserOverlay]) const closeFileTab = useCallback((tabId: string) => { const closingTab = fileTabs.find(t => t.id === tabId) - if (closingTab && !isGraphTabPath(closingTab.path) && !isBaseFilePath(closingTab.path)) { + if (closingTab && !isGraphTabPath(closingTab.path) && !isSuggestedTopicsTabPath(closingTab.path) && !isLiveNotesTabPath(closingTab.path) && !isBgTasksTabPath(closingTab.path) && !isEmailTabPath(closingTab.path) && !isBaseFilePath(closingTab.path)) { removeEditorCacheForPath(closingTab.path) initialContentByPathRef.current.delete(closingTab.path) untitledRenameReadyPathsRef.current.delete(closingTab.path) @@ -2829,7 +2847,7 @@ function App() { setSelectedPath(null) setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) return [] } const idx = prev.findIndex(t => t.id === tabId) @@ -2843,21 +2861,30 @@ function App() { setSelectedPath(null) setIsGraphOpen(true) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) } else if (isSuggestedTopicsTabPath(newActiveTab.path)) { setSelectedPath(null) setIsGraphOpen(false) setIsSuggestedTopicsOpen(true) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) } else if (isLiveNotesTabPath(newActiveTab.path)) { setSelectedPath(null) setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) + setIsBgTasksOpen(false) + setIsEmailOpen(false) setIsLiveNotesOpen(true) + } else if (isEmailTabPath(newActiveTab.path)) { + setSelectedPath(null) + setIsGraphOpen(false) + setIsSuggestedTopicsOpen(false) + setIsLiveNotesOpen(false) + setIsBgTasksOpen(false) + setIsEmailOpen(true) } else { setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) setSelectedPath(newActiveTab.path) } } @@ -2888,12 +2915,13 @@ function App() { dismissBrowserOverlay() handleNewChat() // Left-pane "new chat" should always open full chat view. - if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen) { + if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen) { setExpandedFrom({ path: selectedPath, graph: isGraphOpen, suggestedTopics: isSuggestedTopicsOpen, liveNotes: isLiveNotesOpen, + email: isEmailOpen, }) } else { setExpandedFrom(null) @@ -2902,8 +2930,8 @@ function App() { setSelectedPath(null) setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) - }, [chatTabs, activeChatTabId, dismissBrowserOverlay, handleNewChat, selectedPath, isGraphOpen, isSuggestedTopicsOpen, isLiveNotesOpen, isBgTasksOpen]) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) + }, [chatTabs, activeChatTabId, dismissBrowserOverlay, handleNewChat, selectedPath, isGraphOpen, isSuggestedTopicsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen]) // Sidebar variant: create/switch chat tab without leaving file/graph context. const handleNewChatTabInSidebar = useCallback(() => { @@ -3035,12 +3063,13 @@ function App() { const handleOpenFullScreenChat = useCallback(() => { // Remember where we came from so the close button can return - if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen) { + if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen) { setExpandedFrom({ path: selectedPath, graph: isGraphOpen, suggestedTopics: isSuggestedTopicsOpen, liveNotes: isLiveNotesOpen, + email: isEmailOpen, }) } dismissBrowserOverlay() @@ -3048,27 +3077,35 @@ function App() { setSelectedPath(null) setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) - }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isLiveNotesOpen, isBgTasksOpen, dismissBrowserOverlay]) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) + }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, dismissBrowserOverlay]) const handleCloseFullScreenChat = useCallback(() => { if (expandedFrom) { if (expandedFrom.graph) { setIsGraphOpen(true) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) } else if (expandedFrom.suggestedTopics) { setIsGraphOpen(false) setIsSuggestedTopicsOpen(true) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) } else if (expandedFrom.liveNotes) { setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) + setIsBgTasksOpen(false) + setIsEmailOpen(false) setIsLiveNotesOpen(true) + } else if (expandedFrom.email) { + setIsGraphOpen(false) + setIsSuggestedTopicsOpen(false) + setIsLiveNotesOpen(false) + setIsBgTasksOpen(false) + setIsEmailOpen(true) } else if (expandedFrom.path) { setIsGraphOpen(false) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) setSelectedPath(expandedFrom.path) } setExpandedFrom(null) @@ -3078,12 +3115,13 @@ function App() { const currentViewState = React.useMemo(() => { if (selectedBackgroundTask) return { type: 'task', name: selectedBackgroundTask } + if (isEmailOpen) return { type: 'email' } if (isLiveNotesOpen) return { type: 'live-notes' } if (isSuggestedTopicsOpen) return { type: 'suggested-topics' } if (selectedPath) return { type: 'file', path: selectedPath } if (isGraphOpen) return { type: 'graph' } return { type: 'chat', runId } - }, [selectedBackgroundTask, isLiveNotesOpen, isBgTasksOpen, isSuggestedTopicsOpen, selectedPath, isGraphOpen, runId]) + }, [selectedBackgroundTask, isEmailOpen, isLiveNotesOpen, isBgTasksOpen, isSuggestedTopicsOpen, selectedPath, isGraphOpen, runId]) const appendUnique = useCallback((stack: ViewState[], entry: ViewState) => { const last = stack[stack.length - 1] @@ -3162,12 +3200,37 @@ function App() { setActiveFileTabId(id) }, [fileTabs]) + const ensureEmailFileTab = useCallback(() => { + const existing = fileTabs.find((tab) => isEmailTabPath(tab.path)) + if (existing) { + setActiveFileTabId(existing.id) + return + } + const id = newFileTabId() + setFileTabs((prev) => [...prev, { id, path: EMAIL_TAB_PATH }]) + setActiveFileTabId(id) + }, [fileTabs]) + + const openEmailView = useCallback(() => { + setSelectedPath(null) + setIsGraphOpen(false) + setIsBrowserOpen(false) + setIsSuggestedTopicsOpen(false) + setIsLiveNotesOpen(false) + setIsBgTasksOpen(false) + setSelectedBackgroundTask(null) + setExpandedFrom(null) + setIsRightPaneMaximized(false) + setIsEmailOpen(true) + ensureEmailFileTab() + }, [ensureEmailFileTab]) + const openBgTasksView = useCallback(() => { setSelectedPath(null) setIsGraphOpen(false) setIsBrowserOpen(false) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) setSelectedBackgroundTask(null) setExpandedFrom(null) setIsRightPaneMaximized(false) @@ -3184,7 +3247,7 @@ function App() { // visible in the middle pane. setIsBrowserOpen(false) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) setExpandedFrom(null) // Preserve split vs knowledge-max mode when navigating knowledge files. // Only exit chat-only maximize, because that would hide the selected file. @@ -3199,7 +3262,7 @@ function App() { setSelectedPath(null) setIsBrowserOpen(false) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) setExpandedFrom(null) setIsGraphOpen(true) ensureGraphFileTab() @@ -3212,7 +3275,7 @@ function App() { setIsGraphOpen(false) setIsBrowserOpen(false) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) setExpandedFrom(null) setIsRightPaneMaximized(false) setSelectedBackgroundTask(view.name) @@ -3225,7 +3288,7 @@ function App() { setIsRightPaneMaximized(false) setSelectedBackgroundTask(null) setIsSuggestedTopicsOpen(true) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) ensureSuggestedTopicsFileTab() return case 'live-notes': @@ -3236,9 +3299,24 @@ function App() { setIsRightPaneMaximized(false) setSelectedBackgroundTask(null) setIsSuggestedTopicsOpen(false) + setIsBgTasksOpen(false) + setIsEmailOpen(false) setIsLiveNotesOpen(true) ensureLiveNotesFileTab() return + case 'email': + setSelectedPath(null) + setIsGraphOpen(false) + setIsBrowserOpen(false) + setExpandedFrom(null) + setIsRightPaneMaximized(false) + setSelectedBackgroundTask(null) + setIsSuggestedTopicsOpen(false) + setIsLiveNotesOpen(false) + setIsBgTasksOpen(false) + setIsEmailOpen(true) + ensureEmailFileTab() + return case 'chat': setSelectedPath(null) setIsGraphOpen(false) @@ -3247,7 +3325,7 @@ function App() { setIsRightPaneMaximized(false) setSelectedBackgroundTask(null) setIsSuggestedTopicsOpen(false) - setIsLiveNotesOpen(false); setIsBgTasksOpen(false) + setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false) if (view.runId) { await loadRun(view.runId) } else { @@ -3255,7 +3333,7 @@ function App() { } return } - }, [ensureLiveNotesFileTab, ensureFileTabForPath, ensureGraphFileTab, ensureSuggestedTopicsFileTab, handleNewChat, isRightPaneMaximized, loadRun]) + }, [ensureEmailFileTab, ensureLiveNotesFileTab, ensureFileTabForPath, ensureGraphFileTab, ensureSuggestedTopicsFileTab, handleNewChat, isRightPaneMaximized, loadRun]) const navigateToView = useCallback(async (nextView: ViewState) => { const current = currentViewState @@ -3577,7 +3655,7 @@ function App() { }, []) // Keyboard shortcut: Ctrl+L to toggle main chat view - const isFullScreenChat = !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen && !selectedBackgroundTask && !isBrowserOpen + const isFullScreenChat = !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !selectedBackgroundTask && !isBrowserOpen useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'l') { @@ -3650,11 +3728,11 @@ function App() { const handleTabKeyDown = (e: KeyboardEvent) => { const mod = e.metaKey || e.ctrlKey if (!mod) return - const rightPaneAvailable = Boolean((selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen) && isChatSidebarOpen) + const rightPaneAvailable = Boolean((selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen) && isChatSidebarOpen) const targetPane: ShortcutPane = rightPaneAvailable ? (isRightPaneMaximized ? 'right' : activeShortcutPane) : 'left' - const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen) + const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen) const selectedKnowledgePath = isGraphOpen ? GRAPH_TAB_PATH : isSuggestedTopicsOpen @@ -3663,6 +3741,8 @@ function App() { ? LIVE_NOTES_TAB_PATH : isBgTasksOpen ? BG_TASKS_TAB_PATH + : isEmailOpen + ? EMAIL_TAB_PATH : selectedPath const targetFileTabId = activeFileTabId ?? ( selectedKnowledgePath @@ -3717,7 +3797,7 @@ function App() { } document.addEventListener('keydown', handleTabKeyDown) return () => document.removeEventListener('keydown', handleTabKeyDown) - }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isLiveNotesOpen, isBgTasksOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab]) + }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab]) const toggleExpand = (path: string, kind: 'file' | 'dir') => { if (kind === 'file') { @@ -3742,7 +3822,7 @@ function App() { }), }, })) - if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen && !selectedBackgroundTask) { + if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !selectedBackgroundTask) { setIsChatSidebarOpen(false) setIsRightPaneMaximized(false) } @@ -3868,14 +3948,14 @@ function App() { }, openGraph: () => { // From chat-only landing state, open graph directly in full knowledge view. - if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen && !selectedBackgroundTask) { + if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !selectedBackgroundTask) { setIsChatSidebarOpen(false) setIsRightPaneMaximized(false) } void navigateToView({ type: 'graph' }) }, openBases: () => { - if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen && !selectedBackgroundTask) { + if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !selectedBackgroundTask) { setIsChatSidebarOpen(false) setIsRightPaneMaximized(false) } @@ -4465,7 +4545,7 @@ function App() { const selectedTask = selectedBackgroundTask ? backgroundTasks.find(t => t.name === selectedBackgroundTask) : null - const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isBrowserOpen) + const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isBrowserOpen) const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized const shouldCollapseLeftPane = isRightPaneOnlyMode const openMarkdownTabs = React.useMemo(() => { @@ -4482,7 +4562,7 @@ function App() { return ( { - if (section === 'knowledge' && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen) { + if (section === 'knowledge' && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen) { void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH }) } }}> @@ -4515,7 +4595,7 @@ function App() { onNewChat: handleNewChatTab, onSelectRun: (runIdToLoad) => { cancelRecordingIfActive() - if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isBrowserOpen) { + if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isBrowserOpen) { setIsChatSidebarOpen(true) } @@ -4526,7 +4606,7 @@ function App() { return } // In two-pane mode (file/graph/browser), keep the middle pane and just swap chat context in the right sidebar. - if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isBrowserOpen) { + if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isBrowserOpen) { setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: runIdToLoad } : t)) loadRun(runIdToLoad) return @@ -4550,14 +4630,14 @@ function App() { } else { // Only one tab, reset it to new chat setChatTabs([{ id: tabForRun.id, runId: null }]) - if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isBrowserOpen) { + if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isBrowserOpen) { handleNewChat() } else { void navigateToView({ type: 'chat', runId: null }) } } } else if (runId === runIdToDelete) { - if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isBrowserOpen) { + if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isBrowserOpen) { setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: null } : t)) handleNewChat() } else { @@ -4591,6 +4671,8 @@ function App() { onOpenLiveNotes={() => void navigateToView({ type: 'live-notes' })} isBgTasksOpen={isBgTasksOpen} onOpenBgTasks={openBgTasksView} + isEmailOpen={isEmailOpen} + onOpenEmail={openEmailView} /> - {(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen) && fileTabs.length >= 1 ? ( + {(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen) && fileTabs.length >= 1 ? ( t.id} onSwitchTab={switchFileTab} onCloseTab={closeFileTab} - allowSingleTabClose={fileTabs.length === 1 && (isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || (selectedPath != null && isBaseFilePath(selectedPath)))} + allowSingleTabClose={fileTabs.length === 1 && (isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || (selectedPath != null && isBaseFilePath(selectedPath)))} /> ) : ( Version history )} - {!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen && !selectedTask && !isBrowserOpen && ( + {!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !selectedTask && !isBrowserOpen && ( + +
+ {mode === 'reply' ? 'To' : 'Recipients'} + +
+ {mode === 'forward' && ( +
+ Subject + +
+ )} +