{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-minimal",
  "title": "Mastra Chat (minimal)",
  "description": "Embeddable Agent Controller chat — conversation, composer, tool approvals and ask_user, with no sidebar or workbench.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@mastra-chat-kit/tool",
    "@mastra-chat-kit/chat-tool-views",
    "@mastra-chat-kit/chat-engine",
    "https://ai-sdk.dev/elements/api/registry/confirmation.json",
    "https://ai-sdk.dev/elements/api/registry/conversation.json",
    "https://ai-sdk.dev/elements/api/registry/message.json",
    "https://ai-sdk.dev/elements/api/registry/reasoning.json",
    "https://ai-sdk.dev/elements/api/registry/shimmer.json",
    "@mastra-chat-kit/chat-routes"
  ],
  "files": [
    {
      "path": "components/chat-minimal/minimal-chat.tsx",
      "content": "'use client';\n\nimport { SendIcon, SquareIcon } from 'lucide-react';\nimport { type FormEvent, useState } from 'react';\nimport {\n  Confirmation,\n  ConfirmationAction,\n  ConfirmationActions,\n  ConfirmationRequest,\n  ConfirmationTitle,\n} from '@/components/ai-elements/confirmation';\nimport { Conversation, ConversationContent } from '@/components/ai-elements/conversation';\nimport { Message, MessageContent, MessageResponse } from '@/components/ai-elements/message';\nimport { Reasoning, ReasoningContent, ReasoningTrigger } from '@/components/ai-elements/reasoning';\nimport { Shimmer } from '@/components/ai-elements/shimmer';\nimport {\n  Tool,\n  ToolContent,\n  ToolHeader,\n  ToolInput,\n  ToolOutput,\n} from '@/components/ai-elements/tool';\nimport { AskUserPrompt, GeneratedImage } from '@/components/chat/tool-views';\nimport type { AgentControllerContentPart } from '@/lib/agent-controller/events';\nimport { useAgentControllerChat } from '@/lib/agent-controller/use-agent-controller-chat';\nimport { cn } from '@/lib/utils';\n\n/**\n * A SECOND skin over the same Agent Controller engine (bd 23d).\n *\n * The point of this file is that it is small. It shares nothing with the full\n * `chat` shell except the engine (`useAgentControllerChat`) and the shared tool\n * renderers — no sidebar, no workbench, no model picker, its own plain composer —\n * yet it drives the identical session: same threads, same approvals, same\n * subagents, same workspace. Changing the look does not cost you the harness.\n *\n * What is NOT optional, and why: every tool is approval-gated by the controller,\n * and `ask_user` suspends the run. A skin that omits <Confirmation> or\n * <AskUserPrompt> leaves the agent parked forever with no way to continue. Layout\n * is a choice; those two are the contract.\n *\n * Drop it in anywhere — a panel, a modal, a corner of an existing app:\n *   <MinimalChat />\n */\nexport function MinimalChat({ className }: { className?: string }) {\n  const { transcript, status, sendMessage, approve, answerQuestion, pendingSuspension } =\n    useAgentControllerChat();\n  const [input, setInput] = useState('');\n\n  const busy = status === 'streaming';\n  const { pendingApproval } = transcript;\n\n  const onSubmit = (e: FormEvent) => {\n    e.preventDefault();\n    const text = input.trim();\n    if (!text || busy) return;\n    setInput('');\n    void sendMessage(text);\n  };\n\n  return (\n    <div className={cn('flex h-full min-h-0 flex-col', className)}>\n      <Conversation className=\"min-h-0 flex-1\">\n        <ConversationContent className=\"mx-auto w-full max-w-2xl\">\n          {transcript.messages.length === 0 && (\n            <p className=\"py-12 text-center text-muted-foreground text-sm\">\n              Ask the agent anything.\n            </p>\n          )}\n\n          {transcript.messages.map((m) => {\n            // tool_result parts arrive separately from their tool_call; pair them by id\n            // so a finished call renders with its output (same rule as the full shell).\n            const resultsById = new Map(\n              m.content\n                .filter((p) => p.type === 'tool_result')\n                .map((p) => [\n                  (p as { id: string }).id,\n                  p as { result?: unknown; isError?: boolean },\n                ]),\n            );\n            return (\n              <Message key={m.id} from={m.role === 'user' ? 'user' : 'assistant'}>\n                <MessageContent>\n                  {m.content.map((part, i) => (\n                    // biome-ignore lint/suspicious/noArrayIndexKey: content is append-only; text/thinking parts carry no id\n                    <Part key={`${m.id}-${i}`} part={part} resultsById={resultsById} />\n                  ))}\n                </MessageContent>\n              </Message>\n            );\n          })}\n\n          {busy && transcript.messages.at(-1)?.role === 'user' && (\n            <Shimmer className=\"text-muted-foreground text-sm\">Thinking…</Shimmer>\n          )}\n\n          {/* The agent asked a question; the run stays suspended until it's answered. */}\n          {pendingSuspension && (\n            <AskUserPrompt suspension={pendingSuspension} onAnswer={answerQuestion} />\n          )}\n\n          {/* Every tool is gated — without this the run parks forever. */}\n          {pendingApproval && (\n            <Confirmation state=\"approval-requested\" approval={{ id: pendingApproval.toolCallId }}>\n              <ConfirmationTitle>Run {pendingApproval.toolName}?</ConfirmationTitle>\n              <ConfirmationRequest>\n                <pre className=\"overflow-x-auto text-xs\">\n                  {JSON.stringify(pendingApproval.args, null, 2)}\n                </pre>\n                <ConfirmationActions>\n                  <ConfirmationAction onClick={() => approve('approve')}>\n                    Approve\n                  </ConfirmationAction>\n                  <ConfirmationAction variant=\"outline\" onClick={() => approve('decline')}>\n                    Reject\n                  </ConfirmationAction>\n                </ConfirmationActions>\n              </ConfirmationRequest>\n            </Confirmation>\n          )}\n        </ConversationContent>\n      </Conversation>\n\n      <form onSubmit={onSubmit} className=\"mx-auto flex w-full max-w-2xl gap-2 p-3\">\n        <input\n          value={input}\n          onChange={(e) => setInput(e.target.value)}\n          placeholder=\"Ask anything…\"\n          aria-label=\"Message\"\n          className=\"h-10 flex-1 rounded-lg border border-border bg-background px-3 text-sm outline-none transition-colors placeholder:text-muted-foreground focus:border-ring\"\n        />\n        <button\n          type=\"submit\"\n          disabled={busy || !input.trim()}\n          aria-label={busy ? 'Working' : 'Send'}\n          className=\"flex size-10 items-center justify-center rounded-lg bg-primary text-primary-foreground transition-[scale,opacity] enabled:active:scale-95 disabled:opacity-40\"\n        >\n          {busy ? <SquareIcon className=\"size-4\" /> : <SendIcon className=\"size-4\" />}\n        </button>\n      </form>\n    </div>\n  );\n}\n\n/** One transcript content part → its element. Deliberately fewer cases than the full shell. */\nfunction Part({\n  part,\n  resultsById,\n}: {\n  part: AgentControllerContentPart;\n  resultsById: Map<string, { result?: unknown; isError?: boolean }>;\n}) {\n  if (part.type === 'text') {\n    return <MessageResponse>{(part as { text: string }).text}</MessageResponse>;\n  }\n  if (part.type === 'thinking') {\n    return (\n      <Reasoning isStreaming={false}>\n        <ReasoningTrigger />\n        <ReasoningContent>{(part as { thinking: string }).thinking}</ReasoningContent>\n      </Reasoning>\n    );\n  }\n  if (part.type === 'image') {\n    const img = part as { data: string; mimeType: string };\n    return <GeneratedImage base64={img.data} mediaType={img.mimeType} />;\n  }\n  if (part.type === 'tool_call') {\n    const call = part as { id: string; name: string; args: unknown };\n    // These three own dedicated surfaces elsewhere (the goal card, the live\n    // AskUserPrompt above, the subagent card) — rendering the raw call would double up.\n    if (call.name === 'setGoal' || call.name === 'ask_user') return null;\n    const result = resultsById.get(call.id);\n    const hasOutput = result !== undefined;\n    // generateImage returns only an id; GeneratedImage fetches the bytes.\n    const img = result?.result as\n      | { imageId?: string; mediaType?: string; prompt?: string }\n      | undefined;\n    if (call.name === 'generateImage' && img?.imageId) {\n      return (\n        <GeneratedImage\n          imageId={img.imageId}\n          mediaType={img.mediaType ?? 'image/webp'}\n          prompt={img.prompt}\n        />\n      );\n    }\n    return (\n      <Tool>\n        <ToolHeader\n          type={`tool-${call.name}`}\n          state={hasOutput ? 'output-available' : 'input-available'}\n        />\n        <ToolContent>\n          <ToolInput input={call.args} />\n          {hasOutput && (\n            <ToolOutput\n              output={\n                <pre className=\"overflow-x-auto text-xs\">\n                  {JSON.stringify(result?.result, null, 2)}\n                </pre>\n              }\n              errorText={result?.isError ? 'Tool reported an error' : undefined}\n            />\n          )}\n        </ToolContent>\n      </Tool>\n    );\n  }\n  // tool_result renders alongside its tool_call above; skip standalone.\n  return null;\n}\n",
      "type": "registry:component",
      "target": "components/chat-minimal/minimal-chat.tsx"
    }
  ],
  "docs": "This kit needs TWO things in your project. Check both:\n\n  1. The RADIX base       ->  npx shadcn@latest init --base radix\n  2. The LUCIDE icon set  ->  \"iconLibrary\": \"lucide\" in components.json\n\nWith both, a fresh install typechecks with 0 errors and `next build` exits 0\n(verified on shadcn CLI 4.16.0 / Next 16.2.6).\n\nIf either is wrong:\n  • Base UI instead of Radix (a bare `init` gives you this — --defaults resolves\n    to --preset=base-nova) -> 14 type errors. This kit's own components port fine\n    either way (the CLI rewrites `asChild` to Base UI's `render`), but the upstream\n    Vercel AI Elements it depends on are Radix-authored and don't survive that.\n  • hugeicons instead of lucide -> 1 type error and a failed build, in shadcn's own\n    ui/spinner.tsx. Fix: set iconLibrary to lucide, then\n    `shadcn add spinner --overwrite`.\n\nThen set MASTRA_SERVER_URL to point at your Mastra server (default\nhttp://localhost:4111). See https://mastra-chat-kit-registry.vercel.app for the full endpoint contract.",
  "type": "registry:block"
}