'use client'; import React, { useEffect, useState, useCallback } from 'react'; import { IconChevronRight, IconChevronDown, IconFile, IconFolder, IconFolderOpen } from '@tabler/icons-react'; import { ScrollArea } from '@kandev/ui/scroll-area'; import { getWebSocketClient } from '@/lib/ws/connection'; import { requestFileTree, requestFileContent } from '@/lib/ws/workspace-files'; import type { FileTreeNode, FileContentResponse, OpenFileTab } from '@/lib/types/backend'; type FileBrowserProps = { sessionId: string; onOpenFile: (file: OpenFileTab) => void; }; export function FileBrowser({ sessionId, onOpenFile }: FileBrowserProps) { const [tree, setTree] = useState(null); const [expandedPaths, setExpandedPaths] = useState>(new Set()); const [loadingPaths, setLoadingPaths] = useState>(new Set()); const [isLoadingTree, setIsLoadingTree] = useState(true); // Load initial tree - always try to load, don't wait for agentctl ready status useEffect(() => { setTree(null); setExpandedPaths(new Set()); setLoadingPaths(new Set()); setIsLoadingTree(true); const loadTree = async () => { try { const client = getWebSocketClient(); if (!client) return; const response = await requestFileTree(client, sessionId, '', 1); setTree(response.root); } catch (error) { console.error('Failed to load file tree:', error); } finally { setIsLoadingTree(false); } }; loadTree(); }, [sessionId]); // Subscribe to file changes and refresh tree useEffect(() => { const client = getWebSocketClient(); if (!client) return; const unsubscribe = client.on('session.workspace.file.changes', async () => { // Refresh the root tree when any file changes try { const response = await requestFileTree(client, sessionId, '', 1); setTree(response.root); } catch (error) { console.error('Failed to refresh file tree:', error); } }); return unsubscribe; }, [sessionId]); const toggleExpand = useCallback(async (node: FileTreeNode) => { if (!node.is_dir) return; const newExpanded = new Set(expandedPaths); if (newExpanded.has(node.path)) { newExpanded.delete(node.path); setExpandedPaths(newExpanded); } else { // If children not loaded yet, fetch them if (!node.children || node.children.length === 0) { setLoadingPaths((prev) => new Set(prev).add(node.path)); try { const client = getWebSocketClient(); if (!client) return; const response = await requestFileTree(client, sessionId, node.path, 1); // Update tree with new children const updateNode = (n: FileTreeNode): FileTreeNode => { if (n.path === node.path) { return { ...n, children: response.root.children }; } if (n.children) { return { ...n, children: n.children.map(updateNode) }; } return n; }; if (tree) { setTree(updateNode(tree)); } } catch (error) { console.error('Failed to load children:', error); } finally { setLoadingPaths((prev) => { const next = new Set(prev); next.delete(node.path); return next; }); } } newExpanded.add(node.path); setExpandedPaths(newExpanded); } }, [expandedPaths, sessionId, tree]); const openFile = useCallback(async (node: FileTreeNode) => { if (node.is_dir) return; try { const client = getWebSocketClient(); if (!client) return; const response: FileContentResponse = await requestFileContent(client, sessionId, node.path); onOpenFile({ path: node.path, name: node.name, content: response.content, }); } catch (error) { console.error('Failed to load file content:', error); } }, [sessionId, onOpenFile]); const renderTreeNode = (node: FileTreeNode, depth: number = 0): React.ReactNode => { const isExpanded = expandedPaths.has(node.path); const isLoading = loadingPaths.has(node.path); return (
(node.is_dir ? toggleExpand(node) : openFile(node))} > {node.is_dir && ( {isLoading ? ( ⏳ ) : isExpanded ? ( ) : ( )} )} {node.is_dir ? ( isExpanded ? ( ) : ( ) ) : ( )} {node.name} {!node.is_dir && node.size && ( {(node.size / 1024).toFixed(1)}KB )}
{node.is_dir && isExpanded && node.children && (
{[...node.children] .sort((a, b) => { // Folders first, then alphabetically if (a.is_dir && !b.is_dir) return -1; if (!a.is_dir && b.is_dir) return 1; return a.name.localeCompare(b.name); }) .map((child) => renderTreeNode(child, depth + 1))}
)}
); }; return (
{isLoadingTree ? (
Loading files...
) : tree ? (
{renderTreeNode(tree)}
) : (
No files found
)}
); }