{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slash-command-menu",
  "type": "registry:ui",
  "title": "Slash Command Menu",
  "description": "Inline “/” command menu anchored to the caret of a textarea, with filtering, groups, icons, descriptions and keyboard navigation.",
  "dependencies": [],
  "registryDependencies": [
    "https://kuro.abstergo.fyi/r/kura-theme.json",
    "https://kuro.abstergo.fyi/r/kbd.json",
    "https://kuro.abstergo.fyi/r/kura-keys.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/kura/slash-command-menu/caret-position.ts",
      "type": "registry:ui",
      "target": "components/kura/slash-command-menu/caret-position.ts",
      "content": "/**\n * Viewport coordinates of a character offset inside a <textarea> or <input>, measured with an\n * off-screen mirror element that copies the field's text layout. Browser-only; returns null\n * during SSR.\n */\nexport interface CaretRect {\n  /** Viewport x of the caret's left edge. */\n  left: number;\n  /** Viewport y of the top of the caret's line. */\n  top: number;\n  /** Line height in px. */\n  height: number;\n}\n\nconst COPIED = [\n  \"direction\",\n  \"boxSizing\",\n  \"width\",\n  \"overflowX\",\n  \"overflowY\",\n  \"borderTopWidth\",\n  \"borderRightWidth\",\n  \"borderBottomWidth\",\n  \"borderLeftWidth\",\n  \"borderStyle\",\n  \"paddingTop\",\n  \"paddingRight\",\n  \"paddingBottom\",\n  \"paddingLeft\",\n  \"fontStyle\",\n  \"fontVariant\",\n  \"fontWeight\",\n  \"fontStretch\",\n  \"fontSize\",\n  \"fontSizeAdjust\",\n  \"lineHeight\",\n  \"fontFamily\",\n  \"fontFeatureSettings\",\n  \"fontVariationSettings\",\n  \"textAlign\",\n  \"textTransform\",\n  \"textIndent\",\n  \"textDecoration\",\n  \"letterSpacing\",\n  \"wordSpacing\",\n  \"tabSize\",\n] as const;\n\nexport function getCaretRect(\n  field: HTMLTextAreaElement | HTMLInputElement,\n  offset: number,\n): CaretRect | null {\n  if (typeof window === \"undefined\" || typeof document === \"undefined\") return null;\n  const rect = field.getBoundingClientRect();\n\n  const style = window.getComputedStyle(field);\n  const mirror = document.createElement(\"div\");\n  const isInput = field.tagName === \"INPUT\";\n  const s = mirror.style;\n  for (const prop of COPIED) s[prop] = style[prop];\n  s.position = \"absolute\";\n  s.visibility = \"hidden\";\n  s.top = \"0\";\n  s.left = \"-9999px\";\n  s.whiteSpace = isInput ? \"pre\" : \"pre-wrap\";\n  s.overflowWrap = isInput ? \"normal\" : \"break-word\";\n  s.overflow = \"hidden\";\n  s.height = \"auto\";\n\n  mirror.textContent = field.value.slice(0, offset);\n  const marker = document.createElement(\"span\");\n  // A zero-width marker would collapse; use the rest of the text so wrapping matches.\n  marker.textContent = field.value.slice(offset) || \".\";\n  mirror.appendChild(marker);\n  document.body.appendChild(mirror);\n\n  const lineHeight =\n    Number.parseFloat(style.lineHeight) || Number.parseFloat(style.fontSize) * 1.25 || 20;\n  const result: CaretRect = {\n    left: rect.left + marker.offsetLeft - field.scrollLeft,\n    top: rect.top + marker.offsetTop - field.scrollTop,\n    height: lineHeight,\n  };\n  document.body.removeChild(mirror);\n  return result;\n}\n"
    },
    {
      "path": "registry/kura/slash-command-menu/index.ts",
      "type": "registry:ui",
      "target": "components/kura/slash-command-menu/index.ts",
      "content": "export { type CaretRect, getCaretRect } from \"./caret-position\";\nexport {\n  type SlashCommand,\n  SlashCommandMenu,\n  type SlashCommandMenuProps,\n  type SlashCommandSelectDetails,\n} from \"./slash-command-menu\";\n"
    },
    {
      "path": "registry/kura/slash-command-menu/slash-command-menu.tsx",
      "type": "registry:ui",
      "target": "components/kura/slash-command-menu/slash-command-menu.tsx",
      "content": "// ARIA listbox pattern: the textarea keeps focus and points at the highlighted option with\n// aria-activedescendant, so options are intentionally not focusable and have no key handlers.\n// biome-ignore-all lint/a11y/useSemanticElements: rich rows cannot be native <option>/<fieldset>.\n// biome-ignore-all lint/a11y/useFocusableInteractive: focus stays in the textarea (activedescendant).\n// biome-ignore-all lint/a11y/useKeyWithClickEvents: keyboard selection is handled on the textarea.\n// biome-ignore-all lint/a11y/noStaticElementInteractions: the panel only prevents mousedown to keep textarea focus.\n\"use client\";\n\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/lib/utils\";\nimport type { KeyName } from \"@/lib/kura/keys\";\nimport { Kbd } from \"@/components/kura/kbd\";\nimport { type CaretRect, getCaretRect } from \"./caret-position\";\n\nexport interface SlashCommand {\n  /** Stable id. */\n  id: string;\n  /** Command name shown in the menu and matched by the query, e.g. \"Summarize\". */\n  label: string;\n  /** One line under/next to the label. */\n  description?: string;\n  /** 16px icon, e.g. a lucide icon. */\n  icon?: React.ReactNode;\n  /** Group heading, e.g. \"Actions\". Groups keep first-seen order. */\n  group?: string;\n  /** Extra words that should match, e.g. [\"tl;dr\", \"shorten\"]. */\n  keywords?: readonly string[];\n  /** Shortcut displayed on the right (display only). */\n  shortcut?: readonly KeyName[];\n  /**\n   * Text that replaces the typed \"/query\" when chosen, e.g. \"Summarize this: \". Omit to just\n   * remove the \"/query\" (action commands such as \"/clear\").\n   */\n  insert?: string;\n  disabled?: boolean;\n}\n\nexport interface SlashCommandSelectDetails {\n  /** What the user had typed after the trigger. */\n  query: string;\n}\n\ntype Field = HTMLTextAreaElement | HTMLInputElement;\n\nexport interface SlashCommandMenuProps\n  extends Omit<React.ComponentProps<\"div\">, \"children\" | \"onSelect\"> {\n  /** The field to watch: a ref to a <textarea> or <input> (e.g. ComposerInput's `ref`). */\n  textareaRef: React.RefObject<Field | null>;\n  /** Available commands. */\n  commands: readonly SlashCommand[];\n  /** Called after the command's `insert` text has replaced the \"/query\". */\n  onSelect?: (command: SlashCommand, details: SlashCommandSelectDetails) => void;\n  /** Called when the menu opens or closes. */\n  onOpenChange?: (open: boolean) => void;\n  /** Trigger character. Default \"/\". It only triggers at the start of a line or after a space. */\n  trigger?: string;\n  /** `auto` opens below the caret unless there is no room, then above. Default \"auto\". */\n  side?: \"auto\" | \"top\" | \"bottom\";\n  /** Default \"No commands\". Pass null to close the menu when nothing matches. */\n  emptyMessage?: string | null;\n  /** Turn the menu off without unmounting it. */\n  disabled?: boolean;\n}\n\ninterface Match {\n  start: number;\n  end: number;\n  query: string;\n}\n\nfunction score(cmd: SlashCommand, q: string): number {\n  if (!q) return 1;\n  const label = cmd.label.toLowerCase();\n  if (label.startsWith(q)) return 4;\n  if (label.split(/[\\s-_/]+/).some((w) => w.startsWith(q))) return 3;\n  if (label.includes(q)) return 2;\n  const extra = [cmd.description ?? \"\", ...(cmd.keywords ?? [])].join(\" \").toLowerCase();\n  return extra.includes(q) ? 1 : 0;\n}\n\nfunction escapeRegExp(s: string): string {\n  return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/** Replace a range of a (possibly React-controlled) field, keeping native undo where possible. */\nfunction replaceRange(field: Field, start: number, end: number, text: string): void {\n  field.focus();\n  field.setSelectionRange(start, end);\n  let ok = false;\n  try {\n    ok =\n      typeof document.execCommand === \"function\" &&\n      document.execCommand(text ? \"insertText\" : \"delete\", false, text);\n  } catch {\n    ok = false;\n  }\n  if (!ok || field.value.slice(start, start + text.length) !== text) {\n    field.setRangeText(text, start, end, \"end\");\n    field.dispatchEvent(new Event(\"input\", { bubbles: true }));\n  }\n}\n\nconst MENU_WIDTH = 320;\nconst GAP = 6;\n\n/**\n * An inline \"/\" command menu anchored to the caret of a textarea. It never takes focus: typing\n * keeps filtering, ↑↓ move the highlight, Enter or Tab inserts, Escape dismisses.\n */\nexport function SlashCommandMenu({\n  textareaRef,\n  commands,\n  onSelect,\n  onOpenChange,\n  trigger = \"/\",\n  side = \"auto\",\n  emptyMessage = \"No commands\",\n  disabled = false,\n  className,\n  style,\n  ...props\n}: SlashCommandMenuProps) {\n  const [match, setMatch] = React.useState<Match | null>(null);\n  const [active, setActive] = React.useState<string | null>(null);\n  const [caret, setCaret] = React.useState<CaretRect | null>(null);\n  const [placement, setPlacement] = React.useState<\"top\" | \"bottom\">(\"bottom\");\n  const dismissedAt = React.useRef<number | null>(null);\n  const menuRef = React.useRef<HTMLDivElement>(null);\n  // First pointer position seen over the open menu. A menu that opens under a resting cursor\n  // receives a synthetic move; only real movement after that may change the highlight.\n  const pointerOrigin = React.useRef<{ x: number; y: number } | null>(null);\n  const baseId = React.useId();\n  const listId = `${baseId}-list`;\n  const optionId = React.useCallback(\n    (id: string) => `${baseId}-opt-${id.replace(/[^a-zA-Z0-9_-]/g, \"_\")}`,\n    [baseId],\n  );\n\n  const query = match?.query.toLowerCase() ?? \"\";\n  const results = React.useMemo(() => {\n    if (!match) return [];\n    return commands\n      .map((cmd, index) => ({ cmd, index, s: score(cmd, query) }))\n      .filter((r) => r.s > 0)\n      .sort((a, b) => b.s - a.s || a.index - b.index)\n      .map((r) => r.cmd);\n  }, [commands, match, query]);\n\n  const groups = React.useMemo(() => {\n    const map = new Map<string, SlashCommand[]>();\n    for (const cmd of results) {\n      const key = cmd.group ?? \"\";\n      const list = map.get(key);\n      if (list) list.push(cmd);\n      else map.set(key, [cmd]);\n    }\n    return [...map.entries()];\n  }, [results]);\n  const ordered = React.useMemo(() => groups.flatMap(([, list]) => list), [groups]);\n  const enabled = React.useMemo(() => ordered.filter((c) => !c.disabled), [ordered]);\n  const open = !!match && !disabled && (results.length > 0 || emptyMessage !== null);\n\n  // Detect \"/query\" right before the caret.\n  const detect = React.useCallback(() => {\n    const field = textareaRef.current;\n    if (!field || disabled) {\n      setMatch(null);\n      return;\n    }\n    const caretPos = field.selectionStart ?? 0;\n    if (caretPos !== field.selectionEnd) {\n      setMatch(null);\n      return;\n    }\n    const before = field.value.slice(0, caretPos);\n    const re = new RegExp(`(^|\\\\s)${escapeRegExp(trigger)}([^\\\\s${escapeRegExp(trigger)}]{0,32})$`);\n    const m = re.exec(before);\n    if (!m) {\n      dismissedAt.current = null;\n      setMatch(null);\n      return;\n    }\n    const q = m[2] ?? \"\";\n    const start = caretPos - q.length - trigger.length;\n    if (dismissedAt.current === start) {\n      setMatch(null);\n      return;\n    }\n    setMatch((prev) =>\n      prev && prev.start === start && prev.end === caretPos && prev.query === q\n        ? prev\n        : { start, end: caretPos, query: q },\n    );\n  }, [textareaRef, trigger, disabled]);\n\n  // Watch the field. Input is observed on `document` (after React's own root listener) so a\n  // controlled field has committed its new value before we react to it.\n  React.useEffect(() => {\n    const field = textareaRef.current;\n    if (!field) return;\n    const onInput = (e: Event) => {\n      if (e.target === field) detect();\n    };\n    const onSelectionChange = () => {\n      if (document.activeElement === field) detect();\n    };\n    const onBlur = () => setMatch(null);\n    document.addEventListener(\"input\", onInput);\n    field.addEventListener(\"click\", detect);\n    field.addEventListener(\"blur\", onBlur);\n    document.addEventListener(\"selectionchange\", onSelectionChange);\n    return () => {\n      document.removeEventListener(\"input\", onInput);\n      field.removeEventListener(\"click\", detect);\n      field.removeEventListener(\"blur\", onBlur);\n      document.removeEventListener(\"selectionchange\", onSelectionChange);\n    };\n  }, [textareaRef, detect]);\n\n  // Keep the highlight valid.\n  React.useEffect(() => {\n    if (!open) {\n      setActive(null);\n      pointerOrigin.current = null;\n      return;\n    }\n    setActive((current) =>\n      current && enabled.some((c) => c.id === current) ? current : (enabled[0]?.id ?? null),\n    );\n  }, [open, enabled]);\n\n  const openRef = React.useRef(open);\n  React.useEffect(() => {\n    if (openRef.current !== open) {\n      openRef.current = open;\n      onOpenChange?.(open);\n    }\n  }, [open, onOpenChange]);\n\n  // Position at the trigger character, flipping above when there is no room below.\n  const reposition = React.useCallback(() => {\n    const field = textareaRef.current;\n    if (!field || !match) return;\n    const rect = getCaretRect(field, match.start);\n    if (!rect) return;\n    setCaret(rect);\n    const menuHeight = menuRef.current?.offsetHeight ?? 280;\n    const below = window.innerHeight - (rect.top + rect.height);\n    const above = rect.top;\n    setPlacement(\n      side === \"auto\" ? (below < menuHeight + GAP + 8 && above > below ? \"top\" : \"bottom\") : side,\n    );\n  }, [textareaRef, match, side]);\n\n  React.useLayoutEffect(() => {\n    if (open) reposition();\n  }, [open, reposition]);\n\n  React.useEffect(() => {\n    if (!open) return;\n    const onChange = () => reposition();\n    window.addEventListener(\"resize\", onChange);\n    window.addEventListener(\"scroll\", onChange, true);\n    return () => {\n      window.removeEventListener(\"resize\", onChange);\n      window.removeEventListener(\"scroll\", onChange, true);\n    };\n  }, [open, reposition]);\n\n  const choose = React.useCallback(\n    (cmd: SlashCommand) => {\n      const field = textareaRef.current;\n      if (!field || !match || cmd.disabled) return;\n      const q = match.query;\n      setMatch(null);\n      dismissedAt.current = null;\n      replaceRange(field, match.start, match.end, cmd.insert ?? \"\");\n      onSelect?.(cmd, { query: q });\n    },\n    [textareaRef, match, onSelect],\n  );\n\n  // Keyboard: handled on the field itself, before React handlers (e.g. Composer's Enter).\n  React.useEffect(() => {\n    const field = textareaRef.current;\n    if (!field || !open) return;\n    const onKeyDown = (event: Event) => {\n      const e = event as KeyboardEvent;\n      if (e.isComposing) return;\n      const move = (delta: number) => {\n        if (enabled.length === 0) return;\n        const i = enabled.findIndex((c) => c.id === active);\n        const next = enabled[(i + delta + enabled.length) % enabled.length];\n        if (next) setActive(next.id);\n      };\n      let handled = true;\n      switch (e.key) {\n        case \"ArrowDown\":\n          move(1);\n          break;\n        case \"ArrowUp\":\n          move(-1);\n          break;\n        case \"Enter\":\n        case \"Tab\": {\n          if (e.shiftKey && e.key === \"Enter\") {\n            handled = false;\n            break;\n          }\n          const cmd = enabled.find((c) => c.id === active);\n          if (cmd) choose(cmd);\n          else handled = false;\n          break;\n        }\n        case \"Escape\":\n          dismissedAt.current = match?.start ?? null;\n          setMatch(null);\n          break;\n        default:\n          handled = false;\n      }\n      if (handled) {\n        e.preventDefault();\n        e.stopPropagation();\n      }\n    };\n    field.addEventListener(\"keydown\", onKeyDown);\n    return () => field.removeEventListener(\"keydown\", onKeyDown);\n  }, [textareaRef, open, enabled, active, choose, match]);\n\n  // ARIA wiring on the field we do not own.\n  React.useEffect(() => {\n    const field = textareaRef.current;\n    if (!field) return;\n    if (open) {\n      field.setAttribute(\"aria-controls\", listId);\n      field.setAttribute(\"aria-autocomplete\", \"list\");\n      if (active) field.setAttribute(\"aria-activedescendant\", optionId(active));\n      else field.removeAttribute(\"aria-activedescendant\");\n    } else {\n      field.removeAttribute(\"aria-controls\");\n      field.removeAttribute(\"aria-activedescendant\");\n      field.removeAttribute(\"aria-autocomplete\");\n    }\n  }, [textareaRef, open, active, listId, optionId]);\n\n  React.useEffect(() => {\n    if (!active) return;\n    document.getElementById(optionId(active))?.scrollIntoView?.({ block: \"nearest\" });\n  }, [active, optionId]);\n\n  if (!open || typeof document === \"undefined\") return null;\n\n  const vw = typeof window !== \"undefined\" ? window.innerWidth : 1024;\n  const left = caret ? Math.max(8, Math.min(caret.left - 10, vw - MENU_WIDTH - 8)) : 0;\n  const positioned: React.CSSProperties = caret\n    ? placement === \"bottom\"\n      ? { left, top: caret.top + caret.height + GAP }\n      : { left, bottom: window.innerHeight - caret.top + GAP }\n    : { left: 0, top: 0, visibility: \"hidden\" };\n\n  return createPortal(\n    <div\n      ref={menuRef}\n      data-slot=\"slash-command-menu\"\n      data-side={placement}\n      data-state=\"open\"\n      onMouseDown={(e) => e.preventDefault()}\n      style={{ ...positioned, width: MENU_WIDTH, ...style }}\n      className={cn(\n        \"fixed z-50 flex max-h-[min(18rem,50vh)] max-w-[calc(100vw-16px)] flex-col overflow-hidden rounded-kura-lg bg-kura-overlay font-kura-sans text-kura-fg shadow-kura-lg backdrop-blur-xl backdrop-saturate-150\",\n        \"animate-kura-pop-in data-[side=bottom]:origin-top-left data-[side=top]:origin-bottom-left\",\n        className,\n      )}\n      {...props}\n    >\n      <div\n        id={listId}\n        role=\"listbox\"\n        aria-label=\"Commands\"\n        className=\"scroll-py-1 overflow-y-auto overscroll-contain p-1 [scrollbar-width:thin]\"\n      >\n        {results.length === 0 && (\n          <div className=\"px-3 py-5 text-center text-[13px] text-kura-fg-subtle\">\n            {emptyMessage}\n          </div>\n        )}\n        {groups.map(([group, list]) => {\n          const headingId = `${baseId}-g-${group.replace(/\\W/g, \"_\") || \"default\"}`;\n          return (\n            <div\n              key={group || \"__default\"}\n              role=\"group\"\n              aria-labelledby={group ? headingId : undefined}\n              data-slot=\"slash-command-menu-group\"\n              className=\"not-first:mt-1\"\n            >\n              {group && (\n                <div\n                  id={headingId}\n                  className=\"select-none px-2 pt-1.5 pb-1 font-medium text-[11px] text-kura-fg-subtle\"\n                >\n                  {group}\n                </div>\n              )}\n              {list.map((cmd) => {\n                const isActive = cmd.id === active;\n                return (\n                  <div\n                    key={cmd.id}\n                    id={optionId(cmd.id)}\n                    role=\"option\"\n                    aria-selected={isActive}\n                    aria-disabled={cmd.disabled || undefined}\n                    data-slot=\"slash-command-menu-item\"\n                    data-active={isActive || undefined}\n                    data-disabled={cmd.disabled || undefined}\n                    onPointerMove={(e) => {\n                      const origin = pointerOrigin.current;\n                      if (!origin) {\n                        pointerOrigin.current = { x: e.clientX, y: e.clientY };\n                        return;\n                      }\n                      if (origin.x === e.clientX && origin.y === e.clientY) return;\n                      if (!cmd.disabled && !isActive) setActive(cmd.id);\n                    }}\n                    onClick={() => choose(cmd)}\n                    className=\"flex cursor-pointer select-none items-center gap-2.5 rounded-kura-md px-1.5 py-1.5 data-active:bg-kura-hover data-disabled:cursor-not-allowed data-disabled:opacity-45\"\n                  >\n                    <span\n                      aria-hidden\n                      className=\"flex size-7 shrink-0 items-center justify-center rounded-kura-sm bg-kura-surface text-kura-fg-muted shadow-kura-xs [&_svg]:size-3.5\"\n                    >\n                      {cmd.icon ?? (\n                        <span className=\"font-kura-mono text-[11px] leading-none\">{trigger}</span>\n                      )}\n                    </span>\n                    <span className=\"flex min-w-0 flex-1 flex-col\">\n                      <span className=\"truncate font-medium text-[13px] leading-5\">\n                        <Highlight text={cmd.label} query={query} />\n                      </span>\n                      {cmd.description && (\n                        <span className=\"truncate text-kura-fg-subtle text-xs leading-4\">\n                          {cmd.description}\n                        </span>\n                      )}\n                    </span>\n                    {cmd.shortcut && (\n                      <Kbd keys={cmd.shortcut} variant=\"ghost\" size=\"sm\" className=\"shrink-0\" />\n                    )}\n                  </div>\n                );\n              })}\n            </div>\n          );\n        })}\n      </div>\n      <div\n        data-slot=\"slash-command-menu-footer\"\n        aria-hidden\n        className=\"flex h-7 shrink-0 items-center gap-3 px-3 text-[11px] text-kura-fg-subtle shadow-[inset_0_1px_0_var(--kura-border-subtle)]\"\n      >\n        <span className=\"inline-flex items-center gap-1\">\n          <Kbd keys={[\"up\"]} variant=\"ghost\" size=\"sm\" />\n          <Kbd keys={[\"down\"]} variant=\"ghost\" size=\"sm\" />\n          navigate\n        </span>\n        <span className=\"inline-flex items-center gap-1\">\n          <Kbd keys={[\"enter\"]} variant=\"ghost\" size=\"sm\" />\n          insert\n        </span>\n        <span className=\"inline-flex items-center gap-1\">\n          <Kbd keys={[\"esc\"]} variant=\"ghost\" size=\"sm\" />\n          dismiss\n        </span>\n      </div>\n      <span role=\"status\" className=\"sr-only\">\n        {results.length === 0\n          ? (emptyMessage ?? \"\")\n          : `${results.length} ${results.length === 1 ? \"command\" : \"commands\"} available`}\n      </span>\n    </div>,\n    document.body,\n  );\n}\n\nfunction Highlight({ text, query }: { text: string; query: string }) {\n  if (!query) return <>{text}</>;\n  const i = text.toLowerCase().indexOf(query);\n  if (i === -1) return <>{text}</>;\n  return (\n    <>\n      <span className=\"text-kura-fg-muted\">{text.slice(0, i)}</span>\n      <span className=\"text-kura-fg\">{text.slice(i, i + query.length)}</span>\n      <span className=\"text-kura-fg-muted\">{text.slice(i + query.length)}</span>\n    </>\n  );\n}\n"
    }
  ],
  "categories": [
    "input",
    "slash",
    "commands",
    "autocomplete",
    "mention"
  ],
  "docs": "Docs: https://kuro.abstergo.fyi/components/slash-command-menu · Markdown for agents: https://kuro.abstergo.fyi/components/slash-command-menu.md",
  "meta": {
    "kura": {
      "status": "beta",
      "whenToUse": [
        "Let users insert prompt templates or run actions by typing “/” in a composer (/summarize, /debug, /clear).",
        "Any textarea or input that needs caret-anchored suggestions triggered by a character (use `trigger` for “@” mentions or “#” tags)."
      ],
      "whenNotToUse": [
        "A global ⌘K launcher for navigation and actions — use command-palette.",
        "Choosing a model from a toolbar — use model-picker.",
        "Static clickable starters in an empty chat — use prompt-suggestions."
      ]
    }
  }
}
