7da2dd60f0
Compact workbench panel, collapsible infra bar, narrower topology nodes with wider inter-stage gaps for visible connection lines.
127 lines
5.4 KiB
TypeScript
127 lines
5.4 KiB
TypeScript
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>
|
|
)
|
|
}
|