@assistant-ui/react
Version:
Open-source TypeScript/React library for building production-grade AI chat experiences
131 lines (130 loc) • 4.45 kB
JavaScript
"use client";
import { matchesTriggerItemQuery } from "../primitives/composer/trigger/matchesTriggerItemQuery.js";
import { shallowEqualRecords, useModelContextSnapshot } from "./useModelContextSnapshot.js";
import { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
import { useMemo } from "@assistant-ui/tap/react-shim";
import { useAui } from "@assistant-ui/store";
// src/unstable/useMentionAdapter.ts
const EMPTY_TOOL_MENTIONS = Object.freeze({});
const toolMentionSource = {
empty: EMPTY_TOOL_MENTIONS,
read: (aui) => {
const tools = aui.thread.getModelContext().tools;
if (!tools) return EMPTY_TOOL_MENTIONS;
const mentions = Object.create(null);
for (const [name, tool] of Object.entries(tools)) mentions[name] = tool.description;
return mentions;
},
subscribe: (aui, onChange) => {
const unsubscribeContext = aui.on("thread.modelContextUpdate", onChange);
const unsubscribeSelection = aui.on({
scope: "*",
event: "threads.selectionChanged"
}, onChange);
return () => {
unsubscribeContext();
unsubscribeSelection();
};
},
isEqual: shallowEqualRecords
};
/**
* Creates a spreadable `{ adapter, directive }` bundle for `@` mentions.
* Supports tools registered in model context, explicit items, or both —
* flat or categorized.
*
* @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release.
*
* @example
* ```tsx
* const mention = unstable_useMentionAdapter();
* <ComposerTriggerPopover char="@" {...mention} />
* ```
*/
function unstable_useMentionAdapter(options) {
const aui = useAui();
const items = options?.items;
const categories = options?.categories;
const includeTools = options?.includeModelContextTools ?? (!items && !categories);
const toolsConfig = typeof includeTools === "object" ? includeTools : void 0;
const wantsTools = includeTools !== false;
const formatter = options?.formatter;
const onInserted = options?.onInserted;
const isCategorized = categories !== void 0 || toolsConfig?.category !== void 0 && items === void 0;
const toolMentions = useModelContextSnapshot(aui, wantsTools, toolMentionSource);
return {
adapter: useMemo(() => {
const formatLabel = toolsConfig?.formatLabel;
const defaultIcon = toolsConfig?.icon;
const toolItems = wantsTools ? Object.entries(toolMentions).map(([name, description]) => toTriggerItem({
id: name,
type: "tool",
label: formatLabel ? formatLabel(name) : name,
description,
icon: defaultIcon
})) : [];
if (isCategorized) {
const groups = (categories ?? []).map((cat) => ({
id: cat.id,
label: cat.label,
items: cat.items.map(toTriggerItem)
}));
const allGroups = toolItems.length > 0 ? [...groups, {
id: toolsConfig?.category?.id ?? "tools",
label: toolsConfig?.category?.label ?? "Tools",
items: toolItems
}] : groups;
return {
categories: () => allGroups.map(({ id, label }) => ({
id,
label
})),
categoryItems: (id) => allGroups.find((g) => g.id === id)?.items ?? [],
search: (query) => {
const lower = query.toLowerCase();
return allGroups.flatMap((g) => g.items).filter((item) => matchesTriggerItemQuery(item, lower));
}
};
}
const flatItems = (items ?? []).map(toTriggerItem);
const seen = new Set(flatItems.map((i) => i.id));
const flatPool = [...flatItems, ...toolItems.filter((t) => !seen.has(t.id))];
return {
categories: () => [],
categoryItems: () => [],
search: (query) => {
const lower = query.toLowerCase();
return flatPool.filter((item) => matchesTriggerItemQuery(item, lower));
}
};
}, [
items,
categories,
wantsTools,
toolsConfig,
isCategorized,
toolMentions
]),
directive: useMemo(() => ({
formatter: formatter ?? unstable_defaultDirectiveFormatter,
...onInserted ? { onInserted } : {}
}), [formatter, onInserted]),
...options?.iconMap ? { iconMap: options.iconMap } : {},
...options?.fallbackIcon ? { fallbackIcon: options.fallbackIcon } : {}
};
}
function toTriggerItem(m) {
const metadata = m.icon !== void 0 ? {
...m.metadata ?? {},
icon: m.icon
} : m.metadata;
return {
id: m.id,
type: m.type,
label: m.label,
...m.description !== void 0 ? { description: m.description } : {},
...metadata !== void 0 ? { metadata } : {}
};
}
export { unstable_useMentionAdapter };
//# sourceMappingURL=useMentionAdapter.js.map