@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
@@ -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,43 +68,47 @@ import { ElementsContext } from "./contexts";
67
68
  import { ToolApprovalProvider } from "./ToolApprovalContext";
68
69
  import { ToolExecutionProvider } from "./ToolExecutionContext";
69
70
 
70
- // Reads the active local thread id from the runtime's threads store. Reaches
71
- // into an assistant-ui internal that isn't part of the public type, so it's
72
- // isolated here as the single point of breakage if the API moves.
71
+ // Reads the active local thread id from the runtime's threads store. Goes
72
+ // through assistant-ui's public ThreadListRuntime.getState() API.
73
73
  function getActiveLocalThreadId(
74
74
  runtimeRef: React.RefObject<ReturnType<typeof useChatRuntime> | null>,
75
75
  ): string | undefined {
76
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
- const threadsState = (runtimeRef.current as any)?.threads?.getState?.();
78
- return (threadsState?.mainThreadId ?? threadsState?.threadIds?.[0]) as
79
- | string
80
- | 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];
81
84
  }
82
85
 
86
+ type ExecutableTool = {
87
+ execute?: (args: unknown, options?: unknown) => Promise<unknown>;
88
+ };
89
+
83
90
  /**
84
91
  * Extracts executable tools from frontend tool definitions.
85
92
  * Frontend tools created via defineFrontendTool have an unstable_tool property
86
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.
87
100
  */
88
101
  function extractExecutableTools(
89
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
90
- frontendTools: Record<string, FrontendTool<any, any>> | undefined,
91
- ): Record<
92
- string,
93
- { execute?: (args: unknown, options?: unknown) => Promise<unknown> }
94
- > {
102
+ frontendTools: Record<string, AssistantTool> | undefined,
103
+ ): Record<string, ExecutableTool> {
95
104
  if (!frontendTools) return {};
96
105
 
97
106
  return Object.fromEntries(
98
107
  Object.entries(frontendTools).map(([name, tool]) => {
99
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
- const toolDef = (tool as any).unstable_tool;
101
- return [
102
- name,
103
- {
104
- execute: toolDef?.execute,
105
- },
106
- ];
108
+ const toolDef = tool.unstable_tool as {
109
+ execute?: ExecutableTool["execute"];
110
+ };
111
+ return [name, { execute: toolDef.execute }];
107
112
  }),
108
113
  );
109
114
  }
@@ -154,13 +159,14 @@ function cleanMessagesForModel(messages: UIMessage[]): UIMessage[] {
154
159
  return message;
155
160
  }
156
161
 
157
- // Process each part: strip providerOptions/providerMetadata and filter reasoning
158
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
159
- const cleanedParts = partsArray.map((part: any) => {
160
- // Strip providerOptions and providerMetadata from all remaining parts
161
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
162
- const { callProviderMetadata: _, ...cleanPart } = part;
163
- 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;
164
170
  });
165
171
 
166
172
  return {
@@ -218,6 +224,7 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
218
224
  projectSlug: config.projectSlug,
219
225
  variant: config.variant,
220
226
  });
227
+ // oxlint-disable-next-line react-hooks/exhaustive-deps -- one-time init at mount; later config changes are intentionally ignored
221
228
  }, []);
222
229
 
223
230
  // Generate a stable chat ID for server-side persistence (when history is disabled)
@@ -227,7 +234,11 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
227
234
  // State to expose the current chat ID via context
228
235
  const [currentChatId, setCurrentChatId] = useState<string | null>(null);
229
236
 
