diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx
index 79a2b266..b641575c 100644
--- a/apps/x/apps/renderer/src/App.tsx
+++ b/apps/x/apps/renderer/src/App.tsx
@@ -5,7 +5,7 @@ import { RunEvent, ListRunsResponse } from '@x/shared/src/runs.js';
import type { LanguageModelUsage, ToolUIPart } from 'ai';
import './App.css'
import z from 'zod';
-import { CheckIcon, LoaderIcon, PanelLeftIcon, Maximize2, Minimize2, ChevronLeftIcon, ChevronRightIcon, SquarePen, HistoryIcon } from 'lucide-react';
+import { CheckIcon, LoaderIcon, PanelLeftIcon, Maximize2, Minimize2, ChevronLeftIcon, ChevronRightIcon, SquarePen, SearchIcon, HistoryIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import { MarkdownEditor, type MarkdownEditorHandle } from './components/markdown-editor';
import { ChatSidebar } from './components/chat-sidebar';
@@ -25,6 +25,8 @@ import { SuggestedTopicsView } from '@/components/suggested-topics-view';
import { LiveNotesView } from '@/components/live-notes-view';
import { BgTasksView } from '@/components/bg-tasks-view';
import { EmailView } from '@/components/email-view';
+import { WorkspaceView } from '@/components/workspace-view';
+import { KnowledgeView } from '@/components/knowledge-view';
import { MeetingsView } from '@/components/meetings-view';
import { SidebarSectionProvider } from '@/contexts/sidebar-context';
import {
@@ -184,6 +186,9 @@ const MEETINGS_TAB_PATH = '__rowboat_meetings__'
const LIVE_NOTES_TAB_PATH = '__rowboat_live_notes__'
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) =>
@@ -317,6 +322,8 @@ const isMeetingsTabPath = (path: string) => path === MEETINGS_TAB_PATH
const isLiveNotesTabPath = (path: string) => path === LIVE_NOTES_TAB_PATH
const isBgTasksTabPath = (path: string) => path === BG_TASKS_TAB_PATH
const isEmailTabPath = (path: string) => path === EMAIL_TAB_PATH
+const 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) => {
@@ -567,12 +574,15 @@ type ViewState =
| { type: 'meetings' }
| { 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
if (a.type === 'chat' && b.type === 'chat') return a.runId === b.runId
if (a.type === 'file' && b.type === 'file') return a.path === b.path
if (a.type === 'task' && b.type === 'task') return a.name === b.name
+ if (a.type === 'workspace' && b.type === 'workspace') return (a.path ?? '') === (b.path ?? '')
return true // both graph
}
@@ -616,6 +626,12 @@ function parseDeepLink(input: string): ViewState | null {
return { type: 'meetings' }
case 'live-notes':
return { type: 'live-notes' }
+ case 'workspace': {
+ const path = params.get('path')
+ return { type: 'workspace', path: path ?? undefined }
+ }
+ case 'knowledge-view':
+ return { type: 'knowledge-view' }
default:
return null
}
@@ -624,12 +640,16 @@ function parseDeepLink(input: string): ViewState | null {
/** Sidebar toggle (fixed position, top-left) */
function FixedSidebarToggle({
leftInsetPx,
+ onNewChat,
+ onOpenSearch,
}: {
leftInsetPx: number
+ onNewChat?: () => void
+ onOpenSearch?: () => void
}) {
const { toggleSidebar } = useSidebar()
return (
-
+
{/* Sidebar toggle */}
+ {onNewChat && (
+
+ )}
+ {onOpenSearch && (
+
+ )}
)
}
@@ -725,6 +767,9 @@ 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 [isKnowledgeViewOpen, setIsKnowledgeViewOpen] = useState(false)
const [expandedFrom, setExpandedFrom] = useState<{
path: string | null
graph: boolean
@@ -1079,6 +1124,8 @@ 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 (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
@@ -2793,7 +2840,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); setIsKnowledgeViewOpen(false)
setSelectedPath(path)
return
}
@@ -2802,7 +2849,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); setIsKnowledgeViewOpen(false)
setSelectedPath(path)
}, [fileTabs, dismissBrowserOverlay])
@@ -2821,14 +2868,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); setIsKnowledgeViewOpen(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); setIsKnowledgeViewOpen(false)
return
}
if (isLiveNotesTabPath(tab.path)) {
@@ -2838,6 +2885,8 @@ function App() {
setIsMeetingsOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setIsLiveNotesOpen(true)
return
}
@@ -2847,6 +2896,9 @@ function App() {
setIsSuggestedTopicsOpen(false)
setIsMeetingsOpen(false)
setIsLiveNotesOpen(false)
+ setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setIsBgTasksOpen(true)
return
}
@@ -2857,26 +2909,56 @@ function App() {
setIsMeetingsOpen(true)
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
+ setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
return
}
if (isEmailTabPath(tab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
+ setIsMeetingsOpen(false)
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
+ setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setIsEmailOpen(true)
return
}
+ if (isWorkspaceTabPath(tab.path)) {
+ setSelectedPath(null)
+ setIsGraphOpen(false)
+ setIsSuggestedTopicsOpen(false)
+ setIsMeetingsOpen(false)
+ 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)
+ 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) && !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)
@@ -2899,7 +2981,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); setIsKnowledgeViewOpen(false)
return []
}
const idx = prev.findIndex(t => t.id === tabId)
@@ -2913,12 +2995,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); setIsKnowledgeViewOpen(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); setIsKnowledgeViewOpen(false)
} else if (isMeetingsTabPath(newActiveTab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
@@ -2927,6 +3009,8 @@ function App() {
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
} else if (isLiveNotesTabPath(newActiveTab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
@@ -2934,6 +3018,8 @@ function App() {
setIsMeetingsOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setIsLiveNotesOpen(true)
} else if (isBgTasksTabPath(newActiveTab.path)) {
setSelectedPath(null)
@@ -2943,6 +3029,8 @@ function App() {
setIsLiveNotesOpen(false)
setIsBgTasksOpen(true)
setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
} else if (isEmailTabPath(newActiveTab.path)) {
setSelectedPath(null)
setIsGraphOpen(false)
@@ -2950,11 +3038,33 @@ function App() {
setIsMeetingsOpen(false)
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
+ setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setIsEmailOpen(true)
+ } else if (isWorkspaceTabPath(newActiveTab.path)) {
+ setSelectedPath(null)
+ setIsGraphOpen(false)
+ setIsSuggestedTopicsOpen(false)
+ setIsMeetingsOpen(false)
+ 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)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
setSelectedPath(newActiveTab.path)
}
}
@@ -2985,7 +3095,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 || isKnowledgeViewOpen) {
setExpandedFrom({
path: selectedPath,
graph: isGraphOpen,
@@ -3002,8 +3112,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); 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(() => {
@@ -3135,7 +3245,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 || isKnowledgeViewOpen) {
setExpandedFrom({
path: selectedPath,
graph: isGraphOpen,
@@ -3151,19 +3261,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); 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)
+ 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)
+ setIsMeetingsOpen(false); setIsLiveNotesOpen(false); setIsBgTasksOpen(false); setIsEmailOpen(false); setIsWorkspaceOpen(false); setIsKnowledgeViewOpen(false)
} else if (expandedFrom.meetings) {
setIsGraphOpen(false)
setIsSuggestedTopicsOpen(false)
@@ -3195,7 +3305,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); setIsKnowledgeViewOpen(false)
setSelectedPath(expandedFrom.path)
}
setExpandedFrom(null)
@@ -3209,10 +3319,12 @@ 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 (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, 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]
@@ -3313,6 +3425,28 @@ 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 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)
@@ -3321,6 +3455,8 @@ function App() {
setIsMeetingsOpen(false)
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
+ setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setSelectedBackgroundTask(null)
setExpandedFrom(null)
setIsRightPaneMaximized(false)
@@ -3333,7 +3469,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); setIsKnowledgeViewOpen(false)
setSelectedBackgroundTask(null)
setExpandedFrom(null)
setIsRightPaneMaximized(false)
@@ -3350,6 +3486,8 @@ function App() {
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setSelectedBackgroundTask(null)
setExpandedFrom(null)
setIsRightPaneMaximized(false)
@@ -3365,7 +3503,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); 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.
@@ -3380,7 +3518,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); setIsKnowledgeViewOpen(false)
setExpandedFrom(null)
setIsGraphOpen(true)
ensureGraphFileTab()
@@ -3393,7 +3531,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); setIsKnowledgeViewOpen(false)
setExpandedFrom(null)
setIsRightPaneMaximized(false)
setSelectedBackgroundTask(view.name)
@@ -3406,7 +3544,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); setIsKnowledgeViewOpen(false)
ensureSuggestedTopicsFileTab()
return
case 'meetings':
@@ -3421,6 +3559,8 @@ function App() {
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
ensureMeetingsFileTab()
return
case 'live-notes':
@@ -3434,6 +3574,8 @@ function App() {
setIsMeetingsOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(false)
+ setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(false)
setIsLiveNotesOpen(true)
ensureLiveNotesFileTab()
return
@@ -3449,8 +3591,43 @@ function App() {
setIsLiveNotesOpen(false)
setIsBgTasksOpen(false)
setIsEmailOpen(true)
+ setIsWorkspaceOpen(false)
+ setIsKnowledgeViewOpen(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)
+ 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)
@@ -3459,7 +3636,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); setIsKnowledgeViewOpen(false)
if (view.runId) {
await loadRun(view.runId)
} else {
@@ -3467,7 +3644,7 @@ function App() {
}
return
}
- }, [ensureEmailFileTab, ensureMeetingsFileTab, ensureLiveNotesFileTab, ensureFileTabForPath, ensureGraphFileTab, ensureSuggestedTopicsFileTab, handleNewChat, isRightPaneMaximized, loadRun])
+ }, [ensureEmailFileTab, ensureMeetingsFileTab, ensureLiveNotesFileTab, ensureFileTabForPath, ensureGraphFileTab, ensureSuggestedTopicsFileTab, ensureWorkspaceFileTab, ensureKnowledgeViewFileTab, handleNewChat, isRightPaneMaximized, loadRun])
const navigateToView = useCallback(async (nextView: ViewState) => {
const current = currentViewState
@@ -3789,7 +3966,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 && !isKnowledgeViewOpen && !selectedBackgroundTask && !isBrowserOpen
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'l') {
@@ -3817,6 +3994,18 @@ function App() {
return () => document.removeEventListener('keydown', handleKeyDown)
}, [])
+ // Keyboard shortcut: Cmd+N / Ctrl+N opens a new chat tab.
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'n') {
+ e.preventDefault()
+ handleNewChatTab()
+ }
+ }
+ document.addEventListener('keydown', handleKeyDown)
+ return () => document.removeEventListener('keydown', handleKeyDown)
+ }, [handleNewChatTab])
+
// Route undo/redo to the active markdown tab only (prevents cross-tab browser undo behavior).
useEffect(() => {
const handleHistoryKeyDown = (e: KeyboardEvent) => {
@@ -3862,11 +4051,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 || isKnowledgeViewOpen) && 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 || isKnowledgeViewOpen)
const selectedKnowledgePath = isGraphOpen
? GRAPH_TAB_PATH
: isSuggestedTopicsOpen
@@ -3879,6 +4068,10 @@ function App() {
? BG_TASKS_TAB_PATH
: isEmailOpen
? EMAIL_TAB_PATH
+ : isWorkspaceOpen
+ ? WORKSPACE_TAB_PATH
+ : isKnowledgeViewOpen
+ ? KNOWLEDGE_VIEW_TAB_PATH
: selectedPath
const targetFileTabId = activeFileTabId ?? (
selectedKnowledgePath
@@ -3933,7 +4126,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, isKnowledgeViewOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab])
const toggleExpand = (path: string, kind: 'file' | 'dir') => {
if (kind === 'file') {
@@ -3958,7 +4151,7 @@ function App() {
}),
},
}))
- if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !selectedBackgroundTask) {
+ if (!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedBackgroundTask) {
setIsChatSidebarOpen(false)
setIsRightPaneMaximized(false)
}
@@ -4084,19 +4277,49 @@ 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 && !isKnowledgeViewOpen && !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 && !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 && !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')
+ 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 +4913,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 || isKnowledgeViewOpen || isBrowserOpen)
const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized
const shouldCollapseLeftPane = isRightPaneOnlyMode
const openMarkdownTabs = React.useMemo(() => {
@@ -4707,7 +4930,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 && !isKnowledgeViewOpen) {
void navigateToView({ type: 'file', path: BASES_DEFAULT_TAB_PATH })
}
}}>
@@ -4721,18 +4944,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}
@@ -4740,7 +4953,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 || isKnowledgeViewOpen || isBrowserOpen) {
setIsChatSidebarOpen(true)
}
@@ -4751,7 +4964,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 || isKnowledgeViewOpen || isBrowserOpen) {
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: runIdToLoad } : t))
loadRun(runIdToLoad)
return
@@ -4775,14 +4988,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 || isKnowledgeViewOpen || 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 || isKnowledgeViewOpen || isBrowserOpen) {
setChatTabs(prev => prev.map(t => t.id === activeChatTabId ? { ...t, runId: null } : t))
handleNewChat()
} else {
@@ -4800,8 +5013,6 @@ function App() {
}}
backgroundTasks={backgroundTasks}
selectedBackgroundTask={selectedBackgroundTask}
- onNewChat={handleNewChatTab}
- onOpenSearch={() => setIsSearchOpen(true)}
isSearchOpen={isSearchOpen}
isBrowserOpen={isBrowserOpen}
onToggleBrowser={handleToggleBrowser}
@@ -4809,8 +5020,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 +5043,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 || isKnowledgeViewOpen) && 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 || isKnowledgeViewOpen || (selectedPath != null && isBaseFilePath(selectedPath)))}
/>
) : (
Version history
)}
- {!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !selectedTask && !isBrowserOpen && (
+ {!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !selectedTask && !isBrowserOpen && (
)}
- {!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isBrowserOpen && expandedFrom && (
+ {!selectedPath && !isGraphOpen && !isSuggestedTopicsOpen && !isMeetingsOpen && !isLiveNotesOpen && !isBgTasksOpen && !isEmailOpen && !isWorkspaceOpen && !isKnowledgeViewOpen && !isBrowserOpen && expandedFrom && (
)}
- {(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen) && (
+ {(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen) && (
+ ) : isWorkspaceOpen ? (
+
+ navigateToFile(path)}
+ onCreateWorkspace={async (name) => { await knowledgeActions.createWorkspace(name) }}
+ />
+
+ ) : isKnowledgeViewOpen ? (
+
+ navigateToFile(path)}
+ onOpenGraph={() => knowledgeActions.openGraph()}
+ onOpenSearch={() => setIsSearchOpen(true)}
+ onOpenBases={() => knowledgeActions.openBases()}
+ onVoiceNoteCreated={handleVoiceNoteCreated}
+ />
+
) : selectedPath && isBaseFilePath(selectedPath) ? (
setIsSearchOpen(true)}
/>
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', {
diff --git a/apps/x/apps/renderer/src/components/html-file-viewer.tsx b/apps/x/apps/renderer/src/components/html-file-viewer.tsx
index 994ab73e..8343af28 100644
--- a/apps/x/apps/renderer/src/components/html-file-viewer.tsx
+++ b/apps/x/apps/renderer/src/components/html-file-viewer.tsx
@@ -1,33 +1,11 @@
-import { useEffect, useState } from 'react'
+import { useEffect, useMemo, useState } from 'react'
import { AlertCircleIcon, ExternalLinkIcon, FileTextIcon, Loader2Icon } from 'lucide-react'
const MAX_SIZE_BYTES = 5 * 1024 * 1024
-const CACHE_MAX_ENTRIES = 20
-
-type CacheEntry = { html: string; mtimeMs: number; size: number }
-const htmlCache = new Map()
-
-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 — ``, `
`,
- // `