added chat history page

This commit is contained in:
Arjun 2026-05-19 18:53:24 +05:30
parent e678dd71a0
commit f8100d353d
3 changed files with 388 additions and 103 deletions

View file

@ -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 {
@ -187,6 +188,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) =>
@ -322,6 +324,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) => {
@ -574,6 +577,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
@ -630,6 +634,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
} }
@ -768,6 +774,7 @@ 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 [expandedFrom, setExpandedFrom] = useState<{ const [expandedFrom, setExpandedFrom] = useState<{
path: string | null path: string | null
graph: boolean graph: boolean
@ -1106,6 +1113,7 @@ function App() {
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 'Knowledge'
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
@ -2818,7 +2826,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
} }
@ -2827,7 +2835,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])
@ -2846,14 +2854,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)) {
@ -2865,6 +2873,7 @@ function App() {
setIsEmailOpen(false) setIsEmailOpen(false)
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false)
setIsLiveNotesOpen(true) setIsLiveNotesOpen(true)
return return
} }
@ -2877,6 +2886,7 @@ function App() {
setIsEmailOpen(false) setIsEmailOpen(false)
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false)
setIsBgTasksOpen(true) setIsBgTasksOpen(true)
return return
} }
@ -2890,6 +2900,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)) {
@ -2901,6 +2912,7 @@ function App() {
setIsBgTasksOpen(false) setIsBgTasksOpen(false)
setIsWorkspaceOpen(false) setIsWorkspaceOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false)
setIsEmailOpen(true) setIsEmailOpen(true)
return return
} }
@ -2913,6 +2925,7 @@ function App() {
setIsBgTasksOpen(false) setIsBgTasksOpen(false)
setIsEmailOpen(false) setIsEmailOpen(false)
setIsKnowledgeViewOpen(false) setIsKnowledgeViewOpen(false)
setIsChatHistoryOpen(false)
setIsWorkspaceOpen(true) setIsWorkspaceOpen(true)
return return
} }
@ -2925,18 +2938,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)
@ -2959,7 +2986,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)
@ -2973,12 +3000,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)
@ -2989,6 +3016,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)
@ -2998,6 +3026,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)
@ -3009,6 +3038,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)
@ -3018,6 +3048,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)
@ -3028,6 +3059,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)
@ -3038,11 +3070,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)
} }
} }
@ -3073,7 +3117,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,
@ -3090,8 +3134,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(() => {
@ -3223,7 +3267,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,
@ -3239,19 +3283,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)
@ -3283,7 +3327,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)
@ -3299,10 +3343,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]
@ -3425,6 +3470,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)
@ -3447,7 +3503,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)
@ -3481,7 +3537,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.
@ -3496,7 +3552,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()
@ -3509,7 +3565,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)
@ -3522,7 +3578,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':
@ -3539,6 +3595,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':
@ -3554,6 +3611,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
@ -3571,6 +3629,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':
@ -3587,6 +3646,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
@ -3604,8 +3664,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)
@ -3614,7 +3692,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 {
@ -3622,7 +3700,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
@ -3944,7 +4022,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') {
@ -4029,11 +4107,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
@ -4050,6 +4128,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
@ -4104,7 +4184,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') {
@ -4129,7 +4209,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)
} }
@ -4255,28 +4335,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)
} }
@ -4888,7 +4968,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(() => {
@ -4905,7 +4985,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 })
} }
}}> }}>
@ -4928,7 +5008,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)
} }
@ -4939,7 +5019,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
@ -4963,14 +5043,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 {
@ -4985,6 +5065,9 @@ function App() {
onSelectBackgroundTask: (taskName) => { onSelectBackgroundTask: (taskName) => {
void navigateToView({ type: 'task', name: taskName }) void navigateToView({ type: 'task', name: taskName })
}, },
onOpenChatHistoryView: () => {
void navigateToView({ type: 'chat-history' })
},
}} }}
backgroundTasks={backgroundTasks} backgroundTasks={backgroundTasks}
selectedBackgroundTask={selectedBackgroundTask} selectedBackgroundTask={selectedBackgroundTask}
@ -5018,7 +5101,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 ?? ''}
@ -5026,7 +5109,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
@ -5079,7 +5162,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
@ -5094,7 +5177,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
@ -5109,7 +5192,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
@ -5204,6 +5287,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

