The installation may currently fail. We recommend copying the code below and creating the extension manually in Eidos.
By: Mayne
No description provided.
"use sidebar"
import React, { useState, useEffect } from "react"
import { ChevronRight, ChevronDown, File } from "lucide-react"
import { ScrollArea } from "@/components/ui/scroll-area"
const ROOT_DIR = "~/"
const FileTree = () => {
const [treeData, setTreeData] = useState([])
const [expandedNodes, setExpandedNodes] = useState(new Set())
const [loadingNodes, setLoadingNodes] = useState(new Set())
const loadRootDirectory = async () => {
try {
const entries = await eidos.currentSpace.fs.readdir(ROOT_DIR, {
withFileTypes: true
})
const sortedEntries = entries.sort((a, b) => {
if (a.kind === 'directory' && b.kind !== 'directory') return -1
if (a.kind !== 'directory' && b.kind === 'directory') return 1
return a.name.localeCompare(b.name)
})
setTreeData(sortedEntries)
} catch (error) {
console.error("Failed to load root directory:", error)
}
}
const loadSubDirectory = async (path) => {
if (loadingNodes.has(path)) return
setLoadingNodes(prev => new Set(prev).add(path))
try {
const entries = await eidos.currentSpace.fs.readdir(path, {
withFileTypes: true
})
const sortedEntries = entries.sort((a, b) => {
if (a.kind === 'directory' && b.kind !== 'directory') return -1
if (a.kind !== 'directory' && b.kind === 'directory') return 1
return a.name.localeCompare(b.name)
})
const updateTreeData = (nodes, targetPath, newChildren) => {
return nodes.map(node => {
if (node.path === targetPath) {
return { ...node, children: newChildren }
}
if (node.children) {
return { ...node, children: updateTreeData(node.children, targetPath, newChildren) }
}
return node
})
}
setTreeData(prev => updateTreeData(prev, path, sortedEntries))
} catch (error) {
console.error(`Failed to load directory ${path}:`, error)
} finally {
setLoadingNodes(prev => {
const newSet = new Set(prev)
newSet.delete(path)
return newSet
})
}
}
const toggleNode = async (node) => {
const newExpanded = new Set(expandedNodes)
if (expandedNodes.has(node.path)) {
newExpanded.delete(node.path)
} else {
newExpanded.add(node.path)
if (node.kind === 'directory' && !node.children) {
await loadSubDirectory(node.path)
}
}
setExpandedNodes(newExpanded)
}
const handleFileLick = (path: string) => {
eidos.currentSpace.navigate(`/file-handler/#${path}`)
}
const renderTreeNode = (node, level = 0) => {
const isExpanded = expandedNodes.has(node.path);
const isLoading = loadingNodes.has(node.path);
const hasChildren = node.kind === "directory";
return (
<div key={node.path} className="min-w-0">
<div
className="flex items-center hover:bg-accent rounded transition-colors cursor-pointer select-none"
onClick={() => hasChildren && toggleNode(node)}
>
<div style={{ width: level * 18 }} className="flex-shrink-0" />
<div className="w-4 flex-shrink-0 flex items-center justify-center">
{hasChildren ? (
<button
onClick={(e) => {
e.stopPropagation();
toggleNode(node);
}}
className="p-0 hover:bg-accent rounded transition-colors"
disabled={isLoading}
>
{isLoading ? (
<div className="w-4 h-4 animate-spin rounded-full border-2 border-border border-t-primary" />
) : isExpanded ? (
<ChevronDown className="w-4 h-4 text-muted-foreground" />
) : (
<ChevronRight className="w-4 h-4 text-muted-foreground" />
)}
</button>
) : (
<File className="w-4 h-4 text-muted-foreground" />
)}
</div>
<div className="flex items-center gap-1 px-2 py-1 min-w-0" onClick={() => {
!hasChildren && handleFileLick(node.path)
}}>
<span className="truncate text-foreground">{node.name}</span>
</div>
</div>
{hasChildren && isExpanded && node.children && (
<div className="ml-0">
{node.children.map((child) => renderTreeNode(child, level + 1))}
</div>
)}
</div>
);
}
useEffect(() => {
loadRootDirectory()
}, [])
return (
<ScrollArea className="h-full">
<div className="space-y-1 px-4 bg-sidebar">
{treeData.map((node, index) =>
renderTreeNode(
node,
0,
)
)}
</div>
</ScrollArea>
)
}
export default function () {
return <FileTree />
}