diff --git a/apps/web/components/task/file-browser.tsx b/apps/web/components/task/file-browser.tsx index 84f42be3c4..12065152ca 100644 --- a/apps/web/components/task/file-browser.tsx +++ b/apps/web/components/task/file-browser.tsx @@ -22,6 +22,7 @@ import { } from '@/lib/local-storage'; import { useToast } from '@/components/toast-provider'; import { InlineFileInput } from './inline-file-input'; +import { PanelHeaderBar, PanelHeaderBarSplit } from './panel-primitives'; /** * Merge a freshly-fetched tree node into an existing one, preserving @@ -49,13 +50,16 @@ function mergeTreeNodes(existing: FileTreeNode, incoming: FileTreeNode): FileTre return { ...existing, ...incoming, children: mergedChildren }; } +/** Sort comparator: directories first, then alphabetical by name. */ +const compareTreeNodes = (a: FileTreeNode, b: FileTreeNode): number => { + if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1; + return a.name.localeCompare(b.name); +}; + /** Insert a file node into a parent folder, keeping children sorted (dirs first, then alpha). */ function insertNodeInTree(root: FileTreeNode, parentPath: string, node: FileTreeNode): FileTreeNode { if (root.path === parentPath || (parentPath === '' && root.path === '')) { - const children = [...(root.children ?? []), node].sort((a, b) => { - if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1; - return a.name.localeCompare(b.name); - }); + const children = [...(root.children ?? []), node].sort(compareTreeNodes); return { ...root, children }; } if (!root.children) return root; @@ -77,12 +81,10 @@ type FileBrowserProps = { onOpenFile: (file: OpenFileTab) => void; onCreateFile?: (path: string) => Promise; onDeleteFile?: (path: string) => Promise; - isSearchOpen?: boolean; - onCloseSearch?: () => void; activeFilePath?: string | null; }; -export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile, isSearchOpen = false, onCloseSearch, activeFilePath }: FileBrowserProps) { +export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile, activeFilePath }: FileBrowserProps) { const { toast } = useToast(); const [tree, setTree] = useState(null); const [expandedPaths, setExpandedPaths] = useState>(new Set()); @@ -99,6 +101,7 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile, const { copied, copy: copyPath } = useCopyToClipboard(1000); // Search state + const [isSearchActive, setIsSearchActive] = useState(false); const [localSearchQuery, setLocalSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState(null); const [isSearching, setIsSearching] = useState(false); @@ -176,14 +179,14 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile, // Focus search input when search opens useEffect(() => { - if (isSearchOpen && searchInputRef.current) { + if (isSearchActive && searchInputRef.current) { searchInputRef.current.focus(); } - }, [isSearchOpen]); + }, [isSearchActive]); // Clear search when closing useEffect(() => { - if (!isSearchOpen) { + if (!isSearchActive) { setLocalSearchQuery(''); setSearchResults(null); setIsSearching(false); @@ -191,7 +194,7 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile, clearTimeout(searchTimeoutRef.current); } } - }, [isSearchOpen]); + }, [isSearchActive]); const clearRetryTimer = useCallback(() => { if (retryTimerRef.current) { @@ -474,16 +477,16 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile, }, 300); }, [sessionId]); - // Close search (clears and notifies parent) + // Close search and clear state const handleCloseSearch = useCallback(() => { + setIsSearchActive(false); setLocalSearchQuery(''); setSearchResults(null); setIsSearching(false); if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } - onCloseSearch?.(); - }, [onCloseSearch]); + }, []); // Cleanup search timeout on unmount useEffect(() => { @@ -547,39 +550,6 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile, } }, [expandedPaths, sessionId, tree, showLoading, hideLoading]); - 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); - - // Calculate hash for the file content - const { calculateHash } = await import('@/lib/utils/file-diff'); - const hash = await calculateHash(response.content); - - onOpenFile({ - path: node.path, - name: node.name, - content: response.content, - originalContent: response.content, - originalHash: hash, - isDirty: false, - isBinary: response.is_binary, - }); - } catch (error) { - const reason = error instanceof Error ? error.message : 'Unknown error'; - toast({ - title: 'Failed to open file', - description: reason, - variant: 'error', - }); - } - }, [sessionId, onOpenFile, toast]); - - // Open file from search result (path only) const openFileByPath = useCallback(async (path: string) => { try { const client = getWebSocketClient(); @@ -587,11 +557,9 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile, const response: FileContentResponse = await requestFileContent(client, sessionId, path); - // Calculate hash for the file content const { calculateHash } = await import('@/lib/utils/file-diff'); const hash = await calculateHash(response.content); - // Extract filename from path const name = path.split('/').pop() || path; onOpenFile({ @@ -629,7 +597,7 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile, isActiveFolder && "bg-muted/50" )} style={{ paddingLeft: `${depth * 12 + 8 + (node.is_dir ? 0 : 20)}px` }} - onClick={() => (node.is_dir ? toggleExpand(node) : openFile(node))} + onClick={() => (node.is_dir ? toggleExpand(node) : openFileByPath(node.path))} > {node.is_dir && ( @@ -698,12 +666,7 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile, /> )} {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); - }) + .sort(compareTreeNodes) .map((child) => renderTreeNode(child, depth + 1))} )} @@ -755,102 +718,117 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile, return (
- {/* Search input - only shown when search is open */} - {isSearchOpen && ( -
-
+ {/* Header toolbar */} + {tree && loadState === 'loaded' && ( + isSearchActive ? ( + {isSearching ? ( - + ) : ( - + )} { if (e.key === 'Escape') handleCloseSearch(); }} placeholder="Search files..." - className="pl-7 pr-7 h-7 text-xs" + className="flex-1 min-w-0 h-5 text-xs border-none bg-transparent shadow-none focus-visible:ring-0 px-2" /> -
-
- )} - - {/* Header toolbar */} - {tree && loadState === 'loaded' && ( -
- - - + + Copy workspace path + + + {displayPath} + + + } + right={ + <> + {onCreateFile && ( + + + + + New file + )} - - - Copy workspace path - - - {displayPath} - -
- {onCreateFile && ( - - - - - New file - - )} - - - - - Open workspace folder - - {expandedPaths.size > 0 && ( - - - - - Collapse all - - )} -
-
+ + + + + Open workspace folder + + + + + + Search files + + {expandedPaths.size > 0 && ( + + + + + Collapse all + + )} + + } + /> + ) )} - {isSearchOpen && searchResults !== null ? ( + {isSearchActive && searchResults !== null ? ( renderSearchResults() ) : isSessionFailed ? (
@@ -887,11 +865,7 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile, /> )} {tree.children && [...tree.children] - .sort((a, b) => { - if (a.is_dir && !b.is_dir) return -1; - if (!a.is_dir && b.is_dir) return 1; - return a.name.localeCompare(b.name); - }) + .sort(compareTreeNodes) .map((child) => renderTreeNode(child, 0))}
) : ( diff --git a/apps/web/components/task/files-panel.tsx b/apps/web/components/task/files-panel.tsx index 9424522cf9..70f9d8dc1b 100644 --- a/apps/web/components/task/files-panel.tsx +++ b/apps/web/components/task/files-panel.tsx @@ -1,6 +1,6 @@ 'use client'; -import { memo, useState, useCallback } from 'react'; +import { memo, useCallback } from 'react'; import { PanelRoot, PanelBody } from './panel-primitives'; import { useAppStore } from '@/components/state-provider'; import { useFileOperations } from '@/hooks/use-file-operations'; @@ -13,7 +13,6 @@ type FilesPanelProps = { }; const FilesPanel = memo(function FilesPanel({ onOpenFile }: FilesPanelProps) { - const [isSearchOpen, setIsSearchOpen] = useState(false); const activeSessionId = useAppStore((state) => state.tasks.activeSessionId); const isArchived = useIsTaskArchived(); const { createFile, deleteFile } = useFileOperations(activeSessionId ?? null); @@ -40,8 +39,6 @@ const FilesPanel = memo(function FilesPanel({ onOpenFile }: FilesPanelProps) { onOpenFile={onOpenFile} onCreateFile={handleCreateFile} onDeleteFile={deleteFile} - isSearchOpen={isSearchOpen} - onCloseSearch={() => setIsSearchOpen(false)} /> ) : (
diff --git a/apps/web/components/task/task-files-panel.tsx b/apps/web/components/task/task-files-panel.tsx index 14ee9d2d56..b7184f2c09 100644 --- a/apps/web/components/task/task-files-panel.tsx +++ b/apps/web/components/task/task-files-panel.tsx @@ -10,7 +10,6 @@ import { IconChevronRight, IconGitCommit, IconLoader2, - IconSearch, IconColumns, } from '@tabler/icons-react'; @@ -75,7 +74,6 @@ const TaskFilesPanel = memo(function TaskFilesPanel({ onSelectDiff, onOpenFile, const [topTab, setTopTab] = useState<'diff' | 'files'>('diff'); const [showDiscardDialog, setShowDiscardDialog] = useState(false); const [fileToDiscard, setFileToDiscard] = useState(null); - const [isSearchOpen, setIsSearchOpen] = useState(false); const activeSessionId = useAppStore((state) => state.tasks.activeSessionId); const gitStatus = useSessionGitStatus(activeSessionId); const { commits } = useSessionCommits(activeSessionId ?? null); @@ -93,11 +91,6 @@ const TaskFilesPanel = memo(function TaskFilesPanel({ onSelectDiff, onOpenFile, // Track previous changed files count for auto-switch detection const prevChangedCountRef = useRef(0); - // Close search when switching tabs - useEffect(() => { - setIsSearchOpen(false); - }, [topTab]); - // State for commit diffs const [expandedCommit, setExpandedCommit] = useState(null); const [commitDiffs, setCommitDiffs] = useState>>({}); @@ -344,28 +337,6 @@ const TaskFilesPanel = memo(function TaskFilesPanel({ onSelectDiff, onOpenFile, }, ]; - // Contextual buttons based on active tab - const tabRightContent = useMemo(() => { - if (topTab === 'files') { - return ( - - - - - Search files - - ); - } - // No contextual buttons for diff tab (for now) - return null; - }, [topTab]); - return ( handleTabChange(value as 'diff' | 'files')} className="flex-1 min-h-0" - rightContent={tabRightContent} > @@ -604,8 +574,6 @@ const TaskFilesPanel = memo(function TaskFilesPanel({ onSelectDiff, onOpenFile, onOpenFile={onOpenFile} onCreateFile={handleCreateFile} onDeleteFile={hookDeleteFile} - isSearchOpen={isSearchOpen} - onCloseSearch={() => setIsSearchOpen(false)} activeFilePath={activeFilePath} /> ) : (