diff --git a/packages/ui/src/components/session/DirectoryTree.tsx b/packages/ui/src/components/session/DirectoryTree.tsx index 107b4e98..59e1633c 100644 --- a/packages/ui/src/components/session/DirectoryTree.tsx +++ b/packages/ui/src/components/session/DirectoryTree.tsx @@ -65,6 +65,8 @@ export const DirectoryTree: React.FC = ({ const [creatingInPath, setCreatingInPath] = React.useState(null); const [newDirName, setNewDirName] = React.useState(''); const [isPinnedExpanded, setIsPinnedExpanded] = React.useState(true); + const [pinnedExpandedPaths, setPinnedExpandedPaths] = React.useState>(new Set()); + const [pinnedItemChildren, setPinnedItemChildren] = React.useState>(new Map()); const inputRef = React.useRef(null); const { requestAccess, startAccessing, isDesktop } = useFileSystemAccess(); const previousShowHidden = React.useRef(showHidden); @@ -311,6 +313,33 @@ export const DirectoryTree: React.FC = ({ }); }, [effectiveRoot, isPathWithinHome, stripTrailingSlashes]); + // Clean up pinned expansion state when pinned paths are removed (e.g., unpinned or filtered out) + React.useEffect(() => { + const pinnedRoots = Array.from(pinnedPaths); + const isWithinPinnedRoot = (path: string) => pinnedRoots.some((root) => ( + path === root || path.startsWith(`${root}/`) + )); + + setPinnedExpandedPaths(prev => { + const next = new Set(prev); + for (const path of prev) { + if (!isWithinPinnedRoot(path)) { + next.delete(path); + } + } + return next; + }); + setPinnedItemChildren(prev => { + const next = new Map(prev); + for (const [path] of prev) { + if (!isWithinPinnedRoot(path)) { + next.delete(path); + } + } + return next; + }); + }, [pinnedPaths]); + // Reload directories when showHidden changes, but keep expanded state React.useEffect(() => { if (previousShowHidden.current !== showHidden) { @@ -495,6 +524,30 @@ export const DirectoryTree: React.FC = ({ setDirectories((prev) => updateItems(prev)); }; + const togglePinnedExpanded = async (path: string) => { + if (!rootReady) { + return; + } + const isCurrentlyExpanded = pinnedExpandedPaths.has(path); + const newExpanded = new Set(pinnedExpandedPaths); + + if (isCurrentlyExpanded) { + newExpanded.delete(path); + setPinnedExpandedPaths(newExpanded); + return; + } + + newExpanded.add(path); + setPinnedExpandedPaths(newExpanded); + + const children = await loadDirectory(path); + setPinnedItemChildren(prev => { + const next = new Map(prev); + next.set(path, children); + return next; + }); + }; + React.useEffect(() => { if (creatingInPath && inputRef.current) { inputRef.current.focus(); @@ -869,100 +922,270 @@ export const DirectoryTree: React.FC = ({ ); }; - const renderPinnedRow = (name: string, path: string) => { - if (variant === 'inline') { - const isSelected = currentPath === path; - return ( -
{ + const isExpanded = pinnedExpandedPaths.has(item.path); + const children = pinnedItemChildren.get(item.path); + const hasChildren = item.isDirectory; + const isPinned = pinnedPaths.has(item.path); + const isSelected = currentPath === item.path; + const isInlineVariant = variant === 'inline'; + + const rowContent = ( + <> + {hasChildren && ( + + )} + {!hasChildren &&
} + + - + + + + ); + + if (isInlineVariant) { + return ( +
+
- - + {rowContent} +
+ {isExpanded && children && children.map((child) => renderPinnedTreeItem(child, level + 1))}
); } return ( - { - e.preventDefault(); - handleDirectorySelect(path); - if (selectionBehavior === 'immediate') { - setIsOpen(false); - } - }} - className={cn( - 'flex items-start gap-2 cursor-pointer group py-2', - currentPath === path && 'bg-interactive-selection' - )} - > - -
-
{name}
-
- {formatPathForDisplay(path, homeDirectory)} -
-
- -
+ {rowContent} + + {isExpanded && children && ( +
+ {children.map((child) => renderPinnedTreeItem(child, level + 1))} +
+ )} +
+ ); + }; + + const renderPinnedRow = (name: string, path: string) => { + const isExpanded = pinnedExpandedPaths.has(path); + const children = pinnedItemChildren.get(path); + const isSelected = currentPath === path; + const isInlineVariant = variant === 'inline'; + + if (isInlineVariant) { + return ( +
+
+ + + + +
+ {isExpanded && children && children.map((child) => renderPinnedTreeItem(child, 1))} +
+ ); + } + + return ( +
+ { + e.preventDefault(); + handleDirectorySelect(path); + if (selectionBehavior === 'immediate') { + setIsOpen(false); + } + }} + className={cn( + 'flex items-start gap-2 cursor-pointer group py-2', + currentPath === path && 'bg-interactive-selection' + )} + > + + +
+
{name}
+
+ {formatPathForDisplay(path, homeDirectory)} +
+
+ +
+ {isExpanded && children && ( +
+ {children.map((child) => renderPinnedTreeItem(child, 1))} +
+ )} +
); };