diff --git a/packages/ui/src/components/session/DirectoryExplorerDialog.tsx b/packages/ui/src/components/session/DirectoryExplorerDialog.tsx index 0fa04f6e..62e6498e 100644 --- a/packages/ui/src/components/session/DirectoryExplorerDialog.tsx +++ b/packages/ui/src/components/session/DirectoryExplorerDialog.tsx @@ -8,13 +8,16 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; -import { Toggle } from '@/components/ui/toggle'; +import { Input } from '@/components/ui/input'; import { DirectoryTree } from './DirectoryTree'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useFileSystemAccess } from '@/hooks/useFileSystemAccess'; import { cn, formatPathForDisplay } from '@/lib/utils'; import { toast } from 'sonner'; -import { RiCheckboxBlankLine, RiCheckboxLine } from '@remixicon/react'; +import { + RiCheckboxBlankLine, + RiCheckboxLine, +} from '@remixicon/react'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { useDeviceInfo } from '@/lib/device'; import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel'; @@ -32,6 +35,7 @@ export const DirectoryExplorerDialog: React.FC = ( }) => { const { currentDirectory, homeDirectory, setDirectory, isHomeReady } = useDirectoryStore(); const [pendingPath, setPendingPath] = React.useState(null); + const [pathInputValue, setPathInputValue] = React.useState(''); const [hasUserSelection, setHasUserSelection] = React.useState(false); const [isConfirming, setIsConfirming] = React.useState(false); const [showHidden, setShowHidden] = React.useState(() => { @@ -52,24 +56,37 @@ export const DirectoryExplorerDialog: React.FC = ( const { isDesktop, requestAccess, startAccessing } = useFileSystemAccess(); const { isMobile } = useDeviceInfo(); + // Helper to format path for display + const formatPath = React.useCallback((path: string | null) => { + if (!path) return ''; + return formatPathForDisplay(path, homeDirectory); + }, [homeDirectory]); + + // Reset state when dialog opens React.useEffect(() => { if (open) { - setPendingPath(null); setHasUserSelection(false); setIsConfirming(false); + // Initialize with current directory + const initialPath = currentDirectory || homeDirectory || ''; + setPendingPath(initialPath); + setPathInputValue(formatPath(initialPath)); } - }, [open]); + }, [open, currentDirectory, homeDirectory, formatPath]); + // Set initial pending path to home when ready (only if not yet selected) React.useEffect(() => { - if (!open) { + if (!open || hasUserSelection || pendingPath) { return; } - if (!hasUserSelection && !pendingPath && homeDirectory && isHomeReady) { + if (homeDirectory && isHomeReady) { setPendingPath(homeDirectory); setHasUserSelection(true); + setPathInputValue('~'); } }, [open, hasUserSelection, pendingPath, homeDirectory, isHomeReady]); + // Persist show hidden setting React.useEffect(() => { if (typeof window === 'undefined') { return; @@ -79,13 +96,6 @@ export const DirectoryExplorerDialog: React.FC = ( } catch { /* ignored */ } }, [showHidden]); - const formattedPendingPath = React.useMemo(() => { - if (!pendingPath) { - return 'Select a directory'; - } - return formatPathForDisplay(pendingPath, homeDirectory); - }, [pendingPath, homeDirectory]); - const handleClose = React.useCallback(() => { onOpenChange(false); }, [onOpenChange]); @@ -141,95 +151,141 @@ export const DirectoryExplorerDialog: React.FC = ( ]); const handleConfirm = React.useCallback(async () => { - if (!pendingPath) { + const pathToUse = pathInputValue.trim() || pendingPath; + if (!pathToUse) { return; } - await finalizeSelection(pendingPath); - }, [finalizeSelection, pendingPath]); + await finalizeSelection(pathToUse); + }, [finalizeSelection, pathInputValue, pendingPath]); const handleSelectPath = React.useCallback((path: string) => { setPendingPath(path); setHasUserSelection(true); - }, []); + setPathInputValue(formatPath(path)); + }, [formatPath]); const handleDoubleClickPath = React.useCallback(async (path: string) => { setPendingPath(path); setHasUserSelection(true); + setPathInputValue(formatPath(path)); await finalizeSelection(path); - }, [ - finalizeSelection, - ]); + }, [finalizeSelection, formatPath]); + + const handlePathInputChange = React.useCallback((e: React.ChangeEvent) => { + const value = e.target.value; + setPathInputValue(value); + setHasUserSelection(true); + // Update pending path if it looks like a valid path + if (value.startsWith('/') || value.startsWith('~')) { + // Expand ~ to home directory + const expandedPath = value.startsWith('~') && homeDirectory + ? value.replace(/^~/, homeDirectory) + : value; + setPendingPath(expandedPath); + } + }, [homeDirectory]); + + const handlePathInputKeyDown = React.useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleConfirm(); + } + }, [handleConfirm]); + + const toggleShowHidden = React.useCallback(() => { + setShowHidden(prev => !prev); + }, []); + + const dialogHeader = ( - + Select project directory - Choose the working directory used for sessions, commands, and OpenCode operations. + Choose the working directory for sessions and OpenCode operations. ); - const scrollContent = ( + const pathInputSection = ( + + ); + + const treeSection = ( +
+ +
+ ); + + const showHiddenToggle = ( + + ); + + // Mobile: use flex layout where tree takes remaining space + const mobileContent = ( +
+
{pathInputSection}
+
+ {showHiddenToggle} +
+
+ +
+
+ ); + + const desktopContent = ( -
- Currently selected -
- {formatPathForDisplay(currentDirectory, homeDirectory)} -
-
- -
-
- -
- -
-
- Selected directory -
- {formattedPendingPath} -
-
- setShowHidden(Boolean(value))} - variant="outline" - className="w-full justify-start gap-2 rounded-xl border-border/40 bg-sidebar/60 px-3 py-2 text-foreground min-w-0 h-auto sm:px-4 sm:py-3" - > - {showHidden ? ( - - ) : ( - - )} - Show hidden directories - -
-

- Use the tree to browse, pin frequently used locations, or create a new directory. - Select a folder, then confirm to update the working directory for OpenChamber. -

-
-
+ {pathInputSection} +
+ {showHiddenToggle}
+ {treeSection} ); @@ -239,16 +295,16 @@ export const DirectoryExplorerDialog: React.FC = ( variant="ghost" onClick={handleClose} disabled={isConfirming} - className="w-full sm:w-auto" + className="flex-1 sm:flex-none sm:w-auto" > Cancel ); @@ -260,9 +316,10 @@ export const DirectoryExplorerDialog: React.FC = ( onClose={() => onOpenChange(false)} title="Select project directory" className="max-w-full" - footer={
{renderActionButtons()}
} + contentMaxHeightClassName="max-h-[min(70vh,520px)] h-[min(70vh,520px)]" + footer={
{renderActionButtons()}
} > - {scrollContent} + {mobileContent} ); } @@ -271,13 +328,17 @@ export const DirectoryExplorerDialog: React.FC = ( { + // Prevent auto-focus on input to avoid text selection + e.preventDefault(); + }} > {dialogHeader} - {scrollContent} + {desktopContent} {renderActionButtons()} diff --git a/packages/ui/src/components/session/DirectoryTree.tsx b/packages/ui/src/components/session/DirectoryTree.tsx index a1334a6d..5cd6837c 100644 --- a/packages/ui/src/components/session/DirectoryTree.tsx +++ b/packages/ui/src/components/session/DirectoryTree.tsx @@ -8,7 +8,7 @@ import { DropdownMenuTrigger, DropdownMenuSeparator, } from '@/components/ui/dropdown-menu'; -import { RiAddLine, RiArrowDownSLine, RiArrowRightSLine, RiCheckLine, RiCloseLine, RiFolder6Line, RiPushpin2Line, RiPushpinLine } from '@remixicon/react'; +import { RiAddLine, RiArrowDownSLine, RiArrowRightSLine, RiCheckLine, RiCloseLine, RiFolder6Line, RiPushpin2Line, RiPushpinLine, RiStarLine } from '@remixicon/react'; import { cn, formatPathForDisplay } from '@/lib/utils'; import { opencodeClient } from '@/lib/opencode/client'; import { isDesktopRuntime, getDesktopSettings } from '@/lib/desktop'; @@ -36,6 +36,8 @@ interface DirectoryTreeProps { showHidden?: boolean; rootDirectory?: string | null; isRootReady?: boolean; + /** Always show action icons (add, pin) instead of only on hover */ + alwaysShowActions?: boolean; } export const DirectoryTree: React.FC = ({ @@ -49,6 +51,7 @@ export const DirectoryTree: React.FC = ({ showHidden = false, rootDirectory = null, isRootReady, + alwaysShowActions = false, }) => { const desktopRuntime = React.useMemo(() => isDesktopRuntime(), []); const [directories, setDirectories] = React.useState([]); @@ -59,6 +62,7 @@ export const DirectoryTree: React.FC = ({ const [pinnedPaths, setPinnedPaths] = React.useState>(new Set()); const [creatingInPath, setCreatingInPath] = React.useState(null); const [newDirName, setNewDirName] = React.useState(''); + const [isPinnedExpanded, setIsPinnedExpanded] = React.useState(true); const inputRef = React.useRef(null); const { requestAccess, startAccessing, isDesktop } = useFileSystemAccess(); const previousShowHidden = React.useRef(showHidden); @@ -674,7 +678,10 @@ export const DirectoryTree: React.FC = ({ e.stopPropagation(); startCreatingDirectory(item); }} - className="p-1 opacity-0 group-hover:opacity-100 hover:bg-accent rounded transition-opacity" + className={cn( + "p-1 hover:bg-accent rounded transition-opacity", + alwaysShowActions ? "opacity-70" : "opacity-0 group-hover:opacity-100" + )} title="Create new directory" > @@ -685,7 +692,10 @@ export const DirectoryTree: React.FC = ({ e.stopPropagation(); togglePin(item.path); }} - className="p-1 opacity-0 group-hover:opacity-100 hover:bg-accent rounded transition-opacity" + className={cn( + "p-1 hover:bg-accent rounded transition-opacity", + alwaysShowActions ? "opacity-70" : "opacity-0 group-hover:opacity-100" + )} title={isPinned ? "Unpin directory" : "Pin directory"} > {isPinned ? ( @@ -958,15 +968,32 @@ export const DirectoryTree: React.FC = ({ <> {pinnedDirectories.length > 0 && ( <> -
- Pinned -
- {pinnedDirectories.map(({ name, path }) => renderPinnedRow(name, path))} + + {isPinnedExpanded && pinnedDirectories.map(({ name, path }) => renderPinnedRow(name, path))} {variant === 'dropdown' && } + {variant === 'inline' && isPinnedExpanded && ( +
+ )} )} -
+
+ Browse
diff --git a/packages/ui/src/components/session/SessionSidebar.tsx b/packages/ui/src/components/session/SessionSidebar.tsx index dd6dc5c4..07aacf0b 100644 --- a/packages/ui/src/components/session/SessionSidebar.tsx +++ b/packages/ui/src/components/session/SessionSidebar.tsx @@ -861,7 +861,8 @@ export const SessionSidebar: React.FC = ({