@gram-ai/elements 1.34.0 → 1.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. package/dist/compat-shims-CO9JXXV4.cjs.map +1 -1
  2. package/dist/compat-shims-DxtUrORi.js.map +1 -1
  3. package/dist/compat-shims.d.ts +9 -8
  4. package/dist/components/Chat/index.d.ts +2 -1
  5. package/dist/components/ChatHistory.d.ts +1 -1
  6. package/dist/components/FrontendTools/index.d.ts +1 -1
  7. package/dist/components/Replay.d.ts +1 -1
  8. package/dist/components/Replay.stories.d.ts +2 -2
  9. package/dist/components/ShadowRoot.d.ts +1 -1
  10. package/dist/components/ShareButton/index.d.ts +1 -1
  11. package/dist/components/assistant-ui/thinking-indicator.d.ts +8 -0
  12. package/dist/components/ui/avatar.d.ts +3 -3
  13. package/dist/components/ui/button.d.ts +2 -2
  14. package/dist/components/ui/buttonVariants.d.ts +2 -2
  15. package/dist/components/ui/calendar.d.ts +2 -1
  16. package/dist/components/ui/collapsible.d.ts +3 -3
  17. package/dist/components/ui/dialog.d.ts +10 -10
  18. package/dist/components/ui/popover.d.ts +4 -4
  19. package/dist/components/ui/skeleton.d.ts +1 -1
  20. package/dist/components/ui/time-range-picker.d.ts +2 -1
  21. package/dist/components/ui/tool-ui.d.ts +9 -7
  22. package/dist/components/ui/tooltip.d.ts +4 -4
  23. package/dist/contexts/ConnectionStatusContext.d.ts +1 -1
  24. package/dist/contexts/ElementsProvider.d.ts +1 -1
  25. package/dist/contexts/ToolApprovalContext.d.ts +2 -2
  26. package/dist/contexts/ToolExecutionContext.d.ts +1 -1
  27. package/dist/contexts/portal-container.d.ts +1 -1
  28. package/dist/elements.cjs +1 -1
  29. package/dist/elements.css +1 -1
  30. package/dist/elements.js +2 -2
  31. package/dist/hooks/useDensity.d.ts +1 -1
  32. package/dist/hooks/useElements.d.ts +2 -1
  33. package/dist/hooks/useGramThreadListAdapter.d.ts +13 -0
  34. package/dist/hooks/useRadius.d.ts +1 -1
  35. package/dist/hooks/useThemeProps.d.ts +1 -1
  36. package/dist/hooks/useToolApproval.d.ts +2 -1
  37. package/dist/index-7fTI_vaV.cjs +194 -0
  38. package/dist/index-7fTI_vaV.cjs.map +1 -0
  39. package/dist/{index-B5lZrrO2.js → index-Bv-yE4G1.js} +2919 -2806
  40. package/dist/index-Bv-yE4G1.js.map +1 -0
  41. package/dist/{index-BFU6NvbL.js → index-BziIHO9O.js} +9621 -9308
  42. package/dist/index-BziIHO9O.js.map +1 -0
  43. package/dist/{index-C08dvTEo.cjs → index-CGBkMd0d.cjs} +48 -48
  44. package/dist/index-CGBkMd0d.cjs.map +1 -0
  45. package/dist/lib/errorTracking.d.ts +1 -1
  46. package/dist/lib/tools.d.ts +11 -10
  47. package/dist/plugins/generative-ui/catalog.d.ts +3 -3
  48. package/dist/plugins/generative-ui/ui/accordion-wrapper.d.ts +2 -2
  49. package/dist/plugins/generative-ui/ui/accordion.d.ts +4 -4
  50. package/dist/plugins/generative-ui/ui/action-button.d.ts +1 -1
  51. package/dist/plugins/generative-ui/ui/alert-wrapper.d.ts +2 -1
  52. package/dist/plugins/generative-ui/ui/alert.d.ts +3 -3
  53. package/dist/plugins/generative-ui/ui/avatar-wrapper.d.ts +2 -1
  54. package/dist/plugins/generative-ui/ui/avatar.d.ts +6 -6
  55. package/dist/plugins/generative-ui/ui/badge.d.ts +2 -2
  56. package/dist/plugins/generative-ui/ui/button-wrapper.d.ts +2 -1
  57. package/dist/plugins/generative-ui/ui/button.d.ts +3 -3
  58. package/dist/plugins/generative-ui/ui/card-wrapper.d.ts +1 -1
  59. package/dist/plugins/generative-ui/ui/card.d.ts +7 -7
  60. package/dist/plugins/generative-ui/ui/checkbox-wrapper.d.ts +2 -1
  61. package/dist/plugins/generative-ui/ui/checkbox.d.ts +1 -1
  62. package/dist/plugins/generative-ui/ui/data-table.d.ts +1 -1
  63. package/dist/plugins/generative-ui/ui/dialog.d.ts +10 -10
  64. package/dist/plugins/generative-ui/ui/dropdown-menu.d.ts +15 -15
  65. package/dist/plugins/generative-ui/ui/grid.d.ts +1 -1
  66. package/dist/plugins/generative-ui/ui/index.d.ts +57 -40
  67. package/dist/plugins/generative-ui/ui/input-wrapper.d.ts +2 -1
  68. package/dist/plugins/generative-ui/ui/input.d.ts +1 -1
  69. package/dist/plugins/generative-ui/ui/label.d.ts +1 -1
  70. package/dist/plugins/generative-ui/ui/list.d.ts +2 -1
  71. package/dist/plugins/generative-ui/ui/metric.d.ts +1 -1
  72. package/dist/plugins/generative-ui/ui/pagination.d.ts +7 -7
  73. package/dist/plugins/generative-ui/ui/popover.d.ts +7 -7
  74. package/dist/plugins/generative-ui/ui/progress.d.ts +1 -1
  75. package/dist/plugins/generative-ui/ui/radio-group.d.ts +2 -2
  76. package/dist/plugins/generative-ui/ui/select-wrapper.d.ts +2 -1
  77. package/dist/plugins/generative-ui/ui/select.d.ts +10 -10
  78. package/dist/plugins/generative-ui/ui/separator.d.ts +1 -1
  79. package/dist/plugins/generative-ui/ui/skeleton-wrapper.d.ts +2 -1
  80. package/dist/plugins/generative-ui/ui/skeleton.d.ts +1 -1
  81. package/dist/plugins/generative-ui/ui/stack.d.ts +1 -1
  82. package/dist/plugins/generative-ui/ui/switch.d.ts +1 -1
  83. package/dist/plugins/generative-ui/ui/table.d.ts +8 -8
  84. package/dist/plugins/generative-ui/ui/tabs-wrapper.d.ts +2 -2
  85. package/dist/plugins/generative-ui/ui/tabs.d.ts +4 -4
  86. package/dist/plugins/generative-ui/ui/text.d.ts +1 -1
  87. package/dist/plugins/generative-ui/ui/textarea.d.ts +1 -1
  88. package/dist/plugins/generative-ui/ui/tooltip.d.ts +4 -4
  89. package/dist/plugins.cjs +1 -1
  90. package/dist/plugins.js +1 -1
  91. package/dist/{profiler-BRnyr1GA.cjs → profiler-DuJEf_S6.cjs} +2 -2
  92. package/dist/{profiler-BRnyr1GA.cjs.map → profiler-DuJEf_S6.cjs.map} +1 -1
  93. package/dist/{profiler-KLSTpp6I.js → profiler-xLXOPfmj.js} +2 -2
  94. package/dist/{profiler-KLSTpp6I.js.map → profiler-xLXOPfmj.js.map} +1 -1
  95. package/dist/react-shim.cjs.map +1 -1
  96. package/dist/react-shim.d.ts +1 -1
  97. package/dist/react-shim.js +1 -4
  98. package/dist/react-shim.js.map +1 -1
  99. package/dist/server/bun.cjs.map +1 -1
  100. package/dist/server/bun.js.map +1 -1
  101. package/dist/server/express.cjs.map +1 -1
  102. package/dist/server/express.js.map +1 -1
  103. package/dist/server/fastify.cjs.map +1 -1
  104. package/dist/server/fastify.js.map +1 -1
  105. package/dist/server/hono.cjs.map +1 -1
  106. package/dist/server/hono.js.map +1 -1
  107. package/dist/server/nextjs.cjs.map +1 -1
  108. package/dist/server/nextjs.js.map +1 -1
  109. package/dist/server/tanstack-start.cjs.map +1 -1
  110. package/dist/server/tanstack-start.js.map +1 -1
  111. package/dist/{startRecording-CKx-YWbq.cjs → startRecording-C2XF9-Ol.cjs} +2 -2
  112. package/dist/{startRecording-CKx-YWbq.cjs.map → startRecording-C2XF9-Ol.cjs.map} +1 -1
  113. package/dist/{startRecording-BfxB1xxR.js → startRecording-qKnXr4lw.js} +2 -2
  114. package/dist/{startRecording-BfxB1xxR.js.map → startRecording-qKnXr4lw.js.map} +1 -1
  115. package/dist/types/index.d.ts +29 -3
  116. package/package.json +8 -11
  117. package/src/compat-shims.ts +16 -2
  118. package/src/components/Chat/index.tsx +4 -1
  119. package/src/components/Chat/stories/FrontendTools.stories.tsx +1 -1
  120. package/src/components/Chat/stories/ToolApproval.stories.tsx +2 -2
  121. package/src/components/Chat/stories/Tools.stories.tsx +13 -5
  122. package/src/components/ChatHistory.tsx +3 -1
  123. package/src/components/FrontendTools/index.tsx +1 -1
  124. package/src/components/MessageContent.tsx +1 -0
  125. package/src/components/Replay.stories.tsx +2 -3
  126. package/src/components/Replay.tsx +17 -10
  127. package/src/components/ShadowRoot.tsx +2 -2
  128. package/src/components/ShareButton/index.tsx +4 -2
  129. package/src/components/assistant-ui/assistant-modal.tsx +5 -3
  130. package/src/components/assistant-ui/attachment.tsx +1 -1
  131. package/src/components/assistant-ui/error-boundary.tsx +1 -1
  132. package/src/components/assistant-ui/markdown-text.tsx +1 -1
  133. package/src/components/assistant-ui/thinking-indicator.tsx +175 -0
  134. package/src/components/assistant-ui/thread.tsx +251 -27
  135. package/src/components/assistant-ui/tool-fallback.tsx +8 -8
  136. package/src/components/assistant-ui/tool-group.tsx +4 -13
  137. package/src/components/assistant-ui/tool-mention-autocomplete.tsx +1 -1
  138. package/src/components/ui/avatar.tsx +3 -3
  139. package/src/components/ui/calendar.tsx +1 -1
  140. package/src/components/ui/collapsible.tsx +7 -3
  141. package/src/components/ui/dialog.tsx +18 -10
  142. package/src/components/ui/generative-ui.tsx +9 -4
  143. package/src/components/ui/popover.tsx +4 -4
  144. package/src/components/ui/skeleton.tsx +4 -1
  145. package/src/components/ui/time-range-picker.stories.tsx +164 -154
  146. package/src/components/ui/time-range-picker.tsx +11 -5
  147. package/src/components/ui/tool-ui.tsx +68 -40
  148. package/src/components/ui/tooltip.tsx +4 -4
  149. package/src/contexts/ChatIdContext.tsx +1 -1
  150. package/src/contexts/ConnectionStatusContext.tsx +6 -5
  151. package/src/contexts/ElementsProvider.tsx +64 -41
  152. package/src/contexts/ReplayContext.ts +1 -1
  153. package/src/contexts/ToolApprovalContext.tsx +5 -1
  154. package/src/contexts/ToolExecutionContext.tsx +1 -1
  155. package/src/contexts/portal-container.tsx +1 -1
  156. package/src/global.css +55 -16
  157. package/src/hooks/useAuth.ts +2 -1
  158. package/src/hooks/useDensity.ts +1 -1
  159. package/src/hooks/useElements.ts +2 -1
  160. package/src/hooks/useFollowOnSuggestions.ts +3 -6
  161. package/src/hooks/useGramThreadListAdapter.tsx +50 -3
  162. package/src/hooks/useMCPTools.ts +2 -2
  163. package/src/hooks/useModel.ts +1 -3
  164. package/src/hooks/usePluginComponents.ts +3 -1
  165. package/src/hooks/useRadius.ts +1 -1
  166. package/src/hooks/useSession.ts +3 -1
  167. package/src/hooks/useThemeProps.ts +5 -5
  168. package/src/hooks/useToolApproval.ts +2 -1
  169. package/src/lib/cassette.ts +20 -8
  170. package/src/lib/errorTracking.ts +1 -4
  171. package/src/lib/messageConverter.test.ts +11 -13
  172. package/src/lib/messageConverter.ts +13 -4
  173. package/src/lib/token.ts +2 -5
  174. package/src/lib/tool-mentions.ts +5 -2
  175. package/src/lib/tools.byte-cap.test.ts +1 -1
  176. package/src/lib/tools.test.ts +1 -1
  177. package/src/lib/tools.ts +15 -5
  178. package/src/lib/utils.ts +2 -2
  179. package/src/lib.d.ts +8 -1
  180. package/src/plugins/chart/chart.test.ts +3 -4
  181. package/src/plugins/chart/component.tsx +7 -6
  182. package/src/plugins/chart/ui/area-chart.tsx +1 -1
  183. package/src/plugins/chart/ui/line-chart.tsx +1 -1
  184. package/src/plugins/generative-ui/ui/accordion-wrapper.tsx +2 -2
  185. package/src/plugins/generative-ui/ui/accordion.tsx +4 -4
  186. package/src/plugins/generative-ui/ui/action-button.tsx +4 -2
  187. package/src/plugins/generative-ui/ui/alert-wrapper.tsx +1 -1
  188. package/src/plugins/generative-ui/ui/alert.tsx +7 -3
  189. package/src/plugins/generative-ui/ui/avatar-wrapper.tsx +5 -1
  190. package/src/plugins/generative-ui/ui/avatar.tsx +12 -6
  191. package/src/plugins/generative-ui/ui/badge.tsx +1 -1
  192. package/src/plugins/generative-ui/ui/button-wrapper.tsx +1 -1
  193. package/src/plugins/generative-ui/ui/button.tsx +1 -1
  194. package/src/plugins/generative-ui/ui/card-wrapper.tsx +1 -1
  195. package/src/plugins/generative-ui/ui/card.tsx +28 -7
  196. package/src/plugins/generative-ui/ui/checkbox-wrapper.tsx +1 -1
  197. package/src/plugins/generative-ui/ui/checkbox.tsx +1 -1
  198. package/src/plugins/generative-ui/ui/data-table.tsx +1 -1
  199. package/src/plugins/generative-ui/ui/dialog.tsx +15 -10
  200. package/src/plugins/generative-ui/ui/dropdown-menu.tsx +33 -15
  201. package/src/plugins/generative-ui/ui/grid.tsx +1 -1
  202. package/src/plugins/generative-ui/ui/index.ts +154 -40
  203. package/src/plugins/generative-ui/ui/input-wrapper.tsx +1 -1
  204. package/src/plugins/generative-ui/ui/input.tsx +5 -1
  205. package/src/plugins/generative-ui/ui/label.tsx +1 -1
  206. package/src/plugins/generative-ui/ui/list.tsx +5 -1
  207. package/src/plugins/generative-ui/ui/metric.tsx +2 -1
  208. package/src/plugins/generative-ui/ui/pagination.tsx +12 -7
  209. package/src/plugins/generative-ui/ui/popover.tsx +13 -7
  210. package/src/plugins/generative-ui/ui/progress.tsx +1 -1
  211. package/src/plugins/generative-ui/ui/radio-group.tsx +2 -2
  212. package/src/plugins/generative-ui/ui/select-wrapper.tsx +1 -1
  213. package/src/plugins/generative-ui/ui/select.tsx +14 -10
  214. package/src/plugins/generative-ui/ui/separator.tsx +1 -1
  215. package/src/plugins/generative-ui/ui/skeleton-wrapper.tsx +1 -1
  216. package/src/plugins/generative-ui/ui/skeleton.tsx +4 -1
  217. package/src/plugins/generative-ui/ui/stack.tsx +1 -1
  218. package/src/plugins/generative-ui/ui/switch.tsx +1 -1
  219. package/src/plugins/generative-ui/ui/table.tsx +29 -8
  220. package/src/plugins/generative-ui/ui/tabs-wrapper.tsx +5 -2
  221. package/src/plugins/generative-ui/ui/tabs.tsx +4 -4
  222. package/src/plugins/generative-ui/ui/text.tsx +1 -1
  223. package/src/plugins/generative-ui/ui/textarea.tsx +4 -1
  224. package/src/plugins/generative-ui/ui/tooltip.tsx +4 -4
  225. package/src/react-shim.ts +9 -4
  226. package/src/server/bun.ts +1 -1
  227. package/src/server/express.ts +1 -1
  228. package/src/server/fastify.ts +1 -1
  229. package/src/server/hono.ts +1 -1
  230. package/src/server/nextjs.ts +1 -1
  231. package/src/server/tanstack-start.ts +1 -1
  232. package/src/storybook.d.ts +5 -0
  233. package/src/types/index.ts +39 -3
  234. package/dist/index-B5lZrrO2.js.map +0 -1
  235. package/dist/index-BFU6NvbL.js.map +0 -1
  236. package/dist/index-C08dvTEo.cjs.map +0 -1
  237. package/dist/index-DzZ1-jQY.cjs +0 -194
  238. package/dist/index-DzZ1-jQY.cjs.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  ArrowDownIcon,
