@gram-ai/elements 1.18.5 → 1.18.6

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 (85) hide show
  1. package/dist/components/Chat/stories/Sidecar.stories.d.ts +6 -0
  2. package/dist/elements.cjs +22 -21
  3. package/dist/elements.cjs.map +1 -0
  4. package/dist/elements.js +601 -591
  5. package/dist/elements.js.map +1 -0
  6. package/dist/index-Bj7jPiuy.cjs +1 -0
  7. package/dist/index-Bj7jPiuy.cjs.map +1 -0
  8. package/dist/index-CJRypLIa.js +1 -0
  9. package/dist/index-CJRypLIa.js.map +1 -0
  10. package/dist/plugins.cjs +1 -0
  11. package/dist/plugins.cjs.map +1 -0
  12. package/dist/plugins.js +1 -0
  13. package/dist/plugins.js.map +1 -0
  14. package/dist/server.cjs +1 -0
  15. package/dist/server.cjs.map +1 -0
  16. package/dist/server.js +1 -0
  17. package/dist/server.js.map +1 -0
  18. package/package.json +3 -2
  19. package/src/components/Chat/index.tsx +21 -0
  20. package/src/components/Chat/stories/ColorScheme.stories.tsx +52 -0
  21. package/src/components/Chat/stories/Composer.stories.tsx +42 -0
  22. package/src/components/Chat/stories/Customization.stories.tsx +88 -0
  23. package/src/components/Chat/stories/Density.stories.tsx +52 -0
  24. package/src/components/Chat/stories/FrontendTools.stories.tsx +145 -0
  25. package/src/components/Chat/stories/Modal.stories.tsx +84 -0
  26. package/src/components/Chat/stories/Model.stories.tsx +32 -0
  27. package/src/components/Chat/stories/Plugins.stories.tsx +50 -0
  28. package/src/components/Chat/stories/Radius.stories.tsx +52 -0
  29. package/src/components/Chat/stories/Sidecar.stories.tsx +27 -0
  30. package/src/components/Chat/stories/ToolApproval.stories.tsx +110 -0
  31. package/src/components/Chat/stories/Tools.stories.tsx +175 -0
  32. package/src/components/Chat/stories/Variants.stories.tsx +46 -0
  33. package/src/components/Chat/stories/Welcome.stories.tsx +42 -0
  34. package/src/components/FrontendTools/index.tsx +9 -0
  35. package/src/components/assistant-ui/assistant-modal.tsx +255 -0
  36. package/src/components/assistant-ui/assistant-sidecar.tsx +88 -0
  37. package/src/components/assistant-ui/attachment.tsx +233 -0
  38. package/src/components/assistant-ui/markdown-text.tsx +240 -0
  39. package/src/components/assistant-ui/reasoning.tsx +261 -0
  40. package/src/components/assistant-ui/thread-list.tsx +97 -0
  41. package/src/components/assistant-ui/thread.tsx +632 -0
  42. package/src/components/assistant-ui/tool-fallback.tsx +111 -0
  43. package/src/components/assistant-ui/tool-group.tsx +59 -0
  44. package/src/components/assistant-ui/tooltip-icon-button.tsx +57 -0
  45. package/src/components/ui/avatar.tsx +51 -0
  46. package/src/components/ui/button.tsx +27 -0
  47. package/src/components/ui/buttonVariants.ts +33 -0
  48. package/src/components/ui/collapsible.tsx +31 -0
  49. package/src/components/ui/dialog.tsx +141 -0
  50. package/src/components/ui/popover.tsx +46 -0
  51. package/src/components/ui/skeleton.tsx +13 -0
  52. package/src/components/ui/tool-ui.stories.tsx +146 -0
  53. package/src/components/ui/tool-ui.tsx +676 -0
  54. package/src/components/ui/tooltip.tsx +61 -0
  55. package/src/contexts/ElementsProvider.tsx +256 -0
  56. package/src/contexts/ToolApprovalContext.tsx +120 -0
  57. package/src/contexts/contexts.ts +10 -0
  58. package/src/global.css +136 -0
  59. package/src/hooks/useAuth.ts +71 -0
  60. package/src/hooks/useDensity.ts +110 -0
  61. package/src/hooks/useElements.ts +14 -0
  62. package/src/hooks/useExpanded.ts +20 -0
  63. package/src/hooks/useMCPTools.ts +73 -0
  64. package/src/hooks/usePluginComponents.ts +34 -0
  65. package/src/hooks/useRadius.ts +42 -0
  66. package/src/hooks/useSession.ts +38 -0
  67. package/src/hooks/useThemeProps.ts +24 -0
  68. package/src/hooks/useToolApproval.ts +16 -0
  69. package/src/index.ts +45 -0
  70. package/src/lib/api.test.ts +90 -0
  71. package/src/lib/api.ts +8 -0
  72. package/src/lib/auth.ts +10 -0
  73. package/src/lib/easing.ts +1 -0
  74. package/src/lib/humanize.ts +14 -0
  75. package/src/lib/models.ts +22 -0
  76. package/src/lib/tools.ts +210 -0
  77. package/src/lib/utils.ts +16 -0
  78. package/src/plugins/README.md +49 -0
  79. package/src/plugins/chart/component.tsx +102 -0
  80. package/src/plugins/chart/index.ts +27 -0
  81. package/src/plugins/index.ts +7 -0
  82. package/src/server.ts +89 -0
  83. package/src/types/index.ts +726 -0
  84. package/src/types/plugins.ts +65 -0
  85. package/src/vite-env.d.ts +12 -0
