mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-25 00:16:29 +02:00
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:
parent
fe689c705f
commit
e8d8332e34
2 changed files with 80 additions and 15 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue