mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 06:12:06 +00:00
Major feature implementation for parallel agent execution with dependency-aware scheduling and an engaging multi-agent UI experience. Backend Changes: - Add parallel_orchestrator.py for concurrent feature processing - Add api/dependency_resolver.py with cycle detection (Kahn's algorithm + DFS) - Add atomic feature_claim_next() with retry limit and exponential backoff - Fix circular dependency check arguments in 4 locations - Add AgentTracker class for parsing agent output and emitting updates - Add browser isolation with --isolated flag for Playwright MCP - Extend WebSocket protocol with agent_update messages and log attribution - Add WSAgentUpdateMessage schema with agent states and mascot names - Fix WSProgressMessage to include in_progress field New UI Components: - AgentMissionControl: Dashboard showing active agents with collapsible activity - AgentCard: Individual agent status with avatar and thought bubble - AgentAvatar: SVG mascots (Spark, Fizz, Octo, Hoot, Buzz) with animations - ActivityFeed: Recent activity stream with stable keys (no flickering) - CelebrationOverlay: Confetti animation with click/Escape dismiss - DependencyGraph: Interactive node graph visualization with dagre layout - DependencyBadge: Visual indicator for feature dependencies - ViewToggle: Switch between Kanban and Graph views - KeyboardShortcutsHelp: Help overlay accessible via ? key UI/UX Improvements: - Celebration queue system to handle rapid success messages - Accessibility attributes on AgentAvatar (role, aria-label, aria-live) - Collapsible Recent Activity section with persisted preference - Agent count display in header - Keyboard shortcut G to toggle Kanban/Graph view - Real-time thought bubbles and state animations Bug Fixes: - Fix circular dependency validation (swapped source/target arguments) - Add MAX_CLAIM_RETRIES=10 to prevent stack overflow under contention - Fix THOUGHT_PATTERNS to match actual [Tool: name] format - Fix ActivityFeed key prop to prevent re-renders on new items - Add featureId/agentIndex to log messages for proper attribution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
428 lines
14 KiB
TypeScript
428 lines
14 KiB
TypeScript
/**
|
|
* New Project Modal Component
|
|
*
|
|
* Multi-step modal for creating new projects:
|
|
* 1. Enter project name
|
|
* 2. Select project folder
|
|
* 3. Choose spec method (Claude or manual)
|
|
* 4a. If Claude: Show SpecCreationChat
|
|
* 4b. If manual: Create project and close
|
|
*/
|
|
|
|
import { useState } from 'react'
|
|
import { X, Bot, FileEdit, ArrowRight, ArrowLeft, Loader2, CheckCircle2, Folder } from 'lucide-react'
|
|
import { useCreateProject } from '../hooks/useProjects'
|
|
import { SpecCreationChat } from './SpecCreationChat'
|
|
import { FolderBrowser } from './FolderBrowser'
|
|
import { startAgent } from '../lib/api'
|
|
|
|
type InitializerStatus = 'idle' | 'starting' | 'error'
|
|
|
|
type Step = 'name' | 'folder' | 'method' | 'chat' | 'complete'
|
|
type SpecMethod = 'claude' | 'manual'
|
|
|
|
interface NewProjectModalProps {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
onProjectCreated: (projectName: string) => void
|
|
onStepChange?: (step: Step) => void
|
|
}
|
|
|
|
export function NewProjectModal({
|
|
isOpen,
|
|
onClose,
|
|
onProjectCreated,
|
|
onStepChange,
|
|
}: NewProjectModalProps) {
|
|
const [step, setStep] = useState<Step>('name')
|
|
const [projectName, setProjectName] = useState('')
|
|
const [projectPath, setProjectPath] = useState<string | null>(null)
|
|
const [_specMethod, setSpecMethod] = useState<SpecMethod | null>(null)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [initializerStatus, setInitializerStatus] = useState<InitializerStatus>('idle')
|
|
const [initializerError, setInitializerError] = useState<string | null>(null)
|
|
const [yoloModeSelected, setYoloModeSelected] = useState(false)
|
|
|
|
// Suppress unused variable warning - specMethod may be used in future
|
|
void _specMethod
|
|
|
|
const createProject = useCreateProject()
|
|
|
|
// Wrapper to notify parent of step changes
|
|
const changeStep = (newStep: Step) => {
|
|
setStep(newStep)
|
|
onStepChange?.(newStep)
|
|
}
|
|
|
|
if (!isOpen) return null
|
|
|
|
const handleNameSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
const trimmed = projectName.trim()
|
|
|
|
if (!trimmed) {
|
|
setError('Please enter a project name')
|
|
return
|
|
}
|
|
|
|
if (!/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
|
|
setError('Project name can only contain letters, numbers, hyphens, and underscores')
|
|
return
|
|
}
|
|
|
|
setError(null)
|
|
changeStep('folder')
|
|
}
|
|
|
|
const handleFolderSelect = (path: string) => {
|
|
setProjectPath(path) // Use selected path directly - no subfolder creation
|
|
changeStep('method')
|
|
}
|
|
|
|
const handleFolderCancel = () => {
|
|
changeStep('name')
|
|
}
|
|
|
|
const handleMethodSelect = async (method: SpecMethod) => {
|
|
setSpecMethod(method)
|
|
|
|
if (!projectPath) {
|
|
setError('Please select a project folder first')
|
|
changeStep('folder')
|
|
return
|
|
}
|
|
|
|
if (method === 'manual') {
|
|
// Create project immediately with manual method
|
|
try {
|
|
const project = await createProject.mutateAsync({
|
|
name: projectName.trim(),
|
|
path: projectPath,
|
|
specMethod: 'manual',
|
|
})
|
|
changeStep('complete')
|
|
setTimeout(() => {
|
|
onProjectCreated(project.name)
|
|
handleClose()
|
|
}, 1500)
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : 'Failed to create project')
|
|
}
|
|
} else {
|
|
// Create project then show chat
|
|
try {
|
|
await createProject.mutateAsync({
|
|
name: projectName.trim(),
|
|
path: projectPath,
|
|
specMethod: 'claude',
|
|
})
|
|
changeStep('chat')
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : 'Failed to create project')
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleSpecComplete = async (_specPath: string, yoloMode: boolean = false) => {
|
|
// Save yoloMode for retry
|
|
setYoloModeSelected(yoloMode)
|
|
// Auto-start the initializer agent
|
|
setInitializerStatus('starting')
|
|
try {
|
|
await startAgent(projectName.trim(), { yoloMode })
|
|
// Success - navigate to project
|
|
changeStep('complete')
|
|
setTimeout(() => {
|
|
onProjectCreated(projectName.trim())
|
|
handleClose()
|
|
}, 1500)
|
|
} catch (err) {
|
|
setInitializerStatus('error')
|
|
setInitializerError(err instanceof Error ? err.message : 'Failed to start agent')
|
|
}
|
|
}
|
|
|
|
const handleRetryInitializer = () => {
|
|
setInitializerError(null)
|
|
setInitializerStatus('idle')
|
|
handleSpecComplete('', yoloModeSelected)
|
|
}
|
|
|
|
const handleChatCancel = () => {
|
|
// Go back to method selection but keep the project
|
|
changeStep('method')
|
|
setSpecMethod(null)
|
|
}
|
|
|
|
const handleExitToProject = () => {
|
|
// Exit chat and go directly to project - user can start agent manually
|
|
onProjectCreated(projectName.trim())
|
|
handleClose()
|
|
}
|
|
|
|
const handleClose = () => {
|
|
changeStep('name')
|
|
setProjectName('')
|
|
setProjectPath(null)
|
|
setSpecMethod(null)
|
|
setError(null)
|
|
setInitializerStatus('idle')
|
|
setInitializerError(null)
|
|
setYoloModeSelected(false)
|
|
onClose()
|
|
}
|
|
|
|
const handleBack = () => {
|
|
if (step === 'method') {
|
|
changeStep('folder')
|
|
setSpecMethod(null)
|
|
} else if (step === 'folder') {
|
|
changeStep('name')
|
|
setProjectPath(null)
|
|
}
|
|
}
|
|
|
|
// Full-screen chat view
|
|
if (step === 'chat') {
|
|
return (
|
|
<div className="fixed inset-0 z-50 bg-[var(--color-neo-bg)]">
|
|
<SpecCreationChat
|
|
projectName={projectName.trim()}
|
|
onComplete={handleSpecComplete}
|
|
onCancel={handleChatCancel}
|
|
onExitToProject={handleExitToProject}
|
|
initializerStatus={initializerStatus}
|
|
initializerError={initializerError}
|
|
onRetryInitializer={handleRetryInitializer}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Folder step uses larger modal
|
|
if (step === 'folder') {
|
|
return (
|
|
<div className="neo-modal-backdrop" onClick={handleClose}>
|
|
<div
|
|
className="neo-modal w-full max-w-3xl max-h-[85vh] flex flex-col"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-4 border-b-3 border-[var(--color-neo-border)]">
|
|
<div className="flex items-center gap-3">
|
|
<Folder size={24} className="text-[var(--color-neo-progress)]" />
|
|
<div>
|
|
<h2 className="font-display font-bold text-xl text-[var(--color-neo-text)]">
|
|
Select Project Location
|
|
</h2>
|
|
<p className="text-sm text-[var(--color-neo-text-secondary)]">
|
|
Select the folder to use for project <span className="font-bold font-mono">{projectName}</span>. Create a new folder or choose an existing one.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={handleClose}
|
|
className="neo-btn neo-btn-ghost p-2"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Folder Browser */}
|
|
<div className="flex-1 overflow-hidden">
|
|
<FolderBrowser
|
|
onSelect={handleFolderSelect}
|
|
onCancel={handleFolderCancel}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="neo-modal-backdrop" onClick={handleClose}>
|
|
<div
|
|
className="neo-modal w-full max-w-lg"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-4 border-b-3 border-[var(--color-neo-border)]">
|
|
<h2 className="font-display font-bold text-xl text-[var(--color-neo-text)]">
|
|
{step === 'name' && 'Create New Project'}
|
|
{step === 'method' && 'Choose Setup Method'}
|
|
{step === 'complete' && 'Project Created!'}
|
|
</h2>
|
|
<button
|
|
onClick={handleClose}
|
|
className="neo-btn neo-btn-ghost p-2"
|
|
>
|
|
<X size={20} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="p-6">
|
|
{/* Step 1: Project Name */}
|
|
{step === 'name' && (
|
|
<form onSubmit={handleNameSubmit}>
|
|
<div className="mb-6">
|
|
<label className="block font-bold mb-2 text-[var(--color-neo-text)]">
|
|
Project Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={projectName}
|
|
onChange={(e) => setProjectName(e.target.value)}
|
|
placeholder="my-awesome-app"
|
|
className="neo-input"
|
|
pattern="^[a-zA-Z0-9_-]+$"
|
|
autoFocus
|
|
/>
|
|
<p className="text-sm text-[var(--color-neo-text-secondary)] mt-2">
|
|
Use letters, numbers, hyphens, and underscores only.
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mb-4 p-3 bg-[var(--color-neo-error-bg)] text-[var(--color-neo-error-text)] text-sm border-3 border-[var(--color-neo-error-border)]">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-end">
|
|
<button
|
|
type="submit"
|
|
className="neo-btn neo-btn-primary"
|
|
disabled={!projectName.trim()}
|
|
>
|
|
Next
|
|
<ArrowRight size={16} />
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
|
|
{/* Step 2: Spec Method */}
|
|
{step === 'method' && (
|
|
<div>
|
|
<p className="text-[var(--color-neo-text-secondary)] mb-6">
|
|
How would you like to define your project?
|
|
</p>
|
|
|
|
<div className="space-y-4">
|
|
{/* Claude option */}
|
|
<button
|
|
onClick={() => handleMethodSelect('claude')}
|
|
disabled={createProject.isPending}
|
|
className="
|
|
w-full text-left p-4
|
|
hover:translate-x-[-2px] hover:translate-y-[-2px]
|
|
transition-all duration-150
|
|
disabled:opacity-50 disabled:cursor-not-allowed
|
|
neo-card
|
|
"
|
|
>
|
|
<div className="flex items-start gap-4">
|
|
<div
|
|
className="p-2 bg-[var(--color-neo-progress)] border-2 border-[var(--color-neo-border)]"
|
|
style={{ boxShadow: 'var(--shadow-neo-sm)' }}
|
|
>
|
|
<Bot size={24} className="text-[var(--color-neo-text-on-bright)]" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-bold text-lg text-[var(--color-neo-text)]">Create with Claude</span>
|
|
<span className="neo-badge bg-[var(--color-neo-done)] text-[var(--color-neo-text-on-bright)] text-xs">
|
|
Recommended
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-[var(--color-neo-text-secondary)] mt-1">
|
|
Interactive conversation to define features and generate your app specification automatically.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{/* Manual option */}
|
|
<button
|
|
onClick={() => handleMethodSelect('manual')}
|
|
disabled={createProject.isPending}
|
|
className="
|
|
w-full text-left p-4
|
|
hover:translate-x-[-2px] hover:translate-y-[-2px]
|
|
transition-all duration-150
|
|
disabled:opacity-50 disabled:cursor-not-allowed
|
|
neo-card
|
|
"
|
|
>
|
|
<div className="flex items-start gap-4">
|
|
<div
|
|
className="p-2 bg-[var(--color-neo-pending)] border-2 border-[var(--color-neo-border)]"
|
|
style={{ boxShadow: 'var(--shadow-neo-sm)' }}
|
|
>
|
|
<FileEdit size={24} className="text-[var(--color-neo-text-on-bright)]" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<span className="font-bold text-lg text-[var(--color-neo-text)]">Edit Templates Manually</span>
|
|
<p className="text-sm text-[var(--color-neo-text-secondary)] mt-1">
|
|
Edit the template files directly. Best for developers who want full control.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="mt-4 p-3 bg-[var(--color-neo-error-bg)] text-[var(--color-neo-error-text)] text-sm border-3 border-[var(--color-neo-error-border)]">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{createProject.isPending && (
|
|
<div className="mt-4 flex items-center justify-center gap-2 text-[var(--color-neo-text-secondary)]">
|
|
<Loader2 size={16} className="animate-spin" />
|
|
<span>Creating project...</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-start mt-6">
|
|
<button
|
|
onClick={handleBack}
|
|
className="neo-btn neo-btn-ghost"
|
|
disabled={createProject.isPending}
|
|
>
|
|
<ArrowLeft size={16} />
|
|
Back
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 3: Complete */}
|
|
{step === 'complete' && (
|
|
<div className="text-center py-8">
|
|
<div
|
|
className="inline-flex items-center justify-center w-16 h-16 bg-[var(--color-neo-done)] border-3 border-[var(--color-neo-border)] mb-4"
|
|
style={{ boxShadow: 'var(--shadow-neo-md)' }}
|
|
>
|
|
<CheckCircle2 size={32} className="text-[var(--color-neo-text-on-bright)]" />
|
|
</div>
|
|
<h3 className="font-display font-bold text-xl mb-2">
|
|
{projectName}
|
|
</h3>
|
|
<p className="text-[var(--color-neo-text-secondary)]">
|
|
Your project has been created successfully!
|
|
</p>
|
|
<div className="mt-4 flex items-center justify-center gap-2">
|
|
<Loader2 size={16} className="animate-spin" />
|
|
<span className="text-sm">Redirecting...</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|