'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { Loader } from '@/components/ui/loader' import { useSmartPoll } from '@/lib/use-smart-poll' import { createClientLogger } from '@/lib/client-logger' import { AgentAvatar } from '@/components/ui/agent-avatar' import { OverviewTab, SoulTab, MemoryTab, TasksTab, ActivityTab, ConfigTab, FilesTab, ToolsTab, ChannelsTab, CronTab, ModelsTab, CreateAgentModal } from './agent-detail-tabs' import { formatModelName, buildTaskStatParts } from '@/lib/agent-card-helpers' import { useMissionControl, type Agent } from '@/store' const log = createClientLogger('AgentSquadPhase3') interface WorkItem { type: string count: number items: any[] } interface HeartbeatResponse { status: 'HEARTBEAT_OK' | 'WORK_ITEMS_FOUND' agent: string checked_at: number work_items?: WorkItem[] total_items?: number message?: string } interface SoulTemplate { name: string description: string size: number } const statusColors: Record = { offline: 'bg-gray-500', idle: 'bg-green-500', busy: 'bg-yellow-500', error: 'bg-red-500', } const statusBadgeStyles: Record = { offline: 'bg-slate-500/15 text-slate-300 border-slate-500/30', idle: 'bg-emerald-500/15 text-emerald-300 border-emerald-500/30', busy: 'bg-amber-500/15 text-amber-300 border-amber-500/30', error: 'bg-rose-500/15 text-rose-300 border-rose-500/30', } const statusIcons: Record = { offline: '-', idle: 'o', busy: '~', error: '!', } const defaultCardStyle = { edge: 'from-slate-400/60 to-slate-600/30', glow: 'from-slate-500/10 via-transparent to-transparent', dot: 'bg-slate-400', } const statusCardStyles: Record = { offline: defaultCardStyle, idle: { edge: 'from-emerald-300/80 to-emerald-600/30', glow: 'from-emerald-400/15 via-transparent to-transparent', dot: 'bg-emerald-300', }, busy: { edge: 'from-amber-300/80 to-amber-600/30', glow: 'from-amber-400/15 via-transparent to-transparent', dot: 'bg-amber-300', }, error: { edge: 'from-rose-300/80 to-rose-600/30', glow: 'from-rose-400/15 via-transparent to-transparent', dot: 'bg-rose-300', }, } export function AgentSquadPanelPhase3() { const t = useTranslations('agentSquadPhase3') const { agents, setAgents } = useMissionControl() const [loading, setLoading] = useState(agents.length === 0) const [error, setError] = useState(null) const [selectedAgent, setSelectedAgent] = useState(null) const [showCreateModal, setShowCreateModal] = useState(false) const [showQuickSpawnModal, setShowQuickSpawnModal] = useState(false) const [autoRefresh, setAutoRefresh] = useState(true) const [syncing, setSyncing] = useState(false) const [syncToast, setSyncToast] = useState(null) // Sync agents from gateway config or local disk const syncFromConfig = async (source?: 'local') => { setSyncing(true) setSyncToast(null) try { const url = source === 'local' ? '/api/agents/sync?source=local' : '/api/agents/sync' const response = await fetch(url, { method: 'POST' }) if (response.status === 401) { window.location.assign('/login?next=%2Fagents') return } const data = await response.json() if (response.status === 403) { throw new Error('Admin access required for agent sync') } if (!response.ok) throw new Error(data.error || 'Sync failed') if (source === 'local') { setSyncToast(data.message || 'Local agent sync complete') } else { setSyncToast(`Synced ${data.synced} agents (${data.created} new, ${data.updated} updated)`) } fetchAgents() setTimeout(() => setSyncToast(null), 5000) } catch (err: any) { setSyncToast(`Sync failed: ${err.message}`) setTimeout(() => setSyncToast(null), 5000) } finally { setSyncing(false) } } // Fetch agents const fetchAgents = useCallback(async () => { try { setError(null) if (agents.length === 0) setLoading(true) const response = await fetch('/api/agents') if (response.status === 401) { window.location.assign('/login?next=%2Fagents') return } if (response.status === 403) { throw new Error('Access denied') } if (!response.ok) { const data = await response.json().catch(() => ({})) throw new Error(data.error || 'Failed to fetch agents') } const data = await response.json() setAgents(data.agents || []) } catch (err) { setError(err instanceof Error ? err.message : 'An error occurred') } finally { setLoading(false) } }, [agents.length, setAgents]) // Smart polling with visibility pause useSmartPoll(fetchAgents, 30000, { enabled: autoRefresh, pauseWhenSseConnected: true }) // Update agent status const updateAgentStatus = async (agentName: string, status: Agent['status'], activity?: string) => { try { const response = await fetch('/api/agents', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: agentName, status, last_activity: activity || `Status changed to ${status}` }) }) if (!response.ok) throw new Error('Failed to update agent status') // Update store state setAgents(agents.map(agent => agent.name === agentName ? { ...agent, status, last_activity: activity || `Status changed to ${status}`, last_seen: Math.floor(Date.now() / 1000), updated_at: Math.floor(Date.now() / 1000) } : agent )) } catch (error) { log.error('Failed to update agent status:', error) setError('Failed to update agent status') } } // Wake agent via session_send const wakeAgent = async (agentName: string, sessionKey: string) => { try { const response = await fetch(`/api/agents/${agentName}/wake`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: `🤖 **Wake Up Call**\n\nAgent ${agentName}, you have been manually woken up.\nCheck Mission Control for any pending tasks or notifications.\n\n⏰ ${new Date().toLocaleString()}` }) }) if (!response.ok) { const data = await response.json().catch(() => ({})) throw new Error(data.error || 'Failed to wake agent') } await updateAgentStatus(agentName, 'idle', 'Manually woken via session') } catch (error) { log.error('Failed to wake agent:', error) setError('Failed to wake agent') } } const deleteAgent = async (agentId: number, removeWorkspace: boolean) => { const previousAgents = agents setAgents(agents.filter((agent) => agent.id !== agentId)) const response = await fetch(`/api/agents/${agentId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ remove_workspace: removeWorkspace }), }) const payload = await response.json().catch(() => ({})) if (!response.ok) { setAgents(previousAgents) throw new Error(payload?.error || 'Failed to delete agent') } setSyncToast( removeWorkspace ? `Deleted agent and workspace: ${payload?.deleted || agentId}` : `Deleted agent: ${payload?.deleted || agentId}`, ) await fetchAgents() setTimeout(() => setSyncToast(null), 5000) } // Format last seen time const formatLastSeen = (timestamp?: number) => { if (!timestamp) return 'Never' const now = Date.now() const diffMs = now - (timestamp * 1000) const diffMinutes = Math.floor(diffMs / (1000 * 60)) const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)) if (diffMinutes < 1) return 'Just now' if (diffMinutes < 60) return `${diffMinutes}m ago` if (diffHours < 24) return `${diffHours}h ago` if (diffDays < 7) return `${diffDays}d ago` return new Date(timestamp * 1000).toLocaleDateString() } // Check if agent had recent heartbeat (within 30 minutes) const hasRecentHeartbeat = (agent: Agent) => { if (!agent.last_seen) return false const thirtyMinutesAgo = Math.floor(Date.now() / 1000) - (30 * 60) return agent.last_seen > thirtyMinutesAgo } // Get status distribution for summary const statusCounts = agents.reduce((acc, agent) => { acc[agent.status] = (acc[agent.status] || 0) + 1 return acc }, {} as Record) if (loading && agents.length === 0) { return } return (
{/* Header */}

