{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-10",
  "title": "Agent 10: console with handoff",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "An agent console: the conversation with each tool call as a step you can open to see what was asked and what came back, Stop while it works, Enter to send, and a clear Take over and Hand back so a person is never mistaken for the agent.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/agent-10/agent-10.tsx",
      "content": "// Ballmac UI: Agent 10. https://ui.ballmac.com/blocks/agent-10\n\"use client\"\n\nimport * as React from \"react\"\nimport { CircleAlert, Hand, Loader2, SendHorizontal, Square, Wrench } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { cn } from \"@/lib/utils\"\n\ntype Agent10Tool = { name: string; /** What it was asked, in words. */ input: string; /** What came back, in words. */ output?: string; status: \"running\" | \"done\" | \"failed\" }\ntype Agent10Message =\n  | { id: string; role: \"user\"; text: string; at?: string }\n  | { id: string; role: \"agent\"; text: string; at?: string; tools?: Agent10Tool[] }\n  | { id: string; role: \"human\"; text: string; at?: string; name: string }\n\ntype Agent10Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    messages?: Agent10Message[]\n    title?: string\n    /** True while the agent is working. Shows Stop in place of Send. */\n    running?: boolean\n    /** Whether a person has taken over. The agent stays silent until it is handed back. */\n    humanActive?: boolean\n    onSend?: (text: string) => void | Promise<void>\n    onStop?: () => void\n    /** Called to take over from, or hand back to, the agent. */\n    onHandoff?: (takeOver: boolean) => void | Promise<void>\n  }\n\nconst messages: Agent10Message[] = [\n  { id: \"1\", role: \"user\", text: \"Why did my refund for order 4821 not arrive?\", at: \"10:02\" },\n  {\n    id: \"2\",\n    role: \"agent\",\n    text: \"The refund was issued on 14 October, but it was sent to a card that has since expired, so the bank returned it. I can re-issue it to the card on file, or to store credit instantly.\",\n    at: \"10:02\",\n    tools: [\n      { name: \"lookup_order\", input: \"Order 4821\", output: \"Delivered 6 Oct, refund requested 12 Oct\", status: \"done\" },\n      { name: \"check_refund\", input: \"Refund for order 4821\", output: \"Issued 14 Oct, returned by the bank (card expired)\", status: \"done\" },\n    ],\n  },\n  { id: \"3\", role: \"user\", text: \"Store credit please. And can someone confirm it by email?\", at: \"10:03\" },\n]\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"grid gap-4\">\n      <Skeleton className=\"ms-auto h-10 w-2/3 rounded-2xl\" />\n      <Skeleton className=\"h-28 w-5/6 rounded-2xl\" />\n      <Skeleton className=\"ms-auto h-10 w-1/2 rounded-2xl\" />\n    </div>\n  )\n}\n\nconst TOOL_LABEL = { running: \"Working\", done: \"Done\", failed: \"Failed\" } as const\n\n/**\n * An agent console: the conversation with each tool call shown as a step you can open, Stop while the agent works, and a clear way for a person to take over and hand back.\n * Who said what is written, not only coloured; a person's reply is marked so it is never mistaken for the agent; sending is refused with an empty message.\n */\nfunction Agent10({ messages: list = messages, title = \"Support agent\", running = false, humanActive = false, onSend, onStop, onHandoff, state = \"ready\", onRetry, emptyState, errorState, className, ...props }: Agent10Props) {\n  const id = React.useId()\n  const [text, setText] = React.useState(\"\")\n  const [busy, setBusy] = React.useState(false)\n  const [error, setError] = React.useState<string | null>(null)\n  const [human, setHuman] = React.useState(humanActive)\n  const [local, setLocal] = React.useState<Agent10Message[]>([])\n  const end = React.useRef<HTMLDivElement>(null)\n  const view = state === \"ready\" && list.length === 0 && local.length === 0 ? \"empty\" : state\n  const all = [...list, ...local]\n\n  React.useEffect(() => { end.current?.scrollIntoView?.({ block: \"end\" }) }, [all.length])\n\n  async function send(e: React.FormEvent) {\n    e.preventDefault()\n    const t = text.trim()\n    if (!t) { setError(\"Write a message first.\"); return }\n    setBusy(true)\n    setError(null)\n    try {\n      await onSend?.(t)\n      setLocal((l) => [...l, human ? { id: `l${l.length}`, role: \"human\", name: \"You\", text: t } : { id: `l${l.length}`, role: \"user\", text: t }])\n      setText(\"\")\n    } catch (err) {\n      setError(err instanceof Error ? err.message : \"The message was not sent. It is still here.\")\n    } finally {\n      setBusy(false)\n    }\n  }\n  async function handoff() {\n    const next = !human\n    setError(null)\n    try {\n      await onHandoff?.(next)\n      setHuman(next)\n    } catch (err) {\n      setError(err instanceof Error ? err.message : \"The handoff did not go through.\")\n    }\n  }\n\n  return (\n    <section data-slot=\"agent-10\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto flex w-full max-w-3xl flex-col px-4 py-10 sm:px-6\", className)} {...frameProps(props)}>\n      <header className=\"mb-5 flex flex-wrap items-center justify-between gap-3\">\n        <div>\n          <h1 id={`${id}-title`} className=\"text-2xl font-semibold tracking-[-0.03em]\">{title}</h1>\n          <p aria-live=\"polite\" className=\"text-muted-foreground mt-0.5 text-sm\">{human ? \"A person is handling this. The agent is paused.\" : running ? \"The agent is working…\" : \"The agent is answering.\"}</p>\n        </div>\n        <Button variant={human ? \"default\" : \"outline\"} onClick={() => void handoff()} disabled={state !== \"ready\"}>\n          <Hand aria-hidden=\"true\" /> {human ? \"Hand back to the agent\" : \"Take over\"}\n        </Button>\n      </header>\n\n      <StateView state={view} onRetry={onRetry} label=\"Loading the conversation\" loading={<Skel />} empty={{ title: \"No messages yet\", description: \"Start the conversation below.\", ...emptyState }} error={{ title: \"The conversation could not be loaded\", description: \"Nothing was sent. Check your connection and try again.\", ...errorState }}>\n        <div className=\"bg-card flex min-h-0 flex-col rounded-2xl border\">\n          <div role=\"log\" aria-label=\"Conversation\" aria-live=\"polite\" tabIndex={0} className=\"focus-visible:ring-ring/50 max-h-[32rem] overflow-y-auto rounded-t-2xl p-4 outline-none focus-visible:ring-[3px] sm:p-5\">\n            <ol className=\"grid gap-4\">\n              {all.map((m) => (\n                <li key={m.id} className={cn(\"flex flex-col gap-1.5\", m.role === \"user\" || (m.role === \"human\" && m.name === \"You\") ? \"items-end\" : \"items-start\")}>\n                  <span className=\"text-muted-foreground text-xs\">\n                    {m.role === \"user\" ? \"You\" : m.role === \"agent\" ? \"Agent\" : `${m.name} (a person)`}{m.at ? ` · ${m.at}` : \"\"}\n                  </span>\n                  <div className={cn(\"max-w-[85%] rounded-2xl px-4 py-2.5 text-sm leading-relaxed text-pretty\", m.role === \"user\" ? \"bg-foreground text-background\" : m.role === \"human\" ? \"border-foreground border-2\" : \"bg-muted\")}>{m.text}</div>\n                  {m.role === \"agent\" && m.tools?.length ? (\n                    <ul aria-label=\"Steps the agent took\" className=\"grid w-full max-w-[85%] gap-1.5\">\n                      {m.tools.map((t, i) => (\n                        <li key={i}>\n                          <details className=\"group rounded-lg border text-sm\">\n                            <summary className=\"focus-visible:ring-ring/50 flex cursor-pointer list-none items-center gap-2 rounded-lg px-3 py-2 outline-none focus-visible:ring-[3px] [&::-webkit-details-marker]:hidden\">\n                              {t.status === \"running\" ? <Loader2 aria-hidden=\"true\" className=\"size-3.5 animate-spin motion-reduce:animate-none\" /> : t.status === \"failed\" ? <CircleAlert aria-hidden=\"true\" className=\"text-destructive size-3.5\" /> : <Wrench aria-hidden=\"true\" className=\"size-3.5\" />}\n                              <span className=\"font-mono text-xs\">{t.name}</span>\n                              <span className={cn(\"ms-auto text-xs\", t.status === \"failed\" ? \"text-destructive font-medium\" : \"text-muted-foreground\")}>{TOOL_LABEL[t.status]}</span>\n                            </summary>\n                            <dl className=\"text-muted-foreground grid gap-1 border-t px-3 py-2 text-xs\">\n                              <div><dt className=\"inline font-medium\">Asked: </dt><dd className=\"inline\">{t.input}</dd></div>\n                              {t.output && <div><dt className=\"inline font-medium\">Got: </dt><dd className=\"inline\">{t.output}</dd></div>}\n                            </dl>\n                          </details>\n                        </li>\n                      ))}\n                    </ul>\n                  ) : null}\n                </li>\n              ))}\n            </ol>\n            <div ref={end} />\n          </div>\n\n          <form onSubmit={(e) => void send(e)} noValidate className=\"border-t p-3\">\n            <label htmlFor={`${id}-msg`} className=\"sr-only\">{human ? \"Reply as a person\" : \"Message the agent\"}</label>\n            <div className=\"flex items-end gap-2\">\n              <textarea id={`${id}-msg`} rows={2} value={text} onChange={(e) => { setText(e.target.value); setError(null) }} onKeyDown={(e) => { if (e.key === \"Enter\" && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); e.currentTarget.form?.requestSubmit() } }} aria-invalid={!!error} aria-describedby={error ? `${id}-err` : undefined} placeholder={human ? \"Reply as a person…\" : \"Message the agent…\"} className=\"bg-background focus-visible:ring-ring/50 aria-[invalid=true]:border-destructive min-h-11 flex-1 resize-none rounded-xl border px-3 py-2.5 text-sm outline-none focus-visible:ring-[3px]\" />\n              {running && !human ? (\n                <Button type=\"button\" variant=\"outline\" size=\"lg\" onClick={onStop}><Square aria-hidden=\"true\" /> Stop</Button>\n              ) : (\n                <Button type=\"submit\" size=\"lg\" disabled={busy}>{busy ? <Loader2 aria-hidden=\"true\" className=\"animate-spin\" /> : <SendHorizontal aria-hidden=\"true\" className=\"rtl:-scale-x-100\" />} Send</Button>\n              )}\n            </div>\n            <p id={`${id}-err`} role={error ? \"alert\" : undefined} className=\"text-destructive mt-2 min-h-5 text-sm font-medium\">{error}</p>\n            <p className=\"text-muted-foreground text-xs\">Enter sends. Shift and Enter adds a new line.</p>\n          </form>\n        </div>\n      </StateView>\n    </section>\n  )\n}\n\nexport { Agent10, type Agent10Message, type Agent10Props, type Agent10Tool }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/agent-10/agent-10.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "ai",
      "agent",
      "console",
      "chat",
      "handoff",
      "human in the loop",
      "tool calls",
      "support",
      "transcript"
    ],
    "version": "1.0.0",
    "updated": "2026-10-24",
    "tests": true,
    "ai": {
      "summary": "Agent console. <Agent10 messages={[{ id, role: 'user'|'agent'|'human', text, at?, name? (human), tools?: [{ name, input, output?, status: 'running'|'done'|'failed' }] }]} running humanActive onSend={(text) => Promise} onStop={() => void} onHandoff={(takeOver: boolean) => Promise} />. A person's reply is labelled '(a person)' and framed differently. While a person is active the agent is shown as paused.",
      "whenToUse": [
        "Operating or supervising a support or task agent",
        "Any transcript where tool calls and a human takeover matter"
      ],
      "whenNotToUse": [
        "A public chat widget; use ai-chat-1",
        "Reviewing finished runs; use agent-3"
      ],
      "composesWith": [
        "agent-1",
        "agent-2",
        "agent-9",
        "escalation-card"
      ],
      "a11y": [
        {
          "keys": "Log",
          "action": "A focusable log region that announces new messages politely"
        },
        {
          "keys": "Sender",
          "action": "Who said what is written above every message, including '(a person)'"
        },
        {
          "keys": "Steps",
          "action": "Each tool call is a disclosure with its status in words; failures are marked in words"
        },
        {
          "keys": "Enter",
          "action": "Sends; Shift and Enter adds a line; an empty send is an alert"
        },
        {
          "keys": "Handoff",
          "action": "A button that says Take over or Hand back; the state is announced"
        }
      ],
      "customization": [
        "messages, title, running, humanActive",
        "onSend, onStop, onHandoff"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "agent-10-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton bubbles under the real heading, announced as loading."
      },
      {
        "id": "empty",
        "title": "No messages yet",
        "example": "agent-10-empty",
        "trigger": "state=\"empty\"",
        "note": "Says there are no messages yet; emptyState sets the copy."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "agent-10-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying nothing was sent, with Try again (onRetry). A message that fails to send stays in the box and says why."
      }
    ],
    "examples": [
      "agent-10-demo",
      "agent-10-working",
      "agent-10-human",
      "agent-10-loading",
      "agent-10-empty",
      "agent-10-error"
    ],
    "url": "https://ui.ballmac.com/components/agent-10"
  },
  "categories": [
    "blocks",
    "agents"
  ],
  "type": "registry:block"
}