mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-30 06:12:06 +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:
@@ -1,12 +1,15 @@
|
||||
import { type AgentMascot, type AgentState } from '../lib/types'
|
||||
|
||||
interface AgentAvatarProps {
|
||||
name: AgentMascot
|
||||
name: AgentMascot | 'Unknown'
|
||||
state: AgentState
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
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 }> = {
|
||||
// Original 5
|
||||
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> = {
|
||||
// Original 5
|
||||
Spark: SparkSVG,
|
||||
@@ -561,9 +577,11 @@ function getStateDescription(state: AgentState): string {
|
||||
}
|
||||
|
||||
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 SvgComponent = MASCOT_SVGS[name]
|
||||
const SvgComponent = isUnknown ? UnknownSVG : MASCOT_SVGS[name]
|
||||
const stateDesc = getStateDescription(state)
|
||||
const ariaLabel = `Agent ${name} is ${stateDesc}`
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AgentAvatar } from './AgentAvatar'
|
||||
import type { AgentMascot } from '../lib/types'
|
||||
|
||||
interface CelebrationOverlayProps {
|
||||
agentName: AgentMascot
|
||||
agentName: AgentMascot | 'Unknown'
|
||||
featureName: string
|
||||
onComplete?: () => void
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ interface DependencyGraphProps {
|
||||
|
||||
// Agent info to display on a node
|
||||
interface NodeAgentInfo {
|
||||
name: AgentMascot
|
||||
name: AgentMascot | 'Unknown'
|
||||
state: AgentState
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ interface ActivityItem {
|
||||
|
||||
// Celebration trigger for overlay
|
||||
interface CelebrationTrigger {
|
||||
agentName: AgentMascot
|
||||
agentName: AgentMascot | 'Unknown'
|
||||
featureName: string
|
||||
featureId: number
|
||||
}
|
||||
@@ -190,9 +190,18 @@ export function useProjectWebSocket(projectName: string | null) {
|
||||
if (message.state === 'success' || message.state === 'error') {
|
||||
// Remove agent from active list on completion (success or failure)
|
||||
// But keep the logs in agentLogs map for debugging
|
||||
newAgents = prev.activeAgents.filter(
|
||||
a => a.agentIndex !== message.agentIndex
|
||||
)
|
||||
if (message.agentIndex === -1) {
|
||||
// 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) {
|
||||
// Update existing agent
|
||||
newAgents = [...prev.activeAgents]
|
||||
@@ -411,6 +420,27 @@ export function useProjectWebSocket(projectName: string | null) {
|
||||
}
|
||||
}, [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
|
||||
const clearLogs = useCallback(() => {
|
||||
setState(prev => ({ ...prev, logs: [] }))
|
||||
|
||||
@@ -195,8 +195,8 @@ export interface AgentLogEntry {
|
||||
|
||||
// Agent update from backend
|
||||
export interface ActiveAgent {
|
||||
agentIndex: number
|
||||
agentName: AgentMascot
|
||||
agentIndex: number // -1 for synthetic completions
|
||||
agentName: AgentMascot | 'Unknown'
|
||||
agentType: AgentType // "coding" or "testing"
|
||||
featureId: number
|
||||
featureName: string
|
||||
@@ -265,14 +265,15 @@ export interface WSLogMessage {
|
||||
|
||||
export interface WSAgentUpdateMessage {
|
||||
type: 'agent_update'
|
||||
agentIndex: number
|
||||
agentName: AgentMascot
|
||||
agentIndex: number // -1 for synthetic completions (untracked agents)
|
||||
agentName: AgentMascot | 'Unknown'
|
||||
agentType: AgentType // "coding" or "testing"
|
||||
featureId: number
|
||||
featureName: string
|
||||
state: AgentState
|
||||
thought?: string
|
||||
timestamp: string
|
||||
synthetic?: boolean // True for synthetic completions from untracked agents
|
||||
}
|
||||
|
||||
export interface WSAgentStatusMessage {
|
||||
|
||||
Reference in New Issue
Block a user