From de3a50c0f13e8fc46aac336d9db7f86eb83edcef Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Sat, 14 Mar 2026 14:15:14 -0400 Subject: [PATCH] feat: Add file search functionality to OpenClaw filesystem with highlighting and match counter. --- frontend/src/pages/OpenClaw.tsx | 132 +++++++++++++++++++++++++++++--- 1 file changed, 121 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/OpenClaw.tsx b/frontend/src/pages/OpenClaw.tsx index 79dd782..51aecee 100644 --- a/frontend/src/pages/OpenClaw.tsx +++ b/frontend/src/pages/OpenClaw.tsx @@ -230,17 +230,39 @@ function MetricCard({ label, value, accent, icon }: { label: string; value: stri } -function TreeNode({ node, depth, onSelect }: { node: FileTreeNode; depth: number, onSelect?: (n: FileTreeNode) => void }) { - const [open, setOpen] = useState(depth < 1); +function TreeNode({ node, depth, onSelect, searchQuery }: { node: FileTreeNode; depth: number; onSelect?: (n: FileTreeNode) => void; searchQuery?: string }) { + const [open, setOpen] = useState(depth < 1 || !!searchQuery); const isDir = node.type === 'directory'; const indent = depth * 20; + // Check if this node matches the search query + const matchesSearch = searchQuery && node.name.toLowerCase().includes(searchQuery.toLowerCase()); + + // Highlight matching text in file names + const renderHighlightedName = (name: string) => { + if (!searchQuery) return {name}; + + const lowerQuery = searchQuery.toLowerCase(); + const lowerName = name.toLowerCase(); + const index = lowerName.indexOf(lowerQuery); + + if (index === -1) return {name}; + + return ( + + {name.slice(0, index)} + {name.slice(index, index + searchQuery.length)} + {name.slice(index + searchQuery.length)} + + ); + }; + return (
{ if (isDir) setOpen(!open); @@ -250,15 +272,17 @@ function TreeNode({ node, depth, onSelect }: { node: FileTreeNode; depth: number {isDir ? ( {open ? '📂' : '📁'} ) : ( - 📄 + {matchesSearch ? '🔍' : '📄'} )} - {node.name} + + {renderHighlightedName(node.name)} + {node.size != null && !isDir && ( {formatSize(node.size)} )}
{isDir && open && node.children?.map(child => ( - + ))}
); @@ -1298,6 +1322,7 @@ function FilesystemTab() { const [editContent, setEditContent] = useState(''); const [saving, setSaving] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); useEffect(() => { loadTree(); @@ -1340,6 +1365,52 @@ function FilesystemTab() { } }; + const clearSearch = () => setSearchQuery(''); + + // Filter tree based on search query + const getFilteredTree = useCallback((nodes: FileTreeNode[], query: string): FileTreeNode[] => { + if (!query.trim()) return nodes; + const lowerQuery = query.toLowerCase(); + + const filterNode = (node: FileTreeNode): FileTreeNode | null => { + const matchesName = node.name.toLowerCase().includes(lowerQuery); + + if (node.type === 'directory' && node.children) { + const filteredChildren = node.children + .map(filterNode) + .filter((n): n is FileTreeNode => n !== null); + + if (matchesName || filteredChildren.length > 0) { + return { ...node, children: filteredChildren }; + } + return null; + } + + return matchesName ? node : null; + }; + + return nodes.map(filterNode).filter((n): n is FileTreeNode => n !== null); + }, []); + + const filteredTree = searchQuery ? getFilteredTree(tree, searchQuery) : tree; + + // Count matching files + const countMatches = useCallback((nodes: FileTreeNode[]): number => { + let count = 0; + const countNode = (node: FileTreeNode) => { + if (node.type === 'file' && node.name.toLowerCase().includes(searchQuery.toLowerCase())) { + count++; + } + if (node.children) { + node.children.forEach(countNode); + } + }; + nodes.forEach(countNode); + return count; + }, [searchQuery]); + + const matchCount = searchQuery ? countMatches(filteredTree) : 0; + const getExtensions = () => { if (!selectedFile) return []; const lang = selectedFile.language; @@ -1436,21 +1507,60 @@ function FilesystemTab() {
{/* Sidebar Tree */}
-
+

📂 Arborescence

+ + {/* Search Bar */} +
+
+ + + + +
+ setSearchQuery(e.target.value)} + className="w-full bg-surface-800/80 border border-surface-700 rounded-xl py-2 pl-9 pr-9 text-sm text-gray-300 placeholder-gray-600 focus:outline-none focus:border-purple-500/50 focus:ring-2 focus:ring-purple-500/20 transition-all" + /> + {searchQuery && ( + + )} +
+ + {/* Search Results Counter */} + {searchQuery && ( +
+ 0 ? 'text-purple-400' : 'text-gray-500'}`}> + {matchCount > 0 ? `${matchCount} fichier${matchCount > 1 ? 's' : ''} trouvé${matchCount > 1 ? 's' : ''}` : 'Aucun fichier trouvé'} + +
+ )} +
{treeLoading ? (
Chargement de l'arborescence…
- ) : tree.length === 0 ? ( -

Aucun fichier trouvé

+ ) : filteredTree.length === 0 ? ( +

{searchQuery ? 'Aucun fichier correspondant' : 'Aucun fichier trouvé'}

) : (
- {tree.map(node => ( - + {filteredTree.map(node => ( + ))}
)}