3
3
  ArrowUpIcon,
4
+ AtSign,
4
5
  CheckIcon,
5
6
  ChevronLeftIcon,
6
7
  ChevronRightIcon,
@@ -8,8 +9,10 @@ import {
8
9
  CopyIcon,
9
10
  DownloadIcon,
10
11
  PencilIcon,
12
+ Search,
11
13
  Settings2,
12
14
  Square,
15
+ Wrench,
13
16
  } from "lucide-react";
14
17
 
15
18
  import {
@@ -20,6 +23,7 @@ import {
20
23
  ImageMessagePartProps,
21
24
  MessagePrimitive,
22
25
  ThreadPrimitive,
26
+ useAssistantApi,
23
27
  useAssistantState,
24
28
  } from "@assistant-ui/react";
25
29
 
@@ -51,6 +55,7 @@ import { MarkdownText } from "@/components/assistant-ui/markdown-text";
51
55
  import { MentionedToolsBadges } from "@/components/assistant-ui/mentioned-tools-badges";
52
56
  import { MessageFeedback } from "@/components/assistant-ui/message-feedback";
53
57
  import { Reasoning, ReasoningGroup } from "@/components/assistant-ui/reasoning";
58
+ import { ThinkingIndicator } from "@/components/assistant-ui/thinking-indicator";
54
59
  import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
55
60
  import { ToolMentionAutocomplete } from "@/components/assistant-ui/tool-mention-autocomplete";
56
61
  import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
@@ -68,6 +73,10 @@ import { useToolMentions } from "@/hooks/useToolMentions";
68
73
  import { getApiUrl } from "@/lib/api";
69
74
  import { EASE_OUT_QUINT } from "@/lib/easing";
70
75
  import { MODELS } from "@/lib/models";
76
+ import {
77
+ type MentionableTool,
78
+ toolSetToMentionableTools,
79
+ } from "@/lib/tool-mentions";
71
80
  import { cn } from "@/lib/utils";
72
81
  import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
73
82
  import {
@@ -262,7 +271,7 @@ const ThreadScrollToBottom: FC = () => {
262
271
  const ThreadWelcome: FC = () => {
263
272
  const { config } = useElements();
264
273
  const d = useDensity();
265
- const { title, subtitle } = config.welcome ?? {};
274
+ const { logo, title, subtitle } = config.welcome ?? {};
266
275
  const isStandalone = config.variant === "standalone";
267
276
 
268
277
  return (
@@ -288,6 +297,19 @@ const ThreadWelcome: FC = () => {
288
297
  !isStandalone && d("py-md"),
289
298
  )}
290
299
  >
300
+ {logo && (
301
+ <m.img
302
+ src={logo}
303
+ alt=""
304
+ initial={{ opacity: 0, y: 10 }}
305
+ animate={{ opacity: 1, y: 0 }}
306
+ exit={{ opacity: 0, y: 10 }}
307
+ transition={{ duration: 0.25, ease: EASE_OUT_QUINT }}
308
+ className={cn(
309
+ "aui-thread-welcome-logo mb-2 size-12 object-contain",
310
+ )}
311
+ />
312
+ )}
291
313
  <m.div
292
314
  initial={{ opacity: 0, y: 10 }}
293
315
  animate={{ opacity: 1, y: 0 }}
@@ -524,7 +546,9 @@ const ComposerFeedback: FC = () => {
524
546
  <MessageFeedback
525
547
  className="mx-auto"
526
548
  onResolved={setResolved}
527
- onFeedback={handleFeedback}
549
+ onFeedback={(type) => {
550
+ void handleFeedback(type);
551
+ }}
528
552
  />
529
553
  </m.div>
530
554
  )}
@@ -794,6 +818,218 @@ const ComposerCassetteRecorder: FC = () => {
794
818
  );
795
819
  };
796
820
 
821
+ // Sentinel for the "All" pseudo-category in the tool-mention picker.
822
+ const TOOL_MENTION_ALL_CATEGORY = "__all__";
823
+
824
+ function humanizeToolCategory(raw: string): string {
825
+ const cleaned = raw.replace(/[-_]+/g, " ").trim();
826
+ if (!cleaned) return "Tools";
827
+ return cleaned.replace(/\b\w/g, (c) => c.toUpperCase());
828
+ }
829
+
830
+ // Derive a grouping label for a tool. Tools from multiple MCP servers are
831
+ // namespaced as `<server>__<tool>`; otherwise group by the first
832
+ // underscore-delimited segment (e.g. `platform_search_logs` -> "Platform"),
833
+ // falling back to a single "Tools" bucket.
834
+ function deriveToolCategory(name: string): string {
835
+ const namespaceIdx = name.indexOf("__");
836
+ if (namespaceIdx > 0)
837
+ return humanizeToolCategory(name.slice(0, namespaceIdx));
838
+ const underscoreIdx = name.indexOf("_");
839
+ if (underscoreIdx > 0)
840
+ return humanizeToolCategory(name.slice(0, underscoreIdx));
841
+ return "Tools";
842
+ }
843
+
844
+ interface ToolCategory {
845
+ name: string;
846
+ tools: MentionableTool[];
847
+ }
848
+
849
+ // A discoverable counterpart to the type-`@` autocomplete: a composer button
850
+ // that opens a searchable, category-grouped picker of the available tools and
851
+ // inserts an @mention for the chosen one. Inserts through the composer runtime
852
+ // so it stays in sync with the autocomplete's own textarea handling. Hidden when
853
+ // tool mentions are disabled or there are no tools.
854
+ const ComposerToolMentionPicker: FC = () => {
855
+ const { config, mcpTools, mcpToolsLoading } = useElements();
856
+ const api = useAssistantApi();
857
+ // Read the composer text from the same reactive source the tool-mention
858
+ // badges parse, so an inserted mention renders a pill just like the type-`@`
859
+ // autocomplete does.
860
+ const composerText = useAssistantState(({ composer }) => composer.text);
861
+ const [open, setOpen] = useState(false);
862
+ const [query, setQuery] = useState("");
863
+ const [activeCategory, setActiveCategory] = useState(
864
+ TOOL_MENTION_ALL_CATEGORY,
865
+ );
866
+
867
+ const composerConfig = config.composer;
868
+ const toolMentionsEnabled =
869
+ composerConfig?.toolMentions === undefined ||
870
+ composerConfig.toolMentions === true ||
871
+ (typeof composerConfig.toolMentions === "object" &&
872
+ composerConfig.toolMentions.enabled !== false);
873
+
874
+ const tools = useMemo(() => toolSetToMentionableTools(mcpTools), [mcpTools]);
875
+
876
+ const categories = useMemo<ToolCategory[]>(() => {
877
+ const grouped = new Map<string, MentionableTool[]>();
878
+ for (const tool of tools) {
879
+ const category = deriveToolCategory(tool.name);
880
+ const existing = grouped.get(category);
881
+ if (existing) {
882
+ existing.push(tool);
883
+ } else {
884
+ grouped.set(category, [tool]);
885
+ }
886
+ }
887
+ return [...grouped.entries()]
888
+ .map(([name, categoryTools]) => ({ name, tools: categoryTools }))
889
+ .sort((a, b) => a.name.localeCompare(b.name));
890
+ }, [tools]);
891
+
892
+ // Show the button while tools are still loading (so it appears immediately
893
+ // rather than popping in once the async MCP list resolves) or once there are
894
+ // tools — but hide it when the list has loaded and is empty, so we don't
895
+ // expose a dead-end control.
896
+ if (!toolMentionsEnabled || (!mcpToolsLoading && tools.length === 0)) {
897
+ return null;
898
+ }
899
+
900
+ const normalizedQuery = query.trim().toLowerCase();
901
+ const inActiveCategory =
902
+ activeCategory === TOOL_MENTION_ALL_CATEGORY
903
+ ? tools
904
+ : (categories.find((c) => c.name === activeCategory)?.tools ?? []);
905
+ const visibleTools = normalizedQuery
906
+ ? inActiveCategory.filter(
907
+ (tool) =>
908
+ tool.name.toLowerCase().includes(normalizedQuery) ||
909
+ (tool.description?.toLowerCase().includes(normalizedQuery) ?? false),
910
+ )
911
+ : inActiveCategory;
912
+
913
+ const insertMention = (toolName: string) => {
914
+ const base =
915
+ composerText && !/\s$/.test(composerText)
916
+ ? `${composerText} `
917
+ : composerText;
918
+ api.composer().setText(`${base}@${toolName} `);
919
+ setOpen(false);
920
+ setQuery("");
921
+ };
922
+
923
+ const handleOpenChange = (next: boolean) => {
924
+ setOpen(next);
925
+ if (!next) {
926
+ setQuery("");
927
+ setActiveCategory(TOOL_MENTION_ALL_CATEGORY);
928
+ }
929
+ };
930
+
931
+ return (
932
+ <Popover open={open} onOpenChange={handleOpenChange}>
933
+ <PopoverTrigger asChild>
934
+ <Button
935
+ variant="ghost"
936
+ size="icon"
937
+ data-state={open ? "open" : "closed"}
938
+ className="aui-composer-tool-mention-picker flex w-fit items-center gap-2 rounded-full px-2.5 py-1 text-xs font-semibold data-[state=open]:bg-muted-foreground/15 dark:border-muted-foreground/15 dark:hover:bg-muted-foreground/30"
939
+ aria-label="Mention a tool"
940
+ >
941
+ <AtSign className="size-5 stroke-[1.5px]" />
942
+ </Button>
943
+ </PopoverTrigger>
944
+ <PopoverContent
945
+ side="top"
946
+ align="start"
947
+ className="aui-composer-tool-mention-popover w-[420px] overflow-hidden p-0"
948
+ >
949
+ <div className="flex items-center gap-2 border-b border-input px-3 py-2">
950
+ <Search className="size-4 shrink-0 text-muted-foreground" />
951
+ <input
952
+ autoFocus
953
+ value={query}
954
+ onChange={(e) => setQuery(e.target.value)}
955
+ placeholder="Search tools…"
956
+ className="w-full bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground"
957
+ aria-label="Search tools"
958
+ />
959
+ </div>
960
+ <div className="flex h-72">
961
+ <div className="w-36 shrink-0 overflow-y-auto border-r border-input p-2">
962
+ <div className="px-2 pb-1 text-[10px] font-semibold tracking-wide text-muted-foreground uppercase">
963
+ Categories
964
+ </div>
965
+ <button
966
+ type="button"
967
+ onClick={() => setActiveCategory(TOOL_MENTION_ALL_CATEGORY)}
968
+ className={cn(
969
+ "flex w-full items-center justify-between rounded px-2 py-1 text-left text-xs transition-colors",
970
+ activeCategory === TOOL_MENTION_ALL_CATEGORY
971
+ ? "bg-muted font-medium text-foreground"
972
+ : "text-muted-foreground hover:bg-muted/60",
973
+ )}
974
+ >
975
+ <span className="truncate">All</span>
976
+ <span className="ml-2 shrink-0 tabular-nums opacity-60">
977
+ {tools.length}
978
+ </span>
979
+ </button>
980
+ {categories.map((category) => (
981
+ <button
982
+ key={category.name}
983
+ type="button"
984
+ onClick={() => setActiveCategory(category.name)}
985
+ className={cn(
986
+ "flex w-full items-center justify-between rounded px-2 py-1 text-left text-xs transition-colors",
987
+ activeCategory === category.name
988
+ ? "bg-muted font-medium text-foreground"
989
+ : "text-muted-foreground hover:bg-muted/60",
990
+ )}
991
+ >
992
+ <span className="truncate">{category.name}</span>
993
+ <span className="ml-2 shrink-0 tabular-nums opacity-60">
994
+ {category.tools.length}
995
+ </span>
996
+ </button>
997
+ ))}
998
+ </div>
999
+ <div className="min-w-0 flex-1 overflow-y-auto p-2">
1000
+ {visibleTools.length === 0 ? (
1001
+ <div className="px-2 py-6 text-center text-xs text-muted-foreground">
1002
+ {mcpToolsLoading ? "Loading tools…" : "No tools found"}
1003
+ </div>
1004
+ ) : (
1005
+ visibleTools.map((tool) => (
1006
+ <button
1007
+ key={tool.id}
1008
+ type="button"
1009
+ onClick={() => insertMention(tool.name)}
1010
+ className="flex w-full items-start gap-2 rounded px-2 py-1.5 text-left transition-colors hover:bg-muted"
1011
+ >
1012
+ <Wrench className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
1013
+ <span className="min-w-0 flex-1">
1014
+ <span className="block truncate text-sm font-medium text-foreground">
1015
+ {tool.name}
1016
+ </span>
1017
+ {tool.description && (
1018
+ <span className="line-clamp-2 text-xs text-muted-foreground">
1019
+ {tool.description}
1020
+ </span>
1021
+ )}
1022
+ </span>
1023
+ </button>
1024
+ ))
1025
+ )}
1026
+ </div>
1027
+ </div>
1028
+ </PopoverContent>
1029
+ </Popover>
1030
+ );
1031
+ };
1032
+
797
1033
  const ComposerAction: FC = () => {
798
1034
  const { config } = useElements();
799
1035
  const r = useRadius();
@@ -807,6 +1043,8 @@ const ComposerAction: FC = () => {
807
1043
  <div className="aui-composer-add-attachment-placeholder" />
808
1044
  )}
809
1045
 
1046
+ <ComposerToolMentionPicker />
1047
+
810
1048
  {config.model?.showModelPicker && !config.languageModel && (
811
1049
  <ComposerModelPicker />
812
1050
  )}
@@ -861,39 +1099,25 @@ const MessageError: FC = () => {
861
1099
  );
862
1100
  };
863
1101
 
864
- /**
865
- * Shows the pulsing dot indicator when the message is still running but the
866
- * last rendered part is a tool call (not text). Without this, there's no
867
- * visual feedback that the model is still working after a tool call.
868
- */
869
- const ToolCallStreamingIndicator: FC = () => {
870
- const show = useAssistantState(({ message }) => {
871
- if (message.status?.type !== "running") return false;
872
- const lastPart = message.parts[message.parts.length - 1];
873
- return lastPart?.type === "tool-call";
874
- });
875
- if (!show) return null;
876
- return <div className="aui-md mt-2" data-status="running" />;
877
- };
878
-
879
1102
  const AssistantMessage: FC = () => {
880
1103
  const { config } = useElements();
881
1104
  const toolsConfig = config.tools ?? {};
882
- const components = config.components ?? {};
1105
+ const components = config.components;
1106
+ const toolsComponents = toolsConfig.components;
883
1107
 
884
1108
  const partsComponents = useMemo(
885
1109
  () => ({
886
- Text: components.Text ?? MarkdownText,
887
- Image: components.Image ?? Image,
1110
+ Text: components?.Text ?? MarkdownText,
1111
+ Image: components?.Image ?? Image,
888
1112
  tools: {
889
- by_name: toolsConfig.components,
890
- Fallback: components.ToolFallback ?? ToolFallback,
1113
+ by_name: toolsComponents,
1114
+ Fallback: components?.ToolFallback ?? ToolFallback,
891
1115
  },
892
- Reasoning: components.Reasoning ?? Reasoning,
893
- ReasoningGroup: components.ReasoningGroup ?? ReasoningGroup,
894
- ToolGroup: components.ToolGroup ?? ToolGroup,
1116
+ Reasoning: components?.Reasoning ?? Reasoning,
1117
+ ReasoningGroup: components?.ReasoningGroup ?? ReasoningGroup,
1118
+ ToolGroup: components?.ToolGroup ?? ToolGroup,
895
1119
  }),
896
- [components, toolsConfig.components],
1120
+ [components, toolsComponents],
897
1121
  );
898
1122
 
899
1123
  return (
@@ -904,7 +1128,7 @@ const AssistantMessage: FC = () => {
904
1128
  >
905
1129
  <div className="aui-assistant-message-content mx-2 leading-7 wrap-break-word text-foreground">
906
1130
  <MessagePrimitive.Parts components={partsComponents} />
907
- <ToolCallStreamingIndicator />
1131
+ <ThinkingIndicator />
908
1132
  <MessageError />
909
1133
  </div>
910
1134
 
@@ -26,11 +26,13 @@ export const ToolFallback: ToolCallMessagePartComponent = ({
26
26
 
27
27
  // Check if this specific tool call has a pending approval
28
28
  const pendingApproval = pendingApprovals.get(toolCallId);
29
- const message = useAssistantState(({ message }) => message);
30
- const toolParts = message.parts.filter((part) => part.type === "tool-call");
31
- const matchingMessagePartIndex = toolParts.findIndex(
32
- (part) => part.toolCallId === toolCallId,
33
- );
29
+ // Selecting the whole message would re-render this card on every streamed
30
+ // chunk; select only the derived value.
31
+ const needsTrailingBorder = useAssistantState(({ message }) => {
32
+ const toolParts = message.parts.filter((part) => part.type === "tool-call");
33
+ const index = toolParts.findIndex((part) => part.toolCallId === toolCallId);
34
+ return index !== -1 && index !== toolParts.length - 1;
35
+ });
34
36
 
35
37
  const handleApproveOnce = () => {
36
38
  confirmPendingApproval(toolCallId);
@@ -89,9 +91,7 @@ export const ToolFallback: ToolCallMessagePartComponent = ({
89
91
  <div
90
92
  className={cn(
91
93
  "aui-tool-fallback-root flex w-full flex-col",
92
- matchingMessagePartIndex !== -1 &&
93
- matchingMessagePartIndex !== toolParts.length - 1 &&
94
- "border-b",
94
+ needsTrailingBorder && "border-b",
95
95
  )}
96
96
  >
97
97
  <ToolUI
@@ -1,4 +1,3 @@
1
- import { cn } from "@/lib/utils";
2
1
  import { useAssistantState } from "@assistant-ui/react";
3
2
  import { useMemo, type FC, type PropsWithChildren } from "react";
4
3
  import { useElements } from "@/hooks/useElements";
@@ -44,21 +43,13 @@ export const ToolGroup: FC<
44
43
  return children;
45
44
  }
46
45
 
47
- // For single tool calls, render without the group wrapper
48
- if (toolCount === 1) {
49
- return (
50
- <div className={cn("my-4 w-full max-w-xl")}>
51
- <div className="overflow-hidden rounded-lg border border-border bg-card">
52
- {children}
53
- </div>
54
- </div>
55
- );
56
- }
57
-
58
- // For multiple tool calls, use the group component
46
+ // Single and multiple tool calls must share one wrapper element type —
47
+ // diverging branches would remount every card when a streaming turn grows
48
+ // the group.
59
49
  return (
60
50
  <div className="my-4 w-full max-w-xl">
61
51
  <ToolUIGroup
52
+ headerless={toolCount === 1}
62
53
  title={groupTitle}
63
54
  status={anyMessagePartsAreRunning ? "running" : "complete"}
64
55
  defaultExpanded={defaultExpanded}
@@ -74,7 +74,7 @@ export const ToolMentionAutocomplete: FC<ToolMentionAutocompleteProps> = ({
74
74
  onValueChange(result.text, result.cursorPosition);
75
75
  setIsVisible(false);
76
76
  },
77
- [mentionContext, value, cursorPosition, onValueChange, textareaRef],
77
+ [mentionContext, value, cursorPosition, onValueChange],
78
78
  );
79
79
 
80
80
  useEffect(() => {
@@ -6,7 +6,7 @@ import { cn } from "@/lib/utils";
6
6
  function Avatar({
7
7
  className,
8
8
  ...props
9
- }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
9
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>): React.JSX.Element {
10
10
  return (
11
11
  <AvatarPrimitive.Root
12
12
  data-slot="avatar"
@@ -22,7 +22,7 @@ function Avatar({
22
22
  function AvatarImage({
23
23
  className,
24
24
  ...props
25
- }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
25
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>): React.JSX.Element {
26
26
  return (
27
27
  <AvatarPrimitive.Image
28
28
  data-slot="avatar-image"
@@ -35,7 +35,7 @@ function AvatarImage({
35
35
  function AvatarFallback({
36
36
  className,
37
37
  ...props
38
- }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
38
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>): React.JSX.Element {
39
39
  return (
40
40
  <AvatarPrimitive.Fallback
41
41
  data-slot="avatar-fallback"
@@ -130,7 +130,7 @@ function Calendar({
130
130
  minDate,
131
131
  maxDate,
132
132
  className,
133
- }: CalendarProps) {
133
+ }: CalendarProps): React.JSX.Element {
134
134
  const [viewDate, setViewDate] = React.useState(() => {
135
135
  if (selected.start) return new Date(selected.start);
136
136
  return new Date();
@@ -2,13 +2,15 @@ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
2
2
 
3
3
  function Collapsible({
4
4
  ...props
5
- }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
5
+ }: React.ComponentProps<typeof CollapsiblePrimitive.Root>): React.JSX.Element {
6
6
  return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
7
7
  }
8
8
 
9
9
  function CollapsibleTrigger({
10
10
  ...props
11
- }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
11
+ }: React.ComponentProps<
12
+ typeof CollapsiblePrimitive.CollapsibleTrigger
13
+ >): React.JSX.Element {
12
14
  return (
13
15
  <CollapsiblePrimitive.CollapsibleTrigger
14
16
  data-slot="collapsible-trigger"
@@ -19,7 +21,9 @@ function CollapsibleTrigger({
19
21
 
20
22
  function CollapsibleContent({
21
23
  ...props
22
- }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
24
+ }: React.ComponentProps<
25
+ typeof CollapsiblePrimitive.CollapsibleContent
26
+ >): React.JSX.Element {
23
27
  return (
24
28
  <CollapsiblePrimitive.CollapsibleContent
25
29
  data-slot="collapsible-content"
@@ -7,20 +7,20 @@ import { usePortalContainer } from "@/hooks/usePortalContainer";
7
7
 
8
8
  function Dialog({
9
9
  ...props
10
- }: React.ComponentProps<typeof DialogPrimitive.Root>) {
10
+ }: React.ComponentProps<typeof DialogPrimitive.Root>): React.JSX.Element {
11
11
  return <DialogPrimitive.Root data-slot="dialog" {...props} />;
12
12
  }
13
13
 
14
14
  function DialogTrigger({
15
15
  ...props
16
- }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
16
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>): React.JSX.Element {
17
17
  return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
18
18
  }
19
19
 
20
20
  function DialogPortal({
21
21
  container,
22
22
  ...props
23
- }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
23
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>): React.JSX.Element {
24
24
  const portalContainer = usePortalContainer();
25
25
  return (
26
26
  <DialogPrimitive.Portal
@@ -33,14 +33,14 @@ function DialogPortal({
33
33
 
34
34
  function DialogClose({
35
35
  ...props
36
- }: React.ComponentProps<typeof DialogPrimitive.Close>) {
36
+ }: React.ComponentProps<typeof DialogPrimitive.Close>): React.JSX.Element {
37
37
  return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
38
38
  }
39
39
 
40
40
  function DialogOverlay({
41
41
  className,
42
42
  ...props
43
- }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
43
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>): React.JSX.Element {
44
44
  return (
45
45
  <DialogPrimitive.Overlay
46
46
  data-slot="dialog-overlay"
@@ -60,7 +60,7 @@ function DialogContent({
60
60
  ...props
61
61
  }: React.ComponentProps<typeof DialogPrimitive.Content> & {
62
62
  showCloseButton?: boolean;
63
- }) {
63
+ }): React.JSX.Element {
64
64
  return (
65
65
  <DialogPortal data-slot="dialog-portal">
66
66
  <DialogOverlay />
@@ -87,7 +87,10 @@ function DialogContent({
87
87
  );
88
88
  }
89
89
 
90
- function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
90
+ function DialogHeader({
91
+ className,
92
+ ...props
93
+ }: React.ComponentProps<"div">): React.JSX.Element {
91
94
  return (
92
95
  <div
93
96
  data-slot="dialog-header"
@@ -97,7 +100,10 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
97
100
  );
98
101
  }
99
102
 
100
- function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
103
+ function DialogFooter({
104
+ className,
105
+ ...props
106
+ }: React.ComponentProps<"div">): React.JSX.Element {
101
107
  return (
102
108
  <div
103
109
  data-slot="dialog-footer"
@@ -113,7 +119,7 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
113
119
  function DialogTitle({
114
120
  className,
115
121
  ...props
116
- }: React.ComponentProps<typeof DialogPrimitive.Title>) {
122
+ }: React.ComponentProps<typeof DialogPrimitive.Title>): React.JSX.Element {
117
123
  return (
118
124
  <DialogPrimitive.Title
119
125
  data-slot="dialog-title"
@@ -126,7 +132,9 @@ function DialogTitle({
126
132
  function DialogDescription({
127
133
  className,
128
134
  ...props
129
- }: React.ComponentProps<typeof DialogPrimitive.Description>) {
135
+ }: React.ComponentProps<
136
+ typeof DialogPrimitive.Description
137
+ >): React.JSX.Element {
130
138
  return (
131
139
  <DialogPrimitive.Description
132
140
  data-slot="dialog-description"
@@ -4,7 +4,7 @@ import { useDensity } from "@/hooks/useDensity";
4
4
  import { cn } from "@/lib/utils";
5
5
  import { isJsonRenderTree, type JsonRenderNode } from "@/lib/generative-ui";
6
6
  import { AlertCircleIcon } from "lucide-react";
7
- import { FC, useMemo } from "react";
7
+ import { ElementType, FC, useMemo } from "react";
8
8
 
9
9
  // Import all components from the generative-ui plugin ui directory
10
10
  import {
@@ -42,9 +42,10 @@ interface GenerativeUIProps {
42
42
  /**
43
43
  * Built-in components for rendering json-render trees.
44
44
  * These provide a default set of UI primitives for tool results.
45
+ * Each entry has its own prop shape; the registry erases those generics via
46
+ * `ElementType` so heterogeneous components can coexist under one map.
45
47
  */
46
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
- const components: Record<string, FC<any>> = {
48
+ const components: Record<string, ElementType> = {
48
49
  // Layout
49
50
  Card: CardWrapper,
50
51
  Grid,
@@ -98,7 +99,11 @@ function renderNode(node: JsonRenderNode, key?: number): React.ReactNode {
98
99
  ? node.children.map((child, i) => renderNode(child, i))
99
100
  : undefined;
100
101
 
101
- return <Component key={key} {...(node.props ?? {})} children={children} />;
102
+ return (
103
+ <Component key={key} {...(node.props ?? {})}>
104
+ {children}
105
+ </Component>
106
+ );
102
107
  }
103
108
 
104
109
  /**
@@ -6,13 +6,13 @@ import { usePortalContainer } from "@/hooks/usePortalContainer";
6
6
 
7
7
  function Popover({
8
8
  ...props
9
- }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
9
+ }: React.ComponentProps<typeof PopoverPrimitive.Root>): React.JSX.Element {
10
10
  return <PopoverPrimitive.Root data-slot="popover" {...props} />;
11
11
  }
12
12
 
13
13
  function PopoverTrigger({
14
14
  ...props
15
- }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
15
+ }: React.ComponentProps<typeof PopoverPrimitive.Trigger>): React.JSX.Element {
16
16
  return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
17
17
  }
18
18
 
@@ -24,7 +24,7 @@ function PopoverContent({
24
24
  ...props
25
25
  }: React.ComponentProps<typeof PopoverPrimitive.Content> & {
26
26
  container?: HTMLElement | null;
27
- }) {
27
+ }): React.JSX.Element {
28
28
  const portalContainer = usePortalContainer();
29
29
  return (
30
30
  <PopoverPrimitive.Portal container={container ?? portalContainer}>
@@ -44,7 +44,7 @@ function PopoverContent({
44
44
 
45
45
  function PopoverAnchor({
46
46
  ...props
47
- }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
47
+ }: React.ComponentProps<typeof PopoverPrimitive.Anchor>): React.JSX.Element {
48
48
  return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
49
49
  }
50
50
 
@@ -1,6 +1,9 @@
1
1
  import { cn } from "@/lib/utils";
2
2
 
3
- function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
3
+ function Skeleton({
4
+ className,
5
+ ...props
6
+ }: React.ComponentProps<"div">): React.JSX.Element {
4
7
  return (
5
8
  <div
6
9
  data-slot="skeleton"