Slash Commands

Let users type / in the composer to trigger predefined actions from a popover picker.

Slash commands let users type / in the composer to open a popover, browse available commands, and execute one. Unlike mentions (which only insert a directive into the message), slash commands additionally fire an action callback at the moment of selection.

How It Works

User types "/"  →  Trigger detected  →  Adapter provides commands

              Callback fired  ←  User selects command from popover

       Directive chip left in composer (or removed if removeOnExecute)

The slash command system is built on the same trigger popover architecture as mentions. A slash command declares its behavior with a <TriggerPopover.Action> sub-primitive whose onExecute callback fires when an item is chosen.

By default Action leaves a directive chip in the composer — giving the user (and the LLM) an audit trail of which commands were invoked. Pass removeOnExecute to strip the /command text entirely.

Quick Start

1. Define Commands with unstable_useSlashCommandAdapter

Declare commands (data + execute bundled together, like useAssistantTool). The hook returns { adapter, action } — wire both into a single <TriggerPopover>:

import {
  ComposerPrimitive,
  unstable_useSlashCommandAdapter,
  type Unstable_SlashCommand,
} from "@assistant-ui/react";

const SLASH_COMMANDS: readonly Unstable_SlashCommand[] = [
  {
    id: "summarize",
    description: "Summarize the conversation",
    execute: () => console.log("Summarize!"),
  },
  {
    id: "translate",
    description: "Translate text to another language",
    execute: () => console.log("Translate!"),
  },
  {
    id: "help",
    description: "List all available commands",
    execute: () => console.log("Help!"),
  },
];

function MyComposer() {
  const slash = unstable_useSlashCommandAdapter({ commands: SLASH_COMMANDS });

  return (
    <ComposerPrimitive.Unstable_TriggerPopoverRoot>
      <ComposerPrimitive.Root>
        <ComposerPrimitive.Input placeholder="Type / for commands..." />
        <ComposerPrimitive.Send>Send</ComposerPrimitive.Send>

        <ComposerPrimitive.Unstable_TriggerPopover
          char="/"
          adapter={slash.adapter}
          className="popover"
        >
          <ComposerPrimitive.Unstable_TriggerPopover.Action {...slash.action} />
          <ComposerPrimitive.Unstable_TriggerPopoverItems>
            {(items) =>
              items.map((item, index) => (
                <ComposerPrimitive.Unstable_TriggerPopoverItem
                  key={item.id}
                  item={item}
                  index={index}
                  className="popover-item"
                >
                  <strong>{item.label}</strong>
                  {item.description && <span>{item.description}</span>}
                </ComposerPrimitive.Unstable_TriggerPopoverItem>
              ))
            }
          </ComposerPrimitive.Unstable_TriggerPopoverItems>
        </ComposerPrimitive.Unstable_TriggerPopover>
      </ComposerPrimitive.Root>
    </ComposerPrimitive.Unstable_TriggerPopoverRoot>
  );
}

The label defaults to /${id}; override via label on the command. Icons are strings that your iconMap on the picker UI resolves to components (see ComposerTriggerPopover).

2. Controlling the Chip

By default, a selected /summarize is converted into a directive chip (:command[/summarize]{name=summarize}) in the composer text and the command's execute fires. This keeps an audit trail of which commands were invoked.

To strip the trigger text entirely — useful for purely transient commands — pass removeOnExecute on the hook options:

const slash = unstable_useSlashCommandAdapter({
  commands: SLASH_COMMANDS,
  removeOnExecute: true,
});

3. Custom Dispatch

For side effects on top of execute (logging, analytics, intercept), wrap the hook's onExecute:

<ComposerPrimitive.Unstable_TriggerPopover.Action
  onExecute={(item) => {
    logCommandUsed(item.id);
    slash.action.onExecute(item);
  }}
/>

Categorized Commands

For categorized navigation (drill-down into groups), return categories from categories() and items from categoryItems(). The popover shows categories first, then items within the selected category:

import type { Unstable_TriggerAdapter } from "@assistant-ui/core";

const adapter: Unstable_TriggerAdapter = {
  categories() {
    return [
      { id: "actions", label: "Actions" },
      { id: "export", label: "Export" },
    ];
  },

  categoryItems(categoryId) {
    if (categoryId === "actions") {
      return [
        { id: "summarize", type: "command", label: "/summarize", description: "Summarize the conversation" },
        { id: "translate", type: "command", label: "/translate", description: "Translate text" },
      ];
    }
    if (categoryId === "export") {
      return [
        { id: "pdf", type: "command", label: "/export pdf", description: "Export as PDF" },
        { id: "markdown", type: "command", label: "/export md", description: "Export as Markdown" },
      ];
    }
    return [];
  },

  // Optional — enables search across all categories
  search(query) {
    const lower = query.toLowerCase();
    const all = [...this.categoryItems("actions"), ...this.categoryItems("export")];
    return all.filter(
      (item) => item.label.toLowerCase().includes(lower) || item.description?.toLowerCase().includes(lower),
    );
  },
};

When using a categorized adapter, add TriggerPopoverCategories to your popover UI:

const commandHandlers: Record<string, () => void> = {
  summarize: () => {/* ... */},
  pdf: () => {/* ... */},
};

<ComposerPrimitive.Unstable_TriggerPopover
  char="/"
  adapter={adapter}
