"use client"; import { useState, useMemo, useEffect, useCallback, useRef } from "react"; import { cn } from "@/lib/utils"; import { useAppStore, formatShortcut, type ThemeMode } from "@/store/app-store"; import { CoursePromoBadge } from "@/components/ui/course-promo-badge"; import { FolderOpen, Plus, Settings, FileText, LayoutGrid, Bot, Folder, X, PanelLeft, PanelLeftClose, ChevronDown, Redo2, Check, BookOpen, GripVertical, RotateCcw, Trash2, Undo2, UserCircle, MoreVertical, Palette, Monitor, Search, Bug, Activity, Recycle, Sparkles, Loader2, Terminal, Rocket, Zap, CheckCircle2, ArrowRight, } from "lucide-react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuLabel, } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { useKeyboardShortcuts, useKeyboardShortcutsConfig, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; import { getElectronAPI, Project, TrashedProject, RunningAgent, } from "@/lib/electron"; import { initializeProject, hasAppSpec, hasAutomakerDir, } from "@/lib/project-init"; import { toast } from "sonner"; import { themeOptions } from "@/config/theme-options"; import type { SpecRegenerationEvent } from "@/types/electron"; import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog"; import { NewProjectModal } from "@/components/new-project-modal"; import { ProjectSetupDialog, type FeatureCount, } from "@/components/layout/project-setup-dialog"; 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"; import { getHttpApiClient } from "@/lib/http-api-client"; import type { StarterTemplate } from "@/lib/templates"; interface NavSection { label?: string; items: NavItem[]; } interface NavItem { id: string; label: string; icon: any; shortcut?: string; } // Sortable Project Item Component interface SortableProjectItemProps { project: Project; currentProjectId: string | undefined; isHighlighted: boolean; onSelect: (project: Project) => void; } function SortableProjectItem({ project, currentProjectId, isHighlighted, 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 */} {/* Project content - clickable area */}
onSelect(project)} > {project.name} {currentProjectId === project.id && ( )}
); } // Theme options for project theme selector - derived from the shared config const PROJECT_THEME_OPTIONS = [ { value: "", label: "Use Global", icon: Monitor }, ...themeOptions.map((opt) => ({ value: opt.value, label: opt.label, icon: opt.Icon, })), ] as const; export function Sidebar() { const { projects, trashedProjects, currentProject, currentView, sidebarOpen, projectHistory, upsertAndSetCurrentProject, setCurrentProject, setCurrentView, toggleSidebar, restoreTrashedProject, deleteTrashedProject, emptyTrash, reorderProjects, cyclePrevProject, cycleNextProject, clearProjectHistory, setProjectTheme, setTheme, setPreviewTheme, theme: globalTheme, moveProjectToTrash, specCreatingForProject, setSpecCreatingForProject, } = useAppStore(); // Environment variable flags for hiding sidebar items // Note: Next.js requires static access to process.env variables (no dynamic keys) const hideTerminal = process.env.NEXT_PUBLIC_HIDE_TERMINAL === "true"; const hideWiki = process.env.NEXT_PUBLIC_HIDE_WIKI === "true"; const hideRunningAgents = process.env.NEXT_PUBLIC_HIDE_RUNNING_AGENTS === "true"; const hideContext = process.env.NEXT_PUBLIC_HIDE_CONTEXT === "true"; const hideSpecEditor = process.env.NEXT_PUBLIC_HIDE_SPEC_EDITOR === "true"; const hideAiProfiles = process.env.NEXT_PUBLIC_HIDE_AI_PROFILES === "true"; // Get customizable keyboard shortcuts const shortcuts = useKeyboardShortcutsConfig(); // State for project picker dropdown const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false); const [projectSearchQuery, setProjectSearchQuery] = useState(""); const [selectedProjectIndex, setSelectedProjectIndex] = useState(0); const [showTrashDialog, setShowTrashDialog] = useState(false); const [activeTrashId, setActiveTrashId] = useState(null); const [isEmptyingTrash, setIsEmptyingTrash] = useState(false); // State for delete project confirmation dialog const [showDeleteProjectDialog, setShowDeleteProjectDialog] = useState(false); // State for running agents count const [runningAgentsCount, setRunningAgentsCount] = useState(0); // State for new project modal const [showNewProjectModal, setShowNewProjectModal] = useState(false); const [isCreatingProject, setIsCreatingProject] = useState(false); // State for new project onboarding dialog const [showOnboardingDialog, setShowOnboardingDialog] = useState(false); const [newProjectName, setNewProjectName] = useState(""); const [newProjectPath, setNewProjectPath] = useState(""); // State for new project setup dialog const [showSetupDialog, setShowSetupDialog] = useState(false); const [setupProjectPath, setSetupProjectPath] = useState(""); const [projectOverview, setProjectOverview] = useState(""); const [generateFeatures, setGenerateFeatures] = useState(true); const [featureCount, setFeatureCount] = useState(50); const [showSpecIndicator, setShowSpecIndicator] = useState(true); // Derive isCreatingSpec from store state const isCreatingSpec = specCreatingForProject !== null; const creatingSpecProjectPath = specCreatingForProject; // Ref for project search input const projectSearchInputRef = useRef(null); // Auto-collapse sidebar on small screens useEffect(() => { const mediaQuery = window.matchMedia("(max-width: 1024px)"); // lg breakpoint const handleResize = () => { if (mediaQuery.matches && sidebarOpen) { // Auto-collapse on small screens toggleSidebar(); } }; // Check on mount handleResize(); // Listen for changes mediaQuery.addEventListener("change", handleResize); return () => mediaQuery.removeEventListener("change", handleResize); }, [sidebarOpen, toggleSidebar]); // Filtered projects based on search query const filteredProjects = useMemo(() => { if (!projectSearchQuery.trim()) { return projects; } const query = projectSearchQuery.toLowerCase(); return projects.filter((project) => project.name.toLowerCase().includes(query) ); }, [projects, projectSearchQuery]); // Reset selection when filtered results change useEffect(() => { setSelectedProjectIndex(0); }, [filteredProjects.length, projectSearchQuery]); // Reset search query when dropdown closes useEffect(() => { if (!isProjectPickerOpen) { setProjectSearchQuery(""); setSelectedProjectIndex(0); } }, [isProjectPickerOpen]); // Focus the search input when dropdown opens useEffect(() => { if (isProjectPickerOpen) { // Small delay to ensure the dropdown is rendered setTimeout(() => { projectSearchInputRef.current?.focus(); }, 0); } }, [isProjectPickerOpen]); // 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] ); // Subscribe to spec regeneration events useEffect(() => { const api = getElectronAPI(); if (!api.specRegeneration) return; const unsubscribe = api.specRegeneration.onEvent( (event: SpecRegenerationEvent) => { console.log( "[Sidebar] Spec regeneration event:", event.type, "for project:", event.projectPath ); // Only handle events for the project we're currently setting up if ( event.projectPath !== creatingSpecProjectPath && event.projectPath !== setupProjectPath ) { console.log( "[Sidebar] Ignoring event - not for project being set up" ); return; } if (event.type === "spec_regeneration_complete") { setSpecCreatingForProject(null); setShowSetupDialog(false); setProjectOverview(""); setSetupProjectPath(""); // Clear onboarding state if we came from onboarding setNewProjectName(""); setNewProjectPath(""); toast.success("App specification created", { description: "Your project is now set up and ready to go!", }); } else if (event.type === "spec_regeneration_error") { setSpecCreatingForProject(null); toast.error("Failed to create specification", { description: event.error, }); } } ); return () => { unsubscribe(); }; }, [ setCurrentView, creatingSpecProjectPath, setupProjectPath, setSpecCreatingForProject, ]); // Fetch running agents count function - used for initial load and event-driven updates const fetchRunningAgentsCount = useCallback(async () => { try { const api = getElectronAPI(); if (api.runningAgents) { const result = await api.runningAgents.getAll(); if (result.success && result.runningAgents) { setRunningAgentsCount(result.runningAgents.length); } } } catch (error) { console.error("[Sidebar] Error fetching running agents count:", error); } }, []); // Subscribe to auto-mode events to update running agents count in real-time useEffect(() => { const api = getElectronAPI(); if (!api.autoMode) { // If autoMode is not available, still fetch initial count fetchRunningAgentsCount(); return; } // Initial fetch on mount fetchRunningAgentsCount(); const unsubscribe = api.autoMode.onEvent((event) => { // When a feature starts, completes, or errors, refresh the count if ( event.type === "auto_mode_feature_complete" || event.type === "auto_mode_error" || event.type === "auto_mode_feature_start" ) { fetchRunningAgentsCount(); } }); return () => { unsubscribe(); }; }, [fetchRunningAgentsCount]); // Handle creating initial spec for new project const handleCreateInitialSpec = useCallback(async () => { if (!setupProjectPath || !projectOverview.trim()) return; // Set store state immediately so the loader shows up right away setSpecCreatingForProject(setupProjectPath); setShowSpecIndicator(true); setShowSetupDialog(false); try { const api = getElectronAPI(); if (!api.specRegeneration) { toast.error("Spec regeneration not available"); setSpecCreatingForProject(null); return; } const result = await api.specRegeneration.create( setupProjectPath, projectOverview.trim(), generateFeatures, undefined, // analyzeProject - use default generateFeatures ? featureCount : undefined // only pass maxFeatures if generating features ); if (!result.success) { console.error("[Sidebar] Failed to start spec creation:", result.error); setSpecCreatingForProject(null); toast.error("Failed to create specification", { description: result.error, }); } else { // Show processing toast to inform user toast.info("Generating app specification...", { description: "This may take a minute. You'll be notified when complete.", }); } // If successful, we'll wait for the events to update the state } catch (error) { console.error("[Sidebar] Failed to create spec:", error); setSpecCreatingForProject(null); toast.error("Failed to create specification", { description: error instanceof Error ? error.message : "Unknown error", }); } }, [ setupProjectPath, projectOverview, generateFeatures, featureCount, setSpecCreatingForProject, ]); // Handle skipping setup const handleSkipSetup = useCallback(() => { setShowSetupDialog(false); setProjectOverview(""); setSetupProjectPath(""); // Clear onboarding state if we came from onboarding if (newProjectPath) { setNewProjectName(""); setNewProjectPath(""); } toast.info("Setup skipped", { description: "You can set up your app_spec.txt later from the Spec view.", }); }, [newProjectPath]); // Handle onboarding dialog - generate spec const handleOnboardingGenerateSpec = useCallback(() => { setShowOnboardingDialog(false); // Navigate to the setup dialog flow setSetupProjectPath(newProjectPath); setProjectOverview(""); setShowSetupDialog(true); }, [newProjectPath]); // Handle onboarding dialog - skip const handleOnboardingSkip = useCallback(() => { setShowOnboardingDialog(false); setNewProjectName(""); setNewProjectPath(""); toast.info( "You can generate your app_spec.txt anytime from the Spec view", { description: "Your project is ready to use!", } ); }, []); /** * Create a blank project with just .automaker directory structure */ const handleCreateBlankProject = useCallback( async (projectName: string, parentDir: string) => { setIsCreatingProject(true); try { const api = getElectronAPI(); const projectPath = `${parentDir}/${projectName}`; // Create project directory const mkdirResult = await api.mkdir(projectPath); if (!mkdirResult.success) { toast.error("Failed to create project directory", { description: mkdirResult.error || "Unknown error occurred", }); return; } // Initialize .automaker directory with all necessary files const initResult = await initializeProject(projectPath); if (!initResult.success) { toast.error("Failed to initialize project", { description: initResult.error || "Unknown error occurred", }); return; } // Update the app_spec.txt with the project name // Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts await api.writeFile( `${projectPath}/.automaker/app_spec.txt`, ` ${projectName} Describe your project here. This file will be analyzed by an AI agent to understand your project structure and tech stack. ` ); const trashedProject = trashedProjects.find( (p) => p.path === projectPath ); const effectiveTheme = (trashedProject?.theme as ThemeMode | undefined) || (currentProject?.theme as ThemeMode | undefined) || globalTheme; const project = upsertAndSetCurrentProject( projectPath, projectName, effectiveTheme ); setShowNewProjectModal(false); // Show onboarding dialog for new project setNewProjectName(projectName); setNewProjectPath(projectPath); setShowOnboardingDialog(true); toast.success("Project created", { description: `Created ${projectName} with .automaker directory`, }); } catch (error) { console.error("[Sidebar] Failed to create project:", error); toast.error("Failed to create project", { description: error instanceof Error ? error.message : "Unknown error", }); } finally { setIsCreatingProject(false); } }, [trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject] ); /** * Create a project from a GitHub starter template */ const handleCreateFromTemplate = useCallback( async ( template: StarterTemplate, projectName: string, parentDir: string ) => { setIsCreatingProject(true); try { const httpClient = getHttpApiClient(); const api = getElectronAPI(); // Clone the template repository const cloneResult = await httpClient.templates.clone( template.repoUrl, projectName, parentDir ); if (!cloneResult.success || !cloneResult.projectPath) { toast.error("Failed to clone template", { description: cloneResult.error || "Unknown error occurred", }); return; } const projectPath = cloneResult.projectPath; // Initialize .automaker directory with all necessary files const initResult = await initializeProject(projectPath); if (!initResult.success) { toast.error("Failed to initialize project", { description: initResult.error || "Unknown error occurred", }); return; } // Update the app_spec.txt with template-specific info // Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts await api.writeFile( `${projectPath}/.automaker/app_spec.txt`, ` ${projectName} This project was created from the "${template.name}" starter template. ${template.description} ${template.techStack .map((tech) => `${tech}`) .join("\n ")} ${template.features .map((feature) => `${feature}`) .join("\n ")} ` ); const trashedProject = trashedProjects.find( (p) => p.path === projectPath ); const effectiveTheme = (trashedProject?.theme as ThemeMode | undefined) || (currentProject?.theme as ThemeMode | undefined) || globalTheme; const project = upsertAndSetCurrentProject( projectPath, projectName, effectiveTheme ); setShowNewProjectModal(false); // Show onboarding dialog for new project setNewProjectName(projectName); setNewProjectPath(projectPath); setShowOnboardingDialog(true); toast.success("Project created from template", { description: `Created ${projectName} from ${template.name}`, }); } catch (error) { console.error( "[Sidebar] Failed to create project from template:", error ); toast.error("Failed to create project", { description: error instanceof Error ? error.message : "Unknown error", }); } finally { setIsCreatingProject(false); } }, [trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject] ); /** * Create a project from a custom GitHub URL */ const handleCreateFromCustomUrl = useCallback( async (repoUrl: string, projectName: string, parentDir: string) => { setIsCreatingProject(true); try { const httpClient = getHttpApiClient(); const api = getElectronAPI(); // Clone the repository const cloneResult = await httpClient.templates.clone( repoUrl, projectName, parentDir ); if (!cloneResult.success || !cloneResult.projectPath) { toast.error("Failed to clone repository", { description: cloneResult.error || "Unknown error occurred", }); return; } const projectPath = cloneResult.projectPath; // Initialize .automaker directory with all necessary files const initResult = await initializeProject(projectPath); if (!initResult.success) { toast.error("Failed to initialize project", { description: initResult.error || "Unknown error occurred", }); return; } // Update the app_spec.txt with basic info // Note: Must follow XML format as defined in apps/server/src/lib/app-spec-format.ts await api.writeFile( `${projectPath}/.automaker/app_spec.txt`, ` ${projectName} This project was cloned from ${repoUrl}. The AI agent will analyze the project structure. ` ); const trashedProject = trashedProjects.find( (p) => p.path === projectPath ); const effectiveTheme = (trashedProject?.theme as ThemeMode | undefined) || (currentProject?.theme as ThemeMode | undefined) || globalTheme; const project = upsertAndSetCurrentProject( projectPath, projectName, effectiveTheme ); setShowNewProjectModal(false); // Show onboarding dialog for new project setNewProjectName(projectName); setNewProjectPath(projectPath); setShowOnboardingDialog(true); toast.success("Project created from repository", { description: `Created ${projectName} from ${repoUrl}`, }); } catch (error) { console.error("[Sidebar] Failed to create project from URL:", error); toast.error("Failed to create project", { description: error instanceof Error ? error.message : "Unknown error", }); } finally { setIsCreatingProject(false); } }, [trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject] ); /** * 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]; // Extract folder name from path (works on both Windows and Mac/Linux) const name = path.split(/[/\\]/).filter(Boolean).pop() || "Untitled Project"; try { // Check if this is a brand new project (no .automaker directory) const hadAutomakerDir = await hasAutomakerDir(path); // 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; } // Upsert project and set as current (handles both create and update cases) // Theme preservation is handled by the store action const trashedProject = trashedProjects.find((p) => p.path === path); const effectiveTheme = (trashedProject?.theme as ThemeMode | undefined) || (currentProject?.theme as ThemeMode | undefined) || globalTheme; const project = upsertAndSetCurrentProject(path, name, effectiveTheme); // Check if app_spec.txt exists const specExists = await hasAppSpec(path); if (!hadAutomakerDir && !specExists) { // This is a brand new project - show setup dialog setSetupProjectPath(path); setShowSetupDialog(true); toast.success("Project opened", { description: `Opened ${name}. Let's set up your app specification!`, }); } else 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", }); } } }, [ trashedProjects, upsertAndSetCurrentProject, currentProject, globalTheme, ]); const handleRestoreProject = useCallback( (projectId: string) => { restoreTrashedProject(projectId); toast.success("Project restored", { description: "Added back to your project list.", }); setShowTrashDialog(false); }, [restoreTrashedProject] ); const handleDeleteProjectFromDisk = useCallback( async (trashedProject: TrashedProject) => { const confirmed = window.confirm( `Delete "${trashedProject.name}" from disk?\nThis sends the folder to your system Trash.` ); if (!confirmed) return; setActiveTrashId(trashedProject.id); try { const api = getElectronAPI(); if (!api.trashItem) { throw new Error("System Trash is not available in this build."); } const result = await api.trashItem(trashedProject.path); if (!result.success) { throw new Error(result.error || "Failed to delete project folder"); } deleteTrashedProject(trashedProject.id); toast.success("Project folder sent to system Trash", { description: trashedProject.path, }); } catch (error) { console.error("[Sidebar] Failed to delete project from disk:", error); toast.error("Failed to delete project folder", { description: error instanceof Error ? error.message : "Unknown error", }); } finally { setActiveTrashId(null); } }, [deleteTrashedProject] ); const handleEmptyTrash = useCallback(() => { if (trashedProjects.length === 0) { setShowTrashDialog(false); return; } const confirmed = window.confirm( "Clear all projects from recycle bin? This does not delete folders from disk." ); if (!confirmed) return; setIsEmptyingTrash(true); try { emptyTrash(); toast.success("Recycle bin cleared"); setShowTrashDialog(false); } finally { setIsEmptyingTrash(false); } }, [emptyTrash, trashedProjects.length]); const navSections: NavSection[] = useMemo(() => { const allToolsItems: NavItem[] = [ { id: "spec", label: "Spec Editor", icon: FileText, shortcut: shortcuts.spec, }, { id: "context", label: "Context", icon: BookOpen, shortcut: shortcuts.context, }, { id: "profiles", label: "AI Profiles", icon: UserCircle, shortcut: shortcuts.profiles, }, { id: "terminal", label: "Terminal", icon: Terminal, shortcut: shortcuts.terminal, }, ]; // Filter out hidden items const visibleToolsItems = allToolsItems.filter((item) => { if (item.id === "spec" && hideSpecEditor) { return false; } if (item.id === "context" && hideContext) { return false; } if (item.id === "profiles" && hideAiProfiles) { return false; } if (item.id === "terminal" && hideTerminal) { return false; } return true; }); return [ { label: "Project", items: [ { id: "board", label: "Kanban Board", icon: LayoutGrid, shortcut: shortcuts.board, }, { id: "agent", label: "Agent Runner", icon: Bot, shortcut: shortcuts.agent, }, ], }, { label: "Tools", items: visibleToolsItems, }, ]; }, [shortcuts, hideSpecEditor, hideContext, hideTerminal, hideAiProfiles]); // Handle selecting the currently highlighted project const selectHighlightedProject = useCallback(() => { if ( filteredProjects.length > 0 && selectedProjectIndex < filteredProjects.length ) { setCurrentProject(filteredProjects[selectedProjectIndex]); setIsProjectPickerOpen(false); } }, [filteredProjects, selectedProjectIndex, setCurrentProject]); // Handle keyboard events when project picker is open useEffect(() => { if (!isProjectPickerOpen) return; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { setIsProjectPickerOpen(false); } else if (event.key === "Enter") { event.preventDefault(); selectHighlightedProject(); } else if (event.key === "ArrowDown") { event.preventDefault(); setSelectedProjectIndex((prev) => prev < filteredProjects.length - 1 ? prev + 1 : prev ); } else if (event.key === "ArrowUp") { event.preventDefault(); setSelectedProjectIndex((prev) => (prev > 0 ? prev - 1 : prev)); } else if ( event.key.toLowerCase() === "p" && !event.metaKey && !event.ctrlKey ) { // Toggle off when P is pressed (not with modifiers) while dropdown is open // Only if not typing in the search input if (document.activeElement !== projectSearchInputRef.current) { event.preventDefault(); setIsProjectPickerOpen(false); } } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [isProjectPickerOpen, selectHighlightedProject, filteredProjects.length]); // Build keyboard shortcuts for navigation const navigationShortcuts: KeyboardShortcut[] = useMemo(() => { const shortcutsList: KeyboardShortcut[] = []; // Sidebar toggle shortcut - always available shortcutsList.push({ key: shortcuts.toggleSidebar, action: () => toggleSidebar(), description: "Toggle sidebar", }); // Open project shortcut - opens the folder selection dialog directly shortcutsList.push({ key: shortcuts.openProject, action: () => handleOpenFolder(), description: "Open folder selection dialog", }); // Project picker shortcut - only when we have projects if (projects.length > 0) { shortcutsList.push({ key: shortcuts.projectPicker, action: () => setIsProjectPickerOpen((prev) => !prev), description: "Toggle project picker", }); } // Project cycling shortcuts - only when we have project history if (projectHistory.length > 1) { shortcutsList.push({ key: shortcuts.cyclePrevProject, action: () => cyclePrevProject(), description: "Cycle to previous project (MRU)", }); shortcutsList.push({ key: shortcuts.cycleNextProject, action: () => cycleNextProject(), description: "Cycle to next project (LRU)", }); } // Only enable nav shortcuts if there's a current project if (currentProject) { navSections.forEach((section) => { section.items.forEach((item) => { if (item.shortcut) { shortcutsList.push({ key: item.shortcut, action: () => setCurrentView(item.id as any), description: `Navigate to ${item.label}`, }); } }); }); // Add settings shortcut shortcutsList.push({ key: shortcuts.settings, action: () => setCurrentView("settings"), description: "Navigate to Settings", }); } return shortcutsList; }, [ shortcuts, currentProject, setCurrentView, toggleSidebar, projects.length, handleOpenFolder, projectHistory.length, cyclePrevProject, cycleNextProject, navSections, ]); // Register keyboard shortcuts useKeyboardShortcuts(navigationShortcuts); const isActiveRoute = (id: string) => { return currentView === id; }; return ( ); }