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 => (
+
))}
)}