230
- const { data: mcpTools, mcpHeaders } = useMCPTools({
237
+ const {
238
+ data: mcpTools,
239
+ mcpHeaders,
240
+ isLoading: mcpQueryLoading,
241
+ } = useMCPTools({
231
242
  auth,
232
243
  mcp: config.mcp,
233
244
  mcps: config.mcps,
@@ -235,6 +246,11 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
235
246
  toolsToInclude: config.tools?.toolsToInclude,
236
247
  gramEnvironment: config.gramEnvironment,
237
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;
238
254
 
239
255
  // Store approval helpers in ref so they can be used in async contexts
240
256
  const approvalHelpersRef = useRef<ApprovalHelpers>({
@@ -522,8 +538,11 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
522
538
  config.contextCompaction?.maxTokens,
523
539
  config.contextCompaction?.compactAtFraction,
524
540
  config.contextCompaction?.keepRecent,
541
+ config.gramEnvironment,
542
+ config.api?.headers,
525
543
  model,
526
544
  mcpTools,
545
+ mcpHeaders,
527
546
  getApprovalHelpers,
528
547
  apiUrl,
529
548
  auth.isLoading,
@@ -561,12 +580,13 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
561
580
  setCurrentChatId(chatId);
562
581
  };
563
582
  }, [mcpHeaders, setCurrentChatId]);
583
+ const configTransport = config.transport;
564
584
  const transport = useMemo<ChatTransport<UIMessage>>(() => {
565
- if (typeof config.transport === "function") {
566
- return config.transport({ getChatId, adoptChatId });
585
+ if (typeof configTransport === "function") {
586
+ return configTransport({ getChatId, adoptChatId });
567
587
  }
568
- return config.transport ?? defaultTransport;
569
- }, [config.transport, defaultTransport, getChatId, adoptChatId]);
588
+ return configTransport ?? defaultTransport;
589
+ }, [configTransport, defaultTransport, getChatId, adoptChatId]);
570
590
 
571
591
  const historyEnabled = config.history?.enabled ?? false;
572
592
 
@@ -582,8 +602,9 @@ const ElementsProviderInner = ({ children, config }: ElementsProviderProps) => {
582
602
  setIsOpen,
583
603
  plugins,
584
604
  mcpTools,
605
+ mcpToolsLoading,
585
606
  }),
586
- [config, model, isExpanded, isOpen, plugins, mcpTools],
607
+ [config, model, isExpanded, isOpen, plugins, mcpTools, mcpToolsLoading],
587
608
  );
588
609
 
589
610
  const frontendTools = config.tools?.frontendTools ?? {};