{t('title')}

{/* Status Summary */}
{Object.entries(statusCounts).map(([status, count]) => (
{count}
))}
{/* Active Heartbeats Indicator */}
{t('activeHeartbeats', { count: agents.filter(hasRecentHeartbeat).length })}
{/* Sync Toast */} {syncToast && (
{syncToast}
)} {/* Error Display */} {error && (
{error}
)} {/* Agent Grid */}
{agents.length === 0 ? (

{t('noAgents')}

{t('noAgentsHint')}

) : (
{agents.map(agent => { const modelName = formatModelName(agent.config) const taskStatsLine = buildTaskStatParts(agent.taskStats) return (
setSelectedAgent(agent)} >
{/* Header: avatar + name + status */}

{agent.name}

{(agent as any).source && (agent as any).source !== 'manual' && ( {(agent as any).source} )}

{agent.role}{modelName && <> · {modelName}}

{hasRecentHeartbeat(agent) && (
)} {agent.status}
{/* Task stats — inline */} {taskStatsLine && (
{taskStatsLine.map((part, i) => ( {i > 0 && ·} {part.count} {' '}{part.label} ))}
)} {/* Footer: last seen + actions */}
{formatLastSeen(agent.last_seen)}
{agent.session_key ? ( ) : ( )}
) })}
)}
{/* Agent Detail Modal */} {selectedAgent && ( setSelectedAgent(null)} onUpdate={fetchAgents} onStatusUpdate={updateAgentStatus} onWakeAgent={wakeAgent} onDelete={deleteAgent} /> )} {/* Create Agent Modal */} {showCreateModal && ( setShowCreateModal(false)} onCreated={fetchAgents} /> )} {/* Quick Spawn Modal */} {showQuickSpawnModal && selectedAgent && ( { setShowQuickSpawnModal(false) setSelectedAgent(null) }} onSpawned={fetchAgents} /> )}
) } // Enhanced Agent Detail Modal with Tabs function AgentDetailModalPhase3({ agent, onClose, onUpdate, onStatusUpdate, onWakeAgent, onDelete }: { agent: Agent onClose: () => void onUpdate: () => void onStatusUpdate: (name: string, status: Agent['status'], activity?: string) => Promise onWakeAgent: (name: string, sessionKey: string) => Promise onDelete: (agentId: number, removeWorkspace: boolean) => Promise }) { const [agentState, setAgentState] = useState(agent as Agent & { config?: any; working_memory?: string }) const [activeTab, setActiveTab] = useState<'overview' | 'soul' | 'memory' | 'config' | 'tasks' | 'activity' | 'files' | 'tools' | 'channels' | 'cron' | 'models'>('overview') const [editing, setEditing] = useState(false) const [formData, setFormData] = useState({ role: agent.role, session_key: agent.session_key || '', soul_content: agent.soul_content || '', working_memory: agent.working_memory || '', model: (() => { const p = (agent as any).config?.model?.primary; return (typeof p === 'string' ? p : p?.primary) || '' })(), }) const [workspaceFiles, setWorkspaceFiles] = useState<{ identityMd: string; agentMd: string }>({ identityMd: '', agentMd: '', }) const [soulTemplates, setSoulTemplates] = useState([]) const [heartbeatData, setHeartbeatData] = useState(null) const [loadingHeartbeat, setLoadingHeartbeat] = useState(false) const [deleteBusy, setDeleteBusy] = useState(false) const [deleteError, setDeleteError] = useState(null) const [showDeleteMenu, setShowDeleteMenu] = useState(false) const [saveBusy, setSaveBusy] = useState(false) const deleteMenuRef = useRef(null) useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (deleteBusy) return if (deleteMenuRef.current && !deleteMenuRef.current.contains(e.target as Node)) { setShowDeleteMenu(false) } } if (showDeleteMenu) { document.addEventListener('mousedown', handleClickOutside) } return () => document.removeEventListener('mousedown', handleClickOutside) }, [showDeleteMenu, deleteBusy]) useEffect(() => { setAgentState(agent as Agent & { config?: any; working_memory?: string }) setFormData({ role: agent.role, session_key: agent.session_key || '', soul_content: agent.soul_content || '', working_memory: (agent as any).working_memory || '', model: (() => { const p = (agent as any).config?.model?.primary; return (typeof p === 'string' ? p : p?.primary) || '' })(), }) }, [agent]) useEffect(() => { const loadCanonicalAgentData = async () => { try { const [agentRes, soulRes, memoryRes, filesRes] = await Promise.all([ fetch(`/api/agents/${agent.id}`), fetch(`/api/agents/${agent.id}/soul`), fetch(`/api/agents/${agent.id}/memory`), fetch(`/api/agents/${agent.id}/files`), ]) if (agentRes.ok) { const payload = await agentRes.json() if (payload?.agent) { const freshAgent = payload.agent as Agent & { config?: any; working_memory?: string } setAgentState((prev) => ({ ...prev, ...freshAgent })) setFormData((prev) => ({ ...prev, role: freshAgent.role || prev.role, session_key: freshAgent.session_key || '', model: (freshAgent as any).config?.model?.primary || prev.model, })) } } if (soulRes.ok) { const payload = await soulRes.json() setFormData((prev) => ({ ...prev, soul_content: String(payload?.soul_content || '') })) } if (memoryRes.ok) { const payload = await memoryRes.json() setFormData((prev) => ({ ...prev, working_memory: String(payload?.working_memory || '') })) } if (filesRes.ok) { const payload = await filesRes.json() setWorkspaceFiles({ identityMd: String(payload?.files?.['identity.md']?.content || ''), agentMd: String(payload?.files?.['agent.md']?.content || ''), }) } } catch (error) { log.error('Failed to load canonical agent data:', error) } } loadCanonicalAgentData() }, [agent.id]) const formatLastSeen = (timestamp?: number) => { if (!timestamp) return 'Never' const diffMs = Date.now() - (timestamp * 1000) const diffMinutes = Math.floor(diffMs / (1000 * 60)) const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) if (diffMinutes < 1) return 'Just now' if (diffMinutes < 60) return `${diffMinutes}m ago` if (diffHours < 24) return `${diffHours}h ago` return new Date(timestamp * 1000).toLocaleDateString() } // Load SOUL templates useEffect(() => { const loadTemplates = async () => { try { const response = await fetch(`/api/agents/${agent.name}/soul`, { method: 'PATCH' }) if (response.ok) { const data = await response.json() setSoulTemplates(data.templates || []) } } catch (error) { log.error('Failed to load SOUL templates:', error) } } if (activeTab === 'soul') { loadTemplates() } }, [activeTab, agent.name]) // Perform heartbeat check const performHeartbeat = async () => { setLoadingHeartbeat(true) try { const response = await fetch(`/api/agents/${agent.name}/heartbeat`) if (response.ok) { const data = await response.json() setHeartbeatData(data) } } catch (error) { log.error('Failed to perform heartbeat:', error) } finally { setLoadingHeartbeat(false) } } const handleSave = async () => { setSaveBusy(true) try { const response = await fetch('/api/agents', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: agentState.name, ...formData }) }) if (!response.ok) throw new Error('Failed to update agent') setEditing(false) onUpdate() } catch (error) { log.error('Failed to update agent:', error) } finally { setSaveBusy(false) } } const handleSoulSave = async (content: string, templateName?: string) => { try { const response = await fetch(`/api/agents/${agentState.id}/soul`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ soul_content: content, template_name: templateName }) }) if (!response.ok) throw new Error('Failed to update SOUL') setFormData(prev => ({ ...prev, soul_content: content })) setAgentState(prev => ({ ...prev, soul_content: content })) onUpdate() } catch (error) { log.error('Failed to update SOUL:', error) } } const handleMemorySave = async (content: string, append: boolean = false) => { try { const response = await fetch(`/api/agents/${agentState.id}/memory`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ working_memory: content, append }) }) if (!response.ok) throw new Error('Failed to update memory') const data = await response.json() setFormData(prev => ({ ...prev, working_memory: data.working_memory })) setAgentState(prev => ({ ...prev, working_memory: data.working_memory })) onUpdate() } catch (error) { log.error('Failed to update memory:', error) } } const handleWorkspaceFileSave = async (file: 'identity.md' | 'agent.md', content: string) => { const response = await fetch(`/api/agents/${agentState.id}/files`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ file, content }), }) const payload = await response.json().catch(() => ({})) if (!response.ok) { throw new Error(payload?.error || `Failed to save ${file}`) } setWorkspaceFiles((prev) => ({ ...prev, ...(file === 'identity.md' ? { identityMd: content } : { agentMd: content }), })) } const tabs = [ { id: 'overview', label: 'Overview', icon: 'O' }, { id: 'files', label: 'Files', icon: 'F' }, { id: 'tools', label: 'Tools', icon: 'W' }, { id: 'models', label: 'Models', icon: 'P' }, { id: 'channels', label: 'Channels', icon: 'H' }, { id: 'cron', label: 'Cron', icon: 'R' }, { id: 'soul', label: 'SOUL', icon: 'S' }, { id: 'memory', label: 'Memory', icon: 'M' }, { id: 'tasks', label: 'Tasks', icon: 'T' }, { id: 'config', label: 'Config', icon: 'C' }, { id: 'activity', label: 'Activity', icon: 'A' } ] const handleDelete = async (removeWorkspace: boolean) => { const scope = removeWorkspace ? 'agent and workspace' : 'agent' const confirmed = window.confirm(`Delete ${scope} for "${agentState.name}"? This cannot be undone.`) if (!confirmed) return setDeleteBusy(true) setDeleteError(null) try { await onDelete(agentState.id, removeWorkspace) onClose() } catch (error: any) { setDeleteError(error?.message || `Failed to delete ${scope}`) } finally { setDeleteBusy(false) } } return (
e.stopPropagation()} > {/* Modal Header */}

