@gram-ai/elements 1.21.2 → 1.22.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 (57) hide show
  1. package/dist/components/Chat/stories/StyleIsolation.stories.d.ts +6 -0
  2. package/dist/components/Chat/stories/Theme.stories.d.ts +8 -0
  3. package/dist/components/Chat/stories/ToolMentions.stories.d.ts +19 -0
  4. package/dist/components/ChatHistory.d.ts +5 -0
  5. package/dist/components/ShadowRoot.d.ts +8 -0
  6. package/dist/components/assistant-ui/mentioned-tools-badges.d.ts +10 -0
  7. package/dist/components/assistant-ui/tool-mention-autocomplete.d.ts +12 -0
  8. package/dist/elements.cjs +1 -1
  9. package/dist/elements.css +1 -1
  10. package/dist/elements.js +1 -1
  11. package/dist/embedded.d.ts +0 -0
  12. package/dist/hooks/useGramThreadListAdapter.d.ts +7 -0
  13. package/dist/hooks/useMCPTools.d.ts +2 -1
  14. package/dist/hooks/useToolMentions.d.ts +17 -0
  15. package/dist/index-DSGCuihM.cjs +145 -0
  16. package/dist/index-DSGCuihM.cjs.map +1 -0
  17. package/dist/index-DiaPGyWF.js +38113 -0
  18. package/dist/index-DiaPGyWF.js.map +1 -0
  19. package/dist/index.d.ts +2 -2
  20. package/dist/lib/tool-mentions.d.ts +20 -0
  21. package/dist/{profiler-DPH9zydw.cjs → profiler-Cr1UEa8B.cjs} +2 -2
  22. package/dist/{profiler-DPH9zydw.cjs.map → profiler-Cr1UEa8B.cjs.map} +1 -1
  23. package/dist/{profiler-D_HNXmyv.js → profiler-DL_h66-O.js} +2 -2
  24. package/dist/{profiler-D_HNXmyv.js.map → profiler-DL_h66-O.js.map} +1 -1
  25. package/dist/{startRecording-DOMzQAAr.js → startRecording-BW2zkd0-.js} +2 -2
  26. package/dist/{startRecording-DOMzQAAr.js.map → startRecording-BW2zkd0-.js.map} +1 -1
  27. package/dist/{startRecording-B97e4Mlu.cjs → startRecording-D8-ckYMS.cjs} +2 -2
  28. package/dist/{startRecording-B97e4Mlu.cjs.map → startRecording-D8-ckYMS.cjs.map} +1 -1
  29. package/dist/types/index.d.ts +20 -0
  30. package/package.json +13 -4
  31. package/src/components/Chat/index.tsx +4 -13
  32. package/src/components/Chat/stories/StyleIsolation.stories.tsx +41 -0
  33. package/src/components/Chat/stories/Theme.stories.tsx +74 -0
  34. package/src/components/Chat/stories/ToolApproval.stories.tsx +0 -1
  35. package/src/components/Chat/stories/ToolMentions.stories.tsx +62 -0
  36. package/src/components/ChatHistory.tsx +16 -0
  37. package/src/components/ShadowRoot.tsx +90 -0
  38. package/src/components/assistant-ui/markdown-text.tsx +0 -2
  39. package/src/components/assistant-ui/mentioned-tools-badges.tsx +105 -0
  40. package/src/components/assistant-ui/thread-list.tsx +1 -3
  41. package/src/components/assistant-ui/thread.tsx +106 -1
  42. package/src/components/assistant-ui/tool-mention-autocomplete.tsx +253 -0
  43. package/src/components/ui/tool-ui.tsx +1 -1
  44. package/src/contexts/ElementsProvider.tsx +67 -10
  45. package/src/embedded.ts +3 -0
  46. package/src/global.css +45 -0
  47. package/src/hooks/useGramThreadListAdapter.tsx +90 -9
  48. package/src/hooks/useMCPTools.ts +4 -1
  49. package/src/hooks/useToolMentions.ts +93 -0
  50. package/src/index.ts +2 -1
  51. package/src/lib/tool-mentions.ts +106 -0
  52. package/src/types/index.ts +23 -0
  53. package/src/vite-env.d.ts +5 -0
  54. package/dist/index-BVvrv2G3.cjs +0 -169
  55. package/dist/index-BVvrv2G3.cjs.map +0 -1
  56. package/dist/index-OU3wjArm.js +0 -54670
  57. package/dist/index-OU3wjArm.js.map +0 -1
