Email page (#561)

* email view

* render html emails

* match unread and read status

* move to accordian

* faster loads

* iframe mounted across toggle and cached height

* prefetch on hover

* fix iframe caching

* split inbox

* email processing agent

* summary

* rich text

* email drafts

* add pagination, watcher and separation from gmail sync

* fix first load issue

* handle drafts

* send button opens the thread

* simplify renderer and fix flickering issue

* remove rended driven email path

* support attachments in incoming emails

* fix white background as well as dark mode
This commit is contained in:
arkml 2026-05-18 21:46:26 +05:30 committed by GitHub
parent af618155e1
commit 7dcf8eea70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 3139 additions and 72 deletions

View file

@ -47,6 +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 { listImportantThreads, listEverythingElseThreads, saveMessageBodyHeight, triggerSync as triggerGmailSync } 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';
@ -482,6 +483,20 @@ export function setupIpcHandlers() {
'workspace:remove': async (_event, args) => {
return workspace.remove(args.path, args.opts);
},
'gmail:getImportant': async (_event, args) => {
return listImportantThreads({ cursor: args.cursor, limit: args.limit });
},
'gmail:getEverythingElse': async (_event, args) => {
return listEverythingElseThreads({ cursor: args.cursor, limit: args.limit });
},
'gmail:triggerSync': async () => {
triggerGmailSync();
return {};
},
'gmail:saveMessageHeight': async (_event, args) => {
saveMessageBodyHeight(args.threadId, args.messageId, args.height);
return {};
},
'mcp:listTools': async (_event, args) => {
return mcpCore.listTools(args.serverName, args.cursor);
},

View file

@ -58,6 +58,877 @@
background-image: radial-gradient(circle, oklch(0.7 0 0 / 0.06) 1px, transparent 1px);
}
.gmail-shell {
--gm-bg: #0f0f12;
--gm-bg-card: #131317;
--gm-bg-input: #1c1c20;
--gm-bg-row-hover: #16161a;
--gm-bg-row-selected: #1a1620;
--gm-bg-row-selected-hover: #1d1825;
--gm-bg-iframe: #fafafa;
--gm-bg-pill: #1c1c20;
--gm-bg-pill-hover: #232328;
--gm-text: #e4e4e7;
--gm-text-strong: #fafafa;
--gm-text-muted: #71717a;
--gm-text-faint: #52525b;
--gm-text-body: #d4d4d8;
--gm-border: #1f1f24;
--gm-border-strong: #2e2e35;
--gm-accent: #a78bfa;
--gm-accent-hover: #b9a6ff;
--gm-accent-glow: rgba(167, 139, 250, 0.45);
--gm-accent-fg: #18181b;
--gm-icon-hover-bg: #1f1f24;
--gm-placeholder: #52525b;
display: flex;
height: 100%;
min-height: 0;
width: 100%;
overflow: hidden;
background: var(--gm-bg);
color: var(--gm-text);
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-feature-settings: "ss01", "cv11";
}
.light .gmail-shell {
--gm-bg: #ffffff;
--gm-bg-card: #ffffff;
--gm-bg-input: #f4f4f7;
--gm-bg-row-hover: #f7f7f9;
--gm-bg-row-selected: #f5f0ff;
--gm-bg-row-selected-hover: #ece4ff;
--gm-bg-iframe: #ffffff;
--gm-bg-pill: #ffffff;
--gm-bg-pill-hover: #f4f4f7;
--gm-text: #27272a;
--gm-text-strong: #09090b;
--gm-text-muted: #71717a;
--gm-text-faint: #a1a1aa;
--gm-text-body: #3f3f46;
--gm-border: #e4e4e7;
--gm-border-strong: #d4d4d8;
--gm-accent: #7c3aed;
--gm-accent-hover: #6d28d9;
--gm-accent-glow: rgba(124, 58, 237, 0.3);
--gm-accent-fg: #ffffff;
--gm-icon-hover-bg: #f4f4f7;
--gm-placeholder: #a1a1aa;
}
.gmail-main {
display: flex;
min-width: 0;
min-height: 0;
flex: 1;
flex-direction: column;
padding: 0;
}
.gmail-topbar {
display: flex;
align-items: center;
gap: 12px;
height: 52px;
padding: 0 20px;
border-bottom: 1px solid var(--gm-border);
}
.gmail-search {
display: flex;
align-items: center;
gap: 10px;
width: min(520px, 100%);
height: 32px;
padding: 0 12px;
border-radius: 8px;
background: var(--gm-bg-input);
color: var(--gm-text-muted);
}
.gmail-search input {
flex: 1;
border: none;
outline: none;
background: transparent;
color: var(--gm-text);
font-size: 13px;
letter-spacing: 0.01em;
}
.gmail-search input::placeholder {
color: var(--gm-placeholder);
}
.gmail-icon-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border: none;
border-radius: 6px;
background: transparent;
color: var(--gm-text-muted);
cursor: pointer;
transition: background 120ms ease, color 120ms ease;
}
.gmail-icon-button:hover {
background: var(--gm-icon-hover-bg);
color: var(--gm-text);
}
.gmail-list {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
flex: 1;
overflow: auto;
background: var(--gm-bg);
border: none;
border-radius: 0;
}
.gmail-row-group {
display: flex;
flex-direction: column;
}
.gmail-list-header {
position: sticky;
top: 0;
z-index: 1;
display: flex;
justify-content: space-between;
height: 32px;
padding: 0 24px;
align-items: center;
background: var(--gm-bg);
border-bottom: 1px solid var(--gm-border);
color: var(--gm-text-faint);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.gmail-section {
display: flex;
flex-direction: column;
}
.gmail-section + .gmail-section {
margin-top: 28px;
}
.gmail-section-sentinel {
display: flex;
align-items: center;
justify-content: center;
height: 28px;
color: var(--gm-text-faint);
}
.gmail-row {
display: grid;
grid-template-columns: 12px minmax(140px, 0.22fr) minmax(0, 1fr) 60px;
align-items: center;
gap: 16px;
width: 100%;
min-height: 40px;
padding: 0 24px;
border: none;
background: transparent;
color: var(--gm-text-muted);
text-align: left;
cursor: pointer;
font-family: inherit;
transition: background 120ms ease;
}
.gmail-row:hover {
background: var(--gm-bg-row-hover);
box-shadow: none;
}
.gmail-row-selected {
background: var(--gm-bg-row-selected);
box-shadow: inset 2px 0 0 var(--gm-accent);
}
.gmail-row-selected:hover {
background: var(--gm-bg-row-selected-hover);
}
.gmail-row-unread {
color: var(--gm-text);
}
.gmail-row-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: transparent;
}
.gmail-row-unread .gmail-row-dot {
background: var(--gm-accent);
box-shadow: 0 0 8px var(--gm-accent-glow);
}
.gmail-row-sender,
.gmail-row-content strong,
.gmail-row-date {
font-size: 13px;
font-weight: 400;
letter-spacing: -0.005em;
}
.gmail-row-unread .gmail-row-sender,
.gmail-row-unread .gmail-row-content strong {
font-weight: 600;
color: var(--gm-text-strong);
}
.gmail-row-unread .gmail-row-date {
color: var(--gm-text);
}
.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: 8px;
color: var(--gm-text-faint);
font-size: 13px;
}
.gmail-row-content strong {
flex-shrink: 0;
color: inherit;
font-weight: 400;
}
.gmail-row-date {
justify-self: end;
color: var(--gm-text-faint);
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.gmail-detail {
display: flex;
min-width: 0;
flex-direction: column;
background: transparent;
}
.gmail-detail-inline {
background: var(--gm-bg-card);
border-top: 1px solid var(--gm-border);
border-bottom: 1px solid var(--gm-border);
box-shadow: inset 2px 0 0 var(--gm-accent);
}
.gmail-detail-hidden {
display: none;
}
.gmail-detail-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
height: 48px;
padding: 0 24px;
border-bottom: 1px solid var(--gm-border);
background: transparent;
}
.gmail-thread-subject-inline {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--gm-text-strong);
font-size: 15px;
font-weight: 500;
letter-spacing: -0.01em;
}
.gmail-thread-body {
padding: 20px 24px 28px;
background: transparent;
}
.gmail-thread-summary {
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--gm-border);
}
.gmail-thread-summary-label {
display: block;
margin-bottom: 6px;
color: var(--gm-text-faint);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.gmail-thread-summary-text {
display: block;
color: var(--gm-text);
font-size: 13px;
line-height: 1.55;
}
.gmail-message-stack {
display: flex;
flex-direction: column;
gap: 12px;
}
.gmail-message {
display: grid;
grid-template-columns: 28px minmax(0, 1fr);
gap: 12px;
padding: 12px 0;
border-top: 1px solid var(--gm-border);
}
.gmail-message:first-child {
border-top: 0;
padding-top: 4px;
}
.gmail-message-header {
display: block;
width: 100%;
padding: 0;
margin: 0;
border: none;
background: transparent;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.gmail-message-snippet {
margin-top: 2px;
color: var(--gm-text-muted);
font-size: 12px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.gmail-message:not(.gmail-message-expanded) .gmail-message-header:hover .gmail-message-from strong {
color: var(--gm-accent);
}
.gmail-message-avatar {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
color: #fafafa;
font-weight: 600;
font-size: 11px;
letter-spacing: 0.02em;
}
.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: 13px;
font-weight: 600;
color: var(--gm-text-strong);
letter-spacing: -0.005em;
}
.gmail-message-from span,
.gmail-message-to,
.gmail-message-date {
color: var(--gm-text-muted);
font-size: 12px;
}
.gmail-message-date {
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
.gmail-message-iframe {
display: block;
width: 100%;
max-width: 820px;
margin-top: 12px;
border: 0;
background: var(--gm-bg-iframe);
border-radius: 6px;
}
.gmail-message-iframe-adaptive {
background: var(--gm-bg-card);
}
.gmail-message-plain {
max-width: 820px;
margin-top: 12px;
padding: 10px 14px;
background: var(--gm-bg-iframe);
border-radius: 6px;
color: var(--gm-text);
}
.gmail-message-pre {
margin: 0;
font: 14px/1.6 Arial, sans-serif;
white-space: pre-wrap;
word-wrap: break-word;
}
.gmail-message-pre-quoted {
margin-top: 12px;
color: var(--gm-text-muted);
}
.gmail-quote-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
margin-top: 8px;
height: 22px;
padding: 0 10px;
border: 1px solid var(--gm-border-strong);
border-radius: 4px;
background: var(--gm-bg-pill);
color: var(--gm-text-muted);
font: inherit;
font-size: 12px;
letter-spacing: 0.04em;
cursor: pointer;
transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
.gmail-quote-toggle:hover {
background: var(--gm-bg-pill-hover);
color: var(--gm-text-strong);
border-color: var(--gm-border-strong);
}
.gmail-quote-toggle[aria-expanded="true"] {
background: var(--gm-bg-row-selected);
color: var(--gm-accent);
border-color: var(--gm-accent);
}
.gmail-message-attachments {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
max-width: 820px;
}
.gmail-attachment {
display: inline-flex;
align-items: center;
gap: 8px;
max-width: 320px;
padding: 6px 10px;
border: 1px solid var(--gm-border-strong);
border-radius: 6px;
background: var(--gm-bg-pill);
color: var(--gm-text);
font: inherit;
font-size: 12px;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.gmail-attachment:hover {
background: var(--gm-bg-pill-hover);
border-color: var(--gm-accent);
color: var(--gm-accent);
}
.gmail-attachment-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: 500;
}
.gmail-attachment-size {
flex-shrink: 0;
color: var(--gm-text-muted);
font-size: 11px;
font-variant-numeric: tabular-nums;
}
.gmail-thread-actions {
display: flex;
gap: 8px;
margin: 20px 0 12px 40px;
}
.gmail-thread-actions button {
display: inline-flex;
align-items: center;
gap: 8px;
height: 30px;
padding: 0 14px;
border: 1px solid var(--gm-border-strong);
border-radius: 6px;
background: var(--gm-bg-pill);
color: var(--gm-text-body);
font: inherit;
font-size: 12px;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease;
}
.gmail-thread-actions button:hover {
background: var(--gm-bg-pill-hover);
border-color: var(--gm-border-strong);
}
.gmail-compose-card {
max-width: 720px;
margin-left: 40px;
border: 1px solid var(--gm-border-strong);
border-radius: 8px;
overflow: hidden;
background: var(--gm-bg-card);
}
.gmail-compose-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 32px;
padding: 0 12px;
background: var(--gm-bg-input);
color: var(--gm-text-body);
font-size: 12px;
font-weight: 500;
letter-spacing: 0.01em;
text-transform: uppercase;
}
.gmail-compose-header button,
.gmail-compose-link {
border: none;
background: transparent;
color: var(--gm-text-muted);
cursor: pointer;
}
.gmail-compose-line {
display: flex;
align-items: center;
gap: 8px;
min-height: 32px;
padding: 0 12px;
border-bottom: 1px solid var(--gm-border);
color: var(--gm-text-muted);
font-size: 12px;
}
.gmail-compose-line input {
min-width: 0;
flex: 1;
border: none;
outline: none;
background: transparent;
color: var(--gm-text);
font: inherit;
}
.gmail-compose-toolbar {
display: flex;
align-items: center;
gap: 2px;
flex: 1;
min-width: 0;
justify-content: center;
}
.gmail-compose-link-popover {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-top: 1px solid var(--gm-border);
background: var(--gm-bg-input);
}
.gmail-compose-link-popover input {
flex: 1;
min-width: 0;
height: 28px;
padding: 0 8px;
border: 1px solid var(--gm-border-strong);
border-radius: 4px;
background: var(--gm-bg-card);
color: var(--gm-text);
font: inherit;
font-size: 12px;
outline: none;
}
.gmail-compose-link-popover input:focus {
border-color: var(--gm-accent);
}
.gmail-compose-link-popover button {
height: 26px;
padding: 0 10px;
border: 1px solid var(--gm-border-strong);
border-radius: 4px;
background: var(--gm-bg-pill);
color: var(--gm-text);
font: inherit;
font-size: 12px;
cursor: pointer;
}
.gmail-compose-link-popover button:hover {
background: var(--gm-bg-pill-hover);
}
.gmail-compose-link-popover-apply {
background: var(--gm-accent) !important;
border-color: var(--gm-accent) !important;
color: var(--gm-accent-fg) !important;
font-weight: 600;
}
.gmail-compose-link-popover-apply:hover {
background: var(--gm-accent-hover) !important;
border-color: var(--gm-accent-hover) !important;
}
.gmail-compose-tool {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--gm-text-muted);
cursor: pointer;
transition: background 120ms ease, color 120ms ease;
}
.gmail-compose-tool:hover {
background: var(--gm-bg-pill-hover);
color: var(--gm-text);
}
.gmail-compose-tool.is-active {
background: var(--gm-bg-pill-hover);
color: var(--gm-accent);
}
.gmail-compose-tool-sep {
display: inline-block;
width: 1px;
height: 18px;
margin: 0 6px;
background: var(--gm-border-strong);
}
.gmail-compose-editor {
display: block;
width: 100%;
max-height: 360px;
overflow-y: auto;
}
.gmail-compose-content {
outline: none;
min-height: 120px;
padding: 12px;
background: transparent;
color: var(--gm-text);
font: 13px/1.55 inherit;
}
.gmail-compose-content p {
margin: 0;
}
.gmail-compose-content p + p,
.gmail-compose-content p + ul,
.gmail-compose-content p + ol,
.gmail-compose-content p + blockquote {
margin-top: 8px;
}
.gmail-compose-content ul,
.gmail-compose-content ol {
margin: 0;
padding-left: 22px;
}
.gmail-compose-content ul {
list-style: disc;
}
.gmail-compose-content ol {
list-style: decimal;
}
.gmail-compose-content li {
margin: 2px 0;
}
.gmail-compose-content li > p {
margin: 0;
}
.gmail-compose-content blockquote {
margin: 4px 0;
padding-left: 12px;
border-left: 2px solid var(--gm-border-strong);
color: var(--gm-text-muted);
}
.gmail-compose-content a {
color: var(--gm-accent);
text-decoration: underline;
}
.gmail-compose-content code {
padding: 1px 4px;
border-radius: 3px;
background: var(--gm-bg-pill-hover);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
}
.gmail-compose-content p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
color: var(--gm-placeholder);
float: left;
height: 0;
pointer-events: none;
}
.gmail-compose-actions {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-top: 1px solid var(--gm-border);
}
.gmail-compose-actions-primary {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.gmail-refine-button {
display: inline-flex;
align-items: center;
gap: 8px;
box-sizing: border-box;
height: 30px;
padding: 0 14px;
border: 1px solid var(--gm-border-strong);
border-radius: 6px;
background: var(--gm-bg-pill);
color: var(--gm-text);
font: inherit;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.gmail-refine-button:hover {
background: var(--gm-bg-pill-hover);
border-color: var(--gm-accent);
color: var(--gm-accent);
}
.gmail-send-button {
display: inline-flex;
align-items: center;
gap: 8px;
box-sizing: border-box;
height: 30px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 6px;
background: var(--gm-accent);
color: var(--gm-accent-fg);
font: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.gmail-send-button:hover {
background: var(--gm-accent-hover);
}
.gmail-empty-state {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
min-height: 0;
background: transparent;
color: var(--gm-text-faint);
font-size: 13px;
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);

View file

@ -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<Record<string, BaseConfig>>({})
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<ViewState>(() => {
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)
}
@ -4471,7 +4551,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(() => {
@ -4488,7 +4568,7 @@ function App() {
return (
<TooltipProvider delayDuration={0}>
<SidebarSectionProvider defaultSection="tasks" onSectionChange={(section) => {
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 })
}
}}>
@ -4521,7 +4601,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)
}
@ -4532,7 +4612,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
@ -4556,14 +4636,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 {
@ -4597,6 +4677,8 @@ function App() {
onOpenLiveNotes={() => void navigateToView({ type: 'live-notes' })}
isBgTasksOpen={isBgTasksOpen}
onOpenBgTasks={openBgTasksView}
isEmailOpen={isEmailOpen}
onOpenEmail={openEmailView}
/>
<SidebarInset
className={cn(
@ -4616,7 +4698,7 @@ function App() {
canNavigateForward={canNavigateForward}
collapsedLeftPaddingPx={collapsedLeftPaddingPx}
>
{(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen) && fileTabs.length >= 1 ? (
{(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen) && fileTabs.length >= 1 ? (
<TabBar
tabs={fileTabs}
activeTabId={activeFileTabId ?? ''}
@ -4624,7 +4706,7 @@ function App() {
getTabId={(t) => 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)))}
/>
) : (
<TabBar
@ -4677,7 +4759,7 @@ function App() {
<TooltipContent side="bottom">Version history</TooltipContent>
</Tooltip>
)}
{!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen && !selectedTask && !isBrowserOpen && (
{!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !selectedTask && !isBrowserOpen && (
<Tooltip>
<TooltipTrigger asChild>
<button
@ -4692,7 +4774,7 @@ function App() {
<TooltipContent side="bottom">New chat tab</TooltipContent>
</Tooltip>
)}
{!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isBrowserOpen && expandedFrom && (
{!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isBrowserOpen && expandedFrom && (
<Tooltip>
<TooltipTrigger asChild>
<button
@ -4707,7 +4789,7 @@ function App() {
<TooltipContent side="bottom">Restore two-pane view</TooltipContent>
</Tooltip>
)}
{(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen) && (
{(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen) && (
<Tooltip>
<TooltipTrigger asChild>
<button
@ -4760,6 +4842,10 @@ function App() {
}}
/>
</div>
) : isEmailOpen ? (
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
<EmailView />
</div>
) : selectedPath && isBaseFilePath(selectedPath) ? (
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
<BasesView

File diff suppressed because it is too large Load diff

View file

@ -27,6 +27,7 @@ import {
Lightbulb,
ListChecks,
LoaderIcon,
Mail,
Settings,
Square,
Trash2,
@ -230,6 +231,8 @@ type SidebarContentPanelProps = {
onOpenLiveNotes?: () => void
isBgTasksOpen?: boolean
onOpenBgTasks?: () => void
isEmailOpen?: boolean
onOpenEmail?: () => void
} & React.ComponentProps<typeof Sidebar>
const sectionTabs: { id: ActiveSection; label: string }[] = [
@ -492,6 +495,8 @@ export function SidebarContentPanel({
onOpenLiveNotes,
isBgTasksOpen = false,
onOpenBgTasks,
isEmailOpen = false,
onOpenEmail,
...props
}: SidebarContentPanelProps) {
const { activeSection, setActiveSection } = useSidebarSection()
@ -509,6 +514,7 @@ export function SidebarContentPanel({
const isSuggestedTopicsQuickActionSelected = isSuggestedTopicsOpen && !isBrowserOpen
const isLiveNotesQuickActionSelected = isLiveNotesOpen && !isBrowserOpen
const isBgTasksQuickActionSelected = isBgTasksOpen && !isBrowserOpen
const isEmailQuickActionSelected = isEmailOpen && !isBrowserOpen
const handleRowboatLogin = useCallback(async () => {
try {
@ -697,6 +703,21 @@ export function SidebarContentPanel({
<span>Background tasks</span>
</button>
)}
{onOpenEmail && (
<button
type="button"
onClick={onOpenEmail}
className={cn(
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors",
isEmailQuickActionSelected
? "bg-sidebar-accent text-sidebar-accent-foreground"
: "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
)}
>
<Mail className="size-4" />
<span>Email</span>
</button>
)}
{onOpenLiveNotes && (
<button
type="button"