import { useState, useCallback } from 'react';
import { createLogger } from '@automaker/utils/logger';
import { useNavigate } from '@tanstack/react-router';
import { useAppStore, type ThemeMode } from '@/store/app-store';
import { useOSDetection } from '@/hooks/use-os-detection';
import { getElectronAPI, isElectron } from '@/lib/electron';
import { initializeProject } from '@/lib/project-init';
import { getHttpApiClient } from '@/lib/http-api-client';
import { isMac } from '@/lib/utils';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { NewProjectModal } from '@/components/dialogs/new-project-modal';
import { WorkspacePickerModal } from '@/components/dialogs/workspace-picker-modal';
import type { StarterTemplate } from '@/lib/templates';
import {
FolderOpen,
Plus,
Folder,
Star,
Clock,
Loader2,
ChevronDown,
MessageSquare,
Settings,
MoreVertical,
Trash2,
} from 'lucide-react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
const logger = createLogger('DashboardView');
function getOSAbbreviation(os: string): string {
switch (os) {
case 'mac':
return 'M';
case 'windows':
return 'W';
case 'linux':
return 'L';
default:
return '?';
}
}
export function DashboardView() {
const navigate = useNavigate();
const { os } = useOSDetection();
const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0';
const appMode = import.meta.env.VITE_APP_MODE || '?';
const versionSuffix = `${getOSAbbreviation(os)}${appMode}`;
const {
projects,
trashedProjects,
currentProject,
upsertAndSetCurrentProject,
addProject,
setCurrentProject,
toggleProjectFavorite,
moveProjectToTrash,
theme: globalTheme,
} = useAppStore();
const [showNewProjectModal, setShowNewProjectModal] = useState(false);
const [showWorkspacePicker, setShowWorkspacePicker] = useState(false);
const [isCreating, setIsCreating] = useState(false);
const [isOpening, setIsOpening] = useState(false);
const [projectToRemove, setProjectToRemove] = useState<{ id: string; name: string } | null>(null);
// Sort projects: favorites first, then by last opened
const sortedProjects = [...projects].sort((a, b) => {
// Favorites first
if (a.isFavorite && !b.isFavorite) return -1;
if (!a.isFavorite && b.isFavorite) return 1;
// Then by last opened
const dateA = a.lastOpened ? new Date(a.lastOpened).getTime() : 0;
const dateB = b.lastOpened ? new Date(b.lastOpened).getTime() : 0;
return dateB - dateA;
});
const favoriteProjects = sortedProjects.filter((p) => p.isFavorite);
const recentProjects = sortedProjects.filter((p) => !p.isFavorite);
/**
* Initialize project and navigate to board
*/
const initializeAndOpenProject = useCallback(
async (path: string, name: string) => {
setIsOpening(true);
try {
const initResult = await initializeProject(path);
if (!initResult.success) {
toast.error('Failed to initialize project', {
description: initResult.error || 'Unknown error occurred',
});
return;
}
const trashedProject = trashedProjects.find((p) => p.path === path);
const effectiveTheme =
(trashedProject?.theme as ThemeMode | undefined) ||
(currentProject?.theme as ThemeMode | undefined) ||
globalTheme;
upsertAndSetCurrentProject(path, name, effectiveTheme);
toast.success('Project opened', {
description: `Opened ${name}`,
});
navigate({ to: '/board' });
} catch (error) {
logger.error('[Dashboard] Failed to open project:', error);
toast.error('Failed to open project', {
description: error instanceof Error ? error.message : 'Unknown error',
});
} finally {
setIsOpening(false);
}
},
[trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject, navigate]
);
const handleOpenProject = useCallback(async () => {
try {
const httpClient = getHttpApiClient();
const configResult = await httpClient.workspace.getConfig();
if (configResult.success && configResult.configured) {
setShowWorkspacePicker(true);
} else {
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';
await initializeAndOpenProject(path, name);
}
}
} catch (error) {
logger.error('[Dashboard] Failed to check workspace config:', error);
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';
await initializeAndOpenProject(path, name);
}
}
}, [initializeAndOpenProject]);
const handleWorkspaceSelect = useCallback(
async (path: string, name: string) => {
setShowWorkspacePicker(false);
await initializeAndOpenProject(path, name);
},
[initializeAndOpenProject]
);
const handleProjectClick = useCallback(
async (project: { id: string; name: string; path: string }) => {
await initializeAndOpenProject(project.path, project.name);
},
[initializeAndOpenProject]
);
const handleToggleFavorite = useCallback(
(e: React.MouseEvent, projectId: string) => {
e.stopPropagation();
toggleProjectFavorite(projectId);
},
[toggleProjectFavorite]
);
const handleRemoveProject = useCallback(
(e: React.MouseEvent, project: { id: string; name: string }) => {
e.stopPropagation();
setProjectToRemove(project);
},
[]
);
const handleConfirmRemove = useCallback(() => {
if (projectToRemove) {
moveProjectToTrash(projectToRemove.id);
toast.success('Project removed', {
description: `${projectToRemove.name} has been removed from your projects list`,
});
setProjectToRemove(null);
}
}, [projectToRemove, moveProjectToTrash]);
const handleNewProject = () => {
setShowNewProjectModal(true);
};
const handleInteractiveMode = () => {
navigate({ to: '/interview' });
};
const handleCreateBlankProject = async (projectName: string, parentDir: string) => {
setIsCreating(true);
try {
const api = getElectronAPI();
const projectPath = `${parentDir}/${projectName}`;
const parentExists = await api.exists(parentDir);
if (!parentExists) {
toast.error('Parent directory does not exist', {
description: `Cannot create project in non-existent directory: ${parentDir}`,
});
return;
}
const parentStat = await api.stat(parentDir);
if (parentStat && !parentStat.stats?.isDirectory) {
toast.error('Parent path is not a directory', {
description: `${parentDir} is not a directory`,
});
return;
}
const mkdirResult = await api.mkdir(projectPath);
if (!mkdirResult.success) {
toast.error('Failed to create project directory', {
description: mkdirResult.error || 'Unknown error occurred',
});
return;
}
const initResult = await initializeProject(projectPath);
if (!initResult.success) {
toast.error('Failed to initialize project', {
description: initResult.error || 'Unknown error occurred',
});
return;
}
await api.writeFile(
`${projectPath}/.automaker/app_spec.txt`,
`
Your autonomous AI development studio. Get started by creating a new project or opening an existing one.
Create a new project from scratch with AI-powered development
Open an existing project folder to continue working
{project.name}
{project.path}
{project.lastOpened && ({new Date(project.lastOpened).toLocaleDateString()}
)}{project.name}
{project.path}
{project.lastOpened && ({new Date(project.lastOpened).toLocaleDateString()}
)}Opening project...