"use client"; import { useState, useMemo, useEffect, useCallback } from "react"; import { cn } from "@/lib/utils"; import { useAppStore } from "@/store/app-store"; import Link from "next/link"; import { FolderOpen, Plus, Settings, FileText, LayoutGrid, Bot, ChevronLeft, ChevronRight, Folder, X, Wrench, PanelLeft, PanelLeftClose, Sparkles, ChevronDown, Check, BookOpen, GripVertical, } from "lucide-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useKeyboardShortcuts, NAV_SHORTCUTS, UI_SHORTCUTS, ACTION_SHORTCUTS, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; import { getElectronAPI, Project } from "@/lib/electron"; import { initializeProject } from "@/lib/project-init"; import { toast } from "sonner"; import { DndContext, DragEndEvent, PointerSensor, useSensor, useSensors, closestCenter, } from "@dnd-kit/core"; import { SortableContext, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; interface NavSection { label?: string; items: NavItem[]; } interface NavItem { id: string; label: string; icon: any; shortcut?: string; } // Sortable Project Item Component interface SortableProjectItemProps { project: Project; index: number; currentProjectId: string | undefined; onSelect: (project: Project) => void; } function SortableProjectItem({ project, index, currentProjectId, onSelect, }: SortableProjectItemProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: project.id }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }; return (
{/* Drag Handle */} {/* Hotkey indicator */} {index < 9 && ( {index + 1} )} {/* Project content - clickable area */}
onSelect(project)} > {project.name} {currentProjectId === project.id && ( )}
); } export function Sidebar() { const { projects, currentProject, currentView, sidebarOpen, addProject, setCurrentProject, setCurrentView, toggleSidebar, removeProject, reorderProjects, } = useAppStore(); // State for project picker dropdown const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false); // Sensors for drag-and-drop const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 5, // Small distance to start drag }, }) ); // Handle drag end for reordering projects const handleDragEnd = useCallback( (event: DragEndEvent) => { const { active, over } = event; if (over && active.id !== over.id) { const oldIndex = projects.findIndex((p) => p.id === active.id); const newIndex = projects.findIndex((p) => p.id === over.id); if (oldIndex !== -1 && newIndex !== -1) { reorderProjects(oldIndex, newIndex); } } }, [projects, reorderProjects] ); /** * Opens the system folder selection dialog and initializes the selected project. * Used by both the 'O' keyboard shortcut and the folder icon button. */ const handleOpenFolder = useCallback(async () => { const api = getElectronAPI(); const result = await api.openDirectory(); if (!result.canceled && result.filePaths[0]) { const path = result.filePaths[0]; const name = path.split("/").pop() || "Untitled Project"; try { // Initialize the .automaker directory structure const initResult = await initializeProject(path); if (!initResult.success) { toast.error("Failed to initialize project", { description: initResult.error || "Unknown error occurred", }); return; } const project = { id: `project-${Date.now()}`, name, path, lastOpened: new Date().toISOString(), }; addProject(project); setCurrentProject(project); if (initResult.createdFiles && initResult.createdFiles.length > 0) { toast.success( initResult.isNewProject ? "Project initialized" : "Project updated", { description: `Set up ${initResult.createdFiles.length} file(s) in .automaker`, } ); } else { toast.success("Project opened", { description: `Opened ${name}`, }); } } catch (error) { console.error("[Sidebar] Failed to open project:", error); toast.error("Failed to open project", { description: error instanceof Error ? error.message : "Unknown error", }); } } }, [addProject, setCurrentProject]); const navSections: NavSection[] = [ { label: "Project", items: [ { id: "board", label: "Kanban Board", icon: LayoutGrid, shortcut: NAV_SHORTCUTS.board, }, { id: "agent", label: "Agent Runner", icon: Bot, shortcut: NAV_SHORTCUTS.agent, }, ], }, { label: "Tools", items: [ { id: "spec", label: "Spec Editor", icon: FileText, shortcut: NAV_SHORTCUTS.spec, }, { id: "context", label: "Context", icon: BookOpen, shortcut: NAV_SHORTCUTS.context, }, { id: "tools", label: "Agent Tools", icon: Wrench, shortcut: NAV_SHORTCUTS.tools, }, ], }, ]; // Handler for selecting a project by number key const selectProjectByNumber = useCallback( (num: number) => { const projectIndex = num - 1; if (projectIndex >= 0 && projectIndex < projects.length) { setCurrentProject(projects[projectIndex]); setIsProjectPickerOpen(false); } }, [projects, setCurrentProject] ); // Handle keyboard events when project picker is open useEffect(() => { if (!isProjectPickerOpen) return; const handleKeyDown = (event: KeyboardEvent) => { const num = parseInt(event.key, 10); if (num >= 1 && num <= 9) { event.preventDefault(); selectProjectByNumber(num); } else if (event.key === "Escape") { setIsProjectPickerOpen(false); } else if (event.key.toLowerCase() === "p") { // Toggle off when P is pressed while dropdown is open event.preventDefault(); setIsProjectPickerOpen(false); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [isProjectPickerOpen, selectProjectByNumber]); // Build keyboard shortcuts for navigation const navigationShortcuts: KeyboardShortcut[] = useMemo(() => { const shortcuts: KeyboardShortcut[] = []; // Sidebar toggle shortcut - always available shortcuts.push({ key: UI_SHORTCUTS.toggleSidebar, action: () => toggleSidebar(), description: "Toggle sidebar", }); // Open project shortcut - opens the folder selection dialog directly shortcuts.push({ key: ACTION_SHORTCUTS.openProject, action: () => handleOpenFolder(), description: "Open folder selection dialog", }); // Project picker shortcut - only when we have projects if (projects.length > 0) { shortcuts.push({ key: ACTION_SHORTCUTS.projectPicker, action: () => setIsProjectPickerOpen((prev) => !prev), description: "Toggle project picker", }); } // Only enable nav shortcuts if there's a current project if (currentProject) { navSections.forEach((section) => { section.items.forEach((item) => { if (item.shortcut) { shortcuts.push({ key: item.shortcut, action: () => setCurrentView(item.id as any), description: `Navigate to ${item.label}`, }); } }); }); // Add settings shortcut shortcuts.push({ key: NAV_SHORTCUTS.settings, action: () => setCurrentView("settings"), description: "Navigate to Settings", }); } return shortcuts; }, [ currentProject, setCurrentView, toggleSidebar, projects.length, handleOpenFolder, ]); // Register keyboard shortcuts useKeyboardShortcuts(navigationShortcuts); const isActiveRoute = (id: string) => { return currentView === id; }; return ( ); }