@@ -0,0 +1,105 @@
1
+ import { FC } from 'react'
2
+ import { Wrench, X } from 'lucide-react'
3
+ import { AnimatePresence } from 'motion/react'
4
+ import * as m from 'motion/react-m'
5
+
6
+ import { cn } from '@/lib/utils'
7
+ import { useDensity } from '@/hooks/useDensity'
8
+ import { useRadius } from '@/hooks/useRadius'
9
+ import { EASE_OUT_QUINT } from '@/lib/easing'
10
+ import { MentionableTool } from '@/lib/tool-mentions'
11
+
12
+ export interface MentionedToolsBadgesProps {
13
+ mentionedToolIds: string[]
14
+ tools: MentionableTool[]
15
+ onRemove?: (toolId: string) => void
16
+ className?: string
17
+ }
18
+
19
+ export const MentionedToolsBadges: FC<MentionedToolsBadgesProps> = ({
20
+ mentionedToolIds,
21
+ tools,
22
+ onRemove,
23
+ className,
24
+ }) => {
25
+ const d = useDensity()
26
+ const mentionedTools = tools.filter((tool) =>
27
+ mentionedToolIds.includes(tool.id)
28
+ )
29
+
30
+ if (mentionedTools.length === 0) {
31
+ return null
32
+ }
33
+
34
+ return (
35
+ <div
36
+ className={cn(
37
+ 'aui-mentioned-tools-badges flex flex-wrap items-center gap-1',
38
+ d('px-sm'),
39
+ d('py-xs'),
40
+ className
41
+ )}
42
+ >
43
+ <span className="text-muted-foreground flex-shrink-0 text-xs">
44
+ Tools:
45
+ </span>
46
+ <AnimatePresence mode="popLayout">
47
+ {mentionedTools.map((tool) => (
48
+ <m.div
49
+ key={tool.id}
50
+ layout
51
+ initial={{ opacity: 0, scale: 0.8 }}
52
+ animate={{ opacity: 1, scale: 1 }}
53
+ exit={{ opacity: 0, scale: 0.8 }}
54
+ transition={{ duration: 0.15, ease: EASE_OUT_QUINT }}
55
+ >
56
+ <ToolBadge
57
+ tool={tool}
58
+ onRemove={onRemove ? () => onRemove(tool.id) : undefined}
59
+ />
60
+ </m.div>
61
+ ))}
62
+ </AnimatePresence>
63
+ </div>
64
+ )
65
+ }
66
+
67
+ interface ToolBadgeProps {
68
+ tool: MentionableTool
69
+ onRemove?: () => void
70
+ }
71
+
72
+ const ToolBadge: FC<ToolBadgeProps> = ({ tool, onRemove }) => {
73
+ const d = useDensity()
74
+ const r = useRadius()
75
+
76
+ return (
77
+ <div
78
+ className={cn(
79
+ 'aui-tool-badge bg-primary/10 text-primary inline-flex items-center gap-1',
80
+ r('md'),
81
+ d('px-sm'),
82
+ d('py-xs')
83
+ )}
84
+ >
85
+ <Wrench className="size-3 flex-shrink-0" />
86
+ <span className="text-xs font-medium">{tool.name}</span>
87
+ {onRemove && (
88
+ <button
89
+ type="button"
90
+ onClick={(e) => {
91
+ e.preventDefault()
92
+ e.stopPropagation()
93
+ onRemove()
94
+ }}
95
+ className="hover:opacity-70 focus:outline-none"
96
+ aria-label={`Remove ${tool.name}`}
97
+ >
98
+ <X className="size-3 flex-shrink-0" />
99
+ </button>
100
+ )}
101
+ </div>
102
+ )
103
+ }
104
+
105
+ export default MentionedToolsBadges
@@ -11,7 +11,6 @@ import { Skeleton } from '@/components/ui/skeleton'
11
11
  import { useRadius } from '@/hooks/useRadius'
