Files
atc-agents/ui/src/components/features/AgentFleet.tsx
T

127 lines
5.4 KiB
TypeScript
Raw Normal View History

import { ShieldCheck } from 'lucide-react'
import type { Agent, AgentAnim } from '../../types'
import type { AgentLoad } from '../../hooks/useLiveMetrics'
import { agentTaskLabel, getAgentMeta } from '../../lib/agentMeta'
import { cn } from '../../lib/utils'
type Props = {
agents: Agent[]
animations: Record<string, AgentAnim>
selectedId: string | null
loads: Record<string, AgentLoad>
approvalCount: number
onSelect: (id: string) => void
onOpenApprovals: () => void
}
function AgentCard({
agent,
anim,
load,
selected,
onSelect,
}: {
agent: Agent
anim?: AgentAnim
load?: AgentLoad
selected: boolean
onSelect: () => void
}) {
const meta = getAgentMeta(agent.id)
const Icon = meta.icon
const busy = anim && anim.state !== 'idle'
return (
<button
type="button"
onClick={onSelect}
className={cn(
'flex h-[112px] w-[142px] shrink-0 flex-col gap-1 rounded-lg border bg-surface-raised p-1.5 text-left shadow-sm dark:bg-surface-overlay',
selected ? 'border-docker ring-2 ring-docker/30 shadow-[0_0_20px_rgba(13,183,237,0.15)]' : 'border-border hover:border-docker/30',
)}
>
<div className="flex items-center gap-1.5">
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-surface" style={{ color: meta.accent }}>
<Icon className="h-3.5 w-3.5" />
</span>
<div className="min-w-0 flex-1">
<p className="truncate text-[11px] font-semibold text-foreground">{agent.name.split(' ·')[0]}</p>
<p className="truncate text-[8px] text-foreground-muted">{meta.domain}</p>
</div>
<span className={cn('h-1.5 w-1.5 shrink-0 rounded-full', busy ? 'bg-success animate-pulse' : 'bg-foreground-faint/30')} />
</div>
{selected && (
<span className="rounded border border-docker/40 bg-docker/10 px-1.5 py-0.5 text-[7px] font-semibold uppercase tracking-wide text-docker">
Terminal open
</span>
)}
<p className="line-clamp-2 text-[8px] leading-[10px] text-foreground-muted">{agent.role}</p>
<p className="h-[20px] line-clamp-2 text-[8px] leading-[10px] text-foreground-faint">{agentTaskLabel(agent.id, anim)}</p>
<div className="mt-auto space-y-0.5">
<div className="flex justify-between font-mono text-[7px] tabular-nums text-foreground-faint">
<span>CPU {load?.cpu ?? 0}%</span>
<span>MEM {load?.mem ?? 0}%</span>
</div>
<div className="h-1 overflow-hidden rounded-full bg-surface">
<div className="h-full rounded-full transition-[width] duration-700" style={{ width: `${load?.cpu ?? 0}%`, background: meta.accent }} />
</div>
</div>
</button>
)
}
export function AgentFleet({ agents, animations, selectedId, loads, approvalCount, onSelect, onOpenApprovals }: Props) {
const supervisors = agents.filter((a) => a.supervisor)
const operators = agents.filter((a) => !a.supervisor && a.id !== 'mcp-coordinator')
const mcp = agents.find((a) => a.id === 'mcp-coordinator')
return (
<div className="panel flex shrink-0 flex-col p-2">
<div className="mb-1 flex shrink-0 items-center justify-between gap-2">
<div className="min-w-0">
<h3 className="text-[9px] font-semibold uppercase tracking-wider text-foreground-muted">Agent Fleet</h3>
<p className="truncate text-[8px] text-foreground-faint">Click agent dedicated terminal opens below</p>
</div>
<div className="flex shrink-0 items-center gap-1.5">
<button
type="button"
onClick={onOpenApprovals}
className={cn(
'flex items-center gap-1 rounded-md border px-2 py-1 text-[9px] font-medium',
approvalCount > 0 ? 'border-warning/40 bg-warning/10 text-warning' : 'border-border text-foreground-muted hover:bg-surface-overlay',
)}
>
<ShieldCheck className="h-3 w-3" />
Approvals{approvalCount > 0 ? ` (${approvalCount})` : ''}
</button>
<span className="font-mono text-[8px] text-foreground-faint">{agents.length} agents</span>
</div>
</div>
<div className="scroll-x-stable flex min-h-0 gap-3 pb-1">
<div className="shrink-0">
<p className="mb-1 text-[8px] uppercase tracking-widest text-foreground-faint">Supervisors</p>
<div className="flex gap-1.5">
{supervisors.map((a) => (
<AgentCard key={a.id} agent={a} anim={animations[a.id]} load={loads[a.id]} selected={selectedId === a.id} onSelect={() => onSelect(a.id)} />
))}
</div>
</div>
{mcp && (
<div className="shrink-0">
<p className="mb-1 text-[8px] uppercase tracking-widest text-foreground-faint">MCP Hub</p>
<AgentCard agent={mcp} anim={animations[mcp.id]} load={loads[mcp.id]} selected={selectedId === mcp.id} onSelect={() => onSelect(mcp.id)} />
</div>
)}
<div className="shrink-0">
<p className="mb-1 text-[8px] uppercase tracking-widest text-foreground-faint">Field Operators</p>
<div className="flex gap-1.5">
{operators.map((a) => (
<AgentCard key={a.id} agent={a} anim={animations[a.id]} load={loads[a.id]} selected={selectedId === a.id} onSelect={() => onSelect(a.id)} />
))}
</div>
</div>
</div>
</div>
)
}