View 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>
)
}

View file

@ -183,6 +183,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 = {
@ -1194,62 +1195,66 @@ function TasksSection({
</SidebarMenu> </SidebarMenu>
</> </>
)} )}
{runs.length > 0 && ( <div className="px-3 py-1.5 mt-4 text-xs font-medium text-muted-foreground">
<> Chat history
<div className="px-3 py-1.5 mt-4 text-xs font-medium text-muted-foreground"> </div>
Chat history <SidebarMenu>
</div> {runs.slice(0, 3).map((run) => (
<SidebarMenu> <ContextMenu key={run.id}>
{runs.map((run) => ( <ContextMenuTrigger asChild>
<ContextMenu key={run.id}> <SidebarMenuItem className="group/chat-item">
<ContextMenuTrigger asChild> <SidebarMenuButton
<SidebarMenuItem className="group/chat-item"> isActive={currentRunId === run.id}
<SidebarMenuButton onClick={(e) => {
isActive={currentRunId === run.id} if (e.metaKey && actions?.onOpenInNewTab) {
onClick={(e) => { actions.onOpenInNewTab(run.id)
if (e.metaKey && actions?.onOpenInNewTab) { } else {
actions.onOpenInNewTab(run.id) actions?.onSelectRun(run.id)
} else { }
actions?.onSelectRun(run.id) }}
} >
}} <div className="flex w-full items-center gap-2 min-w-0">
> <span className="min-w-0 flex-1 truncate text-sm">{run.title || '(Untitled chat)'}</span>
<div className="flex w-full items-center gap-2 min-w-0"> {run.createdAt ? (
<span className="min-w-0 flex-1 truncate text-sm">{run.title || '(Untitled chat)'}</span> <span className="shrink-0 text-[10px] text-muted-foreground">
{run.createdAt ? ( {formatRunTime(run.createdAt)}
<span className="shrink-0 text-[10px] text-muted-foreground"> </span>
{formatRunTime(run.createdAt)} ) : null}
</span> </div>
) : null} </SidebarMenuButton>
</div> </SidebarMenuItem>
</SidebarMenuButton> </ContextMenuTrigger>
</SidebarMenuItem> <ContextMenuContent className="w-48">
</ContextMenuTrigger> {actions?.onOpenInNewTab && (
<ContextMenuContent className="w-48"> <ContextMenuItem onClick={() => actions.onOpenInNewTab!(run.id)}>
{actions?.onOpenInNewTab && ( <ExternalLink className="mr-2 size-4" />
<ContextMenuItem onClick={() => actions.onOpenInNewTab!(run.id)}> Open in new tab
<ExternalLink className="mr-2 size-4" /> </ContextMenuItem>
Open in new tab )}
</ContextMenuItem> {!processingRunIds?.has(run.id) && (
)} <>
{!processingRunIds?.has(run.id) && ( {actions?.onOpenInNewTab && <ContextMenuSeparator />}
<> <ContextMenuItem
{actions?.onOpenInNewTab && <ContextMenuSeparator />} variant="destructive"
<ContextMenuItem onClick={() => setPendingDeleteRunId(run.id)}
variant="destructive" >
onClick={() => setPendingDeleteRunId(run.id)} <Trash2 className="mr-2 size-4" />
> Delete
<Trash2 className="mr-2 size-4" /> </ContextMenuItem>
Delete </>
</ContextMenuItem> )}
</> </ContextMenuContent>
)} </ContextMenu>
</ContextMenuContent> ))}
</ContextMenu> {actions?.onOpenChatHistoryView && (
))} <SidebarMenuItem>
</SidebarMenu> <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 */}