feat: enhance knowledge tree navigation and visibility

auto-open the folder chain to reveal the active file in sidebar when a knowledge file is opened
This commit is contained in:
tusharmagar 2026-02-19 15:49:45 +05:30
parent fe689c705f
commit e8d8332e34
2 changed files with 80 additions and 15 deletions

View file

@ -134,6 +134,16 @@ const getBaseName = (path: string) => {
return file.replace(/\.md$/i, '')
}
const getAncestorDirectoryPaths = (path: string): string[] => {
const parts = path.split('/').filter(Boolean)
if (parts.length <= 2) return []
const ancestors: string[] = []
for (let i = 1; i < parts.length - 1; i++) {
ancestors.push(parts.slice(0, i + 1).join('/'))
}
return ancestors
}
const isGraphTabPath = (path: string) => path === GRAPH_TAB_PATH
const normalizeUsage = (usage?: Partial<LanguageModelUsage> | null): LanguageModelUsage | null => {
@ -597,6 +607,25 @@ function App() {
}
}, [selectedPath])
// Keep active file visible in the Knowledge tree by auto-expanding its ancestor folders.
useEffect(() => {
if (!selectedPath) return
const ancestorDirs = getAncestorDirectoryPaths(selectedPath)
if (ancestorDirs.length === 0) return
setExpandedPaths((prev) => {
let changed = false
const next = new Set(prev)
for (const dirPath of ancestorDirs) {
if (!next.has(dirPath)) {
next.add(dirPath)
changed = true
}
}
return changed ? next : prev
})
}, [selectedPath])
// Keep runIdRef in sync with runId state (for use in event handlers to avoid stale closures)
useEffect(() => {
runIdRef.current = runId

View file

@ -820,6 +820,36 @@ function KnowledgeSection({
onVoiceNoteCreated?: (path: string) => void
}) {
const isExpanded = expandedPaths.size > 0
const treeContainerRef = React.useRef<HTMLDivElement | null>(null)
useEffect(() => {
if (!selectedPath) return
let cancelled = false
let rafId: number | null = null
let attempts = 0
const maxAttempts = 20
const revealActiveFile = () => {
if (cancelled) return
const container = treeContainerRef.current
if (!container) return
const activeRow = container.querySelector<HTMLElement>('[data-knowledge-active="true"]')
if (activeRow) {
activeRow.scrollIntoView({ block: "nearest", inline: "nearest" })
return
}
if (attempts >= maxAttempts) return
attempts += 1
rafId = requestAnimationFrame(revealActiveFile)
}
rafId = requestAnimationFrame(revealActiveFile)
return () => {
cancelled = true
if (rafId !== null) cancelAnimationFrame(rafId)
}
}, [selectedPath, expandedPaths, tree])
const quickActions = [
{ icon: FilePlus, label: "New Note", action: () => actions.createNote() },
@ -865,18 +895,20 @@ function KnowledgeSection({
</Tooltip>
</div>
<SidebarGroupContent className="flex-1 overflow-y-auto">
<SidebarMenu>
{tree.map((item, index) => (
<Tree
key={index}
item={item}
selectedPath={selectedPath}
expandedPaths={expandedPaths}
onSelect={onSelectFile}
actions={actions}
/>
))}
</SidebarMenu>
<div ref={treeContainerRef}>
<SidebarMenu>
{tree.map((item, index) => (
<Tree
key={index}
item={item}
selectedPath={selectedPath}
expandedPaths={expandedPaths}
onSelect={onSelectFile}
actions={actions}
/>
))}
</SidebarMenu>
</div>
</SidebarGroupContent>
</SidebarGroup>
</ContextMenuTrigger>
@ -935,7 +967,7 @@ function Tree({
try {
await actions.rename(item.path, trimmedName, isDir)
toast('Renamed successfully', 'success')
} catch (err) {
} catch {
toast('Failed to rename', 'error')
}
}
@ -950,7 +982,7 @@ function Tree({
try {
await actions.remove(item.path)
toast('Moved to trash', 'success')
} catch (err) {
} catch {
toast('Failed to delete', 'error')
}
}
@ -1045,7 +1077,11 @@ function Tree({
return (
<ContextMenu>
<ContextMenuTrigger asChild>
<SidebarMenuItem className="group/file-item">
<SidebarMenuItem
className="group/file-item"
data-knowledge-file-path={item.path}
data-knowledge-active={isSelected ? "true" : "false"}
>
<SidebarMenuButton
isActive={isSelected}
onClick={(e) => {