{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tools",
  "title": "Tool Invocation UI",
  "description": "Displays tool lifecycle: pending, in-progress, approval, and results.",
  "type": "registry:block",
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "collapsible",
    "spinner",
    "https://inference.sh/ui/r/pretext-md.json",
    "https://inference.sh/ui/r/widgets.json",
    "https://inference.sh/ui/r/task.json"
  ],
  "files": [
    {
      "path": "components/infsh/agent/tool-invocation.tsx",
      "content": "import React, { memo, useState, useMemo, useCallback } from 'react';\nimport { cn } from '@/lib/utils';\nimport { MessageCircleCode, CheckCircle2, XCircle, Clock, AlertCircle, CheckCircle, CircleDashed } from 'lucide-react';\nimport { Spinner } from '@/components/ui/spinner';\nimport { Button } from '@/components/ui/button';\nimport { CollapsibleSection } from '@/components/ui/collapsible-section';\nimport {\n  ToolInvocationStatusPending,\n  ToolInvocationStatusInProgress,\n  ToolInvocationStatusAwaitingInput,\n  ToolInvocationStatusAwaitingApproval,\n  ToolInvocationStatusCompleted,\n  ToolInvocationStatusFailed,\n  ToolInvocationStatusCancelled,\n  ToolTypeApp,\n  ToolInvocationDTO,\n  FileRef\n} from '@inferencesh/sdk';\nimport { useAgentActions, useAgentClient } from '@inferencesh/sdk/agent';\nimport { WidgetRenderer } from '@/components/infsh/agent/widget-renderer';\nimport { parseWidget, type WidgetAction, type WidgetFormData } from '@/components/infsh/agent/widget-types';\nimport { IntegrationRequirementCard } from '@/components/infsh/agent/integration-requirement-card';\nimport { TaskOutputWrapper } from '@/components/infsh/task/task-output-wrapper';\nimport { Markdown } from '@/lib/pretext-md/react';\n\n// Tool finish constants\nconst ToolFinishStatusSucceeded = 'succeeded';\nconst ToolFinishStatusFailed = 'failed';\nconst ToolFinishStatusCancelled = 'cancelled';\n\n// Types\ninterface ToolFinish {\n  status: string;\n  result?: unknown;\n  error?: string;\n}\n\ninterface ToolInvocationProps {\n  invocation: ToolInvocationDTO;\n  className?: string;\n  defaultOpen?: boolean;\n}\n\n// ============================================================================\n// Finish Block - Special display for finish tool marking end of chat\n// ============================================================================\n\nconst FinishBlock = memo(function FinishBlock({\n  finish,\n  isActive = false,\n}: {\n  finish?: ToolFinish | null\n  isActive?: boolean\n}) {\n  const getStatusIcon = () => {\n    if (isActive) {\n      return <Spinner className=\"size-3.5\" />\n    }\n    switch (finish?.status) {\n      case ToolFinishStatusSucceeded:\n        return <CheckCircle className=\"h-3.5 w-3.5 text-emerald-400\" />\n      case ToolFinishStatusFailed:\n        return <XCircle className=\"h-3.5 w-3.5 text-red-400\" />\n      case ToolFinishStatusCancelled:\n        return <CircleDashed className=\"h-3.5 w-3.5 text-muted-foreground\" />\n      default:\n        return <CheckCircle className=\"h-3.5 w-3.5 text-emerald-400\" />\n    }\n  }\n\n  const getStatusText = () => {\n    if (isActive) return 'finishing'\n    switch (finish?.status) {\n      case ToolFinishStatusSucceeded:\n        return 'completed'\n      case ToolFinishStatusFailed:\n        return 'failed'\n      case ToolFinishStatusCancelled:\n        return 'cancelled'\n      default:\n        return 'completed'\n    }\n  }\n\n  const getLineColor = () => {\n    switch (finish?.status) {\n      case ToolFinishStatusFailed:\n        return 'bg-red-400/30'\n      case ToolFinishStatusCancelled:\n        return 'bg-muted-foreground/30'\n      default:\n        return 'bg-muted-foreground/20'\n    }\n  }\n\n  const resultMessage = finish?.result && typeof finish.result === 'string' && !isActive\n    ? finish.result.toLowerCase()\n    : null;\n\n  return (\n    <div className=\"my-6 space-y-4\">\n      <div className=\"flex items-center gap-4 w-full\">\n        <div className={cn(\"flex-1 h-px\", getLineColor())} />\n        <div className={cn(\n          \"flex items-center gap-2 text-muted-foreground/50\",\n          isActive && \"animate-pulse\"\n        )}>\n          {getStatusIcon()}\n          <span className=\"text-xs font-medium\">\n            {getStatusText()}\n          </span>\n        </div>\n        <div className={cn(\"flex-1 h-px\", getLineColor())} />\n      </div>\n      <div className=\"border border-border rounded-md p-4 bg-card w-fit max-w-full\">\n        {resultMessage && (\n          <Markdown content={resultMessage} />\n        )}\n\n\n      </div>\n    </div>\n  )\n})\n\n/**\n * ToolInvocation - Single tool call display with widget and task output support\n *\n * @example\n * ```tsx\n * <ToolInvocation invocation={toolInvocation} />\n * ```\n */\nexport const ToolInvocation = memo(function ToolInvocation({\n  invocation,\n  className,\n  defaultOpen = false,\n}: ToolInvocationProps) {\n  // Parse widget from result or data - moved up to check for auto-open\n  const widget = useMemo(() => {\n    // Try to parse from widget field first\n    if (invocation.widget) {\n      return parseWidget(invocation.widget);\n    }\n    // Try to parse from result\n    if (invocation.result) {\n      return parseWidget(invocation.result);\n    }\n    return null;\n  }, [invocation.widget, invocation.result]);\n\n  // Default to open for awaiting approval so users can see what they're approving\n  // Also default to open for widgets so users can see them immediately\n  const isAwaitingApprovalStatus = invocation.status === ToolInvocationStatusAwaitingApproval;\n  const [isOpen, setIsOpen] = useState(defaultOpen || isAwaitingApprovalStatus || !!widget);\n\n  // Get actions: submitToolResult for widgets, approveTool/rejectTool/alwaysAllowTool for HIL approval\n  // sendMessage for completed widget actions (e.g., \"Create Variation\" on finished images)\n  const { submitToolResult, approveTool, rejectTool, alwaysAllowTool, sendMessage } = useAgentActions();\n  // Get client for TaskOutputWrapper\n  const client = useAgentClient();\n\n  // Tool names are now direct (no type prefix) - use as-is\n  const functionName = invocation.function?.name || 'tool';\n\n  const status = invocation.status;\n  const isActive =\n    status === ToolInvocationStatusInProgress ||\n    status === ToolInvocationStatusAwaitingInput ||\n    status === ToolInvocationStatusPending;\n\n  // Check if this is an app tool with an execution_id (task)\n  const isAppTool = invocation.type === ToolTypeApp;\n\n  // Try to get task ID from execution_id, or parse from result as fallback\n  const taskId = useMemo(() => {\n    // First try the execution_id field\n    if (invocation.execution_id) {\n      return invocation.execution_id;\n    }\n    // Fallback: try to parse task ID from result text\n    // Result format: \"Task {task_id} {app_name} completed with output: ...\"\n    if (isAppTool && typeof invocation.result === 'string') {\n      const match = invocation.result.match(/^Task\\s+([a-z0-9]+)\\s+/);\n      if (match) {\n        return match[1];\n      }\n    }\n    return null;\n  }, [invocation.execution_id, invocation.result, isAppTool]);\n\n  const hasTaskOutput = isAppTool && taskId;\n\n  // Check if this is a finish tool - render with special FinishBlock component\n  const isFinishTool = functionName === 'finish';\n\n  // Parse finish data from invocation.data (where backend stores ToolFinish)\n  const finishData = useMemo((): ToolFinish | null => {\n    if (!isFinishTool) return null;\n\n    // Try to parse from data field (where backend stores structured ToolFinish)\n    if (invocation.data) {\n      try {\n        // data might be a string or already parsed object\n        const data = typeof invocation.data === 'string'\n          ? JSON.parse(invocation.data)\n          : invocation.data;\n        // Check if it looks like a ToolFinish (has status field)\n        if (data && typeof data.status === 'string') {\n          return data as ToolFinish;\n        }\n      } catch {\n        // Not valid JSON or not a ToolFinish\n      }\n    }\n\n    // Fallback: try to parse from arguments (for in-progress invocations)\n    if (invocation.function?.arguments) {\n      const args = invocation.function.arguments;\n      if (args.status && typeof args.status === 'string') {\n        return {\n          status: args.status as string,\n          result: args.result as string | undefined,\n        };\n      }\n    }\n\n    return null;\n  }, [isFinishTool, invocation.data, invocation.function?.arguments]);\n\n\n  // Check if this tool invocation is blocked on integration requirements\n  const integrationRequirementErrors = useMemo(() => {\n    if (status !== ToolInvocationStatusAwaitingInput || !invocation.data) return null;\n    try {\n      const data = typeof invocation.data === 'string'\n        ? JSON.parse(invocation.data)\n        : invocation.data;\n      if (data?.requirement_errors && Array.isArray(data.requirement_errors)) {\n        return data.requirement_errors;\n      }\n    } catch {\n      // Not requirement data\n    }\n    return null;\n  }, [status, invocation.data]);\n\n  const statusIcon = useMemo(() => {\n    switch (status) {\n      case ToolInvocationStatusPending:\n      case ToolInvocationStatusInProgress:\n        return <Spinner className=\"size-3\" />;\n      case ToolInvocationStatusAwaitingInput:\n      case ToolInvocationStatusAwaitingApproval:\n        return <Clock className=\"h-3 w-3\" />;\n      case ToolInvocationStatusCompleted:\n        return <CheckCircle2 className=\"h-3 w-3 text-emerald-400\" />;\n      case ToolInvocationStatusFailed:\n        return <AlertCircle className=\"h-3 w-3 text-red-400\" />;\n      case ToolInvocationStatusCancelled:\n        return <XCircle className=\"h-3 w-3 text-muted-foreground\" />;\n      default:\n        return <MessageCircleCode className=\"h-3 w-3\" />;\n    }\n  }, [status]);\n\n  const statusText = useMemo(() => {\n    switch (status) {\n      case ToolInvocationStatusPending:\n        return 'pending';\n      case ToolInvocationStatusInProgress:\n        return 'running';\n      case ToolInvocationStatusAwaitingInput:\n        return 'awaiting input';\n      case ToolInvocationStatusAwaitingApproval:\n        return 'awaiting approval';\n      case ToolInvocationStatusCompleted:\n        return 'completed';\n      case ToolInvocationStatusFailed:\n        return 'failed';\n      case ToolInvocationStatusCancelled:\n        return 'cancelled';\n      default:\n        return '';\n    }\n  }, [status]);\n\n\n  // Handle widget actions\n  // - For awaiting_input: Submit tool result to continue current turn\n  // - For completed: Send new message with action context (e.g., \"Create Variation\" on finished images)\n  const handleWidgetAction = useCallback(async (action: WidgetAction, formData?: WidgetFormData) => {\n    const isAwaitingInput = status === ToolInvocationStatusAwaitingInput;\n\n    if (isAwaitingInput) {\n      // Awaiting input: submit tool result to continue current turn\n      if (!submitToolResult) return;\n      try {\n        await submitToolResult(invocation.id, JSON.stringify({ action, form_data: formData }));\n      } catch (error) {\n        console.error('[ToolInvocation] Failed to submit widget action:', error);\n      }\n    } else {\n      // Completed/other: send as new message to start a new turn\n      if (!sendMessage) return;\n      try {\n        // Build message text from action\n        const actionText = action.payload?.message || action.payload?.text || action.type;\n        // Include image URI if present in payload (for image variations)\n        const files: FileRef[] = [];\n        if (action.payload?.image_uri) {\n          files.push({ uri: action.payload.image_uri as string, filename: 'image.png', content_type: 'image/png' });\n        }\n        await sendMessage(String(actionText), files.length > 0 ? files : undefined);\n      } catch (error) {\n        console.error('[ToolInvocation] Failed to send widget action as message:', error);\n      }\n    }\n  }, [invocation.id, status, submitToolResult, sendMessage]);\n\n  // Handle approve/reject for HIL approval (separate from widget submission)\n  const handleApprove = useCallback(async () => {\n    try {\n      await approveTool(invocation.id);\n    } catch (error) {\n      console.error('[ToolInvocation] Failed to approve:', error);\n    }\n  }, [invocation.id, approveTool]);\n\n  const handleReject = useCallback(async () => {\n    try {\n      await rejectTool(invocation.id);\n    } catch (error) {\n      console.error('[ToolInvocation] Failed to reject:', error);\n    }\n  }, [invocation.id, rejectTool]);\n\n  const handleAlwaysAllow = useCallback(async () => {\n    try {\n      await alwaysAllowTool(invocation.id, functionName);\n    } catch (error) {\n      console.error('[ToolInvocation] Failed to always-allow:', error);\n    }\n  }, [invocation.id, functionName, alwaysAllowTool]);\n\n  const hasArgs =\n    invocation.function?.arguments &&\n    Object.keys(invocation.function.arguments).length > 0;\n  // Note: hasResult only applies when there's no widget (widgets are handled separately)\n  const hasResult = !!invocation.result && !widget && !hasTaskOutput;\n\n  // Widget is interactive when awaiting input OR completed (for actions like \"Create Variation\")\n  const isWidgetInteractive = status === ToolInvocationStatusAwaitingInput ||\n    status === ToolInvocationStatusCompleted;\n\n  // For finish tool with standard schema (has status field), use custom FinishBlock\n  // For custom output schemas, fall through to widget rendering\n  if (isFinishTool && (finishData || isActive)) {\n    return (\n      <FinishBlock\n        finish={finishData}\n        isActive={isActive}\n      />\n    );\n  }\n\n  // For awaiting approval, show approval UI\n  if (isAwaitingApprovalStatus) {\n    // If there's a widget, use it\n    if (widget) {\n      return (\n        <div className={cn('flex flex-col items-start', className)}>\n          <WidgetRenderer\n            widget={widget}\n            onAction={handleWidgetAction}\n            className=\"max-w-md\"\n          />\n        </div>\n      );\n    }\n\n    // Otherwise show default approval UI with buttons in footer\n    return (\n      <div className={cn('flex flex-col items-start', className)}>\n        <div className=\"overflow-hidden rounded border bg-muted/10\">\n          {/* Header */}\n          <div className=\"flex items-center gap-1.5 px-2 py-1.5 text-xs text-muted-foreground\">\n            {statusIcon}\n            <span className=\"lowercase\">\n              {functionName} {statusText}\n            </span>\n          </div>\n\n          {/* Arguments */}\n          {hasArgs && (\n            <div className=\"border-t px-2 py-1.5 text-xs\">\n              <div className=\"text-muted-foreground/50 mb-1\"><span>arguments:</span></div>\n              <pre className=\"text-muted-foreground whitespace-pre-wrap overflow-y-auto max-h-[150px]\">\n                {JSON.stringify(invocation.function?.arguments, null, 2)}\n              </pre>\n            </div>\n          )}\n\n          {/* Footer with action buttons */}\n          <div className=\"flex items-center justify-end gap-2 border-t px-2 py-1.5\">\n            <Button\n              size=\"sm\"\n              variant=\"ghost\"\n              className=\"h-6 px-2 text-xs text-muted-foreground hover:text-foreground\"\n              onClick={handleReject}\n            >\n              skip\n            </Button>\n            <Button\n              size=\"sm\"\n              variant=\"ghost\"\n              className=\"h-6 px-2 text-xs text-emerald-400 hover:text-emerald-400/80 hover:bg-emerald-400/10\"\n              onClick={handleApprove}\n            >\n              allow\n            </Button>\n            <Button\n              size=\"sm\"\n              variant=\"ghost\"\n              className=\"h-6 px-2 text-xs text-blue-400 hover:text-blue-400/80 hover:bg-blue-400/10\"\n              onClick={handleAlwaysAllow}\n            >\n              always allow\n            </Button>\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n  // For tools blocked on integration requirements, show the connect/add-permissions card\n  if (integrationRequirementErrors) {\n    return (\n      <IntegrationRequirementCard\n        errors={integrationRequirementErrors}\n        toolNames={[functionName]}\n        className={className}\n      />\n    );\n  }\n\n  // For app tools with task output, show the TaskOutputWrapper\n  if (hasTaskOutput) {\n    return (\n      <CollapsibleSection\n        icon={statusIcon}\n        label={`${functionName} ${statusText}`}\n        open={isOpen}\n        onOpenChange={setIsOpen}\n        isActive={isActive}\n        className={className}\n      >\n        <div className=\"p-2\">\n          <TaskOutputWrapper client={client} taskId={taskId!} compact={true} />\n        </div>\n      </CollapsibleSection>\n    );\n  }\n\n  // Render widget if present\n  if (widget) {\n    return (\n      <div className={cn('flex flex-col items-start flex-grow-0', className)}>\n        <WidgetRenderer\n          widget={widget}\n          onAction={handleWidgetAction}\n          disabled={!isWidgetInteractive}\n        />\n      </div>\n    );\n  }\n\n  return (\n    <CollapsibleSection\n      icon={statusIcon}\n      label={`${functionName} ${statusText}`}\n      open={isOpen}\n      onOpenChange={setIsOpen}\n      isActive={isActive}\n      className={className}\n    >\n      <div className=\"px-2 py-1.5 text-xs space-y-1.5\">\n        {hasArgs && (\n          <div>\n            <div className=\"text-muted-foreground/50 mb-1\"><span>arguments:</span></div>\n            <pre className=\"text-muted-foreground whitespace-pre-wrap overflow-y-auto max-h-[150px]\">\n              {JSON.stringify(invocation.function?.arguments, null, 2)}\n            </pre>\n          </div>\n        )}\n        {hasResult && (\n          <div>\n            <div className=\"text-muted-foreground/50 mb-1\"><span>result:</span></div>\n            <pre className=\"text-foreground whitespace-pre-wrap overflow-y-auto max-h-[150px]\">\n              {typeof invocation.result === 'string'\n                ? invocation.result\n                : JSON.stringify(invocation.result, null, 2)}\n            </pre>\n          </div>\n        )}\n      </div>\n    </CollapsibleSection>\n  );\n});\n\nToolInvocation.displayName = 'ToolInvocation';\n",
      "type": "registry:component",
      "target": "components/infsh/agent/tool-invocation.tsx"
    },
    {
      "path": "components/infsh/agent/tool-invocations.tsx",
      "content": "import React, { memo, useState } from 'react';\nimport { cn } from '@/lib/utils';\nimport { ToolInvocation } from '@/components/infsh/agent/tool-invocation';\nimport {\n  ToolInvocationStatusAwaitingInput,\n  ToolInvocationStatusAwaitingApproval,\n  ToolInvocationStatusFailed,\n  ToolInvocationStatusCompleted,\n  type ChatMessageDTO,\n  type ToolInvocationDTO,\n} from '@inferencesh/sdk';\nimport { ChevronRight } from 'lucide-react';\n\nconst COLLAPSE_THRESHOLD = 3;\n\n// Collapsed CollapsibleSection trigger height\nconst TOOL_ROW_HEIGHT = 20\n// Collapse toggle button: text-xs + icon + gap\nconst COLLAPSE_BUTTON_HEIGHT = 20\n// space-y-1 gap between tool rows\nconst TOOL_GAP = 4\n// Finish block: my-6 (48px) + divider row (~24px) + result card (~48px)\nconst FINISH_BLOCK_HEIGHT = 120\n// Approval UI: header + arguments + buttons\nconst APPROVAL_HEIGHT = 100\n// Widget: variable, but reasonable default (RO corrects)\nconst WIDGET_HEIGHT = 200\n\n/**\n * Returns the predicted height of a single tool invocation.\n * Accounts for different render paths: finish, widget, approval, regular.\n */\nfunction measureSingleTool(inv: ToolInvocationDTO): number {\n  // Finish tool\n  if (inv.function?.name === 'finish') return FINISH_BLOCK_HEIGHT\n\n  // Widget (default open, variable height)\n  if (inv.widget) return WIDGET_HEIGHT\n\n  // Awaiting approval (expanded by default with buttons)\n  if (inv.status === ToolInvocationStatusAwaitingApproval) return APPROVAL_HEIGHT\n\n  // Regular collapsed row\n  return TOOL_ROW_HEIGHT\n}\n\n/**\n * Returns the predicted height of a tool invocations section.\n * Components own their measurement — strategy just calls this.\n */\nexport function measureToolInvocations(invocations: ToolInvocationDTO[] | undefined): number {\n  if (!invocations?.length) return 0\n\n  const count = invocations.length\n  if (count < COLLAPSE_THRESHOLD) {\n    // All shown individually\n    let height = 0\n    for (const inv of invocations) {\n      height += measureSingleTool(inv) + TOOL_GAP\n    }\n    return height - TOOL_GAP // no trailing gap\n  }\n\n  // Prominent (needs attention) shown individually + collapse button for rest\n  let height = 0\n  let collapsibleCount = 0\n  for (const inv of invocations) {\n    if (needsAttention(inv)) {\n      height += measureSingleTool(inv) + TOOL_GAP\n    } else {\n      collapsibleCount++\n    }\n  }\n  if (collapsibleCount > 0) height += COLLAPSE_BUTTON_HEIGHT\n  return height\n}\n\nfunction needsAttention(inv: ToolInvocationDTO): boolean {\n  return inv.status === ToolInvocationStatusAwaitingInput ||\n    inv.status === ToolInvocationStatusAwaitingApproval ||\n    inv.status === ToolInvocationStatusFailed ||\n    !!inv.widget;\n}\n\ninterface ToolInvocationsProps {\n  message: ChatMessageDTO;\n  className?: string;\n}\n\nexport const ToolInvocations = memo(function ToolInvocations({\n  message,\n  className,\n}: ToolInvocationsProps) {\n  const invocations = message.tool_invocations;\n  const [expanded, setExpanded] = useState(false);\n\n  if (!invocations || invocations.length === 0) {\n    return null;\n  }\n\n  if (invocations.length < COLLAPSE_THRESHOLD) {\n    return (\n      <div className={cn('space-y-1', className)}>\n        {invocations.map((inv, idx) => (\n          <ToolInvocation key={inv.id || idx} invocation={inv} />\n        ))}\n      </div>\n    );\n  }\n\n  const prominent: ToolInvocationDTO[] = [];\n  const collapsible: ToolInvocationDTO[] = [];\n  for (const inv of invocations) {\n    (needsAttention(inv) ? prominent : collapsible).push(inv);\n  }\n\n  const completedCount = collapsible.filter(inv => inv.status === ToolInvocationStatusCompleted).length;\n  const runningCount = collapsible.length - completedCount;\n  const summary = runningCount > 0\n    ? `${completedCount} completed, ${runningCount} running`\n    : `${collapsible.length} tool calls`;\n\n  return (\n    <div className={cn('mt-2 space-y-1', className)}>\n      {prominent.map((inv, idx) => (\n        <ToolInvocation key={inv.id || idx} invocation={inv} />\n      ))}\n\n      {collapsible.length > 0 && (\n        <>\n          <button\n            type=\"button\"\n            onClick={() => setExpanded(prev => !prev)}\n            className=\"flex items-center gap-1 px-0.5 text-xs text-muted-foreground/60 hover:text-muted-foreground transition-colors\"\n          >\n            <ChevronRight className={cn('h-3 w-3 transition-transform', expanded && 'rotate-90')} />\n            <span>{summary}</span>\n          </button>\n          {expanded && (\n            <div className=\"space-y-1\">\n              {collapsible.map((inv, idx) => (\n                <ToolInvocation key={inv.id || idx} invocation={inv} />\n              ))}\n            </div>\n          )}\n        </>\n      )}\n    </div>\n  );\n});\n\nToolInvocations.displayName = 'ToolInvocations';\n",
      "type": "registry:component",
      "target": "components/infsh/agent/tool-invocations.tsx"
    },
    {
      "path": "components/infsh/agent/integration-requirement-card.tsx",
      "content": "import React from 'react';\nimport { cn } from '@/lib/utils';\nimport { AlertTriangle, ExternalLink, Shield } from 'lucide-react';\nimport { Button } from '@/components/ui/button';\n\nexport interface RequirementError {\n  type: string;\n  key: string;\n  message: string;\n  action?: {\n    type: string;\n    provider: string;\n    provider_name?: string;\n    scopes?: string[];\n  };\n}\n\ninterface IntegrationRequirementCardProps {\n  errors: RequirementError[];\n  /** Tool names that are blocked (e.g. [\"gmail_list_messages\", \"calendar_list_events\"]) */\n  toolNames?: string[];\n  className?: string;\n}\n\n/**\n * Compact inline card shown in agent chat when tool invocations are blocked on\n * missing integration or scopes. The backend integration projector automatically\n * retries blocked tools when the user connects.\n */\nexport const IntegrationRequirementCard = React.memo(function IntegrationRequirementCard({\n  errors,\n  toolNames,\n  className,\n}: IntegrationRequirementCardProps) {\n  // Group by provider, collect all scopes\n  const providers = new Map<string, { name: string; isConnect: boolean; scopes: string[] }>();\n  for (const err of errors) {\n    const provider = err.action?.provider;\n    if (!provider) continue;\n\n    if (!providers.has(provider)) {\n      providers.set(provider, {\n        name: err.action?.provider_name || provider,\n        isConnect: err.action?.type === 'connect',\n        scopes: [],\n      });\n    }\n\n    const entry = providers.get(provider)!;\n    if (err.action?.scopes) {\n      for (const scope of err.action.scopes) {\n        if (!entry.scopes.includes(scope)) {\n          entry.scopes.push(scope);\n        }\n      }\n    }\n  }\n\n  const providerEntries = Array.from(providers.entries());\n  if (providerEntries.length === 0) return null;\n\n  const isSingleConnect = providerEntries.length === 1 && providerEntries[0][1].isConnect;\n\n  // Format tool names for display\n  const toolsLabel = toolNames && toolNames.length > 0\n    ? toolNames.map(n => n.replace(/_/g, ' ')).join(', ')\n    : null;\n\n  return (\n    <div className={cn('flex flex-col items-start', className)}>\n      <div className=\"overflow-hidden rounded-lg border border-amber-500/20 bg-amber-500/5 max-w-sm w-full\">\n        <div className=\"flex items-center gap-2 px-3 py-2\">\n          <AlertTriangle className=\"h-3.5 w-3.5 text-amber-500 shrink-0\" />\n          <div className=\"flex-1 min-w-0\">\n            <span className=\"text-xs font-medium\">\n              {isSingleConnect\n                ? `${providerEntries[0][1].name} not connected`\n                : `${providerEntries[0][1].name} — missing permissions`}\n            </span>\n            {toolsLabel && (\n              <span className=\"text-[10px] text-muted-foreground ml-1.5\">\n                ({toolsLabel})\n              </span>\n            )}\n          </div>\n          {providerEntries.map(([provider, entry]) => (\n            <Button\n              key={provider}\n              size=\"sm\"\n              variant=\"outline\"\n              className=\"h-6 px-2 text-[11px] shrink-0\"\n              render={<a href={`/settings/integrations/${provider}`} target=\"_blank\" rel=\"noopener noreferrer\" />}\n            >\n                {entry.isConnect ? 'connect' : 'add permissions'}\n                <ExternalLink className=\"ml-1 h-2.5 w-2.5\" />\n            </Button>\n          ))}\n        </div>\n\n        {/* Show scopes if missing permissions (not connect) */}\n        {!isSingleConnect && providerEntries.some(([, e]) => e.scopes.length > 0) && (\n          <div className=\"flex flex-wrap gap-1 px-3 pb-2\">\n            {providerEntries.flatMap(([, entry]) =>\n              entry.scopes.map((scope) => (\n                <span\n                  key={scope}\n                  className=\"inline-flex items-center rounded bg-background/80 px-1.5 py-0.5 text-[10px] font-mono text-muted-foreground\"\n                >\n                  {scope}\n                </span>\n              ))\n            )}\n          </div>\n        )}\n\n        <div className=\"px-3 pb-1.5\">\n          <p className=\"text-[10px] text-muted-foreground/50\">\n            will retry automatically after connecting\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n});\n",
      "type": "registry:component",
      "target": "components/infsh/agent/integration-requirement-card.tsx"
    }
  ]
}
