{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "task-card",
  "type": "registry:component",
  "title": "Task card",
  "description": "A runtime task card with nested transcripts, timing, results, and grouped task lanes.",
  "dependencies": [
    "@assistant-ui/react@^0.15.23"
  ],
  "registryDependencies": [
    "https://r.assistant-ui.com/elements-task-card.json",
    "https://r.assistant-ui.com/elements-task.json",
    "https://r.assistant-ui.com/markdown-text.json",
    "https://r.assistant-ui.com/tool-fallback.json"
  ],
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/elements/task-card.aui.tsx",
      "sourcePath": "packages/ui/src/components/react/assistant-ui/elements/task-card.aui.tsx",
      "content": "\"use client\";\n\nimport {\n  MessagePrimitive,\n  ReadonlyThreadProvider,\n  ThreadPrimitive,\n  useAui,\n  useAuiState,\n  type ThreadMessage,\n  type ToolCallMessagePart,\n  type ToolCallMessagePartComponent,\n  type ToolCallMessagePartProps,\n  type ToolCallMessagePartStatus,\n} from \"@assistant-ui/react\";\nimport { type FC, useState } from \"react\";\nimport { MarkdownText } from \"@/components/assistant-ui/elements/markdown-text\";\nimport {\n  formatUnknownValue,\n  offersInterruptAction,\n  ToolFallback,\n  ToolFallbackApproval,\n  ToolFallbackError,\n} from \"@/components/assistant-ui/elements/tool-fallback.aui\";\nimport { cn } from \"@/lib/utils\";\nimport { mono } from \"./surfaces\";\nimport { TaskCard as TaskCardBase } from \"./task-card\";\nimport {\n  formatElapsed,\n  TASK_PAGE_SIZE,\n  taskLabel,\n  taskMeta,\n  taskStateOf,\n  useTaskElapsed,\n} from \"../utils/task\";\n\nexport type { TaskCardState } from \"./task-card\";\nexport { TASK_PAGE_SIZE } from \"../utils/task\";\n\nexport type TaskPart = ToolCallMessagePart & {\n  readonly status: ToolCallMessagePartStatus;\n} & Partial<\n    Pick<ToolCallMessagePartProps, \"addResult\" | \"resume\" | \"respondToApproval\">\n  >;\n\nexport const isTaskPart = (part: {\n  readonly type: string;\n  readonly messages?: unknown;\n}) => part.type === \"tool-call\" && part.messages !== undefined;\n\nconst KEY_SEPARATOR = String.fromCharCode(31);\n\nconst ROLE_LABELS = {\n  user: \"instruction\",\n  assistant: \"agent\",\n  system: \"system\",\n} as const;\n\nconst NestedToolCall: ToolCallMessagePartComponent = (part) =>\n  isTaskPart(part) ? <TaskCard part={part} /> : <ToolFallback {...part} />;\n\nconst NestedMessage: FC = () => {\n  const role = useAuiState((s) => s.message.role);\n\n  return (\n    <MessagePrimitive.Root\n      data-slot=\"aui_task-transcript-message\"\n      data-role={role}\n      className=\"flex flex-col gap-1 text-xs leading-relaxed\"\n    >\n      <span className={cn(mono, \"text-foreground/35\")}>\n        {ROLE_LABELS[role]}\n      </span>\n      <MessagePrimitive.Parts\n        components={{ Text: MarkdownText, tools: { Fallback: NestedToolCall } }}\n      />\n    </MessagePrimitive.Root>\n  );\n};\n\nconst TaskTranscript: FC<{ messages: readonly ThreadMessage[] }> = ({\n  messages,\n}) => (\n  <ReadonlyThreadProvider messages={messages}>\n    <ThreadPrimitive.Messages>\n      {() => <NestedMessage />}\n    </ThreadPrimitive.Messages>\n  </ReadonlyThreadProvider>\n);\n\nconst TaskResult: FC<{ result: unknown }> = ({ result }) =>\n  typeof result === \"string\" ? (\n    <p className=\"m-0 whitespace-pre-wrap\">{result}</p>\n  ) : (\n    <pre className=\"m-0 overflow-x-auto whitespace-pre-wrap\">\n      {formatUnknownValue(result, 2)}\n    </pre>\n  );\n\nexport const TaskCard: FC<{ part: TaskPart; className?: string }> = ({\n  part,\n  className,\n}) => {\n  const elapsedMs = useTaskElapsed(\n    part.timing,\n    part.status.type === \"running\" || part.status.type === \"requires-action\",\n  );\n  const canAnswer = useAuiState((s) => s.thread.capabilities.answerToolCall);\n  const messages = part.messages ?? [];\n  const showError =\n    part.status.type === \"incomplete\" &&\n    part.status.error !== undefined &&\n    part.status.error !== null;\n  const result =\n    showError || part.result !== undefined ? (\n      <>\n        {showError && <ToolFallbackError status={part.status} />}\n        {part.result !== undefined && <TaskResult result={part.result} />}\n      </>\n    ) : undefined;\n  const approvalPending =\n    part.approval == null ||\n    (part.approval.approved === undefined &&\n      part.approval.resolution === undefined);\n  const actions =\n    part.status.type === \"requires-action\" &&\n    approvalPending &&\n    offersInterruptAction(part.status, part.approval, part.interrupt) &&\n    (canAnswer || part.approval?.prompt) ? (\n      <ToolFallbackApproval\n        status={part.status}\n        {...(part.approval !== undefined && { approval: part.approval })}\n        {...(part.interrupt !== undefined && { interrupt: part.interrupt })}\n        {...(part.addResult && { addResult: part.addResult })}\n        {...(part.resume && { resume: part.resume })}\n        {...(part.respondToApproval && {\n          respondToApproval: part.respondToApproval,\n        })}\n      />\n    ) : undefined;\n\n  return (\n    <TaskCardBase\n      className={className}\n      label={taskLabel(part.toolName, part.args)}\n      meta={taskMeta(part.args)}\n      state={taskStateOf(part.status, part.isError)}\n      elapsed={elapsedMs === undefined ? undefined : formatElapsed(elapsedMs)}\n      actions={actions}\n      result={result}\n    >\n      {messages.length > 0 ? <TaskTranscript messages={messages} /> : undefined}\n    </TaskCardBase>\n  );\n};\n\nconst TaskLane: FC<{ index: number }> = ({ index }) => {\n  const aui = useAui();\n  const part = useAuiState((s) => s.message.parts[index]);\n  if (part?.type !== \"tool-call\") return null;\n  const client = aui.message.part({ toolCallId: part.toolCallId });\n  return (\n    <TaskCard\n      part={{\n        ...part,\n        addResult: client.addToolResult,\n        resume: client.resumeToolCall,\n        respondToApproval: client.respondToToolApproval,\n      }}\n    />\n  );\n};\n\nexport const TaskGroup: FC<{\n  group: MessagePrimitive.GroupedParts.GroupPart;\n  className?: string;\n}> = ({ group, className }) => {\n  const [visible, setVisible] = useState(TASK_PAGE_SIZE);\n  const { indices, counts } = group;\n  // A selector has to return a stable value, so the lane keys travel as one string and are split afterwards.\n  const laneKeys = useAuiState((s) =>\n    indices\n      .map((index) => {\n        const part = s.message.parts[index];\n        return part?.type === \"tool-call\" ? part.toolCallId : String(index);\n      })\n      .join(KEY_SEPARATOR),\n  ).split(KEY_SEPARATOR);\n  const failed = useAuiState((s) =>\n    indices.reduce((count, index) => {\n      const part = s.message.parts[index];\n      return part?.type === \"tool-call\" &&\n        taskStateOf(part.status, part.isError) === \"failed\"\n        ? count + 1\n        : count;\n    }, 0),\n  );\n  if (indices.length === 1) return <TaskLane index={indices[0]!} />;\n\n  const shown = indices.slice(0, visible);\n  const hidden = indices.length - shown.length;\n  const summary = [\n    `${indices.length} tasks`,\n    counts.running > 0 && `${counts.running} running`,\n    counts.requiresAction > 0 && `${counts.requiresAction} waiting`,\n    failed > 0 && `${failed} failed`,\n  ].filter((entry): entry is string => typeof entry === \"string\");\n\n  return (\n    <div\n      data-slot=\"aui_task-group\"\n      className={cn(\"flex w-full max-w-sm flex-col gap-2\", className)}\n    >\n      <div\n        data-slot=\"aui_task-group-summary\"\n        className=\"text-muted-foreground px-1 text-xs\"\n      >\n        {summary.join(\" · \")}\n      </div>\n      {shown.map((index, position) => (\n        <TaskLane key={laneKeys[position] ?? index} index={index} />\n      ))}\n      {hidden > 0 && (\n        <button\n          type=\"button\"\n          data-slot=\"aui_task-group-more\"\n          onClick={() => setVisible((count) => count + TASK_PAGE_SIZE)}\n          className=\"text-muted-foreground hover:text-foreground w-fit px-1 text-xs transition-colors\"\n        >\n          Show {Math.min(hidden, TASK_PAGE_SIZE)} more\n        </button>\n      )}\n    </div>\n  );\n};\n"
    }
  ]
}