>
  <ComposerPrimitive.Unstable_TriggerPopover.Action
    formatter={unstable_defaultDirectiveFormatter}
    onExecute={(item) => commandHandlers[item.id]?.()}
  />
  <ComposerPrimitive.Unstable_TriggerPopoverBack>← Back</ComposerPrimitive.Unstable_TriggerPopoverBack>
  <ComposerPrimitive.Unstable_TriggerPopoverCategories>
    {(categories) => categories.map((cat) => (
      <ComposerPrimitive.Unstable_TriggerPopoverCategoryItem key={cat.id} categoryId={cat.id}>
        {cat.label}
      </ComposerPrimitive.Unstable_TriggerPopoverCategoryItem>
    ))}
  </ComposerPrimitive.Unstable_TriggerPopoverCategories>
  <ComposerPrimitive.Unstable_TriggerPopoverItems>
    {(items) => items.map((item, index) => (
      <ComposerPrimitive.Unstable_TriggerPopoverItem key={item.id} item={item} index={index}>
        {item.label}
      </ComposerPrimitive.Unstable_TriggerPopoverItem>
    ))}
  </ComposerPrimitive.Unstable_TriggerPopoverItems>
</ComposerPrimitive.Unstable_TriggerPopover>

Combining with Mentions

Slash commands and mentions live under the same TriggerPopoverRoot. Declare one TriggerPopover per trigger — each with its own behavior sub-primitive:

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

    {/* @ mention popover */}
    <ComposerPrimitive.Unstable_TriggerPopover
      char="@"
      adapter={mention.adapter}
    >
      <ComposerPrimitive.Unstable_TriggerPopover.Directive {...mention.directive} />
      <ComposerPrimitive.Unstable_TriggerPopoverItems>
        {(items) => items.map((item) => (
          <ComposerPrimitive.Unstable_TriggerPopoverItem key={item.id} item={item}>
            {item.label}
          </ComposerPrimitive.Unstable_TriggerPopoverItem>
        ))}
      </ComposerPrimitive.Unstable_TriggerPopoverItems>
    </ComposerPrimitive.Unstable_TriggerPopover>

    {/* / slash command popover */}
    <ComposerPrimitive.Unstable_TriggerPopover
      char="/"
      adapter={slash.adapter}
    >
      <ComposerPrimitive.Unstable_TriggerPopover.Action {...slash.action} />
      <ComposerPrimitive.Unstable_TriggerPopoverItems>
        {(items) => items.map((item, index) => (
          <ComposerPrimitive.Unstable_TriggerPopoverItem key={item.id} item={item} index={index}>
            {item.label}
          </ComposerPrimitive.Unstable_TriggerPopoverItem>
        ))}
      </ComposerPrimitive.Unstable_TriggerPopoverItems>
    </ComposerPrimitive.Unstable_TriggerPopover>
  </ComposerPrimitive.Root>
</ComposerPrimitive.Unstable_TriggerPopoverRoot>

Each TriggerPopover is its own scope — the @ popover and the / popover read state from their own declaration and never collide. Keyboard events route to whichever popover is currently active.

Keyboard Navigation

Same keyboard bindings as mentions:

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

Trigger Popover Architecture

Both mentions and slash commands are built on a generic trigger popover system:

  • ComposerPrimitive.Unstable_TriggerPopoverRoot — root provider that groups triggers and owns the input plugin registry
  • ComposerPrimitive.Unstable_TriggerPopover — declares one trigger (id, char, adapter) and renders its popover container
  • Behavior sub-primitives — exactly one per TriggerPopover:
    • Unstable_TriggerPopover.Directive — writes a formatted directive on selection ("mention" path)
    • Unstable_TriggerPopover.Action — fires a callback on selection ("slash" path); inserts a chip by default, strip with removeOnExecute
  • Shared sub-primitives (TriggerPopoverCategories, TriggerPopoverItems, TriggerPopoverBack) live inside a TriggerPopover

You can declare any number of triggers under one root and mix behavior types.

ComposerInput Plugin Protocol

Under the hood, each TriggerPopover registers a ComposerInputPlugin with the composer input. This is a generic protocol that decouples the input from any specific trigger:

type ComposerInputPlugin = {
  handleKeyDown(e: KeyboardEvent): boolean;
  setCursorPosition(pos: number): void;
};

The input iterates over registered plugins for keyboard events and cursor changes. This is what enables multiple triggers to coexist without conflict.

Primitives Reference

PrimitiveDescription
Unstable_TriggerPopoverRootRoot — groups triggers, provides input plugin registry
Unstable_TriggerPopoverDeclares a trigger and renders its popover container
Unstable_TriggerPopover.DirectiveBehavior sub-primitive — inserts a formatted directive on selection
Unstable_TriggerPopover.ActionBehavior sub-primitive — runs onExecute on selection; chip-by-default
Unstable_TriggerPopoverCategoriesRender-function for the top-level category list
Unstable_TriggerPopoverCategoryItemButton that drills into a category (role="option", auto data-highlighted)
Unstable_TriggerPopoverItemsRender-function for items within the active category or search results
Unstable_TriggerPopoverItemButton that selects an item (role="option", auto data-highlighted)
Unstable_TriggerPopoverBackButton that navigates back from items to categories