diff --git a/server/websocket.py b/server/websocket.py
index 18680ac..635094b 100644
--- a/server/websocket.py
+++ b/server/websocket.py
@@ -54,6 +54,21 @@ THOUGHT_PATTERNS = [
(re.compile(r'(?:FAIL|failed|error)', re.I), 'struggling'),
]
+# Orchestrator event patterns for Mission Control observability
+ORCHESTRATOR_PATTERNS = {
+ 'init_start': re.compile(r'Running initializer agent'),
+ 'init_complete': re.compile(r'INITIALIZATION COMPLETE'),
+ 'capacity_check': re.compile(r'\[DEBUG\] Spawning loop: (\d+) ready, (\d+) slots'),
+ 'at_capacity': re.compile(r'At max capacity|at max testing agents|At max total agents'),
+ 'feature_start': re.compile(r'Starting feature \d+/\d+: #(\d+) - (.+)'),
+ 'coding_spawn': re.compile(r'Started coding agent for feature #(\d+)'),
+ 'testing_spawn': re.compile(r'Started testing agent for feature #(\d+)'),
+ 'coding_complete': re.compile(r'Feature #(\d+) (completed|failed)'),
+ 'testing_complete': re.compile(r'Feature #(\d+) testing (completed|failed)'),
+ 'all_complete': re.compile(r'All features complete'),
+ 'blocked_features': re.compile(r'(\d+) blocked by dependencies'),
+}
+
class AgentTracker:
"""Tracks active agents and their states for multi-agent mode.
@@ -250,6 +265,194 @@ class AgentTracker:
return result
+class OrchestratorTracker:
+ """Tracks orchestrator state for Mission Control observability.
+
+ Parses orchestrator stdout for key events and emits orchestrator_update
+ WebSocket messages showing what decisions the orchestrator is making.
+ """
+
+ def __init__(self):
+ self.state = 'idle'
+ self.coding_agents = 0
+ self.testing_agents = 0
+ self.max_concurrency = 3 # Default, will be updated from output
+ self.ready_count = 0
+ self.blocked_count = 0
+ self.recent_events: list[dict] = []
+ self._lock = asyncio.Lock()
+
+ async def process_line(self, line: str) -> dict | None:
+ """
+ Process an output line and return an orchestrator_update message if relevant.
+
+ Returns None if no update should be emitted.
+ """
+ async with self._lock:
+ update = None
+
+ # Check for initializer start
+ if ORCHESTRATOR_PATTERNS['init_start'].search(line):
+ self.state = 'initializing'
+ update = self._create_update(
+ 'init_start',
+ 'Initializing project features...'
+ )
+
+ # Check for initializer complete
+ elif ORCHESTRATOR_PATTERNS['init_complete'].search(line):
+ self.state = 'scheduling'
+ update = self._create_update(
+ 'init_complete',
+ 'Initialization complete, preparing to schedule features'
+ )
+
+ # Check for capacity status
+ elif match := ORCHESTRATOR_PATTERNS['capacity_check'].search(line):
+ self.ready_count = int(match.group(1))
+ slots = int(match.group(2))
+ self.state = 'scheduling' if self.ready_count > 0 else 'monitoring'
+ update = self._create_update(
+ 'capacity_check',
+ f'{self.ready_count} features ready, {slots} slots available'
+ )
+
+ # Check for at capacity
+ elif ORCHESTRATOR_PATTERNS['at_capacity'].search(line):
+ self.state = 'monitoring'
+ update = self._create_update(
+ 'at_capacity',
+ 'At maximum capacity, monitoring active agents'
+ )
+
+ # Check for feature start
+ elif match := ORCHESTRATOR_PATTERNS['feature_start'].search(line):
+ feature_id = int(match.group(1))
+ feature_name = match.group(2).strip()
+ self.state = 'spawning'
+ update = self._create_update(
+ 'feature_start',
+ f'Preparing Feature #{feature_id}: {feature_name}',
+ feature_id=feature_id,
+ feature_name=feature_name
+ )
+
+ # Check for coding agent spawn
+ elif match := ORCHESTRATOR_PATTERNS['coding_spawn'].search(line):
+ feature_id = int(match.group(1))
+ self.coding_agents += 1
+ self.state = 'spawning'
+ update = self._create_update(
+ 'coding_spawn',
+ f'Spawned coding agent for Feature #{feature_id}',
+ feature_id=feature_id
+ )
+
+ # Check for testing agent spawn
+ elif match := ORCHESTRATOR_PATTERNS['testing_spawn'].search(line):
+ feature_id = int(match.group(1))
+ self.testing_agents += 1
+ self.state = 'spawning'
+ update = self._create_update(
+ 'testing_spawn',
+ f'Spawned testing agent for Feature #{feature_id}',
+ feature_id=feature_id
+ )
+
+ # Check for coding agent complete
+ elif match := ORCHESTRATOR_PATTERNS['coding_complete'].search(line):
+ # Only match if "testing" is not in the line
+ if 'testing' not in line.lower():
+ feature_id = int(match.group(1))
+ self.coding_agents = max(0, self.coding_agents - 1)
+ self.state = 'monitoring'
+ update = self._create_update(
+ 'coding_complete',
+ f'Coding agent finished Feature #{feature_id}',
+ feature_id=feature_id
+ )
+
+ # Check for testing agent complete
+ elif match := ORCHESTRATOR_PATTERNS['testing_complete'].search(line):
+ feature_id = int(match.group(1))
+ self.testing_agents = max(0, self.testing_agents - 1)
+ self.state = 'monitoring'
+ update = self._create_update(
+ 'testing_complete',
+ f'Testing agent finished Feature #{feature_id}',
+ feature_id=feature_id
+ )
+
+ # Check for blocked features count
+ elif match := ORCHESTRATOR_PATTERNS['blocked_features'].search(line):
+ self.blocked_count = int(match.group(1))
+
+ # Check for all complete
+ elif ORCHESTRATOR_PATTERNS['all_complete'].search(line):
+ self.state = 'complete'
+ self.coding_agents = 0
+ self.testing_agents = 0
+ update = self._create_update(
+ 'all_complete',
+ 'All features complete!'
+ )
+
+ return update
+
+ def _create_update(
+ self,
+ event_type: str,
+ message: str,
+ feature_id: int | None = None,
+ feature_name: str | None = None
+ ) -> dict:
+ """Create an orchestrator_update WebSocket message."""
+ timestamp = datetime.now().isoformat()
+
+ # Add to recent events (keep last 5)
+ event = {
+ 'eventType': event_type,
+ 'message': message,
+ 'timestamp': timestamp,
+ }
+ if feature_id is not None:
+ event['featureId'] = feature_id
+ if feature_name is not None:
+ event['featureName'] = feature_name
+
+ self.recent_events = [event] + self.recent_events[:4]
+
+ update = {
+ 'type': 'orchestrator_update',
+ 'eventType': event_type,
+ 'state': self.state,
+ 'message': message,
+ 'timestamp': timestamp,
+ 'codingAgents': self.coding_agents,
+ 'testingAgents': self.testing_agents,
+ 'maxConcurrency': self.max_concurrency,
+ 'readyCount': self.ready_count,
+ 'blockedCount': self.blocked_count,
+ }
+
+ if feature_id is not None:
+ update['featureId'] = feature_id
+ if feature_name is not None:
+ update['featureName'] = feature_name
+
+ return update
+
+ async def reset(self):
+ """Reset tracker state when orchestrator stops or crashes."""
+ async with self._lock:
+ self.state = 'idle'
+ self.coding_agents = 0
+ self.testing_agents = 0
+ self.ready_count = 0
+ self.blocked_count = 0
+ self.recent_events.clear()
+
+
def _get_project_path(project_name: str) -> Path:
"""Get project path from registry."""
import sys
@@ -400,6 +603,9 @@ async def project_websocket(websocket: WebSocket, project_name: str):
# Create agent tracker for multi-agent mode
agent_tracker = AgentTracker()
+ # Create orchestrator tracker for observability
+ orchestrator_tracker = OrchestratorTracker()
+
async def on_output(line: str):
"""Handle agent output - broadcast to this WebSocket."""
try:
@@ -429,6 +635,11 @@ async def project_websocket(websocket: WebSocket, project_name: str):
agent_update = await agent_tracker.process_line(line)
if agent_update:
await websocket.send_json(agent_update)
+
+ # Also check for orchestrator events and emit orchestrator_update messages
+ orch_update = await orchestrator_tracker.process_line(line)
+ if orch_update:
+ await websocket.send_json(orch_update)
except Exception:
pass # Connection may be closed
@@ -439,9 +650,10 @@ async def project_websocket(websocket: WebSocket, project_name: str):
"type": "agent_status",
"status": status,
})
- # Reset tracker when agent stops OR crashes to prevent ghost agents on restart
+ # Reset trackers when agent stops OR crashes to prevent ghost agents on restart
if status in ("stopped", "crashed"):
await agent_tracker.reset()
+ await orchestrator_tracker.reset()
except Exception:
pass # Connection may be closed
diff --git a/ui/src/App.tsx b/ui/src/App.tsx
index ef46cc9..59ed0ab 100644
--- a/ui/src/App.tsx
+++ b/ui/src/App.tsx
@@ -348,14 +348,13 @@ function App() {
isConnected={wsState.isConnected}
/>
- {/* Agent Mission Control - shows active agents in parallel mode */}
- {wsState.activeAgents.length > 0 && (
-
- )}
+ {/* Agent Mission Control - shows orchestrator status and active agents in parallel mode */}
+
{/* Agent Thought - shows latest agent narrative (single agent mode) */}
{wsState.activeAgents.length === 0 && (
diff --git a/ui/src/components/AgentMissionControl.tsx b/ui/src/components/AgentMissionControl.tsx
index c4ed1b8..9fae801 100644
--- a/ui/src/components/AgentMissionControl.tsx
+++ b/ui/src/components/AgentMissionControl.tsx
@@ -2,12 +2,14 @@ import { Rocket, ChevronDown, ChevronUp, Activity } from 'lucide-react'
import { useState } from 'react'
import { AgentCard, AgentLogModal } from './AgentCard'
import { ActivityFeed } from './ActivityFeed'
-import type { ActiveAgent, AgentLogEntry } from '../lib/types'
+import { OrchestratorStatusCard } from './OrchestratorStatusCard'
+import type { ActiveAgent, AgentLogEntry, OrchestratorStatus } from '../lib/types'
const ACTIVITY_COLLAPSED_KEY = 'autocoder-activity-collapsed'
interface AgentMissionControlProps {
agents: ActiveAgent[]
+ orchestratorStatus: OrchestratorStatus | null
recentActivity: Array<{
agentName: string
thought: string
@@ -20,6 +22,7 @@ interface AgentMissionControlProps {
export function AgentMissionControl({
agents,
+ orchestratorStatus,
recentActivity,
isExpanded: defaultExpanded = true,
getAgentLogs,
@@ -45,8 +48,8 @@ export function AgentMissionControl({
}
}
- // Don't render if no agents
- if (agents.length === 0) {
+ // Don't render if no orchestrator status and no agents
+ if (!orchestratorStatus && agents.length === 0) {
return null
}
@@ -63,7 +66,14 @@ export function AgentMissionControl({
Mission Control
- {agents.length} {agents.length === 1 ? 'agent' : 'agents'} active
+ {agents.length > 0
+ ? `${agents.length} ${agents.length === 1 ? 'agent' : 'agents'} active`
+ : orchestratorStatus?.state === 'initializing'
+ ? 'Initializing'
+ : orchestratorStatus?.state === 'complete'
+ ? 'Complete'
+ : 'Orchestrating'
+ }
{isExpanded ? (
@@ -77,25 +87,32 @@ export function AgentMissionControl({
+ {/* Orchestrator Status Card */}
+ {orchestratorStatus && (
+
+ )}
+
{/* Agent Cards Row */}
-
- {agents.map((agent) => (
-
{
- const agentToShow = agents.find(a => a.agentIndex === agentIndex)
- if (agentToShow) {
- setSelectedAgentForLogs(agentToShow)
- }
- }}
- />
- ))}
-
+ {agents.length > 0 && (
+
+ {agents.map((agent) => (
+
{
+ const agentToShow = agents.find(a => a.agentIndex === agentIndex)
+ if (agentToShow) {
+ setSelectedAgentForLogs(agentToShow)
+ }
+ }}
+ />
+ ))}
+
+ )}
{/* Collapsible Activity Feed */}
{recentActivity.length > 0 && (
diff --git a/ui/src/components/OrchestratorAvatar.tsx b/ui/src/components/OrchestratorAvatar.tsx
new file mode 100644
index 0000000..bbf3dab
--- /dev/null
+++ b/ui/src/components/OrchestratorAvatar.tsx
@@ -0,0 +1,178 @@
+import type { OrchestratorState } from '../lib/types'
+
+interface OrchestratorAvatarProps {
+ state: OrchestratorState
+ size?: 'sm' | 'md' | 'lg'
+}
+
+const SIZES = {
+ sm: { svg: 32, font: 'text-xs' },
+ md: { svg: 48, font: 'text-sm' },
+ lg: { svg: 64, font: 'text-base' },
+}
+
+// Maestro color scheme - Deep violet
+const MAESTRO_COLORS = {
+ primary: '#7C3AED', // Violet-600
+ secondary: '#A78BFA', // Violet-400
+ accent: '#EDE9FE', // Violet-100
+ baton: '#FBBF24', // Amber-400 for the baton
+ gold: '#F59E0B', // Amber-500 for accents
+}
+
+// Maestro SVG - Robot conductor with baton
+function MaestroSVG({ size, state }: { size: number; state: OrchestratorState }) {
+ // Animation transform based on state
+ const batonAnimation = state === 'spawning' ? 'animate-conducting' :
+ state === 'scheduling' ? 'animate-baton-tap' : ''
+
+ return (
+
+ )
+}
+
+// Animation classes based on orchestrator state
+function getStateAnimation(state: OrchestratorState): string {
+ switch (state) {
+ case 'idle':
+ return 'animate-bounce-gentle'
+ case 'initializing':
+ return 'animate-thinking'
+ case 'scheduling':
+ return 'animate-thinking'
+ case 'spawning':
+ return 'animate-working'
+ case 'monitoring':
+ return 'animate-bounce-gentle'
+ case 'complete':
+ return 'animate-celebrate'
+ default:
+ return ''
+ }
+}
+
+// Glow effect based on state
+function getStateGlow(state: OrchestratorState): string {
+ switch (state) {
+ case 'initializing':
+ return 'shadow-[0_0_12px_rgba(124,58,237,0.4)]'
+ case 'scheduling':
+ return 'shadow-[0_0_10px_rgba(167,139,250,0.5)]'
+ case 'spawning':
+ return 'shadow-[0_0_16px_rgba(124,58,237,0.6)]'
+ case 'monitoring':
+ return 'shadow-[0_0_8px_rgba(167,139,250,0.4)]'
+ case 'complete':
+ return 'shadow-[0_0_20px_rgba(112,224,0,0.6)]'
+ default:
+ return ''
+ }
+}
+
+// Get human-readable state description for accessibility
+function getStateDescription(state: OrchestratorState): string {
+ switch (state) {
+ case 'idle':
+ return 'waiting'
+ case 'initializing':
+ return 'initializing features'
+ case 'scheduling':
+ return 'selecting next features'
+ case 'spawning':
+ return 'spawning agents'
+ case 'monitoring':
+ return 'monitoring progress'
+ case 'complete':
+ return 'all features complete'
+ default:
+ return state
+ }
+}
+
+export function OrchestratorAvatar({ state, size = 'md' }: OrchestratorAvatarProps) {
+ const { svg: svgSize } = SIZES[size]
+ const stateDesc = getStateDescription(state)
+ const ariaLabel = `Orchestrator Maestro is ${stateDesc}`
+
+ return (
+
+ )
+}
diff --git a/ui/src/components/OrchestratorStatusCard.tsx b/ui/src/components/OrchestratorStatusCard.tsx
new file mode 100644
index 0000000..90db078
--- /dev/null
+++ b/ui/src/components/OrchestratorStatusCard.tsx
@@ -0,0 +1,152 @@
+import { useState } from 'react'
+import { ChevronDown, ChevronUp, Code, FlaskConical, Clock, Lock, Sparkles } from 'lucide-react'
+import { OrchestratorAvatar } from './OrchestratorAvatar'
+import type { OrchestratorStatus, OrchestratorState } from '../lib/types'
+
+interface OrchestratorStatusCardProps {
+ status: OrchestratorStatus
+}
+
+// Get a friendly state description
+function getStateText(state: OrchestratorState): string {
+ switch (state) {
+ case 'idle':
+ return 'Standing by...'
+ case 'initializing':
+ return 'Setting up features...'
+ case 'scheduling':
+ return 'Planning next moves...'
+ case 'spawning':
+ return 'Deploying agents...'
+ case 'monitoring':
+ return 'Watching progress...'
+ case 'complete':
+ return 'Mission accomplished!'
+ default:
+ return 'Orchestrating...'
+ }
+}
+
+// Get state color
+function getStateColor(state: OrchestratorState): string {
+ switch (state) {
+ case 'complete':
+ return 'text-neo-done'
+ case 'spawning':
+ return 'text-[#7C3AED]' // Violet
+ case 'scheduling':
+ case 'monitoring':
+ return 'text-neo-progress'
+ case 'initializing':
+ return 'text-neo-pending'
+ default:
+ return 'text-neo-text-secondary'
+ }
+}
+
+// Format timestamp to relative time
+function formatRelativeTime(timestamp: string): string {
+ const now = new Date()
+ const then = new Date(timestamp)
+ const diffMs = now.getTime() - then.getTime()
+ const diffSecs = Math.floor(diffMs / 1000)
+
+ if (diffSecs < 5) return 'just now'
+ if (diffSecs < 60) return `${diffSecs}s ago`
+ const diffMins = Math.floor(diffSecs / 60)
+ if (diffMins < 60) return `${diffMins}m ago`
+ return `${Math.floor(diffMins / 60)}h ago`
+}
+
+export function OrchestratorStatusCard({ status }: OrchestratorStatusCardProps) {
+ const [showEvents, setShowEvents] = useState(false)
+
+ return (
+
+
+ {/* Avatar */}
+
+
+ {/* Main content */}
+
+ {/* Header row */}
+
+
+ Maestro
+
+
+ {getStateText(status.state)}
+
+
+
+ {/* Current message */}
+
+ {status.message}
+
+
+ {/* Status badges row */}
+
+ {/* Coding agents badge */}
+
+
+ Coding: {status.codingAgents}
+
+
+ {/* Testing agents badge */}
+
+
+ Testing: {status.testingAgents}
+
+
+ {/* Ready queue badge */}
+
+
+ Ready: {status.readyCount}
+
+
+ {/* Blocked badge (only show if > 0) */}
+ {status.blockedCount > 0 && (
+
+
+ Blocked: {status.blockedCount}
+
+ )}
+
+
+
+ {/* Recent events toggle */}
+ {status.recentEvents.length > 0 && (
+
+ )}
+
+
+ {/* Collapsible recent events */}
+ {showEvents && status.recentEvents.length > 0 && (
+
+
+ {status.recentEvents.map((event, idx) => (
+
+
+ {formatRelativeTime(event.timestamp)}
+
+
+ {event.message}
+
+
+ ))}
+
+
+ )}
+
+ )
+}
diff --git a/ui/src/hooks/useWebSocket.ts b/ui/src/hooks/useWebSocket.ts
index 533c2ab..648e365 100644
--- a/ui/src/hooks/useWebSocket.ts
+++ b/ui/src/hooks/useWebSocket.ts
@@ -10,6 +10,8 @@ import type {
ActiveAgent,
AgentMascot,
AgentLogEntry,
+ OrchestratorStatus,
+ OrchestratorEvent,
} from '../lib/types'
// Activity item for the feed
@@ -48,6 +50,8 @@ interface WebSocketState {
// Celebration queue to handle rapid successes without race conditions
celebrationQueue: CelebrationTrigger[]
celebration: CelebrationTrigger | null
+ // Orchestrator state for Mission Control
+ orchestratorStatus: OrchestratorStatus | null
}
const MAX_LOGS = 100 // Keep last 100 log lines
@@ -68,6 +72,7 @@ export function useProjectWebSocket(projectName: string | null) {
agentLogs: new Map(),
celebrationQueue: [],
celebration: null,
+ orchestratorStatus: null,
})
const wsRef = useRef
(null)
@@ -112,8 +117,12 @@ export function useProjectWebSocket(projectName: string | null) {
setState(prev => ({
...prev,
agentStatus: message.status,
- // Clear active agents when process stops OR crashes to prevent stale UI
- ...((message.status === 'stopped' || message.status === 'crashed') && { activeAgents: [], recentActivity: [] }),
+ // Clear active agents and orchestrator status when process stops OR crashes to prevent stale UI
+ ...((message.status === 'stopped' || message.status === 'crashed') && {
+ activeAgents: [],
+ recentActivity: [],
+ orchestratorStatus: null,
+ }),
}))
break
@@ -261,6 +270,33 @@ export function useProjectWebSocket(projectName: string | null) {
})
break
+ case 'orchestrator_update':
+ setState(prev => {
+ const newEvent: OrchestratorEvent = {
+ eventType: message.eventType,
+ message: message.message,
+ timestamp: message.timestamp,
+ featureId: message.featureId,
+ featureName: message.featureName,
+ }
+
+ return {
+ ...prev,
+ orchestratorStatus: {
+ state: message.state,
+ message: message.message,
+ codingAgents: message.codingAgents ?? prev.orchestratorStatus?.codingAgents ?? 0,
+ testingAgents: message.testingAgents ?? prev.orchestratorStatus?.testingAgents ?? 0,
+ maxConcurrency: message.maxConcurrency ?? prev.orchestratorStatus?.maxConcurrency ?? 3,
+ readyCount: message.readyCount ?? prev.orchestratorStatus?.readyCount ?? 0,
+ blockedCount: message.blockedCount ?? prev.orchestratorStatus?.blockedCount ?? 0,
+ timestamp: message.timestamp,
+ recentEvents: [newEvent, ...(prev.orchestratorStatus?.recentEvents ?? []).slice(0, 4)],
+ },
+ }
+ })
+ break
+
case 'dev_log':
setState(prev => ({
...prev,
@@ -346,6 +382,7 @@ export function useProjectWebSocket(projectName: string | null) {
agentLogs: new Map(),
celebrationQueue: [],
celebration: null,
+ orchestratorStatus: null,
})
if (!projectName) {
diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts
index 26e9c8b..5f8b9c2 100644
--- a/ui/src/lib/types.ts
+++ b/ui/src/lib/types.ts
@@ -206,8 +206,39 @@ export interface ActiveAgent {
logs?: AgentLogEntry[] // Per-agent log history
}
+// Orchestrator state for Mission Control
+export type OrchestratorState =
+ | 'idle'
+ | 'initializing'
+ | 'scheduling'
+ | 'spawning'
+ | 'monitoring'
+ | 'complete'
+
+// Orchestrator event for recent activity
+export interface OrchestratorEvent {
+ eventType: string
+ message: string
+ timestamp: string
+ featureId?: number
+ featureName?: string
+}
+
+// Orchestrator status for Mission Control
+export interface OrchestratorStatus {
+ state: OrchestratorState
+ message: string
+ codingAgents: number
+ testingAgents: number
+ maxConcurrency: number
+ readyCount: number
+ blockedCount: number
+ timestamp: string
+ recentEvents: OrchestratorEvent[]
+}
+
// WebSocket message types
-export type WSMessageType = 'progress' | 'feature_update' | 'log' | 'agent_status' | 'pong' | 'dev_log' | 'dev_server_status' | 'agent_update'
+export type WSMessageType = 'progress' | 'feature_update' | 'log' | 'agent_status' | 'pong' | 'dev_log' | 'dev_server_status' | 'agent_update' | 'orchestrator_update'
export interface WSProgressMessage {
type: 'progress'
@@ -265,6 +296,21 @@ export interface WSDevServerStatusMessage {
url: string | null
}
+export interface WSOrchestratorUpdateMessage {
+ type: 'orchestrator_update'
+ eventType: string
+ state: OrchestratorState
+ message: string
+ timestamp: string
+ codingAgents?: number
+ testingAgents?: number
+ maxConcurrency?: number
+ readyCount?: number
+ blockedCount?: number
+ featureId?: number
+ featureName?: string
+}
+
export type WSMessage =
| WSProgressMessage
| WSFeatureUpdateMessage
@@ -274,6 +320,7 @@ export type WSMessage =
| WSPongMessage
| WSDevLogMessage
| WSDevServerStatusMessage
+ | WSOrchestratorUpdateMessage
// ============================================================================
// Spec Chat Types
diff --git a/ui/src/styles/globals.css b/ui/src/styles/globals.css
index 5c8199a..e5a2e49 100644
--- a/ui/src/styles/globals.css
+++ b/ui/src/styles/globals.css
@@ -960,6 +960,67 @@
}
}
+/* ============================================================================
+ Orchestrator (Maestro) Animations
+ ============================================================================ */
+
+@keyframes conducting {
+ 0%, 100% {
+ transform: rotate(-10deg);
+ }
+ 25% {
+ transform: rotate(5deg);
+ }
+ 50% {
+ transform: rotate(-5deg);
+ }
+ 75% {
+ transform: rotate(10deg);
+ }
+}
+
+@keyframes baton-tap {
+ 0%, 100% {
+ transform: translateY(0) rotate(0deg);
+ }
+ 25% {
+ transform: translateY(-3px) rotate(-2deg);
+ }
+ 50% {
+ transform: translateY(0) rotate(0deg);
+ }
+ 75% {
+ transform: translateY(-3px) rotate(2deg);
+ }
+}
+
+@keyframes maestro-idle {
+ 0%, 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-2px);
+ }
+}
+
+@keyframes maestro-complete {
+ 0% {
+ transform: scale(1) rotate(0deg);
+ }
+ 25% {
+ transform: scale(1.05) rotate(-3deg);
+ }
+ 50% {
+ transform: scale(1.1) rotate(0deg);
+ }
+ 75% {
+ transform: scale(1.05) rotate(3deg);
+ }
+ 100% {
+ transform: scale(1) rotate(0deg);
+ }
+}
+
/* ============================================================================
Utilities Layer
============================================================================ */
@@ -1089,6 +1150,23 @@
.animate-confetti {
animation: confetti 2s ease-out forwards;
}
+
+ /* Orchestrator (Maestro) animation utilities */
+ .animate-conducting {
+ animation: conducting 1s ease-in-out infinite;
+ }
+
+ .animate-baton-tap {
+ animation: baton-tap 0.6s ease-in-out infinite;
+ }
+
+ .animate-maestro-idle {
+ animation: maestro-idle 2s ease-in-out infinite;
+ }
+
+ .animate-maestro-complete {
+ animation: maestro-complete 0.8s ease-in-out;
+ }
}
/* ============================================================================