mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
- Fix 75 ESLint errors by updating eslint.config.mjs: - Add missing browser globals (MouseEvent, AbortController, Response, etc.) - Add Vite define global (__APP_VERSION__) - Configure @ts-nocheck to require descriptions - Add no-unused-vars rule for .mjs scripts - Fix runtime bug in agent-output-modal.tsx (setOutput -> setStreamedContent) - Remove ~120 unused variable warnings across 97 files: - Remove unused imports (React hooks, lucide icons, types) - Remove unused constants and variables - Remove unused function definitions - Prefix intentionally unused parameters with underscore - Add descriptions to all @ts-nocheck comments (25 files) - Clean up misc issues: - Remove invalid deprecation plugin comments - Fix eslint-disable comment placement - Add missing RefreshCw import in code-view.tsx Reduces lint warnings from ~300 to 67 (all remaining are no-explicit-any) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
358 lines
12 KiB
TypeScript
358 lines
12 KiB
TypeScript
import { useState, useEffect, useCallback } from 'react';
|
|
import { FolderOpen, Folder, ChevronRight, HardDrive, Clock, X } from 'lucide-react';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
import { Button } from '@/components/ui/button';
|
|
import { PathInput } from '@/components/ui/path-input';
|
|
import { Kbd, KbdGroup } from '@/components/ui/kbd';
|
|
import { getDefaultWorkspaceDirectory, saveLastProjectDirectory } from '@/lib/workspace-config';
|
|
import { useOSDetection } from '@/hooks';
|
|
import { apiPost } from '@/lib/api-fetch';
|
|
import { useAppStore } from '@/store/app-store';
|
|
|
|
interface DirectoryEntry {
|
|
name: string;
|
|
path: string;
|
|
}
|
|
|
|
interface BrowseResult {
|
|
success: boolean;
|
|
currentPath: string;
|
|
parentPath: string | null;
|
|
directories: DirectoryEntry[];
|
|
drives?: string[];
|
|
error?: string;
|
|
warning?: string;
|
|
}
|
|
|
|
interface FileBrowserDialogProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
onSelect: (path: string) => void;
|
|
title?: string;
|
|
description?: string;
|
|
initialPath?: string;
|
|
}
|
|
|
|
export function FileBrowserDialog({
|
|
open,
|
|
onOpenChange,
|
|
onSelect,
|
|
title = 'Select Project Directory',
|
|
description = 'Navigate to your project folder or paste a path directly',
|
|
initialPath,
|
|
}: FileBrowserDialogProps) {
|
|
const { isMac } = useOSDetection();
|
|
const [currentPath, setCurrentPath] = useState<string>('');
|
|
const [parentPath, setParentPath] = useState<string | null>(null);
|
|
const [directories, setDirectories] = useState<DirectoryEntry[]>([]);
|
|
const [drives, setDrives] = useState<string[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState('');
|
|
const [warning, setWarning] = useState('');
|
|
|
|
// Use recent folders from app store (synced via API)
|
|
const recentFolders = useAppStore((s) => s.recentFolders);
|
|
const setRecentFolders = useAppStore((s) => s.setRecentFolders);
|
|
const addRecentFolder = useAppStore((s) => s.addRecentFolder);
|
|
|
|
const handleRemoveRecent = useCallback(
|
|
(e: React.MouseEvent, path: string) => {
|
|
e.stopPropagation();
|
|
const updated = recentFolders.filter((p) => p !== path);
|
|
setRecentFolders(updated);
|
|
},
|
|
[recentFolders, setRecentFolders]
|
|
);
|
|
|
|
const browseDirectory = useCallback(async (dirPath?: string) => {
|
|
setLoading(true);
|
|
setError('');
|
|
setWarning('');
|
|
|
|
try {
|
|
const result = await apiPost<BrowseResult>('/api/fs/browse', { dirPath });
|
|
|
|
if (result.success) {
|
|
setCurrentPath(result.currentPath);
|
|
setParentPath(result.parentPath);
|
|
setDirectories(result.directories);
|
|
setDrives(result.drives || []);
|
|
setWarning(result.warning || '');
|
|
} else {
|
|
setError(result.error || 'Failed to browse directory');
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to load directories');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const handleSelectRecent = useCallback(
|
|
(path: string) => {
|
|
browseDirectory(path);
|
|
},
|
|
[browseDirectory]
|
|
);
|
|
|
|
// Reset state when dialog closes
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setCurrentPath('');
|
|
setParentPath(null);
|
|
setDirectories([]);
|
|
setError('');
|
|
setWarning('');
|
|
}
|
|
}, [open]);
|
|
|
|
// Load initial path or workspace directory when dialog opens
|
|
useEffect(() => {
|
|
if (open && !currentPath) {
|
|
// Priority order:
|
|
// 1. Last selected directory from this file browser (from localStorage)
|
|
// 2. initialPath prop (from parent component)
|
|
// 3. Default workspace directory
|
|
// 4. Home directory
|
|
const loadInitialPath = async () => {
|
|
try {
|
|
// First, check for last selected directory from getDefaultWorkspaceDirectory
|
|
// which already implements the priority: last used > Documents/Automaker > DATA_DIR
|
|
const defaultDir = await getDefaultWorkspaceDirectory();
|
|
|
|
// If we have a default directory, use it (unless initialPath is explicitly provided and different)
|
|
const pathToUse = initialPath || defaultDir;
|
|
|
|
if (pathToUse) {
|
|
browseDirectory(pathToUse);
|
|
} else {
|
|
// No default directory, browse home directory
|
|
browseDirectory();
|
|
}
|
|
} catch {
|
|
// If config fetch fails, try initialPath or fall back to home directory
|
|
if (initialPath) {
|
|
browseDirectory(initialPath);
|
|
} else {
|
|
browseDirectory();
|
|
}
|
|
}
|
|
};
|
|
|
|
loadInitialPath();
|
|
}
|
|
}, [open, initialPath, currentPath, browseDirectory]);
|
|
|
|
const handleSelectDirectory = (dir: DirectoryEntry) => {
|
|
browseDirectory(dir.path);
|
|
};
|
|
|
|
const handleGoHome = useCallback(() => {
|
|
browseDirectory();
|
|
}, [browseDirectory]);
|
|
|
|
const handleNavigate = useCallback(
|
|
(path: string) => {
|
|
browseDirectory(path);
|
|
},
|
|
[browseDirectory]
|
|
);
|
|
|
|
const handleSelectDrive = (drivePath: string) => {
|
|
browseDirectory(drivePath);
|
|
};
|
|
|
|
const handleSelect = useCallback(() => {
|
|
if (currentPath) {
|
|
addRecentFolder(currentPath);
|
|
// Save to last project directory so it's used as default next time
|
|
saveLastProjectDirectory(currentPath);
|
|
onSelect(currentPath);
|
|
onOpenChange(false);
|
|
}
|
|
}, [currentPath, onSelect, onOpenChange]);
|
|
|
|
// Handle Command/Ctrl+Enter keyboard shortcut to select current folder
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
// Check for Command+Enter (Mac) or Ctrl+Enter (Windows/Linux)
|
|
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
e.preventDefault();
|
|
if (currentPath && !loading) {
|
|
handleSelect();
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [open, currentPath, loading, handleSelect]);
|
|
|
|
// Helper to get folder name from path
|
|
const getFolderName = (path: string) => {
|
|
const parts = path.split(/[/\\]/).filter(Boolean);
|
|
return parts[parts.length - 1] || path;
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="bg-popover border-border max-w-3xl max-h-[85vh] overflow-hidden flex flex-col p-4 focus:outline-none focus-visible:outline-none">
|
|
<DialogHeader className="pb-1">
|
|
<DialogTitle className="flex items-center gap-2 text-base">
|
|
<FolderOpen className="w-4 h-4 text-brand-500" />
|
|
{title}
|
|
</DialogTitle>
|
|
<DialogDescription className="text-muted-foreground text-xs">
|
|
{description}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="flex flex-col gap-2 min-h-[350px] flex-1 overflow-hidden py-1">
|
|
{/* Path navigation */}
|
|
<PathInput
|
|
currentPath={currentPath}
|
|
parentPath={parentPath}
|
|
loading={loading}
|
|
error={!!error}
|
|
onNavigate={handleNavigate}
|
|
onHome={handleGoHome}
|
|
entries={directories.map((dir) => ({ ...dir, isDirectory: true }))}
|
|
onSelectEntry={(entry) => {
|
|
if (entry.isDirectory) {
|
|
handleSelectDirectory(entry);
|
|
}
|
|
}}
|
|
/>
|
|
|
|
{/* Recent folders */}
|
|
{recentFolders.length > 0 && (
|
|
<div className="flex flex-wrap gap-1.5 p-2 rounded-md bg-sidebar-accent/10 border border-sidebar-border">
|
|
<div className="flex items-center gap-1 text-xs text-muted-foreground mr-1">
|
|
<Clock className="w-3 h-3" />
|
|
<span>Recent:</span>
|
|
</div>
|
|
{recentFolders.map((folder) => (
|
|
<button
|
|
key={folder}
|
|
onClick={() => handleSelectRecent(folder)}
|
|
className="group flex items-center gap-1 h-6 px-2 text-xs bg-sidebar-accent/20 hover:bg-sidebar-accent/40 rounded border border-sidebar-border transition-colors"
|
|
disabled={loading}
|
|
title={folder}
|
|
>
|
|
<Folder className="w-3 h-3 text-brand-500 shrink-0" />
|
|
<span className="truncate max-w-[120px]">{getFolderName(folder)}</span>
|
|
<button
|
|
onClick={(e) => handleRemoveRecent(e, folder)}
|
|
className="ml-0.5 opacity-0 group-hover:opacity-100 hover:text-destructive transition-opacity"
|
|
title="Remove from recent"
|
|
>
|
|
<X className="w-3 h-3" />
|
|
</button>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Drives selector (Windows only) */}
|
|
{drives.length > 0 && (
|
|
<div className="flex flex-wrap gap-1.5 p-2 rounded-md bg-sidebar-accent/10 border border-sidebar-border">
|
|
<div className="flex items-center gap-1 text-xs text-muted-foreground mr-1">
|
|
<HardDrive className="w-3 h-3" />
|
|
<span>Drives:</span>
|
|
</div>
|
|
{drives.map((drive) => (
|
|
<Button
|
|
key={drive}
|
|
variant={currentPath.startsWith(drive) ? 'default' : 'outline'}
|
|
size="sm"
|
|
onClick={() => handleSelectDrive(drive)}
|
|
className="h-6 px-2 text-xs"
|
|
disabled={loading}
|
|
>
|
|
{drive.replace('\\', '')}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Directory list */}
|
|
<div className="flex-1 overflow-y-auto border border-sidebar-border rounded-md scrollbar-styled">
|
|
{loading && (
|
|
<div className="flex items-center justify-center h-full p-4">
|
|
<div className="text-xs text-muted-foreground">Loading directories...</div>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="flex items-center justify-center h-full p-4">
|
|
<div className="text-xs text-destructive">{error}</div>
|
|
</div>
|
|
)}
|
|
|
|
{warning && (
|
|
<div className="p-2 bg-yellow-500/10 border border-yellow-500/30 rounded-md mb-1">
|
|
<div className="text-xs text-yellow-500">{warning}</div>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && !error && !warning && directories.length === 0 && (
|
|
<div className="flex items-center justify-center h-full p-4">
|
|
<div className="text-xs text-muted-foreground">No subdirectories found</div>
|
|
</div>
|
|
)}
|
|
|
|
{!loading && !error && directories.length > 0 && (
|
|
<div className="divide-y divide-sidebar-border">
|
|
{directories.map((dir) => (
|
|
<button
|
|
key={dir.path}
|
|
onClick={() => handleSelectDirectory(dir)}
|
|
className="w-full flex items-center gap-2 px-2 py-1.5 hover:bg-sidebar-accent/10 transition-colors text-left group"
|
|
>
|
|
<Folder className="w-4 h-4 text-brand-500 shrink-0" />
|
|
<span className="flex-1 truncate text-xs">{dir.name}</span>
|
|
<ChevronRight className="w-3.5 h-3.5 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="text-[10px] text-muted-foreground">
|
|
Paste a full path above, or click on folders to navigate. Press Enter or click → to jump
|
|
to a path.
|
|
</div>
|
|
</div>
|
|
|
|
<DialogFooter className="border-t border-border pt-3 gap-2 mt-1">
|
|
<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
onClick={handleSelect}
|
|
disabled={!currentPath || loading}
|
|
title="Select current folder (Cmd+Enter / Ctrl+Enter)"
|
|
>
|
|
<FolderOpen className="w-3.5 h-3.5 mr-1.5" />
|
|
Select Current Folder
|
|
<KbdGroup className="ml-1">
|
|
<Kbd>{isMac ? '⌘' : 'Ctrl'}</Kbd>
|
|
<Kbd>↵</Kbd>
|
|
</KbdGroup>
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|