import { useState, useCallback, useEffect } from 'react'; import { createLogger } from '@automaker/utils/logger'; import { useNavigate, useLocation } from '@tanstack/react-router'; import { PanelLeftClose, ChevronDown } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useAppStore } from '@/store/app-store'; import { useNotificationsStore } from '@/store/notifications-store'; import { useKeyboardShortcuts, useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts'; import { getElectronAPI } from '@/lib/electron'; import { initializeProject, hasAppSpec, hasAutomakerDir } from '@/lib/project-init'; import { toast } from 'sonner'; import { useIsCompact } from '@/hooks/use-media-query'; import type { Project } from '@/lib/electron'; // Sidebar components import { SidebarNavigation, CollapseToggleButton, MobileSidebarToggle, SidebarHeader, SidebarFooter, } from './components'; import { SIDEBAR_FEATURE_FLAGS } from './constants'; import { useSidebarAutoCollapse, useRunningAgents, useSpecRegeneration, useNavigation, useProjectCreation, useSetupDialog, useTrashOperations, useUnviewedValidations, } from './hooks'; import { TrashDialog, OnboardingDialog } from './dialogs'; // Reuse dialogs from project-switcher import { ProjectContextMenu } from '../project-switcher/components/project-context-menu'; import { EditProjectDialog } from '../project-switcher/components/edit-project-dialog'; // Import shared dialogs import { DeleteProjectDialog } from '@/components/views/settings-view/components/delete-project-dialog'; import { NewProjectModal } from '@/components/dialogs/new-project-modal'; import { CreateSpecDialog } from '@/components/views/spec-view/dialogs'; const logger = createLogger('Sidebar'); export function Sidebar() { const navigate = useNavigate(); const location = useLocation(); const { projects, trashedProjects, currentProject, sidebarOpen, mobileSidebarHidden, projectHistory, upsertAndSetCurrentProject, toggleSidebar, toggleMobileSidebarHidden, restoreTrashedProject, deleteTrashedProject, emptyTrash, cyclePrevProject, cycleNextProject, moveProjectToTrash, specCreatingForProject, setSpecCreatingForProject, setCurrentProject, } = useAppStore(); const isCompact = useIsCompact(); // Environment variable flags for hiding sidebar items const { hideTerminal, hideRunningAgents, hideContext, hideSpecEditor, hideWiki } = SIDEBAR_FEATURE_FLAGS; // Get customizable keyboard shortcuts const shortcuts = useKeyboardShortcutsConfig(); // Get unread notifications count const unreadNotificationsCount = useNotificationsStore((s) => s.unreadCount); // State for context menu const [contextMenuProject, setContextMenuProject] = useState(null); const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | null>( null ); const [editDialogProject, setEditDialogProject] = useState(null); // State for delete project confirmation dialog const [showDeleteProjectDialog, setShowDeleteProjectDialog] = useState(false); // State for trash dialog const [showTrashDialog, setShowTrashDialog] = useState(false); // Project creation state and handlers const { showNewProjectModal, setShowNewProjectModal, isCreatingProject, showOnboardingDialog, setShowOnboardingDialog, newProjectName, setNewProjectName, newProjectPath, setNewProjectPath, handleCreateBlankProject, handleCreateFromTemplate, handleCreateFromCustomUrl, } = useProjectCreation({ upsertAndSetCurrentProject, }); // Setup dialog state and handlers const { showSetupDialog, setShowSetupDialog, setupProjectPath, setSetupProjectPath, projectOverview, setProjectOverview, generateFeatures, setGenerateFeatures, analyzeProject, setAnalyzeProject, featureCount, setFeatureCount, handleCreateInitialSpec, handleSkipSetup, handleOnboardingGenerateSpec, handleOnboardingSkip, } = useSetupDialog({ setSpecCreatingForProject, newProjectPath, setNewProjectName, setNewProjectPath, setShowOnboardingDialog, }); // Derive isCreatingSpec from store state const isCreatingSpec = specCreatingForProject !== null; const creatingSpecProjectPath = specCreatingForProject; // Check if the current project is specifically the one generating spec const isCurrentProjectGeneratingSpec = specCreatingForProject !== null && specCreatingForProject === currentProject?.path; // Auto-collapse sidebar on small screens useSidebarAutoCollapse({ sidebarOpen, toggleSidebar }); // Running agents count const { runningAgentsCount } = useRunningAgents(); // Unviewed validations count const { count: unviewedValidationsCount } = useUnviewedValidations(currentProject); // Trash operations const { activeTrashId, isEmptyingTrash, handleRestoreProject, handleDeleteProjectFromDisk, handleEmptyTrash, } = useTrashOperations({ restoreTrashedProject, deleteTrashedProject, emptyTrash, }); // Spec regeneration events useSpecRegeneration({ creatingSpecProjectPath, setupProjectPath, setSpecCreatingForProject, setShowSetupDialog, setProjectOverview, setSetupProjectPath, setNewProjectName, setNewProjectPath, }); // Context menu handlers const handleContextMenu = useCallback((project: Project, event: React.MouseEvent) => { event.preventDefault(); setContextMenuProject(project); setContextMenuPosition({ x: event.clientX, y: event.clientY }); }, []); const handleCloseContextMenu = useCallback(() => { setContextMenuProject(null); setContextMenuPosition(null); }, []); const handleEditProject = useCallback( (project: Project) => { setEditDialogProject(project); handleCloseContextMenu(); }, [handleCloseContextMenu] ); /** * Opens the system folder selection dialog and initializes the selected project. */ 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(/[/\\]/).filter(Boolean).pop() || 'Untitled Project'; try { const hadAutomakerDir = await hasAutomakerDir(path); const initResult = await initializeProject(path); if (!initResult.success) { toast.error('Failed to initialize project', { description: initResult.error || 'Unknown error occurred', }); return; } upsertAndSetCurrentProject(path, name); const specExists = await hasAppSpec(path); if (!hadAutomakerDir && !specExists) { 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}`, }); } navigate({ to: '/board' }); } catch (error) { logger.error('Failed to open project:', error); toast.error('Failed to open project', { description: error instanceof Error ? error.message : 'Unknown error', }); } } }, [upsertAndSetCurrentProject, navigate, setSetupProjectPath, setShowSetupDialog]); const handleNewProject = useCallback(() => { setShowNewProjectModal(true); }, [setShowNewProjectModal]); // Navigation sections and keyboard shortcuts const { navSections, navigationShortcuts } = useNavigation({ shortcuts, hideSpecEditor, hideContext, hideTerminal, currentProject, projects, projectHistory, navigate, toggleSidebar, handleOpenFolder, cyclePrevProject, cycleNextProject, unviewedValidationsCount, unreadNotificationsCount, isSpecGenerating: isCurrentProjectGeneratingSpec, }); // Register keyboard shortcuts useKeyboardShortcuts(navigationShortcuts); // Keyboard shortcuts for project switching (1-9, 0) useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { const target = event.target as HTMLElement; if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { return; } if (event.ctrlKey || event.metaKey || event.altKey) { return; } const key = event.key; let projectIndex: number | null = null; if (key >= '1' && key <= '9') { projectIndex = parseInt(key, 10) - 1; } else if (key === '0') { projectIndex = 9; } if (projectIndex !== null && projectIndex < projects.length) { const targetProject = projects[projectIndex]; if (targetProject && targetProject.id !== currentProject?.id) { setCurrentProject(targetProject); navigate({ to: '/board' }); } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [projects, currentProject, setCurrentProject, navigate]); const isActiveRoute = (id: string) => { const routePath = id === 'welcome' ? '/' : `/${id}`; return location.pathname === routePath; }; // Track if nav can scroll down const [canScrollDown, setCanScrollDown] = useState(false); // Check if sidebar should be completely hidden on mobile const shouldHideSidebar = isCompact && mobileSidebarHidden; return ( <> {/* Floating toggle to show sidebar on mobile when hidden */} {/* Mobile backdrop overlay */} {sidebarOpen && !shouldHideSidebar && (
)} {/* Context Menu */} {contextMenuProject && contextMenuPosition && ( )} {/* Edit Project Dialog */} {editDialogProject && ( !open && setEditDialogProject(null)} /> )} ); }