@gram-ai/elements 1.33.2 → 1.35.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 (243) 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/ui/avatar.d.ts +3 -3
  12. package/dist/components/ui/button.d.ts +2 -2
  13. package/dist/components/ui/buttonVariants.d.ts +2 -2
  14. package/dist/components/ui/calendar.d.ts +2 -1
  15. package/dist/components/ui/collapsible.d.ts +3 -3
  16. package/dist/components/ui/dialog.d.ts +10 -10
  17. package/dist/components/ui/popover.d.ts +4 -4
  18. package/dist/components/ui/skeleton.d.ts +1 -1
  19. package/dist/components/ui/time-range-picker.d.ts +18 -1
  20. package/dist/components/ui/time-range-picker.test.d.ts +1 -0
  21. package/dist/components/ui/tool-ui.d.ts +7 -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 +19 -16
  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 +26 -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-reVrRxP1.js → index-BhIowiZF.js} +9744 -9493
  38. package/dist/index-BhIowiZF.js.map +1 -0
  39. package/dist/{index-DAWGW1Nj.cjs → index-D0jIGQr7.cjs} +43 -43
  40. package/dist/index-D0jIGQr7.cjs.map +1 -0
  41. package/dist/{index-CGoLfO5p.js → index-Dz13dSDa.js} +108 -51
  42. package/dist/index-Dz13dSDa.js.map +1 -0
  43. package/dist/index-PXd3rs95.cjs +194 -0
  44. package/dist/index-PXd3rs95.cjs.map +1 -0
  45. package/dist/index.d.ts +4 -1
  46. package/dist/lib/errorTracking.d.ts +1 -1
  47. package/dist/lib/messageConverter.d.ts +58 -8
  48. package/dist/lib/models.d.ts +1 -1
  49. package/dist/lib/tools.d.ts +11 -10
  50. package/dist/lib/utils.d.ts +2 -0
  51. package/dist/plugins/generative-ui/catalog.d.ts +3 -3
  52. package/dist/plugins/generative-ui/ui/accordion-wrapper.d.ts +2 -2
  53. package/dist/plugins/generative-ui/ui/accordion.d.ts +4 -4
  54. package/dist/plugins/generative-ui/ui/action-button.d.ts +1 -1
  55. package/dist/plugins/generative-ui/ui/alert-wrapper.d.ts +2 -1
  56. package/dist/plugins/generative-ui/ui/alert.d.ts +3 -3
  57. package/dist/plugins/generative-ui/ui/avatar-wrapper.d.ts +2 -1
  58. package/dist/plugins/generative-ui/ui/avatar.d.ts +6 -6
  59. package/dist/plugins/generative-ui/ui/badge.d.ts +2 -2
  60. package/dist/plugins/generative-ui/ui/button-wrapper.d.ts +2 -1
  61. package/dist/plugins/generative-ui/ui/button.d.ts +3 -3
  62. package/dist/plugins/generative-ui/ui/card-wrapper.d.ts +1 -1
  63. package/dist/plugins/generative-ui/ui/card.d.ts +7 -7
  64. package/dist/plugins/generative-ui/ui/checkbox-wrapper.d.ts +2 -1
  65. package/dist/plugins/generative-ui/ui/checkbox.d.ts +1 -1
  66. package/dist/plugins/generative-ui/ui/data-table.d.ts +1 -1
  67. package/dist/plugins/generative-ui/ui/dialog.d.ts +10 -10
  68. package/dist/plugins/generative-ui/ui/dropdown-menu.d.ts +15 -15
  69. package/dist/plugins/generative-ui/ui/grid.d.ts +1 -1
  70. package/dist/plugins/generative-ui/ui/index.d.ts +57 -40
  71. package/dist/plugins/generative-ui/ui/input-wrapper.d.ts +2 -1
  72. package/dist/plugins/generative-ui/ui/input.d.ts +1 -1
  73. package/dist/plugins/generative-ui/ui/label.d.ts +1 -1
  74. package/dist/plugins/generative-ui/ui/list.d.ts +2 -1
  75. package/dist/plugins/generative-ui/ui/metric.d.ts +1 -1
  76. package/dist/plugins/generative-ui/ui/pagination.d.ts +7 -7
  77. package/dist/plugins/generative-ui/ui/popover.d.ts +7 -7
  78. package/dist/plugins/generative-ui/ui/progress.d.ts +1 -1
  79. package/dist/plugins/generative-ui/ui/radio-group.d.ts +2 -2
  80. package/dist/plugins/generative-ui/ui/select-wrapper.d.ts +2 -1
  81. package/dist/plugins/generative-ui/ui/select.d.ts +10 -10
  82. package/dist/plugins/generative-ui/ui/separator.d.ts +1 -1
  83. package/dist/plugins/generative-ui/ui/skeleton-wrapper.d.ts +2 -1
  84. package/dist/plugins/generative-ui/ui/skeleton.d.ts +1 -1
  85. package/dist/plugins/generative-ui/ui/stack.d.ts +1 -1
  86. package/dist/plugins/generative-ui/ui/switch.d.ts +1 -1
  87. package/dist/plugins/generative-ui/ui/table.d.ts +8 -8
  88. package/dist/plugins/generative-ui/ui/tabs-wrapper.d.ts +2 -2
  89. package/dist/plugins/generative-ui/ui/tabs.d.ts +4 -4
  90. package/dist/plugins/generative-ui/ui/text.d.ts +1 -1
  91. package/dist/plugins/generative-ui/ui/textarea.d.ts +1 -1
  92. package/dist/plugins/generative-ui/ui/tooltip.d.ts +4 -4
  93. package/dist/plugins.cjs +1 -1
  94. package/dist/plugins.js +1 -1
  95. package/dist/{profiler-noho3NG9.js → profiler-CtGKTWWP.js} +2 -2
  96. package/dist/{profiler-noho3NG9.js.map → profiler-CtGKTWWP.js.map} +1 -1
  97. package/dist/{profiler-B3tfiOx4.cjs → profiler-l7_HjTyw.cjs} +2 -2
  98. package/dist/{profiler-B3tfiOx4.cjs.map → profiler-l7_HjTyw.cjs.map} +1 -1
  99. package/dist/react-shim.cjs.map +1 -1
  100. package/dist/react-shim.d.ts +1 -1
  101. package/dist/react-shim.js +1 -4
  102. package/dist/react-shim.js.map +1 -1
  103. package/dist/server/bun.cjs.map +1 -1
  104. package/dist/server/bun.js.map +1 -1
  105. package/dist/server/express.cjs.map +1 -1
  106. package/dist/server/express.js.map +1 -1
  107. package/dist/server/fastify.cjs.map +1 -1
  108. package/dist/server/fastify.js.map +1 -1
  109. package/dist/server/hono.cjs.map +1 -1
  110. package/dist/server/hono.js.map +1 -1
  111. package/dist/server/nextjs.cjs.map +1 -1
  112. package/dist/server/nextjs.js.map +1 -1
  113. package/dist/server/tanstack-start.cjs.map +1 -1
  114. package/dist/server/tanstack-start.js.map +1 -1
  115. package/dist/{startRecording-7Oy6wM18.cjs → startRecording-DEw2Aeq4.cjs} +2 -2
  116. package/dist/{startRecording-7Oy6wM18.cjs.map → startRecording-DEw2Aeq4.cjs.map} +1 -1
  117. package/dist/{startRecording-mkmig-2n.js → startRecording-iYEL0-vr.js} +2 -2
  118. package/dist/{startRecording-mkmig-2n.js.map → startRecording-iYEL0-vr.js.map} +1 -1
  119. package/dist/types/index.d.ts +93 -4
  120. package/package.json +8 -9
  121. package/src/compat-shims.ts +16 -2
  122. package/src/components/Chat/index.tsx +4 -1
  123. package/src/components/Chat/stories/FrontendTools.stories.tsx +1 -1
  124. package/src/components/Chat/stories/ToolApproval.stories.tsx +2 -2
  125. package/src/components/Chat/stories/Tools.stories.tsx +13 -5
  126. package/src/components/ChatHistory.tsx +3 -1
  127. package/src/components/FrontendTools/index.tsx +1 -1
  128. package/src/components/MessageContent.tsx +1 -0
  129. package/src/components/Replay.stories.tsx +2 -3
  130. package/src/components/Replay.tsx +17 -10
  131. package/src/components/ShadowRoot.tsx +2 -2
  132. package/src/components/ShareButton/index.tsx +4 -2
  133. package/src/components/assistant-ui/assistant-modal.tsx +5 -3
  134. package/src/components/assistant-ui/attachment.tsx +1 -1
  135. package/src/components/assistant-ui/error-boundary.tsx +1 -1
  136. package/src/components/assistant-ui/markdown-text.tsx +1 -1
  137. package/src/components/assistant-ui/thread.tsx +256 -14
  138. package/src/components/assistant-ui/tool-mention-autocomplete.tsx +1 -1
  139. package/src/components/ui/avatar.tsx +3 -3
  140. package/src/components/ui/calendar.tsx +1 -1
  141. package/src/components/ui/collapsible.tsx +7 -3
  142. package/src/components/ui/dialog.tsx +18 -10
  143. package/src/components/ui/generative-ui.tsx +9 -4
  144. package/src/components/ui/popover.tsx +4 -4
  145. package/src/components/ui/skeleton.tsx +4 -1
  146. package/src/components/ui/time-range-picker.stories.tsx +164 -154
  147. package/src/components/ui/time-range-picker.test.ts +57 -0
  148. package/src/components/ui/time-range-picker.tsx +40 -9
  149. package/src/components/ui/tool-ui.tsx +18 -9
  150. package/src/components/ui/tooltip.tsx +4 -4
  151. package/src/contexts/ChatIdContext.tsx +1 -1
  152. package/src/contexts/ConnectionStatusContext.tsx +6 -5
  153. package/src/contexts/ElementsProvider.tsx +109 -37
  154. package/src/contexts/ReplayContext.ts +1 -1
  155. package/src/contexts/ToolApprovalContext.tsx +5 -1
  156. package/src/contexts/ToolExecutionContext.tsx +1 -1
  157. package/src/contexts/portal-container.tsx +1 -1
  158. package/src/hooks/useAuth.ts +2 -1
  159. package/src/hooks/useDensity.ts +1 -1
  160. package/src/hooks/useElements.ts +2 -1
  161. package/src/hooks/useFollowOnSuggestions.ts +3 -6
  162. package/src/hooks/useGramThreadListAdapter.tsx +118 -9
  163. package/src/hooks/useMCPTools.ts +2 -2
  164. package/src/hooks/useModel.ts +1 -3
  165. package/src/hooks/usePluginComponents.ts +3 -1
  166. package/src/hooks/useRadius.ts +1 -1
  167. package/src/hooks/useSession.ts +3 -1
  168. package/src/hooks/useThemeProps.ts +5 -5
  169. package/src/hooks/useToolApproval.ts +2 -1
  170. package/src/index.ts +16 -0
  171. package/src/lib/cassette.ts +21 -27
  172. package/src/lib/contextCompaction.test.ts +2 -2
  173. package/src/lib/contextCompaction.ts +20 -8
  174. package/src/lib/errorTracking.ts +1 -4
  175. package/src/lib/messageConverter.test.ts +11 -13
  176. package/src/lib/messageConverter.ts +105 -58
  177. package/src/lib/models.ts +19 -7
  178. package/src/lib/token.ts +2 -5
  179. package/src/lib/tool-mentions.ts +5 -2
  180. package/src/lib/tools.byte-cap.test.ts +1 -1
  181. package/src/lib/tools.test.ts +1 -1
  182. package/src/lib/tools.ts +15 -5
  183. package/src/lib/utils.ts +22 -2
  184. package/src/lib.d.ts +8 -1
  185. package/src/plugins/chart/chart.test.ts +3 -4
  186. package/src/plugins/chart/component.tsx +7 -6
  187. package/src/plugins/chart/ui/area-chart.tsx +1 -1
  188. package/src/plugins/chart/ui/line-chart.tsx +1 -1
  189. package/src/plugins/generative-ui/ui/accordion-wrapper.tsx +2 -2
  190. package/src/plugins/generative-ui/ui/accordion.tsx +4 -4
  191. package/src/plugins/generative-ui/ui/action-button.tsx +4 -2
  192. package/src/plugins/generative-ui/ui/alert-wrapper.tsx +1 -1
  193. package/src/plugins/generative-ui/ui/alert.tsx +7 -3
  194. package/src/plugins/generative-ui/ui/avatar-wrapper.tsx +5 -1
  195. package/src/plugins/generative-ui/ui/avatar.tsx +12 -6
  196. package/src/plugins/generative-ui/ui/badge.tsx +1 -1
  197. package/src/plugins/generative-ui/ui/button-wrapper.tsx +1 -1
  198. package/src/plugins/generative-ui/ui/button.tsx +1 -1
  199. package/src/plugins/generative-ui/ui/card-wrapper.tsx +1 -1
  200. package/src/plugins/generative-ui/ui/card.tsx +28 -7
  201. package/src/plugins/generative-ui/ui/checkbox-wrapper.tsx +1 -1
  202. package/src/plugins/generative-ui/ui/checkbox.tsx +1 -1
  203. package/src/plugins/generative-ui/ui/data-table.tsx +1 -1
  204. package/src/plugins/generative-ui/ui/dialog.tsx +15 -10
  205. package/src/plugins/generative-ui/ui/dropdown-menu.tsx +33 -15
  206. package/src/plugins/generative-ui/ui/grid.tsx +1 -1
  207. package/src/plugins/generative-ui/ui/index.ts +154 -40
  208. package/src/plugins/generative-ui/ui/input-wrapper.tsx +1 -1
  209. package/src/plugins/generative-ui/ui/input.tsx +5 -1
  210. package/src/plugins/generative-ui/ui/label.tsx +1 -1
  211. package/src/plugins/generative-ui/ui/list.tsx +5 -1
  212. package/src/plugins/generative-ui/ui/metric.tsx +2 -1
  213. package/src/plugins/generative-ui/ui/pagination.tsx +12 -7
  214. package/src/plugins/generative-ui/ui/popover.tsx +13 -7
  215. package/src/plugins/generative-ui/ui/progress.tsx +1 -1
  216. package/src/plugins/generative-ui/ui/radio-group.tsx +2 -2
  217. package/src/plugins/generative-ui/ui/select-wrapper.tsx +1 -1
  218. package/src/plugins/generative-ui/ui/select.tsx +14 -10
  219. package/src/plugins/generative-ui/ui/separator.tsx +1 -1
  220. package/src/plugins/generative-ui/ui/skeleton-wrapper.tsx +1 -1
  221. package/src/plugins/generative-ui/ui/skeleton.tsx +4 -1
  222. package/src/plugins/generative-ui/ui/stack.tsx +1 -1
  223. package/src/plugins/generative-ui/ui/switch.tsx +1 -1
  224. package/src/plugins/generative-ui/ui/table.tsx +29 -8
  225. package/src/plugins/generative-ui/ui/tabs-wrapper.tsx +5 -2
  226. package/src/plugins/generative-ui/ui/tabs.tsx +4 -4
  227. package/src/plugins/generative-ui/ui/text.tsx +1 -1
  228. package/src/plugins/generative-ui/ui/textarea.tsx +4 -1
  229. package/src/plugins/generative-ui/ui/tooltip.tsx +4 -4
  230. package/src/react-shim.ts +9 -4
  231. package/src/server/bun.ts +1 -1
  232. package/src/server/express.ts +1 -1
  233. package/src/server/fastify.ts +1 -1
  234. package/src/server/hono.ts +1 -1
  235. package/src/server/nextjs.ts +1 -1
  236. package/src/server/tanstack-start.ts +1 -1
  237. package/src/storybook.d.ts +5 -0
  238. package/src/types/index.ts +112 -4
  239. package/dist/index-BCV7Zf9E.cjs +0 -194
  240. package/dist/index-BCV7Zf9E.cjs.map +0 -1
  241. package/dist/index-CGoLfO5p.js.map +0 -1
  242. package/dist/index-DAWGW1Nj.cjs.map +0 -1
  243. package/dist/index-reVrRxP1.js.map +0 -1
