{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"agent-panel","type":"registry:ui","title":"AgentPanel","description":"Per-agent control panel.","package":"agent-integrations","dependencies":[],"registryDependencies":[],"files":[{"path":"components\/fancy\/agent-panel\/AgentPanel.tsx","content":"import { type CSSProperties, type ReactNode, useEffect, useRef } from \"react\";\r\n\r\nexport type AgentActivity = {\r\n  id: string;\r\n  \/** Wall-clock timestamp; component formats it. *\/\r\n  at: number;\r\n  \/** \"tool\" for MCP tool invocations, \"message\" for chat, \"info\" for status. *\/\r\n  kind: \"tool\" | \"message\" | \"info\" | \"error\";\r\n  \/** Short label, e.g. \"whiteboard_add_sticky\" or \"Agent\". *\/\r\n  source: string;\r\n  \/** Body text. *\/\r\n  text: string;\r\n  \/** Optional structured payload, rendered as collapsed JSON. *\/\r\n  detail?: unknown;\r\n};\r\n\r\nexport type AgentPanelProps = {\r\n  \/** The agent's identity (name + color appears in the header). *\/\r\n  agent?: { name?: string; color?: string };\r\n  \/** Activity stream. Most recent at the end. *\/\r\n  activity: AgentActivity[];\r\n  \/** Optional chat composer \u2014 pass an onSubmit to enable. *\/\r\n  onSubmit?: (message: string) => void;\r\n  \/** Disabled while a request is in flight. *\/\r\n  busy?: boolean;\r\n  \/** Right-rail header actions. *\/\r\n  actions?: ReactNode;\r\n  className?: string;\r\n  style?: CSSProperties;\r\n};\r\n\r\n\/**\r\n * AgentPanel \u2014 sidebar showing the agent's identity, a tool-and-chat log,\r\n * and an optional input composer. Pure presentational: hosts feed it the\r\n * activity stream from their own state (typically the MCP transport log).\r\n *\/\r\nexport function AgentPanel({ agent, activity, onSubmit, busy, actions, className, style }: AgentPanelProps) {\r\n  const scrollRef = useRef<HTMLDivElement>(null);\r\n  const inputRef = useRef<HTMLTextAreaElement>(null);\r\n\r\n  useEffect(() => {\r\n    const el = scrollRef.current;\r\n    if (!el) return;\r\n    el.scrollTop = el.scrollHeight;\r\n  }, [activity.length]);\r\n\r\n  const handleSubmit = (e: React.FormEvent) => {\r\n    e.preventDefault();\r\n    const value = inputRef.current?.value.trim();\r\n    if (!value || !onSubmit) return;\r\n    onSubmit(value);\r\n    if (inputRef.current) inputRef.current.value = \"\";\r\n  };\r\n\r\n  const color = agent?.color ?? \"#a855f7\";\r\n  const name = agent?.name ?? \"Agent\";\r\n\r\n  return (\r\n    <div className={[\"fai-panel\", className ?? \"\"].filter(Boolean).join(\" \")} style={style}>\r\n      <header className=\"fai-panel__header\">\r\n        <div\r\n          className=\"fai-panel__avatar\"\r\n          style={{ background: color }}\r\n          aria-hidden\r\n        >\r\n          {name.slice(0, 1)}\r\n        <\/div>\r\n        <div className=\"fai-panel__title\">\r\n          <strong>{name}<\/strong>\r\n          <span className=\"fai-panel__subtitle\">\r\n            {busy ? \"Working\u2026\" : `${activity.length} event${activity.length === 1 ? \"\" : \"s\"}`}\r\n          <\/span>\r\n        <\/div>\r\n        {actions && <div className=\"fai-panel__actions\">{actions}<\/div>}\r\n      <\/header>\r\n\r\n      <div ref={scrollRef} className=\"fai-panel__stream\">\r\n        {activity.length === 0 ? (\r\n          <p className=\"fai-panel__empty\">No activity yet.<\/p>\r\n        ) : (\r\n          activity.map((a) => <ActivityRow key={a.id} item={a} \/>)\r\n        )}\r\n      <\/div>\r\n\r\n      {onSubmit && (\r\n        <form className=\"fai-panel__composer\" onSubmit={handleSubmit}>\r\n          <textarea\r\n            ref={inputRef}\r\n            className=\"fai-panel__input\"\r\n            placeholder={busy ? \"Working\u2026\" : \"Ask the agent\u2026\"}\r\n            disabled={busy}\r\n            rows={2}\r\n            onKeyDown={(e) => {\r\n              if (e.key === \"Enter\" && !e.shiftKey) {\r\n                e.preventDefault();\r\n                handleSubmit(e as unknown as React.FormEvent);\r\n              }\r\n            }}\r\n          \/>\r\n          <button type=\"submit\" className=\"fai-panel__send\" disabled={busy}>\r\n            Send\r\n          <\/button>\r\n        <\/form>\r\n      )}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nfunction ActivityRow({ item }: { item: AgentActivity }) {\r\n  const time = formatTime(item.at);\r\n  return (\r\n    <div className={`fai-row fai-row--${item.kind}`}>\r\n      <div className=\"fai-row__meta\">\r\n        <span className=\"fai-row__source\">{item.source}<\/span>\r\n        <span className=\"fai-row__time\">{time}<\/span>\r\n      <\/div>\r\n      <div className=\"fai-row__text\">{item.text}<\/div>\r\n      {item.detail !== undefined && (\r\n        <details className=\"fai-row__detail\">\r\n          <summary>details<\/summary>\r\n          <pre>{safeJson(item.detail)}<\/pre>\r\n        <\/details>\r\n      )}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nfunction formatTime(at: number): string {\r\n  const d = new Date(at);\r\n  const hh = d.getHours().toString().padStart(2, \"0\");\r\n  const mm = d.getMinutes().toString().padStart(2, \"0\");\r\n  const ss = d.getSeconds().toString().padStart(2, \"0\");\r\n  return `${hh}:${mm}:${ss}`;\r\n}\r\n\r\nfunction safeJson(v: unknown): string {\r\n  try {\r\n    return JSON.stringify(v, null, 2);\r\n  } catch {\r\n    return String(v);\r\n  }\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/agent-panel\/AgentPanel.tsx"},{"path":"components\/fancy\/agent-panel\/index.ts","content":"export { AgentPanel } from \".\/AgentPanel\";\r\nexport type { AgentPanelProps, AgentActivity } from \".\/AgentPanel\";\r\n","type":"registry:ui","target":"components\/fancy\/agent-panel\/index.ts"}]}