diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 2f4337a9..f9019ede 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -30,7 +30,7 @@ import { BgTasksView } from '@/components/bg-tasks-view'; import { EmailView } from '@/components/email-view'; import { WorkspaceView } from '@/components/workspace-view'; import { CodingRunBlock } from '@/components/coding-run'; -import { KnowledgeView } from '@/components/knowledge-view'; +import { KnowledgeView, type KnowledgeViewMode } from '@/components/knowledge-view'; import { ChatHistoryView } from '@/components/chat-history-view'; import { HomeView } from '@/components/home-view'; import { MeetingsView } from '@/components/meetings-view'; @@ -591,7 +591,7 @@ type ViewState = | { type: 'live-notes' } | { type: 'email' } | { type: 'workspace'; path?: string } - | { type: 'knowledge-view'; folderPath?: string } + | { type: 'knowledge-view'; folderPath?: string; mode?: KnowledgeViewMode } | { type: 'chat-history' } | { type: 'home' } | { type: 'code' } @@ -602,7 +602,7 @@ function viewStatesEqual(a: ViewState, b: ViewState): boolean { 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 ?? '') - if (a.type === 'knowledge-view' && b.type === 'knowledge-view') return (a.folderPath ?? '') === (b.folderPath ?? '') + if (a.type === 'knowledge-view' && b.type === 'knowledge-view') return (a.folderPath ?? '') === (b.folderPath ?? '') && (a.mode ?? '') === (b.mode ?? '') return true // both graph } @@ -652,7 +652,12 @@ function parseDeepLink(input: string): ViewState | null { } case 'knowledge-view': { const folderPath = params.get('folderPath') - return { type: 'knowledge-view', folderPath: folderPath ?? undefined } + const mode = params.get('mode') + return { + type: 'knowledge-view', + folderPath: folderPath ?? undefined, + mode: mode === 'graph' || mode === 'basis' || mode === 'files' ? mode : undefined, + } } case 'chat-history': return { type: 'chat-history' } @@ -775,6 +780,7 @@ function App() { const [isWorkspaceOpen, setIsWorkspaceOpen] = useState(false) const [workspaceInitialPath, setWorkspaceInitialPath] = useState(null) const [isKnowledgeViewOpen, setIsKnowledgeViewOpen] = useState(false) + const [knowledgeViewMode, setKnowledgeViewMode] = useState('graph') // Folder being browsed inside the knowledge view (null = root overview). // Lives in ViewState so folder drill-down participates in back/forward history. const [knowledgeViewFolderPath, setKnowledgeViewFolderPath] = useState(null) @@ -1197,7 +1203,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 'Notes' + if (isKnowledgeViewTabPath(tab.path)) return 'Brain' if (isChatHistoryTabPath(tab.path)) return 'Chat history' if (isHomeTabPath(tab.path)) return 'Home' if (isCodeTabPath(tab.path)) return 'Code' @@ -3627,14 +3633,14 @@ 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', folderPath: knowledgeViewFolderPath ?? undefined } + if (isKnowledgeViewOpen) return { type: 'knowledge-view', folderPath: knowledgeViewFolderPath ?? undefined, mode: knowledgeViewMode } if (isChatHistoryOpen) return { type: 'chat-history' } if (isHomeOpen) return { type: 'home' } if (isCodeOpen) return { type: 'code' } 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, isKnowledgeViewOpen, knowledgeViewFolderPath, isChatHistoryOpen, isHomeOpen, isCodeOpen, workspaceInitialPath, runId]) + }, [selectedBackgroundTask, isEmailOpen, isMeetingsOpen, isLiveNotesOpen, isBgTasksOpen, isSuggestedTopicsOpen, selectedPath, isGraphOpen, isWorkspaceOpen, isKnowledgeViewOpen, knowledgeViewFolderPath, knowledgeViewMode, isChatHistoryOpen, isHomeOpen, isCodeOpen, workspaceInitialPath, runId]) const appendUnique = useCallback((stack: ViewState[], entry: ViewState) => { const last = stack[stack.length - 1] @@ -3997,6 +4003,7 @@ function App() { setIsEmailOpen(false) setIsWorkspaceOpen(false) setIsKnowledgeViewOpen(true) + setKnowledgeViewMode(view.mode ?? (view.folderPath ? 'files' : 'graph')) setKnowledgeViewFolderPath(view.folderPath ?? null) setIsChatHistoryOpen(false) setIsHomeOpen(false) @@ -4223,10 +4230,9 @@ function App() { setBaseConfigByPath((prev) => ({ ...prev, [path]: config })) }, []) - const handleBaseSave = useCallback(async (name: string | null) => { - if (!selectedPath) return - const isDefault = selectedPath === BASES_DEFAULT_TAB_PATH - const config = baseConfigByPath[selectedPath] ?? DEFAULT_BASE_CONFIG + const handleBaseSave = useCallback(async (path: string, name: string | null) => { + const isDefault = path === BASES_DEFAULT_TAB_PATH + const config = baseConfigByPath[path] ?? DEFAULT_BASE_CONFIG if (isDefault && name) { // Save as new base file @@ -4250,14 +4256,14 @@ function App() { // Save in place try { await window.ipc.invoke('workspace:writeFile', { - path: selectedPath, + path, data: JSON.stringify(config, null, 2), }) } catch (err) { console.error('Failed to save base:', err) } } - }, [selectedPath, baseConfigByPath, loadDirectory, navigateToView]) + }, [baseConfigByPath, loadDirectory, navigateToView]) // External search set by app-navigation tool (passed to BasesView) const [externalBaseSearch, setExternalBaseSearch] = useState(undefined) @@ -5121,8 +5127,10 @@ function App() { }, }), [knowledgeFiles, recentWikiFiles, openWikiLink, ensureWikiFile]) + const isBrainGraphOpen = isKnowledgeViewOpen && knowledgeViewMode === 'graph' + useEffect(() => { - if (!isGraphOpen) return + if (!isGraphOpen && !isBrainGraphOpen) return let cancelled = false const buildGraph = async () => { @@ -5237,7 +5245,7 @@ function App() { return () => { cancelled = true } - }, [isGraphOpen, knowledgeFilePaths]) + }, [isGraphOpen, isBrainGraphOpen, knowledgeFilePaths]) const renderConversationItem = ( item: ConversationItem, @@ -5760,12 +5768,44 @@ function App() { revealInFileManager: knowledgeActions.revealInFileManager, onOpenInNewTab: knowledgeActions.onOpenInNewTab, }} + mode={knowledgeViewMode} + onModeChange={setKnowledgeViewMode} + graphContent={( + { + navigateToFile(path) + }} + /> + )} + basisContent={( + navigateToFile(path)} + config={baseConfigByPath[BASES_DEFAULT_TAB_PATH] ?? DEFAULT_BASE_CONFIG} + onConfigChange={(cfg) => handleBaseConfigChange(BASES_DEFAULT_TAB_PATH, cfg)} + isDefaultBase + onSave={(name) => void handleBaseSave(BASES_DEFAULT_TAB_PATH, name)} + externalSearch={externalBaseSearch} + onExternalSearchConsumed={() => setExternalBaseSearch(undefined)} + actions={{ + rename: knowledgeActions.rename, + remove: knowledgeActions.remove, + copyPath: knowledgeActions.copyPath, + revealInFileManager: knowledgeActions.revealInFileManager, + }} + /> + )} folderPath={knowledgeViewFolderPath} - onNavigateFolder={(path) => { void navigateToView({ type: 'knowledge-view', folderPath: path ?? undefined }) }} + onNavigateFolder={(path) => { + setKnowledgeViewMode('files') + void navigateToView({ type: 'knowledge-view', folderPath: path ?? undefined, mode: 'files' }) + }} onOpenNote={(path) => navigateToFile(path)} - onOpenGraph={() => knowledgeActions.openGraph()} onOpenSearch={() => { setSearchDefaultScope('knowledge'); setIsSearchOpen(true) }} - onOpenBases={() => knowledgeActions.openBases()} onVoiceNoteCreated={handleVoiceNoteCreated} /> @@ -5796,7 +5836,7 @@ function App() { config={baseConfigByPath[selectedPath] ?? DEFAULT_BASE_CONFIG} onConfigChange={(cfg) => handleBaseConfigChange(selectedPath, cfg)} isDefaultBase={selectedPath === BASES_DEFAULT_TAB_PATH} - onSave={(name) => void handleBaseSave(name)} + onSave={(name) => void handleBaseSave(selectedPath, name)} externalSearch={externalBaseSearch} onExternalSearchConsumed={() => setExternalBaseSearch(undefined)} actions={{ diff --git a/apps/x/apps/renderer/src/components/knowledge-view.tsx b/apps/x/apps/renderer/src/components/knowledge-view.tsx index e7ebe780..0945ffcf 100644 --- a/apps/x/apps/renderer/src/components/knowledge-view.tsx +++ b/apps/x/apps/renderer/src/components/knowledge-view.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react' import { ArrowLeft, ChevronRight, @@ -46,17 +46,21 @@ export type KnowledgeViewActions = { onOpenInNewTab?: (path: string) => void } +export type KnowledgeViewMode = 'graph' | 'basis' | 'files' + type KnowledgeViewProps = { tree: TreeNode[] actions: KnowledgeViewActions + mode: KnowledgeViewMode + onModeChange: (mode: KnowledgeViewMode) => void + graphContent: ReactNode + basisContent: ReactNode // Folder currently being browsed (null = root overview). Controlled by the // app so drill-down participates in the global back/forward history. folderPath: string | null onNavigateFolder: (path: string | null) => void onOpenNote: (path: string) => void - onOpenGraph: () => void onOpenSearch: () => void - onOpenBases: () => void onVoiceNoteCreated?: (path: string) => void } @@ -145,12 +149,14 @@ function displayName(node: TreeNode): string { export function KnowledgeView({ tree, actions, + mode, + onModeChange, + graphContent, + basisContent, folderPath, onNavigateFolder, onOpenNote, - onOpenGraph, onOpenSearch, - onOpenBases, onVoiceNoteCreated, }: KnowledgeViewProps) { const [renameTarget, setRenameTarget] = useState(null) @@ -184,27 +190,46 @@ export function KnowledgeView({
-

