94 lines
3.5 KiB
TypeScript
94 lines
3.5 KiB
TypeScript
|
|
import { useEffect, useRef } from 'react'
|
||
|
|
import { Loader2, Terminal } from 'lucide-react'
|
||
|
|
import type { Agent, TerminalLine } from '../../types'
|
||
|
|
import { getAgentMeta } from '../../lib/agentMeta'
|
||
|
|
import { cn } from '../../lib/utils'
|
||
|
|
import { subTabIdle } from '../../lib/tabActive'
|
||
|
|
|
||
|
|
type Props = {
|
||
|
|
agent: Agent
|
||
|
|
lines: TerminalLine[]
|
||
|
|
busy: boolean
|
||
|
|
onSendPrompt: (message: string, agentId?: string) => void
|
||
|
|
}
|
||
|
|
|
||
|
|
const LEVEL: Record<string, string> = {
|
||
|
|
info: 'text-foreground-muted',
|
||
|
|
ok: 'text-success',
|
||
|
|
warn: 'text-warning',
|
||
|
|
err: 'text-danger',
|
||
|
|
cmd: 'text-docker',
|
||
|
|
llm: 'text-violet-400',
|
||
|
|
fetch: 'text-cyan-400',
|
||
|
|
probe: 'text-amber-300',
|
||
|
|
}
|
||
|
|
|
||
|
|
export function AgentWorkbench({ agent, lines, busy, onSendPrompt }: Props) {
|
||
|
|
const meta = getAgentMeta(agent.id)
|
||
|
|
const Icon = meta.icon
|
||
|
|
const bottomRef = useRef<HTMLDivElement>(null)
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||
|
|
}, [lines, busy])
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="flex h-full min-h-0 flex-col bg-[#0a0e14]">
|
||
|
|
<header className="flex shrink-0 items-center gap-3 border-b border-border/60 px-4 py-3">
|
||
|
|
<span
|
||
|
|
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border border-border/80 bg-surface-overlay"
|
||
|
|
style={{ color: meta.accent, boxShadow: `0 0 24px ${meta.accent}33` }}
|
||
|
|
>
|
||
|
|
<Icon className="h-6 w-6" />
|
||
|
|
</span>
|
||
|
|
<div className="min-w-0 flex-1">
|
||
|
|
<h3 className="truncate text-base font-bold text-foreground">{agent.name}</h3>
|
||
|
|
<p className="text-[11px] text-docker">{meta.domain} · {agent.zone}</p>
|
||
|
|
<p className="truncate text-[10px] text-foreground-muted">{agent.role}</p>
|
||
|
|
</div>
|
||
|
|
<div className="flex shrink-0 items-center gap-2">
|
||
|
|
{busy && (
|
||
|
|
<span className="flex items-center gap-1 text-[10px] text-success">
|
||
|
|
<Loader2 className="h-3 w-3 animate-spin" /> Live
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
<span className="flex items-center gap-1 rounded border border-emerald-500/40 bg-emerald-500/10 px-2 py-1 text-[10px] text-emerald-300">
|
||
|
|
<Terminal className="h-3 w-3" /> Agent terminal
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</header>
|
||
|
|
|
||
|
|
<div className="flex shrink-0 flex-wrap gap-1 border-b border-border/40 px-3 py-2">
|
||
|
|
{(agent.suggested_prompts || []).slice(0, 6).map((prompt) => (
|
||
|
|
<button
|
||
|
|
key={prompt}
|
||
|
|
type="button"
|
||
|
|
onClick={() => onSendPrompt(prompt, agent.id)}
|
||
|
|
className={cn('rounded border border-border/60 px-2 py-1 text-[10px] text-foreground-muted hover:border-docker/40 hover:text-docker', subTabIdle)}
|
||
|
|
>
|
||
|
|
{prompt}
|
||
|
|
</button>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="scrollbar-thin min-h-0 flex-1 overflow-y-auto px-4 py-2 font-mono text-[11px] leading-relaxed">
|
||
|
|
{lines.length === 0 && (
|
||
|
|
<p className="py-8 text-center text-foreground-faint">
|
||
|
|
Agent terminal ready — probe output and LLM responses appear here
|
||
|
|
</p>
|
||
|
|
)}
|
||
|
|
{lines.map((line) => (
|
||
|
|
<div key={line.id} className={LEVEL[line.level] || 'text-foreground-muted'}>
|
||
|
|
<span className="text-foreground-faint/50">
|
||
|
|
{line.ts ? new Date(line.ts).toLocaleTimeString('en-US', { hour12: false }) : ''}
|
||
|
|
</span>{' '}
|
||
|
|
<span className="text-docker/70">[{line.phase}]</span> {line.text}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
{busy && <span className="text-docker animate-pulse">█</span>}
|
||
|
|
<div ref={bottomRef} />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|