@@ -657,8 +678,7 @@ interface ElementsProviderWithHistoryProps {
657
678
  headers: Record<string, string>;
658
679
  contextValue: React.ContextType<typeof ElementsContext>;
659
680
  runtimeRef: React.RefObject<ReturnType<typeof useChatRuntime> | null>;
660
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
661
- frontendTools: Record<string, AssistantTool | FrontendTool<any, any>>;
681
+ frontendTools: Record<string, AssistantTool>;
662
682
  localIdToUuidMap: Map<string, string>;
663
683
  currentRemoteIdRef: React.RefObject<string | null>;
664
684
  executableTools: ExecutableToolSet;
@@ -707,6 +727,7 @@ const ElementsProviderWithHistory = ({
707
727
  localIdToUuidMap,
708
728
  threadListFilters: contextValue?.config.history?.threadListFilters,
709
729
  deferThreadIdMinting: contextValue?.config.history?.deferThreadIdMinting,
730
+ transformChatMessage: contextValue?.config.history?.transformChatMessage,
710
731
  });
711
732
  const initialThreadId = contextValue?.config.history?.initialThreadId;
712
733
 
@@ -714,6 +735,7 @@ const ElementsProviderWithHistory = ({
714
735
  // half-finished: the tool-result is patched in but the agent never resumes,
715
736
  // so the next user message lands on top of an unresolved tool-call sequence.
716
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
717
739
  return useChatRuntime({
718
740
  transport,
719
741
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
@@ -785,8 +807,7 @@ interface ElementsProviderWithoutHistoryProps {
785
807
  transport: ChatTransport<UIMessage>;
786
808
  contextValue: React.ContextType<typeof ElementsContext>;
787
809
  runtimeRef: React.RefObject<ReturnType<typeof useChatRuntime> | null>;
788
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
789
- frontendTools: Record<string, AssistantTool | FrontendTool<any, any>>;
810
+ frontendTools: Record<string, AssistantTool>;
790
811
  executableTools: ExecutableToolSet;
791
812
  currentChatId: string | null;
792
813
  }
@@ -835,7 +856,9 @@ const ElementsProviderWithoutHistory = ({
835
856
 
836
857
  const queryClient = new QueryClient();
837
858
 
838
- export const ElementsProvider = (props: ElementsProviderProps) => {
859
+ export const ElementsProvider = (
860
+ props: ElementsProviderProps,
861
+ ): React.JSX.Element => {
839
862
  return (
840
863
  <QueryClientProvider client={queryClient}>
841
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}
package/src/global.css CHANGED
@@ -258,14 +258,36 @@
258
258
 
259
259
  /* assistant-ui streaming indicator — rainbow gradient ring matches the
260
260
  Speakeasy brand palette used elsewhere (see `INSIGHTS_AI_RAINBOW_BORDER_CLASS`
261
- and the login page's BrandGradientBar). */
261
+ and the login page's BrandGradientBar). Shared as a variable so the inline
262
+ trailing dot and the standalone "thinking" dot stay in sync. */
263
+ .gram-elements {
264
+ --aui-rainbow: conic-gradient(
265
+ from 0deg,
266
+ #320f1e,
267
+ #c83228,
268
+ #fb873f,
269
+ #d2dc91,
270
+ #5a8250,
271
+ #002314,
272
+ #00143c,
273
+ #2873d7,
274
+ #9bc3ff,
275
+ #320f1e
276
+ );
277
+ /* Spin shorthand shared by both rainbow dots so a single reduced-motion
278
+ override (below) stops all of them at once. */
279
+ --aui-spin: aui-rainbow-spin 1.6s linear infinite;
280
+ }
281
+
262
282
  @keyframes aui-rainbow-spin {
263
283
  to {
264
284
  transform: rotate(360deg);
265
285
  }
266
286
  }
267
287
 
268
- .gram-elements :where(.aui-md[data-status="running"]):empty::after,
288
+ /* Trailing dot that follows answer text as it streams. The "before first
289
+ token" state is handled by <ThinkingIndicator> (cycling verbs), so there is
290
+ deliberately no `:empty::after` rule here. */
269
291
  .gram-elements
270
292
  :where(.aui-md[data-status="running"])
271
293
  > :where(:not(ol):not(ul):not(pre)):last-child::after,
@@ -301,19 +323,29 @@
301
323
  margin-right: 0.15rem;
302
324
  padding: 3px;
303
325
  border-radius: 50%;
304
- background: conic-gradient(
305
- from 0deg,
306
- #320f1e,
307
- #c83228,
308
- #fb873f,
309
- #d2dc91,
310
- #5a8250,
311
- #002314,
312
- #00143c,
313
- #2873d7,
314
- #9bc3ff,
315
- #320f1e
316
- );
326
+ background: var(--aui-rainbow);
327
+ -webkit-mask:
328
+ linear-gradient(#fff 0 0) content-box,
329
+ linear-gradient(#fff 0 0);
330
+ -webkit-mask-composite: xor;
331
+ mask:
332
+ linear-gradient(#fff 0 0) content-box,
333
+ linear-gradient(#fff 0 0);
334
+ mask-composite: exclude;
335
+ animation: var(--aui-spin);
336
+ }
337
+
338
+ /* Standalone rainbow dot for the cycling "thinking" indicator (shown before
339
+ the assistant streams any answer text). Same ring as the inline dot above. */
340
+ .gram-elements .aui-thinking-dot {
341
+ display: inline-block;
342
+ box-sizing: border-box;
343
+ flex-shrink: 0;
344
+ width: 0.95em;
345
+ height: 0.95em;
346
+ padding: 2.5px;
347
+ border-radius: 50%;
348
+ background: var(--aui-rainbow);
317
349
  -webkit-mask:
318
350
  linear-gradient(#fff 0 0) content-box,
319
351
  linear-gradient(#fff 0 0);
@@ -322,7 +354,14 @@
322
354
  linear-gradient(#fff 0 0) content-box,
323
355
  linear-gradient(#fff 0 0);
324
356
  mask-composite: exclude;
325
- animation: aui-rainbow-spin 1.6s linear infinite;
357
+ animation: var(--aui-spin);
358
+ }
359
+
360
+ @media (prefers-reduced-motion: reduce) {
361
+ /* Stops both the trailing streaming dot and the standalone thinking dot. */
362
+ .gram-elements {
363
+ --aui-spin: none;
364
+ }
326
365
  }
327
366
 
328
367
  /* Simple shimmer animation for title text */
@@ -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,6 +10,7 @@ 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";
@@ -60,6 +61,14 @@ async function waitForMappedId(
60
61
  }
61
62
  }
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
+
63
72
  export interface ThreadListAdapterOptions {
64
73
  apiUrl: string;
65
74
  headers: Record<string, string>;
@@ -78,6 +87,13 @@ export interface ThreadListAdapterOptions {
78
87
  * `crypto.randomUUID()`. Use this when the backend owns chat-id creation.
79
88
  */
80
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;
81
97
  }
82
98
 
83
99
  interface ListChatsResponse {
@@ -93,15 +109,41 @@ class GramThreadHistoryAdapter {
93
109
  private apiUrl: string;
94
110
  private headers: Record<string, string>;
95
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;
96
116
 
97
117
  constructor(
98
118
  apiUrl: string,
99
119
  headers: Record<string, string>,
100
120
  store: AssistantApi,
121
+ getTransformChatMessage?: () => ChatMessageTransform | undefined,
101
122
  ) {
102
123
  this.apiUrl = apiUrl;
103
124
  this.headers = headers;
104
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;
105
147
  }
106
148
 
107
149
  async load() {
@@ -122,7 +164,7 @@ class GramThreadHistoryAdapter {
122
164
  }
123
165
 
124
166
  const chat = (await response.json()) as GramChat;
125
- return convertGramMessagesToExported(chat.messages);
167
+ return convertGramMessagesToExported(this.applyTransform(chat.messages));
126
168
  } catch (error) {
127
169
  console.error("Error loading chat:", error);
128
170
  return { messages: [], headId: null };
@@ -156,7 +198,9 @@ class GramThreadHistoryAdapter {
156
198
  }
157
199
 
158
200
  const chat = (await response.json()) as GramChat;
159
- return convertGramMessagesToUIMessages(chat.messages);
201
+ return convertGramMessagesToUIMessages(
202
+ this.applyTransform(chat.messages),
203
+ );
160
204
 
161
205
  // // Filter out system messages (assistant-ui doesn't support them in the import path)
162
206
  // const filteredMessages = chat.messages.filter(
@@ -211,6 +255,7 @@ function useGramThreadHistoryAdapter(
211
255
  optionsRef.current.apiUrl,
212
256
  optionsRef.current.headers,
213
257
  store,
258
+ () => optionsRef.current.transformChatMessage,
214
259
  ),
215
260
  );
216
261
  // Cast to ThreadHistoryAdapter - the withFormat generic doesn't match but works at runtime
@@ -360,7 +405,9 @@ export function useGramThreadListAdapter(
360
405
  // Title generation happens async server-side via Temporal after first completion.
361
406
  // This delay allows the OpenRouter LLM call to complete before we fetch the title.
362
407
  const TITLE_GENERATION_DELAY_MS = 2000;
363
- await new Promise((r) => setTimeout(r, TITLE_GENERATION_DELAY_MS));
408
+ await new Promise((r) => {
409
+ setTimeout(r, TITLE_GENERATION_DELAY_MS);
410
+ });
364
411
 
365
412
  try {
366
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) {
@@ -34,7 +34,7 @@ type RadiusSize = "sm" | "md" | "lg" | "xl" | "full";
34
34
  * Hook to get radius classes based on theme config
35
35
  * Use: const r = useRadius(); then r('lg') returns the appropriate rounded class
36
36
  */
37
- export const useRadius = () => {
37
+ export const useRadius = (): ((size: RadiusSize) => string) => {
38
38
  const { config } = useElements();
39
39
  const radius = config.theme?.radius ?? "soft";
40
40
 
@@ -1,7 +1,9 @@
1
1
  import { GetSessionFn } from "@/types";
2
2
  import { useQuery, useQueryClient } from "@tanstack/react-query";
3
3
 
4
- export function getChatSessionQueryKey(projectSlug: string) {
4
+ export function getChatSessionQueryKey(
5
+ projectSlug: string,
6
+ ): readonly ["chatSession", string] {
5
7
  return ["chatSession", projectSlug] as const;
6
8
  }
7
9
 
@@ -4,13 +4,13 @@ import { useElements } from "./useElements";
4
4
  /**
5
5
  * Hook to get theme-related props including dark mode class
6
6
  */
7
- export const useThemeProps = () => {
7
+ export const useThemeProps = (): {
8
+ readonly className: string | undefined;
9
+ } => {
8
10
  const { config } = useElements();
9
- const theme = config.theme ?? {};
11
+ const colorScheme = config.theme?.colorScheme ?? "light";
10
12
 
11
13
  return useMemo(() => {
12
- const { colorScheme = "light" } = theme;
13
-
14
14
  const isDark =
15
15
  colorScheme === "dark" ||
16
16
  (colorScheme === "system" &&
@@ -20,5 +20,5 @@ export const useThemeProps = () => {
20
20
  return {
21
21
  className: isDark ? "dark" : undefined,
22
22
  } as const;
23
- }, [theme]);
23
+ }, [colorScheme]);
24
24
  };
@@ -1,11 +1,12 @@
1
1
  import { useContext } from "react";
2
2
  import { ToolApprovalContext } from "@/contexts/contexts";
3
+ import type { ToolApprovalContextType } from "@/contexts/ToolApprovalContext";
3
4
 
4
5
  /**
5
6
  * Hook to access the tool approval context for managing human-in-the-loop
6
7
  * tool execution approval.
7
8
  */
8
- export const useToolApproval = () => {
9
+ export const useToolApproval = (): ToolApprovalContextType => {
9
10
  const context = useContext(ToolApprovalContext);
10
11
  if (!context) {
11
12
  throw new Error(