Composer Trigger Popover

Reusable picker UI for @ mentions, / slash commands, and any other character-triggered popover.

Mention — categories
Slash — flat commands
Back

Getting Started

Add composer-trigger-popover

npx shadcn@latest add https://r.assistant-ui.com/composer-trigger-popover.json

This adds /components/assistant-ui/composer-trigger-popover.tsx — a generic picker UI (Categories + Items + Back) driven by an adapter and an onSelect behavior.

Wrap the composer

Place ComposerPrimitive.Unstable_TriggerPopoverRoot around your composer. Any number of ComposerTriggerPopover declarations can live inside — each with its own triggerId, trigger character, adapter, and behavior.

components/assistant-ui/thread.tsx
import { ComposerPrimitive } from "@assistant-ui/react";
import { ComposerTriggerPopover } from "@/components/assistant-ui/composer-trigger-popover";

const Composer = () => (
  <ComposerPrimitive.Unstable_TriggerPopoverRoot>
    <ComposerPrimitive.Root>
      <ComposerPrimitive.Input placeholder="Type @ to mention, / for commands..." />
      <ComposerPrimitive.Send />

      {/* triggers declared here */}
    </ComposerPrimitive.Root>
  </ComposerPrimitive.Unstable_TriggerPopoverRoot>
);

@ Mention

Pair the popover with unstable_useToolMentionAdapter and an insertDirective behavior so selecting an item writes a :tool[Label]{name=id} directive into the composer text.

import { unstable_useToolMentionAdapter } from "@assistant-ui/react";
import { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
import { WrenchIcon } from "lucide-react";

const mentionAdapter = unstable_useToolMentionAdapter();

<ComposerTriggerPopover
  triggerId="mention"
  char="@"
  adapter={mentionAdapter}
  onSelect={{
    type: "insertDirective",
    formatter: unstable_defaultDirectiveFormatter,
  }}
  fallbackIcon={WrenchIcon}
/>;

Render selected mentions as chips in user messages with DirectiveText. For inline chips inside the composer, use LexicalComposerInput.

/ Slash Command

Use an action behavior so selecting an item removes /command from the composer and runs a handler. iconMap lets you map item.icon strings to Lucide icons.

import {
  unstable_useSlashCommandAdapter,
} from "@assistant-ui/react";
import { FileTextIcon, GlobeIcon, LanguagesIcon, SlashIcon } from "lucide-react";

const slashAdapter = unstable_useSlashCommandAdapter({
  commands: [
    { name: "summarize", description: "Summarize the conversation", icon: "FileText" },
    { name: "translate", description: "Translate to another language", icon: "Languages" },
    { name: "search", description: "Search the web", icon: "Globe" },
  ],
});

<ComposerTriggerPopover
  triggerId="slash"
  char="/"
  adapter={slashAdapter}
  onSelect={{
    type: "action",
    handler: (item) => item.execute?.(),
  }}
  iconMap={{
    FileText: FileTextIcon,
    Languages: LanguagesIcon,
    Globe: GlobeIcon,
  }}
  fallbackIcon={SlashIcon}
/>;

Combining Triggers

Multiple popovers coexist under one TriggerPopoverRoot. Each reads state from its own declaration, so @ and / never collide.

<ComposerPrimitive.Unstable_TriggerPopoverRoot>
  <ComposerPrimitive.Root>
    <ComposerPrimitive.Input placeholder="Type @ to mention, / for commands..." />

    <ComposerTriggerPopover triggerId="mention" char="@" adapter={mentionAdapter} onSelect={/* insertDirective */} fallbackIcon={WrenchIcon} />
    <ComposerTriggerPopover triggerId="slash" char="/" adapter={slashAdapter} onSelect={/* action */} iconMap={slashIcons} fallbackIcon={SlashIcon} />
  </ComposerPrimitive.Root>
</ComposerPrimitive.Unstable_TriggerPopoverRoot>

Keyboard Navigation

KeyAction
ArrowDownHighlight next item
ArrowUpHighlight previous item
EnterSelect highlighted item / drill into category
EscapeClose popover
BackspaceGo back to categories (when query is empty)

API Reference

PropTypeDefaultDescription
triggerIdstringUnique id for this trigger within the root (required)
charstringTrigger character, e.g. "@" or "/" (required)
adapterUnstable_TriggerAdapterProvides categories, items, and search (required)
onSelectUnstable_OnSelectBehavior{ type: "insertDirective", formatter } or { type: "action", handler } (required)
iconMapRecord<string, IconComponent>Maps item.icon / category.icon strings to icons
fallbackIconIconComponentSparklesIconIcon used when no iconMap entry matches
backLabelstring"Back"Back button label
emptyCategoriesLabelstring"No items available"Shown when no categories are available
emptyItemsLabelstring"No matching items"Shown when no items match

All other props (className, etc.) forward to the underlying popover div.