import { useState, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import type { User, Project } from '../lib/api'; import { projectApi, droneApi, chatSessionApi, type DroneRegistration, type ChatSession, type AiProvider, providerApi } from '../lib/api'; import { socketClient } from '../lib/socket'; interface ProjectManagerProps { user: User | null; } function NewProjectForm({ onCancel, onSuccess }: { onCancel: () => void; onSuccess: () => void }) { const [name, setName] = useState(''); const [slug, setSlug] = useState(''); const [gitUrl, setGitUrl] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(''); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!name || !slug) { setError('Name and slug are required'); return; } setSubmitting(true); setError(''); try { await projectApi.create({ name, slug, gitUrl: gitUrl || undefined }); onSuccess(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create project'); } finally { setSubmitting(false); } }; return (

Create New Project

setName(e.target.value)} className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none" placeholder="My Project" />
setSlug(e.target.value)} className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none" placeholder="my-project" />

Unique identifier for the project directory

setGitUrl(e.target.value)} className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none" placeholder="https://github.com/user/repo.git" />
{error &&

{error}

}
); } interface ProjectInspectorProps { project: Project; onDelete: () => void; } function ProjectInspector({ project, onDelete }: ProjectInspectorProps) { const [deleting, setDeleting] = useState(false); const handleDelete = async () => { if (!confirm('Are you sure you want to delete this project? This cannot be undone.')) { return; } setDeleting(true); try { await projectApi.delete(project._id); onDelete(); } catch (err) { console.error('Failed to delete project', err); } finally { setDeleting(false); } }; return (

Project Inspector

Name
{project.name}
Slug
{project.slug}
Git URL
{project.gitUrl || Not configured}
Status
{project.status}
Created
{new Date(project.createdAt).toLocaleDateString()}
); } interface RightSidebarProps { project: Project; onOpenChatSession: (sessionId: string) => void; } function RightSidebar({ project, onOpenChatSession }: RightSidebarProps) { const [drones, setDrones] = useState([]); const [chatSessions, setChatSessions] = useState([]); const [providers, setProviders] = useState([]); const [selectedDrone, setSelectedDrone] = useState(null); const [showNewChatModal, setShowNewChatModal] = useState(false); const [loading, setLoading] = useState(true); const [deletingSessions, setDeletingSessions] = useState>(new Set()); useEffect(() => { loadData(); }, [project]); const loadData = async () => { setLoading(true); try { // Load available drones (backend filters offline) const availableDrones = await droneApi.getAll(); setDrones(availableDrones); // Load chat sessions for this project const sessions = await chatSessionApi.getAll(project._id); setChatSessions(sessions); // Load providers for new chat session const allProviders = await providerApi.getAll(); setProviders(allProviders); } catch (err) { console.error('Failed to load sidebar data', err); } finally { setLoading(false); } }; const handleSelectDrone = (drone: DroneRegistration) => { setSelectedDrone(drone); }; const handleCreateChatSession = async (data: { providerId: string; selectedModel: string; mode: string; name?: string; }) => { try { const session = await chatSessionApi.create({ projectId: project._id, providerId: data.providerId, selectedModel: data.selectedModel, mode: data.mode as any, name: data.name, }); setShowNewChatModal(false); // Lock the drone to this session BEFORE navigating if (selectedDrone) { const success = await socketClient.requestSessionLock(selectedDrone, project, session); if (!success) { console.error('Failed to lock drone session'); } } onOpenChatSession(session._id); await loadData(); // Refresh list } catch (err) { console.error('Failed to create chat session', err); } }; const handleDeleteChatSession = async (sessionId: string) => { if (!confirm('Delete this chat session?')) return; setDeletingSessions(prev => new Set(prev).add(sessionId)); try { await chatSessionApi.delete(sessionId); await loadData(); } catch (err) { console.error('Failed to delete chat session', err); } finally { setDeletingSessions(prev => { const next = new Set(prev); next.delete(sessionId); return next; }); } }; return ( <> {/* New Chat Session Modal */} {showNewChatModal && ( setShowNewChatModal(false)} onCreate={handleCreateChatSession} /> )} ); } interface NewChatSessionModalProps { providers: AiProvider[]; selectedDrone: DroneRegistration | null; onCancel: () => void; onCreate: (data: { providerId: string; selectedModel: string; mode: string; name?: string }) => void; } function NewChatSessionModal({ providers, selectedDrone, onCancel, onCreate }: NewChatSessionModalProps) { const [selectedProviderId, setSelectedProviderId] = useState(''); const [selectedModel, setSelectedModel] = useState(''); const [mode, setMode] = useState('build'); const [name, setName] = useState(''); const [creating, setCreating] = useState(false); const selectedProvider = providers.find(p => p._id === selectedProviderId); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedProviderId || !selectedModel) { return; } setCreating(true); try { await onCreate({ providerId: selectedProviderId, selectedModel, mode, name: name || undefined, }); } catch (err) { console.error('Failed to create chat session', err); } finally { setCreating(false); } }; return (

New Chat Session

{selectedDrone && (
Selected Drone
{selectedDrone.hostname}
{selectedDrone.workspaceDir}
)}
setName(e.target.value)} className="w-full px-3 py-2 bg-bg-tertiary border border-border-default rounded text-text-primary focus:border-brand focus:outline-none" placeholder="Auto-generated from first prompt" />
{selectedProvider && (
{selectedProvider.models.length === 0 && (

No models discovered. Run `pnpm cli provider probe {selectedProvider._id}` to discover models.

)}
)}
); } export default function ProjectManager({ user }: ProjectManagerProps) { const navigate = useNavigate(); const { slug } = useParams(); const [projects, setProjects] = useState([]); const [selectedProject, setSelectedProject] = useState(null); const [loading, setLoading] = useState(true); const [showNewForm, setShowNewForm] = useState(false); useEffect(() => { loadProjects(); }, []); useEffect(() => { if (slug && projects.length > 0) { const found = projects.find((p) => p.slug === slug); setSelectedProject(found || null); } else if (!slug) { setSelectedProject(null); } }, [slug, projects]); const loadProjects = async () => { try { const data = await projectApi.getAll(); setProjects(data); } catch (err) { console.error('Failed to load projects', err); } finally { setLoading(false); } }; const handleSelectProject = (project: Project) => { navigate(`/projects/${project.slug}`); }; const handleProjectCreated = () => { setShowNewForm(false); loadProjects(); }; const handleProjectDeleted = () => { setSelectedProject(null); loadProjects(); navigate('/projects'); }; const handleOpenChatSession = (sessionId: string) => { if (selectedProject) { navigate(`/projects/${selectedProject._id}/chat-session/${sessionId}`); } }; if (!user) { return (

Please sign in to view projects.

); } if (loading) { return (

Loading projects...

); } return (
{/* Left Sidebar - Project List */} {/* Main Content Area */}
{showNewForm ? (
setShowNewForm(false)} onSuccess={handleProjectCreated} />
) : selectedProject ? ( <> {/* Center - Project Inspector */} {/* Right Sidebar - Drones & Chat Sessions */} ) : (

Select a project to view details

or create a new project to get started

)}
); }