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:
Auto
2026-01-23 13:19:45 +02:00
parent a03d945fcd
commit 1be42cc734
6 changed files with 94 additions and 35 deletions

View File

@@ -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}`