diff --git a/apps/ui/src/components/layout/index.ts b/apps/ui/src/components/layout/index.ts
index bfed6246..d702d78d 100644
--- a/apps/ui/src/components/layout/index.ts
+++ b/apps/ui/src/components/layout/index.ts
@@ -1 +1,2 @@
export { Sidebar } from './sidebar';
+export { UnifiedSidebar } from './unified-sidebar';
diff --git a/apps/ui/src/components/layout/unified-sidebar/components/index.ts b/apps/ui/src/components/layout/unified-sidebar/components/index.ts
new file mode 100644
index 00000000..42f3195f
--- /dev/null
+++ b/apps/ui/src/components/layout/unified-sidebar/components/index.ts
@@ -0,0 +1,2 @@
+export { SidebarHeader } from './sidebar-header';
+export { SidebarFooter } from './sidebar-footer';
diff --git a/apps/ui/src/components/layout/unified-sidebar/components/sidebar-footer.tsx b/apps/ui/src/components/layout/unified-sidebar/components/sidebar-footer.tsx
new file mode 100644
index 00000000..1c8bcc8e
--- /dev/null
+++ b/apps/ui/src/components/layout/unified-sidebar/components/sidebar-footer.tsx
@@ -0,0 +1,372 @@
+import { useCallback } from 'react';
+import type { NavigateOptions } from '@tanstack/react-router';
+import { cn } from '@/lib/utils';
+import { formatShortcut } from '@/store/app-store';
+import { Activity, Settings, User, Bug, BookOpen, ExternalLink } from 'lucide-react';
+import { useOSDetection } from '@/hooks/use-os-detection';
+import { getElectronAPI } from '@/lib/electron';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+
+function getOSAbbreviation(os: string): string {
+ switch (os) {
+ case 'mac':
+ return 'M';
+ case 'windows':
+ return 'W';
+ case 'linux':
+ return 'L';
+ default:
+ return '?';
+ }
+}
+
+interface SidebarFooterProps {
+ sidebarOpen: boolean;
+ isActiveRoute: (id: string) => boolean;
+ navigate: (opts: NavigateOptions) => void;
+ hideRunningAgents: boolean;
+ hideWiki: boolean;
+ runningAgentsCount: number;
+ shortcuts: {
+ settings: string;
+ };
+}
+
+export function SidebarFooter({
+ sidebarOpen,
+ isActiveRoute,
+ navigate,
+ hideRunningAgents,
+ hideWiki,
+ runningAgentsCount,
+ shortcuts,
+}: SidebarFooterProps) {
+ const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0';
+ const { os } = useOSDetection();
+ const appMode = import.meta.env.VITE_APP_MODE || '?';
+ const versionSuffix = `${getOSAbbreviation(os)}${appMode}`;
+
+ const handleWikiClick = useCallback(() => {
+ navigate({ to: '/wiki' });
+ }, [navigate]);
+
+ const handleBugReportClick = useCallback(() => {
+ const api = getElectronAPI();
+ api.openExternalLink('https://github.com/AutoMaker-Org/automaker/issues');
+ }, []);
+
+ // Collapsed state
+ if (!sidebarOpen) {
+ return (
+
+
+ {/* Running Agents */}
+ {!hideRunningAgents && (
+
+
+
+
+
+
+ Running Agents
+ {runningAgentsCount > 0 && (
+
+ {runningAgentsCount}
+
+ )}
+
+
+
+ )}
+
+ {/* Settings */}
+
+
+
+
+
+
+ Global Settings
+
+ {formatShortcut(shortcuts.settings, true)}
+
+
+
+
+
+ {/* User Dropdown */}
+
+
+
+
+
+
+
+
+
+ More options
+
+
+
+
+ {!hideWiki && (
+
+
+ Documentation
+
+ )}
+
+
+ Report Bug
+
+
+
+
+
+ v{appVersion} {versionSuffix}
+
+
+
+
+
+
+ );
+ }
+
+ // Expanded state
+ return (
+
+ {/* Running Agents Link */}
+ {!hideRunningAgents && (
+
+
+
+ )}
+
+ {/* Settings Link */}
+
+
+
+
+ {/* User area with dropdown */}
+
+
+
+
+
+
+ {!hideWiki && (
+
+
+ Documentation
+
+ )}
+
+
+ Report Bug / Feature Request
+
+
+
+
+
+
+ );
+}
diff --git a/apps/ui/src/components/layout/unified-sidebar/components/sidebar-header.tsx b/apps/ui/src/components/layout/unified-sidebar/components/sidebar-header.tsx
new file mode 100644
index 00000000..4a531718
--- /dev/null
+++ b/apps/ui/src/components/layout/unified-sidebar/components/sidebar-header.tsx
@@ -0,0 +1,349 @@
+import { useState, useCallback } from 'react';
+import { useNavigate } from '@tanstack/react-router';
+import { ChevronDown, Folder, Plus, FolderOpen, Check } from 'lucide-react';
+import * as LucideIcons from 'lucide-react';
+import type { LucideIcon } from 'lucide-react';
+import { cn, isMac } from '@/lib/utils';
+import { isElectron, type Project } from '@/lib/electron';
+import { getAuthenticatedImageUrl } from '@/lib/api-fetch';
+import { useAppStore } from '@/store/app-store';
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from '@/components/ui/dropdown-menu';
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from '@/components/ui/tooltip';
+
+interface SidebarHeaderProps {
+ sidebarOpen: boolean;
+ currentProject: Project | null;
+ onNewProject: () => void;
+ onOpenFolder: () => void;
+ onProjectContextMenu: (project: Project, event: React.MouseEvent) => void;
+}
+
+export function SidebarHeader({
+ sidebarOpen,
+ currentProject,
+ onNewProject,
+ onOpenFolder,
+ onProjectContextMenu,
+}: SidebarHeaderProps) {
+ const navigate = useNavigate();
+ const { projects, setCurrentProject } = useAppStore();
+ const [dropdownOpen, setDropdownOpen] = useState(false);
+
+ const handleLogoClick = useCallback(() => {
+ navigate({ to: '/dashboard' });
+ }, [navigate]);
+
+ const handleProjectSelect = useCallback(
+ (project: Project) => {
+ setCurrentProject(project);
+ setDropdownOpen(false);
+ navigate({ to: '/board' });
+ },
+ [setCurrentProject, navigate]
+ );
+
+ const getIconComponent = (project: Project): LucideIcon => {
+ if (project?.icon && project.icon in LucideIcons) {
+ return (LucideIcons as unknown as Record)[project.icon];
+ }
+ return Folder;
+ };
+
+ const renderProjectIcon = (project: Project, size: 'sm' | 'md' = 'md') => {
+ const IconComponent = getIconComponent(project);
+ const sizeClasses = size === 'sm' ? 'w-6 h-6' : 'w-8 h-8';
+ const iconSizeClasses = size === 'sm' ? 'w-4 h-4' : 'w-5 h-5';
+
+ if (project.customIconPath) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ // Collapsed state - show logo only
+ if (!sidebarOpen) {
+ return (
+
+
+
+
+
+
+
+ Go to Dashboard
+
+
+
+
+ {/* Collapsed project icon */}
+ {currentProject && (
+ <>
+
+
+
+
+
+
+
+ {currentProject.name}
+
+
+
+ >
+ )}
+
+ );
+ }
+
+ // Expanded state - show logo + project dropdown
+ return (
+
+ {/* Header with logo and project dropdown */}
+
+ {/* Logo */}
+
+
+ {/* Project Dropdown */}
+ {currentProject ? (
+
+
+
+
+
+
+ Projects
+
+ {projects.map((project, index) => {
+ const isActive = currentProject?.id === project.id;
+ const hotkeyLabel = index < 9 ? `${index + 1}` : index === 9 ? '0' : undefined;
+
+ return (
+ handleProjectSelect(project)}
+ onContextMenu={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setDropdownOpen(false);
+ onProjectContextMenu(project, e);
+ }}
+ className={cn(
+ 'flex items-center gap-3 cursor-pointer',
+ isActive && 'bg-brand-500/10'
+ )}
+ data-testid={`project-item-${project.id}`}
+ >
+ {renderProjectIcon(project, 'sm')}
+ {project.name}
+ {hotkeyLabel && (
+
+ {hotkeyLabel}
+
+ )}
+ {isActive && }
+
+ );
+ })}
+
+ {
+ setDropdownOpen(false);
+ onNewProject();
+ }}
+ className="cursor-pointer"
+ data-testid="new-project-dropdown-item"
+ >
+
+ New Project
+
+ {
+ setDropdownOpen(false);
+ onOpenFolder();
+ }}
+ className="cursor-pointer"
+ data-testid="open-project-dropdown-item"
+ >
+
+ Open Project
+
+
+
+ ) : (
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/ui/src/components/layout/unified-sidebar/index.ts b/apps/ui/src/components/layout/unified-sidebar/index.ts
new file mode 100644
index 00000000..a88954e5
--- /dev/null
+++ b/apps/ui/src/components/layout/unified-sidebar/index.ts
@@ -0,0 +1 @@
+export { UnifiedSidebar } from './unified-sidebar';
diff --git a/apps/ui/src/components/layout/unified-sidebar/unified-sidebar.tsx b/apps/ui/src/components/layout/unified-sidebar/unified-sidebar.tsx
new file mode 100644
index 00000000..eb8841ac
--- /dev/null
+++ b/apps/ui/src/components/layout/unified-sidebar/unified-sidebar.tsx
@@ -0,0 +1,479 @@
+import { useState, useCallback, useEffect } from 'react';
+import { createLogger } from '@automaker/utils/logger';
+import { useNavigate, useLocation } from '@tanstack/react-router';
+import { PanelLeftClose } 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';
+
+// Reuse existing sidebar components
+import { SidebarNavigation, CollapseToggleButton, MobileSidebarToggle } from '../sidebar/components';
+import { SIDEBAR_FEATURE_FLAGS } from '../sidebar/constants';
+import {
+ useSidebarAutoCollapse,
+ useRunningAgents,
+ useSpecRegeneration,
+ useNavigation,
+ useProjectCreation,
+ useSetupDialog,
+ useTrashOperations,
+ useUnviewedValidations,
+} from '../sidebar/hooks';
+import { TrashDialog, OnboardingDialog } from '../sidebar/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';
+
+// Local components
+import { SidebarHeader, SidebarFooter } from './components';
+
+const logger = createLogger('UnifiedSidebar');
+
+export function UnifiedSidebar() {
+ 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;
+ };
+
+ // 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)}
+ />
+ )}
+ >
+ );
+}
diff --git a/apps/ui/src/routes/__root.tsx b/apps/ui/src/routes/__root.tsx
index 907d2b19..f8379c70 100644
--- a/apps/ui/src/routes/__root.tsx
+++ b/apps/ui/src/routes/__root.tsx
@@ -3,8 +3,7 @@ import { useEffect, useState, useCallback, useDeferredValue, useRef } from 'reac
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { createLogger } from '@automaker/utils/logger';
-import { Sidebar } from '@/components/layout/sidebar';
-import { ProjectSwitcher } from '@/components/layout/project-switcher';
+import { UnifiedSidebar } from '@/components/layout/unified-sidebar';
import {
FileBrowserProvider,
useFileBrowser,
@@ -171,8 +170,6 @@ function RootLayoutContent() {
skipSandboxWarning,
setSkipSandboxWarning,
fetchCodexModels,
- sidebarOpen,
- toggleSidebar,
} = useAppStore();
const { setupComplete, codexCliStatus } = useSetupStore();
const navigate = useNavigate();
@@ -186,7 +183,7 @@ function RootLayoutContent() {
// Load project settings when switching projects
useProjectSettingsLoader();
- // Check if we're in compact mode (< 1240px) to hide project switcher
+ // Check if we're in compact mode (< 1240px)
const isCompact = useIsCompact();
const isSetupRoute = location.pathname === '/setup';
@@ -853,11 +850,6 @@ function RootLayoutContent() {
);
}
- // Show project switcher on all app pages (not on dashboard, setup, or login)
- // Also hide on compact screens (< 1240px) - the sidebar will show a logo instead
- const showProjectSwitcher =
- !isDashboardRoute && !isSetupRoute && !isLoginRoute && !isLoggedOutRoute && !isCompact;
-
return (
<>
@@ -868,8 +860,7 @@ function RootLayoutContent() {
aria-hidden="true"
/>
)}
- {showProjectSwitcher && }
-
+