knowledge to brain

This commit is contained in:
Arjun 2026-06-19 11:59:56 +05:30
parent 1f8ac2cf34
commit 811ae22bb1
4 changed files with 117 additions and 43 deletions

View file

@ -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<string | null>(null)
const [isKnowledgeViewOpen, setIsKnowledgeViewOpen] = useState(false)
const [knowledgeViewMode, setKnowledgeViewMode] = useState<KnowledgeViewMode>('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<string | null>(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<string | undefined>(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={(
<GraphView
nodes={graphData.nodes}
edges={graphData.edges}
isLoading={false}
error={graphStatus === 'error' ? (graphError ?? 'Failed to build graph') : null}
onSelectNode={(path) => {
navigateToFile(path)
}}
/>
)}
basisContent={(
<BasesView
tree={tree}
onSelectNote={(path) => 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}
/>
</div>
@ -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={{

View file

@ -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<string | null>(null)
@ -184,27 +190,46 @@ export function KnowledgeView({
<div className="flex h-full flex-col overflow-hidden">
<div className="shrink-0 flex items-start justify-between gap-4 border-b border-border px-8 py-6">
<div className="min-w-0">
<h1 className="text-2xl font-bold tracking-tight">Notes</h1>
<h1 className="text-2xl font-bold tracking-tight">Brain</h1>
<p className="mt-1 text-sm text-muted-foreground">
{totalNotes} {totalNotes === 1 ? 'note' : 'notes'} across {folders.length}{' '}
{folders.length === 1 ? 'folder' : 'folders'}
</p>
</div>
<div className="flex shrink-0 items-center gap-2">
<div className="inline-flex overflow-hidden rounded-lg border border-border bg-background">
<ViewModeButton
icon={Network}
label="Graph"
active={mode === 'graph'}
onClick={() => onModeChange('graph')}
/>
<ViewModeButton
icon={Table2}
label="Base"
active={mode === 'basis'}
onClick={() => onModeChange('basis')}
/>
<ViewModeButton
icon={FileText}
label="Files"
active={mode === 'files'}
onClick={() => onModeChange('files')}
/>
</div>
<VoiceNoteButton onNoteCreated={onVoiceNoteCreated} />
<SecondaryButton icon={SearchIcon} label="Search" onClick={onOpenSearch} />
<SecondaryButton icon={Network} label="Graph" onClick={onOpenGraph} />
<button
type="button"
onClick={() => actions.createNote(currentFolder?.path)}
className="inline-flex items-center gap-1.5 rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
<FilePlus className="size-4" />
<span>New note</span>
</button>
</div>
</div>
{mode === 'graph' ? (
<div className="flex-1 min-h-0 overflow-hidden">
{graphContent}
</div>
) : mode === 'basis' ? (
<div className="flex-1 min-h-0 overflow-hidden">
{basisContent}
</div>
) : (
<div className="flex-1 overflow-y-auto">
<div className="mx-auto w-full max-w-3xl px-8 py-6">
{currentFolder ? (
@ -267,11 +292,12 @@ export function KnowledgeView({
<QuickActions
actions={actions}
currentFolder={currentFolder}
onOpenBases={onOpenBases}
onOpenSearch={onOpenSearch}
onFolderCreated={setRenameTarget}
/>
</div>
</div>
)}
</div>
)
}
@ -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({
<SectionHeader label="Quick actions" />
<div className="flex flex-wrap gap-2">
<QuickAction icon={FilePlus} label="New note" onClick={() => actions.createNote(parent)} />
<QuickAction icon={SearchIcon} label="Search" onClick={onOpenSearch} />
<QuickAction
icon={FolderPlus}
label="New folder"
@ -304,7 +331,6 @@ function QuickActions({
} catch { /* ignore */ }
}}
/>
<QuickAction icon={Table2} label="Open as base" onClick={onOpenBases} />
<QuickAction
icon={FolderOpen}
label={`Reveal in ${getFileManagerName()}`}
@ -315,20 +341,26 @@ function QuickActions({
)
}
function SecondaryButton({
function ViewModeButton({
icon: Icon,
label,
active,
onClick,
}: {
icon: typeof SearchIcon
label: string
active: boolean
onClick: () => void
}) {
return (
<button
type="button"
onClick={onClick}
className="inline-flex items-center gap-1.5 rounded-lg border border-border bg-background px-3 py-1.5 text-sm text-foreground transition-colors hover:bg-accent"
aria-pressed={active}
className={cn(
'inline-flex items-center gap-1.5 px-3 py-1.5 text-sm transition-colors',
active ? 'bg-accent text-foreground' : 'text-muted-foreground hover:bg-accent/60 hover:text-foreground',
)}
>
<Icon className="size-4" />
<span>{label}</span>
@ -532,7 +564,7 @@ function FolderDetail({
onClick={() => onNavigate(null)}
className="rounded-md px-1.5 py-1 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
>
Notes
Brain
</button>
{crumbs.map((c, i) => (
<span key={c.path} className="flex min-w-0 items-center gap-1.5">

View file

@ -819,7 +819,7 @@ export function SidebarContentPanel({
>
<FileText className="size-4 shrink-0" />
<div className="flex min-w-0 flex-1 flex-col">
<span className="truncate">Knowledge</span>
<span className="truncate">Brain</span>
{knowledgeUpdatedLabel && (
<span className="truncate text-[11px] text-muted-foreground">{knowledgeUpdatedLabel}</span>
)}

View file

@ -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