mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-31 14:43:35 +00:00
feat: add concurrent agents with dependency system and delightful UI
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>
This commit is contained in:
133
ui/src/App.tsx
133
ui/src/App.tsx
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useQueryClient, useQuery } from '@tanstack/react-query'
|
||||
import { useProjects, useFeatures, useAgentStatus, useSettings } from './hooks/useProjects'
|
||||
import { useProjectWebSocket } from './hooks/useWebSocket'
|
||||
import { useFeatureSound } from './hooks/useFeatureSound'
|
||||
@@ -13,16 +13,23 @@ import { AddFeatureForm } from './components/AddFeatureForm'
|
||||
import { FeatureModal } from './components/FeatureModal'
|
||||
import { DebugLogViewer, type TabType } from './components/DebugLogViewer'
|
||||
import { AgentThought } from './components/AgentThought'
|
||||
import { AgentMissionControl } from './components/AgentMissionControl'
|
||||
import { CelebrationOverlay } from './components/CelebrationOverlay'
|
||||
import { AssistantFAB } from './components/AssistantFAB'
|
||||
import { AssistantPanel } from './components/AssistantPanel'
|
||||
import { ExpandProjectModal } from './components/ExpandProjectModal'
|
||||
import { SettingsModal } from './components/SettingsModal'
|
||||
import { DevServerControl } from './components/DevServerControl'
|
||||
import { ViewToggle, type ViewMode } from './components/ViewToggle'
|
||||
import { DependencyGraph } from './components/DependencyGraph'
|
||||
import { KeyboardShortcutsHelp } from './components/KeyboardShortcutsHelp'
|
||||
import { getDependencyGraph } from './lib/api'
|
||||
import { Loader2, Settings, Moon, Sun } from 'lucide-react'
|
||||
import type { Feature } from './lib/types'
|
||||
|
||||
const STORAGE_KEY = 'autocoder-selected-project'
|
||||
const DARK_MODE_KEY = 'autocoder-dark-mode'
|
||||
const VIEW_MODE_KEY = 'autocoder-view-mode'
|
||||
|
||||
function App() {
|
||||
// Initialize selected project from localStorage
|
||||
@@ -42,6 +49,7 @@ function App() {
|
||||
const [debugActiveTab, setDebugActiveTab] = useState<TabType>('agent')
|
||||
const [assistantOpen, setAssistantOpen] = useState(false)
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false)
|
||||
const [isSpecCreating, setIsSpecCreating] = useState(false)
|
||||
const [darkMode, setDarkMode] = useState(() => {
|
||||
try {
|
||||
@@ -50,6 +58,14 @@ function App() {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const [viewMode, setViewMode] = useState<ViewMode>(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem(VIEW_MODE_KEY)
|
||||
return (stored === 'graph' ? 'graph' : 'kanban') as ViewMode
|
||||
} catch {
|
||||
return 'kanban'
|
||||
}
|
||||
})
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const { data: projects, isLoading: projectsLoading } = useProjects()
|
||||
@@ -58,6 +74,14 @@ function App() {
|
||||
useAgentStatus(selectedProject) // Keep polling for status updates
|
||||
const wsState = useProjectWebSocket(selectedProject)
|
||||
|
||||
// Fetch graph data when in graph view
|
||||
const { data: graphData } = useQuery({
|
||||
queryKey: ['dependencyGraph', selectedProject],
|
||||
queryFn: () => getDependencyGraph(selectedProject!),
|
||||
enabled: !!selectedProject && viewMode === 'graph',
|
||||
refetchInterval: 5000, // Refresh every 5 seconds
|
||||
})
|
||||
|
||||
// Apply dark mode class to document
|
||||
useEffect(() => {
|
||||
if (darkMode) {
|
||||
@@ -72,6 +96,15 @@ function App() {
|
||||
}
|
||||
}, [darkMode])
|
||||
|
||||
// Persist view mode to localStorage
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(VIEW_MODE_KEY, viewMode)
|
||||
} catch {
|
||||
// localStorage not available
|
||||
}
|
||||
}, [viewMode])
|
||||
|
||||
// Play sounds when features move between columns
|
||||
useFeatureSound(features)
|
||||
|
||||
@@ -154,9 +187,23 @@ function App() {
|
||||
setShowSettings(true)
|
||||
}
|
||||
|
||||
// G : Toggle between Kanban and Graph view (when project selected)
|
||||
if ((e.key === 'g' || e.key === 'G') && selectedProject) {
|
||||
e.preventDefault()
|
||||
setViewMode(prev => prev === 'kanban' ? 'graph' : 'kanban')
|
||||
}
|
||||
|
||||
// ? : Show keyboard shortcuts help
|
||||
if (e.key === '?') {
|
||||
e.preventDefault()
|
||||
setShowKeyboardHelp(true)
|
||||
}
|
||||
|
||||
// Escape : Close modals
|
||||
if (e.key === 'Escape') {
|
||||
if (showExpandProject) {
|
||||
if (showKeyboardHelp) {
|
||||
setShowKeyboardHelp(false)
|
||||
} else if (showExpandProject) {
|
||||
setShowExpandProject(false)
|
||||
} else if (showSettings) {
|
||||
setShowSettings(false)
|
||||
@@ -174,7 +221,7 @@ function App() {
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [selectedProject, showAddFeature, showExpandProject, selectedFeature, debugOpen, debugActiveTab, assistantOpen, features, showSettings, isSpecCreating])
|
||||
}, [selectedProject, showAddFeature, showExpandProject, selectedFeature, debugOpen, debugActiveTab, assistantOpen, features, showSettings, showKeyboardHelp, isSpecCreating, viewMode])
|
||||
|
||||
// Combine WebSocket progress with feature data
|
||||
const progress = wsState.progress.total > 0 ? wsState.progress : {
|
||||
@@ -284,11 +331,21 @@ function App() {
|
||||
isConnected={wsState.isConnected}
|
||||
/>
|
||||
|
||||
{/* Agent Thought - shows latest agent narrative */}
|
||||
<AgentThought
|
||||
logs={wsState.logs}
|
||||
agentStatus={wsState.agentStatus}
|
||||
/>
|
||||
{/* Agent Mission Control - shows active agents in parallel mode */}
|
||||
{wsState.activeAgents.length > 0 && (
|
||||
<AgentMissionControl
|
||||
agents={wsState.activeAgents}
|
||||
recentActivity={wsState.recentActivity}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Agent Thought - shows latest agent narrative (single agent mode) */}
|
||||
{wsState.activeAgents.length === 0 && (
|
||||
<AgentThought
|
||||
logs={wsState.logs}
|
||||
agentStatus={wsState.agentStatus}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Initializing Features State - show when agent is running but no features yet */}
|
||||
{features &&
|
||||
@@ -307,13 +364,45 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Kanban Board */}
|
||||
<KanbanBoard
|
||||
features={features}
|
||||
onFeatureClick={setSelectedFeature}
|
||||
onAddFeature={() => setShowAddFeature(true)}
|
||||
onExpandProject={() => setShowExpandProject(true)}
|
||||
/>
|
||||
{/* View Toggle - only show when there are features */}
|
||||
{features && (features.pending.length + features.in_progress.length + features.done.length) > 0 && (
|
||||
<div className="flex justify-center">
|
||||
<ViewToggle viewMode={viewMode} onViewModeChange={setViewMode} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Kanban Board or Dependency Graph based on view mode */}
|
||||
{viewMode === 'kanban' ? (
|
||||
<KanbanBoard
|
||||
features={features}
|
||||
onFeatureClick={setSelectedFeature}
|
||||
onAddFeature={() => setShowAddFeature(true)}
|
||||
onExpandProject={() => setShowExpandProject(true)}
|
||||
activeAgents={wsState.activeAgents}
|
||||
/>
|
||||
) : (
|
||||
<div className="neo-card overflow-hidden" style={{ height: '600px' }}>
|
||||
{graphData ? (
|
||||
<DependencyGraph
|
||||
graphData={graphData}
|
||||
onNodeClick={(nodeId) => {
|
||||
// Find the feature and open the modal
|
||||
const allFeatures = [
|
||||
...(features?.pending ?? []),
|
||||
...(features?.in_progress ?? []),
|
||||
...(features?.done ?? [])
|
||||
]
|
||||
const feature = allFeatures.find(f => f.id === nodeId)
|
||||
if (feature) setSelectedFeature(feature)
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<Loader2 size={32} className="animate-spin text-neo-progress" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
@@ -383,6 +472,20 @@ function App() {
|
||||
{showSettings && (
|
||||
<SettingsModal onClose={() => setShowSettings(false)} />
|
||||
)}
|
||||
|
||||
{/* Keyboard Shortcuts Help */}
|
||||
{showKeyboardHelp && (
|
||||
<KeyboardShortcutsHelp onClose={() => setShowKeyboardHelp(false)} />
|
||||
)}
|
||||
|
||||
{/* Celebration Overlay - shows when a feature is completed by an agent */}
|
||||
{wsState.celebration && (
|
||||
<CelebrationOverlay
|
||||
agentName={wsState.celebration.agentName}
|
||||
featureName={wsState.celebration.featureName}
|
||||
onComplete={wsState.clearCelebration}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user