@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
@@ -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(
package/src/index.ts CHANGED
@@ -56,6 +56,8 @@ export type {
56
56
  Dimension,
57
57
  Dimensions,
58
58
  ElementsConfig,
59
+ ElementsTransportContext,
60
+ ElementsTransportFactory,
59
61
  ErrorTrackingConfigOption,
60
62
  GetSessionFn,
61
63
  HistoryConfig,
@@ -80,6 +82,20 @@ export type {
80
82
 
81
83
  export { MODELS } from "./lib/models";
82
84
 
85
+ // Chat-message conversion — for consumers building a custom transport against
86
+ // the Gram chat service (e.g. the dashboard's server-assistant transport).
87
+ export {
88
+ convertGramMessagesToUIMessages,
89
+ convertGramMessagesToExported,
90
+ } from "@/lib/messageConverter";
91
+
92
+ export { sleep } from "@/lib/utils";
93
+ export type {
94
+ GramChat,
95
+ GramChatMessage,
96
+ GramChatOverview,
97
+ } from "@/lib/messageConverter";
98
+
83
99
  export type { Plugin } from "./types/plugins";
84
100
 
85
101
  // Time Range Picker
@@ -10,8 +10,14 @@
10
10
  * 3. Pass it to `<Replay cassette={...}>` to play it back
11
11
  */
12
12
 
13
- import { createUIMessageStream, type ChatTransport, type UIMessage } from "ai";
13
+ import {
14
+ createUIMessageStream,
15
+ type ChatTransport,
16
+ type UIMessage,
17
+ type UIMessageStreamWriter,
18
+ } from "ai";
14
19
  import type { ThreadMessage } from "@assistant-ui/react";
20
+ import { sleep } from "@/lib/utils";
15
21
 
16
22
  // ---------------------------------------------------------------------------
17
23
  // Cassette types
@@ -94,6 +100,12 @@ export function recordCassette(messages: readonly ThreadMessage[]): Cassette {
94
100
  });
95
101
  break;
96
102
  // Skip image, file, audio, source, data parts for now
103
+ case "image":
104
+ case "file":
105
+ case "audio":
106
+ case "source":
107
+ case "data":
108
+ break;
97
109
  }
98
110
  }
99
111
 
