mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 22:32: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>
161 lines
4.8 KiB
TypeScript
161 lines
4.8 KiB
TypeScript
import { useMemo, useState, useEffect } from 'react'
|
|
import { Brain, Sparkles } from 'lucide-react'
|
|
import type { AgentStatus } from '../lib/types'
|
|
|
|
interface AgentThoughtProps {
|
|
logs: Array<{ line: string; timestamp: string }>
|
|
agentStatus: AgentStatus
|
|
}
|
|
|
|
const IDLE_TIMEOUT = 30000 // 30 seconds
|
|
|
|
/**
|
|
* Determines if a log line is an agent "thought" (narrative text)
|
|
* vs. tool mechanics that should be hidden
|
|
*/
|
|
function isAgentThought(line: string): boolean {
|
|
const trimmed = line.trim()
|
|
|
|
// Skip tool mechanics
|
|
if (/^\[Tool:/.test(trimmed)) return false
|
|
if (/^\s*Input:\s*\{/.test(trimmed)) return false
|
|
if (/^\[(Done|Error)\]/.test(trimmed)) return false
|
|
if (/^\[Error\]/.test(trimmed)) return false
|
|
if (/^Output:/.test(trimmed)) return false
|
|
|
|
// Skip JSON and very short lines
|
|
if (/^[[{]/.test(trimmed)) return false
|
|
if (trimmed.length < 10) return false
|
|
|
|
// Skip lines that are just paths or technical output
|
|
if (/^[A-Za-z]:\\/.test(trimmed)) return false
|
|
if (/^\/[a-z]/.test(trimmed)) return false
|
|
|
|
// Keep narrative text (looks like a sentence, relaxed filter)
|
|
return trimmed.length > 10
|
|
}
|
|
|
|
/**
|
|
* Extracts the latest agent thought from logs
|
|
*/
|
|
function getLatestThought(logs: Array<{ line: string; timestamp: string }>): string | null {
|
|
// Search from most recent
|
|
for (let i = logs.length - 1; i >= 0; i--) {
|
|
if (isAgentThought(logs[i].line)) {
|
|
return logs[i].line.trim()
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
export function AgentThought({ logs, agentStatus }: AgentThoughtProps) {
|
|
const thought = useMemo(() => getLatestThought(logs), [logs])
|
|
const [displayedThought, setDisplayedThought] = useState<string | null>(null)
|
|
const [textVisible, setTextVisible] = useState(true)
|
|
const [isVisible, setIsVisible] = useState(false)
|
|
|
|
// Get last log timestamp for idle detection
|
|
const lastLogTimestamp = logs.length > 0
|
|
? new Date(logs[logs.length - 1].timestamp).getTime()
|
|
: 0
|
|
|
|
// Determine if component should be visible
|
|
const shouldShow = useMemo(() => {
|
|
if (!thought) return false
|
|
if (agentStatus === 'running') return true
|
|
if (agentStatus === 'paused') {
|
|
return Date.now() - lastLogTimestamp < IDLE_TIMEOUT
|
|
}
|
|
return false
|
|
}, [thought, agentStatus, lastLogTimestamp])
|
|
|
|
// Animate text changes using CSS transitions
|
|
useEffect(() => {
|
|
if (thought !== displayedThought && thought) {
|
|
// Fade out
|
|
setTextVisible(false)
|
|
// After fade out, update text and fade in
|
|
const timeout = setTimeout(() => {
|
|
setDisplayedThought(thought)
|
|
setTextVisible(true)
|
|
}, 150) // Match transition duration
|
|
return () => clearTimeout(timeout)
|
|
}
|
|
}, [thought, displayedThought])
|
|
|
|
// Handle visibility transitions
|
|
useEffect(() => {
|
|
if (shouldShow) {
|
|
setIsVisible(true)
|
|
} else {
|
|
// Delay hiding to allow exit animation
|
|
const timeout = setTimeout(() => setIsVisible(false), 300)
|
|
return () => clearTimeout(timeout)
|
|
}
|
|
}, [shouldShow])
|
|
|
|
if (!isVisible || !displayedThought) return null
|
|
|
|
const isRunning = agentStatus === 'running'
|
|
|
|
return (
|
|
<div
|
|
className={`
|
|
transition-all duration-300 ease-out overflow-hidden
|
|
${shouldShow ? 'opacity-100 max-h-20' : 'opacity-0 max-h-0'}
|
|
`}
|
|
>
|
|
<div
|
|
className={`
|
|
relative
|
|
bg-[var(--color-neo-card)]
|
|
border-3 border-[var(--color-neo-border)]
|
|
shadow-[var(--shadow-neo-sm)]
|
|
px-4 py-3
|
|
flex items-center gap-3
|
|
${isRunning ? 'animate-pulse-neo' : ''}
|
|
`}
|
|
>
|
|
{/* Brain Icon with subtle glow */}
|
|
<div className="relative shrink-0">
|
|
<Brain
|
|
size={22}
|
|
className="text-[var(--color-neo-progress)]"
|
|
strokeWidth={2.5}
|
|
/>
|
|
{isRunning && (
|
|
<Sparkles
|
|
size={10}
|
|
className="absolute -top-1 -right-1 text-[var(--color-neo-pending)] animate-pulse"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Thought text with fade transition + shimmer effect when running */}
|
|
<p
|
|
className={`
|
|
font-mono text-sm truncate transition-all duration-150 ease-out
|
|
${isRunning ? 'animate-shimmer' : 'text-[var(--color-neo-text)]'}
|
|
`}
|
|
style={{
|
|
opacity: textVisible ? 1 : 0,
|
|
transform: textVisible ? 'translateY(0)' : 'translateY(-4px)',
|
|
}}
|
|
>
|
|
{displayedThought?.replace(/:$/, '')}
|
|
</p>
|
|
|
|
{/* Subtle running indicator bar */}
|
|
{isRunning && (
|
|
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-[var(--color-neo-progress)] opacity-50">
|
|
<div
|
|
className="h-full bg-[var(--color-neo-progress)] animate-pulse"
|
|
style={{ width: '100%' }}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|