@@ -36,7 +36,7 @@ interface ConnectionStatusProviderProps {
36
36
 
37
37
  export const ConnectionStatusProvider = ({
38
38
  children,
39
- }: ConnectionStatusProviderProps) => {
39
+ }: ConnectionStatusProviderProps): React.JSX.Element => {
40
40
  const [state, setState] = useState<ConnectionState>("connected");
41
41
  const [retryCount, setRetryCount] = useState(0);
42
42
  const [isOnline, setIsOnline] = useState(
@@ -138,7 +138,7 @@ export const ConnectionStatusProvider = ({
138
138
  );
139
139
  };
140
140
 
141
- export const useConnectionStatus = () => {
141
+ export const useConnectionStatus = (): ConnectionStatusContextValue => {
142
142
  const context = useContext(ConnectionStatusContext);
143
143
  if (!context) {
144
144
  throw new Error(
@@ -153,6 +153,7 @@ export const useConnectionStatus = () => {
153
153
  * Returns null if not within a ConnectionStatusProvider (for backwards compatibility).
154
154
  */
155
155
 
156
- export const useConnectionStatusOptional = () => {
157
- return useContext(ConnectionStatusContext);
158
- };
156
+ export const useConnectionStatusOptional =
157
+ (): ConnectionStatusContextValue | null => {
158
+ return useContext(ConnectionStatusContext);
159
+ };
@@ -17,7 +17,6 @@ import {
17
17
  wrapToolsWithApproval,
18
18
  wrapToolsWithByteCap,
19
19
  type ApprovalHelpers,
20
- type FrontendTool,
21
20
  } from "@/lib/tools";
22
21
  import { compactForModel } from "@/lib/contextCompaction";
23
22
  import { describeStreamError } from "@/lib/streamErrorMessage";
@@ -49,6 +48,8 @@ import {
49
48
  type ChatTransport,
50
49
  type UIMessage,
51
50
  } from "ai";
51
+
52
+ type UIMessagePart = UIMessage["parts"][number];
52
53
  import {
53
54
  ReactNode,
54
55
  useCallback,
@@ -67,30 +68,47 @@ import { ElementsContext } from "./contexts";
67
68
  import { ToolApprovalProvider } from "./ToolApprovalContext";
68
69
  import { ToolExecutionProvider } from "./ToolExecutionContext";
69
70
 
71
+ // Reads the active local thread id from the runtime's threads store. Goes
72
+ // through assistant-ui's public ThreadListRuntime.getState() API.
73
+ function getActiveLocalThreadId(
74
+ runtimeRef: React.RefObject<ReturnType<typeof useChatRuntime> | null>,
75
+ ): string | undefined {
76
+ const threadsState = runtimeRef.current?.threads.getState();
77
+ if (!threadsState) return undefined;
78
+ // `mainThreadId` is always populated by the SDK; the secondary read is a
79
+ // defensive fallback in case the SDK ever returns a state shape with an
80
+ // older `threadIds` field instead. The cast widens to an indexable shape
81
+ // because `ThreadListState` doesn't declare that historical field.
82
+ const legacy = (threadsState as { threadIds?: readonly string[] }).threadIds;
83
+ return threadsState.mainThreadId ?? legacy?.[0];
84
+ }
85
+
86
+ type ExecutableTool = {
87
+ execute?: (args: unknown, options?: unknown) => Promise<unknown>;
88
+ };
89
+
70
90
  /**
71
91
  * Extracts executable tools from frontend tool definitions.
72
92
  * Frontend tools created via defineFrontendTool have an unstable_tool property
73
93
  * that contains the tool definition with execute function.
94
+ *
95
+ * The AI SDK's `ToolExecuteFunction<INPUT, OUTPUT>` signature is too strict on
96
+ * its second parameter (a typed `ToolCallOptions`) and too broad on its return
97
+ * (`AsyncIterable | PromiseLike | OUTPUT`) to match `ExecutableTool.execute`
98
+ * directly. The reference is copied as-is — no runtime wrapping — and only the
99
+ * type surface is widened.
74
100
  */
75
101
  function extractExecutableTools(
76
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
- frontendTools: Record<string, FrontendTool<any, any>> | undefined,
78
- ): Record<
79
- string,
80
- { execute?: (args: unknown, options?: unknown) => Promise<unknown> }
81
- > {
102
+ frontendTools: Record<string, AssistantTool> | undefined,
103
+ ): Record<string, ExecutableTool> {
82
104
  if (!frontendTools) return {};
83
105
 
84
106
  return Object.fromEntries(
85
107
  Object.entries(frontendTools).map(([name, tool]) => {
86
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
87
- const toolDef = (tool as any).unstable_tool;
88
- return [
89
- name,
90
- {
91
- execute: toolDef?.execute,
92
- },
93
- ];
108
+ const toolDef = tool.unstable_tool as {
109
+ execute?: ExecutableTool["execute"];
110
+ };
111
+ return [name, { execute: toolDef.execute }];
94
112
  }),
95
113
  );
96
114
  }
@@ -141,13 +159,14 @@ function cleanMessagesForModel(messages: UIMessage[]): UIMessage[] {
141
159
  return message;
142
160
  }
143
161
 
144
- // Process each part: strip providerOptions/providerMetadata and filter reasoning
145
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
- const cleanedParts = partsArray.map((part: any) => {
147
- // Strip providerOptions and providerMetadata from all remaining parts
148
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
149
- const { callProviderMetadata: _, ...cleanPart } = part;
150
- return cleanPart;
162
+ // Process each part: strip providerOptions/providerMetadata and filter reasoning.
163
+ // `callProviderMetadata` is not declared on `UIMessagePart`, so we widen the
164
+ // part to an indexable record just for the destructure.
165
+ const cleanedParts = partsArray.map((part) => {
166
+ const { callProviderMetadata: _omit, ...cleanPart } =
167
+ part as UIMessagePart & { callProviderMetadata?: unknown };
168
+ void _omit;
169
+ return cleanPart as UIMessagePart;
151
170
  });
152
171
 
153
172
  return {
@@ -205,6 +224,7 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
205
224
  projectSlug: config.projectSlug,
206
225
  variant: config.variant,
207
226
  });
227
+ // oxlint-disable-next-line react-hooks/exhaustive-deps -- one-time init at mount; later config changes are intentionally ignored
208
228
  }, []);
209
229
 
210
230
  // Generate a stable chat ID for server-side persistence (when history is disabled)
@@ -214,7 +234,11 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
214
234
  // State to expose the current chat ID via context
215
235
  const [currentChatId, setCurrentChatId] = useState<string | null>(null);
216
236
 
217
- const { data: mcpTools, mcpHeaders } = useMCPTools({
237
+ const {
238
+ data: mcpTools,
239
+ mcpHeaders,
240
+ isLoading: mcpQueryLoading,
241
+ } = useMCPTools({
218
242
  auth,
219
243
  mcp: config.mcp,
220
244
  mcps: config.mcps,
@@ -222,6 +246,11 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
222
246
  toolsToInclude: config.tools?.toolsToInclude,
223
247
  gramEnvironment: config.gramEnvironment,
224
248
  });
249
+ // Treat auth-loading as "tools not yet resolved" too — the MCP query is
250
+ // disabled (and so not "loading") until auth settles, so without this a
251
+ // tool-list consumer would briefly see an empty, settled state before tools
252
+ // arrive.
253
+ const mcpToolsLoading = auth.isLoading || mcpQueryLoading;
225
254
 
226
255
  // Store approval helpers in ref so they can be used in async contexts
227
256
  const approvalHelpersRef = useRef<ApprovalHelpers>({
@@ -276,8 +305,10 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
276
305
  // in a way that's accessible from the transport's sendMessages function.
277
306
  const currentRemoteIdRef = useRef<string | null>(null);
278
307
 
279
- // Create chat transport configuration
280
- const transport = useMemo<ChatTransport<UIMessage>>(
308
+ // Create chat transport configuration. This is the built-in client-side
309
+ // streaming transport; a consumer can override it via config.transport (see
310
+ // below) to route the conversation through a server-side assistant instead.
311
+ const defaultTransport = useMemo<ChatTransport<UIMessage>>(
281
312
  () => ({
282
313
  sendMessages: async ({ messages, abortSignal }) => {
283
314
  const usingCustomModel = !!config.languageModel;
@@ -298,12 +329,7 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
298
329
  // chatId is already set correctly from the synced ref
299
330
  } else if (isLocalThreadId(chatId) || !chatId) {
300
331
  // For local thread IDs or no ID, check/generate UUID mapping
301
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
302
- const runtimeAny = runtimeRef.current as any;
303
- const threadsState = runtimeAny?.threads?.getState?.();
304
- const localThreadId = (threadsState?.mainThreadId ??
305
- threadsState?.threadIds?.[0]) as string | undefined;
306
-
332
+ const localThreadId = getActiveLocalThreadId(runtimeRef);
307
333
  const lookupKey = chatId ?? localThreadId;
308
334
  if (lookupKey) {
309
335
  const existingUuid = localIdToUuidMapRef.current.get(lookupKey);
@@ -512,8 +538,11 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
512
538
  config.contextCompaction?.maxTokens,
513
539
  config.contextCompaction?.compactAtFraction,
514
540
  config.contextCompaction?.keepRecent,
541
+ config.gramEnvironment,
542
+ config.api?.headers,
515
543
  model,
516
544
  mcpTools,
545
+ mcpHeaders,
517
546
  getApprovalHelpers,
518
547
  apiUrl,
519
548
  auth.isLoading,
@@ -521,6 +550,44 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
521
550
  ],
522
551
  );
523
552
 
553
+ // A consumer-supplied transport (e.g. a server-side assistant transport) takes
554
+ // precedence over the built-in client-side one. It may be a ChatTransport or a
555
+ // factory: a factory is invoked here, inside the provider, with a getChatId()
556
+ // sourced from the synced thread state, so the transport can read the active
557
+ // chat id at send time without reaching into Elements internals. Local
558
+ // (unpersisted) thread ids read as null so the transport can treat them as a
559
+ // brand-new conversation.
560
+ const getChatId = useCallback(() => {
561
+ const id = currentRemoteIdRef.current;
562
+ return id && !isLocalThreadId(id) ? id : null;
563
+ }, []);
564
+ // Capture the active local thread identity now and return a bind function
565
+ // closing over it. Consumer transports call this at the start of
566
+ // `sendMessages`; once a server-minted chat id is known, invoking the
567
+ // returned function reconciles the captured thread to it — the same
568
+ // reconciliation the built-in transport does inline when it generates an id.
569
+ // Closing over the captured id (instead of re-reading active state at bind
570
+ // time) is what makes a thread switch or a parallel send on another thread
571
+ // during the round-trip safe.
572
+ const adoptChatId = useCallback(() => {
573
+ const capturedLocalThreadId = getActiveLocalThreadId(runtimeRef);
574
+ return (chatId: string) => {
575
+ if (capturedLocalThreadId) {
576
+ localIdToUuidMapRef.current.set(capturedLocalThreadId, chatId);
577
+ }
578
+ currentRemoteIdRef.current = chatId;
579
+ mcpHeaders["Gram-Chat-ID"] = chatId;
580
+ setCurrentChatId(chatId);
581
+ };
582
+ }, [mcpHeaders, setCurrentChatId]);
583
+ const configTransport = config.transport;
584
+ const transport = useMemo<ChatTransport<UIMessage>>(() => {
585
+ if (typeof configTransport === "function") {
586
+ return configTransport({ getChatId, adoptChatId });
587
+ }
588
+ return configTransport ?? defaultTransport;
589
+ }, [configTransport, defaultTransport, getChatId, adoptChatId]);
590
+
524
591
  const historyEnabled = config.history?.enabled ?? false;
525
592
 
526
593
  // Shared context value for ElementsContext
@@ -535,8 +602,9 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
535
602
  setIsOpen,
536
603
  plugins,
537
604
  mcpTools,
605
+ mcpToolsLoading,
538
606
  }),
539
- [config, model, isExpanded, isOpen, plugins, mcpTools],
607
+ [config, model, isExpanded, isOpen, plugins, mcpTools, mcpToolsLoading],
540
608
  );
541
609
 
542
610
  const frontendTools = config.tools?.frontendTools ?? {};
@@ -610,8 +678,7 @@ interface ElementsProviderWithHistoryProps {
610
678
  headers: Record<string, string>;
611
679
  contextValue: React.ContextType<typeof ElementsContext>;
612
680
  runtimeRef: React.RefObject<ReturnType<typeof useChatRuntime> | null>;
613
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
614
- frontendTools: Record<string, AssistantTool | FrontendTool<any, any>>;
681
+ frontendTools: Record<string, AssistantTool>;
615
682
  localIdToUuidMap: Map<string, string>;
616
683
  currentRemoteIdRef: React.RefObject<string | null>;
617
684
  executableTools: ExecutableToolSet;
@@ -658,6 +725,9 @@ const ElementsProviderWithHistory = ({
658
725
  apiUrl,
659
726
  headers,
660
727
  localIdToUuidMap,
728
+ threadListFilters: contextValue?.config.history?.threadListFilters,
729
+ deferThreadIdMinting: contextValue?.config.history?.deferThreadIdMinting,
730
+ transformChatMessage: contextValue?.config.history?.transformChatMessage,
661
731
  });
662
732
  const initialThreadId = contextValue?.config.history?.initialThreadId;
663
733
 
@@ -665,6 +735,7 @@ const ElementsProviderWithHistory = ({
665
735
  // half-finished: the tool-result is patched in but the agent never resumes,
666
736
  // so the next user message lands on top of an unresolved tool-call sequence.
667
737
  const useChatRuntimeHook = useCallback(() => {
738
+ // oxlint-disable-next-line react-hooks/rules-of-hooks -- intentional: useChatRuntime is invoked by useRemoteThreadListRuntime as a hook for each thread
668
739
  return useChatRuntime({
669
740
  transport,
670
741
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
@@ -736,8 +807,7 @@ interface ElementsProviderWithoutHistoryProps {
736
807
  transport: ChatTransport<UIMessage>;
737
808
  contextValue: React.ContextType<typeof ElementsContext>;
738
809
  runtimeRef: React.RefObject<ReturnType<typeof useChatRuntime> | null>;
739
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
740
- frontendTools: Record<string, AssistantTool | FrontendTool<any, any>>;
810
+ frontendTools: Record<string, AssistantTool>;
741
811
  executableTools: ExecutableToolSet;
742
812
  currentChatId: string | null;
743
813
  }
@@ -786,7 +856,9 @@ const ElementsProviderWithoutHistory = ({
786
856
 
787
857
  const queryClient = new QueryClient();
788
858
 
789
- export const ElementsProvider = (props: ElementsProviderProps) => {
859
+ export const ElementsProvider = (
860
+ props: ElementsProviderProps,
861
+ ): React.JSX.Element => {
790
862
  return (
791
863
  <QueryClientProvider client={queryClient}>
792
864
  <ConnectionStatusProvider>
@@ -2,6 +2,6 @@ import { createContext, useContext } from "react";
2
2
 
3
3
  export const ReplayContext = createContext<{ isReplay: boolean } | null>(null);
4
4
 
5
- export function useReplayContext() {
5
+ export function useReplayContext(): { isReplay: boolean } | null {
6
6
  return useContext(ReplayContext);
7
7
  }
@@ -26,7 +26,11 @@ interface ToolApprovalContextType {
26
26
  getPendingApproval: (toolCallId: string) => PendingApproval | undefined;
27
27
  }
28
28
 
29
- export function ToolApprovalProvider({ children }: { children: ReactNode }) {
29
+ export function ToolApprovalProvider({
30
+ children,
31
+ }: {
32
+ children: ReactNode;
33
+ }): React.JSX.Element {
30
34
  const [pendingApprovals, setPendingApprovals] = useState<
31
35
  Map<string, PendingApproval>
32
36
  >(new Map());
@@ -36,7 +36,7 @@ interface ToolExecutionProviderProps {
36
36
  export function ToolExecutionProvider({
37
37
  children,
38
38
  tools,
39
- }: ToolExecutionProviderProps) {
39
+ }: ToolExecutionProviderProps): React.JSX.Element {
40
40
  const executeTool = useCallback(
41
41
  async (
42
42
  toolName: string,
@@ -11,7 +11,7 @@ export function PortalContainerProvider({
11
11
  }: {
12
12
  containerRef: RefObject<HTMLElement | null>;
13
13
  children: React.ReactNode;
14
- }) {
14
+ }): React.JSX.Element {
15
15
  return (
16
16
  <PortalContainerContext.Provider value={containerRef}>
17
17
  {children}
@@ -131,7 +131,8 @@ export const useAuth = ({
131
131
  // useMCPTools). Today the dashboard uses this to forward
132
132
  // `Authorization: Bearer <user-session JWT>` so the runtime gateway can
133
133
  // resolve the user's upstream credentials for issuer-gated toolsets.
134
- const extraHeaders = auth?.headers ?? {};
134
+ const authHeaders = auth?.headers;
135
+ const extraHeaders = useMemo(() => authHeaders ?? {}, [authHeaders]);
135
136
 
136
137
  const ensureValidHeaders = useCallback(async (): Promise<
137
138
  Record<string, string>
@@ -102,7 +102,7 @@ type DensityToken = keyof (typeof densityClasses)["normal"];
102
102
  * Hook to get density classes based on theme config
103
103
  * Use: const d = useDensity(); then d('p-md') returns the appropriate padding class
104
104
  */
105
- export const useDensity = () => {
105
+ export const useDensity = (): ((token: DensityToken) => string) => {
106
106
  const { config } = useElements();
107
107
  const density = config.theme?.density ?? "normal";
108
108
 
@@ -1,11 +1,12 @@
1
1
  import { useContext } from "react";
2
2
  import { ElementsContext } from "@/contexts/contexts";
3
+ import type { ElementsContextType } from "@/types";
3
4
 
4
5
  /**
5
6
  * @private Internal hook to access the ElementsContext
6
7
  *
7
8
  */
8
- export const useElements = () => {
9
+ export const useElements = (): ElementsContextType => {
9
10
  const context = useContext(ElementsContext);
10
11
  if (!context) {
11
12
  throw new Error("useElements must be used within a ElementsProvider");
@@ -1,5 +1,4 @@
1
1
  import { useReplayContext } from "@/contexts/ReplayContext";
2
- import { getApiUrl } from "@/lib/api";
3
2
  import { useAssistantState } from "@assistant-ui/react";
4
3
  import { generateObject } from "ai";
5
4
  import { useCallback, useEffect, useRef, useState } from "react";
@@ -64,8 +63,6 @@ export function useFollowOnSuggestions(): {
64
63
  const isRunning = useAssistantState(({ thread }) => thread.isRunning);
65
64
  const messages = useAssistantState(({ thread }) => thread.messages);
66
65
 
67
- const apiUrl = getApiUrl(config);
68
-
69
66
  const fetchSuggestions = useCallback(async () => {
70
67
  if (!isEnabled || auth.isLoading || !auth.headers) return;
71
68
 
@@ -89,7 +86,7 @@ export function useFollowOnSuggestions(): {
89
86
  let lastAssistantMessage = "";
90
87
  for (let i = recentMessages.length - 1; i >= 0; i--) {
91
88
  const msg = recentMessages[i];
92
- if (msg.role === "assistant") {
89
+ if (msg && msg.role === "assistant") {
93
90
  lastAssistantMessage = msg.content;
94
91
  break;
95
92
  }
@@ -191,7 +188,7 @@ ${conversation}`,
191
188
  abortControllerRef.current = null;
192
189
  }
193
190
  }
194
- }, [isEnabled, apiUrl, auth.headers, auth.isLoading, messages]);
191
+ }, [isEnabled, auth.headers, auth.isLoading, messages, model]);
195
192
 
196
193
  // Fetch suggestions when:
197
194
  // 1. The thread stops running (assistant finished responding)
@@ -224,7 +221,7 @@ ${conversation}`,
224
221
  if (lastProcessedMessageIdRef.current === lastMessage.id) return;
225
222
 
226
223
  lastProcessedMessageIdRef.current = lastMessage.id;
227
- fetchSuggestions();
224
+ void fetchSuggestions();
228
225
  }, [isRunning, messages, fetchSuggestions, auth.isLoading, auth.headers]);
229
226
 
230
227
  // Cleanup on unmount
@@ -10,9 +10,11 @@ import { createAssistantStream, type AssistantStream } from "assistant-stream";
10
10
  import {
11
11
  GramChatOverview,
12
12
  GramChat,
13
+ GramChatMessage,
13
14
  convertGramMessagesToExported,
14
15
  convertGramMessagesToUIMessages,
15
16
  } from "@/lib/messageConverter";
17
+ import { sleep } from "@/lib/utils";
16
18
  import {
17
19
  useCallback,
18
20
  useEffect,
@@ -37,11 +39,61 @@ export function isLocalThreadId(threadId: string | null | undefined): boolean {
37
39
  return !!threadId?.startsWith(LOCAL_THREAD_ID_PREFIX);
38
40
  }
39
41
 
42
+ /**
43
+ * Polls the shared local→remote id map until the transport assigns an id for
44
+ * `threadId`, or a deadline passes. Used in deferred-minting mode so a new
45
+ * thread adopts the backend-minted chat id instead of a client-generated one.
46
+ * The timeout is generous to tolerate cold serverless boots on the first send.
47
+ */
48
+ async function waitForMappedId(
49
+ map: Map<string, string> | undefined,
50
+ threadId: string,
51
+ timeoutMs = 30_000,
52
+ intervalMs = 50,
53
+ ): Promise<string | undefined> {
54
+ if (!map) return undefined;
55
+ const deadline = Date.now() + timeoutMs;
56
+ for (;;) {
57
+ const id = map.get(threadId);
58
+ if (id) return id;
59
+ if (Date.now() >= deadline) return undefined;
60
+ await sleep(intervalMs);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Transforms or drops a persisted chat message before it is rendered from
66
+ * history. Return the (possibly rewritten) message, or `null` to omit it.
67
+ */
68
+ export type ChatMessageTransform = (
69
+ message: GramChatMessage,
70
+ ) => GramChatMessage | null;
71
+
40
72
  export interface ThreadListAdapterOptions {
41
73
  apiUrl: string;
42
74
  headers: Record<string, string>;
43
75
  /** Map to translate local thread IDs to UUIDs (shared with transport) */
44
76
  localIdToUuidMap?: Map<string, string>;
77
+ /**
78
+ * Extra query parameters forwarded to `chat.list` to filter which threads are
79
+ * listed. Opaque to the adapter — the consumer chooses the keys.
80
+ */
81
+ threadListFilters?: Record<string, string>;
82
+ /**
83
+ * Don't client-mint a chat id for a brand-new thread. When true, `initialize`
84
+ * waits for the transport to assign the id (via the shared `localIdToUuidMap`,
85
+ * e.g. a server-minted id reported through the transport context's
86
+ * `adoptChatId` bind closure) instead of generating one with
87
+ * `crypto.randomUUID()`. Use this when the backend owns chat-id creation.
88
+ */
89
+ deferThreadIdMinting?: boolean;
90
+ /**
91
+ * Optional hook to transform or drop each persisted message before it is
92
+ * converted for rendering. Return a message to render it (possibly rewritten),
93
+ * or `null` to omit it. Keeps product-specific transcript conventions out of
94
+ * the library — see {@link HistoryConfig.transformChatMessage}.
95
+ */
96
+ transformChatMessage?: ChatMessageTransform;
45
97
  }
46
98
 
47
99
  interface ListChatsResponse {
@@ -57,15 +109,41 @@ class GramThreadHistoryAdapter {
57
109
  private apiUrl: string;
58
110
  private headers: Record<string, string>;
59
111
  private store: AssistantApi;
112
+ // Read lazily rather than captured: the adapter is constructed once, but the
113
+ // consumer may swap `transformChatMessage` across renders, so resolve it from
114
+ // the live options on every load instead of snapshotting it here.
115
+ private getTransformChatMessage?: () => ChatMessageTransform | undefined;
60
116
 
61
117
  constructor(
62
118
  apiUrl: string,
63
119
  headers: Record<string, string>,
64
120
  store: AssistantApi,
121
+ getTransformChatMessage?: () => ChatMessageTransform | undefined,
65
122
  ) {
66
123
  this.apiUrl = apiUrl;
67
124
  this.headers = headers;
68
125
  this.store = store;
126
+ this.getTransformChatMessage = getTransformChatMessage;
127
+ }
128
+
129
+ /**
130
+ * Applies the consumer-supplied `transformChatMessage` hook to a loaded
131
+ * transcript: rewrites each message and drops any the hook returns `null` for.
132
+ * Without a hook configured the messages pass through untouched.
133
+ */
134
+ private applyTransform(messages: GramChatMessage[]): GramChatMessage[] {
135
+ const transform = this.getTransformChatMessage?.();
136
+ if (!transform) {
137
+ return messages;
138
+ }
139
+ const result: GramChatMessage[] = [];
140
+ for (const message of messages) {
141
+ const transformed = transform(message);
142
+ if (transformed) {
143
+ result.push(transformed);
144
+ }
145
+ }
146
+ return result;
69
147
  }
70
148
 
71
149
  async load() {
@@ -86,7 +164,7 @@ class GramThreadHistoryAdapter {
86
164
  }
87
165
 
88
166
  const chat = (await response.json()) as GramChat;
89
- return convertGramMessagesToExported(chat.messages);
167
+ return convertGramMessagesToExported(this.applyTransform(chat.messages));
90
168
  } catch (error) {
91
169
  console.error("Error loading chat:", error);
92
170
  return { messages: [], headId: null };
@@ -120,7 +198,9 @@ class GramThreadHistoryAdapter {
120
198
  }
121
199
 
122
200
  const chat = (await response.json()) as GramChat;
123
- return convertGramMessagesToUIMessages(chat.messages);
201
+ return convertGramMessagesToUIMessages(
202
+ this.applyTransform(chat.messages),
203
+ );
124
204
 
125
205
  // // Filter out system messages (assistant-ui doesn't support them in the import path)
126
206
  // const filteredMessages = chat.messages.filter(
@@ -175,6 +255,7 @@ function useGramThreadHistoryAdapter(
175
255
  optionsRef.current.apiUrl,
176
256
  optionsRef.current.headers,
177
257
  store,
258
+ () => optionsRef.current.transformChatMessage,
178
259
  ),
179
260
  );
180
261
  // Cast to ThreadHistoryAdapter - the withFormat generic doesn't match but works at runtime
@@ -213,12 +294,14 @@ export function useGramThreadListAdapter(
213
294
 
214
295
  async list() {
215
296
  try {
216
- const response = await fetch(
217
- `${optionsRef.current.apiUrl}/rpc/chat.list`,
218
- {
219
- headers: optionsRef.current.headers,
220
- },
221
- );
297
+ const { apiUrl, headers, threadListFilters } = optionsRef.current;
298
+ const qs = threadListFilters
299
+ ? new URLSearchParams(threadListFilters).toString()
300
+ : "";
301
+ const listUrl = qs
302
+ ? `${apiUrl}/rpc/chat.list?${qs}`
303
+ : `${apiUrl}/rpc/chat.list`;
304
+ const response = await fetch(listUrl, { headers });
222
305
 
223
306
  if (!response.ok) {
224
307
  console.error("Failed to list chats:", response.status);
@@ -252,6 +335,30 @@ export function useGramThreadListAdapter(
252
335
  externalId: existingUuid,
253
336
  };
254
337
  }
338
+ // When the backend owns chat-id creation, don't client-mint: wait for
339
+ // the transport to report the server-minted id (it assigns it during
340
+ // the first send via the adoptChatId bind closure, populating the
341
+ // shared map — the same path the built-in transport uses).
342
+ if (optionsRef.current.deferThreadIdMinting) {
343
+ const mappedUuid = await waitForMappedId(
344
+ optionsRef.current.localIdToUuidMap,
345
+ threadId,
346
+ );
347
+ if (mappedUuid) {
348
+ return {
349
+ remoteId: mappedUuid,
350
+ externalId: mappedUuid,
351
+ };
352
+ }
353
+ // Falling through to crypto.randomUUID() here would defeat deferred
354
+ // minting: the client id would race the server-minted one reported
355
+ // later via the adoptChatId bind closure, leaving runtime state
356
+ // and the local→remote map disagreeing. Surface the failure to
357
+ // the user instead.
358
+ throw new Error(
359
+ "Backend did not mint a chat id in time — first send may have failed or is still in flight. Retry the send.",
360
+ );
361
+ }
255
362
  // Otherwise generate a new one and store it
256
363
  const uuid = crypto.randomUUID();
257
364
  optionsRef.current.localIdToUuidMap?.set(threadId, uuid);
@@ -298,7 +405,9 @@ export function useGramThreadListAdapter(
298
405
  // Title generation happens async server-side via Temporal after first completion.
299
406
  // This delay allows the OpenRouter LLM call to complete before we fetch the title.
300
407
  const TITLE_GENERATION_DELAY_MS = 2000;
301
- await new Promise((r) => setTimeout(r, TITLE_GENERATION_DELAY_MS));
408
+ await new Promise((r) => {
409
+ setTimeout(r, TITLE_GENERATION_DELAY_MS);
410
+ });
302
411
 
303
412
  try {
304
413
  // TODO: rename generateTitle endpoint to getTitle
@@ -42,10 +42,10 @@ export function useMCPTools({
42
42
  );
43
43
 
44
44
  const envQueryKey = Object.entries(environment ?? {}).map(
45
- ([k, v]) => `${k}:${v}`,
45
+ ([k, v]) => `${k}:${String(v)}`,
46
46
  );
47
47
  const authQueryKey = Object.entries(auth.headers ?? {}).map(
48
- ([k, v]) => `${k}:${v}`,
48
+ ([k, v]) => `${k}:${String(v)}`,
49
49
  );
50
50
  const serversQueryKey = servers.map(
51
51
  (s) => `${s.url}|${s.name ?? ""}|${s.environment ?? ""}`,
@@ -5,9 +5,7 @@ import { useAuth } from "./useAuth";
5
5
  import { useElements } from "./useElements";
6
6
 
7
7
  // Creates an OpenRouter client to be used for "internal Gram" usage, such as follow-on suggestions
8
- export const useModel = (
9
- model: string = "openai/gpt-5.4-mini",
10
- ): LanguageModel => {
8
+ export const useModel = (model = "openai/gpt-5.4-mini"): LanguageModel => {
11
9
  const { config } = useElements();
12
10
 
13
11
  const auth = useAuth({
@@ -15,7 +15,9 @@ type ComponentsByLanguage =
15
15
  >
16
16
  | undefined;
17
17
 
18
- export function useComponentsByLanguage(plugins: Plugin[]) {
18
+ export function useComponentsByLanguage(
19
+ plugins: Plugin[],
20
+ ): ComponentsByLanguage {
19
21
  return useMemo(() => {
20
22
  return plugins.reduce((acc, plugin) => {
21
23
  if (acc?.[plugin.language] && !plugin.overrideExisting) {