{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"agent-handoff","type":"registry:ui","title":"Agent Handoff","description":"One agent passing work to another — who asked, what was carried across, and what the receiving agent was not told.","author":"Scrim UI (https://scrimui.dev)","categories":["agents"],"docs":"https://scrimui.dev/components/agent-handoff","dependencies":[],"registryDependencies":[],"files":[{"path":"src/showcase/agent-handoff/agent-handoff.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * One agent handing work to another.\n *\n * Every multi-agent demo shows the arrow. The arrow is not the problem.\n *\n * **The failure mode of multi-agent systems is context loss**, and it is\n * silent. The receiving agent gets a task description and none of the six\n * things the first agent learned on the way to writing it, then confidently\n * redoes work, or answers a question that was already settled, or asks the\n * user something it was told two turns ago. Nothing errors. The output is\n * merely worse than it should be, in a way that is almost impossible to\n * attribute afterwards.\n *\n * So this component's argument is that **what was NOT carried across is the\n * part worth rendering.** `carried` is reassuring and mostly ignorable;\n * `withheld` is where the next bug comes from, and putting it on screen is\n * how a reader catches it while the run is still cheap to fix.\n *\n * **A handoff is a return trip.** Rendering it as a one-way arrow loses the\n * question of what came back and whether the caller is still waiting. Hence\n * `returned`, with the result, rather than two unrelated cards.\n *\n * A handoff is also a good place to be honest about cost: it is one of the\n * few architectural decisions that silently multiplies token spend, since the\n * receiving agent re-reads whatever context it was given.\n */\n\nexport type HandoffState = \"handing-off\" | \"accepted\" | \"returned\" | \"failed\";\n\nexport type AgentHandoffProps = {\n  from: string;\n  to: string;\n  /** Why this agent and not the one already running. One line. */\n  reason?: string;\n  /** The task as the receiving agent will see it. */\n  task: string;\n  /** Context carried across. */\n  carried?: string[];\n  /**\n   * Context deliberately or accidentally left behind. The interesting half:\n   * this is where the next silent failure comes from.\n   */\n  withheld?: string[];\n  state?: HandoffState;\n  /** What came back, once it did. */\n  result?: string;\n  className?: string;\n};\n\n/* ------------------------------------------------------------------ */\n/* Icons                                                               */\n/* ------------------------------------------------------------------ */\n\nfunction ArrowIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"14\" height=\"14\" {...props}>\n      <path d=\"M5 12h14M13 6l6 6-6 6\" />\n    </svg>\n  );\n}\n\nfunction ReturnIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"14\" height=\"14\" {...props}>\n      <path d=\"M19 12H5M11 6l-6 6 6 6\" />\n    </svg>\n  );\n}\n\nconst STATE_LABEL: Record<HandoffState, string> = {\n  \"handing-off\": \"handing off\",\n  accepted: \"working\",\n  returned: \"returned\",\n  failed: \"failed\",\n};\n\n/* ------------------------------------------------------------------ */\n/* AgentHandoff                                                        */\n/* ------------------------------------------------------------------ */\n\nexport function AgentHandoff({\n  from,\n  to,\n  reason,\n  task,\n  carried = [],\n  withheld = [],\n  state = \"accepted\",\n  result,\n  className = \"\",\n}: AgentHandoffProps) {\n  const [openWithheld, setOpenWithheld] = React.useState(false);\n\n  return (\n    <div\n      className={`rounded-xl border bg-white dark:bg-zinc-900 ${\n        state === \"failed\"\n          ? \"border-red-200 dark:border-red-900/60\"\n          : \"border-zinc-200 dark:border-zinc-800\"\n      } ${className}`}\n    >\n      <div className=\"flex flex-wrap items-center gap-2 border-b border-zinc-100 px-3.5 py-2.5 dark:border-zinc-800\">\n        <span className=\"font-mono text-[11px] text-zinc-600 dark:text-zinc-300\">{from}</span>\n        <span className={`shrink-0 ${state === \"returned\" ? \"text-emerald-600 dark:text-emerald-400\" : \"text-zinc-400 dark:text-zinc-500\"}`}>\n          {state === \"returned\" ? <ReturnIcon /> : <ArrowIcon />}\n        </span>\n        <span className=\"font-mono text-[11px] font-medium text-zinc-900 dark:text-zinc-100\">{to}</span>\n        <span\n          className={`ml-auto inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[10px] font-medium ${\n            state === \"returned\"\n              ? \"bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-400\"\n              : state === \"failed\"\n                ? \"bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400\"\n                : \"bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-300\"\n          }`}\n        >\n          {state === \"accepted\" && <span className=\"h-1.5 w-1.5 animate-pulse rounded-full bg-current\" />}\n          {STATE_LABEL[state]}\n        </span>\n      </div>\n\n      <div className=\"px-3.5 py-3\">\n        {reason && (\n          <p className=\"mb-2 text-[11px] text-zinc-500 dark:text-zinc-400\">{reason}</p>\n        )}\n\n        <p className=\"rounded-lg bg-zinc-50 px-2.5 py-2 text-[13px] leading-5 text-zinc-700 dark:bg-zinc-800/60 dark:text-zinc-200\">\n          {task}\n        </p>\n\n        {carried.length > 0 && (\n          <div className=\"mt-2.5\">\n            <p className=\"text-[10px] font-medium uppercase tracking-wide text-zinc-400 dark:text-zinc-500\">\n              Carried across\n            </p>\n            <ul className=\"mt-1 space-y-0.5\">\n              {carried.map((item) => (\n                <li key={item} className=\"flex gap-1.5 text-[12px] leading-5 text-zinc-600 dark:text-zinc-300\">\n                  <span className=\"mt-2 h-1 w-1 shrink-0 rounded-full bg-zinc-300 dark:bg-zinc-600\" />\n                  {item}\n                </li>\n              ))}\n            </ul>\n          </div>\n        )}\n\n        {withheld.length > 0 && (\n          <div className=\"mt-2.5\">\n            <button\n              type=\"button\"\n              onClick={() => setOpenWithheld((v) => !v)}\n              aria-expanded={openWithheld}\n              className=\"inline-flex items-center gap-1.5 rounded-md text-[10px] font-medium uppercase tracking-wide text-amber-600 transition-colors hover:text-amber-700 dark:text-amber-500 dark:hover:text-amber-400\"\n            >\n              <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"10\" height=\"10\" className={openWithheld ? \"rotate-180\" : \"\"}>\n                <path d=\"m6 9 6 6 6-6\" />\n              </svg>\n              Not carried across ({withheld.length})\n            </button>\n            {openWithheld && (\n              <ul className=\"mt-1 space-y-0.5\">\n                {withheld.map((item) => (\n                  <li key={item} className=\"flex gap-1.5 text-[12px] leading-5 text-zinc-500 dark:text-zinc-400\">\n                    <span className=\"mt-2 h-1 w-1 shrink-0 rounded-full bg-amber-400\" />\n                    {item}\n                  </li>\n                ))}\n              </ul>\n            )}\n          </div>\n        )}\n\n        {result && (\n          <div className=\"mt-3 border-t border-zinc-100 pt-2.5 dark:border-zinc-800\">\n            <p className=\"text-[10px] font-medium uppercase tracking-wide text-zinc-400 dark:text-zinc-500\">\n              Returned to {from}\n            </p>\n            <p className=\"mt-1 text-[13px] leading-5 text-zinc-700 dark:text-zinc-200\">{result}</p>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n","type":"registry:ui","target":"components/ui/agent-handoff.tsx"}]}