{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "widgets",
  "title": "Widget Renderer",
  "description": "Renders declarative UI nodes (forms, buttons, cards) from agent tool outputs.",
  "type": "registry:block",
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "checkbox",
    "input",
    "label",
    "select",
    "textarea",
    "https://inference.sh/ui/r/pretext-md.json"
  ],
  "files": [
    {
      "path": "components/infsh/agent/widget-renderer.tsx",
      "content": "\"use client\"\nimport { Markdown } from \"@/lib/pretext-md/react\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\"\nimport type { Widget, WidgetAction, WidgetFormData, WidgetNode } from \"@/components/infsh/agent/widget-types\"\nimport { cn } from \"@/lib/utils\"\nimport React, { createContext, useContext, useState } from \"react\"\n\n// Use the shared form data type\ntype FormData = WidgetFormData\n\n// Local variant types (shadcn doesn't export the Props types)\ntype ButtonVariant = \"default\" | \"secondary\" | \"outline\" | \"ghost\" | \"destructive\" | \"link\"\ntype BadgeVariant = \"default\" | \"secondary\" | \"outline\" | \"destructive\"\n\nconst toButtonVariant = (v?: string): ButtonVariant => {\n  if (v === \"default\" || v === \"secondary\" || v === \"outline\" || v === \"ghost\" || v === \"destructive\" || v === \"link\") {\n    return v\n  }\n  return \"default\"\n}\n\nconst toBadgeVariant = (v?: string): BadgeVariant => {\n  if (v === \"default\" || v === \"secondary\" || v === \"outline\" || v === \"destructive\") {\n    return v\n  }\n  return \"default\"\n}\n\n// Context for handling widget actions\ntype WidgetContextValue = {\n  onAction: (action: WidgetAction, formData?: FormData) => void\n  formData: FormData\n  setFormValue: (name: string, value: string | boolean) => void\n  isDisabled: boolean\n  loadingActionType: string | null\n}\n\nconst WidgetContext = createContext<WidgetContextValue | null>(null)\n\nconst useWidgetContext = () => {\n  const ctx = useContext(WidgetContext)\n  if (!ctx) throw new Error(\"useWidgetContext must be used within WidgetRenderer\")\n  return ctx\n}\n\n// Helper to map text variant to CSS classes\nconst toTextClasses = (variant?: string): string => {\n  switch (variant) {\n    case \"muted\":\n      return \"text-xs text-muted-foreground\"\n    case \"bold\":\n      return \"text-sm font-medium\"\n    case \"title\":\n      return \"text-sm font-medium\"\n    case \"small\":\n      return \"text-xs\"\n    default:\n      return \"text-sm\"\n  }\n}\n\n// Helper to map size to font size classes\nconst toSizeClasses = (size?: string): string => {\n  switch (size) {\n    case \"xs\": return \"text-xs\"\n    case \"sm\": return \"text-sm\"\n    case \"md\": return \"text-base\"\n    case \"lg\": return \"text-lg\"\n    case \"xl\": return \"text-xl\"\n    case \"2xl\": return \"text-2xl\"\n    case \"3xl\": return \"text-3xl\"\n    default: return \"text-sm\"\n  }\n}\n\n// Helper to map weight to font weight classes\nconst toWeightClasses = (weight?: string): string => {\n  switch (weight) {\n    case \"normal\": return \"font-normal\"\n    case \"medium\": return \"font-medium\"\n    case \"semibold\": return \"font-semibold\"\n    case \"bold\": return \"font-bold\"\n    default: return \"\"\n  }\n}\n\n// Helper to map align to flex classes\nconst toAlignClasses = (align?: string): string => {\n  switch (align) {\n    case \"start\": return \"items-start\"\n    case \"center\": return \"items-center\"\n    case \"end\": return \"items-end\"\n    case \"baseline\": return \"items-baseline\"\n    case \"stretch\": return \"items-stretch\"\n    default: return \"\"\n  }\n}\n\n// Helper to map justify to flex classes\nconst toJustifyClasses = (justify?: string): string => {\n  switch (justify) {\n    case \"start\": return \"justify-start\"\n    case \"center\": return \"justify-center\"\n    case \"end\": return \"justify-end\"\n    case \"between\": return \"justify-between\"\n    case \"around\": return \"justify-around\"\n    case \"evenly\": return \"justify-evenly\"\n    default: return \"\"\n  }\n}\n\n// Helper to map radius to border radius classes\nconst toRadiusClasses = (radius?: string): string => {\n  switch (radius) {\n    case \"none\": return \"rounded-none\"\n    case \"sm\": return \"rounded-sm\"\n    case \"md\": return \"rounded-md\"\n    case \"lg\": return \"rounded-lg\"\n    case \"xl\": return \"rounded-xl\"\n    case \"2xl\": return \"rounded-2xl\"\n    case \"full\": return \"rounded-full\"\n    default: return \"rounded-md\"\n  }\n}\n\n// Helper to map background to CSS classes or inline styles\ntype BackgroundResult = { className?: string; style?: React.CSSProperties }\n\nconst toBackgroundStyles = (background?: string | { light?: string; dark?: string }): BackgroundResult => {\n  if (!background) return {}\n\n  if (typeof background === \"object\") {\n    const value = background.dark || background.light\n    if (!value) return {}\n    return toBackgroundStyles(value)\n  }\n\n  // Semantic tokens\n  switch (background) {\n    case \"surface-secondary\":\n      return { className: \"bg-muted\" }\n    case \"surface-tertiary\":\n      return { className: \"bg-muted/50\" }\n\n    // Preset gradients\n    case \"gradient-blue\":\n      return { className: \"bg-gradient-to-br from-blue-500/60 to-indigo-500/60\" }\n    case \"gradient-purple\":\n      return { className: \"bg-gradient-to-br from-purple-500/60 to-pink-500/60\" }\n    case \"gradient-warm\":\n      return { className: \"bg-gradient-to-br from-orange-500/60 to-red-500/60\" }\n    case \"gradient-cool\":\n      return { className: \"bg-gradient-to-br from-cyan-500/60 to-emerald-500/60\" }\n    case \"gradient-sunset\":\n      return { className: \"bg-gradient-to-br from-rose-500/60 to-orange-500/60\" }\n    case \"gradient-ocean\":\n      return { className: \"bg-gradient-to-br from-blue-600/60 to-teal-500/60\" }\n    case \"gradient-forest\":\n      return { className: \"bg-gradient-to-br from-green-600/60 to-teal-600/60\" }\n    case \"gradient-midnight\":\n      return { className: \"bg-gradient-to-br from-slate-700/60 to-purple-900/60\" }\n\n    // Alpha tokens\n    case \"alpha-5\":\n      return { style: { background: \"rgba(255, 255, 255, 0.05)\" } }\n    case \"alpha-10\":\n      return { style: { background: \"rgba(255, 255, 255, 0.10)\" } }\n    case \"alpha-15\":\n      return { style: { background: \"rgba(255, 255, 255, 0.15)\" } }\n    case \"alpha-20\":\n      return { style: { background: \"rgba(255, 255, 255, 0.20)\" } }\n    case \"alpha-30\":\n      return { style: { background: \"rgba(255, 255, 255, 0.30)\" } }\n    case \"alpha-50\":\n      return { style: { background: \"rgba(255, 255, 255, 0.50)\" } }\n\n    // Basic color tokens\n    case \"white\":\n      return { className: \"bg-white\" }\n    case \"black\":\n      return { className: \"bg-black\" }\n    case \"transparent\":\n      return { className: \"bg-transparent\" }\n    case \"primary\":\n      return { className: \"bg-primary\" }\n    case \"accent\":\n      return { className: \"bg-accent\" }\n  }\n\n  // Raw CSS value\n  if (background.startsWith(\"#\") ||\n    background.startsWith(\"rgb\") ||\n    background.startsWith(\"hsl\") ||\n    background.startsWith(\"linear-gradient\") ||\n    background.startsWith(\"radial-gradient\")) {\n    return { style: { background } }\n  }\n\n  return {}\n}\n\n// Recursive node renderer\nconst NodeRenderer = React.memo(({ node }: { node: WidgetNode }) => {\n  const { onAction, formData, setFormValue, isDisabled, loadingActionType } = useWidgetContext()\n\n  // Get the effective action (onClickAction or deprecated action)\n  const getClickAction = (n: WidgetNode): WidgetAction | undefined => n.onClickAction || n.action\n\n  switch (node.type) {\n    case \"text\":\n      return <span className={toTextClasses(node.variant)}>{node.value ?? \"\"}</span>\n\n    case \"title\":\n      return (\n        <h3 className={cn(\n          \"font-semibold leading-tight\",\n          toSizeClasses(node.size),\n          toWeightClasses(node.weight)\n        )}>\n          {node.value ?? \"\"}\n        </h3>\n      )\n\n    case \"caption\":\n      return (\n        <p className={cn(\n          \"text-xs text-muted-foreground\",\n          node.color === \"tertiary\" && \"opacity-70\"\n        )}>\n          {node.value ?? \"\"}\n        </p>\n      )\n\n    case \"label\":\n      return (\n        <Label htmlFor={node.fieldName} className=\"text-sm font-medium\">\n          {node.value ?? \"\"}\n        </Label>\n      )\n\n    case \"markdown\":\n      return <Markdown content={node.value ?? \"\"} />\n\n    case \"image\":\n      return (\n        <img\n          src={node.src ?? \"\"}\n          alt={node.alt ?? \"\"}\n          className=\"rounded-md max-w-full h-auto\"\n          style={{\n            height: typeof node.height === 'number' ? `${node.height}px` : node.height,\n            width: typeof node.width === 'number' ? `${node.width}px` : node.width,\n          }}\n        />\n      )\n\n    case \"badge\":\n      return <Badge variant={toBadgeVariant(node.variant)} className=\"lowercase\">{node.label}</Badge>\n\n    case \"icon\":\n      return (\n        <span className={cn(\n          \"inline-flex items-center justify-center\",\n          node.size === \"sm\" ? \"w-4 h-4 text-sm\" :\n            node.size === \"lg\" ? \"w-6 h-6 text-lg\" : \"w-5 h-5 text-base\"\n        )}>\n          {node.iconName || \"●\"}\n        </span>\n      )\n\n    case \"button\": {\n      const clickAction = getClickAction(node)\n      const isLoading = loadingActionType === clickAction?.type\n      const shouldShowSelfLoading = clickAction?.loadingBehavior === 'self' || clickAction?.loadingBehavior === 'auto'\n      return (\n        <Button\n          variant={toButtonVariant(node.variant)}\n          size=\"sm\"\n          disabled={isDisabled || node.disabled || isLoading}\n          onClick={() => clickAction && onAction(clickAction, formData)}\n        >\n          {isLoading && shouldShowSelfLoading ? \"...\" : node.label}\n        </Button>\n      )\n    }\n\n    case \"input\":\n      return (\n        <Input\n          name={node.name}\n          placeholder={node.placeholder}\n          value={(node.name ? formData[node.name] as string : node.defaultValue) ?? node.defaultValue ?? \"\"}\n          onChange={(e) => node.name && setFormValue(node.name, e.target.value)}\n          disabled={isDisabled || !!node.disabled}\n          required={node.required}\n          className=\"w-full\"\n        />\n      )\n\n    case \"textarea\":\n      return (\n        <textarea\n          name={node.name}\n          placeholder={node.placeholder}\n          value={(node.name ? formData[node.name] as string : node.defaultValue) ?? node.defaultValue ?? \"\"}\n          onChange={(e) => node.name && setFormValue(node.name, e.target.value)}\n          disabled={isDisabled || !!node.disabled}\n          required={node.required}\n          rows={node.rows || 3}\n          className=\"w-full min-h-[80px] rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\"\n        />\n      )\n\n    case \"select\":\n      return (\n        <Select\n          value={node.name ? (formData[node.name] as string | undefined) : undefined}\n          onValueChange={(value) => node.name && setFormValue(node.name, value ?? \"\")}\n          disabled={isDisabled || !!node.disabled}\n        >\n          <SelectTrigger className=\"w-full\">\n            <SelectValue placeholder={node.placeholder ?? \"Select...\"} />\n          </SelectTrigger>\n          <SelectContent>\n            {(node.options ?? []).map((opt) => (\n              <SelectItem key={opt.value} value={opt.value}>\n                {opt.label}\n              </SelectItem>\n            ))}\n          </SelectContent>\n        </Select>\n      )\n\n    case \"checkbox\": {\n      const checkboxId = `checkbox-${node.name}`\n      const isChecked = node.name ? (formData[node.name] as boolean | undefined) ?? node.defaultChecked : node.defaultChecked\n      return (\n        <div className=\"flex items-center space-x-2\">\n          <Checkbox\n            id={checkboxId}\n            checked={isChecked}\n            disabled={isDisabled || !!node.disabled}\n            onCheckedChange={(checked) => node.name && setFormValue(node.name, !!checked)}\n          />\n          <Label htmlFor={checkboxId} className={cn(\"text-sm\", (isDisabled || node.disabled) ? \"cursor-default\" : \"cursor-pointer\")}>\n            {node.label}\n          </Label>\n        </div>\n      )\n    }\n\n    case \"radio-group\": {\n      const radioValue = node.name ? (formData[node.name] as string | undefined) : undefined\n      return (\n        <div className={cn(\n          \"flex gap-2\",\n          node.direction === \"row\" ? \"flex-row flex-wrap\" : \"flex-col\"\n        )}>\n          {(node.options ?? []).map((opt) => {\n            const radioId = `radio-${node.name}-${opt.value}`\n            return (\n              <div key={opt.value} className=\"flex items-center space-x-2\">\n                <input\n                  type=\"radio\"\n                  id={radioId}\n                  name={node.name}\n                  value={opt.value}\n                  checked={radioValue === opt.value}\n                  onChange={() => node.name && setFormValue(node.name, opt.value)}\n                  disabled={isDisabled || !!node.disabled}\n                  className=\"h-4 w-4 border-border text-primary focus:ring-ring\"\n                />\n                <Label htmlFor={radioId} className=\"text-sm\">{opt.label}</Label>\n              </div>\n            )\n          })}\n        </div>\n      )\n    }\n\n    case \"row\":\n      return (\n        <div className={cn(\n          \"flex flex-row flex-wrap\",\n          toAlignClasses(node.align) || \"items-center\",\n          toJustifyClasses(node.justify),\n          `gap-${node.gap ?? 2}`\n        )}>\n          {(node.children ?? []).map((child, i) => (\n            <NodeRenderer key={i} node={child} />\n          ))}\n        </div>\n      )\n\n    case \"col\":\n      return (\n        <div className={cn(\n          \"flex flex-col\",\n          toAlignClasses(node.align),\n          toJustifyClasses(node.justify),\n          `gap-${node.gap ?? 2}`\n        )}>\n          {(node.children ?? []).map((child, i) => (\n            <NodeRenderer key={i} node={child} />\n          ))}\n        </div>\n      )\n\n    case \"box\": {\n      const bgStyles = toBackgroundStyles(node.background as string | { light?: string; dark?: string } | undefined)\n      const boxStyle: React.CSSProperties = {\n        ...bgStyles.style,\n        minHeight: typeof node.minHeight === 'number' ? `${node.minHeight}px` : node.minHeight,\n        maxHeight: typeof node.maxHeight === 'number' ? `${node.maxHeight}px` : node.maxHeight,\n        minWidth: typeof node.minWidth === 'number' ? `${node.minWidth}px` : node.minWidth,\n        maxWidth: typeof node.maxWidth === 'number' ? `${node.maxWidth}px` : node.maxWidth,\n        aspectRatio: node.aspectRatio,\n      }\n      return (\n        <div\n          className={cn(\n            \"flex flex-col\",\n            typeof node.padding === 'number' ? `p-${node.padding}` : \"p-3\",\n            bgStyles.className,\n            toRadiusClasses(node.radius),\n            `gap-${node.gap ?? 2}`\n          )}\n          style={boxStyle}\n        >\n          {(node.children ?? []).map((child, i) => (\n            <NodeRenderer key={i} node={child} />\n          ))}\n        </div>\n      )\n    }\n\n    case \"spacer\":\n      return <div className=\"flex-1\" style={{ minHeight: typeof node.minSize === 'number' ? node.minSize : undefined }} />\n\n    case \"divider\":\n      return (\n        <div className={cn(\n          \"w-full h-px bg-border\",\n          typeof node.spacing === 'number' ? `my-${node.spacing}` : \"my-2\"\n        )} />\n      )\n\n    case \"form\":\n      return (\n        <form\n          className={cn(\n            \"flex\",\n            node.direction === \"row\" ? \"flex-row flex-wrap\" : \"flex-col\",\n            `gap-${node.gap ?? 3}`\n          )}\n          onSubmit={(e) => {\n            e.preventDefault()\n            if (node.onSubmitAction) {\n              onAction(node.onSubmitAction, formData)\n            }\n          }}\n        >\n          {(node.children ?? []).map((child, i) => (\n            <NodeRenderer key={i} node={child} />\n          ))}\n        </form>\n      )\n\n    default:\n      console.warn(`Unknown widget node type: ${node.type}`)\n      return null\n  }\n})\nNodeRenderer.displayName = \"NodeRenderer\"\n\n// Helper to extract default form values from widget nodes\nconst extractDefaultFormValues = (nodes: WidgetNode[]): FormData => {\n  const defaults: FormData = {}\n\n  const processNode = (node: WidgetNode) => {\n    // Extract default values from form fields\n    if (node.name) {\n      switch (node.type) {\n        case 'checkbox':\n          defaults[node.name] = node.defaultChecked ?? false\n          break\n        case 'input':\n        case 'textarea':\n          defaults[node.name] = node.defaultValue ?? ''\n          break\n        case 'select':\n        case 'radio-group':\n          defaults[node.name] = '' // Empty string means no selection\n          break\n      }\n    }\n\n    // Recursively process children\n    if (node.children) {\n      node.children.forEach(processNode)\n    }\n  }\n\n  nodes.forEach(processNode)\n  return defaults\n}\n\n// Main widget renderer\nexport interface WidgetRendererProps {\n  widget: Widget | string\n  onAction?: (action: WidgetAction, formData?: FormData) => void | Promise<void>\n  className?: string\n  disabled?: boolean\n  /** Wrap content in a Card (default: only when title exists) */\n  asCard?: boolean\n}\n\nexport const WidgetRenderer = React.memo(({ widget, onAction, className, disabled, asCard }: WidgetRendererProps) => {\n  // Initialize form data with default values from widget nodes\n  const [formData, setFormData] = useState<FormData>(() => {\n    if (typeof widget === 'string') return {}\n    return extractDefaultFormValues(widget.children ?? [])\n  })\n  const [isSubmitting, setIsSubmitting] = useState(false)\n  const [loadingActionType, setLoadingActionType] = useState<string | null>(null)\n\n  const setFormValue = (name: string, value: string | boolean) => {\n    setFormData((prev) => ({ ...prev, [name]: value }))\n  }\n\n  const handleAction = async (action: WidgetAction, actionFormData?: FormData) => {\n    if (disabled || isSubmitting) return\n\n    // Merge form data with action payload (payload values override form data)\n    const mergedFormData: FormData = { ...actionFormData }\n    if (action.payload) {\n      for (const [key, value] of Object.entries(action.payload)) {\n        if (typeof value === 'string' || typeof value === 'boolean' || value === undefined) {\n          mergedFormData[key] = value\n        }\n      }\n    }\n\n    // Set loading state based on loadingBehavior\n    if (action.loadingBehavior !== 'none') {\n      setIsSubmitting(true)\n      if (action.loadingBehavior === 'self' || action.loadingBehavior === 'auto') {\n        setLoadingActionType(action.type)\n      }\n    }\n\n    try {\n      await onAction?.(action, mergedFormData)\n    } finally {\n      setIsSubmitting(false)\n      setLoadingActionType(null)\n    }\n  }\n\n  const isContainerLoading = isSubmitting && loadingActionType === null\n  const isDisabled = disabled || isContainerLoading\n\n  // Handle string widget (fallback)\n  if (typeof widget === \"string\") {\n    return (\n      <div className={cn(\"w-min-0 w-max-full border border-border rounded-xl bg-card p-4\", className)}>\n        <pre>{widget}</pre>\n      </div>\n    )\n  }\n\n  // Determine if we should use Card wrapper\n  const shouldUseCard = asCard ?? !!widget.title\n\n  const content = (\n    <div className={cn(\"space-y-3\", isContainerLoading && \"opacity-70 pointer-events-none\")}>\n      {(widget.children ?? []).map((node, i) => (\n        <NodeRenderer key={i} node={node} />\n      ))}\n    </div>\n  )\n\n  if (shouldUseCard) {\n    return (\n      <WidgetContext.Provider value={{ onAction: handleAction, formData, setFormValue, isDisabled, loadingActionType }}>\n        <div className={cn(\"w-min-0 w-max-full border border-border rounded-xl bg-card p-4\", className)}>\n          {widget.title && (\n            <div className=\"text-sm font-medium mb-3\">{widget.title}</div>\n          )}\n          {content}\n        </div>\n      </WidgetContext.Provider>\n    )\n  }\n\n  // No Card wrapper - just render content directly\n  return (\n    <WidgetContext.Provider value={{ onAction: handleAction, formData, setFormValue, isDisabled, loadingActionType }}>\n      <div className={cn(\"w-full\", className)}>\n        {content}\n      </div>\n    </WidgetContext.Provider>\n  )\n})\nWidgetRenderer.displayName = \"WidgetRenderer\"\n",
      "type": "registry:component",
      "target": "components/infsh/agent/widget-renderer.tsx"
    },
    {
      "path": "components/infsh/agent/widget-types.ts",
      "content": "export const WidgetTypeUI = 'ui';\nexport const WidgetTypeHTML = 'html';\n\n// Widget action\nexport interface WidgetAction {\n  type: string;\n  payload?: Record<string, unknown>;\n  handler?: 'server' | 'client';\n  loadingBehavior?: 'auto' | 'self' | 'container' | 'none';\n}\n\n// Widget form data\nexport interface WidgetFormData {\n  [key: string]: string | boolean | undefined;\n}\n\n// Widget node (tree structure)\nexport interface WidgetNode {\n  type: string;\n  value?: string;\n  variant?: string;\n  size?: string;\n  weight?: string;\n  color?: string;\n  fieldName?: string;\n  src?: string;\n  alt?: string;\n  height?: number | string;\n  width?: number | string;\n  label?: string;\n  iconName?: string;\n  action?: WidgetAction;\n  onClickAction?: WidgetAction;\n  disabled?: boolean;\n  name?: string;\n  placeholder?: string;\n  defaultValue?: string;\n  required?: boolean;\n  rows?: number;\n  options?: { value: string; label: string }[];\n  defaultChecked?: boolean;\n  children?: WidgetNode[];\n  gap?: number;\n  align?: string;\n  justify?: string;\n  direction?: string;\n  padding?: number;\n  background?: string | { light?: string; dark?: string };\n  radius?: string;\n  minHeight?: number | string;\n  maxHeight?: number | string;\n  minWidth?: number | string;\n  maxWidth?: number | string;\n  aspectRatio?: string;\n  minSize?: number;\n  spacing?: number;\n  onSubmitAction?: WidgetAction;\n  asForm?: boolean;\n  confirmAction?: WidgetAction;\n}\n\n// Widget structure\nexport interface Widget {\n  type: string;\n  title?: string;\n  html?: string;\n  content?: string;\n  children?: WidgetNode[];\n  actions?: { label: string; variant?: string; action: WidgetAction }[];\n  [key: string]: unknown;\n}\n\n// Valid widget types - \"ui\" (interactive) and \"html\" (static)\n// Also accept \"card\" for backwards compatibility (legacy data)\nconst VALID_WIDGET_TYPES = [WidgetTypeUI, \"card\"];\n\n// Helper to parse widget JSON from tool result or Widget object\nexport function parseWidget(input: string | Widget | unknown): Widget | null {\n  // If it's already a Widget object\n  if (input && typeof input === \"object\") {\n    // Unwrap { widget: { ... } } envelope if present\n    const maybeEnvelope = input as Record<string, unknown>;\n    if (maybeEnvelope.widget && typeof maybeEnvelope.widget === \"object\") {\n      return parseWidget(maybeEnvelope.widget);\n    }\n\n    const widget = input as Widget;\n    if (VALID_WIDGET_TYPES.includes(widget.type)) {\n      // Normalize \"card\" to \"ui\" for consistency\n      if (widget.type === \"card\") {\n        widget.type = WidgetTypeUI;\n      }\n      return widget;\n    }\n    return null;\n  }\n\n  // If it's a JSON string\n  if (typeof input === \"string\") {\n    try {\n      const parsed = JSON.parse(input);\n      // Recursively parse - handles both direct widget and { widget: { ... } } envelope\n      return parseWidget(parsed);\n    } catch {\n      return null;\n    }\n  }\n\n  return null;\n}\n",
      "type": "registry:lib",
      "target": "components/infsh/agent/widget-types.ts"
    }
  ]
}