@@ -112,25 +124,6 @@ export function recordCassette(messages: readonly ThreadMessage[]): Cassette {
112
124
  // Playback: Cassette → ChatTransport
113
125
  // ---------------------------------------------------------------------------
114
126
 
115
- /** Sleep that respects AbortSignal for clean cancellation. */
116
- function sleep(ms: number, signal?: AbortSignal): Promise<void> {
117
- return new Promise((resolve, reject) => {
118
- if (signal?.aborted) {
119
- reject(new DOMException("Aborted", "AbortError"));
120
- return;
121
- }
122
- const timeout = setTimeout(resolve, ms);
123
- signal?.addEventListener(
124
- "abort",
125
- () => {
126
- clearTimeout(timeout);
127
- reject(new DOMException("Aborted", "AbortError"));
128
- },
129
- { once: true },
130
- );
131
- });
132
- }
133
-
134
127
  /**
135
128
  * Creates a ChatTransport that replays pre-recorded assistant messages
136
129
  * from a cassette. Each call to `sendMessages` (triggered by a user message
@@ -153,7 +146,7 @@ export function createReplayTransport(
153
146
  // Advance cursor past it (it should be pointing at a user message).
154
147
  if (
155
148
  cursor < cassette.messages.length &&
156
- cassette.messages[cursor].role === "user"
149
+ cassette.messages[cursor]?.role === "user"
157
150
  ) {
158
151
  cursor++;
159
152
  }
@@ -163,9 +156,10 @@ export function createReplayTransport(
163
156
  const assistantMessages: CassetteMessage[] = [];
164
157
  while (
165
158
  cursor < cassette.messages.length &&
166
- cassette.messages[cursor].role === "assistant"
159
+ cassette.messages[cursor]?.role === "assistant"
167
160
  ) {
168
- assistantMessages.push(cassette.messages[cursor]);
161
+ const m = cassette.messages[cursor];
162
+ if (m) assistantMessages.push(m);
169
163
  cursor++;
170
164
  }
171
165
 
@@ -204,10 +198,10 @@ export function createReplayTransport(
204
198
  // Stream writing helpers
205
199
  // ---------------------------------------------------------------------------
206
200
 
207
- interface StreamWriter {
208
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
209
- write(part: any): void;
210
- }
201
+ // Match the `write` shape of `UIMessageStreamWriter<UIMessage>` so callers can
202
+ // pass the writer they receive from `createUIMessageStream` directly. We only
203
+ // rely on `write` here, so this stays narrow on purpose.
204
+ type StreamWriter = Pick<UIMessageStreamWriter<UIMessage>, "write">;
211
205
 
212
206
  async function writeReplayPart(
213
207
  writer: StreamWriter,
@@ -38,8 +38,8 @@ describe("getModelContextLimit", () => {
38
38
  expect(getModelContextLimit("anthropic/claude-sonnet-4.6")).toBe(1_000_000);
39
39
  });
40
40
 
41
- it("returns known mapping for Claude 4 (non-1M)", () => {
42
- expect(getModelContextLimit("anthropic/claude-sonnet-4")).toBe(200_000);
41
+ it("returns known mapping for Claude Sonnet 4", () => {
42
+ expect(getModelContextLimit("anthropic/claude-sonnet-4")).toBe(1_000_000);
43
43
  });
44
44
 
45
45
  it("returns DEFAULT_CONTEXT_LIMIT for unknown models", () => {
@@ -32,39 +32,51 @@ export const DEFAULT_CONTEXT_LIMIT = 200_000;
32
32
  */
33
33
  const MODEL_CONTEXT_LIMITS: Partial<Record<KnownModelId, number>> = {
34
34
  // Anthropic (1M tier where available, else 200K)
35
+ "anthropic/claude-opus-4.8": 1_000_000,
36
+ "anthropic/claude-opus-4.7": 1_000_000,
35
37
  "anthropic/claude-opus-4.6": 1_000_000,
36
- "anthropic/claude-opus-4.5": 1_000_000,
37
- "anthropic/claude-opus-4.1": 200_000,
38
+ "anthropic/claude-opus-4.5": 200_000,
38
39
  "anthropic/claude-sonnet-4.6": 1_000_000,
39
40
  "anthropic/claude-sonnet-4.5": 1_000_000,
40
- "anthropic/claude-sonnet-4": 200_000,
41
+ "anthropic/claude-sonnet-4": 1_000_000,
41
42
  "anthropic/claude-haiku-4.5": 200_000,
42
43
 
43
44
  // OpenAI
44
- "openai/gpt-5.4": 400_000,
45
+ "openai/gpt-5.5": 1_000_000,
46
+ "openai/gpt-5.5-pro": 1_000_000,
47
+ "openai/gpt-5.4": 1_000_000,
45
48
  "openai/gpt-5.4-mini": 400_000,
49
+ "openai/gpt-5.4-nano": 400_000,
50
+ "openai/gpt-5.3-codex": 400_000,
46
51
  "openai/gpt-5.1": 400_000,
47
- "openai/gpt-5.1-codex": 400_000,
48
52
  "openai/gpt-5": 400_000,
49
53
  "openai/gpt-4.1": 1_000_000,
50
54
  "openai/o4-mini": 200_000,
51
55
  "openai/o3": 200_000,
52
56
 
53
57
  // Google
58
+ "google/gemini-3.5-flash": 1_000_000,
54
59
  "google/gemini-3.1-pro-preview": 1_000_000,
60
+ "google/gemini-3.1-flash-lite": 1_000_000,
55
61
  "google/gemini-2.5-pro": 1_000_000,
56
62
  "google/gemini-2.5-flash": 1_000_000,
57
63
 
58
64
  // Others
65
+ "deepseek/deepseek-v4-pro": 1_000_000,
66
+ "deepseek/deepseek-v4-flash": 1_000_000,
59
67
  "deepseek/deepseek-r1": 128_000,
60
68
  "deepseek/deepseek-v3.2": 128_000,
61
69
  "meta-llama/llama-4-maverick": 1_000_000,
62
- "x-ai/grok-4": 256_000,
70
+ "x-ai/grok-4.3": 1_000_000,
71
+ "x-ai/grok-4.20": 2_000_000,
72
+ "qwen/qwen3.7-max": 1_000_000,
63
73
  "qwen/qwen3-coder": 256_000,
64
- "moonshotai/kimi-k2.5": 128_000,
74
+ "moonshotai/kimi-k2.6": 256_000,
75
+ "moonshotai/kimi-k2.5": 256_000,
76
+ "mistralai/mistral-medium-3-5": 256_000,
65
77
  "mistralai/mistral-medium-3.1": 128_000,
66
78
  "mistralai/codestral-2508": 256_000,
67
- "mistralai/devstral-small": 128_000,
79
+ "mistralai/devstral-2512": 256_000,
68
80
  };
69
81
 
70
82
  /**
@@ -75,10 +75,7 @@ export function initErrorTracking(config: ErrorTrackingConfig = {}): void {
75
75
  * Track an error to Datadog RUM.
76
76
  * Includes context about where the error originated.
77
77
  */
78
- export function trackError(
79
- error: Error | unknown,
80
- context: ErrorContext,
81
- ): void {
78
+ export function trackError(error: unknown, context: ErrorContext): void {
82
79
  if (!enabled || !initialized) {
83
80
  return;
84
81
  }
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
1
  import { describe, expect, it } from "vitest";
3
2
  import {
4
3
  convertGramMessagesToExported,
@@ -6,6 +5,7 @@ import {
6
5
  convertGramMessagePartsToUIMessageParts,
7
6
  type GramChatMessage,
8
7
  } from "./messageConverter";
8
+ import type { ToolCallMessagePart } from "@assistant-ui/react";
9
9
 
10
10
  /**
11
11
  * Helper to create a minimal GramChatMessage for testing.
@@ -41,10 +41,7 @@ describe("convertGramMessagePartsToUIMessageParts", () => {
41
41
  tool_calls: makeToolCallsJSON([{ id: "tc_1", name: "search_deals" }]),
42
42
  } as Partial<GramChatMessage> & { role: string });
43
43
 
44
- const parts = convertGramMessagePartsToUIMessageParts(
45
- msg as any,
46
- new Map(),
47
- );
44
+ const parts = convertGramMessagePartsToUIMessageParts(msg, new Map());
48
45
 
49
46
  const toolParts = parts.filter((p) => p.type === "dynamic-tool");
50
47
  expect(toolParts).toHaveLength(1);
@@ -66,11 +63,7 @@ describe("convertGramMessagePartsToUIMessageParts", () => {
66
63
  ]),
67
64
  } as Partial<GramChatMessage> & { role: string });
68
65
 
69
- const parts = convertGramMessagePartsToUIMessageParts(
70
- msg as any,
71
- new Map(),
72
- seen,
73
- );
66
+ const parts = convertGramMessagePartsToUIMessageParts(msg, new Map(), seen);
74
67
 
75
68
  const toolParts = parts.filter((p) => p.type === "dynamic-tool");
76
69
  expect(toolParts).toHaveLength(1);
@@ -230,10 +223,15 @@ describe("convertGramMessagesToExported - string content with tool calls", () =>
230
223
 
231
224
  const result = convertGramMessagesToExported(messages);
232
225
  const assistantEntry = result.messages.find(
233
- (m) => m.message.role === "assistant",
226
+ (m): m is typeof m & { message: { role: "assistant" } } =>
227
+ m.message.role === "assistant",
234
228
  )!;
235
- const toolCallParts = (assistantEntry.message as any).content.filter(
236
- (p: any) => p.type === "tool-call",
229
+ const assistantMessage = assistantEntry.message;
230
+ if (assistantMessage.role !== "assistant") {
231
+ throw new Error("expected assistant message");
232
+ }
233
+ const toolCallParts = assistantMessage.content.filter(
234
+ (p): p is ToolCallMessagePart => p.type === "tool-call",
237
235
  );
238
236
  expect(toolCallParts).toHaveLength(1);
239
237
  expect(toolCallParts[0]).toMatchObject({
@@ -1,5 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
-
3
1
  /**
4
2
  * Message format converter for Gram API <-> assistant-ui.
5
3
  *
@@ -19,23 +17,69 @@ import type {
19
17
  ThreadAssistantMessagePart,
20
18
  TextMessagePart,
21
19
  } from "@assistant-ui/react";
22
- import type {
23
- Message,
24
- UserMessage,
25
- AssistantMessage,
26
- ToolResponseMessage,
27
- } from "@openrouter/sdk/models";
28
20
  import { UIMessage } from "ai";
29
21
 
30
22
  /**
31
- * Represents a chat message from the Gram API.
32
- * This mirrors the ChatMessage type from @gram/sdk without requiring the SDK dependency.
23
+ * A single text part of a multi-modal chat message.
24
+ */
25
+ export interface GramChatTextPart {
26
+ type: "text";
27
+ text: string;
28
+ }
29
+
30
+ /**
31
+ * A single image part of a multi-modal chat message. The wire shape mirrors the
32
+ * upstream OpenAI/OpenRouter chat schema (`image_url.url`) so the converter can
33
+ * read it without normalisation.
34
+ */
35
+ export interface GramChatImagePart {
36
+ type: "image_url";
37
+ image_url?: { url?: string };
38
+ }
39
+
40
+ /**
41
+ * A single audio part of a multi-modal chat message. Mirrors the OpenAI/OpenRouter
42
+ * `input_audio.{data, format}` shape on the wire.
43
+ */
44
+ export interface GramChatAudioPart {
45
+ type: "input_audio";
46
+ input_audio?: { data?: string; format?: string };
47
+ }
48
+
49
+ /**
50
+ * Content part of a multi-modal chat message.
33
51
  */
34
- export type GramChatMessage = Message & {
52
+ export type GramChatContentPart =
53
+ | GramChatTextPart
54
+ | GramChatImagePart
55
+ | GramChatAudioPart;
56
+
57
+ /**
58
+ * Content of a chat message — either a plain string or an array of parts for
59
+ * multi-modal messages.
60
+ */
61
+ export type GramChatContent = string | GramChatContentPart[];
62
+
63
+ /**
64
+ * Represents a chat message from the Gram API. Only fields actually surfaced
65
+ * through Elements' public converters are modelled; provider-specific extras
66
+ * remain on the wire shape but are intentionally not part of the contract.
67
+ *
68
+ * `tool_calls` is the JSON-encoded string the Gram chat service stores on
69
+ * assistant rows; `tool_call_id` is the id the corresponding tool-response row
70
+ * carries when `role === "tool"`.
71
+ */
72
+ export interface GramChatMessage {
35
73
  id: string;
36
74
  model: string;
37
75
  created_at: Date | string;
38
- };
76
+ role: "system" | "developer" | "user" | "assistant" | "tool";
77
+ content?: GramChatContent | null;
78
+ name?: string;
79
+ tool_calls?: string;
80
+ tool_call_id?: string;
81
+ reasoning?: string | null;
82
+ }
39
83
 
40
84
  /**
41
85
  * Represents a chat from the Gram API.
@@ -96,29 +140,23 @@ function buildUserContentParts(msg: GramChatMessage): ThreadUserMessagePart[] {
96
140
  text: item.text,
97
141
  });
98
142
  break;
99
- case "image_url":
143
+ case "image_url": {
144
+ const url = item.image_url?.url ?? "";
100
145
  parts.push({
101
146
  type: "image",
102
- image: (item as any).image_url?.url as FIXME<
103
- string,
104
- "Fixed by switching to Gram TS SDK."
105
- >,
147
+ image: url,
106
148
  });
107
149
  break;
150
+ }
108
151
  case "input_audio": {
109
- const format = (item as any).input_audio?.format as FIXME<
110
- string,
111
- "Fixed by switching to Gram TS SDK."
112
- >;
113
- if (format === "mp3" || format === "wav") {
152
+ const format = item.input_audio?.format;
153
+ const data = item.input_audio?.data;
154
+ if ((format === "mp3" || format === "wav") && data) {
114
155
  parts.push({
115
156
  type: "audio",
116
157
  audio: {
117
- data: (item as any).input_audio.data as FIXME<
118
- string,
119
- "Fixed by switching to Gram TS SDK."
120
- >,
121
- format: format,
158
+ data,
159
+ format,
122
160
  },
123
161
  });
124
162
  }
@@ -155,12 +193,18 @@ function buildAssistantContentParts(
155
193
  });
156
194
  }
157
195
 
158
- const toolCallsJSON = (msg as any).tool_calls as FIXME<
159
- string | undefined,
160
- "Fixed by switching to Gram TS SDK."
161
- >;
196
+ // Accept both the OpenAI/OpenRouter shape (`{ id, function: { name, arguments } }`)
197
+ // and the assistant-ui shape (`{ toolCallId, toolName, args }`). Tool calls
198
+ // arrive as JSON the server stored opaquely, so we model the union here.
199
+ type WireToolCall = {
200
+ id?: string;
201
+ toolCallId?: string;
202
+ function?: { name?: string; arguments?: string | Record<string, unknown> };
203
+ toolName?: string;
204
+ args?: string | Record<string, unknown>;
205
+ };
162
206
 
163
- let toolCalls = tryParseJSON(toolCallsJSON || "[]");
207
+ let toolCalls = tryParseJSON<WireToolCall[]>(msg.tool_calls || "[]");
164
208
  if (!Array.isArray(toolCalls)) {
165
209
  console.warn("Invalid tool_calls format, expected an array.");
166
210
  toolCalls = [];
@@ -280,8 +324,10 @@ function convertGramMessageToThreadMessage(
280
324
  * Converts an array of Gram ChatMessages to an ExportedMessageRepository.
281
325
  * Creates parent-child relationships based on message order.
282
326
  *
283
- * Note: System messages are filtered out because assistant-ui's
284
- * `fromThreadMessageLike` doesn't support them in the exported format.
327
+ * Note: system, developer, and tool messages are filtered out. assistant-ui's
328
+ * exported format only models user/assistant turns; system/developer rows are
329
+ * pre-prompt instructions the UI doesn't render, and tool rows are folded into
330
+ * the preceding assistant message as `tool-call` parts via `tool_calls`.
285
331
  */
286
332
  export function convertGramMessagesToExported(
287
333
  messages: GramChatMessage[],
@@ -294,8 +340,11 @@ export function convertGramMessagesToExported(
294
340
  let prevId: string | null = null;
295
341
 
296
342
  for (const msg of messages) {
297
- // Skip system messages - they're not supported in the exported message format
298
- if (msg.role === "system") {
343
+ if (
344
+ msg.role === "system" ||
345
+ msg.role === "developer" ||
346
+ msg.role === "tool"
347
+ ) {
299
348
  continue;
300
349
  }
301
350
 
@@ -322,17 +371,17 @@ export function convertGramMessagesToUIMessages(messages: GramChatMessage[]): {
322
371
  return { messages: [], headId: null };
323
372
  }
324
373
 
325
- const toolCallResults = new Map<string, ToolResponseMessage>();
374
+ const toolCallResults = new Map<string, GramChatMessage>();
326
375
  for (const msg of messages) {
327
376
  if (msg.role !== "tool") {
328
377
  continue;
329
378
  }
330
- const id = (msg as any).tool_call_id;
379
+ const id = msg.tool_call_id;
331
380
  if (typeof id !== "string") {
332
381
  continue;
333
382
  }
334
383
 
335
- toolCallResults.set(id, msg as ToolResponseMessage);
384
+ toolCallResults.set(id, msg);
336
385
  }
337
386
 
338
387
  const uiMessages: { parentId: string | null; message: UIMessage }[] = [];
@@ -415,9 +464,19 @@ export function convertGramMessagesToUIMessages(messages: GramChatMessage[]): {
415
464
  };
416
465
  }
417
466
 
467
+ /**
468
+ * Parsed shape of a single entry inside an assistant message's `tool_calls`
469
+ * JSON string. Mirrors the OpenAI/OpenRouter tool-call wire format.
470
+ */
471
+ interface GramToolCall {
472
+ id: string;
473
+ type?: "function";
474
+ function?: { name?: string; arguments?: string | Record<string, unknown> };
475
+ }
476
+
418
477
  export function convertGramMessagePartsToUIMessageParts(
419
- msg: UserMessage | AssistantMessage,
420
- toolResults: Map<string, ToolResponseMessage>,
478
+ msg: GramChatMessage,
479
+ toolResults: Map<string, GramChatMessage>,
421
480
  seenToolCallIds?: Set<string>,
422
481
  ): UIMessage["parts"] {
423
482
  const uiparts: UIMessage["parts"] = [];
@@ -440,10 +499,7 @@ export function convertGramMessagePartsToUIMessageParts(
440
499
  break;
441
500
  }
442
501
  case "image_url": {
443
- const url = (p as any).image_url?.url as FIXME<
444
- string | undefined,
445
- "Fixed by switching to Gram TS SDK."
446
- >;
502
+ const url = p.image_url?.url;
447
503
  if (!url) {
448
504
  break;
449
505
  }
@@ -456,10 +512,7 @@ export function convertGramMessagePartsToUIMessageParts(
456
512
  break;
457
513
  }
458
514
  case "input_audio": {
459
- const url = (p as any).input_audio?.data as FIXME<
460
- string | undefined,
461
- "Fixed by switching to Gram TS SDK."
462
- >;
515
+ const url = p.input_audio?.data;
463
516
  if (!url) {
464
517
  break;
465
518
  }
@@ -481,14 +534,8 @@ export function convertGramMessagePartsToUIMessageParts(
481
534
  });
482
535
  }
483
536
 
484
- if (msg.role === "assistant" && (msg as any).tool_calls) {
485
- const toolCallsJSON = (msg as any).tool_calls as FIXME<
486
- string,
487
- "Fixed by switching to Gram TS SDK."
488
- >;
489
- let toolCalls = tryParseJSON<AssistantMessage["toolCalls"]>(
490
- toolCallsJSON || "[]",
491
- );
537
+ if (msg.role === "assistant" && msg.tool_calls) {
538
+ let toolCalls = tryParseJSON<GramToolCall[]>(msg.tool_calls || "[]");
492
539
  if (!Array.isArray(toolCalls)) {
493
540
  console.warn("Invalid tool_calls format, expected an array.");
494
541
  toolCalls = [];
@@ -526,7 +573,7 @@ function mediaTypeFromURL(url: string): string {
526
573
  return match?.[1] || unspecified;
527
574
  }
528
575
 
529
- function tryParseJSON<T = any>(str: string): T | null {
576
+ function tryParseJSON<T = unknown>(str: string): T | null {
530
577
  try {
531
578
  return JSON.parse(str) as T;
532
579
  } catch {
package/src/lib/models.ts CHANGED
@@ -1,31 +1,43 @@
1
1
  // List of openrouter models available to the user
2
2
  // This list should be updated to match the model whitelist on the backend side.
3
3
  export const MODELS = [
4
- "anthropic/claude-opus-4.6",
4
+ "anthropic/claude-opus-4.8",
5
+ "anthropic/claude-opus-4.7",
5
6
  "anthropic/claude-sonnet-4.6",
6
7
  "anthropic/claude-sonnet-4.5",
8
+ "anthropic/claude-opus-4.6",
7
9
  "anthropic/claude-opus-4.5",
8
10
  "anthropic/claude-haiku-4.5",
9
- "anthropic/claude-opus-4.1",
10
11
  "anthropic/claude-sonnet-4",
12
+ "openai/gpt-5.5",
13
+ "openai/gpt-5.5-pro",
11
14
  "openai/gpt-5.4",
12
15
  "openai/gpt-5.4-mini",
16
+ "openai/gpt-5.4-nano",
17
+ "openai/gpt-5.3-codex",
13
18
  "openai/gpt-5.1",
14
- "openai/gpt-5.1-codex",
15
19
  "openai/gpt-5",
16
20
  "openai/gpt-4.1",
17
21
  "openai/o4-mini",
18
22
  "openai/o3",
23
+ "google/gemini-3.5-flash",
19
24
  "google/gemini-3.1-pro-preview",
25
+ "google/gemini-3.1-flash-lite",
20
26
  "google/gemini-2.5-pro",
21
27
  "google/gemini-2.5-flash",
22
- "deepseek/deepseek-r1",
28
+ "deepseek/deepseek-v4-pro",
29
+ "deepseek/deepseek-v4-flash",
23
30
  "deepseek/deepseek-v3.2",
31
+ "deepseek/deepseek-r1",
24
32
  "meta-llama/llama-4-maverick",
25
- "x-ai/grok-4",
33
+ "x-ai/grok-4.3",
34
+ "x-ai/grok-4.20",
35
+ "qwen/qwen3.7-max",
26
36
  "qwen/qwen3-coder",
37
+ "moonshotai/kimi-k2.6",
27
38
  "moonshotai/kimi-k2.5",
28
- "mistralai/mistral-medium-3.1",
39
+ "mistralai/mistral-medium-3-5",
29
40
  "mistralai/codestral-2508",
30
- "mistralai/devstral-small",
41
+ "mistralai/devstral-2512",
42
+ "mistralai/mistral-medium-3.1",
31
43
  ] as const;
package/src/lib/token.ts CHANGED
@@ -9,7 +9,7 @@ export function getTokenExpiry(token: string): number | null {
9
9
  if (parts.length !== 3) return null;
10
10
 
11
11
  // base64url → base64 → decode
12
- let payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
12
+ let payload = parts[1]!.replace(/-/g, "+").replace(/_/g, "/");
13
13
  while (payload.length % 4) payload += "=";
14
14
 
15
15
  const json = atob(payload);
@@ -29,10 +29,7 @@ export function getTokenExpiry(token: string): number | null {
29
29
  * of expiry. Fails open (returns false) for non-JWT tokens or tokens
30
30
  * without an `exp` claim so they pass through unchanged.
31
31
  */
32
- export function isTokenExpired(
33
- token: string,
34
- bufferMs: number = 30_000,
35
- ): boolean {
32
+ export function isTokenExpired(token: string, bufferMs = 30_000): boolean {
36
33
  const exp = getTokenExpiry(token);
37
34
  if (exp === null) return false; // fail-open for non-JWT tokens
38
35
  return Date.now() >= exp * 1000 - bufferMs;