From 215e0aedd6c4736a50abc675242b5acbff67f60c Mon Sep 17 00:00:00 2001
From: Arjun <6592213+arkml@users.noreply.github.com>
Date: Mon, 18 May 2026 23:18:47 +0530
Subject: [PATCH 01/12] make new chat an icon on the top
---
apps/x/apps/renderer/src/App.tsx | 16 +++++++++++++++-
.../renderer/src/components/sidebar-content.tsx | 11 -----------
2 files changed, 15 insertions(+), 12 deletions(-)
diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx
index e6c050b3..cac3dbf4 100644
--- a/apps/x/apps/renderer/src/App.tsx
+++ b/apps/x/apps/renderer/src/App.tsx
@@ -622,12 +622,14 @@ function parseDeepLink(input: string): ViewState | null {
/** Sidebar toggle (fixed position, top-left) */
function FixedSidebarToggle({
leftInsetPx,
+ onNewChat,
}: {
leftInsetPx: number
+ onNewChat?: () => void
}) {
const { toggleSidebar } = useSidebar()
return (
-
+
{/* Sidebar toggle */}
+ {onNewChat && (
+
+ )}
)
}
@@ -5388,6 +5401,7 @@ function App() {
{/* Rendered last so its no-drag region paints over the sidebar drag region */}
diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx
index f4c469bc..88654f68 100644
--- a/apps/x/apps/renderer/src/components/sidebar-content.tsx
+++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx
@@ -21,7 +21,6 @@ import {
Pencil,
Radio,
SearchIcon,
- SquarePen,
Table2,
Plug,
Lightbulb,
@@ -591,16 +590,6 @@ export function SidebarContentPanel({
{/* Quick action buttons */}
- {onNewChat && (
-
- )}
{onOpenSearch && (
)}
+ {onOpenSearch && (
+
+ )}
)
}
@@ -736,6 +759,8 @@ function App() {
const [isLiveNotesOpen, setIsLiveNotesOpen] = useState(false)
const [isBgTasksOpen, setIsBgTasksOpen] = useState(false)
const [isEmailOpen, setIsEmailOpen] = useState(false)
+ const [isWorkspaceOpen, setIsWorkspaceOpen] = useState(false)
+ const [workspaceInitialPath, setWorkspaceInitialPath] = useState(null)
const [expandedFrom, setExpandedFrom] = useState<{
path: string | null
graph: boolean
@@ -1072,6 +1097,7 @@ function App() {
if (isLiveNotesTabPath(tab.path)) return 'Live notes'
if (isBgTasksTabPath(tab.path)) return 'Background tasks'
if (isEmailTabPath(tab.path)) return 'Email'
+ if (isWorkspaceTabPath(tab.path)) return 'Workspace'
if (tab.path === BASES_DEFAULT_TAB_PATH) return 'Bases'
if (tab.path.endsWith('.base')) return tab.path.split('/').pop()?.replace(/\.base$/i, '') || 'Base'
return tab.path.split('/').pop()?.replace(/\.md$/i, '') || tab.path
@@ -2784,7 +2810,7 @@ function App() {
setActiveFileTabId(existingTab.id)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
setSelectedPath(path)
return
}
@@ -2793,7 +2819,7 @@ function App() {
setActiveFileTabId(id)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
setSelectedPath(path)
}, [fileTabs, dismissBrowserOverlay])
@@ -2812,14 +2838,14 @@ function App() {
setSelectedPath(null)
setIsGraphOpen(true)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
return
}
if (isSuggestedTopicsTabPath(tab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(true)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
return
}
if (isLiveNotesTabPath(tab.path)) {
@@ -2829,6 +2855,7 @@ function App() {
setIsMeetingsOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
setIsLiveNotesOpen(true)
return
}
@@ -2838,6 +2865,8 @@ function App() {
setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false)
setIsLiveNotesOpen(false)
+ setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
setIsBgTasksOpen(true)
return
}
@@ -2848,26 +2877,41 @@ function App() {
setIsMeetingsOpen(true)
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
+ setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
return
}
if (isEmailTabPath(tab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
+ setIsMeetingsOpen(false)
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
+ setIsWorkspaceOpen(false)
setIsEmailOpen(true)
return
}
+ if (isWorkspaceTabPath(tab.path)) {
+ setSelectedPath(null)
+ setIsGraphOpen(false)
+ setIsSuggestedTopicsOpen(false)
+ setIsMeetingsOpen(false)
+ setIsLiveNotesOpen(false)
+ setIsBgTasksOpen(false)
+ setIsEmailOpen(false)
+ setIsWorkspaceOpen(true)
+ return
+ }
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
setSelectedPath(tab.path)
}, [fileTabs, isRightPaneMaximized, dismissBrowserOverlay])
const closeFileTab = useCallback((tabId: string) => {
const closingTab = fileTabs.find(t => t.id === tabId)
- if (closingTab && !isGraphTabPath(closingTab.path) && !isSuggestedTopicsTabPath(closingTab.path) && !isLiveNotesTabPath(closingTab.path) && !isBgTasksTabPath(closingTab.path) && !isEmailTabPath(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) && !isBaseFilePath(closingTab.path)) {
removeEditorCacheForPath(closingTab.path)
initialContentByPathRef.current.delete(closingTab.path)
untitledRenameReadyPathsRef.current.delete(closingTab.path)
@@ -2890,7 +2934,7 @@ function App() {
setSelectedPath(null)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
return []
}
const idx = prev.findIndex(t => t.id === tabId)
@@ -2904,12 +2948,12 @@ function App() {
setSelectedPath(null)
setIsGraphOpen(true)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
} else if (isSuggestedTopicsTabPath(newActiveTab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(true)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
} else if (isMeetingsTabPath(newActiveTab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
@@ -2918,6 +2962,7 @@ function App() {
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
} else if (isLiveNotesTabPath(newActiveTab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
@@ -2925,6 +2970,7 @@ function App() {
setIsMeetingsOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
setIsLiveNotesOpen(true)
} else if (isBgTasksTabPath(newActiveTab.path)) {
setSelectedPath(null)
@@ -2934,6 +2980,7 @@ function App() {
setIsLiveNotesOpen(false)
setIsBgTasksOpen(true)
setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
} else if (isEmailTabPath(newActiveTab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
@@ -2941,11 +2988,21 @@ function App() {
setIsMeetingsOpen(false)
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
+ setIsWorkspaceOpen(false)
setIsEmailOpen(true)
+ } else if (isWorkspaceTabPath(newActiveTab.path)) {
+ setSelectedPath(null)
+ setIsGraphOpen(false)
+ setIsSuggestedTopicsOpen(false)
+ setIsMeetingsOpen(false)
+ setIsLiveNotesOpen(false)
+ setIsBgTasksOpen(false)
+ setIsEmailOpen(false)
+ setIsWorkspaceOpen(true)
} else {
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
setSelectedPath(newActiveTab.path)
}
}
@@ -2976,7 +3033,7 @@ function App() {
dismissBrowserOverlay()
handleNewChat()
// Left-pane "new chat" should always open full chat view.
- if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen) {
+ if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen) {
setExpandedFrom({
path: selectedPath,
graph: isGraphOpen,
@@ -2993,8 +3050,8 @@ function App() {
setSelectedPath(null)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
- }, [chatTabs, activeChatTabId, dismissBrowserOverlay, handleNewChat, selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen])
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ }, [chatTabs, activeChatTabId, dismissBrowserOverlay, handleNewChat, selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen])
// Sidebar variant: create/switch chat tab without leaving file/graph context.
const handleNewChatTabInSidebar = useCallback(() => {
@@ -3126,7 +3183,7 @@ function App() {
const handleOpenFullScreenChat = useCallback(() => {
// Remember where we came from so the close button can return
- if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen) {
+ if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen) {
setExpandedFrom({
path: selectedPath,
graph: isGraphOpen,
@@ -3142,19 +3199,19 @@ function App() {
setSelectedPath(null)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
- }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, dismissBrowserOverlay])
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, dismissBrowserOverlay])
const handleCloseFullScreenChat = useCallback(() => {
if (expandedFrom) {
if (expandedFrom.graph) {
setIsGraphOpen(true)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
} else if (expandedFrom.suggestedTopics) {
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(true)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
} else if (expandedFrom.meetings) {
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
@@ -3186,7 +3243,7 @@ function App() {
} else if (expandedFrom.path) {
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
setSelectedPath(expandedFrom.path)
}
setExpandedFrom(null)
@@ -3200,10 +3257,11 @@ function App() {
if (isMeetingsOpen) return { type: 'meetings' }
if (isLiveNotesOpen) return { type: 'live-notes' }
if (isSuggestedTopicsOpen) return { type: 'suggested-topics' }
+ if (isWorkspaceOpen) return { type: 'workspace', path: workspaceInitialPath ?? undefined }
if (selectedPath) return { type: 'file', path: selectedPath }
if (isGraphOpen) return { type: 'graph' }
return { type: 'chat', runId }
- }, [selectedBackgroundTask, isEmailOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isSuggestedTopicsOpen, selectedPath, isGraphOpen, runId])
+ }, [selectedBackgroundTask, isEmailOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isSuggestedTopicsOpen, selectedPath, isGraphOpen, isWorkspaceOpen, workspaceInitialPath, runId])
const appendUnique = useCallback((stack: ViewState[], entry: ViewState) => {
const last = stack[stack.length - 1]
@@ -3304,6 +3362,17 @@ function App() {
setActiveFileTabId(id)
}, [fileTabs])
+ const ensureWorkspaceFileTab = useCallback(() => {
+ const existing = fileTabs.find((tab) => isWorkspaceTabPath(tab.path))
+ if (existing) {
+ setActiveFileTabId(existing.id)
+ return
+ }
+ const id = newFileTabId()
+ setFileTabs((prev) => [...prev, { id, path: WORKSPACE_TAB_PATH }])
+ setActiveFileTabId(id)
+ }, [fileTabs])
+
const openEmailView = useCallback(() => {
setSelectedPath(null)
setIsGraphOpen(false)
@@ -3312,6 +3381,7 @@ function App() {
setIsMeetingsOpen(false)
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
+ setIsWorkspaceOpen(false)
setSelectedBackgroundTask(null)
setExpandedFrom(null)
setIsRightPaneMaximized(false)
@@ -3324,7 +3394,7 @@ function App() {
setIsGraphOpen(false)
setIsBrowserOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
setSelectedBackgroundTask(null)
setExpandedFrom(null)
setIsRightPaneMaximized(false)
@@ -3341,6 +3411,7 @@ function App() {
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
setSelectedBackgroundTask(null)
setExpandedFrom(null)
setIsRightPaneMaximized(false)
@@ -3356,7 +3427,7 @@ function App() {
// visible in the middle pane.
setIsBrowserOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
setExpandedFrom(null)
// Preserve split vs knowledge-max mode when navigating knowledge files.
// Only exit chat-only maximize, because that would hide the selected file.
@@ -3371,7 +3442,7 @@ function App() {
setSelectedPath(null)
setIsBrowserOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
setExpandedFrom(null)
setIsGraphOpen(true)
ensureGraphFileTab()
@@ -3384,7 +3455,7 @@ function App() {
setIsGraphOpen(false)
setIsBrowserOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
setExpandedFrom(null)
setIsRightPaneMaximized(false)
setSelectedBackgroundTask(view.name)
@@ -3397,7 +3468,7 @@ function App() {
setIsRightPaneMaximized(false)
setSelectedBackgroundTask(null)
setIsSuggestedTopicsOpen(true)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
ensureSuggestedTopicsFileTab()
return
case 'meetings':
@@ -3412,6 +3483,7 @@ function App() {
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
ensureMeetingsFileTab()
return
case 'live-notes':
@@ -3425,6 +3497,7 @@ function App() {
setIsMeetingsOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
setIsLiveNotesOpen(true)
ensureLiveNotesFileTab()
return
@@ -3440,8 +3513,25 @@ function App() {
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(true)
+ setIsWorkspaceOpen(false)
ensureEmailFileTab()
return
+ case 'workspace':
+ setSelectedPath(null)
+ setIsGraphOpen(false)
+ setIsBrowserOpen(false)
+ setExpandedFrom(null)
+ setIsRightPaneMaximized(false)
+ setSelectedBackgroundTask(null)
+ setIsSuggestedTopicsOpen(false)
+ setIsMeetingsOpen(false)
+ setIsLiveNotesOpen(false)
+ setIsBgTasksOpen(false)
+ setIsEmailOpen(false)
+ setIsWorkspaceOpen(true)
+ setWorkspaceInitialPath(view.path ?? null)
+ ensureWorkspaceFileTab()
+ return
case 'chat':
setSelectedPath(null)
setIsGraphOpen(false)
@@ -3450,7 +3540,7 @@ function App() {
setIsRightPaneMaximized(false)
setSelectedBackgroundTask(null)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
if (view.runId) {
await loadRun(view.runId)
} else {
@@ -3458,7 +3548,7 @@ function App() {
}
return
}
- }, [ensureEmailFileTab, ensureMeetingsFileTab, ensureLiveNotesFileTab, ensureFileTabForPath, ensureGraphFileTab, ensureSuggestedTopicsFileTab, handleNewChat, isRightPaneMaximized, loadRun])
+ }, [ensureEmailFileTab, ensureMeetingsFileTab, ensureLiveNotesFileTab, ensureFileTabForPath, ensureGraphFileTab, ensureSuggestedTopicsFileTab, ensureWorkspaceFileTab, handleNewChat, isRightPaneMaximized, loadRun])
const navigateToView = useCallback(async (nextView: ViewState) => {
const current = currentViewState
@@ -3780,7 +3870,7 @@ function App() {
}, [])
// Keyboard shortcut: Ctrl+L to toggle main chat view
- const isFullScreenChat = !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !selectedBackgroundTask && !isBrowserOpen
+ const isFullScreenChat = !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !selectedBackgroundTask && !isBrowserOpen
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'l') {
@@ -3865,11 +3955,11 @@ function App() {
const handleTabKeyDown = (e: KeyboardEvent) => {
const mod = e.metaKey || e.ctrlKey
if (!mod) return
- const rightPaneAvailable = Boolean((selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen) && isChatSidebarOpen)
+ const rightPaneAvailable = Boolean((selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen) && isChatSidebarOpen)
const targetPane: ShortcutPane = rightPaneAvailable
? (isRightPaneMaximized ? 'right' : activeShortcutPane)
: 'left'
- const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen)
+ const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen)
const selectedKnowledgePath = isGraphOpen
? GRAPH_TAB_PATH
: isSuggestedTopicsOpen
@@ -3882,6 +3972,8 @@ function App() {
? BG_TASKS_TAB_PATH
: isEmailOpen
? EMAIL_TAB_PATH
+ : isWorkspaceOpen
+ ? WORKSPACE_TAB_PATH
: selectedPath
const targetFileTabId = activeFileTabId ?? (
selectedKnowledgePath
@@ -3936,7 +4028,7 @@ function App() {
}
document.addEventListener('keydown', handleTabKeyDown)
return () => document.removeEventListener('keydown', handleTabKeyDown)
- }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab])
+ }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab])
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
if (kind === 'file') {
@@ -3961,7 +4053,7 @@ function App() {
}),
},
}))
- if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !selectedBackgroundTask) {
+ if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !selectedBackgroundTask) {
setIsChatSidebarOpen(false)
setIsRightPaneMaximized(false)
}
@@ -4087,19 +4179,42 @@ function App() {
},
openGraph: () => {
// From chat-only landing state, open graph directly in full knowledge view.
- if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !selectedBackgroundTask) {
+ if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !selectedBackgroundTask) {
setIsChatSidebarOpen(false)
setIsRightPaneMaximized(false)
}
void navigateToView({ type: 'graph' })
},
openBases: () => {
- if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !selectedBackgroundTask) {
+ if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !selectedBackgroundTask) {
setIsChatSidebarOpen(false)
setIsRightPaneMaximized(false)
}
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
},
+ openWorkspaceAt: (path?: string) => {
+ if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !selectedBackgroundTask) {
+ setIsChatSidebarOpen(false)
+ setIsRightPaneMaximized(false)
+ }
+ void navigateToView({ type: 'workspace', path })
+ },
+ createWorkspace: async (name: string): Promise => {
+ const trimmed = name.trim()
+ if (!trimmed) throw new Error('Name is required')
+ if (trimmed.includes('/')) throw new Error('Name cannot contain "/"')
+ const rootExists = await window.ipc.invoke('workspace:exists', { path: WORKSPACE_ROOT })
+ if (!rootExists.exists) {
+ await window.ipc.invoke('workspace:mkdir', { path: WORKSPACE_ROOT, recursive: true })
+ }
+ const target = `${WORKSPACE_ROOT}/${trimmed}`
+ const exists = await window.ipc.invoke('workspace:exists', { path: target })
+ if (exists.exists) {
+ throw new Error(`A workspace named "${trimmed}" already exists`)
+ }
+ await window.ipc.invoke('workspace:mkdir', { path: target, recursive: true })
+ return target
+ },
expandAll: () => setExpandedPaths(new Set(collectDirPaths(tree))),
collapseAll: () => setExpandedPaths(new Set()),
rename: async (oldPath: string, newName: string, isDir: boolean) => {
@@ -4690,7 +4805,7 @@ function App() {
const selectedTask = selectedBackgroundTask
? backgroundTasks.find(t => t.name === selectedBackgroundTask)
: null
- const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isBrowserOpen)
+ const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isBrowserOpen)
const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized
const shouldCollapseLeftPane = isRightPaneOnlyMode
const openMarkdownTabs = React.useMemo(() => {
@@ -4707,7 +4822,7 @@ function App() {
return (
{
- if (section === 'knowledge' && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen) {
+ if (section === 'knowledge' && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen) {
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
}
}}>
@@ -4740,7 +4855,7 @@ function App() {
onNewChat: handleNewChatTab,
onSelectRun: (runIdToLoad) => {
cancelRecordingIfActive()
- if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isBrowserOpen) {
+ if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isBrowserOpen) {
setIsChatSidebarOpen(true)
}
@@ -4751,7 +4866,7 @@ function App() {
return
}
// In two-pane mode (file/graph/browser), keep the middle pane and just swap chat context in the right sidebar.
- if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isBrowserOpen) {
+ if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isBrowserOpen) {
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: runIdToLoad } : t))
loadRun(runIdToLoad)
return
@@ -4775,14 +4890,14 @@ function App() {
} else {
// Only one tab, reset it to new chat
setChatTabs([{ id: tabForRun.id, runId: null }])
- if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isBrowserOpen) {
+ if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isBrowserOpen) {
handleNewChat()
} else {
void navigateToView({ type: 'chat', runId: null })
}
}
} else if (runId === runIdToDelete) {
- if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isBrowserOpen) {
+ if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isBrowserOpen) {
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: null } : t))
handleNewChat()
} else {
@@ -4800,8 +4915,6 @@ function App() {
}}
backgroundTasks={backgroundTasks}
selectedBackgroundTask={selectedBackgroundTask}
- onNewChat={handleNewChatTab}
- onOpenSearch={() => setIsSearchOpen(true)}
isSearchOpen={isSearchOpen}
isBrowserOpen={isBrowserOpen}
onToggleBrowser={handleToggleBrowser}
@@ -4809,8 +4922,6 @@ function App() {
onOpenSuggestedTopics={() => void navigateToView({ type: 'suggested-topics' })}
isMeetingsOpen={isMeetingsOpen}
onOpenMeetings={openMeetingsView}
- isLiveNotesOpen={isLiveNotesOpen}
- onOpenLiveNotes={() => void navigateToView({ type: 'live-notes' })}
isBgTasksOpen={isBgTasksOpen}
onOpenBgTasks={openBgTasksView}
isEmailOpen={isEmailOpen}
@@ -4834,7 +4945,7 @@ function App() {
canNavigateForward={canNavigateForward}
collapsedLeftPaddingPx={collapsedLeftPaddingPx}
>
- {(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen) && fileTabs.length >= 1 ? (
+ {(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen) && fileTabs.length >= 1 ? (
t.id}
onSwitchTab={switchFileTab}
onCloseTab={closeFileTab}
- allowSingleTabClose={fileTabs.length === 1 && (isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || (selectedPath != null && isBaseFilePath(selectedPath)))}
+ allowSingleTabClose={fileTabs.length === 1 && (isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || (selectedPath != null && isBaseFilePath(selectedPath)))}
/>
) : (
Version history
)}
- {!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !selectedTask && !isBrowserOpen && (
+ {!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !selectedTask && !isBrowserOpen && (
)}
- {!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isBrowserOpen && expandedFrom && (
+ {!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isBrowserOpen && expandedFrom && (
)}
- {(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen) && (
+ {(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen) && (
)}
+ {/* Secondary quick actions (above bottom divider) */}
+ {(onToggleBrowser || onOpenSuggestedTopics) && (
+
+
+ {onToggleBrowser && (
+
+
+ Run browser task
+
+ )}
+ {onOpenSuggestedTopics && (
+
+
+ Suggested Topics
+
+ )}
+
+
+ )}
{/* Bottom actions */}
@@ -1109,10 +1048,18 @@ function KnowledgeSection({
const treeContainerRef = React.useRef
(null)
const [selectedFolderPath, setSelectedFolderPath] = useState(null)
const [renameTarget, setRenameTarget] = useState(null)
+ const [knowledgeTab, setKnowledgeTab] = useState<'notes' | 'workspaces'>('notes')
const visibleTree = React.useMemo(
- () => tree.filter((item) => item.path !== 'knowledge/Meetings'),
+ () => tree.filter((item) => item.path !== 'knowledge/Meetings' && item.path !== 'knowledge/Workspace'),
[tree],
)
+ const workspaceFolders = React.useMemo(() => {
+ const root = tree.find((item) => item.path === 'knowledge/Workspace')
+ const children = root?.children ?? []
+ return [...children]
+ .filter((c) => c.kind === 'dir')
+ .sort((a, b) => a.name.localeCompare(b.name))
+ }, [tree])
useEffect(() => {
if (!selectedPath) return
@@ -1184,75 +1131,267 @@ function KnowledgeSection({
]
return (
-
-
-
-
- {quickActions.map((action) => (
-
-
-
-
-
-
- {action.label}
-
- ))}
-
-
-
-
- {isExpanded ? (
-
- ) : (
-
- )}
-
-
-
- {isExpanded ? "Collapse All" : "Expand All"}
-
-
-
-
-
-
- {visibleTree.map((item, index) => (
- setRenameTarget(null)}
- />
+
+
+ Knowledge
+
+
+
+ setKnowledgeTab('notes')}
+ className={cn(
+ "flex-1 rounded px-2 py-1 text-xs font-medium transition-colors",
+ knowledgeTab === 'notes'
+ ? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
+ : "text-sidebar-foreground/70 hover:text-sidebar-foreground"
+ )}
+ >
+ Notes
+
+ setKnowledgeTab('workspaces')}
+ className={cn(
+ "flex-1 rounded px-2 py-1 text-xs font-medium transition-colors",
+ knowledgeTab === 'workspaces'
+ ? "bg-sidebar-accent text-sidebar-accent-foreground shadow-sm"
+ : "text-sidebar-foreground/70 hover:text-sidebar-foreground"
+ )}
+ >
+ Workspaces
+
+
+
+ {knowledgeTab === 'notes' ? (
+
+
+
+
+ {quickActions.map((action) => (
+
+
+
+
+
+
+ {action.label}
+
))}
-
+
+
+
+
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isExpanded ? "Collapse All" : "Expand All"}
+
+
+
+
+
+
+ {visibleTree.map((item, index) => (
+ setRenameTarget(null)}
+ />
+ ))}
+
+
+
-
-
-
-
- wrappedActions.createNote()}>
-
- New Note
-
- void wrappedActions.createFolder()}>
-
- New Folder
-
-
-
+
+
+ wrappedActions.createNote()}>
+
+ New Note
+
+ void wrappedActions.createFolder()}>
+
+ New Folder
+
+
+
+ ) : (
+ actions.openWorkspaceAt(path)}
+ onOpenRoot={() => actions.openWorkspaceAt()}
+ onCreate={actions.createWorkspace}
+ onRevealInFileManager={() => actions.revealInFileManager('knowledge/Workspace', true)}
+ />
+ )}
+
+ )
+}
+
+function WorkspacesPanel({
+ workspaces,
+ onOpen,
+ onOpenRoot,
+ onCreate,
+ onRevealInFileManager,
+}: {
+ workspaces: TreeNode[]
+ onOpen: (path: string) => void
+ onOpenRoot: () => void
+ onCreate: (name: string) => Promise
+ onRevealInFileManager: () => void
+}) {
+ const fileManagerName = getFileManagerName()
+ const [addOpen, setAddOpen] = useState(false)
+ const [newName, setNewName] = useState('')
+ const [creating, setCreating] = useState(false)
+ const [error, setError] = useState(null)
+
+ const resetDialog = useCallback(() => {
+ setNewName('')
+ setError(null)
+ setCreating(false)
+ }, [])
+
+ const handleCreate = useCallback(async () => {
+ setCreating(true)
+ setError(null)
+ try {
+ const newPath = await onCreate(newName)
+ setAddOpen(false)
+ resetDialog()
+ onOpen(newPath)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to create workspace')
+ setCreating(false)
+ }
+ }, [newName, onCreate, onOpen, resetDialog])
+
+ return (
+ <>
+
+
+
+ setAddOpen(true)}
+ className="text-sidebar-foreground/70 hover:text-sidebar-foreground hover:bg-sidebar-accent rounded p-1.5 transition-colors"
+ >
+
+
+
+ Add workspace
+
+
+
+
+
+
+
+ Workspace view
+
+
+
+
+
+
+
+ Open in {fileManagerName}
+
+
+
+
+ {workspaces.length === 0 ? (
+
+ No workspaces yet.
+
+ ) : (
+ workspaces.map((ws) => (
+
+ onOpen(ws.path)}>
+
+ {ws.name}
+
+
+ ))
+ )}
+
+
+
+ >
)
}
@@ -1608,8 +1747,8 @@ function TasksSection({
const [pendingDeleteRunId, setPendingDeleteRunId] = useState(null)
return (
-
-
+
+
{/* Background Tasks Section */}
{backgroundTasks.length > 0 && (
<>
diff --git a/apps/x/apps/renderer/src/components/workspace-view.tsx b/apps/x/apps/renderer/src/components/workspace-view.tsx
new file mode 100644
index 00000000..4001140c
--- /dev/null
+++ b/apps/x/apps/renderer/src/components/workspace-view.tsx
@@ -0,0 +1,262 @@
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import { ChevronRight, File as FileIcon, Folder as FolderIcon, Home, Plus } from 'lucide-react'
+
+import { Button } from '@/components/ui/button'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@/components/ui/dialog'
+import { Input } from '@/components/ui/input'
+import { cn } from '@/lib/utils'
+
+const WORKSPACE_ROOT = 'knowledge/Workspace'
+
+interface TreeNode {
+ path: string
+ name: string
+ kind: 'file' | 'dir'
+ children?: TreeNode[]
+}
+
+type WorkspaceViewProps = {
+ tree: TreeNode[]
+ initialPath?: string | null
+ onOpenNote: (path: string) => void
+ onCreateWorkspace: (name: string) => Promise
+}
+
+function findNode(nodes: TreeNode[] | undefined, path: string): TreeNode | null {
+ if (!nodes) return null
+ for (const node of nodes) {
+ if (node.path === path) return node
+ if (node.kind === 'dir' && path.startsWith(`${node.path}/`)) {
+ const found = findNode(node.children, path)
+ if (found) return found
+ }
+ }
+ return null
+}
+
+function countChildren(node: TreeNode | null): number {
+ if (!node || node.kind !== 'dir' || !node.children) return 0
+ return node.children.length
+}
+
+export function WorkspaceView({ tree, initialPath, onOpenNote, onCreateWorkspace }: WorkspaceViewProps) {
+ const [currentPath, setCurrentPath] = useState(initialPath || WORKSPACE_ROOT)
+ const [addOpen, setAddOpen] = useState(false)
+ const [newName, setNewName] = useState('')
+ const [creating, setCreating] = useState(false)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ if (initialPath) setCurrentPath(initialPath)
+ }, [initialPath])
+
+ const isRoot = currentPath === WORKSPACE_ROOT
+
+ const currentNode = useMemo(() => findNode(tree, currentPath), [tree, currentPath])
+
+ const items = useMemo(() => {
+ const children = currentNode?.children ?? []
+ const filtered = isRoot ? children.filter((c) => c.kind === 'dir') : children
+ return [...filtered].sort((a, b) => {
+ if (a.kind !== b.kind) return a.kind === 'dir' ? -1 : 1
+ return a.name.localeCompare(b.name)
+ })
+ }, [currentNode, isRoot])
+
+ const breadcrumbs = useMemo(() => {
+ if (isRoot) return [] as { path: string; name: string }[]
+ const rel = currentPath.slice(WORKSPACE_ROOT.length + 1)
+ const parts = rel.split('/').filter(Boolean)
+ let acc = WORKSPACE_ROOT
+ return parts.map((seg) => {
+ acc = `${acc}/${seg}`
+ return { path: acc, name: seg }
+ })
+ }, [currentPath, isRoot])
+
+ const handleItemClick = useCallback(
+ (item: TreeNode) => {
+ if (item.kind === 'dir') {
+ setCurrentPath(item.path)
+ } else {
+ onOpenNote(item.path)
+ }
+ },
+ [onOpenNote],
+ )
+
+ const resetAddDialog = useCallback(() => {
+ setNewName('')
+ setError(null)
+ setCreating(false)
+ }, [])
+
+ const handleCreate = useCallback(async () => {
+ const trimmed = newName.trim()
+ if (!trimmed) {
+ setError('Name is required')
+ return
+ }
+ if (trimmed.includes('/')) {
+ setError('Name cannot contain "/"')
+ return
+ }
+ setCreating(true)
+ setError(null)
+ try {
+ await onCreateWorkspace(trimmed)
+ setAddOpen(false)
+ resetAddDialog()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to create workspace')
+ setCreating(false)
+ }
+ }, [newName, onCreateWorkspace, resetAddDialog])
+
+ return (
+
+
+
+ setCurrentPath(WORKSPACE_ROOT)}
+ className={cn(
+ 'inline-flex items-center gap-1.5 rounded-md px-2 py-1 transition-colors',
+ isRoot ? 'text-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-accent',
+ )}
+ >
+
+ Workspace
+
+ {breadcrumbs.map((crumb, idx) => {
+ const isLast = idx === breadcrumbs.length - 1
+ return (
+
+
+ {isLast ? (
+
+ {crumb.name}
+
+ ) : (
+ setCurrentPath(crumb.path)}
+ className="rounded-md px-2 py-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground truncate"
+ >
+ {crumb.name}
+
+ )}
+
+ )
+ })}
+
+ {isRoot && (
+
setAddOpen(true)}>
+
+ Add workspace
+
+ )}
+
+
+
+ {items.length === 0 ? (
+
+
+
+ {isRoot
+ ? 'No workspaces yet. Create one to get started.'
+ : 'This folder is empty.'}
+
+ {isRoot && (
+
setAddOpen(true)}>
+
+ Add workspace
+
+ )}
+
+ ) : (
+
+ {items.map((item) => {
+ const childCount = item.kind === 'dir' ? countChildren(item) : 0
+ const Icon = item.kind === 'dir' ? FolderIcon : FileIcon
+ return (
+
handleItemClick(item)}
+ className="group flex flex-col items-start gap-2 rounded-lg border border-border bg-card p-4 text-left transition-colors hover:border-foreground/20 hover:bg-accent"
+ >
+
+
+
{item.name}
+ {item.kind === 'dir' && (
+
+ {childCount} {childCount === 1 ? 'item' : 'items'}
+
+ )}
+
+
+ )
+ })}
+
+ )}
+
+
+
+
+ )
+}
From 1b2ed1bc2e80eb775229d2843d8c8c63921feba8 Mon Sep 17 00:00:00 2001
From: Arjun <6592213+arkml@users.noreply.github.com>
Date: Tue, 19 May 2026 10:31:37 +0530
Subject: [PATCH 04/12] default to workspace in set directory
---
.../src/components/chat-input-with-mentions.tsx | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx
index ccd96805..97386508 100644
--- a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx
+++ b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx
@@ -274,9 +274,21 @@ function ChatInputInner({
const handleSetWorkDir = useCallback(async () => {
try {
+ let defaultPath: string | undefined = workDir ?? undefined
+ try {
+ const { root } = await window.ipc.invoke('workspace:getRoot', null)
+ const workspaceRel = 'knowledge/Workspace'
+ const exists = await window.ipc.invoke('workspace:exists', { path: workspaceRel })
+ if (!exists.exists) {
+ await window.ipc.invoke('workspace:mkdir', { path: workspaceRel, recursive: true })
+ }
+ defaultPath = `${root.replace(/\/$/, '')}/${workspaceRel}`
+ } catch (err) {
+ console.error('Failed to resolve Workspace path; falling back to current workDir', err)
+ }
const { path: chosen } = await window.ipc.invoke('dialog:openDirectory', {
title: 'Choose work directory',
- defaultPath: workDir ?? undefined,
+ defaultPath,
})
if (!chosen) return
await window.ipc.invoke('workspace:writeFile', {
From 43537c5e20bf45ee6b11997618e78a3b6e3f587d Mon Sep 17 00:00:00 2001
From: Arjun <6592213+arkml@users.noreply.github.com>
Date: Tue, 19 May 2026 18:24:48 +0530
Subject: [PATCH 05/12] view more in knowledge
---
apps/x/apps/renderer/src/App.tsx | 205 ++++--
.../src/components/html-file-viewer.tsx | 58 +-
.../src/components/knowledge-view.tsx | 405 ++++++++++++
.../src/components/sidebar-content.tsx | 610 +++---------------
4 files changed, 656 insertions(+), 622 deletions(-)
create mode 100644 apps/x/apps/renderer/src/components/knowledge-view.tsx
diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx
index 558e19e3..1544306c 100644
--- a/apps/x/apps/renderer/src/App.tsx
+++ b/apps/x/apps/renderer/src/App.tsx
@@ -26,6 +26,7 @@ import { LiveNotesView } from '@/components/live-notes-view';
import { BgTasksView } from '@/components/bg-tasks-view';
import { EmailView } from '@/components/email-view';
import { WorkspaceView } from '@/components/workspace-view';
+import { KnowledgeView } from '@/components/knowledge-view';
import { MeetingsView } from '@/components/meetings-view';
import { SidebarSectionProvider } from '@/contexts/sidebar-context';
import {
@@ -185,6 +186,7 @@ const BG_TASKS_TAB_PATH = '__rowboat_bg_tasks__'
const EMAIL_TAB_PATH = '__rowboat_email__'
const WORKSPACE_TAB_PATH = '__rowboat_workspace__'
const WORKSPACE_ROOT = 'knowledge/Workspace'
+const KNOWLEDGE_VIEW_TAB_PATH = '__rowboat_knowledge_view__'
const BASES_DEFAULT_TAB_PATH = '__rowboat_bases_default__'
const clampNumber = (value: number, min: number, max: number) =>
@@ -319,6 +321,7 @@ const isLiveNotesTabPath = (path: string) => path === LIVE_NOTES_TAB_PATH
const isBgTasksTabPath = (path: string) => path === BG_TASKS_TAB_PATH
const isEmailTabPath = (path: string) => path === EMAIL_TAB_PATH
const isWorkspaceTabPath = (path: string) => path === WORKSPACE_TAB_PATH
+const isKnowledgeViewTabPath = (path: string) => path === KNOWLEDGE_VIEW_TAB_PATH
const isBaseFilePath = (path: string) => path.endsWith('.base') || path === BASES_DEFAULT_TAB_PATH
const getSuggestedTopicTargetFolder = (category?: string) => {
@@ -570,6 +573,7 @@ type ViewState =
| { type: 'live-notes' }
| { type: 'email' }
| { type: 'workspace'; path?: string }
+ | { type: 'knowledge-view' }
function viewStatesEqual(a: ViewState, b: ViewState): boolean {
if (a.type !== b.type) return false
@@ -624,6 +628,8 @@ function parseDeepLink(input: string): ViewState | null {
const path = params.get('path')
return { type: 'workspace', path: path ?? undefined }
}
+ case 'knowledge-view':
+ return { type: 'knowledge-view' }
default:
return null
}
@@ -761,6 +767,7 @@ function App() {
const [isEmailOpen, setIsEmailOpen] = useState(false)
const [isWorkspaceOpen, setIsWorkspaceOpen] = useState(false)
const [workspaceInitialPath, setWorkspaceInitialPath] = useState(null)
+ const [isKnowledgeViewOpen, setIsKnowledgeViewOpen] = useState(false)
const [expandedFrom, setExpandedFrom] = useState<{
path: string | null
graph: boolean
@@ -1098,6 +1105,7 @@ function App() {
if (isBgTasksTabPath(tab.path)) return 'Background tasks'
if (isEmailTabPath(tab.path)) return 'Email'
if (isWorkspaceTabPath(tab.path)) return 'Workspace'
+ if (isKnowledgeViewTabPath(tab.path)) return 'Knowledge'
if (tab.path === BASES_DEFAULT_TAB_PATH) return 'Bases'
if (tab.path.endsWith('.base')) return tab.path.split('/').pop()?.replace(/\.base$/i, '') || 'Base'
return tab.path.split('/').pop()?.replace(/\.md$/i, '') || tab.path
@@ -2810,7 +2818,7 @@ function App() {
setActiveFileTabId(existingTab.id)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
setSelectedPath(path)
return
}
@@ -2819,7 +2827,7 @@ function App() {
setActiveFileTabId(id)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
setSelectedPath(path)
}, [fileTabs, dismissBrowserOverlay])
@@ -2838,14 +2846,14 @@ function App() {
setSelectedPath(null)
setIsGraphOpen(true)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
return
}
if (isSuggestedTopicsTabPath(tab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(true)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
return
}
if (isLiveNotesTabPath(tab.path)) {
@@ -2856,6 +2864,7 @@ function App() {
setIsBgTasksOpen(false)
setIsEmailOpen(false)
setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setIsLiveNotesOpen(true)
return
}
@@ -2867,6 +2876,7 @@ function App() {
setIsLiveNotesOpen(false)
setIsEmailOpen(false)
setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setIsBgTasksOpen(true)
return
}
@@ -2879,6 +2889,7 @@ function App() {
setIsBgTasksOpen(false)
setIsEmailOpen(false)
setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
return
}
if (isEmailTabPath(tab.path)) {
@@ -2889,6 +2900,7 @@ function App() {
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setIsEmailOpen(true)
return
}
@@ -2900,18 +2912,31 @@ function App() {
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
+ setIsKnowledgeViewOpen(false)
setIsWorkspaceOpen(true)
return
}
+ if (isKnowledgeViewTabPath(tab.path)) {
+ setSelectedPath(null)
+ setIsGraphOpen(false)
+ setIsSuggestedTopicsOpen(false)
+ setIsMeetingsOpen(false)
+ setIsLiveNotesOpen(false)
+ setIsBgTasksOpen(false)
+ setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(true)
+ return
+ }
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
setSelectedPath(tab.path)
}, [fileTabs, isRightPaneMaximized, dismissBrowserOverlay])
const closeFileTab = useCallback((tabId: string) => {
const closingTab = fileTabs.find(t => t.id === tabId)
- if (closingTab && !isGraphTabPath(closingTab.path) && !isSuggestedTopicsTabPath(closingTab.path) && !isLiveNotesTabPath(closingTab.path) && !isBgTasksTabPath(closingTab.path) && !isEmailTabPath(closingTab.path) && !isWorkspaceTabPath(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) && !isBaseFilePath(closingTab.path)) {
removeEditorCacheForPath(closingTab.path)
initialContentByPathRef.current.delete(closingTab.path)
untitledRenameReadyPathsRef.current.delete(closingTab.path)
@@ -2934,7 +2959,7 @@ function App() {
setSelectedPath(null)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
return []
}
const idx = prev.findIndex(t => t.id === tabId)
@@ -2948,12 +2973,12 @@ function App() {
setSelectedPath(null)
setIsGraphOpen(true)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
} else if (isSuggestedTopicsTabPath(newActiveTab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(true)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
} else if (isMeetingsTabPath(newActiveTab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
@@ -2963,6 +2988,7 @@ function App() {
setIsBgTasksOpen(false)
setIsEmailOpen(false)
setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
} else if (isLiveNotesTabPath(newActiveTab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
@@ -2971,6 +2997,7 @@ function App() {
setIsBgTasksOpen(false)
setIsEmailOpen(false)
setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setIsLiveNotesOpen(true)
} else if (isBgTasksTabPath(newActiveTab.path)) {
setSelectedPath(null)
@@ -2981,6 +3008,7 @@ function App() {
setIsBgTasksOpen(true)
setIsEmailOpen(false)
setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
} else if (isEmailTabPath(newActiveTab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
@@ -2989,6 +3017,7 @@ function App() {
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setIsEmailOpen(true)
} else if (isWorkspaceTabPath(newActiveTab.path)) {
setSelectedPath(null)
@@ -2998,11 +3027,22 @@ function App() {
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
+ setIsKnowledgeViewOpen(false)
setIsWorkspaceOpen(true)
+ } else if (isKnowledgeViewTabPath(newActiveTab.path)) {
+ setSelectedPath(null)
+ setIsGraphOpen(false)
+ setIsSuggestedTopicsOpen(false)
+ setIsMeetingsOpen(false)
+ setIsLiveNotesOpen(false)
+ setIsBgTasksOpen(false)
+ setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(true)
} else {
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
setSelectedPath(newActiveTab.path)
}
}
@@ -3033,7 +3073,7 @@ function App() {
dismissBrowserOverlay()
handleNewChat()
// Left-pane "new chat" should always open full chat view.
- if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen) {
+ if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen) {
setExpandedFrom({
path: selectedPath,
graph: isGraphOpen,
@@ -3050,8 +3090,8 @@ function App() {
setSelectedPath(null)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
- }, [chatTabs, activeChatTabId, dismissBrowserOverlay, handleNewChat, selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen])
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
+ }, [chatTabs, activeChatTabId, dismissBrowserOverlay, handleNewChat, selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen])
// Sidebar variant: create/switch chat tab without leaving file/graph context.
const handleNewChatTabInSidebar = useCallback(() => {
@@ -3183,7 +3223,7 @@ function App() {
const handleOpenFullScreenChat = useCallback(() => {
// Remember where we came from so the close button can return
- if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen) {
+ if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen) {
setExpandedFrom({
path: selectedPath,
graph: isGraphOpen,
@@ -3199,19 +3239,19 @@ function App() {
setSelectedPath(null)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
- }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, dismissBrowserOverlay])
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
+ }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, dismissBrowserOverlay])
const handleCloseFullScreenChat = useCallback(() => {
if (expandedFrom) {
if (expandedFrom.graph) {
setIsGraphOpen(true)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
} else if (expandedFrom.suggestedTopics) {
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(true)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
} else if (expandedFrom.meetings) {
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
@@ -3243,7 +3283,7 @@ function App() {
} else if (expandedFrom.path) {
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
setSelectedPath(expandedFrom.path)
}
setExpandedFrom(null)
@@ -3258,10 +3298,11 @@ function App() {
if (isLiveNotesOpen) return { type: 'live-notes' }
if (isSuggestedTopicsOpen) return { type: 'suggested-topics' }
if (isWorkspaceOpen) return { type: 'workspace', path: workspaceInitialPath ?? undefined }
+ if (isKnowledgeViewOpen) return { type: 'knowledge-view' }
if (selectedPath) return { type: 'file', path: selectedPath }
if (isGraphOpen) return { type: 'graph' }
return { type: 'chat', runId }
- }, [selectedBackgroundTask, isEmailOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isSuggestedTopicsOpen, selectedPath, isGraphOpen, isWorkspaceOpen, workspaceInitialPath, runId])
+ }, [selectedBackgroundTask, isEmailOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isSuggestedTopicsOpen, selectedPath, isGraphOpen, isWorkspaceOpen, isKnowledgeViewOpen, workspaceInitialPath, runId])
const appendUnique = useCallback((stack: ViewState[], entry: ViewState) => {
const last = stack[stack.length - 1]
@@ -3373,6 +3414,17 @@ function App() {
setActiveFileTabId(id)
}, [fileTabs])
+ const ensureKnowledgeViewFileTab = useCallback(() => {
+ const existing = fileTabs.find((tab) => isKnowledgeViewTabPath(tab.path))
+ if (existing) {
+ setActiveFileTabId(existing.id)
+ return
+ }
+ const id = newFileTabId()
+ setFileTabs((prev) => [...prev, { id, path: KNOWLEDGE_VIEW_TAB_PATH }])
+ setActiveFileTabId(id)
+ }, [fileTabs])
+
const openEmailView = useCallback(() => {
setSelectedPath(null)
setIsGraphOpen(false)
@@ -3382,6 +3434,7 @@ function App() {
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setSelectedBackgroundTask(null)
setExpandedFrom(null)
setIsRightPaneMaximized(false)
@@ -3394,7 +3447,7 @@ function App() {
setIsGraphOpen(false)
setIsBrowserOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
setSelectedBackgroundTask(null)
setExpandedFrom(null)
setIsRightPaneMaximized(false)
@@ -3412,6 +3465,7 @@ function App() {
setIsBgTasksOpen(false)
setIsEmailOpen(false)
setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setSelectedBackgroundTask(null)
setExpandedFrom(null)
setIsRightPaneMaximized(false)
@@ -3427,7 +3481,7 @@ function App() {
// visible in the middle pane.
setIsBrowserOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
setExpandedFrom(null)
// Preserve split vs knowledge-max mode when navigating knowledge files.
// Only exit chat-only maximize, because that would hide the selected file.
@@ -3442,7 +3496,7 @@ function App() {
setSelectedPath(null)
setIsBrowserOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
setExpandedFrom(null)
setIsGraphOpen(true)
ensureGraphFileTab()
@@ -3455,7 +3509,7 @@ function App() {
setIsGraphOpen(false)
setIsBrowserOpen(false)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
setExpandedFrom(null)
setIsRightPaneMaximized(false)
setSelectedBackgroundTask(view.name)
@@ -3468,7 +3522,7 @@ function App() {
setIsRightPaneMaximized(false)
setSelectedBackgroundTask(null)
setIsSuggestedTopicsOpen(true)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
ensureSuggestedTopicsFileTab()
return
case 'meetings':
@@ -3484,6 +3538,7 @@ function App() {
setIsBgTasksOpen(false)
setIsEmailOpen(false)
setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
ensureMeetingsFileTab()
return
case 'live-notes':
@@ -3498,6 +3553,7 @@ function App() {
setIsBgTasksOpen(false)
setIsEmailOpen(false)
setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setIsLiveNotesOpen(true)
ensureLiveNotesFileTab()
return
@@ -3514,6 +3570,7 @@ function App() {
setIsBgTasksOpen(false)
setIsEmailOpen(true)
setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
ensureEmailFileTab()
return
case 'workspace':
@@ -3529,9 +3586,26 @@ function App() {
setIsBgTasksOpen(false)
setIsEmailOpen(false)
setIsWorkspaceOpen(true)
+ setIsKnowledgeViewOpen(false)
setWorkspaceInitialPath(view.path ?? null)
ensureWorkspaceFileTab()
return
+ case 'knowledge-view':
+ 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(true)
+ ensureKnowledgeViewFileTab()
+ return
case 'chat':
setSelectedPath(null)
setIsGraphOpen(false)
@@ -3540,7 +3614,7 @@ function App() {
setIsRightPaneMaximized(false)
setSelectedBackgroundTask(null)
setIsSuggestedTopicsOpen(false)
- setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
if (view.runId) {
await loadRun(view.runId)
} else {
@@ -3548,7 +3622,7 @@ function App() {
}
return
}
- }, [ensureEmailFileTab, ensureMeetingsFileTab, ensureLiveNotesFileTab, ensureFileTabForPath, ensureGraphFileTab, ensureSuggestedTopicsFileTab, ensureWorkspaceFileTab, handleNewChat, isRightPaneMaximized, loadRun])
+ }, [ensureEmailFileTab, ensureMeetingsFileTab, ensureLiveNotesFileTab, ensureFileTabForPath, ensureGraphFileTab, ensureSuggestedTopicsFileTab, ensureWorkspaceFileTab, ensureKnowledgeViewFileTab, handleNewChat, isRightPaneMaximized, loadRun])
const navigateToView = useCallback(async (nextView: ViewState) => {
const current = currentViewState
@@ -3870,7 +3944,7 @@ function App() {
}, [])
// Keyboard shortcut: Ctrl+L to toggle main chat view
- const isFullScreenChat = !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !selectedBackgroundTask && !isBrowserOpen
+ const isFullScreenChat = !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedBackgroundTask && !isBrowserOpen
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'l') {
@@ -3955,11 +4029,11 @@ function App() {
const handleTabKeyDown = (e: KeyboardEvent) => {
const mod = e.metaKey || e.ctrlKey
if (!mod) return
- const rightPaneAvailable = Boolean((selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen) && isChatSidebarOpen)
+ const rightPaneAvailable = Boolean((selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen) && isChatSidebarOpen)
const targetPane: ShortcutPane = rightPaneAvailable
? (isRightPaneMaximized ? 'right' : activeShortcutPane)
: 'left'
- const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen)
+ const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen)
const selectedKnowledgePath = isGraphOpen
? GRAPH_TAB_PATH
: isSuggestedTopicsOpen
@@ -3974,6 +4048,8 @@ function App() {
? EMAIL_TAB_PATH
: isWorkspaceOpen
? WORKSPACE_TAB_PATH
+ : isKnowledgeViewOpen
+ ? KNOWLEDGE_VIEW_TAB_PATH
: selectedPath
const targetFileTabId = activeFileTabId ?? (
selectedKnowledgePath
@@ -4028,7 +4104,7 @@ function App() {
}
document.addEventListener('keydown', handleTabKeyDown)
return () => document.removeEventListener('keydown', handleTabKeyDown)
- }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab])
+ }, [selectedPath, isGraphOpen, isSuggestedTopicsOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isEmailOpen, isWorkspaceOpen, isKnowledgeViewOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab])
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
if (kind === 'file') {
@@ -4053,7 +4129,7 @@ function App() {
}),
},
}))
- if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !selectedBackgroundTask) {
+ if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedBackgroundTask) {
setIsChatSidebarOpen(false)
setIsRightPaneMaximized(false)
}
@@ -4179,26 +4255,33 @@ function App() {
},
openGraph: () => {
// From chat-only landing state, open graph directly in full knowledge view.
- if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !selectedBackgroundTask) {
+ if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedBackgroundTask) {
setIsChatSidebarOpen(false)
setIsRightPaneMaximized(false)
}
void navigateToView({ type: 'graph' })
},
openBases: () => {
- if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !selectedBackgroundTask) {
+ if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedBackgroundTask) {
setIsChatSidebarOpen(false)
setIsRightPaneMaximized(false)
}
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
},
openWorkspaceAt: (path?: string) => {
- if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !selectedBackgroundTask) {
+ if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedBackgroundTask) {
setIsChatSidebarOpen(false)
setIsRightPaneMaximized(false)
}
void navigateToView({ type: 'workspace', path })
},
+ openKnowledgeView: () => {
+ if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedBackgroundTask) {
+ setIsChatSidebarOpen(false)
+ setIsRightPaneMaximized(false)
+ }
+ void navigateToView({ type: 'knowledge-view' })
+ },
createWorkspace: async (name: string): Promise => {
const trimmed = name.trim()
if (!trimmed) throw new Error('Name is required')
@@ -4805,7 +4888,7 @@ function App() {
const selectedTask = selectedBackgroundTask
? backgroundTasks.find(t => t.name === selectedBackgroundTask)
: null
- const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isBrowserOpen)
+ const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isBrowserOpen)
const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized
const shouldCollapseLeftPane = isRightPaneOnlyMode
const openMarkdownTabs = React.useMemo(() => {
@@ -4822,7 +4905,7 @@ function App() {
return (
{
- if (section === 'knowledge' && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen) {
+ if (section === 'knowledge' && !selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen) {
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
}
}}>
@@ -4836,18 +4919,8 @@ function App() {
{
- setExpandedPaths((prev) => {
- const next = new Set(prev)
- if (next.has(path)) next.delete(path)
- else next.add(path)
- return next
- })
- }}
knowledgeActions={knowledgeActions}
- onVoiceNoteCreated={handleVoiceNoteCreated}
runs={runs}
currentRunId={runId}
processingRunIds={processingRunIds}
@@ -4855,7 +4928,7 @@ function App() {
onNewChat: handleNewChatTab,
onSelectRun: (runIdToLoad) => {
cancelRecordingIfActive()
- if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isBrowserOpen) {
+ if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isBrowserOpen) {
setIsChatSidebarOpen(true)
}
@@ -4866,7 +4939,7 @@ function App() {
return
}
// In two-pane mode (file/graph/browser), keep the middle pane and just swap chat context in the right sidebar.
- if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isBrowserOpen) {
+ if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isBrowserOpen) {
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: runIdToLoad } : t))
loadRun(runIdToLoad)
return
@@ -4890,14 +4963,14 @@ function App() {
} else {
// Only one tab, reset it to new chat
setChatTabs([{ id: tabForRun.id, runId: null }])
- if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isBrowserOpen) {
+ if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isBrowserOpen) {
handleNewChat()
} else {
void navigateToView({ type: 'chat', runId: null })
}
}
} else if (runId === runIdToDelete) {
- if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isBrowserOpen) {
+ if (selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isBrowserOpen) {
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: null } : t))
handleNewChat()
} else {
@@ -4945,7 +5018,7 @@ function App() {
canNavigateForward={canNavigateForward}
collapsedLeftPaddingPx={collapsedLeftPaddingPx}
>
- {(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen) && fileTabs.length >= 1 ? (
+ {(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen) && fileTabs.length >= 1 ? (
t.id}
onSwitchTab={switchFileTab}
onCloseTab={closeFileTab}
- allowSingleTabClose={fileTabs.length === 1 && (isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || (selectedPath != null && isBaseFilePath(selectedPath)))}
+ allowSingleTabClose={fileTabs.length === 1 && (isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || (selectedPath != null && isBaseFilePath(selectedPath)))}
/>
) : (
Version history
)}
- {!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !selectedTask && !isBrowserOpen && (
+ {!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedTask && !isBrowserOpen && (
New chat tab
)}
- {!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isBrowserOpen && expandedFrom && (
+ {!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isBrowserOpen && expandedFrom && (
Restore two-pane view
)}
- {(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen) && (
+ {(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen) && (
{ await knowledgeActions.createWorkspace(name) }}
/>
+ ) : isKnowledgeViewOpen ? (
+
+ navigateToFile(path)}
+ onOpenGraph={() => knowledgeActions.openGraph()}
+ onOpenSearch={() => setIsSearchOpen(true)}
+ onOpenBases={() => knowledgeActions.openBases()}
+ onVoiceNoteCreated={handleVoiceNoteCreated}
+ />
+
) : selectedPath && isBaseFilePath(selectedPath) ? (
()
-
-function getCached(path: string, mtimeMs: number, size: number): string | null {
- const entry = htmlCache.get(path)
- if (!entry || entry.mtimeMs !== mtimeMs || entry.size !== size) return null
- // Refresh LRU position
- htmlCache.delete(path)
- htmlCache.set(path, entry)
- return entry.html
-}
-
-function setCached(path: string, html: string, mtimeMs: number, size: number) {
- htmlCache.set(path, { html, mtimeMs, size })
- while (htmlCache.size > CACHE_MAX_ENTRIES) {
- const oldest = htmlCache.keys().next().value
- if (oldest === undefined) break
- htmlCache.delete(oldest)
- }
-}
type ViewerState =
| { kind: 'loading' }
- | { kind: 'loaded'; html: string }
+ | { kind: 'loaded' }
| { kind: 'empty' }
| { kind: 'tooLarge'; sizeMB: number }
| { kind: 'error'; message: string }
@@ -36,9 +14,15 @@ interface HtmlFileViewerProps {
path: string
}
+function toAppWorkspaceUrl(path: string): string {
+ const segments = path.split('/').filter(Boolean).map((seg) => encodeURIComponent(seg))
+ return `app://workspace/${segments.join('/')}`
+}
+
export function HtmlFileViewer({ path }: HtmlFileViewerProps) {
const [state, setState] = useState({ kind: 'loading' })
const [iframeLoaded, setIframeLoaded] = useState(false)
+ const iframeSrc = useMemo(() => toAppWorkspaceUrl(path), [path])
useEffect(() => {
let cancelled = false
@@ -57,19 +41,11 @@ export function HtmlFileViewer({ path }: HtmlFileViewerProps) {
setState({ kind: 'tooLarge', sizeMB: stat.size / (1024 * 1024) })
return
}
- const cachedHtml = getCached(path, stat.mtimeMs, stat.size)
- if (cachedHtml !== null) {
- setState(cachedHtml.trim() === '' ? { kind: 'empty' } : { kind: 'loaded', html: cachedHtml })
- return
- }
- const result = await window.ipc.invoke('workspace:readFile', { path })
- if (cancelled) return
- setCached(path, result.data, stat.mtimeMs, stat.size)
- if (!result.data || result.data.trim() === '') {
+ if (stat.size === 0) {
setState({ kind: 'empty' })
return
}
- setState({ kind: 'loaded', html: result.data })
+ setState({ kind: 'loaded' })
} catch (err) {
if (cancelled) return
const message = err instanceof Error ? err.message : String(err)
@@ -124,20 +100,16 @@ export function HtmlFileViewer({ path }: HtmlFileViewerProps) {
)
}
- // We use `srcDoc` here (not `src=app://workspace/`) so the iframe
- // gets a null origin with no base URL. Trade-off: relative assets inside
- // the file — ``, `
`,
- // `