mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-02-01 06:53:36 +00:00
fix: ensure agents are removed from Mission Control UI on completion
Previously, agents that completed their work would remain visible in the Mission Control UI until a manual page refresh. This occurred because the AgentTracker._handle_agent_complete method silently dropped completion messages when an agent wasn't tracked (e.g., due to missed start messages from WebSocket connection issues). Backend changes: - Modified _handle_agent_complete in server/websocket.py to always emit completion messages, even for untracked agents - Synthetic completions use agentIndex=-1 and agentName='Unknown' as sentinel values to indicate untracked agents Frontend changes: - Updated useWebSocket.ts to handle synthetic completions by removing agents by featureId when agentIndex is -1 - Added 30-minute stale agent cleanup as defense-in-depth for users who leave the UI open for extended periods - Updated TypeScript types to allow 'Unknown' as valid agent name Component updates: - AgentAvatar.tsx: Added UNKNOWN_COLORS and UnknownSVG fallback for rendering unknown agents with a neutral gray question mark icon - CelebrationOverlay.tsx, DependencyGraph.tsx: Updated interfaces to accept 'Unknown' agent names Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -238,31 +238,41 @@ class AgentTracker:
|
|||||||
}
|
}
|
||||||
|
|
||||||
async def _handle_agent_complete(self, feature_id: int, is_success: bool) -> dict | None:
|
async def _handle_agent_complete(self, feature_id: int, is_success: bool) -> dict | None:
|
||||||
"""Handle agent completion message from orchestrator."""
|
"""Handle agent completion - ALWAYS emits a message, even if agent wasn't tracked."""
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
if feature_id not in self.active_agents:
|
|
||||||
return None
|
|
||||||
|
|
||||||
agent = self.active_agents[feature_id]
|
|
||||||
state = 'success' if is_success else 'error'
|
state = 'success' if is_success else 'error'
|
||||||
agent_type = agent.get('agent_type', 'coding')
|
|
||||||
|
|
||||||
result = {
|
if feature_id in self.active_agents:
|
||||||
'type': 'agent_update',
|
# Normal case: agent was tracked
|
||||||
'agentIndex': agent['agent_index'],
|
agent = self.active_agents[feature_id]
|
||||||
'agentName': agent['name'],
|
result = {
|
||||||
'agentType': agent_type,
|
'type': 'agent_update',
|
||||||
'featureId': feature_id,
|
'agentIndex': agent['agent_index'],
|
||||||
'featureName': agent['feature_name'],
|
'agentName': agent['name'],
|
||||||
'state': state,
|
'agentType': agent.get('agent_type', 'coding'),
|
||||||
'thought': 'Completed successfully!' if is_success else 'Failed to complete',
|
'featureId': feature_id,
|
||||||
'timestamp': datetime.now().isoformat(),
|
'featureName': agent['feature_name'],
|
||||||
}
|
'state': state,
|
||||||
|
'thought': 'Completed successfully!' if is_success else 'Failed to complete',
|
||||||
# Remove from active agents
|
'timestamp': datetime.now().isoformat(),
|
||||||
del self.active_agents[feature_id]
|
}
|
||||||
|
del self.active_agents[feature_id]
|
||||||
return result
|
return result
|
||||||
|
else:
|
||||||
|
# Synthetic completion for untracked agent
|
||||||
|
# This ensures UI always receives completion messages
|
||||||
|
return {
|
||||||
|
'type': 'agent_update',
|
||||||
|
'agentIndex': -1, # Sentinel for untracked
|
||||||
|
'agentName': 'Unknown',
|
||||||
|
'agentType': 'coding',
|
||||||
|
'featureId': feature_id,
|
||||||
|
'featureName': f'Feature #{feature_id}',
|
||||||
|
'state': state,
|
||||||
|
'thought': 'Completed successfully!' if is_success else 'Failed to complete',
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'synthetic': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class OrchestratorTracker:
|
class OrchestratorTracker:
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { type AgentMascot, type AgentState } from '../lib/types'
|
import { type AgentMascot, type AgentState } from '../lib/types'
|
||||||
|
|
||||||
interface AgentAvatarProps {
|
interface AgentAvatarProps {
|
||||||
name: AgentMascot
|
name: AgentMascot | 'Unknown'
|
||||||
state: AgentState
|
state: AgentState
|
||||||
size?: 'sm' | 'md' | 'lg'
|
size?: 'sm' | 'md' | 'lg'
|
||||||
showName?: boolean
|
showName?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback colors for unknown agents (neutral gray)
|
||||||
|
const UNKNOWN_COLORS = { primary: '#6B7280', secondary: '#9CA3AF', accent: '#F3F4F6' }
|
||||||
|
|
||||||
const AVATAR_COLORS: Record<AgentMascot, { primary: string; secondary: string; accent: string }> = {
|
const AVATAR_COLORS: Record<AgentMascot, { primary: string; secondary: string; accent: string }> = {
|
||||||
// Original 5
|
// Original 5
|
||||||
Spark: { primary: '#3B82F6', secondary: '#60A5FA', accent: '#DBEAFE' }, // Blue robot
|
Spark: { primary: '#3B82F6', secondary: '#60A5FA', accent: '#DBEAFE' }, // Blue robot
|
||||||
@@ -473,6 +476,19 @@ function FluxSVG({ colors, size }: { colors: typeof AVATAR_COLORS.Flux; size: nu
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unknown agent fallback - simple question mark icon
|
||||||
|
function UnknownSVG({ colors, size }: { colors: typeof UNKNOWN_COLORS; size: number }) {
|
||||||
|
return (
|
||||||
|
<svg width={size} height={size} viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
{/* Circle background */}
|
||||||
|
<circle cx="32" cy="32" r="28" fill={colors.primary} />
|
||||||
|
<circle cx="32" cy="32" r="24" fill={colors.secondary} />
|
||||||
|
{/* Question mark */}
|
||||||
|
<text x="32" y="44" textAnchor="middle" fontSize="32" fontWeight="bold" fill="white">?</text>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const MASCOT_SVGS: Record<AgentMascot, typeof SparkSVG> = {
|
const MASCOT_SVGS: Record<AgentMascot, typeof SparkSVG> = {
|
||||||
// Original 5
|
// Original 5
|
||||||
Spark: SparkSVG,
|
Spark: SparkSVG,
|
||||||
@@ -561,9 +577,11 @@ function getStateDescription(state: AgentState): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function AgentAvatar({ name, state, size = 'md', showName = false }: AgentAvatarProps) {
|
export function AgentAvatar({ name, state, size = 'md', showName = false }: AgentAvatarProps) {
|
||||||
const colors = AVATAR_COLORS[name]
|
// Handle 'Unknown' agents (synthetic completions from untracked agents)
|
||||||
|
const isUnknown = name === 'Unknown'
|
||||||
|
const colors = isUnknown ? UNKNOWN_COLORS : AVATAR_COLORS[name]
|
||||||
const { svg: svgSize, font } = SIZES[size]
|
const { svg: svgSize, font } = SIZES[size]
|
||||||
const SvgComponent = MASCOT_SVGS[name]
|
const SvgComponent = isUnknown ? UnknownSVG : MASCOT_SVGS[name]
|
||||||
const stateDesc = getStateDescription(state)
|
const stateDesc = getStateDescription(state)
|
||||||
const ariaLabel = `Agent ${name} is ${stateDesc}`
|
const ariaLabel = `Agent ${name} is ${stateDesc}`
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { AgentAvatar } from './AgentAvatar'
|
|||||||
import type { AgentMascot } from '../lib/types'
|
import type { AgentMascot } from '../lib/types'
|
||||||
|
|
||||||
interface CelebrationOverlayProps {
|
interface CelebrationOverlayProps {
|
||||||
agentName: AgentMascot
|
agentName: AgentMascot | 'Unknown'
|
||||||
featureName: string
|
featureName: string
|
||||||
onComplete?: () => void
|
onComplete?: () => void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ interface DependencyGraphProps {
|
|||||||
|
|
||||||
// Agent info to display on a node
|
// Agent info to display on a node
|
||||||
interface NodeAgentInfo {
|
interface NodeAgentInfo {
|
||||||
name: AgentMascot
|
name: AgentMascot | 'Unknown'
|
||||||
state: AgentState
|
state: AgentState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ interface ActivityItem {
|
|||||||
|
|
||||||
// Celebration trigger for overlay
|
// Celebration trigger for overlay
|
||||||
interface CelebrationTrigger {
|
interface CelebrationTrigger {
|
||||||
agentName: AgentMascot
|
agentName: AgentMascot | 'Unknown'
|
||||||
featureName: string
|
featureName: string
|
||||||
featureId: number
|
featureId: number
|
||||||
}
|
}
|
||||||
@@ -190,9 +190,18 @@ export function useProjectWebSocket(projectName: string | null) {
|
|||||||
if (message.state === 'success' || message.state === 'error') {
|
if (message.state === 'success' || message.state === 'error') {
|
||||||
// Remove agent from active list on completion (success or failure)
|
// Remove agent from active list on completion (success or failure)
|
||||||
// But keep the logs in agentLogs map for debugging
|
// But keep the logs in agentLogs map for debugging
|
||||||
newAgents = prev.activeAgents.filter(
|
if (message.agentIndex === -1) {
|
||||||
a => a.agentIndex !== message.agentIndex
|
// Synthetic completion: remove by featureId
|
||||||
)
|
// This handles agents that weren't tracked but still completed
|
||||||
|
newAgents = prev.activeAgents.filter(
|
||||||
|
a => a.featureId !== message.featureId
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Normal completion: remove by agentIndex
|
||||||
|
newAgents = prev.activeAgents.filter(
|
||||||
|
a => a.agentIndex !== message.agentIndex
|
||||||
|
)
|
||||||
|
}
|
||||||
} else if (existingAgentIdx >= 0) {
|
} else if (existingAgentIdx >= 0) {
|
||||||
// Update existing agent
|
// Update existing agent
|
||||||
newAgents = [...prev.activeAgents]
|
newAgents = [...prev.activeAgents]
|
||||||
@@ -411,6 +420,27 @@ export function useProjectWebSocket(projectName: string | null) {
|
|||||||
}
|
}
|
||||||
}, [projectName, connect, sendPing])
|
}, [projectName, connect, sendPing])
|
||||||
|
|
||||||
|
// Defense-in-depth: cleanup stale agents for users who leave UI open for hours
|
||||||
|
// This catches edge cases where completion messages are missed
|
||||||
|
useEffect(() => {
|
||||||
|
const STALE_THRESHOLD_MS = 30 * 60 * 1000 // 30 minutes
|
||||||
|
|
||||||
|
const cleanup = setInterval(() => {
|
||||||
|
setState(prev => {
|
||||||
|
const now = Date.now()
|
||||||
|
const fresh = prev.activeAgents.filter(a =>
|
||||||
|
now - new Date(a.timestamp).getTime() < STALE_THRESHOLD_MS
|
||||||
|
)
|
||||||
|
if (fresh.length !== prev.activeAgents.length) {
|
||||||
|
return { ...prev, activeAgents: fresh }
|
||||||
|
}
|
||||||
|
return prev
|
||||||
|
})
|
||||||
|
}, 60000) // Check every minute
|
||||||
|
|
||||||
|
return () => clearInterval(cleanup)
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Clear logs function
|
// Clear logs function
|
||||||
const clearLogs = useCallback(() => {
|
const clearLogs = useCallback(() => {
|
||||||
setState(prev => ({ ...prev, logs: [] }))
|
setState(prev => ({ ...prev, logs: [] }))
|
||||||
|
|||||||
@@ -195,8 +195,8 @@ export interface AgentLogEntry {
|
|||||||
|
|
||||||
// Agent update from backend
|
// Agent update from backend
|
||||||
export interface ActiveAgent {
|
export interface ActiveAgent {
|
||||||
agentIndex: number
|
agentIndex: number // -1 for synthetic completions
|
||||||
agentName: AgentMascot
|
agentName: AgentMascot | 'Unknown'
|
||||||
agentType: AgentType // "coding" or "testing"
|
agentType: AgentType // "coding" or "testing"
|
||||||
featureId: number
|
featureId: number
|
||||||
featureName: string
|
featureName: string
|
||||||
@@ -265,14 +265,15 @@ export interface WSLogMessage {
|
|||||||
|
|
||||||
export interface WSAgentUpdateMessage {
|
export interface WSAgentUpdateMessage {
|
||||||
type: 'agent_update'
|
type: 'agent_update'
|
||||||
agentIndex: number
|
agentIndex: number // -1 for synthetic completions (untracked agents)
|
||||||
agentName: AgentMascot
|
agentName: AgentMascot | 'Unknown'
|
||||||
agentType: AgentType // "coding" or "testing"
|
agentType: AgentType // "coding" or "testing"
|
||||||
featureId: number
|
featureId: number
|
||||||
featureName: string
|
featureName: string
|
||||||
state: AgentState
|
state: AgentState
|
||||||
thought?: string
|
thought?: string
|
||||||
timestamp: string
|
timestamp: string
|
||||||
|
synthetic?: boolean // True for synthetic completions from untracked agents
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WSAgentStatusMessage {
|
export interface WSAgentStatusMessage {
|
||||||
|
|||||||
Reference in New Issue
Block a user