{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-7",
  "title": "Agent 7: agent builder",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "An agent builder: instructions and model, each tool set to off, ask first or allowed, the knowledge it may read, guardrails, and a live preview that changes with every setting. Publishing shows submitting, success and error.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/approval-card.json",
    "https://ui.ballmac.com/r/badge.json",
    "https://ui.ballmac.com/r/blur-fade.json",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/cover-art.json",
    "https://ui.ballmac.com/r/model-picker.json",
    "https://ui.ballmac.com/r/segmented-control.json",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json",
    "https://ui.ballmac.com/r/switch.json",
    "https://ui.ballmac.com/r/textarea.json",
    "https://ui.ballmac.com/r/tool-call-card.json",
    "https://ui.ballmac.com/r/use-resource.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/agent-7/agent-7.tsx",
      "content": "// Ballmac UI: Agent 7. https://ui.ballmac.com/blocks/agent-7\n\"use client\"\n\nimport * as React from \"react\"\nimport { Bot, BookOpen, CircleAlert, FileText, Globe, Loader2, PenLine, Plus, RefreshCw, RotateCcw, Rocket, Search, ShieldCheck, Trash2, Wrench } from \"lucide-react\"\n\nimport { ApprovalCard, type ApprovalStatus } from \"@/components/ballmac/approval-card\"\nimport { Badge } from \"@/components/ballmac/badge\"\nimport { BlurFade } from \"@/components/ballmac/blur-fade\"\nimport { Button } from \"@/components/ballmac/button\"\nimport { CoverArt } from \"@/components/ballmac/cover-art\"\nimport { ModelPicker, type ModelOption } from \"@/components/ballmac/model-picker\"\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ballmac/segmented-control\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { frameProps, StateView } from \"@/components/ballmac/state-view\"\nimport { Switch } from \"@/components/ballmac/switch\"\nimport { Textarea } from \"@/components/ballmac/textarea\"\nimport { ToolCallCard } from \"@/components/ballmac/tool-call-card\"\nimport { formPhase, type FormStateProps } from \"@/lib/ballmac/use-resource\"\nimport { cn } from \"@/lib/utils\"\n\ntype Agent7Permission = \"off\" | \"ask\" | \"allow\"\n\ntype Agent7Tool = {\n  id: string\n  name: string\n  description: string\n  icon?: React.ReactNode\n  /** The tool changes data or sends something out. It is flagged, and defaults to Ask first. */\n  writes?: boolean\n}\n\ntype Agent7Source = {\n  id: string\n  name: string\n  /** What it is (\"Help center\", \"PDF\", \"Website\"). */\n  kind: string\n  status: \"ready\" | \"syncing\" | \"failed\"\n  /** Already written for people (\"Synced 2 hours ago\"). */\n  synced?: string\n}\n\ntype Agent7Config = {\n  name: string\n  instructions: string\n  model: string\n  /** Each tool's permission by id. A tool not listed is off. */\n  tools: Record<string, Agent7Permission>\n  /** Ids of the knowledge sources the agent may read. */\n  sources: string[]\n  askBeforeExternal: boolean\n  maxSteps: number\n}\n\ntype Agent7Props = Omit<React.ComponentProps<\"section\">, \"title\" | \"onChange\"> &\n  FormStateProps & {\n    tools?: Agent7Tool[]\n    sources?: Agent7Source[]\n    models?: ModelOption[]\n    /** The settings when the page opens. */\n    defaultConfig?: Agent7Config\n    /** Called with every change. */\n    onChange?: (config: Agent7Config) => void\n    /** Called by Publish with the settings. Resolve to confirm; throw to show the error. Defaults to a short simulated publish. */\n    onPublish?: (config: Agent7Config) => void | Promise<void>\n    /** Called by \"Add source\". Without it the button is hidden. */\n    onAddSource?: () => void\n    /** Called by the remove button on a source. */\n    onRemoveSource?: (id: string) => void\n    /** The most characters the instructions may have. */\n    maxInstructions?: number\n    /** The text of the sample request in the preview. */\n    sampleRequest?: string\n  }\n\nconst sampleTools: Agent7Tool[] = [\n  { id: \"orders.lookup\", name: \"Look up orders\", description: \"Reads an order, its items and its payments.\", icon: <Search /> },\n  { id: \"payments.refund\", name: \"Issue refunds\", description: \"Refunds a charge to the customer’s card.\", icon: <Wrench />, writes: true },\n  { id: \"email.send\", name: \"Send email\", description: \"Sends a message from the support address.\", icon: <PenLine />, writes: true },\n  { id: \"web.fetch\", name: \"Read web pages\", description: \"Opens public pages to check a link or a policy.\", icon: <Globe /> },\n]\n\nconst sampleSources: Agent7Source[] = [\n  { id: \"help\", name: \"Help center\", kind: \"Website\", status: \"ready\", synced: \"Synced 2 hours ago\" },\n  { id: \"policy\", name: \"Refund policy.pdf\", kind: \"PDF\", status: \"ready\", synced: \"Uploaded last week\" },\n  { id: \"macros\", name: \"Support macros\", kind: \"Spreadsheet\", status: \"syncing\", synced: \"Syncing…\" },\n  { id: \"wiki\", name: \"Internal wiki\", kind: \"Website\", status: \"failed\", synced: \"Could not sign in\" },\n]\n\nconst sampleModels: ModelOption[] = [\n  { id: \"lyra-xl\", name: \"Lyra XL\", provider: \"Lyra\", description: \"Best for hard problems and careful writing.\", capabilities: [\"vision\", \"reasoning\", \"tools\"], contextWindow: 1_000_000, cost: 3 },\n  { id: \"lyra-mid\", name: \"Lyra Mid\", provider: \"Lyra\", description: \"Balanced speed and quality for everyday work.\", capabilities: [\"vision\", \"tools\", \"fast\"], contextWindow: 200_000, cost: 2 },\n  { id: \"lyra-mini\", name: \"Lyra Mini\", provider: \"Lyra\", description: \"Near-instant replies for simple tasks.\", capabilities: [\"fast\"], contextWindow: 128_000, cost: 1 },\n]\n\nconst defaults: Agent7Config = {\n  name: \"Support agent\",\n  instructions:\n    \"You help customers of Acme with billing and account questions.\\n\\nBe warm, brief and specific. Check the order before you answer. Refunds under $200 for duplicate charges are fine to issue; anything else goes to a person. Never promise what you cannot do.\",\n  model: \"lyra-mid\",\n  tools: { \"orders.lookup\": \"allow\", \"payments.refund\": \"ask\", \"email.send\": \"ask\" },\n  sources: [\"help\", \"policy\"],\n  askBeforeExternal: true,\n  maxSteps: 12,\n}\n\nconst kindIcon = (k: string) => (/pdf|doc|sheet/i.test(k) ? <FileText /> : /web|site/i.test(k) ? <Globe /> : <BookOpen />)\n\nfunction BuilderSkeleton() {\n  return (\n    <div className=\"grid gap-6 lg:grid-cols-[minmax(0,1fr)_24rem]\" aria-hidden=\"true\">\n      <div className=\"grid gap-4\">\n        <Skeleton className=\"h-64 rounded-2xl\" />\n        <Skeleton className=\"h-56 rounded-2xl\" />\n        <Skeleton className=\"h-40 rounded-2xl\" />\n      </div>\n      <Skeleton className=\"h-[34rem] rounded-2xl\" />\n    </div>\n  )\n}\n\nfunction Section({ index, title, description, children, id }: { index: string; title: string; description: string; children: React.ReactNode; id: string }) {\n  return (\n    <section aria-labelledby={id} className=\"bg-card rounded-2xl border\">\n      <header className=\"flex items-start gap-4 border-b p-5 pb-4\">\n        <span aria-hidden=\"true\" className=\"text-muted-foreground bg-muted flex h-6 min-w-6 items-center justify-center rounded-md px-1 font-mono text-[11px] tabular-nums\">\n          {index}\n        </span>\n        <div className=\"min-w-0\">\n          <h2 id={id} className=\"text-sm font-semibold\">\n            {title}\n          </h2>\n          <p className=\"text-muted-foreground mt-0.5 text-xs text-pretty\">{description}</p>\n        </div>\n      </header>\n      <div className=\"p-5\">{children}</div>\n    </section>\n  )\n}\n\n/**\n * An agent builder: name and instructions, a model, each tool with its own permission (off, ask first, allowed), the knowledge it may\n * read, guardrails, and a live preview that changes with every setting. Publishing shows submitting, success and error.\n */\nfunction Agent7({\n  tools = sampleTools,\n  sources = sampleSources,\n  models = sampleModels,\n  defaultConfig = defaults,\n  onChange,\n  onPublish,\n  onAddSource,\n  onRemoveSource,\n  maxInstructions = 2000,\n  sampleRequest = \"Refund the duplicate charge on order 4821 and let the customer know.\",\n  state: stateProp,\n  errorState,\n  className,\n  ...props\n}: Agent7Props) {\n  const id = React.useId()\n  const [config, setConfig] = React.useState(defaultConfig)\n  const [removed, setRemoved] = React.useState<string[]>([])\n  const [internal, setInternal] = React.useState<\"idle\" | \"sending\" | \"error\" | \"done\">(\"idle\")\n  const [message, setMessage] = React.useState<string>()\n  const [touched, setTouched] = React.useState(false)\n  const [decision, setDecision] = React.useState<ApprovalStatus>(\"pending\")\n  const [announce, setAnnounce] = React.useState(\"\")\n  const forced = formPhase(stateProp)\n  const phase = forced ?? internal\n  const set = (patch: Partial<Agent7Config>) =>\n    setConfig((c) => {\n      const next = { ...c, ...patch }\n      onChange?.(next)\n      return next\n    })\n  const [dirty, setDirty] = React.useState(false)\n\n  const nameError = !config.name.trim() ? \"Give the agent a name.\" : undefined\n  const instructionsError = config.instructions.trim().length < 20 ? \"Describe what the agent should do in a sentence or two.\" : undefined\n  const invalid = !!nameError || !!instructionsError\n  const permissionOf = (t: Agent7Tool): Agent7Permission => config.tools[t.id] ?? \"off\"\n  const enabled = tools.filter((t) => permissionOf(t) !== \"off\")\n  const shownSources = sources.filter((s) => !removed.includes(s.id))\n  const model = models.find((m) => m.id === config.model)\n  // the preview shows the most interesting tool: one that changes data, because that is where Ask first matters\n  const first = enabled.find((t) => t.writes) ?? enabled[0]\n  const firstPermission = first ? permissionOf(first) : \"off\"\n  const used = shownSources.filter((s) => config.sources.includes(s.id) && s.status !== \"failed\")\n\n  async function publish() {\n    setTouched(true)\n    if (invalid) {\n      setAnnounce(\"Fix the highlighted fields before publishing\")\n      return\n    }\n    setInternal(\"sending\")\n    setMessage(undefined)\n    try {\n      if (onPublish) await onPublish(config)\n      else await new Promise((r) => setTimeout(r, 1000))\n      setInternal(\"done\")\n      setDirty(false)\n      setAnnounce(\"Published\")\n    } catch (err) {\n      setInternal(\"error\")\n      setMessage(err instanceof Error && err.message ? err.message : undefined)\n    }\n  }\n\n  const change = (patch: Partial<Agent7Config>) => {\n    set(patch)\n    setDirty(true)\n    if (internal !== \"sending\") setInternal(\"idle\")\n    setDecision(\"pending\")\n  }\n\n  const status =\n    phase === \"sending\"\n      ? { text: \"Publishing…\", tone: \"bg-chart-3\" }\n      : phase === \"error\"\n        ? { text: \"Not published\", tone: \"bg-destructive\" }\n        : phase === \"done\" && !dirty\n          ? { text: \"Published\", tone: \"bg-chart-2\" }\n          : dirty\n            ? { text: \"Unpublished changes\", tone: \"bg-chart-3\" }\n            : { text: \"Draft\", tone: \"bg-muted-foreground\" }\n\n  const left = (\n    <div className=\"grid min-w-0 content-start gap-5\">\n      <Section id={`${id}-s1`} index=\"01\" title=\"Instructions\" description=\"Say who the agent is, what it should do and where it should stop.\">\n        <div className=\"grid gap-5\">\n          <div className=\"grid gap-2\">\n            <div className=\"flex items-center justify-between gap-3\">\n              <label htmlFor={`${id}-instructions`} className=\"text-sm font-medium\">\n                What should it do?\n              </label>\n              <span className={cn(\"text-xs tabular-nums\", config.instructions.length > maxInstructions * 0.95 ? \"text-destructive\" : \"text-muted-foreground\")}>\n                {config.instructions.length} / {maxInstructions}\n              </span>\n            </div>\n            <Textarea\n              id={`${id}-instructions`}\n              rows={9}\n              maxLength={maxInstructions}\n              value={config.instructions}\n              aria-invalid={(touched && !!instructionsError) || undefined}\n              aria-describedby={touched && instructionsError ? `${id}-instructions-error` : undefined}\n              onChange={(e) => change({ instructions: e.target.value })}\n              className=\"min-h-44 resize-y text-[13px] leading-relaxed\"\n            />\n            {touched && instructionsError ? (\n              <p id={`${id}-instructions-error`} role=\"alert\" className=\"text-destructive text-xs\">\n                {instructionsError}\n              </p>\n            ) : null}\n          </div>\n          <div className=\"grid gap-2\">\n            <span id={`${id}-model`} className=\"text-sm font-medium\">\n              Model\n            </span>\n            <div className=\"flex flex-wrap items-center gap-3\">\n              <ModelPicker models={models} value={config.model} onValueChange={(m) => change({ model: m })} aria-labelledby={`${id}-model`} />\n              {model?.description ? <p className=\"text-muted-foreground min-w-0 flex-1 basis-48 text-xs text-pretty\">{model.description}</p> : null}\n            </div>\n          </div>\n        </div>\n      </Section>\n\n      <Section id={`${id}-s2`} index=\"02\" title=\"Tools\" description=\"What the agent can do. Anything that changes data is flagged, and you choose whether it asks first.\">\n        <ul className=\"grid gap-2.5\">\n          {tools.map((t) => {\n            const p = permissionOf(t)\n            return (\n              <li key={t.id} className={cn(\"flex flex-wrap items-center gap-x-4 gap-y-3 rounded-xl border p-3.5 transition-colors duration-200\", p !== \"off\" && \"bg-accent/30 border-foreground/15\")}>\n                <span aria-hidden=\"true\" className=\"bg-background flex size-9 shrink-0 items-center justify-center rounded-lg border [&_svg]:size-4\">\n                  {t.icon ?? <Wrench />}\n                </span>\n                <div className=\"min-w-0 flex-1 basis-52\">\n                  <p id={`${id}-t-${t.id}`} className=\"flex flex-wrap items-center gap-2 text-sm font-medium\">\n                    {t.name}\n                    {t.writes ? (\n                      <Badge status=\"warning\" className=\"font-normal\">\n                        Changes data\n                      </Badge>\n                    ) : null}\n                  </p>\n                  <p className=\"text-muted-foreground mt-0.5 text-xs text-pretty\">{t.description}</p>\n                </div>\n                {p !== \"off\" ? (\n                  <SegmentedControl value={p} onValueChange={(v) => change({ tools: { ...config.tools, [t.id]: v as Agent7Permission } })} aria-label={`${t.name}: when to use`} size=\"sm\">\n                    <SegmentedControlItem value=\"ask\">Ask first</SegmentedControlItem>\n                    <SegmentedControlItem value=\"allow\">Allowed</SegmentedControlItem>\n                  </SegmentedControl>\n                ) : null}\n                <Switch\n                  checked={p !== \"off\"}\n                  onCheckedChange={(on) => change({ tools: { ...config.tools, [t.id]: on ? (t.writes ? \"ask\" : \"allow\") : \"off\" } })}\n                  aria-labelledby={`${id}-t-${t.id}`}\n                />\n              </li>\n            )\n          })}\n        </ul>\n      </Section>\n\n      <Section id={`${id}-s3`} index=\"03\" title=\"Knowledge\" description=\"What the agent may read before it answers. It cites these in its replies.\">\n        <ul className=\"grid gap-2\">\n          {shownSources.map((s) => {\n            const on = config.sources.includes(s.id)\n            return (\n              <li key={s.id} className=\"flex items-center gap-3 rounded-xl border p-3\">\n                <span aria-hidden=\"true\" className=\"bg-muted flex size-8 shrink-0 items-center justify-center rounded-lg [&_svg]:size-4\">\n                  {kindIcon(s.kind)}\n                </span>\n                <div className=\"min-w-0 flex-1\">\n                  <p id={`${id}-k-${s.id}`} className=\"truncate text-sm font-medium\">\n                    {s.name}\n                  </p>\n                  <p className={cn(\"flex items-center gap-1.5 text-xs\", s.status === \"failed\" ? \"text-destructive\" : \"text-muted-foreground\")}>\n                    {s.status === \"syncing\" ? <RefreshCw aria-hidden=\"true\" className=\"size-3 animate-spin motion-reduce:animate-none\" /> : s.status === \"failed\" ? <CircleAlert aria-hidden=\"true\" className=\"size-3\" /> : null}\n                    {s.kind} · {s.synced}\n                  </p>\n                </div>\n                {onRemoveSource ? (\n                  <Button\n                    variant=\"ghost\"\n                    size=\"icon-sm\"\n                    aria-label={`Remove ${s.name}`}\n                    onClick={() => {\n                      setRemoved((r) => [...r, s.id])\n                      onRemoveSource(s.id)\n                      change({ sources: config.sources.filter((x) => x !== s.id) })\n                    }}\n                  >\n                    <Trash2 aria-hidden=\"true\" />\n                  </Button>\n                ) : null}\n                <Switch checked={on} disabled={s.status === \"failed\"} onCheckedChange={(v) => change({ sources: v ? [...config.sources, s.id] : config.sources.filter((x) => x !== s.id) })} aria-labelledby={`${id}-k-${s.id}`} />\n              </li>\n            )\n          })}\n        </ul>\n        {onAddSource ? (\n          <Button variant=\"outline\" size=\"sm\" className=\"mt-3\" onClick={onAddSource}>\n            <Plus aria-hidden=\"true\" />\n            Add source\n          </Button>\n        ) : null}\n      </Section>\n\n      <Section id={`${id}-s4`} index=\"04\" title=\"Guardrails\" description=\"Limits that always apply, whatever the instructions say.\">\n        <div className=\"grid gap-4\">\n          <div className=\"flex items-center gap-4\">\n            <div className=\"min-w-0 flex-1\">\n              <p id={`${id}-g1`} className=\"text-sm font-medium\">\n                Ask before anything leaves the company\n              </p>\n              <p className=\"text-muted-foreground mt-0.5 text-xs text-pretty\">Emails, messages and requests to other services wait for a person to approve them.</p>\n            </div>\n            <Switch checked={config.askBeforeExternal} onCheckedChange={(v) => change({ askBeforeExternal: v })} aria-labelledby={`${id}-g1`} />\n          </div>\n          <div className=\"flex items-center gap-4 border-t pt-4\">\n            <div className=\"min-w-0 flex-1\">\n              <p id={`${id}-steps`} className=\"text-sm font-medium\">\n                Stop after this many steps\n              </p>\n              <p className=\"text-muted-foreground mt-0.5 text-xs text-pretty\">A run that goes past it stops and asks for help, so a loop cannot run up a bill.</p>\n            </div>\n            <SegmentedControl value={String(config.maxSteps)} onValueChange={(v) => change({ maxSteps: Number(v) })} aria-labelledby={`${id}-steps`} size=\"sm\">\n              {[6, 12, 25, 50].map((n) => (\n                <SegmentedControlItem key={n} value={String(n)}>\n                  {n}\n                </SegmentedControlItem>\n              ))}\n            </SegmentedControl>\n          </div>\n        </div>\n      </Section>\n    </div>\n  )\n\n  const preview = (\n    <aside aria-label=\"Preview\" className=\"bg-card flex min-h-[32rem] flex-col overflow-hidden rounded-2xl border lg:sticky lg:top-6 lg:max-h-[calc(100vh-3rem)]\">\n      <div className=\"flex items-center gap-3 border-b p-4\">\n        <CoverArt seed={config.name || \"agent\"} variant=\"mesh\" className=\"size-9 shrink-0 rounded-xl border\" />\n        <div className=\"min-w-0 flex-1\">\n          <p className=\"truncate text-sm font-semibold\">{config.name || \"Untitled agent\"}</p>\n          <p className=\"text-muted-foreground truncate text-xs\">{model?.name ?? config.model}</p>\n        </div>\n        <Badge variant=\"outline\" className=\"font-normal\">\n          Preview\n        </Badge>\n        <Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Replay the preview\" onClick={() => setDecision(\"pending\")}>\n          <RotateCcw aria-hidden=\"true\" />\n        </Button>\n      </div>\n      <div className=\"flex-1 space-y-4 overflow-y-auto p-4\">\n        <p className=\"text-muted-foreground text-center text-xs\">Sample conversation. It shows how your settings behave, not a real model’s words.</p>\n        <div className=\"bg-primary text-primary-foreground ms-auto w-fit max-w-[85%] rounded-2xl rounded-ee-md px-3.5 py-2 text-sm text-pretty\">{sampleRequest}</div>\n        <div className=\"flex gap-2.5\">\n          <span aria-hidden=\"true\" className=\"bg-muted mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full border [&_svg]:size-3.5\">\n            <Bot />\n          </span>\n          <div className=\"grid min-w-0 flex-1 gap-3\">\n            <p className=\"text-sm text-pretty\">\n              {used.length\n                ? `I’ll check ${used.length === 1 ? used[0]!.name : `${used.length} sources, starting with ${used[0]!.name}`}, then handle it.`\n                : \"I don’t have any knowledge sources turned on, so I’ll work from my instructions.\"}\n            </p>\n            {first ? (\n              firstPermission === \"ask\" && decision !== \"approved\" ? (\n                <ApprovalCard\n                  key={first.id}\n                  title={`Use “${first.name}”`}\n                  description={first.description}\n                  risk={first.writes ? \"high\" : \"low\"}\n                  status={decision}\n                  onApprove={() => setDecision(\"approved\")}\n                  onDeny={() => setDecision(\"denied\")}\n                />\n              ) : (\n                <ToolCallCard name={first.id} status=\"success\" duration={640} description={first.name} />\n              )\n            ) : (\n              <p className=\"border-border text-muted-foreground rounded-xl border border-dashed px-3 py-2.5 text-xs\">No tools are turned on, so it can only answer in words.</p>\n            )}\n            {decision === \"denied\" ? (\n              <p className=\"text-sm text-pretty\">Understood, I won’t do that. I’ll tell the customer a person will follow up.</p>\n            ) : !first ? (\n              <p className=\"text-sm text-pretty\">I can explain what to do, but I can’t do it myself. Turn on a tool to let me.</p>\n            ) : firstPermission === \"allow\" || decision === \"approved\" ? (\n              <p className=\"text-sm text-pretty\">Done. {first.writes ? \"The change is made and the customer has been told.\" : \"I found what you asked for.\"} Anything outside my limits goes to a person{config.askBeforeExternal ? \", and I’ll ask before anything leaves the company\" : \"\"}.</p>\n            ) : null}\n          </div>\n        </div>\n      </div>\n      <dl className=\"text-muted-foreground grid grid-cols-3 gap-px border-t bg-border text-center text-[11px] [&>div]:bg-card [&>div]:px-2 [&>div]:py-2.5\">\n        <div>\n          <dt>Tools</dt>\n          <dd className=\"text-foreground text-sm font-semibold tabular-nums\">{enabled.length}</dd>\n        </div>\n        <div>\n          <dt>Sources</dt>\n          <dd className=\"text-foreground text-sm font-semibold tabular-nums\">{used.length}</dd>\n        </div>\n        <div>\n          <dt>Step limit</dt>\n          <dd className=\"text-foreground text-sm font-semibold tabular-nums\">{config.maxSteps}</dd>\n        </div>\n      </dl>\n    </aside>\n  )\n\n  return (\n    <section data-slot=\"agent-7\" data-state={stateProp ?? (phase === \"sending\" ? \"submitting\" : phase === \"error\" ? \"error\" : phase === \"done\" ? \"success\" : \"ready\")} aria-label=\"Agent builder\" className={cn(\"mx-auto w-full max-w-6xl px-4 py-8 sm:px-6\", className)} {...frameProps(props)}>\n      <StateView state={stateProp === \"loading\" ? \"loading\" : \"ready\"} label=\"Loading the agent\" loading={<BuilderSkeleton />}>\n        <BlurFade direction=\"up\" offset={8} duration={0.45}>\n          <header className=\"flex flex-wrap items-center gap-x-4 gap-y-3 pb-6\">\n            <CoverArt seed={config.name || \"agent\"} variant=\"mesh\" className=\"size-12 shrink-0 rounded-2xl border\" />\n            <div className=\"min-w-0 flex-1 basis-56\">\n              <label htmlFor={`${id}-name`} className=\"sr-only\">\n                Agent name\n              </label>\n              <input\n                id={`${id}-name`}\n                value={config.name}\n                placeholder=\"Name your agent\"\n                maxLength={60}\n                aria-invalid={(touched && !!nameError) || undefined}\n                aria-describedby={touched && nameError ? `${id}-name-error` : undefined}\n                onChange={(e) => change({ name: e.target.value })}\n                className=\"hover:border-border focus-visible:border-ring focus-visible:ring-ring/40 aria-invalid:border-destructive placeholder:text-muted-foreground -mx-2 w-[calc(100%+1rem)] rounded-lg border border-transparent bg-transparent px-2 py-0.5 text-2xl font-semibold tracking-[-0.03em] outline-none focus-visible:ring-[3px]\"\n              />\n              {touched && nameError ? (\n                <p id={`${id}-name-error`} role=\"alert\" className=\"text-destructive text-xs\">\n                  {nameError}\n                </p>\n              ) : null}\n              <p role=\"status\" className=\"text-muted-foreground mt-0.5 flex items-center gap-2 text-xs\">\n                <span aria-hidden=\"true\" className={cn(\"size-1.5 rounded-full\", status.tone)} />\n                {status.text}\n              </p>\n            </div>\n            <Button onClick={publish} disabled={phase === \"sending\"} aria-disabled={phase === \"sending\" || undefined} className=\"min-w-32\">\n              {phase === \"sending\" ? <Loader2 aria-hidden=\"true\" className=\"animate-spin motion-reduce:animate-none\" /> : phase === \"done\" && !dirty ? <ShieldCheck aria-hidden=\"true\" /> : <Rocket aria-hidden=\"true\" />}\n              {phase === \"sending\" ? \"Publishing…\" : phase === \"done\" && !dirty ? \"Published\" : \"Publish\"}\n            </Button>\n          </header>\n          {phase === \"error\" ? (\n            <div role=\"alert\" className=\"border-destructive/30 bg-destructive/5 mb-6 flex items-start gap-3 rounded-xl border px-4 py-3 text-sm\">\n              <CircleAlert aria-hidden=\"true\" className=\"text-destructive mt-0.5 size-4 shrink-0\" />\n              <div>\n                <p className=\"font-medium\">{errorState?.title ?? \"The agent could not be published\"}</p>\n                <p className=\"text-muted-foreground mt-0.5 text-xs\">{errorState?.description ?? message ?? \"Your changes are kept here. Check your connection and try again.\"}</p>\n              </div>\n            </div>\n          ) : null}\n        </BlurFade>\n\n        <div className=\"grid items-start gap-6 lg:grid-cols-[minmax(0,1fr)_24rem]\">\n          {left}\n          {preview}\n        </div>\n      </StateView>\n      <p aria-live=\"polite\" className=\"sr-only\">\n        {announce}\n      </p>\n    </section>\n  )\n}\n\nexport { Agent7, type Agent7Props, type Agent7Config, type Agent7Tool, type Agent7Source, type Agent7Permission }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/agent-7/agent-7.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "ai",
      "agent",
      "builder",
      "no-code",
      "tools",
      "permissions",
      "knowledge",
      "guardrails",
      "instructions",
      "publish",
      "preview"
    ],
    "version": "1.0.0",
    "updated": "2026-10-17",
    "tests": true,
    "ai": {
      "summary": "An agent configuration page. <Agent7 tools={[{ id, name, description, icon, writes }]} sources={[{ id, name, kind, status: 'ready' | 'syncing' | 'failed', synced }]} models={ModelOption[]} defaultConfig={{ name, instructions, model, tools: { [id]: 'off' | 'ask' | 'allow' }, sources: [ids], askBeforeExternal, maxSteps }} onChange onPublish={async (config) => …} onAddSource onRemoveSource />. The preview shows the first enabled tool: behind an approval card when set to Ask first, as a finished tool call when Allowed.",
      "whenToUse": [
        "Letting people create or edit an AI agent in a no-code way",
        "An internal admin page for configuring an agent's tools, knowledge and limits"
      ],
      "whenNotToUse": [
        "Chatting with an agent (use ai-chat-1)",
        "Approving an agent's actions as it works (use agent-1 or agent-2)",
        "Choosing a model only (use model-picker)"
      ],
      "composesWith": [
        "agent-2",
        "agent-3",
        "agent-6",
        "app-shell-1"
      ],
      "a11y": [
        {
          "keys": "Tab",
          "action": "Each tool and source has a switch named for it; a tool's permission is a radio group that appears when it is on"
        },
        {
          "keys": "Publish with missing fields",
          "action": "The name and instructions are marked invalid and their reasons are announced as alerts"
        },
        {
          "keys": "Status",
          "action": "Draft, Unpublished changes, Publishing… and Published are a status region"
        }
      ],
      "customization": [
        "maxInstructions, sampleRequest (the text of the request in the preview)",
        "state: \"submitting\" | \"success\" | \"error\" to drive publishing yourself; leave it out and the block runs it",
        "Tools marked writes default to Ask first when turned on; map the permission to your agent runtime's approval setting"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "agent-7-loading",
        "trigger": "state=\"loading\"",
        "note": "A skeleton of the settings and the preview under the real header."
      },
      {
        "id": "submitting",
        "title": "Publishing",
        "example": "agent-7-publishing",
        "trigger": "state=\"submitting\"",
        "note": "Publish shows a spinner and \"Publishing…\" and is disabled; the status reads Publishing…"
      },
      {
        "id": "success",
        "title": "Published",
        "example": "agent-7-published",
        "trigger": "state=\"success\"",
        "note": "The button and status say Published until something changes again."
      },
      {
        "id": "error",
        "title": "Could not publish",
        "example": "agent-7-error",
        "trigger": "state=\"error\"",
        "note": "An alert says the changes are kept and why it failed (errorState sets the copy or throw an Error from onPublish)."
      }
    ],
    "examples": [
      "agent-7-demo",
      "agent-7-bare",
      "agent-7-loading",
      "agent-7-publishing",
      "agent-7-published",
      "agent-7-error"
    ],
    "url": "https://ui.ballmac.com/components/agent-7"
  },
  "categories": [
    "blocks",
    "agents"
  ],
  "type": "registry:block"
}