"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 (
);
}