@@ -0,0 +1,676 @@
1
+ import * as React from 'react'
2
+ import { useState, useEffect } from 'react'
3
+ import { cva } from 'class-variance-authority'
4
+ import {
5
+ CheckIcon,
6
+ ChevronDownIcon,
7
+ ChevronRightIcon,
8
+ CopyIcon,
9
+ LoaderIcon,
10
+ XIcon,
11
+ } from 'lucide-react'
12
+ import { cn } from '@/lib/utils'
13
+ import { codeToHtml, BundledLanguage } from 'shiki'
14
+ import { Button } from './button'
15
+ import { Popover, PopoverContent, PopoverTrigger } from './popover'
16
+
17
+ /* -----------------------------------------------------------------------------
18
+ * Status indicator styles
19
+ * -------------------------------------------------------------------------- */
20
+
21
+ const statusVariants = cva(
22
+ 'flex size-5 items-center justify-center rounded-full',
23
+ {
24
+ variants: {
25
+ status: {
26
+ pending: 'border border-dashed border-muted-foreground/50',
27
+ running: 'text-primary',
28
+ complete: 'text-green-600 dark:text-green-500',
29
+ error: 'text-destructive',
30
+ approval: 'text-amber-500',
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ status: 'pending',
35
+ },
36
+ }
37
+ )
38
+
39
+ /* -----------------------------------------------------------------------------
40
+ * Types
41
+ * -------------------------------------------------------------------------- */
42
+
43
+ type ToolStatus = 'pending' | 'running' | 'complete' | 'error' | 'approval'
44
+
45
+ type ContentItem =
46
+ | { type: 'text'; text: string; _meta?: { 'getgram.ai/mime-type'?: string } }
47
+ | { type: 'image'; data: string; _meta?: { 'getgram.ai/mime-type'?: string } }
48
+
49
+ interface ToolUIProps {
50
+ /** Display name of the tool */
51
+ name: string
52
+ /** Optional icon to display (defaults to first letter of name) */
53
+ icon?: React.ReactNode
54
+ /** Provider/source name (e.g., "Notion", "GitHub") */
55
+ provider?: string
56
+ /** Current status of the tool execution */
57
+ status?: ToolStatus
58
+ /** Request/input data - can be string or object */
59
+ request?: string | Record<string, unknown>
60
+ /** Result/output data - can be string, object, or structured content array */
61
+ result?: string | Record<string, unknown> | { content: ContentItem[] }
62
+ /** Whether the tool card starts expanded */
63
+ defaultExpanded?: boolean
64
+ /** Additional class names */
65
+ className?: string
66
+ /** Approval callbacks */
67
+ onApproveOnce?: () => void
68
+ onApproveForSession?: () => void
69
+ onDeny?: () => void
70
+ }
71
+
72
+ interface ToolUISectionProps {
73
+ /** Section title */
74
+ title: string
75
+ /** Content to display - string or object (will be JSON stringified) */
76
+ content: string | Record<string, unknown> | { content: ContentItem[] }
77
+ /** Whether section starts expanded */
78
+ defaultExpanded?: boolean
79
+ /** Enable syntax highlighting */
80
+ highlightSyntax?: boolean
81
+ /** Language hint for syntax highlighting */
82
+ language?: BundledLanguage
83
+ }
84
+
85
+ /* -----------------------------------------------------------------------------
86
+ * Helper Functions
87
+ * -------------------------------------------------------------------------- */
88
+
89
+ function getLanguageFromMimeType(
90
+ mimeType: string
91
+ ): BundledLanguage | undefined {
92
+ switch (mimeType) {
93
+ case 'text/markdown':
94
+ return 'markdown'
95
+ case 'text/html':
96
+ return 'html'
97
+ case 'text/css':
98
+ return 'css'
99
+ case 'application/json':
100
+ return 'json'
101
+ case 'text/javascript':
102
+ return 'javascript'
103
+ case 'text/typescript':
104
+ return 'typescript'
105
+ case 'text/python':
106
+ return 'python'
107
+ default:
108
+ return undefined
109
+ }
110
+ }
111
+
112
+ function formatTextForLanguage(
113
+ text: string,
114
+ language: BundledLanguage | undefined
115
+ ): string {
116
+ if (language === 'json') {
117
+ try {
118
+ return JSON.stringify(JSON.parse(text), null, 2)
119
+ } catch {
120
+ return text
121
+ }
122
+ }
123
+ return text
124
+ }
125
+
126
+ function isStructuredContent(
127
+ content: unknown
128
+ ): content is { content: ContentItem[] } {
129
+ return (
130
+ typeof content === 'object' &&
131
+ content !== null &&
132
+ 'content' in content &&
133
+ Array.isArray((content as { content: unknown }).content)
134
+ )
135
+ }
136
+
137
+ /* -----------------------------------------------------------------------------
138
+ * Helper Components
139
+ * -------------------------------------------------------------------------- */
140
+
141
+ function StatusIndicator({ status }: { status: ToolStatus }) {
142
+ return (
143
+ <div className={cn(statusVariants({ status }))}>
144
+ {status === 'pending' && null}
145
+ {status === 'running' && <LoaderIcon className="size-4 animate-spin" />}
146
+ {status === 'complete' && <CheckIcon className="size-4" />}
147
+ {status === 'error' && <XIcon className="size-4" />}
148
+ {status === 'approval' && (
149
+ <LoaderIcon className="text-muted-foreground size-4 animate-spin" />
150
+ )}
151
+ </div>
152
+ )
153
+ }
154
+
155
+ function CopyButton({ content }: { content: string }) {
156
+ const [copied, setCopied] = useState(false)
157
+
158
+ const handleCopy = async (e: React.MouseEvent) => {
159
+ e.stopPropagation()
160
+ await navigator.clipboard.writeText(content)
161
+ setCopied(true)
162
+ setTimeout(() => setCopied(false), 2000)
163
+ }
164
+
165
+ return (
166
+ <button
167
+ onClick={handleCopy}
168
+ className="text-muted-foreground hover:bg-accent hover:text-foreground rounded p-1 transition-colors"
169
+ aria-label="Copy to clipboard"
170
+ >
171
+ {copied ? (
172
+ <CheckIcon className="size-4" />
173
+ ) : (
174
+ <CopyIcon className="size-4" />
175
+ )}
176
+ </button>
177
+ )
178
+ }
179
+
180
+ /* -----------------------------------------------------------------------------
181
+ * SyntaxHighlightedCode - Code block with shiki syntax highlighting
182
+ * -------------------------------------------------------------------------- */
183
+
184
+ function SyntaxHighlightedCode({
185
+ text,
186
+ language,
187
+ className,
188
+ }: {
189
+ text: string
190
+ language?: BundledLanguage
191
+ className?: string
192
+ }) {
193
+ const [highlightedCode, setHighlightedCode] = useState<string | null>(null)
194
+
195
+ useEffect(() => {
196
+ if (!language) return
197
+ codeToHtml(text, {
198
+ lang: language,
199
+ theme: 'github-dark-default',
200
+ rootStyle: 'background-color: transparent;',
201
+ transformers: [
202
+ {
203
+ pre(node) {
204
+ node.properties.class =
205
+ 'w-full py-3 px-4 max-h-[300px] overflow-y-auto whitespace-pre-wrap text-left text-sm'
206
+ },
207
+ },
208
+ ],
209
+ }).then(setHighlightedCode)
210
+ }, [text, language])
211
+
212
+ if (!highlightedCode) {
213
+ return (
214
+ <pre
215
+ className={cn(
216
+ 'w-full bg-slate-800/90 px-4 py-3 text-sm whitespace-pre-wrap text-slate-100',
217
+ className
218
+ )}
219
+ >
220
+ {text}
221
+ </pre>
222
+ )
223
+ }
224
+
225
+ return (
226
+ <div
227
+ className={cn('w-full bg-slate-800/90', className)}
228
+ dangerouslySetInnerHTML={{ __html: highlightedCode }}
229
+ />
230
+ )
231
+ }
232
+
233
+ /* -----------------------------------------------------------------------------
234
+ * ImageContent - Display base64 encoded images with checkerboard background
235
+ * -------------------------------------------------------------------------- */
236
+
237
+ function ImageContent({ data }: { data: string }) {
238
+ const image = `data:image/png;base64,${data}`
239
+ return (
240
+ <div
241
+ className="flex items-center justify-center rounded-lg p-5"
242
+ style={{
243
+ backgroundImage: `linear-gradient(45deg, #ccc 25%, transparent 25%),
244
+ linear-gradient(135deg, #ccc 25%, transparent 25%),
245
+ linear-gradient(45deg, transparent 75%, #ccc 75%),
246
+ linear-gradient(135deg, transparent 75%, #ccc 75%)`,
247
+ backgroundSize: '25px 25px',
248
+ backgroundPosition: '0 0, 12.5px 0, 12.5px -12.5px, 0px 12.5px',
249
+ }}
250
+ >
251
+ <img src={image} className="max-h-[300px] max-w-full object-contain" />
252
+ </div>
253
+ )
254
+ }
255
+
256
+ /* -----------------------------------------------------------------------------
257
+ * StructuredResultContent - Renders structured content array
258
+ * -------------------------------------------------------------------------- */
259
+
260
+ function StructuredResultContent({
261
+ content,
262
+ }: {
263
+ content: { content: ContentItem[] }
264
+ }) {
265
+ return (
266
+ <div className="w-full">
267
+ {content.content.map((item, index) => {
268
+ switch (item.type) {
269
+ case 'text': {
270
+ const language = getLanguageFromMimeType(
271
+ item._meta?.['getgram.ai/mime-type'] ?? 'text/plain'
272
+ )
273
+ const formattedText = formatTextForLanguage(item.text, language)
274
+ return (
275
+ <SyntaxHighlightedCode
276
+ key={index}
277
+ text={formattedText}
278
+ language={language}
279
+ />
280
+ )
281
+ }
282
+ case 'image': {
283
+ return <ImageContent key={index} data={item.data} />
284
+ }
285
+ default:
286
+ return (
287
+ <pre
288
+ key={index}
289
+ className="px-4 py-3 text-sm whitespace-pre-wrap"
290
+ >
291
+ {JSON.stringify(item, null, 2)}
292
+ </pre>
293
+ )
294
+ }
295
+ })}
296
+ </div>
297
+ )
298
+ }
299
+
300
+ /* -----------------------------------------------------------------------------
301
+ * ToolUISection - Expandable section for Request/Result
302
+ * -------------------------------------------------------------------------- */
303
+
304
+ function ToolUISection({
305
+ title,
306
+ content,
307
+ defaultExpanded = false,
308
+ highlightSyntax = true,
309
+ language = 'json',
310
+ }: ToolUISectionProps) {
311
+ const [isExpanded, setIsExpanded] = useState(defaultExpanded)
312
+
313
+ // For structured content, we don't stringify it
314
+ const isStructured = isStructuredContent(content)
315
+ const contentString = isStructured
316
+ ? JSON.stringify(content, null, 2)
317
+ : typeof content === 'string'
318
+ ? content
319
+ : JSON.stringify(content, null, 2)
320
+
321
+ return (
322
+ <div data-slot="tool-ui-section" className="border-border border-t">
323
+ <button
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"
326
+ >
327
+ <span className="text-muted-foreground text-sm">{title}</span>
328
+ <div className="flex items-center gap-1">
329
+ <CopyButton content={contentString} />
330
+ <ChevronRightIcon
331
+ className={cn(
332
+ 'text-muted-foreground size-4 transition-transform duration-200',
333
+ isExpanded && 'rotate-90'
334
+ )}
335
+ />
336
+ </div>
337
+ </button>
338
+ {isExpanded && (
339
+ <div className="border-border border-t">
340
+ {isStructured ? (
341
+ <StructuredResultContent content={content} />
342
+ ) : highlightSyntax ? (
343
+ <SyntaxHighlightedCode text={contentString} language={language} />
344
+ ) : (
345
+ <pre className="text-foreground overflow-x-auto px-4 py-3 text-sm whitespace-pre-wrap">
346
+ {contentString}
347
+ </pre>
348
+ )}
349
+ </div>
350
+ )}
351
+ </div>
352
+ )
353
+ }
354
+
355
+ type ApprovalMode = 'one-time' | 'for-session'
356
+
357
+ /* -----------------------------------------------------------------------------
358
+ * ToolUI - Main component
359
+ * -------------------------------------------------------------------------- */
360
+
361
+ function ToolUI({
362
+ name,
363
+ icon,
364
+ provider,
365
+ status = 'complete',
366
+ request,
367
+ result,
368
+ defaultExpanded = false,
369
+ className,
370
+ onApproveOnce,
371
+ onApproveForSession,
372
+ onDeny,
373
+ }: ToolUIProps) {
374
+ const isApprovalPending =
375
+ status === 'approval' && onApproveOnce !== undefined && onDeny !== undefined
376
+ // Auto-expand when approval is pending, collapse when approved
377
+ const [isExpanded, setIsExpanded] = useState(defaultExpanded)
378
+ const hasContent = request !== undefined || result !== undefined
379
+
380
+ // Track approval mode: 'one-time' or 'for-session'
381
+ const [approvalMode, setApprovalMode] = useState<ApprovalMode>('one-time')
382
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false)
383
+
384
+ // Collapse when transitioning from approval to non-approval (i.e., when approved/denied)
385
+ useEffect(() => {
386
+ if (!isApprovalPending && isExpanded && !defaultExpanded) {
387
+ setIsExpanded(false)
388
+ }
389
+ }, [isApprovalPending])
390
+
391
+ // Handle approve based on selected mode
392
+ const handleApprove = () => {
393
+ if (approvalMode === 'for-session' && onApproveForSession) {
394
+ onApproveForSession()
395
+ } else if (onApproveOnce) {
396
+ onApproveOnce()
397
+ }
398
+ }
399
+
400
+ return (
401
+ <div
402
+ data-slot="tool-ui"
403
+ className={cn(
404
+ 'border-border bg-card overflow-hidden rounded-lg border',
405
+ className
406
+ )}
407
+ >
408
+ {/* Header with provider */}
409
+ {provider && (
410
+ <div
411
+ data-slot="tool-ui-provider"
412
+ className={cn(
413
+ 'border-border flex items-center gap-2 border-b px-4 py-2.5'
414
+ )}
415
+ >
416
+ {icon ? (
417
+ <span className="flex size-5 items-center justify-center">
418
+ {icon}
419
+ </span>
420
+ ) : (
421
+ <span className="bg-muted flex size-5 items-center justify-center rounded text-xs font-medium">
422
+ {provider.charAt(0).toUpperCase()}
423
+ </span>
424
+ )}
425
+ <span className="text-sm font-medium">{provider}</span>
426
+ </div>
427
+ )}
428
+
429
+ {/* Tool row */}
430
+ <button
431
+ onClick={() => hasContent && setIsExpanded(!isExpanded)}
432
+ disabled={!hasContent}
433
+ className={cn(
434
+ 'flex w-full items-center gap-2 px-4 py-3 text-left',
435
+ hasContent && 'hover:bg-accent/50 cursor-pointer transition-colors'
436
+ )}
437
+ >
438
+ <StatusIndicator status={status} />
439
+ <span
440
+ className={cn(
441
+ 'flex-1 text-sm',
442
+ !provider && isApprovalPending && 'shimmer'
443
+ )}
444
+ >
445
+ {name}
446
+ </span>
447
+ {hasContent && (
448
+ <ChevronDownIcon
449
+ className={cn(
450
+ 'text-muted-foreground size-4 transition-transform duration-200',
451
+ isExpanded && 'rotate-180'
452
+ )}
453
+ />
454
+ )}
455
+ </button>
456
+
457
+ {/* Expandable content */}
458
+ {isExpanded && hasContent && (
459
+ <div data-slot="tool-ui-content">
460
+ {/* When not approval pending, use collapsible section */}
461
+ {request !== undefined && (
462
+ <ToolUISection
463
+ title="Arguments"
464
+ content={request}
465
+ highlightSyntax
466
+ language="json"
467
+ />
468
+ )}
469
+ {/* Hide output when approval is pending */}
470
+ {result !== undefined && (
471
+ <ToolUISection
472
+ title="Output"
473
+ content={result}
474
+ highlightSyntax
475
+ language="json"
476
+ />
477
+ )}
478
+ </div>
479
+ )}
480
+
481
+ {/* Approval actions */}
482
+ {isApprovalPending && (
483
+ <div
484
+ data-slot="tool-ui-approval-actions"
485
+ className="border-border flex items-center justify-end gap-2 border-t px-4 py-3"
486
+ >
487
+ <div>
488
+ <span className="text-muted-foreground text-sm">
489
+ This tool requires approval
490
+ </span>
491
+ </div>
492
+ <div className="ml-auto flex items-center gap-2">
493
+ <Button
494
+ variant="outline"
495
+ size="sm"
496
+ onClick={onDeny}
497
+ className="text-destructive hover:bg-destructive/10"
498
+ >
499
+ <XIcon className="mr-1 size-3" />
500
+ Deny
501
+ </Button>
502
+ {/* Split button: main approve + dropdown for options */}
503
+ <div className="flex items-center">
504
+ <Button
505
+ variant="default"
506
+ size="sm"
507
+ onClick={handleApprove}
508
+ className="flex cursor-pointer justify-between gap-1 rounded-r-none bg-emerald-600 hover:bg-emerald-700"
509
+ >
510
+ <CheckIcon className="mr-1 size-3" />
511
+
512
+ {/* The min-width is needed to prevent the button from shifting when the text changes */}
513
+ <span className="min-w-[110px]">
514
+ {approvalMode === 'one-time'
515
+ ? 'Approve this time'
516
+ : 'Approve always'}
517
+ </span>
518
+ </Button>
519
+ <Popover open={isDropdownOpen} onOpenChange={setIsDropdownOpen}>
520
+ <PopoverTrigger asChild>
521
+ <Button
522
+ variant="default"
523
+ size="sm"
524
+ className="cursor-pointer rounded-l-none border-l border-emerald-700 bg-emerald-600 px-2 hover:bg-emerald-700"
525
+ >
526
+ <ChevronDownIcon className="size-3" />
527
+ </Button>
528
+ </PopoverTrigger>
529
+ <PopoverContent align="end" className="w-64 p-1" sideOffset={4}>
530
+ <button
531
+ onClick={() => {
532
+ setApprovalMode('one-time')
533
+ setIsDropdownOpen(false)
534
+ }}
535
+ className="hover:bg-accent relative flex w-full items-start gap-2 rounded-sm px-2 py-2 text-left"
536
+ >
537
+ <CheckIcon
538
+ className={cn(
539
+ 'relative top-1 mt-0.5 size-3 shrink-0',
540
+ approvalMode !== 'one-time' && 'invisible'
541
+ )}
542
+ />
543
+ <div className="flex flex-col gap-0.5">
544
+ <span className="text-sm">Approve only once</span>
545
+ <span className="text-muted-foreground text-xs">
546
+ You'll be asked again next time
547
+ </span>
548
+ </div>
549
+ </button>
550
+ {onApproveForSession && (
551
+ <button
552
+ onClick={() => {
553
+ setApprovalMode('for-session')
554
+ setIsDropdownOpen(false)
555
+ }}
556
+ className="hover:bg-accent relative flex w-full items-start gap-2 rounded-sm px-2 py-2 text-left"
557
+ >
558
+ <CheckIcon
559
+ className={cn(
560
+ 'relative top-1 mt-0.5 size-3 shrink-0',
561
+ approvalMode !== 'for-session' && 'invisible'
562
+ )}
563
+ />
564
+ <div className="flex flex-col gap-0.5">
565
+ <span className="text-sm">Approve always</span>
566
+ <span className="text-muted-foreground text-xs">
567
+ Trust this tool for the session
568
+ </span>
569
+ </div>
570
+ </button>
571
+ )}
572
+ </PopoverContent>
573
+ </Popover>
574
+ </div>
575
+ </div>
576
+ </div>
577
+ )}
578
+ </div>
579
+ )
580
+ }
581
+
582
+ /* -----------------------------------------------------------------------------
583
+ * ToolUIGroup - Container for multiple tool calls
584
+ * -------------------------------------------------------------------------- */
585
+
586
+ interface ToolUIGroupProps {
587
+ /** Title for the group header */
588
+ title: string
589
+ /** Optional icon */
590
+ icon?: React.ReactNode
591
+ /** Overall status of the group */
592
+ status?: 'running' | 'complete'
593
+ /** Whether the group starts expanded */
594
+ defaultExpanded?: boolean
595
+ /** Child tool UI components */
596
+ children: React.ReactNode
597
+ /** Additional class names */
598
+ className?: string
599
+ }
600
+
601
+ function ToolUIGroup({
602
+ title,
603
+ icon,
604
+ status = 'complete',
605
+ defaultExpanded = false,
606
+ children,
607
+ className,
608
+ }: ToolUIGroupProps) {
609
+ const [isExpanded, setIsExpanded] = useState(defaultExpanded)
610
+
611
+ return (
612
+ <div
613
+ data-slot="tool-ui-group"
614
+ className={cn(
615
+ 'border-border bg-card overflow-hidden rounded-lg border',
616
+ className
617
+ )}
618
+ >
619
+ {/* Group header */}
620
+ <button
621
+ onClick={() => setIsExpanded(!isExpanded)}
622
+ className="hover:bg-accent/50 flex w-full items-center gap-2 px-4 py-3 text-left transition-colors"
623
+ >
624
+ {icon || (
625
+ <StatusIndicator
626
+ status={status === 'running' ? 'running' : 'complete'}
627
+ />
628
+ )}
629
+ <span
630
+ className={cn(
631
+ 'flex-1 text-sm font-medium',
632
+ status === 'running' && 'shimmer'
633
+ )}
634
+ >
635
+ {title}
636
+ </span>
637
+ <ChevronDownIcon
638
+ className={cn(
639
+ 'text-muted-foreground size-4 transition-transform duration-200',
640
+ isExpanded && 'rotate-180'
641
+ )}
642
+ />
643
+ </button>
644
+
645
+ {/* Expandable children */}
646
+ {isExpanded && (
647
+ <div
648
+ data-slot="tool-ui-group-content"
649
+ className="border-border border-t"
650
+ >
651
+ {children}
652
+ </div>
653
+ )}
654
+ </div>
655
+ )
656
+ }
657
+
658
+ /* -----------------------------------------------------------------------------
659
+ * Exports
660
+ * -------------------------------------------------------------------------- */
661
+
662
+ export {
663
+ ToolUI,
664
+ ToolUISection,
665
+ ToolUIGroup,
666
+ SyntaxHighlightedCode,
667
+ StatusIndicator,
668
+ CopyButton,
669
+ }
670
+ export type {
671
+ ToolUIProps,
672
+ ToolUISectionProps,
673
+ ToolUIGroupProps,
674
+ ToolStatus,
675
+ ContentItem,
676
+ }