12
12
  import { cn } from '@/lib/utils'
13
13
  import { useDensity } from '@/hooks/useDensity'
14
- import { ROOT_SELECTOR } from '@/constants/tailwind'
15
14
 
16
15
  interface ThreadListProps {
17
16
  className?: string
@@ -24,8 +23,7 @@ export const ThreadList: FC<ThreadListProps> = ({ className }) => {
24
23
  className={cn(
25
24
  'aui-root aui-thread-list-root bg-background flex flex-col items-stretch',
26
25
  d('gap-sm'),
27
- className,
28
- ROOT_SELECTOR
26
+ className
29
27
  )}
30
28
  >
31
29
  <div
@@ -23,6 +23,7 @@ import {
23
23
  import { LazyMotion, MotionConfig, domAnimation } from 'motion/react'
24
24
  import * as m from 'motion/react-m'
25
25
  import { useEffect, useRef, useState, type FC } from 'react'
26
+ import { AnimatePresence } from 'motion/react'
26
27
 
27
28
  import {
28
29
  ComposerAddAttachment,
@@ -32,8 +33,11 @@ import {
32
33
  import { MarkdownText } from '@/components/assistant-ui/markdown-text'
33
34
  import { Reasoning, ReasoningGroup } from '@/components/assistant-ui/reasoning'
34
35
  import { ToolFallback } from '@/components/assistant-ui/tool-fallback'
36
+ import { ToolMentionAutocomplete } from '@/components/assistant-ui/tool-mention-autocomplete'
37
+ import { MentionedToolsBadges } from '@/components/assistant-ui/mentioned-tools-badges'
35
38
  import { TooltipIconButton } from '@/components/assistant-ui/tooltip-icon-button'
36
39
  import { Button } from '@/components/ui/button'
40
+ import { useToolMentions } from '@/hooks/useToolMentions'
37
41
 
38
42
  import { useDensity } from '@/hooks/useDensity'
39
43
  import { useElements } from '@/hooks/useElements'
@@ -262,8 +266,97 @@ const ThreadSuggestions: FC = () => {
262
266
  )
263
267
  }
264
268
 
269
+ /**
270
+ * Component that handles tool mentions (@tool) in the composer.
271
+ * Shows autocomplete dropdown and badges for mentioned tools.
272
+ */
273
+ const ComposerToolMentions: FC<{
274
+ tools: Record<string, unknown> | undefined
275
+ }> = ({ tools }) => {
276
+ const containerRef = useRef<HTMLDivElement>(null)
277
+
278
+ const {
279
+ mentionableTools,
280
+ mentionedToolIds,
281
+ value,
282
+ cursorPosition,
283
+ textareaRef,
284
+ updateCursorPosition,
285
+ handleAutocompleteChange,
286
+ removeMention,
287
+ isActive,
288
+ } = useToolMentions({ tools })
289
+
290
+ // Find and attach to the textarea within the composer
291
+ useEffect(() => {
292
+ if (!isActive) return
293
+
294
+ const findTextarea = () => {
295
+ const textarea = document.querySelector(
296
+ '.aui-composer-input'
297
+ ) as HTMLTextAreaElement | null
298
+ if (textarea && textareaRef.current !== textarea) {
299
+ textareaRef.current = textarea
300
+
301
+ const handleSelectionChange = () => updateCursorPosition()
302
+ textarea.addEventListener('click', handleSelectionChange)
303
+ textarea.addEventListener('keyup', handleSelectionChange)
304
+ textarea.addEventListener('input', handleSelectionChange)
305
+
306
+ return () => {
307
+ textarea.removeEventListener('click', handleSelectionChange)
308
+ textarea.removeEventListener('keyup', handleSelectionChange)
309
+ textarea.removeEventListener('input', handleSelectionChange)
310
+ }
311
+ }
312
+ }
313
+
314
+ const cleanup = findTextarea()
315
+
316
+ const observer = new MutationObserver(() => {
317
+ findTextarea()
318
+ })
319
+
320
+ observer.observe(document.body, {
321
+ childList: true,
322
+ subtree: true,
323
+ })
324
+
325
+ return () => {
326
+ cleanup?.()
327
+ observer.disconnect()
328
+ }
329
+ }, [isActive, textareaRef, updateCursorPosition])
330
+
331
+ if (!isActive) {
332
+ return null
333
+ }
334
+
335
+ return (
336
+ <div ref={containerRef} className="aui-composer-tool-mentions relative">
337
+ {/* Badges showing mentioned tools */}
338
+ <MentionedToolsBadges
339
+ mentionedToolIds={mentionedToolIds}
340
+ tools={mentionableTools}
341
+ onRemove={removeMention}
342
+ />
343
+
344
+ {/* Autocomplete dropdown */}
345
+ <AnimatePresence>
346
+ <ToolMentionAutocomplete
347
+ tools={mentionableTools}
348
+ value={value}
349
+ cursorPosition={cursorPosition}
350
+ onValueChange={handleAutocompleteChange}
351
+ textareaRef={textareaRef}
352
+ />
353
+ </AnimatePresence>
354
+ </div>
355
+ )
356
+ }
357
+
265
358
  const Composer: FC = () => {
266
- const { config } = useElements()
359
+ const { config, mcpTools } = useElements()
267
360
  const r = useRadius()
268
361
  const d = useDensity()
269
362
  const composerConfig = config.composer ?? {
@@ -272,6 +365,15 @@ const Composer: FC = () => {
272
365
  }
273
366
  const components = config.components ?? {}
274
367
 
368
+ // Determine if tool mentions are enabled (default: true)
369
+ const toolMentionsEnabled =
370
+ composerConfig.toolMentions === undefined ||
371
+ composerConfig.toolMentions === true ||
372
+ (typeof composerConfig.toolMentions === 'object' &&
373
+ composerConfig.toolMentions.enabled !== false)
374
+
375
+ const composerRootRef = useRef<HTMLFormElement>(null)
376
+
275
377
  if (components.Composer) {
276
378
  return <components.Composer />
277
379
  }
@@ -287,6 +389,7 @@ const Composer: FC = () => {
287
389
  >
288
390
  <ThreadScrollToBottom />
289
391
  <ComposerPrimitive.Root
392
+ ref={composerRootRef}
290
393
  className={cn(
291
394
  'aui-composer-root group/input-group border-input bg-background has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-ring/5 dark:bg-background relative flex w-full flex-col border px-1 pt-2 shadow-xs transition-[color,box-shadow] outline-none has-[textarea:focus-visible]:ring-1',
292
395
  r('xl')
@@ -294,6 +397,8 @@ const Composer: FC = () => {
294
397
  >
295
398
  {composerConfig.attachments && <ComposerAttachments />}
296
399
 
400
+ {toolMentionsEnabled && <ComposerToolMentions tools={mcpTools} />}
401
+
297
402
  <ComposerPrimitive.Input
298
403
  placeholder={composerConfig.placeholder}
299
404
  className={cn(
@@ -0,0 +1,253 @@
1
+ import { FC, useCallback, useEffect, useRef, useState } from 'react'
2
+ import { Wrench } from 'lucide-react'
3
+ import * as m from 'motion/react-m'
4
+
5
+ import { cn } from '@/lib/utils'
6
+ import { useDensity } from '@/hooks/useDensity'
7
+ import { useRadius } from '@/hooks/useRadius'
8
+ import { EASE_OUT_QUINT } from '@/lib/easing'
9
+ import {
10
+ MentionableTool,
11
+ detectMentionContext,
12
+ filterToolsByQuery,
13
+ insertToolMention,
14
+ } from '@/lib/tool-mentions'
15
+
16
+ export interface ToolMentionAutocompleteProps {
17
+ tools: MentionableTool[]
18
+ value: string
19
+ cursorPosition: number
20
+ onValueChange: (value: string, cursorPosition: number) => void
21
+ textareaRef: React.RefObject<HTMLTextAreaElement | null>
22
+ className?: string
23
+ }
24
+
25
+ export const ToolMentionAutocomplete: FC<ToolMentionAutocompleteProps> = ({
26
+ tools,
27
+ value,
28
+ cursorPosition,
29
+ onValueChange,
30
+ textareaRef,
31
+ className,
32
+ }) => {
33
+ const [selectedIndex, setSelectedIndex] = useState(0)
34
+ const [isVisible, setIsVisible] = useState(false)
35
+ const [filteredTools, setFilteredTools] = useState<MentionableTool[]>([])
36
+ const [mentionContext, setMentionContext] = useState<{
37
+ atPosition: number
38
+ query: string
39
+ } | null>(null)
40
+
41
+ const containerRef = useRef<HTMLDivElement>(null)
42
+ const d = useDensity()
43
+ const r = useRadius()
44
+
45
+ useEffect(() => {
46
+ const context = detectMentionContext(value, cursorPosition)
47
+
48
+ if (context.isInMention && tools.length > 0) {
49
+ const filtered = filterToolsByQuery(tools, context.query)
50
+ setFilteredTools(filtered)
51
+ setIsVisible(filtered.length > 0)
52
+ setMentionContext({
53
+ atPosition: context.atPosition,
54
+ query: context.query,
55
+ })
56
+ setSelectedIndex(0)
57
+ } else {
58
+ setIsVisible(false)
59
+ setMentionContext(null)
60
+ }
61
+ }, [value, cursorPosition, tools])
62
+
63
+ const selectTool = useCallback(
64
+ (tool: MentionableTool) => {
65
+ if (!mentionContext) return
66
+
67
+ const result = insertToolMention(
68
+ value,
69
+ tool.name,
70
+ mentionContext.atPosition,
71
+ cursorPosition
72
+ )
73
+
74
+ onValueChange(result.text, result.cursorPosition)
75
+ setIsVisible(false)
76
+ },
77
+ [mentionContext, value, cursorPosition, onValueChange, textareaRef]
78
+ )
79
+
80
+ useEffect(() => {
81
+ if (!isVisible) return
82
+
83
+ const handleKeyDown = (e: KeyboardEvent) => {
84
+ switch (e.key) {
85
+ case 'ArrowDown':
86
+ e.preventDefault()
87
+ e.stopPropagation()
88
+ setSelectedIndex((prev) => (prev + 1) % filteredTools.length)
89
+ break
90
+ case 'ArrowUp':
91
+ e.preventDefault()
92
+ e.stopPropagation()
93
+ setSelectedIndex(
94
+ (prev) => (prev - 1 + filteredTools.length) % filteredTools.length
95
+ )
96
+ break
97
+ case 'Enter':
98
+ case 'Tab':
99
+ e.preventDefault()
100
+ e.stopPropagation()
101
+ if (filteredTools[selectedIndex]) {
102
+ selectTool(filteredTools[selectedIndex])
103
+ }
104
+ break
105
+ case 'Escape':
106
+ e.preventDefault()
107
+ e.stopPropagation()
108
+ setIsVisible(false)
109
+ break
110
+ }
111
+ }
112
+
113
+ const textarea = textareaRef.current
114
+ if (textarea) {
115
+ textarea.addEventListener('keydown', handleKeyDown, { capture: true })
116
+ return () =>
117
+ textarea.removeEventListener('keydown', handleKeyDown, {
118
+ capture: true,
119
+ })
120
+ }
121
+ }, [isVisible, filteredTools, selectedIndex, selectTool, textareaRef])
122
+
123
+ // Scroll selected item into view
124
+ useEffect(() => {
125
+ if (!isVisible) return
126
+ const container = containerRef.current
127
+ if (!container) return
128
+
129
+ const selectedItem = container.querySelector(
130
+ `[data-index="${selectedIndex}"]`
131
+ ) as HTMLElement
132
+ if (selectedItem) {
133
+ selectedItem.scrollIntoView({ block: 'nearest' })
134
+ }
135
+ }, [selectedIndex, isVisible])
136
+
137
+ useEffect(() => {
138
+ if (!isVisible) return
139
+
140
+ const handleClickOutside = (e: MouseEvent) => {
141
+ if (
142
+ containerRef.current &&
143
+ !containerRef.current.contains(e.target as Node) &&
144
+ textareaRef.current &&
145
+ !textareaRef.current.contains(e.target as Node)
146
+ ) {
147
+ setIsVisible(false)
148
+ }
149
+ }
150
+
151
+ document.addEventListener('mousedown', handleClickOutside)
152
+ return () => document.removeEventListener('mousedown', handleClickOutside)
153
+ }, [isVisible, textareaRef])
154
+
155
+ // When autocomplete is visible, modify composer styles and position autocomplete to match composer width
156
+ useEffect(() => {
157
+ const textarea = textareaRef.current
158
+ if (!textarea) return
159
+
160
+ const composer = textarea.closest('.aui-composer-root') as HTMLElement
161
+ if (!composer) return
162
+
163
+ const updateStyles = () => {
164
+ const autocomplete = containerRef.current
165
+ if (!autocomplete) return
166
+
167
+ if (isVisible) {
168
+ // Modify composer to connect with autocomplete
169
+ composer.style.borderTopColor = 'var(--ring)'
170
+ composer.style.borderTopLeftRadius = '0'
171
+ composer.style.borderTopRightRadius = '0'
172
+
173
+ // Position autocomplete to match composer width
174
+ const composerRect = composer.getBoundingClientRect()
175
+ const autocompleteParent = autocomplete.offsetParent as HTMLElement
176
+ if (autocompleteParent) {
177
+ const parentRect = autocompleteParent.getBoundingClientRect()
178
+ autocomplete.style.left = `${composerRect.left - parentRect.left}px`
179
+ autocomplete.style.right = 'auto'
180
+ autocomplete.style.width = `${composerRect.width}px`
181
+ }
182
+ }
183
+ }
184
+
185
+ if (isVisible) {
186
+ // Use requestAnimationFrame to ensure DOM is updated
187
+ requestAnimationFrame(updateStyles)
188
+ } else {
189
+ composer.style.borderTopColor = ''
190
+ composer.style.borderTopLeftRadius = ''
191
+ composer.style.borderTopRightRadius = ''
192
+ }
193
+
194
+ return () => {
195
+ composer.style.borderTopColor = ''
196
+ composer.style.borderTopLeftRadius = ''
197
+ composer.style.borderTopRightRadius = ''
198
+ }
199
+ }, [isVisible, textareaRef])
200
+
201
+ if (!isVisible || filteredTools.length === 0) {
202
+ return null
203
+ }
204
+
205
+ return (
206
+ <m.div
207
+ ref={containerRef}
208
+ initial={{ opacity: 0, y: 8 }}
209
+ animate={{ opacity: 1, y: 0 }}
210
+ exit={{ opacity: 0, y: 8 }}
211
+ transition={{ duration: 0.15, ease: EASE_OUT_QUINT }}
212
+ className={cn(
213
+ 'aui-tool-mention-autocomplete border-ring bg-background absolute bottom-full z-50 max-h-[220px] overflow-clip overflow-y-auto overscroll-contain rounded-br-none! rounded-bl-none! border border-b-0 shadow-xs',
214
+ r('xl'),
215
+ className
216
+ )}
217
+ >
218
+ <div className="flex flex-col gap-1">
219
+ {filteredTools.map((tool, index) => (
220
+ <button
221
+ key={tool.id}
222
+ type="button"
223
+ data-index={index}
224
+ className={cn(
225
+ 'aui-tool-mention-item flex w-full items-center gap-2 text-left transition-colors',
226
+ d('p-sm'),
227
+ 'hover:bg-accent hover:text-accent-foreground',
228
+ index === selectedIndex && 'bg-accent text-accent-foreground'
229
+ )}
230
+ onClick={(e) => {
231
+ e.preventDefault()
232
+ e.stopPropagation()
233
+ selectTool(tool)
234
+ }}
235
+ onMouseEnter={() => setSelectedIndex(index)}
236
+ >
237
+ <Wrench className="mt-0.5 size-4 flex-shrink-0 opacity-50" />
238
+ <div className="min-w-0 flex-1">
239
+ <div className="text-sm font-medium">{tool.name}</div>
240
+ {tool.description && (
241
+ <div className="text-muted-foreground line-clamp-2 text-xs">
242
+ {tool.description}
243
+ </div>
244
+ )}
245
+ </div>
246
+ </button>
247
+ ))}
248
+ </div>
249
+ </m.div>
250
+ )
251
+ }
252
+
253
+ export default ToolMentionAutocomplete
@@ -322,7 +322,7 @@ function ToolUISection({
322
322
  <div data-slot="tool-ui-section" className="border-border border-t">
323
323
  <button
324
324
  onClick={() => setIsExpanded(!isExpanded)}
325
- className="hover:bg-accent/50 flex w-full cursor-pointer items-center justify-between px-4 py-2.5 text-left transition-colors"
325
+ className="hover:bg-accent/50 flex w-full cursor-pointer items-center justify-between px-5 py-2.5 text-left transition-colors"
326
326
  >
327
327
  <span className="text-muted-foreground text-sm">{title}</span>
328
328
  <div className="flex items-center gap-1">
@@ -46,7 +46,11 @@ import {
46
46
  import { useAuth } from '../hooks/useAuth'
47
47
  import { ElementsContext } from './contexts'
48
48
  import { ToolApprovalProvider } from './ToolApprovalContext'
49
- import { useGramThreadListAdapter } from '@/hooks/useGramThreadListAdapter'
49
+ import {
50
+ isLocalThreadId,
51
+ useGramThreadListAdapter,
52
+ } from '@/hooks/useGramThreadListAdapter'
53
+ import { ROOT_SELECTOR } from '@/constants/tailwind'
50
54
 
51
55
  export interface ElementsProviderProps {
52
56
  children: ReactNode
@@ -139,6 +143,7 @@ const ElementsProviderWithApproval = ({
139
143
  auth,
140
144
  mcp: config.mcp,
141
145
  environment: config.environment ?? {},
146
+ gramEnvironment: config.gramEnvironment,
142
147
  })
143
148
 
144
149
  // Store approval helpers in ref so they can be used in async contexts
@@ -187,6 +192,9 @@ const ElementsProviderWithApproval = ({
187
192
  // When history is enabled, the thread adapter manages chat IDs instead
188
193
  const chatIdRef = useRef<string | null>(null)
189
194
 
195
+ // Map to share local thread IDs to UUIDs between adapter and transport (for history mode)
196
+ const localIdToUuidMapRef = useRef(new Map<string, string>())
197
+
190
198
  // Create chat transport configuration
191
199
  const transport = useMemo<ChatTransport<UIMessage>>(
192
200
  () => ({
@@ -197,9 +205,47 @@ const ElementsProviderWithApproval = ({
197
205
  throw new Error('Session is loading')
198
206
  }
199
207
 
200
- // Generate chat ID on first message if not already set
201
- if (!chatIdRef.current) {
202
- chatIdRef.current = crypto.randomUUID()
208
+ // Get chat ID - try runtime's thread remoteId first (history mode),
209
+ // fall back to generated ID (non-history mode)
210
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
211
+ const runtimeAny = runtimeRef.current as any
212
+
213
+ // Try multiple paths to get thread info
214
+ const threadListItemState = runtimeAny?.threadListItem?.getState?.()
215
+ const threadsState = runtimeAny?.threads?.getState?.()
216
+
217
+ // Get the thread ID - try different sources
218
+ const threadRemoteId = threadListItemState?.remoteId as
219
+ | string
220
+ | undefined
221
+ const localThreadId = (threadListItemState?.id ??
222
+ threadsState?.mainThreadId ??
223
+ threadsState?.threadIds?.[0]) as string | undefined
224
+
225
+ let chatId = threadRemoteId
226
+
227
+ if (isLocalThreadId(chatId) || (!chatId && localThreadId)) {
228
+ const lookupKey = chatId ?? localThreadId
229
+ if (lookupKey) {
230
+ // For local thread IDs, check if we already have a UUID mapping
231
+ const existingUuid = localIdToUuidMapRef.current.get(lookupKey)
232
+ if (existingUuid) {
233
+ chatId = existingUuid
234
+ } else {
235
+ // Generate a new UUID and store the mapping
236
+ const newUuid = crypto.randomUUID()
237
+ localIdToUuidMapRef.current.set(lookupKey, newUuid)
238
+ chatId = newUuid
239
+ }
240
+ }
241
+ }
242
+
243
+ if (!chatId) {
244
+ // Non-history mode fallback - use stable chatIdRef
245
+ if (!chatIdRef.current) {
246
+ chatIdRef.current = crypto.randomUUID()
247
+ }
248
+ chatId = chatIdRef.current
203
249
  }
204
250
 
205
251
  const context = runtimeRef.current?.thread.getModelContext()
@@ -207,10 +253,13 @@ const ElementsProviderWithApproval = ({
207
253
  getEnabledTools(context?.tools ?? {})
208
254
  )
209
255
 
210
- // Include Gram-Chat-ID header for chat persistence
256
+ // Include Gram-Chat-ID header for chat persistence and Gram-Environment for environment selection
211
257
  const headersWithChatId = {
212
258
  ...auth.headers,
213
- 'Gram-Chat-ID': chatIdRef.current,
259
+ 'Gram-Chat-ID': chatId,
260
+ ...(config.gramEnvironment && {
261
+ 'Gram-Environment': config.gramEnvironment,
262
+ }),
214
263
  }
215
264
 
216
265
  // Create OpenRouter model (only needed when not using custom model)
@@ -300,8 +349,9 @@ const ElementsProviderWithApproval = ({
300
349
  isOpen: isOpen ?? false,
301
350
  setIsOpen,
302
351
  plugins,
352
+ mcpTools,
303
353
  }),
304
- [config, model, isExpanded, isOpen, plugins]
354
+ [config, model, isExpanded, isOpen, plugins, mcpTools]
305
355
  )
306
356
 
307
357
  const frontendTools = config.tools?.frontendTools ?? {}
@@ -317,6 +367,7 @@ const ElementsProviderWithApproval = ({
317
367
  contextValue={contextValue}
318
368
  runtimeRef={runtimeRef}
319
369
  frontendTools={frontendTools}
370
+ localIdToUuidMap={localIdToUuidMapRef.current}
320
371
  >
321
372
  {children}
322
373
  </ElementsProviderWithHistory>
@@ -344,6 +395,7 @@ interface ElementsProviderWithHistoryProps {
344
395
  contextValue: React.ContextType<typeof ElementsContext>
345
396
  runtimeRef: React.RefObject<ReturnType<typeof useChatRuntime> | null>
346
397
  frontendTools: Record<string, AssistantTool>
398
+ localIdToUuidMap: Map<string, string>
347
399
  }
348
400
 
349
401
  const ElementsProviderWithHistory = ({
@@ -354,8 +406,13 @@ const ElementsProviderWithHistory = ({
354
406
  contextValue,
355
407
  runtimeRef,
356
408
  frontendTools,
409
+ localIdToUuidMap,
357
410
  }: ElementsProviderWithHistoryProps) => {
358
- const threadListAdapter = useGramThreadListAdapter({ apiUrl, headers })
411
+ const threadListAdapter = useGramThreadListAdapter({
412
+ apiUrl,
413
+ headers,
414
+ localIdToUuidMap,
415
+ })
359
416
 
360
417
  // Hook factory for creating the base chat runtime
361
418
  const useChatRuntimeHook = useCallback(() => {
@@ -381,7 +438,7 @@ const ElementsProviderWithHistory = ({
381
438
  <AssistantRuntimeProvider runtime={runtime}>
382
439
  <HistoryProvider>
383
440
  <ElementsContext.Provider value={contextValue}>
384
- {children}
441
+ <div className={`${ROOT_SELECTOR} h-full`}>{children}</div>
385
442
  <FrontendTools tools={frontendTools} />
386
443
  </ElementsContext.Provider>
387
444
  </HistoryProvider>
@@ -415,7 +472,7 @@ const ElementsProviderWithoutHistory = ({
415
472
  return (
416
473
  <AssistantRuntimeProvider runtime={runtime}>
417
474
  <ElementsContext.Provider value={contextValue}>
418
- {children}
475
+ <div className={`${ROOT_SELECTOR} h-full`}>{children}</div>
419
476
  <FrontendTools tools={frontendTools} />
420
477
  </ElementsContext.Provider>
421
478
  </AssistantRuntimeProvider>
@@ -0,0 +1,3 @@
1
+ // Side-effect import to include embedded CSS in build
2
+ // Use this CSS for embedding elements in apps that already have Tailwind CSS
3
+ import './global-embedded.css'