Notes

+

Brain

{totalNotes} {totalNotes === 1 ? 'note' : 'notes'} across {folders.length}{' '} {folders.length === 1 ? 'folder' : 'folders'}

+
+ onModeChange('graph')} + /> + onModeChange('basis')} + /> + onModeChange('files')} + /> +
- - -
+ {mode === 'graph' ? ( +
+ {graphContent} +
+ ) : mode === 'basis' ? ( +
+ {basisContent} +
+ ) : (
{currentFolder ? ( @@ -267,11 +292,12 @@ export function KnowledgeView({
+ )}
) } @@ -279,12 +305,12 @@ export function KnowledgeView({ function QuickActions({ actions, currentFolder, - onOpenBases, + onOpenSearch, onFolderCreated, }: { actions: KnowledgeViewActions currentFolder: TreeNode | null - onOpenBases: () => void + onOpenSearch: () => void onFolderCreated: (path: string) => void }) { // Inside a folder these target that folder; at the root they target knowledge/. @@ -294,6 +320,7 @@ function QuickActions({
actions.createNote(parent)} /> + - void }) { return ( {crumbs.map((c, i) => ( diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index 8de6402a..986d76cc 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -819,7 +819,7 @@ export function SidebarContentPanel({ >
- Knowledge + Brain {knowledgeUpdatedLabel && ( {knowledgeUpdatedLabel} )} diff --git a/apps/x/packages/core/src/application/assistant/instructions.ts b/apps/x/packages/core/src/application/assistant/instructions.ts index 8ffb7751..ed3576b0 100644 --- a/apps/x/packages/core/src/application/assistant/instructions.ts +++ b/apps/x/packages/core/src/application/assistant/instructions.ts @@ -158,6 +158,8 @@ Unlike other AI assistants that start cold every session, you have access to a l When a user asks you to prep them for a call with someone, you already know every prior decision, concerns they've raised, and commitments on both sides - because memory has been accumulating across every email and call, not reconstructed on demand. ## The Knowledge Graph +The knowledge graph is the user's **Brain**. If the user says "my brain", "the brain", "look into your brain", "check my brain", "Brain", or similar, they mean the knowledge graph stored in \`knowledge/\`. Treat "Brain" and "knowledge graph" as the same thing. + The knowledge graph is stored as plain markdown with Obsidian-style backlinks in \`knowledge/\` (inside the workspace). The folder is organized into these categories: - **Notes/** - Default location for user-authored notes. Create new notes here unless the user specifies a different folder. - **People/** - Notes on individuals, tracking relationships, decisions, and commitments