mission-control/src/components/panels/agent-squad-panel-phase3.tsx

865 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { useState, useEffect, useCallback } from 'react'
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,
CreateAgentModal
} from './agent-detail-tabs'
const log = createClientLogger('AgentSquadPhase3')
interface Agent {
id: number
name: string
role: string
session_key?: string
soul_content?: string
working_memory?: string
status: 'offline' | 'idle' | 'busy' | 'error'
last_seen?: number
last_activity?: string
created_at: number
updated_at: number
config?: any
taskStats?: {
total: number
assigned: number
in_progress: number
completed: number
}
}
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<string, string> = {
offline: 'bg-gray-500',
idle: 'bg-green-500',
busy: 'bg-yellow-500',
error: 'bg-red-500',
}
const statusIcons: Record<string, string> = {
offline: '-',
idle: 'o',
busy: '~',
error: '!',
}
export function AgentSquadPanelPhase3() {
const [agents, setAgents] = useState<Agent[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [selectedAgent, setSelectedAgent] = useState<Agent | null>(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<string | null>(null)
// Sync agents from gateway config
const syncFromConfig = async () => {
setSyncing(true)
setSyncToast(null)
try {
const response = await fetch('/api/agents/sync', { method: 'POST' })
const data = await response.json()
if (!response.ok) throw new Error(data.error || 'Sync failed')
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.ok) throw new 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])
// 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 local state
setAgents(prev => prev.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')
}
}
// 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<string, number>)
if (loading && agents.length === 0) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
<span className="ml-2 text-muted-foreground">Loading agents...</span>
</div>
)
}
return (
<div className="h-full flex flex-col">
{/* Header */}
<div className="flex justify-between items-center p-4 border-b border-border flex-shrink-0">
<div className="flex items-center gap-4">
<h2 className="text-xl font-bold text-foreground">Agent Squad</h2>
{/* Status Summary */}
<div className="flex gap-2 text-sm">
{Object.entries(statusCounts).map(([status, count]) => (
<div key={status} className="flex items-center gap-1">
<div className={`w-2 h-2 rounded-full ${statusColors[status]}`}></div>
<span className="text-muted-foreground">{count}</span>
</div>
))}
</div>
{/* Active Heartbeats Indicator */}
<div className="flex items-center gap-1">
<div className="w-2 h-2 rounded-full bg-cyan-400 animate-pulse"></div>
<span className="text-sm text-muted-foreground">
{agents.filter(hasRecentHeartbeat).length} active heartbeats
</span>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => setAutoRefresh(!autoRefresh)}
className={`px-3 py-1.5 text-sm rounded-md transition-smooth ${
autoRefresh
? 'bg-green-500/20 text-green-400 border border-green-500/30'
: 'bg-secondary text-muted-foreground'
}`}
>
{autoRefresh ? 'Live' : 'Manual'}
</button>
<button
onClick={syncFromConfig}
disabled={syncing}
className="px-3 py-2 bg-cyan-500/20 text-cyan-400 border border-cyan-500/30 rounded-md hover:bg-cyan-500/30 disabled:opacity-50 transition-smooth text-sm"
>
{syncing ? 'Syncing...' : 'Sync from Config'}
</button>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-smooth text-sm font-medium"
>
+ Add Agent
</button>
<button
onClick={fetchAgents}
className="px-4 py-2 bg-secondary text-muted-foreground rounded-md hover:bg-surface-2 transition-smooth text-sm"
>
Refresh
</button>
</div>
</div>
{/* Sync Toast */}
{syncToast && (
<div className={`p-3 m-4 rounded-lg text-sm ${syncToast.includes('failed') ? 'bg-red-500/10 border border-red-500/20 text-red-400' : 'bg-green-500/10 border border-green-500/20 text-green-400'}`}>
{syncToast}
</div>
)}
{/* Error Display */}
{error && (
<div className="bg-red-500/10 border border-red-500/20 text-red-400 p-3 m-4 rounded-lg text-sm flex items-center justify-between">
<span>{error}</span>
<button
onClick={() => setError(null)}
className="text-red-400/60 hover:text-red-400 ml-2"
>
×
</button>
</div>
)}
{/* Agent Grid */}
<div className="flex-1 p-4 overflow-y-auto">
{agents.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground/50">
<div className="w-12 h-12 rounded-full bg-surface-2 flex items-center justify-center mb-3">
<svg width="20" height="20" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
<circle cx="8" cy="5" r="3" />
<path d="M2 14c0-3.3 2.7-6 6-6s6 2.7 6 6" />
</svg>
</div>
<p className="text-sm font-medium">No agents found</p>
<p className="text-xs mt-1">Add your first agent to get started</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{agents.map(agent => (
<div
key={agent.id}
className={`bg-card rounded-lg p-4 border-l-4 hover:bg-surface-1 transition-smooth cursor-pointer ${
hasRecentHeartbeat(agent) ? 'border-cyan-400' : 'border-border'
}`}
onClick={() => setSelectedAgent(agent)}
>
{/* Agent Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2 min-w-0">
<AgentAvatar name={agent.name} size="md" />
<div className="min-w-0">
<h3 className="font-semibold text-foreground text-lg truncate">{agent.name}</h3>
<p className="text-muted-foreground text-sm truncate">{agent.role}</p>
</div>
</div>
<div className="flex items-center gap-2">
{/* Heartbeat indicator */}
{hasRecentHeartbeat(agent) && (
<div className="w-3 h-3 rounded-full bg-cyan-400 animate-pulse" title="Recent heartbeat"></div>
)}
<div className={`w-3 h-3 rounded-full ${statusColors[agent.status]} animate-pulse`}></div>
<span className="text-xs text-muted-foreground">{agent.status}</span>
</div>
</div>
{/* Session Info */}
<div className="text-xs text-muted-foreground mb-2">
<div className="flex items-center justify-between">
<span>
<span className="font-medium">Session:</span> {agent.session_key || 'Not set'}
</span>
{agent.session_key && (
<div className="flex items-center gap-1">
<div className="w-2 h-2 rounded-full bg-green-400"></div>
<span>Active</span>
</div>
)}
</div>
</div>
{/* Task Stats */}
{agent.taskStats && (
<div className="grid grid-cols-2 gap-2 mb-3">
<div className="bg-surface-1/50 rounded p-2 text-center">
<div className="text-lg font-semibold text-foreground">{agent.taskStats.total}</div>
<div className="text-xs text-muted-foreground">Total Tasks</div>
</div>
<div className="bg-surface-1/50 rounded p-2 text-center">
<div className="text-lg font-semibold text-yellow-400">{agent.taskStats.in_progress}</div>
<div className="text-xs text-muted-foreground">In Progress</div>
</div>
</div>
)}
{/* Last Activity */}
<div className="text-xs text-muted-foreground mb-3">
<div>
<span className="font-medium">Last seen:</span> {formatLastSeen(agent.last_seen)}
</div>
{agent.last_activity && (
<div className="mt-1 truncate" title={agent.last_activity}>
<span className="font-medium">Activity:</span> {agent.last_activity}
</div>
)}
</div>
{/* Quick Actions */}
<div className="flex gap-1">
{agent.session_key ? (
<button
onClick={(e) => {
e.stopPropagation()
wakeAgent(agent.name, agent.session_key!)
}}
className="flex-1 px-2 py-1 text-xs bg-cyan-500/20 text-cyan-400 border border-cyan-500/30 rounded-md hover:bg-cyan-500/30 transition-smooth"
title="Wake agent via session"
>
Wake Agent
</button>
) : (
<button
onClick={(e) => {
e.stopPropagation()
updateAgentStatus(agent.name, 'idle', 'Manually activated')
}}
disabled={agent.status === 'idle'}
className="flex-1 px-2 py-1 text-xs bg-green-500/20 text-green-400 border border-green-500/30 rounded-md hover:bg-green-500/30 disabled:opacity-50 disabled:cursor-not-allowed transition-smooth"
>
Wake
</button>
)}
<button
onClick={(e) => {
e.stopPropagation()
updateAgentStatus(agent.name, 'busy', 'Manually set to busy')
}}
disabled={agent.status === 'busy'}
className="flex-1 px-2 py-1 text-xs bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 rounded-md hover:bg-yellow-500/30 disabled:opacity-50 disabled:cursor-not-allowed transition-smooth"
>
Busy
</button>
<button
onClick={(e) => {
e.stopPropagation()
setSelectedAgent(agent)
setShowQuickSpawnModal(true)
}}
className="flex-1 px-2 py-1 text-xs bg-blue-500/20 text-blue-400 border border-blue-500/30 rounded-md hover:bg-blue-500/30 transition-smooth"
>
Spawn
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Agent Detail Modal */}
{selectedAgent && (
<AgentDetailModalPhase3
agent={selectedAgent}
onClose={() => setSelectedAgent(null)}
onUpdate={fetchAgents}
onStatusUpdate={updateAgentStatus}
onWakeAgent={wakeAgent}
/>
)}
{/* Create Agent Modal */}
{showCreateModal && (
<CreateAgentModal
onClose={() => setShowCreateModal(false)}
onCreated={fetchAgents}
/>
)}
{/* Quick Spawn Modal */}
{showQuickSpawnModal && selectedAgent && (
<QuickSpawnModal
agent={selectedAgent}
onClose={() => {
setShowQuickSpawnModal(false)
setSelectedAgent(null)
}}
onSpawned={fetchAgents}
/>
)}
</div>
)
}
// Enhanced Agent Detail Modal with Tabs
function AgentDetailModalPhase3({
agent,
onClose,
onUpdate,
onStatusUpdate,
onWakeAgent
}: {
agent: Agent
onClose: () => void
onUpdate: () => void
onStatusUpdate: (name: string, status: Agent['status'], activity?: string) => Promise<void>
onWakeAgent: (name: string, sessionKey: string) => Promise<void>
}) {
const [activeTab, setActiveTab] = useState<'overview' | 'soul' | 'memory' | 'config' | 'tasks' | 'activity'>('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 || ''
})
const [soulTemplates, setSoulTemplates] = useState<SoulTemplate[]>([])
const [heartbeatData, setHeartbeatData] = useState<HeartbeatResponse | null>(null)
const [loadingHeartbeat, setLoadingHeartbeat] = useState(false)
// 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 () => {
try {
const response = await fetch('/api/agents', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: agent.name,
...formData
})
})
if (!response.ok) throw new Error('Failed to update agent')
setEditing(false)
onUpdate()
} catch (error) {
log.error('Failed to update agent:', error)
}
}
const handleSoulSave = async (content: string, templateName?: string) => {
try {
const response = await fetch(`/api/agents/${agent.name}/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 }))
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/${agent.name}/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 }))
onUpdate()
} catch (error) {
log.error('Failed to update memory:', error)
}
}
const tabs = [
{ id: 'overview', label: 'Overview', icon: '#' },
{ id: 'soul', label: 'SOUL', icon: '~' },
{ id: 'memory', label: 'Memory', icon: '@' },
{ id: 'tasks', label: 'Tasks', icon: '+' },
{ id: 'config', label: 'Config', icon: '*' },
{ id: 'activity', label: 'Activity', icon: '>' }
]
return (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-card border border-border rounded-lg max-w-4xl w-full max-h-[90vh] flex flex-col">
{/* Modal Header */}
<div className="p-6 border-b border-border">
<div className="flex justify-between items-start">
<div>
<h3 className="text-xl font-bold text-foreground">{agent.name}</h3>
<p className="text-muted-foreground">{agent.role}</p>
</div>
<div className="flex items-center gap-3">
<div className={`w-4 h-4 rounded-full ${statusColors[agent.status]}`}></div>
<span className="text-foreground">{agent.status}</span>
<button onClick={onClose} className="text-muted-foreground hover:text-foreground text-2xl transition-smooth">×</button>
</div>
</div>
{/* Tab Navigation */}
<div className="flex gap-1 mt-4">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={`px-4 py-2 text-sm rounded-md flex items-center gap-2 transition-smooth ${
activeTab === tab.id
? 'bg-primary text-primary-foreground'
: 'bg-secondary text-muted-foreground hover:bg-surface-2'
}`}
>
<span>{tab.icon}</span>
{tab.label}
</button>
))}
</div>
</div>
{/* Tab Content */}
<div className="flex-1 overflow-y-auto">
{activeTab === 'overview' && (
<OverviewTab
agent={agent}
editing={editing}
formData={formData}
setFormData={setFormData}
onSave={handleSave}
onStatusUpdate={onStatusUpdate}
onWakeAgent={onWakeAgent}
onEdit={() => setEditing(true)}
onCancel={() => setEditing(false)}
heartbeatData={heartbeatData}
loadingHeartbeat={loadingHeartbeat}
onPerformHeartbeat={performHeartbeat}
/>
)}
{activeTab === 'soul' && (
<SoulTab
agent={agent}
soulContent={formData.soul_content}
templates={soulTemplates}
onSave={handleSoulSave}
/>
)}
{activeTab === 'memory' && (
<MemoryTab
agent={agent}
workingMemory={formData.working_memory}
onSave={handleMemorySave}
/>
)}
{activeTab === 'tasks' && (
<TasksTab agent={agent} />
)}
{activeTab === 'config' && (
<ConfigTab agent={agent} onSave={onUpdate} />
)}
{activeTab === 'activity' && (
<ActivityTab agent={agent} />
)}
</div>
</div>
</div>
)
}
// 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<any>(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 (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-card border border-border rounded-lg max-w-md w-full p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-bold text-foreground">
Quick Spawn for {agent.name}
</h3>
<button onClick={onClose} className="text-muted-foreground hover:text-foreground text-2xl transition-smooth">×</button>
</div>
{spawnResult ? (
<div className="space-y-4">
<div className="bg-green-500/10 border border-green-500/20 text-green-400 p-3 rounded-lg text-sm">
Agent spawned successfully!
</div>
<div className="text-sm text-foreground/80">
<p><strong>Agent ID:</strong> {spawnResult.agentId}</p>
<p><strong>Session:</strong> {spawnResult.sessionId}</p>
<p><strong>Model:</strong> {spawnResult.model}</p>
</div>
</div>
) : (
<div className="space-y-4">
{/* Task Description */}
<div>
<label className="block text-sm font-medium text-foreground/80 mb-2">
Task Description *
</label>
<textarea
value={spawnData.task}
onChange={(e) => setSpawnData(prev => ({ ...prev, task: e.target.value }))}
placeholder={`Delegate a subtask to ${agent.name}...`}
className="w-full h-24 px-3 py-2 bg-surface-1 border border-border rounded text-foreground placeholder-muted-foreground focus:border-primary/50 focus:ring-1 focus:ring-primary/50 resize-none"
/>
</div>
{/* Model Selection */}
<div>
<label className="block text-sm font-medium text-foreground/80 mb-2">
Model
</label>
<select
value={spawnData.model}
onChange={(e) => setSpawnData(prev => ({ ...prev, model: e.target.value }))}
className="w-full px-3 py-2 bg-surface-1 border border-border rounded text-foreground focus:border-primary/50 focus:ring-1 focus:ring-primary/50"
>
{models.map(model => (
<option key={model.id} value={model.id}>
{model.name} - {model.cost} ({model.speed})
</option>
))}
</select>
</div>
{/* Agent Label */}
<div>
<label className="block text-sm font-medium text-foreground/80 mb-2">
Agent Label
</label>
<input
type="text"
value={spawnData.label}
onChange={(e) => setSpawnData(prev => ({ ...prev, label: e.target.value }))}
className="w-full px-3 py-2 bg-surface-1 border border-border rounded text-foreground focus:border-primary/50 focus:ring-1 focus:ring-primary/50"
/>
</div>
{/* Timeout */}
<div>
<label className="block text-sm font-medium text-foreground/80 mb-2">
Timeout (seconds)
</label>
<input
type="number"
value={spawnData.timeoutSeconds}
onChange={(e) => setSpawnData(prev => ({ ...prev, timeoutSeconds: parseInt(e.target.value) }))}
min={30}
max={3600}
className="w-full px-3 py-2 bg-surface-1 border border-border rounded text-foreground focus:border-primary/50 focus:ring-1 focus:ring-primary/50"
/>
</div>
{/* Action Buttons */}
<div className="flex gap-3 pt-4">
<button
onClick={handleSpawn}
disabled={isSpawning || !spawnData.task.trim()}
className="flex-1 px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-smooth"
>
{isSpawning ? 'Spawning...' : 'Spawn Agent'}
</button>
<button
onClick={onClose}
className="px-4 py-2 bg-secondary text-muted-foreground rounded-md hover:bg-surface-2 transition-smooth"
>
Cancel
</button>
</div>
</div>
)}
</div>
</div>
)
}
export default AgentSquadPanelPhase3