{agentState.name}

{agentState.status} {agentState.session_key && ( Session )}
{agentState.role} · seen {formatLastSeen(agentState.last_seen)}
{showDeleteMenu && (
)}
{deleteError && (
{deleteError}
)} {/* Tab Navigation */}
{tabs.map(tab => ( ))}
{/* Tab Content */}
{activeTab === 'overview' && ( setEditing(true)} onCancel={() => setEditing(false)} heartbeatData={heartbeatData} loadingHeartbeat={loadingHeartbeat} onPerformHeartbeat={performHeartbeat} /> )} {activeTab === 'soul' && ( )} {activeTab === 'memory' && ( )} {activeTab === 'tasks' && ( )} {activeTab === 'config' && ( )} {activeTab === 'files' && ( )} {activeTab === 'tools' && ( )} {activeTab === 'channels' && ( )} {activeTab === 'cron' && ( )} {activeTab === 'models' && ( )} {activeTab === 'activity' && ( )}
) } // Quick Spawn Modal Component function QuickSpawnModal({ agent, onClose, onSpawned }: { agent: Agent onClose: () => void onSpawned: () => void }) { const [spawnData, setSpawnData] = useState({ task: '', model: 'sonnet', label: `${agent.name}-subtask-${Date.now()}`, timeoutSeconds: 300 }) const [isSpawning, setIsSpawning] = useState(false) const [spawnResult, setSpawnResult] = useState(null) const models = [ { id: 'haiku', name: 'Claude Haiku', cost: '$0.25/1K', speed: 'Ultra Fast' }, { id: 'sonnet', name: 'Claude Sonnet', cost: '$3.00/1K', speed: 'Fast' }, { id: 'opus', name: 'Claude Opus', cost: '$15.00/1K', speed: 'Slow' }, { id: 'groq-fast', name: 'Groq Llama 8B', cost: '$0.05/1K', speed: '840 tok/s' }, { id: 'groq', name: 'Groq Llama 70B', cost: '$0.59/1K', speed: '150 tok/s' }, { id: 'deepseek', name: 'DeepSeek R1', cost: 'FREE', speed: 'Local' }, ] const handleSpawn = async () => { if (!spawnData.task.trim()) { alert('Please enter a task description') return } setIsSpawning(true) try { const response = await fetch('/api/spawn', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...spawnData, parentAgent: agent.name, sessionKey: agent.session_key }) }) const result = await response.json() if (response.ok) { setSpawnResult(result) onSpawned() // Auto-close after 2 seconds if successful setTimeout(() => { onClose() }, 2000) } else { alert(result.error || 'Failed to spawn agent') } } catch (error) { log.error('Spawn failed:', error) alert('Network error occurred') } finally { setIsSpawning(false) } } return (

Quick Spawn for {agent.name}

{spawnResult ? (
Agent spawned successfully!

Agent ID: {spawnResult.agentId}

Session: {spawnResult.sessionId}

Model: {spawnResult.model}

) : (
{/* Task Description */}