mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-25 18:55:19 +02:00
move tasks, meetings, and email into the sidebar; add chat history
Surface background tasks, upcoming meetings (with a live recording indicator), and important unread email directly in the sidebar; add a chat history page with chat icons and a home action row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
346c685ac9
commit
193c2a9131
6 changed files with 1128 additions and 316 deletions
|
|
@ -5,7 +5,7 @@ import { RunEvent, ListRunsResponse } from '@x/shared/src/runs.js';
|
||||||
import type { LanguageModelUsage, ToolUIPart } from 'ai';
|
import type { LanguageModelUsage, ToolUIPart } from 'ai';
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { CheckIcon, LoaderIcon, PanelLeftIcon, Maximize2, Minimize2, ChevronLeftIcon, ChevronRightIcon, SquarePen, SearchIcon, HistoryIcon } from 'lucide-react';
|
import { CheckIcon, LoaderIcon, PanelLeftIcon, Maximize2, Minimize2, ChevronLeftIcon, ChevronRightIcon, SquarePen, HistoryIcon } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { MarkdownEditor, type MarkdownEditorHandle } from './components/markdown-editor';
|
import { MarkdownEditor, type MarkdownEditorHandle } from './components/markdown-editor';
|
||||||
import { ChatSidebar } from './components/chat-sidebar';
|
import { ChatSidebar } from './components/chat-sidebar';
|
||||||
|
|
@ -27,6 +27,7 @@ import { BgTasksView } from '@/components/bg-tasks-view';
|
||||||
import { EmailView } from '@/components/email-view';
|
import { EmailView } from '@/components/email-view';
|
||||||
import { WorkspaceView } from '@/components/workspace-view';
|
import { WorkspaceView } from '@/components/workspace-view';
|
||||||
import { KnowledgeView } from '@/components/knowledge-view';
|
import { KnowledgeView } from '@/components/knowledge-view';
|
||||||
|
import { ChatHistoryView } from '@/components/chat-history-view';
|
||||||
import { MeetingsView } from '@/components/meetings-view';
|
import { MeetingsView } from '@/components/meetings-view';
|
||||||
import { SidebarSectionProvider } from '@/contexts/sidebar-context';
|
import { SidebarSectionProvider } from '@/contexts/sidebar-context';
|
||||||
import {
|
import {
|
||||||
|
|
@ -189,6 +190,7 @@ const EMAIL_TAB_PATH = '__rowboat_email__'
|
||||||
const WORKSPACE_TAB_PATH = '__rowboat_workspace__'
|
const WORKSPACE_TAB_PATH = '__rowboat_workspace__'
|
||||||
const WORKSPACE_ROOT = 'knowledge/Workspace'
|
const WORKSPACE_ROOT = 'knowledge/Workspace'
|
||||||
const KNOWLEDGE_VIEW_TAB_PATH = '__rowboat_knowledge_view__'
|
const KNOWLEDGE_VIEW_TAB_PATH = '__rowboat_knowledge_view__'
|
||||||
|
const CHAT_HISTORY_TAB_PATH = '__rowboat_chat_history__'
|
||||||
const BASES_DEFAULT_TAB_PATH = '__rowboat_bases_default__'
|
const BASES_DEFAULT_TAB_PATH = '__rowboat_bases_default__'
|
||||||
|
|
||||||
const clampNumber = (value: number, min: number, max: number) =>
|
const clampNumber = (value: number, min: number, max: number) =>
|
||||||
|
|
@ -324,6 +326,7 @@ const isBgTasksTabPath = (path: string) => path === BG_TASKS_TAB_PATH
|
||||||
const isEmailTabPath = (path: string) => path === EMAIL_TAB_PATH
|
const isEmailTabPath = (path: string) => path === EMAIL_TAB_PATH
|
||||||
const isWorkspaceTabPath = (path: string) => path === WORKSPACE_TAB_PATH
|
const isWorkspaceTabPath = (path: string) => path === WORKSPACE_TAB_PATH
|
||||||
const isKnowledgeViewTabPath = (path: string) => path === KNOWLEDGE_VIEW_TAB_PATH
|
const isKnowledgeViewTabPath = (path: string) => path === KNOWLEDGE_VIEW_TAB_PATH
|
||||||
|
const isChatHistoryTabPath = (path: string) => path === CHAT_HISTORY_TAB_PATH
|
||||||
const isBaseFilePath = (path: string) => path.endsWith('.base') || path === BASES_DEFAULT_TAB_PATH
|
const isBaseFilePath = (path: string) => path.endsWith('.base') || path === BASES_DEFAULT_TAB_PATH
|
||||||
|
|
||||||
const getSuggestedTopicTargetFolder = (category?: string) => {
|
const getSuggestedTopicTargetFolder = (category?: string) => {
|
||||||
|
|
@ -576,6 +579,7 @@ type ViewState =
|
||||||
| { type: 'email' }
|
| { type: 'email' }
|
||||||
| { type: 'workspace'; path?: string }
|
| { type: 'workspace'; path?: string }
|
||||||
| { type: 'knowledge-view' }
|
| { type: 'knowledge-view' }
|
||||||
|
| { type: 'chat-history' }
|
||||||
|
|
||||||
function viewStatesEqual(a: ViewState, b: ViewState): boolean {
|
function viewStatesEqual(a: ViewState, b: ViewState): boolean {
|
||||||
if (a.type !== b.type) return false
|
if (a.type !== b.type) return false
|
||||||
|
|
@ -632,6 +636,8 @@ function parseDeepLink(input: string): ViewState | null {
|
||||||
}
|
}
|
||||||
case 'knowledge-view':
|
case 'knowledge-view':
|
||||||
return { type: 'knowledge-view' }
|
return { type: 'knowledge-view' }
|
||||||
|
case 'chat-history':
|
||||||
|
return { type: 'chat-history' }
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
@ -640,12 +646,8 @@ function parseDeepLink(input: string): ViewState | null {
|
||||||
/** Sidebar toggle (fixed position, top-left) */
|
/** Sidebar toggle (fixed position, top-left) */
|
||||||
function FixedSidebarToggle({
|
function FixedSidebarToggle({
|
||||||
leftInsetPx,
|
leftInsetPx,
|
||||||
onNewChat,
|
|
||||||
onOpenSearch,
|
|
||||||
}: {
|
}: {
|
||||||
leftInsetPx: number
|
leftInsetPx: number
|
||||||
onNewChat?: () => void
|
|
||||||
onOpenSearch?: () => void
|
|
||||||
}) {
|
}) {
|
||||||
const { toggleSidebar } = useSidebar()
|
const { toggleSidebar } = useSidebar()
|
||||||
return (
|
return (
|
||||||
|
|
@ -661,28 +663,6 @@ function FixedSidebarToggle({
|
||||||
>
|
>
|
||||||
<PanelLeftIcon className="size-5" />
|
<PanelLeftIcon className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
{onNewChat && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onNewChat}
|
|
||||||
className="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
|
||||||
aria-label="New chat"
|
|
||||||
title="New chat"
|
|
||||||
>
|
|
||||||
<SquarePen className="size-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{onOpenSearch && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onOpenSearch}
|
|
||||||
className="flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
|
||||||
aria-label="Search"
|
|
||||||
title="Search"
|
|
||||||
>
|
|
||||||
<SearchIcon className="size-5" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -770,6 +750,9 @@ function App() {
|
||||||
const [isWorkspaceOpen, setIsWorkspaceOpen] = useState(false)
|
const [isWorkspaceOpen, setIsWorkspaceOpen] = useState(false)
|
||||||
const [workspaceInitialPath, setWorkspaceInitialPath] = useState<string | null>(null)
|
const [workspaceInitialPath, setWorkspaceInitialPath] = useState<string | null>(null)
|
||||||
const [isKnowledgeViewOpen, setIsKnowledgeViewOpen] = useState(false)
|
const [isKnowledgeViewOpen, setIsKnowledgeViewOpen] = useState(false)
|
||||||
|
const [isChatHistoryOpen, setIsChatHistoryOpen] = useState(false)
|
||||||
|
const [emailInitialThreadId, setEmailInitialThreadId] = useState<string | null>(null)
|
||||||
|
const [emailThreadIdVersion, setEmailThreadIdVersion] = useState(0)
|
||||||
const [expandedFrom, setExpandedFrom] = useState<{
|
const [expandedFrom, setExpandedFrom] = useState<{
|
||||||
path: string | null
|
path: string | null
|
||||||
graph: boolean
|
graph: boolean
|
||||||
|
|
@ -1125,7 +1108,8 @@ function App() {
|
||||||
if (isBgTasksTabPath(tab.path)) return 'Background tasks'
|
if (isBgTasksTabPath(tab.path)) return 'Background tasks'
|
||||||
if (isEmailTabPath(tab.path)) return 'Email'
|
if (isEmailTabPath(tab.path)) return 'Email'
|
||||||
if (isWorkspaceTabPath(tab.path)) return 'Workspace'
|
if (isWorkspaceTabPath(tab.path)) return 'Workspace'
|
||||||
if (isKnowledgeViewTabPath(tab.path)) return 'Knowledge'
|
if (isKnowledgeViewTabPath(tab.path)) return 'Notes'
|
||||||
|
if (isChatHistoryTabPath(tab.path)) return 'Chat history'
|
||||||
if (tab.path === BASES_DEFAULT_TAB_PATH) return 'Bases'
|
if (tab.path === BASES_DEFAULT_TAB_PATH) return 'Bases'
|
||||||
if (tab.path.endsWith('.base')) return tab.path.split('/').pop()?.replace(/\.base$/i, '') || 'Base'
|
if (tab.path.endsWith('.base')) return tab.path.split('/').pop()?.replace(/\.base$/i, '') || 'Base'
|
||||||
return tab.path.split('/').pop()?.replace(/\.md$/i, '') || tab.path
|
return tab.path.split('/').pop()?.replace(/\.md$/i, '') || tab.path
|
||||||
|
|
@ -1414,6 +1398,13 @@ function App() {
|
||||||
loadBackgroundTasks()
|
loadBackgroundTasks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reload bg-task summaries if anything under bg-tasks/ changed
|
||||||
|
if (
|
||||||
|
eventPaths.some((p) => p === 'bg-tasks' || p.startsWith('bg-tasks/'))
|
||||||
|
) {
|
||||||
|
loadBgTaskSummaries()
|
||||||
|
}
|
||||||
|
|
||||||
// Invalidate cached content for files changed outside the active editor.
|
// Invalidate cached content for files changed outside the active editor.
|
||||||
// This prevents stale backlinks after rename-rewrite passes touch many files.
|
// This prevents stale backlinks after rename-rewrite passes touch many files.
|
||||||
for (const path of eventPaths) {
|
for (const path of eventPaths) {
|
||||||
|
|
@ -1759,6 +1750,37 @@ function App() {
|
||||||
loadRuns()
|
loadRuns()
|
||||||
}, [loadRuns])
|
}, [loadRuns])
|
||||||
|
|
||||||
|
const [bgTaskSummaries, setBgTaskSummaries] = useState<Array<{
|
||||||
|
slug: string
|
||||||
|
name: string
|
||||||
|
active: boolean
|
||||||
|
createdAt: string
|
||||||
|
lastAttemptAt?: string
|
||||||
|
lastRunAt?: string
|
||||||
|
}>>([])
|
||||||
|
const [bgTaskInitialSlug, setBgTaskInitialSlug] = useState<string | null>(null)
|
||||||
|
const [bgTaskSlugVersion, setBgTaskSlugVersion] = useState(0)
|
||||||
|
|
||||||
|
const loadBgTaskSummaries = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.ipc.invoke('bg-task:list', { limit: 200 })
|
||||||
|
setBgTaskSummaries(result.items.map((it) => ({
|
||||||
|
slug: it.slug,
|
||||||
|
name: it.name,
|
||||||
|
active: it.active,
|
||||||
|
createdAt: it.createdAt,
|
||||||
|
lastAttemptAt: it.lastAttemptAt,
|
||||||
|
lastRunAt: it.lastRunAt,
|
||||||
|
})))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load bg-task summaries:', err)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadBgTaskSummaries()
|
||||||
|
}, [loadBgTaskSummaries])
|
||||||
|
|
||||||
// Load background tasks
|
// Load background tasks
|
||||||
const loadBackgroundTasks = useCallback(async () => {
|
const loadBackgroundTasks = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -2840,7 +2862,7 @@ function App() {
|
||||||
setActiveFileTabId(existingTab.id)
|
setActiveFileTabId(existingTab.id)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
setSelectedPath(path)
|
setSelectedPath(path)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -2849,7 +2871,7 @@ function App() {
|
||||||
setActiveFileTabId(id)
|
setActiveFileTabId(id)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
setSelectedPath(path)
|
setSelectedPath(path)
|
||||||
}, [fileTabs, dismissBrowserOverlay])
|
}, [fileTabs, dismissBrowserOverlay])
|
||||||
|
|
||||||
|
|
@ -2868,14 +2890,14 @@ function App() {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(true)
|
setIsGraphOpen(true)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isSuggestedTopicsTabPath(tab.path)) {
|
if (isSuggestedTopicsTabPath(tab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(true)
|
setIsSuggestedTopicsOpen(true)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isLiveNotesTabPath(tab.path)) {
|
if (isLiveNotesTabPath(tab.path)) {
|
||||||
|
|
@ -2887,6 +2909,7 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
setIsLiveNotesOpen(true)
|
setIsLiveNotesOpen(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -2899,6 +2922,7 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
setIsBgTasksOpen(true)
|
setIsBgTasksOpen(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -2912,6 +2936,7 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (isEmailTabPath(tab.path)) {
|
if (isEmailTabPath(tab.path)) {
|
||||||
|
|
@ -2923,6 +2948,7 @@ function App() {
|
||||||
setIsBgTasksOpen(false)
|
setIsBgTasksOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
setIsEmailOpen(true)
|
setIsEmailOpen(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -2935,6 +2961,7 @@ function App() {
|
||||||
setIsBgTasksOpen(false)
|
setIsBgTasksOpen(false)
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
setIsWorkspaceOpen(true)
|
setIsWorkspaceOpen(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -2947,18 +2974,32 @@ function App() {
|
||||||
setIsBgTasksOpen(false)
|
setIsBgTasksOpen(false)
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
setIsKnowledgeViewOpen(true)
|
setIsKnowledgeViewOpen(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (isChatHistoryTabPath(tab.path)) {
|
||||||
|
setSelectedPath(null)
|
||||||
|
setIsGraphOpen(false)
|
||||||
|
setIsSuggestedTopicsOpen(false)
|
||||||
|
setIsMeetingsOpen(false)
|
||||||
|
setIsLiveNotesOpen(false)
|
||||||
|
setIsBgTasksOpen(false)
|
||||||
|
setIsEmailOpen(false)
|
||||||
|
setIsWorkspaceOpen(false)
|
||||||
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
setSelectedPath(tab.path)
|
setSelectedPath(tab.path)
|
||||||
}, [fileTabs, isRightPaneMaximized, dismissBrowserOverlay])
|
}, [fileTabs, isRightPaneMaximized, dismissBrowserOverlay])
|
||||||
|
|
||||||
const closeFileTab = useCallback((tabId: string) => {
|
const closeFileTab = useCallback((tabId: string) => {
|
||||||
const closingTab = fileTabs.find(t => t.id === tabId)
|
const closingTab = fileTabs.find(t => t.id === tabId)
|
||||||
if (closingTab && !isGraphTabPath(closingTab.path) && !isSuggestedTopicsTabPath(closingTab.path) && !isLiveNotesTabPath(closingTab.path) && !isBgTasksTabPath(closingTab.path) && !isEmailTabPath(closingTab.path) && !isWorkspaceTabPath(closingTab.path) && !isKnowledgeViewTabPath(closingTab.path) && !isBaseFilePath(closingTab.path)) {
|
if (closingTab && !isGraphTabPath(closingTab.path) && !isSuggestedTopicsTabPath(closingTab.path) && !isLiveNotesTabPath(closingTab.path) && !isBgTasksTabPath(closingTab.path) && !isEmailTabPath(closingTab.path) && !isWorkspaceTabPath(closingTab.path) && !isKnowledgeViewTabPath(closingTab.path) && !isChatHistoryTabPath(closingTab.path) && !isBaseFilePath(closingTab.path)) {
|
||||||
removeEditorCacheForPath(closingTab.path)
|
removeEditorCacheForPath(closingTab.path)
|
||||||
initialContentByPathRef.current.delete(closingTab.path)
|
initialContentByPathRef.current.delete(closingTab.path)
|
||||||
untitledRenameReadyPathsRef.current.delete(closingTab.path)
|
untitledRenameReadyPathsRef.current.delete(closingTab.path)
|
||||||
|
|
@ -2981,7 +3022,7 @@ function App() {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
const idx = prev.findIndex(t => t.id === tabId)
|
const idx = prev.findIndex(t => t.id === tabId)
|
||||||
|
|
@ -2995,12 +3036,12 @@ function App() {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(true)
|
setIsGraphOpen(true)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
} else if (isSuggestedTopicsTabPath(newActiveTab.path)) {
|
} else if (isSuggestedTopicsTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(true)
|
setIsSuggestedTopicsOpen(true)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
} else if (isMeetingsTabPath(newActiveTab.path)) {
|
} else if (isMeetingsTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
|
|
@ -3011,6 +3052,7 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
} else if (isLiveNotesTabPath(newActiveTab.path)) {
|
} else if (isLiveNotesTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
|
|
@ -3020,6 +3062,7 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
setIsLiveNotesOpen(true)
|
setIsLiveNotesOpen(true)
|
||||||
} else if (isBgTasksTabPath(newActiveTab.path)) {
|
} else if (isBgTasksTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
|
|
@ -3031,6 +3074,7 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
} else if (isEmailTabPath(newActiveTab.path)) {
|
} else if (isEmailTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
|
|
@ -3040,6 +3084,7 @@ function App() {
|
||||||
setIsBgTasksOpen(false)
|
setIsBgTasksOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
setIsEmailOpen(true)
|
setIsEmailOpen(true)
|
||||||
} else if (isWorkspaceTabPath(newActiveTab.path)) {
|
} else if (isWorkspaceTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
|
|
@ -3050,6 +3095,7 @@ function App() {
|
||||||
setIsBgTasksOpen(false)
|
setIsBgTasksOpen(false)
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
setIsWorkspaceOpen(true)
|
setIsWorkspaceOpen(true)
|
||||||
} else if (isKnowledgeViewTabPath(newActiveTab.path)) {
|
} else if (isKnowledgeViewTabPath(newActiveTab.path)) {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
|
|
@ -3060,11 +3106,23 @@ function App() {
|
||||||
setIsBgTasksOpen(false)
|
setIsBgTasksOpen(false)
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
setIsKnowledgeViewOpen(true)
|
setIsKnowledgeViewOpen(true)
|
||||||
|
} else if (isChatHistoryTabPath(newActiveTab.path)) {
|
||||||
|
setSelectedPath(null)
|
||||||
|
setIsGraphOpen(false)
|
||||||
|
setIsSuggestedTopicsOpen(false)
|
||||||
|
setIsMeetingsOpen(false)
|
||||||
|
setIsLiveNotesOpen(false)
|
||||||
|
setIsBgTasksOpen(false)
|
||||||
|
setIsEmailOpen(false)
|
||||||
|
setIsWorkspaceOpen(false)
|
||||||
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(true)
|
||||||
} else {
|
} else {
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
setSelectedPath(newActiveTab.path)
|
setSelectedPath(newActiveTab.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3095,7 +3153,7 @@ function App() {
|
||||||
dismissBrowserOverlay()
|
dismissBrowserOverlay()
|
||||||
handleNewChat()
|
handleNewChat()
|
||||||
// Left-pane "new chat" should always open full chat view.
|
// Left-pane "new chat" should always open full chat view.
|
||||||
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen) {
|
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen) {
|
||||||
setExpandedFrom({
|
setExpandedFrom({
|
||||||
path: selectedPath,
|
path: selectedPath,
|
||||||
graph: isGraphOpen,
|
graph: isGraphOpen,
|
||||||
|
|
@ -3112,8 +3170,8 @@ function App() {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
}, [chatTabs, activeChatTabId, dismissBrowserOverlay, handleNewChat, selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen])
|
}, [chatTabs, activeChatTabId, dismissBrowserOverlay, handleNewChat, selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen])
|
||||||
|
|
||||||
// Sidebar variant: create/switch chat tab without leaving file/graph context.
|
// Sidebar variant: create/switch chat tab without leaving file/graph context.
|
||||||
const handleNewChatTabInSidebar = useCallback(() => {
|
const handleNewChatTabInSidebar = useCallback(() => {
|
||||||
|
|
@ -3245,7 +3303,7 @@ function App() {
|
||||||
|
|
||||||
const handleOpenFullScreenChat = useCallback(() => {
|
const handleOpenFullScreenChat = useCallback(() => {
|
||||||
// Remember where we came from so the close button can return
|
// Remember where we came from so the close button can return
|
||||||
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen) {
|
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen) {
|
||||||
setExpandedFrom({
|
setExpandedFrom({
|
||||||
path: selectedPath,
|
path: selectedPath,
|
||||||
graph: isGraphOpen,
|
graph: isGraphOpen,
|
||||||
|
|
@ -3261,19 +3319,19 @@ function App() {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
}, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, dismissBrowserOverlay])
|
}, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, dismissBrowserOverlay])
|
||||||
|
|
||||||
const handleCloseFullScreenChat = useCallback(() => {
|
const handleCloseFullScreenChat = useCallback(() => {
|
||||||
if (expandedFrom) {
|
if (expandedFrom) {
|
||||||
if (expandedFrom.graph) {
|
if (expandedFrom.graph) {
|
||||||
setIsGraphOpen(true)
|
setIsGraphOpen(true)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
} else if (expandedFrom.suggestedTopics) {
|
} else if (expandedFrom.suggestedTopics) {
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(true)
|
setIsSuggestedTopicsOpen(true)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
} else if (expandedFrom.meetings) {
|
} else if (expandedFrom.meetings) {
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
|
|
@ -3305,7 +3363,7 @@ function App() {
|
||||||
} else if (expandedFrom.path) {
|
} else if (expandedFrom.path) {
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
setSelectedPath(expandedFrom.path)
|
setSelectedPath(expandedFrom.path)
|
||||||
}
|
}
|
||||||
setExpandedFrom(null)
|
setExpandedFrom(null)
|
||||||
|
|
@ -3321,10 +3379,11 @@ function App() {
|
||||||
if (isSuggestedTopicsOpen) return { type: 'suggested-topics' }
|
if (isSuggestedTopicsOpen) return { type: 'suggested-topics' }
|
||||||
if (isWorkspaceOpen) return { type: 'workspace', path: workspaceInitialPath ?? undefined }
|
if (isWorkspaceOpen) return { type: 'workspace', path: workspaceInitialPath ?? undefined }
|
||||||
if (isKnowledgeViewOpen) return { type: 'knowledge-view' }
|
if (isKnowledgeViewOpen) return { type: 'knowledge-view' }
|
||||||
|
if (isChatHistoryOpen) return { type: 'chat-history' }
|
||||||
if (selectedPath) return { type: 'file', path: selectedPath }
|
if (selectedPath) return { type: 'file', path: selectedPath }
|
||||||
if (isGraphOpen) return { type: 'graph' }
|
if (isGraphOpen) return { type: 'graph' }
|
||||||
return { type: 'chat', runId }
|
return { type: 'chat', runId }
|
||||||
}, [selectedBackgroundTask, isEmailOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isSuggestedTopicsOpen, selectedPath, isGraphOpen, isWorkspaceOpen, isKnowledgeViewOpen, workspaceInitialPath, runId])
|
}, [selectedBackgroundTask, isEmailOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isSuggestedTopicsOpen, selectedPath, isGraphOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, workspaceInitialPath, runId])
|
||||||
|
|
||||||
const appendUnique = useCallback((stack: ViewState[], entry: ViewState) => {
|
const appendUnique = useCallback((stack: ViewState[], entry: ViewState) => {
|
||||||
const last = stack[stack.length - 1]
|
const last = stack[stack.length - 1]
|
||||||
|
|
@ -3447,6 +3506,17 @@ function App() {
|
||||||
setActiveFileTabId(id)
|
setActiveFileTabId(id)
|
||||||
}, [fileTabs])
|
}, [fileTabs])
|
||||||
|
|
||||||
|
const ensureChatHistoryFileTab = useCallback(() => {
|
||||||
|
const existing = fileTabs.find((tab) => isChatHistoryTabPath(tab.path))
|
||||||
|
if (existing) {
|
||||||
|
setActiveFileTabId(existing.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const id = newFileTabId()
|
||||||
|
setFileTabs((prev) => [...prev, { id, path: CHAT_HISTORY_TAB_PATH }])
|
||||||
|
setActiveFileTabId(id)
|
||||||
|
}, [fileTabs])
|
||||||
|
|
||||||
const openEmailView = useCallback(() => {
|
const openEmailView = useCallback(() => {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
|
|
@ -3469,7 +3539,7 @@ function App() {
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsBrowserOpen(false)
|
setIsBrowserOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
setSelectedBackgroundTask(null)
|
setSelectedBackgroundTask(null)
|
||||||
setExpandedFrom(null)
|
setExpandedFrom(null)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
|
|
@ -3503,7 +3573,7 @@ function App() {
|
||||||
// visible in the middle pane.
|
// visible in the middle pane.
|
||||||
setIsBrowserOpen(false)
|
setIsBrowserOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
setExpandedFrom(null)
|
setExpandedFrom(null)
|
||||||
// Preserve split vs knowledge-max mode when navigating knowledge files.
|
// Preserve split vs knowledge-max mode when navigating knowledge files.
|
||||||
// Only exit chat-only maximize, because that would hide the selected file.
|
// Only exit chat-only maximize, because that would hide the selected file.
|
||||||
|
|
@ -3518,7 +3588,7 @@ function App() {
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsBrowserOpen(false)
|
setIsBrowserOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
setExpandedFrom(null)
|
setExpandedFrom(null)
|
||||||
setIsGraphOpen(true)
|
setIsGraphOpen(true)
|
||||||
ensureGraphFileTab()
|
ensureGraphFileTab()
|
||||||
|
|
@ -3531,7 +3601,7 @@ function App() {
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
setIsBrowserOpen(false)
|
setIsBrowserOpen(false)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
setExpandedFrom(null)
|
setExpandedFrom(null)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
setSelectedBackgroundTask(view.name)
|
setSelectedBackgroundTask(view.name)
|
||||||
|
|
@ -3544,7 +3614,7 @@ function App() {
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
setSelectedBackgroundTask(null)
|
setSelectedBackgroundTask(null)
|
||||||
setIsSuggestedTopicsOpen(true)
|
setIsSuggestedTopicsOpen(true)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
ensureSuggestedTopicsFileTab()
|
ensureSuggestedTopicsFileTab()
|
||||||
return
|
return
|
||||||
case 'meetings':
|
case 'meetings':
|
||||||
|
|
@ -3561,6 +3631,7 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
ensureMeetingsFileTab()
|
ensureMeetingsFileTab()
|
||||||
return
|
return
|
||||||
case 'live-notes':
|
case 'live-notes':
|
||||||
|
|
@ -3576,6 +3647,7 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
setIsLiveNotesOpen(true)
|
setIsLiveNotesOpen(true)
|
||||||
ensureLiveNotesFileTab()
|
ensureLiveNotesFileTab()
|
||||||
return
|
return
|
||||||
|
|
@ -3593,6 +3665,7 @@ function App() {
|
||||||
setIsEmailOpen(true)
|
setIsEmailOpen(true)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
ensureEmailFileTab()
|
ensureEmailFileTab()
|
||||||
return
|
return
|
||||||
case 'workspace':
|
case 'workspace':
|
||||||
|
|
@ -3609,6 +3682,7 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(true)
|
setIsWorkspaceOpen(true)
|
||||||
setIsKnowledgeViewOpen(false)
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
setWorkspaceInitialPath(view.path ?? null)
|
setWorkspaceInitialPath(view.path ?? null)
|
||||||
ensureWorkspaceFileTab()
|
ensureWorkspaceFileTab()
|
||||||
return
|
return
|
||||||
|
|
@ -3626,8 +3700,26 @@ function App() {
|
||||||
setIsEmailOpen(false)
|
setIsEmailOpen(false)
|
||||||
setIsWorkspaceOpen(false)
|
setIsWorkspaceOpen(false)
|
||||||
setIsKnowledgeViewOpen(true)
|
setIsKnowledgeViewOpen(true)
|
||||||
|
setIsChatHistoryOpen(false)
|
||||||
ensureKnowledgeViewFileTab()
|
ensureKnowledgeViewFileTab()
|
||||||
return
|
return
|
||||||
|
case 'chat-history':
|
||||||
|
setSelectedPath(null)
|
||||||
|
setIsGraphOpen(false)
|
||||||
|
setIsBrowserOpen(false)
|
||||||
|
setExpandedFrom(null)
|
||||||
|
setIsRightPaneMaximized(false)
|
||||||
|
setSelectedBackgroundTask(null)
|
||||||
|
setIsSuggestedTopicsOpen(false)
|
||||||
|
setIsMeetingsOpen(false)
|
||||||
|
setIsLiveNotesOpen(false)
|
||||||
|
setIsBgTasksOpen(false)
|
||||||
|
setIsEmailOpen(false)
|
||||||
|
setIsWorkspaceOpen(false)
|
||||||
|
setIsKnowledgeViewOpen(false)
|
||||||
|
setIsChatHistoryOpen(true)
|
||||||
|
ensureChatHistoryFileTab()
|
||||||
|
return
|
||||||
case 'chat':
|
case 'chat':
|
||||||
setSelectedPath(null)
|
setSelectedPath(null)
|
||||||
setIsGraphOpen(false)
|
setIsGraphOpen(false)
|
||||||
|
|
@ -3636,7 +3728,7 @@ function App() {
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
setSelectedBackgroundTask(null)
|
setSelectedBackgroundTask(null)
|
||||||
setIsSuggestedTopicsOpen(false)
|
setIsSuggestedTopicsOpen(false)
|
||||||
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
|
setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false); setIsChatHistoryOpen(false)
|
||||||
if (view.runId) {
|
if (view.runId) {
|
||||||
await loadRun(view.runId)
|
await loadRun(view.runId)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3644,7 +3736,7 @@ function App() {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}, [ensureEmailFileTab, ensureMeetingsFileTab, ensureLiveNotesFileTab, ensureFileTabForPath, ensureGraphFileTab, ensureSuggestedTopicsFileTab, ensureWorkspaceFileTab, ensureKnowledgeViewFileTab, handleNewChat, isRightPaneMaximized, loadRun])
|
}, [ensureEmailFileTab, ensureMeetingsFileTab, ensureLiveNotesFileTab, ensureFileTabForPath, ensureGraphFileTab, ensureSuggestedTopicsFileTab, ensureWorkspaceFileTab, ensureKnowledgeViewFileTab, ensureChatHistoryFileTab, handleNewChat, isRightPaneMaximized, loadRun])
|
||||||
|
|
||||||
const navigateToView = useCallback(async (nextView: ViewState) => {
|
const navigateToView = useCallback(async (nextView: ViewState) => {
|
||||||
const current = currentViewState
|
const current = currentViewState
|
||||||
|
|
@ -3966,7 +4058,7 @@ function App() {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Keyboard shortcut: Ctrl+L to toggle main chat view
|
// Keyboard shortcut: Ctrl+L to toggle main chat view
|
||||||
const isFullScreenChat = !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedBackgroundTask && !isBrowserOpen
|
const isFullScreenChat = !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask && !isBrowserOpen
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'l') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'l') {
|
||||||
|
|
@ -4051,11 +4143,11 @@ function App() {
|
||||||
const handleTabKeyDown = (e: KeyboardEvent) => {
|
const handleTabKeyDown = (e: KeyboardEvent) => {
|
||||||
const mod = e.metaKey || e.ctrlKey
|
const mod = e.metaKey || e.ctrlKey
|
||||||
if (!mod) return
|
if (!mod) return
|
||||||
const rightPaneAvailable = Boolean((selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen) && isChatSidebarOpen)
|
const rightPaneAvailable = Boolean((selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen) && isChatSidebarOpen)
|
||||||
const targetPane: ShortcutPane = rightPaneAvailable
|
const targetPane: ShortcutPane = rightPaneAvailable
|
||||||
? (isRightPaneMaximized ? 'right' : activeShortcutPane)
|
? (isRightPaneMaximized ? 'right' : activeShortcutPane)
|
||||||
: 'left'
|
: 'left'
|
||||||
const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen)
|
const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen)
|
||||||
const selectedKnowledgePath = isGraphOpen
|
const selectedKnowledgePath = isGraphOpen
|
||||||
? GRAPH_TAB_PATH
|
? GRAPH_TAB_PATH
|
||||||
: isSuggestedTopicsOpen
|
: isSuggestedTopicsOpen
|
||||||
|
|
@ -4072,6 +4164,8 @@ function App() {
|
||||||
? WORKSPACE_TAB_PATH
|
? WORKSPACE_TAB_PATH
|
||||||
: isKnowledgeViewOpen
|
: isKnowledgeViewOpen
|
||||||
? KNOWLEDGE_VIEW_TAB_PATH
|
? KNOWLEDGE_VIEW_TAB_PATH
|
||||||
|
: isChatHistoryOpen
|
||||||
|
? CHAT_HISTORY_TAB_PATH
|
||||||
: selectedPath
|
: selectedPath
|
||||||
const targetFileTabId = activeFileTabId ?? (
|
const targetFileTabId = activeFileTabId ?? (
|
||||||
selectedKnowledgePath
|
selectedKnowledgePath
|
||||||
|
|
@ -4126,7 +4220,7 @@ function App() {
|
||||||
}
|
}
|
||||||
document.addEventListener('keydown', handleTabKeyDown)
|
document.addEventListener('keydown', handleTabKeyDown)
|
||||||
return () => document.removeEventListener('keydown', handleTabKeyDown)
|
return () => document.removeEventListener('keydown', handleTabKeyDown)
|
||||||
}, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab])
|
}, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatHistoryOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab])
|
||||||
|
|
||||||
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
|
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
|
||||||
if (kind === 'file') {
|
if (kind === 'file') {
|
||||||
|
|
@ -4151,7 +4245,7 @@ function App() {
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedBackgroundTask) {
|
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
|
||||||
setIsChatSidebarOpen(false)
|
setIsChatSidebarOpen(false)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
}
|
}
|
||||||
|
|
@ -4277,28 +4371,28 @@ function App() {
|
||||||
},
|
},
|
||||||
openGraph: () => {
|
openGraph: () => {
|
||||||
// From chat-only landing state, open graph directly in full knowledge view.
|
// From chat-only landing state, open graph directly in full knowledge view.
|
||||||
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedBackgroundTask) {
|
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
|
||||||
setIsChatSidebarOpen(false)
|
setIsChatSidebarOpen(false)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
}
|
}
|
||||||
void navigateToView({ type: 'graph' })
|
void navigateToView({ type: 'graph' })
|
||||||
},
|
},
|
||||||
openBases: () => {
|
openBases: () => {
|
||||||
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedBackgroundTask) {
|
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
|
||||||
setIsChatSidebarOpen(false)
|
setIsChatSidebarOpen(false)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
}
|
}
|
||||||
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
|
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
|
||||||
},
|
},
|
||||||
openWorkspaceAt: (path?: string) => {
|
openWorkspaceAt: (path?: string) => {
|
||||||
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedBackgroundTask) {
|
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
|
||||||
setIsChatSidebarOpen(false)
|
setIsChatSidebarOpen(false)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
}
|
}
|
||||||
void navigateToView({ type: 'workspace', path })
|
void navigateToView({ type: 'workspace', path })
|
||||||
},
|
},
|
||||||
openKnowledgeView: () => {
|
openKnowledgeView: () => {
|
||||||
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedBackgroundTask) {
|
if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedBackgroundTask) {
|
||||||
setIsChatSidebarOpen(false)
|
setIsChatSidebarOpen(false)
|
||||||
setIsRightPaneMaximized(false)
|
setIsRightPaneMaximized(false)
|
||||||
}
|
}
|
||||||
|
|
@ -4471,12 +4565,14 @@ function App() {
|
||||||
const pendingCalendarEventRef = useRef<CalendarEventMeta | undefined>(undefined)
|
const pendingCalendarEventRef = useRef<CalendarEventMeta | undefined>(undefined)
|
||||||
const [meetingSummarizing, setMeetingSummarizing] = useState(false)
|
const [meetingSummarizing, setMeetingSummarizing] = useState(false)
|
||||||
const [showMeetingPermissions, setShowMeetingPermissions] = useState(false)
|
const [showMeetingPermissions, setShowMeetingPermissions] = useState(false)
|
||||||
|
const [recordingMeetingSource, setRecordingMeetingSource] = useState<string | null>(null)
|
||||||
|
|
||||||
const [checkingPermission, setCheckingPermission] = useState(false)
|
const [checkingPermission, setCheckingPermission] = useState(false)
|
||||||
|
|
||||||
const startMeetingNow = useCallback(async () => {
|
const startMeetingNow = useCallback(async () => {
|
||||||
const calEvent = pendingCalendarEventRef.current
|
const calEvent = pendingCalendarEventRef.current
|
||||||
pendingCalendarEventRef.current = undefined
|
pendingCalendarEventRef.current = undefined
|
||||||
|
setRecordingMeetingSource(calEvent?.source ?? null)
|
||||||
const notePath = await meetingTranscription.start(calEvent)
|
const notePath = await meetingTranscription.start(calEvent)
|
||||||
if (notePath) {
|
if (notePath) {
|
||||||
meetingNotePathRef.current = notePath
|
meetingNotePathRef.current = notePath
|
||||||
|
|
@ -4504,6 +4600,7 @@ function App() {
|
||||||
const handleToggleMeeting = useCallback(async () => {
|
const handleToggleMeeting = useCallback(async () => {
|
||||||
if (meetingTranscription.state === 'recording') {
|
if (meetingTranscription.state === 'recording') {
|
||||||
await meetingTranscription.stop()
|
await meetingTranscription.stop()
|
||||||
|
setRecordingMeetingSource(null)
|
||||||
|
|
||||||
// Read the final transcript and generate meeting notes via LLM
|
// Read the final transcript and generate meeting notes via LLM
|
||||||
const notePath = meetingNotePathRef.current
|
const notePath = meetingNotePathRef.current
|
||||||
|
|
@ -4913,7 +5010,7 @@ function App() {
|
||||||
const selectedTask = selectedBackgroundTask
|
const selectedTask = selectedBackgroundTask
|
||||||
? backgroundTasks.find(t => t.name === selectedBackgroundTask)
|
? backgroundTasks.find(t => t.name === selectedBackgroundTask)
|
||||||
: null
|
: null
|
||||||
const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isBrowserOpen)
|
const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isBrowserOpen)
|
||||||
const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized
|
const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized
|
||||||
const shouldCollapseLeftPane = isRightPaneOnlyMode
|
const shouldCollapseLeftPane = isRightPaneOnlyMode
|
||||||
const openMarkdownTabs = React.useMemo(() => {
|
const openMarkdownTabs = React.useMemo(() => {
|
||||||
|
|
@ -4930,7 +5027,7 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<SidebarSectionProvider defaultSection="tasks" onSectionChange={(section) => {
|
<SidebarSectionProvider defaultSection="tasks" onSectionChange={(section) => {
|
||||||
if (section === 'knowledge' && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen) {
|
if (section === 'knowledge' && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen) {
|
||||||
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
|
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
|
|
@ -4953,7 +5050,7 @@ function App() {
|
||||||
onNewChat: handleNewChatTab,
|
onNewChat: handleNewChatTab,
|
||||||
onSelectRun: (runIdToLoad) => {
|
onSelectRun: (runIdToLoad) => {
|
||||||
cancelRecordingIfActive()
|
cancelRecordingIfActive()
|
||||||
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isBrowserOpen) {
|
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isBrowserOpen) {
|
||||||
setIsChatSidebarOpen(true)
|
setIsChatSidebarOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4964,7 +5061,7 @@ function App() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// In two-pane mode (file/graph/browser), keep the middle pane and just swap chat context in the right sidebar.
|
// In two-pane mode (file/graph/browser), keep the middle pane and just swap chat context in the right sidebar.
|
||||||
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isBrowserOpen) {
|
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isBrowserOpen) {
|
||||||
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: runIdToLoad } : t))
|
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: runIdToLoad } : t))
|
||||||
loadRun(runIdToLoad)
|
loadRun(runIdToLoad)
|
||||||
return
|
return
|
||||||
|
|
@ -4988,14 +5085,14 @@ function App() {
|
||||||
} else {
|
} else {
|
||||||
// Only one tab, reset it to new chat
|
// Only one tab, reset it to new chat
|
||||||
setChatTabs([{ id: tabForRun.id, runId: null }])
|
setChatTabs([{ id: tabForRun.id, runId: null }])
|
||||||
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isBrowserOpen) {
|
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isBrowserOpen) {
|
||||||
handleNewChat()
|
handleNewChat()
|
||||||
} else {
|
} else {
|
||||||
void navigateToView({ type: 'chat', runId: null })
|
void navigateToView({ type: 'chat', runId: null })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (runId === runIdToDelete) {
|
} else if (runId === runIdToDelete) {
|
||||||
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isBrowserOpen) {
|
if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isBrowserOpen) {
|
||||||
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: null } : t))
|
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: null } : t))
|
||||||
handleNewChat()
|
handleNewChat()
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -5010,20 +5107,30 @@ function App() {
|
||||||
onSelectBackgroundTask: (taskName) => {
|
onSelectBackgroundTask: (taskName) => {
|
||||||
void navigateToView({ type: 'task', name: taskName })
|
void navigateToView({ type: 'task', name: taskName })
|
||||||
},
|
},
|
||||||
|
onOpenChatHistoryView: () => {
|
||||||
|
void navigateToView({ type: 'chat-history' })
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
bgTaskSummaries={bgTaskSummaries}
|
||||||
|
onOpenBgTask={(slug) => {
|
||||||
|
setBgTaskInitialSlug(slug)
|
||||||
|
setBgTaskSlugVersion((v) => v + 1)
|
||||||
|
openBgTasksView()
|
||||||
}}
|
}}
|
||||||
backgroundTasks={backgroundTasks}
|
|
||||||
selectedBackgroundTask={selectedBackgroundTask}
|
|
||||||
isSearchOpen={isSearchOpen}
|
|
||||||
isBrowserOpen={isBrowserOpen}
|
|
||||||
onToggleBrowser={handleToggleBrowser}
|
|
||||||
isSuggestedTopicsOpen={isSuggestedTopicsOpen}
|
|
||||||
onOpenSuggestedTopics={() => void navigateToView({ type: 'suggested-topics' })}
|
|
||||||
isMeetingsOpen={isMeetingsOpen}
|
|
||||||
onOpenMeetings={openMeetingsView}
|
onOpenMeetings={openMeetingsView}
|
||||||
isBgTasksOpen={isBgTasksOpen}
|
meetingRecordingState={meetingTranscription.state}
|
||||||
onOpenBgTasks={openBgTasksView}
|
recordingMeetingSource={recordingMeetingSource}
|
||||||
isEmailOpen={isEmailOpen}
|
onToggleMeetingRecording={() => { void handleToggleMeeting() }}
|
||||||
onOpenEmail={openEmailView}
|
onOpenBgTasks={() => { setBgTaskInitialSlug(null); setBgTaskSlugVersion((v) => v + 1); openBgTasksView() }}
|
||||||
|
onOpenEmail={(threadId) => {
|
||||||
|
setEmailInitialThreadId(threadId ?? null)
|
||||||
|
setEmailThreadIdVersion((v) => v + 1)
|
||||||
|
openEmailView()
|
||||||
|
}}
|
||||||
|
onOpenHome={navigateToFullScreenChat}
|
||||||
|
onNewChat={handleNewChatTab}
|
||||||
|
onOpenSearch={() => setIsSearchOpen(true)}
|
||||||
|
onToggleBrowser={handleToggleBrowser}
|
||||||
/>
|
/>
|
||||||
<SidebarInset
|
<SidebarInset
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -5043,7 +5150,7 @@ function App() {
|
||||||
canNavigateForward={canNavigateForward}
|
canNavigateForward={canNavigateForward}
|
||||||
collapsedLeftPaddingPx={collapsedLeftPaddingPx}
|
collapsedLeftPaddingPx={collapsedLeftPaddingPx}
|
||||||
>
|
>
|
||||||
{(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen) && fileTabs.length >= 1 ? (
|
{(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen) && fileTabs.length >= 1 ? (
|
||||||
<TabBar
|
<TabBar
|
||||||
tabs={fileTabs}
|
tabs={fileTabs}
|
||||||
activeTabId={activeFileTabId ?? ''}
|
activeTabId={activeFileTabId ?? ''}
|
||||||
|
|
@ -5051,7 +5158,7 @@ function App() {
|
||||||
getTabId={(t) => t.id}
|
getTabId={(t) => t.id}
|
||||||
onSwitchTab={switchFileTab}
|
onSwitchTab={switchFileTab}
|
||||||
onCloseTab={closeFileTab}
|
onCloseTab={closeFileTab}
|
||||||
allowSingleTabClose={fileTabs.length === 1 && (isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || (selectedPath != null && isBaseFilePath(selectedPath)))}
|
allowSingleTabClose={fileTabs.length === 1 && (isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || (selectedPath != null && isBaseFilePath(selectedPath)))}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TabBar
|
<TabBar
|
||||||
|
|
@ -5104,7 +5211,7 @@ function App() {
|
||||||
<TooltipContent side="bottom">Version history</TooltipContent>
|
<TooltipContent side="bottom">Version history</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedTask && !isBrowserOpen && (
|
{!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !selectedTask && !isBrowserOpen && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
|
|
@ -5119,7 +5226,7 @@ function App() {
|
||||||
<TooltipContent side="bottom">New chat tab</TooltipContent>
|
<TooltipContent side="bottom">New chat tab</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isBrowserOpen && expandedFrom && (
|
{!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isChatHistoryOpen && !isBrowserOpen && expandedFrom && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
|
|
@ -5134,7 +5241,7 @@ function App() {
|
||||||
<TooltipContent side="bottom">Restore two-pane view</TooltipContent>
|
<TooltipContent side="bottom">Restore two-pane view</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen) && (
|
{(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen) && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
|
|
@ -5188,6 +5295,8 @@ function App() {
|
||||||
) : isBgTasksOpen ? (
|
) : isBgTasksOpen ? (
|
||||||
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||||
<BgTasksView
|
<BgTasksView
|
||||||
|
initialSlug={bgTaskInitialSlug}
|
||||||
|
slugVersion={bgTaskSlugVersion}
|
||||||
onCreateWithCopilot={(description) => {
|
onCreateWithCopilot={(description) => {
|
||||||
submitFromPalette(buildBgTaskSetupPrompt(description), null)
|
submitFromPalette(buildBgTaskSetupPrompt(description), null)
|
||||||
}}
|
}}
|
||||||
|
|
@ -5198,7 +5307,7 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
) : isEmailOpen ? (
|
) : isEmailOpen ? (
|
||||||
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||||
<EmailView />
|
<EmailView initialThreadId={emailInitialThreadId} threadIdVersion={emailThreadIdVersion} />
|
||||||
</div>
|
</div>
|
||||||
) : isWorkspaceOpen ? (
|
) : isWorkspaceOpen ? (
|
||||||
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||||
|
|
@ -5229,6 +5338,26 @@ function App() {
|
||||||
onVoiceNoteCreated={handleVoiceNoteCreated}
|
onVoiceNoteCreated={handleVoiceNoteCreated}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : isChatHistoryOpen ? (
|
||||||
|
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||||
|
<ChatHistoryView
|
||||||
|
runs={runs}
|
||||||
|
currentRunId={runId}
|
||||||
|
processingRunIds={processingRunIds}
|
||||||
|
onSelectRun={(rid) => void navigateToView({ type: 'chat', runId: rid })}
|
||||||
|
onOpenInNewTab={(rid) => openChatInNewTab(rid)}
|
||||||
|
onDeleteRun={async (rid) => {
|
||||||
|
try {
|
||||||
|
await window.ipc.invoke('runs:delete', { runId: rid })
|
||||||
|
await loadRuns()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to delete run:', err)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onNewChat={handleNewChatTab}
|
||||||
|
onOpenSearch={() => setIsSearchOpen(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : selectedPath && isBaseFilePath(selectedPath) ? (
|
) : selectedPath && isBaseFilePath(selectedPath) ? (
|
||||||
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
||||||
<BasesView
|
<BasesView
|
||||||
|
|
@ -5651,8 +5780,6 @@ function App() {
|
||||||
{/* Rendered last so its no-drag region paints over the sidebar drag region */}
|
{/* Rendered last so its no-drag region paints over the sidebar drag region */}
|
||||||
<FixedSidebarToggle
|
<FixedSidebarToggle
|
||||||
leftInsetPx={isMac ? MACOS_TRAFFIC_LIGHTS_RESERVED_PX : 0}
|
leftInsetPx={isMac ? MACOS_TRAFFIC_LIGHTS_RESERVED_PX : 0}
|
||||||
onNewChat={handleNewChatTab}
|
|
||||||
onOpenSearch={() => setIsSearchOpen(true)}
|
|
||||||
/>
|
/>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1418,6 +1418,18 @@ export interface BgTasksViewProps {
|
||||||
* "Edit with Copilot" button in the detail-view sidebar footer.
|
* "Edit with Copilot" button in the detail-view sidebar footer.
|
||||||
*/
|
*/
|
||||||
onEditWithCopilot?: (slug: string) => void
|
onEditWithCopilot?: (slug: string) => void
|
||||||
|
/**
|
||||||
|
* If provided, the view opens with this task already selected. Updates to
|
||||||
|
* this prop sync into internal state so the sidebar can swap which task is
|
||||||
|
* focused without remounting the view.
|
||||||
|
*/
|
||||||
|
initialSlug?: string | null
|
||||||
|
/**
|
||||||
|
* Bump this counter to force a re-focus on `initialSlug` even when the
|
||||||
|
* slug value itself didn't change (e.g. user clicks the same task in the
|
||||||
|
* sidebar twice after navigating away inside the view).
|
||||||
|
*/
|
||||||
|
slugVersion?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLastRanLabel(iso: string | null | undefined): string {
|
function formatLastRanLabel(iso: string | null | undefined): string {
|
||||||
|
|
@ -1425,9 +1437,12 @@ function formatLastRanLabel(iso: string | null | undefined): string {
|
||||||
return formatRelativeTime(iso) || 'Never'
|
return formatRelativeTime(iso) || 'Never'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BgTasksView({ onCreateWithCopilot, onEditWithCopilot }: BgTasksViewProps = {}) {
|
export function BgTasksView({ onCreateWithCopilot, onEditWithCopilot, initialSlug, slugVersion }: BgTasksViewProps = {}) {
|
||||||
const [items, setItems] = useState<BackgroundTaskSummary[]>([])
|
const [items, setItems] = useState<BackgroundTaskSummary[]>([])
|
||||||
const [selectedSlug, setSelectedSlug] = useState<string | null>(null)
|
const [selectedSlug, setSelectedSlug] = useState<string | null>(initialSlug ?? null)
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedSlug(initialSlug ?? null)
|
||||||
|
}, [initialSlug, slugVersion])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [showNewDialog, setShowNewDialog] = useState(false)
|
const [showNewDialog, setShowNewDialog] = useState(false)
|
||||||
|
|
|
||||||
177
apps/x/apps/renderer/src/components/chat-history-view.tsx
Normal file
177
apps/x/apps/renderer/src/components/chat-history-view.tsx
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
|
import { ExternalLink, MessageSquare, SearchIcon, SquarePen, Trash2 } from 'lucide-react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
ContextMenu,
|
||||||
|
ContextMenuContent,
|
||||||
|
ContextMenuItem,
|
||||||
|
ContextMenuSeparator,
|
||||||
|
ContextMenuTrigger,
|
||||||
|
} from '@/components/ui/context-menu'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { formatRelativeTime } from '@/lib/relative-time'
|
||||||
|
|
||||||
|
type Run = {
|
||||||
|
id: string
|
||||||
|
title?: string
|
||||||
|
createdAt: string
|
||||||
|
agentId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatHistoryViewProps = {
|
||||||
|
runs: Run[]
|
||||||
|
currentRunId?: string | null
|
||||||
|
processingRunIds?: Set<string>
|
||||||
|
onSelectRun: (runId: string) => void
|
||||||
|
onOpenInNewTab?: (runId: string) => void
|
||||||
|
onDeleteRun: (runId: string) => Promise<void> | void
|
||||||
|
onNewChat?: () => void
|
||||||
|
onOpenSearch?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChatHistoryView({
|
||||||
|
runs,
|
||||||
|
currentRunId,
|
||||||
|
processingRunIds,
|
||||||
|
onSelectRun,
|
||||||
|
onOpenInNewTab,
|
||||||
|
onDeleteRun,
|
||||||
|
onNewChat,
|
||||||
|
onOpenSearch,
|
||||||
|
}: ChatHistoryViewProps) {
|
||||||
|
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const sortedRuns = useMemo(() => {
|
||||||
|
return [...runs].sort((a, b) => {
|
||||||
|
const at = new Date(a.createdAt).getTime()
|
||||||
|
const bt = new Date(b.createdAt).getTime()
|
||||||
|
return (Number.isNaN(bt) ? 0 : bt) - (Number.isNaN(at) ? 0 : at)
|
||||||
|
})
|
||||||
|
}, [runs])
|
||||||
|
|
||||||
|
const handleConfirmDelete = useCallback(async () => {
|
||||||
|
if (!pendingDeleteId) return
|
||||||
|
const id = pendingDeleteId
|
||||||
|
setPendingDeleteId(null)
|
||||||
|
await onDeleteRun(id)
|
||||||
|
}, [pendingDeleteId, onDeleteRun])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col overflow-hidden">
|
||||||
|
<div className="shrink-0 flex items-center justify-between gap-3 border-b border-border px-8 py-6">
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight">Chat history</h1>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{onOpenSearch && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onOpenSearch}
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3 py-1.5 text-sm text-foreground transition-colors hover:bg-accent"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-4" />
|
||||||
|
<span>Search</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onNewChat && (
|
||||||
|
<Button size="sm" onClick={onNewChat}>
|
||||||
|
<SquarePen className="size-4" />
|
||||||
|
New chat
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
<div className="min-w-[480px]">
|
||||||
|
<div className="sticky top-0 z-10 flex items-center border-b border-border bg-background px-6 py-2 text-xs font-medium text-muted-foreground">
|
||||||
|
<div className="flex-1">Title</div>
|
||||||
|
<div className="w-32 shrink-0">Created</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{sortedRuns.length === 0 ? (
|
||||||
|
<div className="px-6 py-8 text-sm text-muted-foreground">No chats yet.</div>
|
||||||
|
) : (
|
||||||
|
sortedRuns.map((run) => {
|
||||||
|
const isActive = currentRunId === run.id
|
||||||
|
const isProcessing = processingRunIds?.has(run.id)
|
||||||
|
return (
|
||||||
|
<ContextMenu key={run.id}>
|
||||||
|
<ContextMenuTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.metaKey && onOpenInNewTab) {
|
||||||
|
onOpenInNewTab(run.id)
|
||||||
|
} else {
|
||||||
|
onSelectRun(run.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={[
|
||||||
|
'flex w-full items-center border-b border-border/60 px-6 py-1.5 text-left text-sm transition-colors hover:bg-accent',
|
||||||
|
isActive ? 'bg-accent/60' : '',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
<div className="flex flex-1 items-center gap-2 min-w-0">
|
||||||
|
<MessageSquare className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="min-w-0 truncate">{run.title || '(Untitled chat)'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-32 shrink-0 text-xs text-muted-foreground tabular-nums">
|
||||||
|
{formatRelativeTime(run.createdAt)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</ContextMenuTrigger>
|
||||||
|
<ContextMenuContent className="w-48">
|
||||||
|
{onOpenInNewTab && (
|
||||||
|
<>
|
||||||
|
<ContextMenuItem onClick={() => onOpenInNewTab(run.id)}>
|
||||||
|
<ExternalLink className="mr-2 size-4" />
|
||||||
|
Open in new tab
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuSeparator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isProcessing && (
|
||||||
|
<ContextMenuItem
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => setPendingDeleteId(run.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-2 size-4" />
|
||||||
|
Delete
|
||||||
|
</ContextMenuItem>
|
||||||
|
)}
|
||||||
|
</ContextMenuContent>
|
||||||
|
</ContextMenu>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={!!pendingDeleteId} onOpenChange={(open) => { if (!open) setPendingDeleteId(null) }}>
|
||||||
|
<DialogContent showCloseButton={false} className="sm:max-w-sm">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Delete chat</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to delete this chat?
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setPendingDeleteId(null)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={() => void handleConfirmDelete()}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -817,12 +817,28 @@ function clearLoadingFlag(state: SectionState | null): SectionState {
|
||||||
return { ...state, loadingPage: false }
|
return { ...state, loadingPage: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmailView() {
|
export type EmailViewProps = {
|
||||||
|
/** If provided, the view opens with this thread already expanded. */
|
||||||
|
initialThreadId?: string | null
|
||||||
|
/** Bump to re-focus on the same threadId after navigating away inside the view. */
|
||||||
|
threadIdVersion?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmailView({ initialThreadId, threadIdVersion }: EmailViewProps = {}) {
|
||||||
const [important, setImportant] = useState<SectionState>(() => clearLoadingFlag(persistedImportant))
|
const [important, setImportant] = useState<SectionState>(() => clearLoadingFlag(persistedImportant))
|
||||||
const [other, setOther] = useState<SectionState>(() => clearLoadingFlag(persistedOther))
|
const [other, setOther] = useState<SectionState>(() => clearLoadingFlag(persistedOther))
|
||||||
const hadPersistedDataOnMount = useRef(persistedImportant !== null)
|
const hadPersistedDataOnMount = useRef(persistedImportant !== null)
|
||||||
const [selectedThreadId, setSelectedThreadId] = useState<string | null>(null)
|
const [selectedThreadId, setSelectedThreadId] = useState<string | null>(initialThreadId ?? null)
|
||||||
const [openedThreadIds, setOpenedThreadIds] = useState<string[]>([])
|
const [openedThreadIds, setOpenedThreadIds] = useState<string[]>(initialThreadId ? [initialThreadId] : [])
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedThreadId(initialThreadId ?? null)
|
||||||
|
if (initialThreadId) {
|
||||||
|
setOpenedThreadIds((prev) => {
|
||||||
|
const without = prev.filter((id) => id !== initialThreadId)
|
||||||
|
return [...without, initialThreadId].slice(-MAX_KEPT_OPEN)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [initialThreadId, threadIdVersion])
|
||||||
const [refreshing, setRefreshing] = useState(!hadPersistedDataOnMount.current)
|
const [refreshing, setRefreshing] = useState(!hadPersistedDataOnMount.current)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ export function KnowledgeView({
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col overflow-hidden">
|
<div className="flex h-full flex-col overflow-hidden">
|
||||||
<div className="shrink-0 flex items-center justify-between gap-3 border-b border-border px-8 py-6">
|
<div className="shrink-0 flex items-center justify-between gap-3 border-b border-border px-8 py-6">
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Knowledge</h1>
|
<h1 className="text-2xl font-bold tracking-tight">Notes</h1>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,16 @@ import {
|
||||||
Globe,
|
Globe,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
|
Home,
|
||||||
Mic,
|
Mic,
|
||||||
|
SearchIcon,
|
||||||
|
SquarePen,
|
||||||
Plug,
|
Plug,
|
||||||
Lightbulb,
|
Plus,
|
||||||
ListChecks,
|
Video,
|
||||||
LoaderIcon,
|
LoaderIcon,
|
||||||
Mail,
|
Mail,
|
||||||
|
MessageSquare,
|
||||||
Settings,
|
Settings,
|
||||||
Square,
|
Square,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
|
@ -80,6 +84,7 @@ import { HelpPopover } from "@/components/help-popover"
|
||||||
import { SettingsDialog } from "@/components/settings-dialog"
|
import { SettingsDialog } from "@/components/settings-dialog"
|
||||||
import { toast } from "@/lib/toast"
|
import { toast } from "@/lib/toast"
|
||||||
import { formatRelativeTime as formatRunTime } from "@/lib/relative-time"
|
import { formatRelativeTime as formatRunTime } from "@/lib/relative-time"
|
||||||
|
import { extractConferenceLink } from "@/lib/calendar-event"
|
||||||
import { useBilling } from "@/hooks/useBilling"
|
import { useBilling } from "@/hooks/useBilling"
|
||||||
import { ServiceEvent } from "@x/shared/src/service-events.js"
|
import { ServiceEvent } from "@x/shared/src/service-events.js"
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
|
@ -124,21 +129,13 @@ type RunListItem = {
|
||||||
agentId: string
|
agentId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type BackgroundTaskItem = {
|
type TaskSummary = {
|
||||||
|
slug: string
|
||||||
name: string
|
name: string
|
||||||
description?: string
|
active: boolean
|
||||||
schedule: {
|
createdAt: string
|
||||||
type: "cron" | "window" | "once"
|
lastAttemptAt?: string
|
||||||
expression?: string
|
lastRunAt?: string
|
||||||
cron?: string
|
|
||||||
startTime?: string
|
|
||||||
endTime?: string
|
|
||||||
runAt?: string
|
|
||||||
}
|
|
||||||
enabled: boolean
|
|
||||||
status?: "scheduled" | "running" | "finished" | "failed" | "triggered"
|
|
||||||
nextRunAt?: string | null
|
|
||||||
lastRunAt?: string | null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServiceEventType = z.infer<typeof ServiceEvent>
|
type ServiceEventType = z.infer<typeof ServiceEvent>
|
||||||
|
|
@ -183,6 +180,7 @@ type TasksActions = {
|
||||||
onDeleteRun: (runId: string) => void
|
onDeleteRun: (runId: string) => void
|
||||||
onOpenInNewTab?: (runId: string) => void
|
onOpenInNewTab?: (runId: string) => void
|
||||||
onSelectBackgroundTask?: (taskName: string) => void
|
onSelectBackgroundTask?: (taskName: string) => void
|
||||||
|
onOpenChatHistoryView?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type SidebarContentPanelProps = {
|
type SidebarContentPanelProps = {
|
||||||
|
|
@ -194,19 +192,18 @@ type SidebarContentPanelProps = {
|
||||||
currentRunId?: string | null
|
currentRunId?: string | null
|
||||||
processingRunIds?: Set<string>
|
processingRunIds?: Set<string>
|
||||||
tasksActions?: TasksActions
|
tasksActions?: TasksActions
|
||||||
backgroundTasks?: BackgroundTaskItem[]
|
bgTaskSummaries?: TaskSummary[]
|
||||||
selectedBackgroundTask?: string | null
|
onOpenBgTask?: (slug: string) => void
|
||||||
isSearchOpen?: boolean
|
|
||||||
isBrowserOpen?: boolean
|
|
||||||
onToggleBrowser?: () => void
|
|
||||||
isSuggestedTopicsOpen?: boolean
|
|
||||||
onOpenSuggestedTopics?: () => void
|
|
||||||
isMeetingsOpen?: boolean
|
|
||||||
onOpenMeetings?: () => void
|
onOpenMeetings?: () => void
|
||||||
isBgTasksOpen?: boolean
|
meetingRecordingState?: 'idle' | 'connecting' | 'recording' | 'stopping'
|
||||||
|
recordingMeetingSource?: string | null
|
||||||
|
onToggleMeetingRecording?: () => void
|
||||||
onOpenBgTasks?: () => void
|
onOpenBgTasks?: () => void
|
||||||
isEmailOpen?: boolean
|
onOpenEmail?: (threadId?: string) => void
|
||||||
onOpenEmail?: () => void
|
onOpenHome?: () => void
|
||||||
|
onNewChat?: () => void
|
||||||
|
onOpenSearch?: () => void
|
||||||
|
onToggleBrowser?: () => void
|
||||||
} & React.ComponentProps<typeof Sidebar>
|
} & React.ComponentProps<typeof Sidebar>
|
||||||
|
|
||||||
function formatEventTime(ts: string): string {
|
function formatEventTime(ts: string): string {
|
||||||
|
|
@ -443,19 +440,18 @@ export function SidebarContentPanel({
|
||||||
currentRunId,
|
currentRunId,
|
||||||
processingRunIds,
|
processingRunIds,
|
||||||
tasksActions,
|
tasksActions,
|
||||||
backgroundTasks = [],
|
bgTaskSummaries = [],
|
||||||
selectedBackgroundTask,
|
onOpenBgTask,
|
||||||
isSearchOpen = false,
|
|
||||||
isBrowserOpen = false,
|
|
||||||
onToggleBrowser,
|
|
||||||
isSuggestedTopicsOpen = false,
|
|
||||||
onOpenSuggestedTopics,
|
|
||||||
isMeetingsOpen = false,
|
|
||||||
onOpenMeetings,
|
onOpenMeetings,
|
||||||
isBgTasksOpen = false,
|
meetingRecordingState,
|
||||||
|
recordingMeetingSource,
|
||||||
|
onToggleMeetingRecording,
|
||||||
onOpenBgTasks,
|
onOpenBgTasks,
|
||||||
isEmailOpen = false,
|
|
||||||
onOpenEmail,
|
onOpenEmail,
|
||||||
|
onOpenHome,
|
||||||
|
onNewChat,
|
||||||
|
onOpenSearch,
|
||||||
|
onToggleBrowser,
|
||||||
...props
|
...props
|
||||||
}: SidebarContentPanelProps) {
|
}: SidebarContentPanelProps) {
|
||||||
const [hasOauthError, setHasOauthError] = useState(false)
|
const [hasOauthError, setHasOauthError] = useState(false)
|
||||||
|
|
@ -467,11 +463,6 @@ export function SidebarContentPanel({
|
||||||
const [loggingIn, setLoggingIn] = useState(false)
|
const [loggingIn, setLoggingIn] = useState(false)
|
||||||
const [appUrl, setAppUrl] = useState<string | null>(null)
|
const [appUrl, setAppUrl] = useState<string | null>(null)
|
||||||
const { billing } = useBilling(isRowboatConnected)
|
const { billing } = useBilling(isRowboatConnected)
|
||||||
const isBrowserQuickActionSelected = isBrowserOpen && !isSearchOpen
|
|
||||||
const isSuggestedTopicsQuickActionSelected = isSuggestedTopicsOpen && !isBrowserOpen
|
|
||||||
const isMeetingsQuickActionSelected = isMeetingsOpen && !isBrowserOpen
|
|
||||||
const isBgTasksQuickActionSelected = isBgTasksOpen && !isBrowserOpen
|
|
||||||
const isEmailQuickActionSelected = isEmailOpen && !isBrowserOpen
|
|
||||||
|
|
||||||
const handleRowboatLogin = useCallback(async () => {
|
const handleRowboatLogin = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -534,56 +525,70 @@ export function SidebarContentPanel({
|
||||||
<SidebarHeader className="titlebar-drag-region">
|
<SidebarHeader className="titlebar-drag-region">
|
||||||
{/* Top spacer to clear the traffic lights + fixed toggle row */}
|
{/* Top spacer to clear the traffic lights + fixed toggle row */}
|
||||||
<div className="h-8" />
|
<div className="h-8" />
|
||||||
{/* Quick action buttons */}
|
<div className="titlebar-no-drag flex items-center gap-1 px-2 pb-1">
|
||||||
<div className="rowboat-quick-actions titlebar-no-drag flex flex-col gap-0.5 px-2 pb-1">
|
{onOpenHome && (
|
||||||
{onOpenEmail && (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onOpenEmail}
|
onClick={onOpenHome}
|
||||||
className={cn(
|
className="flex size-8 items-center justify-center rounded-md text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors"
|
||||||
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors",
|
aria-label="Home"
|
||||||
isEmailQuickActionSelected
|
title="Home"
|
||||||
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
|
||||||
: "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Mail className="size-4" />
|
<Home className="size-4" />
|
||||||
<span>Email</span>
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{onOpenMeetings && (
|
{onNewChat && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onOpenMeetings}
|
onClick={onNewChat}
|
||||||
className={cn(
|
className="flex size-8 items-center justify-center rounded-md text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors"
|
||||||
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors",
|
aria-label="New chat"
|
||||||
isMeetingsQuickActionSelected
|
title="New chat"
|
||||||
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
|
||||||
: "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Mic className="size-4" />
|
<SquarePen className="size-4" />
|
||||||
<span>Meetings</span>
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{onOpenBgTasks && (
|
{onToggleBrowser && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onOpenBgTasks}
|
onClick={onToggleBrowser}
|
||||||
className={cn(
|
className="flex size-8 items-center justify-center rounded-md text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors"
|
||||||
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors",
|
aria-label="Run browser task"
|
||||||
isBgTasksQuickActionSelected
|
title="Run browser task"
|
||||||
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
|
||||||
: "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<ListChecks className="size-4" />
|
<Globe className="size-4" />
|
||||||
<span>Agents</span>
|
</button>
|
||||||
|
)}
|
||||||
|
{onOpenSearch && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onOpenSearch}
|
||||||
|
className="flex size-8 items-center justify-center rounded-md text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors"
|
||||||
|
aria-label="Search"
|
||||||
|
title="Search"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
|
<EmailSidebarSection
|
||||||
|
onOpenEmailView={onOpenEmail}
|
||||||
|
onOpenConnectors={() => setConnectorsOpen(true)}
|
||||||
|
/>
|
||||||
|
<MeetingsSidebarSection
|
||||||
|
onOpenMeetingsView={onOpenMeetings}
|
||||||
|
onOpenConnectors={() => setConnectorsOpen(true)}
|
||||||
|
recordingState={meetingRecordingState ?? 'idle'}
|
||||||
|
recordingSource={recordingMeetingSource ?? null}
|
||||||
|
onToggleRecording={onToggleMeetingRecording}
|
||||||
|
/>
|
||||||
|
<TasksSidebarSection
|
||||||
|
tasks={bgTaskSummaries}
|
||||||
|
onOpenTask={onOpenBgTask}
|
||||||
|
onOpenTasksView={onOpenBgTasks}
|
||||||
|
/>
|
||||||
<KnowledgeSection
|
<KnowledgeSection
|
||||||
tree={tree}
|
tree={tree}
|
||||||
selectedPath={selectedPath}
|
selectedPath={selectedPath}
|
||||||
|
|
@ -596,8 +601,6 @@ export function SidebarContentPanel({
|
||||||
currentRunId={currentRunId}
|
currentRunId={currentRunId}
|
||||||
processingRunIds={processingRunIds}
|
processingRunIds={processingRunIds}
|
||||||
actions={tasksActions}
|
actions={tasksActions}
|
||||||
backgroundTasks={backgroundTasks}
|
|
||||||
selectedBackgroundTask={selectedBackgroundTask}
|
|
||||||
/>
|
/>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
{/* Billing / upgrade CTA or Log in CTA */}
|
{/* Billing / upgrade CTA or Log in CTA */}
|
||||||
|
|
@ -638,43 +641,6 @@ export function SidebarContentPanel({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Secondary quick actions (above bottom divider) */}
|
|
||||||
{(onToggleBrowser || onOpenSuggestedTopics) && (
|
|
||||||
<div className="px-2 pb-1">
|
|
||||||
<div className="flex flex-col gap-0.5">
|
|
||||||
{onToggleBrowser && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onToggleBrowser}
|
|
||||||
className={cn(
|
|
||||||
"flex w-full items-center gap-2 rounded-md px-2 py-1 text-xs transition-colors",
|
|
||||||
isBrowserQuickActionSelected
|
|
||||||
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
|
||||||
: "text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Globe className="size-4" />
|
|
||||||
<span>Run browser task</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{onOpenSuggestedTopics && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onOpenSuggestedTopics}
|
|
||||||
className={cn(
|
|
||||||
"flex w-full items-center gap-2 rounded-md px-2 py-1 text-xs transition-colors",
|
|
||||||
isSuggestedTopicsQuickActionSelected
|
|
||||||
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
|
||||||
: "text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Lightbulb className="size-4" />
|
|
||||||
<span>Suggested Topics</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* Bottom actions */}
|
{/* Bottom actions */}
|
||||||
<div className="border-t border-sidebar-border px-2 py-2">
|
<div className="border-t border-sidebar-border px-2 py-2">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
|
@ -1040,7 +1006,7 @@ function KnowledgeSection({
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<SidebarGroup className="flex flex-col">
|
<SidebarGroup className="flex flex-col">
|
||||||
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
||||||
Knowledge
|
Notes
|
||||||
</div>
|
</div>
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
|
|
@ -1056,9 +1022,24 @@ function KnowledgeSection({
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton onClick={() => actions.openKnowledgeView()}>
|
<SidebarMenuButton
|
||||||
|
onClick={() =>
|
||||||
|
recentNotes.length === 0
|
||||||
|
? actions.createNote()
|
||||||
|
: actions.openKnowledgeView()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{recentNotes.length === 0 ? (
|
||||||
|
<>
|
||||||
|
<Plus className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">New note</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<ArrowUpRight className="size-4 shrink-0 text-muted-foreground" />
|
<ArrowUpRight className="size-4 shrink-0 text-muted-foreground" />
|
||||||
<span className="text-muted-foreground">View all</span>
|
<span className="text-muted-foreground">View all</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
|
|
@ -1112,8 +1093,17 @@ export function WorkspaceSection({
|
||||||
))}
|
))}
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<SidebarMenuButton onClick={() => actions.openWorkspaceAt()}>
|
<SidebarMenuButton onClick={() => actions.openWorkspaceAt()}>
|
||||||
|
{recentWorkspaces.length === 0 ? (
|
||||||
|
<>
|
||||||
|
<Plus className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">New workspace</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<ArrowUpRight className="size-4 shrink-0 text-muted-foreground" />
|
<ArrowUpRight className="size-4 shrink-0 text-muted-foreground" />
|
||||||
<span className="text-muted-foreground">View all</span>
|
<span className="text-muted-foreground">View all</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
|
|
@ -1123,25 +1113,540 @@ export function WorkspaceSection({
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Get status indicator color
|
type UpcomingMeeting = {
|
||||||
function getStatusColor(status?: string, enabled?: boolean): string {
|
id: string
|
||||||
// Disabled agents always show gray
|
summary: string
|
||||||
if (enabled === false) {
|
start: Date
|
||||||
return "bg-gray-400"
|
isAllDay: boolean
|
||||||
|
location: string | null
|
||||||
|
htmlLink: string | null
|
||||||
|
conferenceLink: string | null
|
||||||
|
source: string
|
||||||
|
rawStart: { dateTime?: string; date?: string } | undefined
|
||||||
|
rawEnd: { dateTime?: string; date?: string } | undefined
|
||||||
}
|
}
|
||||||
switch (status) {
|
|
||||||
case "running":
|
type RawCalendarEvent = {
|
||||||
return "bg-blue-500"
|
id?: string
|
||||||
case "finished":
|
summary?: string
|
||||||
return "bg-green-500"
|
start?: { dateTime?: string; date?: string }
|
||||||
case "failed":
|
end?: { dateTime?: string; date?: string }
|
||||||
return "bg-red-500"
|
location?: string
|
||||||
case "triggered":
|
htmlLink?: string
|
||||||
return "bg-gray-400"
|
status?: string
|
||||||
case "scheduled":
|
attendees?: Array<{ self?: boolean; responseStatus?: string }>
|
||||||
default:
|
|
||||||
return "bg-yellow-500"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseAllDayDate(s: string): Date | null {
|
||||||
|
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(s)
|
||||||
|
if (!m) return null
|
||||||
|
return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]))
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeUpcomingMeeting(raw: RawCalendarEvent, sourcePath: string): UpcomingMeeting | null {
|
||||||
|
if (raw.status === 'cancelled') return null
|
||||||
|
const declined = raw.attendees?.find((a) => a.self)?.responseStatus === 'declined'
|
||||||
|
if (declined) return null
|
||||||
|
const allDayStart = raw.start?.date
|
||||||
|
const timedStart = raw.start?.dateTime
|
||||||
|
const isAllDay = !timedStart && Boolean(allDayStart)
|
||||||
|
let start: Date | null = null
|
||||||
|
let end: Date | null = null
|
||||||
|
if (timedStart) {
|
||||||
|
start = new Date(timedStart)
|
||||||
|
end = raw.end?.dateTime ? new Date(raw.end.dateTime) : null
|
||||||
|
} else if (allDayStart) {
|
||||||
|
start = parseAllDayDate(allDayStart)
|
||||||
|
end = raw.end?.date ? parseAllDayDate(raw.end.date) : null
|
||||||
|
}
|
||||||
|
if (!start || Number.isNaN(start.getTime())) return null
|
||||||
|
const now = new Date()
|
||||||
|
const effectiveEnd = end ?? (isAllDay ? new Date(start.getTime() + 24 * 60 * 60 * 1000) : start)
|
||||||
|
if (effectiveEnd <= now) return null
|
||||||
|
const conferenceLink = extractConferenceLink(raw as unknown as Record<string, unknown>) ?? null
|
||||||
|
return {
|
||||||
|
id: raw.id ?? sourcePath,
|
||||||
|
summary: raw.summary?.trim() || '(No title)',
|
||||||
|
start,
|
||||||
|
isAllDay,
|
||||||
|
location: raw.location?.trim() || null,
|
||||||
|
htmlLink: raw.htmlLink ?? null,
|
||||||
|
conferenceLink,
|
||||||
|
source: sourcePath,
|
||||||
|
rawStart: raw.start,
|
||||||
|
rawEnd: raw.end,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerMeetingCapture(event: UpcomingMeeting, openConference: boolean) {
|
||||||
|
window.__pendingCalendarEvent = {
|
||||||
|
summary: event.summary,
|
||||||
|
start: event.rawStart,
|
||||||
|
end: event.rawEnd,
|
||||||
|
location: event.location ?? undefined,
|
||||||
|
htmlLink: event.htmlLink ?? undefined,
|
||||||
|
conferenceLink: event.conferenceLink ?? undefined,
|
||||||
|
source: event.source,
|
||||||
|
}
|
||||||
|
if (openConference && event.conferenceLink) {
|
||||||
|
window.open(event.conferenceLink, '_blank')
|
||||||
|
}
|
||||||
|
window.dispatchEvent(new Event('calendar-block:join-meeting'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameLocalDay(a: Date, b: Date): boolean {
|
||||||
|
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMeetingTime(event: UpcomingMeeting): string {
|
||||||
|
if (event.isAllDay) return 'All day'
|
||||||
|
const now = new Date()
|
||||||
|
const tomorrow = new Date(now)
|
||||||
|
tomorrow.setDate(tomorrow.getDate() + 1)
|
||||||
|
const time = event.start.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
|
||||||
|
if (isSameLocalDay(event.start, now)) return time
|
||||||
|
if (isSameLocalDay(event.start, tomorrow)) return `Tmrw ${time}`
|
||||||
|
return event.start.toLocaleDateString([], { month: 'numeric', day: 'numeric' })
|
||||||
|
}
|
||||||
|
|
||||||
|
type SidebarEmailThread = {
|
||||||
|
threadId: string
|
||||||
|
subject: string
|
||||||
|
from: string
|
||||||
|
date: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEmailFrom(from: string): string {
|
||||||
|
const match = /^\s*"?([^"<]+?)"?\s*<.+>\s*$/.exec(from)
|
||||||
|
if (match) return match[1].trim()
|
||||||
|
return from
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatEmailTime(value: string): string {
|
||||||
|
if (!value) return ''
|
||||||
|
const date = new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) return value
|
||||||
|
const now = new Date()
|
||||||
|
const diffMs = now.getTime() - date.getTime()
|
||||||
|
const diffMin = Math.round(diffMs / 60000)
|
||||||
|
if (diffMin < 1) return 'now'
|
||||||
|
if (diffMin < 60) return `${diffMin}m`
|
||||||
|
const sameDay = date.toDateString() === now.toDateString()
|
||||||
|
if (sameDay) return `${Math.round(diffMin / 60)}h`
|
||||||
|
const yesterday = new Date(now)
|
||||||
|
yesterday.setDate(now.getDate() - 1)
|
||||||
|
if (date.toDateString() === yesterday.toDateString()) return 'Yest'
|
||||||
|
if (diffMs < 7 * 24 * 60 * 60 * 1000) return date.toLocaleDateString([], { weekday: 'short' })
|
||||||
|
if (date.getFullYear() === now.getFullYear()) return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
||||||
|
return date.toLocaleDateString([], { month: 'short', day: 'numeric', year: '2-digit' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmailSidebarSection({
|
||||||
|
onOpenEmailView,
|
||||||
|
onOpenConnectors,
|
||||||
|
}: {
|
||||||
|
onOpenEmailView?: (threadId?: string) => void
|
||||||
|
onOpenConnectors?: () => void
|
||||||
|
}) {
|
||||||
|
const [threads, setThreads] = useState<SidebarEmailThread[]>([])
|
||||||
|
const [connected, setConnected] = useState<boolean | null>(null)
|
||||||
|
|
||||||
|
const refreshConnected = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.ipc.invoke('composio:get-connection-status', { toolkitSlug: 'gmail' })
|
||||||
|
setConnected(result.isConnected)
|
||||||
|
} catch {
|
||||||
|
setConnected(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refreshConnected()
|
||||||
|
const cleanup = window.ipc.on('oauth:didConnect', () => { void refreshConnected() })
|
||||||
|
return cleanup
|
||||||
|
}, [refreshConnected])
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.ipc.invoke('gmail:getImportant', { limit: 25 })
|
||||||
|
const unread = result.threads
|
||||||
|
.filter((t) => t.unread === true)
|
||||||
|
.slice(0, 3)
|
||||||
|
.map<SidebarEmailThread>((t) => ({
|
||||||
|
threadId: t.threadId,
|
||||||
|
subject: t.subject ?? '(No subject)',
|
||||||
|
from: t.from ?? '',
|
||||||
|
date: t.date ?? '',
|
||||||
|
}))
|
||||||
|
setThreads(unread)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load important emails:', err)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void load()
|
||||||
|
}, [load])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
const scheduleReload = () => {
|
||||||
|
if (timeout) clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(() => { timeout = null; void load() }, 500)
|
||||||
|
}
|
||||||
|
const matches = (p: string | undefined) =>
|
||||||
|
typeof p === 'string' && (p === 'gmail_sync' || p.startsWith('gmail_sync/'))
|
||||||
|
const cleanup = window.ipc.on('workspace:didChange', (event) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'created':
|
||||||
|
case 'changed':
|
||||||
|
case 'deleted':
|
||||||
|
if (matches(event.path)) scheduleReload()
|
||||||
|
break
|
||||||
|
case 'moved':
|
||||||
|
if (matches(event.from) || matches(event.to)) scheduleReload()
|
||||||
|
break
|
||||||
|
case 'bulkChanged':
|
||||||
|
if (!event.paths || event.paths.some(matches)) scheduleReload()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
if (timeout) clearTimeout(timeout)
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
}, [load])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarGroup className="flex flex-col">
|
||||||
|
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
||||||
|
Email
|
||||||
|
</div>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
{threads.map((t) => (
|
||||||
|
<SidebarMenuItem key={t.threadId}>
|
||||||
|
<SidebarMenuButton onClick={() => onOpenEmailView?.(t.threadId)} className="gap-2">
|
||||||
|
<Mail className="size-4 shrink-0" />
|
||||||
|
<span className="min-w-0 flex-1 truncate text-sm">
|
||||||
|
{formatEmailFrom(t.from)}
|
||||||
|
<span className="text-muted-foreground"> · {t.subject}</span>
|
||||||
|
</span>
|
||||||
|
{t.date && (
|
||||||
|
<span className="shrink-0 text-[10px] text-muted-foreground">
|
||||||
|
{formatEmailTime(t.date)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
))}
|
||||||
|
{connected === false && threads.length === 0 ? (
|
||||||
|
onOpenConnectors && (
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton onClick={onOpenConnectors}>
|
||||||
|
<Plug className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Connect Email</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
onOpenEmailView && (
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton onClick={() => onOpenEmailView()}>
|
||||||
|
<ArrowUpRight className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">View all</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function MeetingsSidebarSection({
|
||||||
|
onOpenMeetingsView,
|
||||||
|
onOpenConnectors,
|
||||||
|
recordingState,
|
||||||
|
recordingSource,
|
||||||
|
onToggleRecording,
|
||||||
|
}: {
|
||||||
|
onOpenMeetingsView?: () => void
|
||||||
|
onOpenConnectors?: () => void
|
||||||
|
recordingState: 'idle' | 'connecting' | 'recording' | 'stopping'
|
||||||
|
recordingSource: string | null
|
||||||
|
onToggleRecording?: () => void
|
||||||
|
}) {
|
||||||
|
const [meetings, setMeetings] = useState<UpcomingMeeting[]>([])
|
||||||
|
const [calendarConnected, setCalendarConnected] = useState<boolean | null>(null)
|
||||||
|
|
||||||
|
const refreshCalendarConnected = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.ipc.invoke('composio:get-connection-status', { toolkitSlug: 'googlecalendar' })
|
||||||
|
setCalendarConnected(result.isConnected)
|
||||||
|
} catch {
|
||||||
|
setCalendarConnected(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refreshCalendarConnected()
|
||||||
|
const cleanup = window.ipc.on('oauth:didConnect', () => { void refreshCalendarConnected() })
|
||||||
|
return cleanup
|
||||||
|
}, [refreshCalendarConnected])
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const exists = await window.ipc.invoke('workspace:exists', { path: 'calendar_sync' })
|
||||||
|
if (!exists.exists) {
|
||||||
|
setMeetings([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const entries = await window.ipc.invoke('workspace:readdir', {
|
||||||
|
path: 'calendar_sync',
|
||||||
|
opts: { recursive: false, includeHidden: false, includeStats: false },
|
||||||
|
})
|
||||||
|
const jsonEntries = entries.filter((e) => e.kind === 'file' && e.name.endsWith('.json'))
|
||||||
|
const settled = await Promise.allSettled(
|
||||||
|
jsonEntries.map(async (entry): Promise<UpcomingMeeting | null> => {
|
||||||
|
const result = await window.ipc.invoke('workspace:readFile', {
|
||||||
|
path: entry.path,
|
||||||
|
encoding: 'utf8',
|
||||||
|
})
|
||||||
|
const raw = JSON.parse(result.data) as RawCalendarEvent
|
||||||
|
return normalizeUpcomingMeeting(raw, entry.path)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const collected: UpcomingMeeting[] = []
|
||||||
|
for (const r of settled) {
|
||||||
|
if (r.status === 'fulfilled' && r.value) collected.push(r.value)
|
||||||
|
}
|
||||||
|
collected.sort((a, b) => {
|
||||||
|
if (a.isAllDay !== b.isAllDay) return a.isAllDay ? -1 : 1
|
||||||
|
return a.start.getTime() - b.start.getTime()
|
||||||
|
})
|
||||||
|
setMeetings(collected.slice(0, 3))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load upcoming meetings:', err)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void load()
|
||||||
|
}, [load])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
const scheduleReload = () => {
|
||||||
|
if (timeout) clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(() => { timeout = null; void load() }, 250)
|
||||||
|
}
|
||||||
|
const matches = (p: string | undefined) =>
|
||||||
|
typeof p === 'string' && (p === 'calendar_sync' || p.startsWith('calendar_sync/'))
|
||||||
|
const cleanup = window.ipc.on('workspace:didChange', (event) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'created':
|
||||||
|
case 'changed':
|
||||||
|
case 'deleted':
|
||||||
|
if (matches(event.path)) scheduleReload()
|
||||||
|
break
|
||||||
|
case 'moved':
|
||||||
|
if (matches(event.from) || matches(event.to)) scheduleReload()
|
||||||
|
break
|
||||||
|
case 'bulkChanged':
|
||||||
|
if (!event.paths || event.paths.some(matches)) scheduleReload()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const tick = setInterval(() => void load(), 60 * 60 * 1000)
|
||||||
|
return () => {
|
||||||
|
if (timeout) clearTimeout(timeout)
|
||||||
|
clearInterval(tick)
|
||||||
|
cleanup()
|
||||||
|
}
|
||||||
|
}, [load])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarGroup className="flex flex-col">
|
||||||
|
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
||||||
|
Meetings
|
||||||
|
</div>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
{meetings.map((m) => {
|
||||||
|
const hasConference = Boolean(m.conferenceLink)
|
||||||
|
const isThisRecording = recordingSource === m.source && (recordingState === 'recording' || recordingState === 'connecting' || recordingState === 'stopping')
|
||||||
|
const isBusy = isThisRecording && (recordingState === 'connecting' || recordingState === 'stopping')
|
||||||
|
return (
|
||||||
|
<SidebarMenuItem key={m.id}>
|
||||||
|
<SidebarMenuButton onClick={onOpenMeetingsView} className="gap-2">
|
||||||
|
<Mic className="size-4 shrink-0" />
|
||||||
|
<span className="min-w-0 flex-1 truncate text-sm">{m.summary}</span>
|
||||||
|
<span
|
||||||
|
className={`shrink-0 text-[10px] text-muted-foreground ${isThisRecording ? '' : 'group-hover/menu-item:hidden'}`}
|
||||||
|
>
|
||||||
|
{isThisRecording ? null : formatMeetingTime(m)}
|
||||||
|
</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
{isThisRecording ? (
|
||||||
|
<div className="absolute top-1.5 right-1 flex items-center gap-1.5">
|
||||||
|
<span className="relative flex size-2">
|
||||||
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75" />
|
||||||
|
<span className="relative inline-flex size-2 rounded-full bg-red-500" />
|
||||||
|
</span>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Stop recording"
|
||||||
|
disabled={isBusy}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onToggleRecording?.()
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
className="flex aspect-square w-5 items-center justify-center rounded-md text-destructive hover:bg-destructive/10 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isBusy ? <LoaderIcon className="size-4 animate-spin" /> : <Square className="size-3.5 fill-current" />}
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">
|
||||||
|
{recordingState === 'connecting' ? 'Starting…' : recordingState === 'stopping' ? 'Stopping…' : 'Stop recording'}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="absolute top-1.5 right-1 flex items-center gap-0.5 opacity-0 transition-opacity group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Take notes"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
triggerMeetingCapture(m, false)
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
className="flex aspect-square w-5 items-center justify-center rounded-md text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||||
|
>
|
||||||
|
<Mic className="size-4" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">Take notes</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
{hasConference && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Join & take notes"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
triggerMeetingCapture(m, true)
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
className="flex aspect-square w-5 items-center justify-center rounded-md text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
|
||||||
|
>
|
||||||
|
<Video className="size-4" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">Join & take notes</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SidebarMenuItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{calendarConnected === false && meetings.length === 0 ? (
|
||||||
|
onOpenConnectors && (
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton onClick={onOpenConnectors}>
|
||||||
|
<Plug className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Connect Calendar</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
onOpenMeetingsView && (
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton onClick={onOpenMeetingsView}>
|
||||||
|
<ArrowUpRight className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">View all</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TasksSidebarSection({
|
||||||
|
tasks,
|
||||||
|
onOpenTask,
|
||||||
|
onOpenTasksView,
|
||||||
|
}: {
|
||||||
|
tasks: TaskSummary[]
|
||||||
|
onOpenTask?: (slug: string) => void
|
||||||
|
onOpenTasksView?: () => void
|
||||||
|
}) {
|
||||||
|
const recentTasks = React.useMemo<TaskSummary[]>(() => {
|
||||||
|
const toTime = (s?: string | null): number => {
|
||||||
|
if (!s) return 0
|
||||||
|
const t = new Date(s).getTime()
|
||||||
|
return Number.isNaN(t) ? 0 : t
|
||||||
|
}
|
||||||
|
const activity = (t: TaskSummary): number =>
|
||||||
|
Math.max(toTime(t.lastRunAt), toTime(t.lastAttemptAt), toTime(t.createdAt))
|
||||||
|
return [...tasks]
|
||||||
|
.sort((a, b) => activity(b) - activity(a))
|
||||||
|
.slice(0, 3)
|
||||||
|
}, [tasks])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarGroup className="flex flex-col">
|
||||||
|
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
||||||
|
Tasks
|
||||||
|
</div>
|
||||||
|
<SidebarGroupContent>
|
||||||
|
<SidebarMenu>
|
||||||
|
{recentTasks.map((task) => (
|
||||||
|
<SidebarMenuItem key={task.slug}>
|
||||||
|
<SidebarMenuButton
|
||||||
|
onClick={() => onOpenTask?.(task.slug)}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Bot className="size-4 shrink-0" />
|
||||||
|
<span className={`truncate text-sm ${!task.active ? "text-muted-foreground" : ""}`}>
|
||||||
|
{task.name}
|
||||||
|
</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
))}
|
||||||
|
{onOpenTasksView && (
|
||||||
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton onClick={onOpenTasksView}>
|
||||||
|
{recentTasks.length === 0 ? (
|
||||||
|
<>
|
||||||
|
<Plus className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">New Task</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ArrowUpRight className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">View all</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
|
)}
|
||||||
|
</SidebarMenu>
|
||||||
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tasks Section
|
// Tasks Section
|
||||||
|
|
@ -1150,57 +1655,22 @@ function TasksSection({
|
||||||
currentRunId,
|
currentRunId,
|
||||||
processingRunIds,
|
processingRunIds,
|
||||||
actions,
|
actions,
|
||||||
backgroundTasks = [],
|
|
||||||
selectedBackgroundTask,
|
|
||||||
}: {
|
}: {
|
||||||
runs: RunListItem[]
|
runs: RunListItem[]
|
||||||
currentRunId?: string | null
|
currentRunId?: string | null
|
||||||
processingRunIds?: Set<string>
|
processingRunIds?: Set<string>
|
||||||
actions?: TasksActions
|
actions?: TasksActions
|
||||||
backgroundTasks?: BackgroundTaskItem[]
|
|
||||||
selectedBackgroundTask?: string | null
|
|
||||||
}) {
|
}) {
|
||||||
const [pendingDeleteRunId, setPendingDeleteRunId] = useState<string | null>(null)
|
const [pendingDeleteRunId, setPendingDeleteRunId] = useState<string | null>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarGroup className="flex flex-col">
|
<SidebarGroup className="flex flex-col">
|
||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
{/* Background Tasks Section */}
|
|
||||||
{backgroundTasks.length > 0 && (
|
|
||||||
<>
|
|
||||||
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
|
||||||
Background Tasks
|
|
||||||
</div>
|
|
||||||
<SidebarMenu>
|
|
||||||
{backgroundTasks.map((task) => (
|
|
||||||
<SidebarMenuItem key={task.name}>
|
|
||||||
<SidebarMenuButton
|
|
||||||
isActive={selectedBackgroundTask === task.name}
|
|
||||||
onClick={() => actions?.onSelectBackgroundTask?.(task.name)}
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<div className="relative">
|
|
||||||
<Bot className="size-4 shrink-0" />
|
|
||||||
<span
|
|
||||||
className={`absolute -bottom-0.5 -right-0.5 size-2 rounded-full ${getStatusColor(task.status, task.enabled)} ${task.status === "running" && task.enabled ? "animate-pulse" : ""}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<span className={`truncate text-sm ${!task.enabled ? "text-muted-foreground" : ""}`}>
|
|
||||||
{task.name}
|
|
||||||
</span>
|
|
||||||
</SidebarMenuButton>
|
|
||||||
</SidebarMenuItem>
|
|
||||||
))}
|
|
||||||
</SidebarMenu>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{runs.length > 0 && (
|
|
||||||
<>
|
|
||||||
<div className="px-3 py-1.5 mt-4 text-xs font-medium text-muted-foreground">
|
|
||||||
Chat history
|
Chat history
|
||||||
</div>
|
</div>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{runs.map((run) => (
|
{runs.slice(0, 3).map((run) => (
|
||||||
<ContextMenu key={run.id}>
|
<ContextMenu key={run.id}>
|
||||||
<ContextMenuTrigger asChild>
|
<ContextMenuTrigger asChild>
|
||||||
<SidebarMenuItem className="group/chat-item">
|
<SidebarMenuItem className="group/chat-item">
|
||||||
|
|
@ -1215,6 +1685,7 @@ function TasksSection({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center gap-2 min-w-0">
|
<div className="flex w-full items-center gap-2 min-w-0">
|
||||||
|
<MessageSquare className="size-4 shrink-0 text-muted-foreground" />
|
||||||
<span className="min-w-0 flex-1 truncate text-sm">{run.title || '(Untitled chat)'}</span>
|
<span className="min-w-0 flex-1 truncate text-sm">{run.title || '(Untitled chat)'}</span>
|
||||||
{run.createdAt ? (
|
{run.createdAt ? (
|
||||||
<span className="shrink-0 text-[10px] text-muted-foreground">
|
<span className="shrink-0 text-[10px] text-muted-foreground">
|
||||||
|
|
@ -1247,9 +1718,15 @@ function TasksSection({
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
))}
|
))}
|
||||||
</SidebarMenu>
|
{runs.length > 0 && actions?.onOpenChatHistoryView && (
|
||||||
</>
|
<SidebarMenuItem>
|
||||||
|
<SidebarMenuButton onClick={() => actions.onOpenChatHistoryView?.()}>
|
||||||
|
<ArrowUpRight className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">View all</span>
|
||||||
|
</SidebarMenuButton>
|
||||||
|
</SidebarMenuItem>
|
||||||
)}
|
)}
|
||||||
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
|
|
||||||
{/* Delete confirmation dialog */}
|
{/* Delete confirmation dialog */}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue