feat: add Windows drive switcher dropdown in FolderPicker

When navigating to a drive root on Windows (e.g. C:\), path.dirname
returns itself, disabling the "go up" button and trapping the user on
that drive. This adds a compact dropdown next to the up button that
lists all available drives (auto-detected via fs.accessSync), allowing
quick switching between drives. macOS/Linux are unaffected (empty array,
dropdown not rendered).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
xuxu777xuandClaude Opus 4.6 committed 2026-02-07 13:29:59 +08:00
1 parent 262b295f63
commit 3ae44a7277
2 files changed
+53 -1

No files matched your search

+18
View File
@@ -4,6 +4,21 @@ import path from 'path';
import os from 'os';
import type { ErrorResponse } from '@/types';
function getWindowsDrives(): string[] {
if (process.platform !== 'win32') return [];
const drives: string[] = [];
for (let i = 65; i <= 90; i++) {
const drive = String.fromCharCode(i) + ':\\';
try {
fs.accessSync(drive);
drives.push(drive);
} catch {
// drive not available
}
}
return drives;
}
// List only directories for folder browsing (no safety restriction since user is choosing where to work)
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
@@ -28,10 +43,13 @@ export async function GET(request: NextRequest) {
}))
.sort((a, b) => a.name.localeCompare(b.name));
const drives = getWindowsDrives();
return NextResponse.json({
current: resolvedDir,
parent: path.dirname(resolvedDir) !== resolvedDir ? path.dirname(resolvedDir) : null,
directories,
drives,
});
} catch {
return NextResponse.json<ErrorResponse>(
+35 -1
View File
@@ -11,6 +11,12 @@ import {
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
@@ -23,6 +29,7 @@ interface BrowseResponse {
current: string;
parent: string | null;
directories: FolderEntry[];
drives?: string[];
}
interface FolderPickerProps {
@@ -38,6 +45,7 @@ export function FolderPicker({ open, onOpenChange, onSelect, initialPath }: Fold
const [directories, setDirectories] = useState<FolderEntry[]>([]);
const [loading, setLoading] = useState(false);
const [pathInput, setPathInput] = useState('');
const [drives, setDrives] = useState<string[]>([]);
const browse = useCallback(async (dir?: string) => {
setLoading(true);
@@ -52,6 +60,7 @@ export function FolderPicker({ open, onOpenChange, onSelect, initialPath }: Fold
setParentDir(data.parent);
setDirectories(data.directories);
setPathInput(data.current);
setDrives(data.drives || []);
}
} catch {
// silently fail
@@ -108,7 +117,7 @@ export function FolderPicker({ open, onOpenChange, onSelect, initialPath }: Fold
{/* Directory browser */}
<div className="rounded-md border border-border">
{/* Current path + go up */}
{/* Current path + go up + drive switcher */}
<div className="flex items-center gap-2 border-b border-border bg-muted/30 px-3 py-2">
<Button
variant="ghost"
@@ -119,6 +128,31 @@ export function FolderPicker({ open, onOpenChange, onSelect, initialPath }: Fold
>
<HugeiconsIcon icon={ArrowUp01Icon} className="h-4 w-4" />
</Button>
{drives.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-6 px-1.5 text-xs font-mono shrink-0">
{currentDir.charAt(0).toUpperCase()}:
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{drives.map((drive) => {
const letter = drive.charAt(0).toUpperCase();
const isCurrent = currentDir.toUpperCase().startsWith(letter + ':');
return (
<DropdownMenuItem
key={drive}
className="font-mono text-sm gap-2"
onClick={() => browse(drive)}
>
<span className={isCurrent ? 'font-bold' : ''}>{letter}:</span>
<span className="text-muted-foreground text-xs">{drive}</span>
</DropdownMenuItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
)}
<span className="truncate text-xs font-mono text-muted-foreground">
{currentDir}
</span>