From c8ed3fafce8281c26f5c6651138e6763325d5ea2 Mon Sep 17 00:00:00 2001 From: Stefan de Vogelaere Date: Fri, 23 Jan 2026 02:18:36 +0100 Subject: [PATCH] feat(ui): add New Project and Open Project buttons to Dashboard header Add project creation and folder opening functionality directly to the Dashboard view header for quick access. Includes NewProjectModal with template support and WorkspacePickerModal integration. Also renamed title to "Automaker Dashboard". Co-Authored-By: Claude Opus 4.5 --- .../ui/src/components/views/overview-view.tsx | 258 +++++++++++++++++- 1 file changed, 256 insertions(+), 2 deletions(-) diff --git a/apps/ui/src/components/views/overview-view.tsx b/apps/ui/src/components/views/overview-view.tsx index b280f7dd..68ae60f5 100644 --- a/apps/ui/src/components/views/overview-view.tsx +++ b/apps/ui/src/components/views/overview-view.tsx @@ -5,19 +5,31 @@ * recent completions, and alerts. Quick navigation to any project or feature. */ +import { useState, useCallback } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { createLogger } from '@automaker/utils/logger'; import { useMultiProjectStatus } from '@/hooks/use-multi-project-status'; -import { isElectron } from '@/lib/electron'; +import { useAppStore } from '@/store/app-store'; +import { isElectron, getElectronAPI } from '@/lib/electron'; import { isMac } from '@/lib/utils'; +import { initializeProject } from '@/lib/project-init'; +import { getHttpApiClient } from '@/lib/http-api-client'; +import { toast } from 'sonner'; import { Spinner } from '@/components/ui/spinner'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { NewProjectModal } from '@/components/dialogs/new-project-modal'; +import { WorkspacePickerModal } from '@/components/dialogs/workspace-picker-modal'; import { ProjectStatusCard } from './overview/project-status-card'; import { RecentActivityFeed } from './overview/recent-activity-feed'; import { RunningAgentsPanel } from './overview/running-agents-panel'; +import type { StarterTemplate } from '@/lib/templates'; import { LayoutDashboard, RefreshCw, Folder, + FolderOpen, + Plus, Activity, CheckCircle2, XCircle, @@ -26,8 +38,222 @@ import { Bell, } from 'lucide-react'; +const logger = createLogger('OverviewView'); + export function OverviewView() { + const navigate = useNavigate(); const { overview, isLoading, error, refresh } = useMultiProjectStatus(15000); // Refresh every 15s + const { addProject, setCurrentProject } = useAppStore(); + + // Modal state + const [showNewProjectModal, setShowNewProjectModal] = useState(false); + const [showWorkspacePicker, setShowWorkspacePicker] = useState(false); + const [isCreating, setIsCreating] = useState(false); + + 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('[Overview] 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); + } + } + }, []); + + const initializeAndOpenProject = useCallback( + async (path: string, name: string) => { + try { + 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); + + toast.success('Project opened', { description: `Opened ${name}` }); + navigate({ to: '/board' }); + } catch (error) { + logger.error('[Overview] Failed to open project:', error); + toast.error('Failed to open project', { + description: error instanceof Error ? error.message : 'Unknown error', + }); + } + }, + [addProject, setCurrentProject, navigate] + ); + + const handleWorkspaceSelect = useCallback( + async (path: string, name: string) => { + setShowWorkspacePicker(false); + await initializeAndOpenProject(path, name); + }, + [initializeAndOpenProject] + ); + + const handleCreateBlankProject = useCallback( + async (projectName: string, parentDir: string) => { + setIsCreating(true); + try { + const api = getElectronAPI(); + const projectPath = `${parentDir}/${projectName}`; + + await api.mkdir(projectPath); + await initializeProject(projectPath); + + await api.writeFile( + `${projectPath}/.automaker/app_spec.txt`, + ` + ${projectName} + Describe your project here. + + + +` + ); + + const project = { + id: `project-${Date.now()}`, + name: projectName, + path: projectPath, + lastOpened: new Date().toISOString(), + }; + + addProject(project); + setCurrentProject(project); + setShowNewProjectModal(false); + + toast.success('Project created', { description: `Created ${projectName}` }); + navigate({ to: '/board' }); + } catch (error) { + logger.error('Failed to create project:', error); + toast.error('Failed to create project', { + description: error instanceof Error ? error.message : 'Unknown error', + }); + } finally { + setIsCreating(false); + } + }, + [addProject, setCurrentProject, navigate] + ); + + const handleCreateFromTemplate = useCallback( + async (template: StarterTemplate, projectName: string, parentDir: string) => { + setIsCreating(true); + try { + const httpClient = getHttpApiClient(); + 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; + } + + await initializeProject(cloneResult.projectPath); + + const project = { + id: `project-${Date.now()}`, + name: projectName, + path: cloneResult.projectPath, + lastOpened: new Date().toISOString(), + }; + + addProject(project); + setCurrentProject(project); + setShowNewProjectModal(false); + + toast.success('Project created from template', { + description: `Created ${projectName} from ${template.name}`, + }); + navigate({ to: '/board' }); + } catch (error) { + logger.error('Failed to create from template:', error); + toast.error('Failed to create project', { + description: error instanceof Error ? error.message : 'Unknown error', + }); + } finally { + setIsCreating(false); + } + }, + [addProject, setCurrentProject, navigate] + ); + + const handleCreateFromCustomUrl = useCallback( + async (repoUrl: string, projectName: string, parentDir: string) => { + setIsCreating(true); + try { + const httpClient = getHttpApiClient(); + 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; + } + + await initializeProject(cloneResult.projectPath); + + const project = { + id: `project-${Date.now()}`, + name: projectName, + path: cloneResult.projectPath, + lastOpened: new Date().toISOString(), + }; + + addProject(project); + setCurrentProject(project); + setShowNewProjectModal(false); + + toast.success('Project created from repository', { description: `Created ${projectName}` }); + navigate({ to: '/board' }); + } catch (error) { + logger.error('Failed to create from custom URL:', error); + toast.error('Failed to create project', { + description: error instanceof Error ? error.message : 'Unknown error', + }); + } finally { + setIsCreating(false); + } + }, + [addProject, setCurrentProject, navigate] + ); return (
@@ -47,7 +273,7 @@ export function OverviewView() {
-

Auto-Maker Dashboard

+

Automaker Dashboard

{overview ? `${overview.aggregate.projectCounts.total} projects` : 'Loading...'}

@@ -66,6 +292,18 @@ export function OverviewView() { Refresh + +
@@ -268,6 +506,22 @@ export function OverviewView() { )} + + {/* Modals */} + + + ); }