@tulip-systems/ai 0.11.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 (129) hide show
  1. package/LICENSE +662 -0
  2. package/dist/agents/lib/definitions.server.d.mts +11 -0
  3. package/dist/agents/lib/definitions.server.mjs +14 -0
  4. package/dist/agents/lib/delegation.server.d.mts +15 -0
  5. package/dist/agents/lib/delegation.server.mjs +28 -0
  6. package/dist/agents/lib/instructions.server.d.mts +14 -0
  7. package/dist/agents/lib/instructions.server.mjs +26 -0
  8. package/dist/agents/lib/types.d.mts +26 -0
  9. package/dist/agents/server.d.mts +5 -0
  10. package/dist/agents/server.mjs +5 -0
  11. package/dist/chat/client.d.mts +10 -0
  12. package/dist/chat/client.mjs +12 -0
  13. package/dist/chat/components/agent-activity.client.d.mts +14 -0
  14. package/dist/chat/components/agent-activity.client.mjs +26 -0
  15. package/dist/chat/components/assistant-panel.client.d.mts +40 -0
  16. package/dist/chat/components/assistant-panel.client.mjs +53 -0
  17. package/dist/chat/components/chat-composer.client.d.mts +160 -0
  18. package/dist/chat/components/chat-composer.client.mjs +266 -0
  19. package/dist/chat/components/chat-message.client.d.mts +92 -0
  20. package/dist/chat/components/chat-message.client.mjs +121 -0
  21. package/dist/chat/components/chat-thread.client.d.mts +130 -0
  22. package/dist/chat/components/chat-thread.client.mjs +164 -0
  23. package/dist/chat/components/message-part.client.d.mts +23 -0
  24. package/dist/chat/components/message-part.client.mjs +38 -0
  25. package/dist/chat/components/starter-prompts.client.d.mts +25 -0
  26. package/dist/chat/components/starter-prompts.client.mjs +31 -0
  27. package/dist/chat/components/tool-call-card.client.d.mts +29 -0
  28. package/dist/chat/components/tool-call-card.client.mjs +60 -0
  29. package/dist/chat/lib/classes.mjs +7 -0
  30. package/dist/chat/lib/parts.d.mts +10 -0
  31. package/dist/chat/lib/parts.mjs +25 -0
  32. package/dist/chat/lib/stream.server.d.mts +24 -0
  33. package/dist/chat/lib/stream.server.mjs +34 -0
  34. package/dist/chat/lib/types.d.mts +21 -0
  35. package/dist/chat/server.d.mts +3 -0
  36. package/dist/chat/server.mjs +3 -0
  37. package/dist/chat.d.mts +3 -0
  38. package/dist/chat.mjs +3 -0
  39. package/dist/index.d.mts +1 -0
  40. package/dist/index.mjs +1 -0
  41. package/dist/mcp/lib/content.d.mts +66 -0
  42. package/dist/mcp/lib/content.d.mts.map +1 -0
  43. package/dist/mcp/lib/content.mjs +115 -0
  44. package/dist/mcp/lib/content.mjs.map +1 -0
  45. package/dist/mcp/lib/tool-result.server.d.mts +12 -0
  46. package/dist/mcp/lib/tool-result.server.d.mts.map +1 -0
  47. package/dist/mcp/lib/tool-result.server.mjs +10 -0
  48. package/dist/mcp/lib/tool-result.server.mjs.map +1 -0
  49. package/dist/mcp/lib/types.d.mts +27 -0
  50. package/dist/mcp/server.d.mts +3 -0
  51. package/dist/mcp/server.mjs +3 -0
  52. package/dist/mcp/types.d.mts +27 -0
  53. package/dist/mcp/types.d.mts.map +1 -0
  54. package/dist/server/mcp-content.d.mts +44 -0
  55. package/dist/server/mcp-content.d.mts.map +1 -0
  56. package/dist/server/mcp-content.mjs +71 -0
  57. package/dist/server/mcp-content.mjs.map +1 -0
  58. package/dist/server/tools.d.mts +61 -0
  59. package/dist/server/tools.d.mts.map +1 -0
  60. package/dist/server/tools.mjs +47 -0
  61. package/dist/server/tools.mjs.map +1 -0
  62. package/dist/server.d.mts +3 -0
  63. package/dist/server.mjs +4 -0
  64. package/dist/tools/client.d.mts +3 -0
  65. package/dist/tools/client.mjs +5 -0
  66. package/dist/tools/lib/ai-sdk.server.d.mts +12 -0
  67. package/dist/tools/lib/ai-sdk.server.d.mts.map +1 -0
  68. package/dist/tools/lib/ai-sdk.server.mjs +38 -0
  69. package/dist/tools/lib/ai-sdk.server.mjs.map +1 -0
  70. package/dist/tools/lib/define-tool.server.d.mts +9 -0
  71. package/dist/tools/lib/define-tool.server.d.mts.map +1 -0
  72. package/dist/tools/lib/define-tool.server.mjs +13 -0
  73. package/dist/tools/lib/define-tool.server.mjs.map +1 -0
  74. package/dist/tools/lib/definitions.client.d.mts +20 -0
  75. package/dist/tools/lib/definitions.client.mjs +21 -0
  76. package/dist/tools/lib/definitions.d.mts +16 -0
  77. package/dist/tools/lib/definitions.d.mts.map +1 -0
  78. package/dist/tools/lib/definitions.mjs +20 -0
  79. package/dist/tools/lib/definitions.mjs.map +1 -0
  80. package/dist/tools/lib/resolvers.d.mts +41 -0
  81. package/dist/tools/lib/resolvers.d.mts.map +1 -0
  82. package/dist/tools/lib/resolvers.mjs +43 -0
  83. package/dist/tools/lib/resolvers.mjs.map +1 -0
  84. package/dist/tools/lib/types.client.d.mts +13 -0
  85. package/dist/tools/lib/types.d.mts +33 -0
  86. package/dist/tools/resolvers/agent.server.d.mts +27 -0
  87. package/dist/tools/resolvers/agent.server.d.mts.map +1 -0
  88. package/dist/tools/resolvers/agent.server.mjs +44 -0
  89. package/dist/tools/resolvers/agent.server.mjs.map +1 -0
  90. package/dist/tools/resolvers/mcp.server.d.mts +31 -0
  91. package/dist/tools/resolvers/mcp.server.d.mts.map +1 -0
  92. package/dist/tools/resolvers/mcp.server.mjs +20 -0
  93. package/dist/tools/resolvers/mcp.server.mjs.map +1 -0
  94. package/dist/tools/server.d.mts +5 -0
  95. package/dist/tools/server.mjs +5 -0
  96. package/dist/tools/types.d.mts +33 -0
  97. package/dist/tools/types.d.mts.map +1 -0
  98. package/package.json +61 -0
  99. package/src/agents/entry.server.ts +7 -0
  100. package/src/agents/lib/definitions.server.ts +14 -0
  101. package/src/agents/lib/delegation.server.ts +33 -0
  102. package/src/agents/lib/instructions.server.ts +37 -0
  103. package/src/agents/lib/types.ts +32 -0
  104. package/src/chat/components/agent-activity.client.tsx +21 -0
  105. package/src/chat/components/chat-composer.client.tsx +379 -0
  106. package/src/chat/components/chat-message.client.tsx +166 -0
  107. package/src/chat/components/chat-thread.client.tsx +242 -0
  108. package/src/chat/components/message-part.client.tsx +59 -0
  109. package/src/chat/components/starter-prompts.client.tsx +49 -0
  110. package/src/chat/components/tool-call-card.client.tsx +78 -0
  111. package/src/chat/entry.client.ts +18 -0
  112. package/src/chat/entry.server.ts +5 -0
  113. package/src/chat/entry.ts +5 -0
  114. package/src/chat/lib/parts.ts +33 -0
  115. package/src/chat/lib/stream.server.ts +40 -0
  116. package/src/chat/lib/types.ts +20 -0
  117. package/src/entry.ts +24 -0
  118. package/src/mcp/entry.server.ts +2 -0
  119. package/src/mcp/lib/content.ts +131 -0
  120. package/src/mcp/lib/types.ts +28 -0
  121. package/src/styles.css +1 -0
  122. package/src/tools/entry.client.ts +7 -0
  123. package/src/tools/entry.server.ts +11 -0
  124. package/src/tools/lib/definitions.client.ts +21 -0
  125. package/src/tools/lib/definitions.ts +24 -0
  126. package/src/tools/lib/types.client.ts +11 -0
  127. package/src/tools/lib/types.ts +44 -0
  128. package/src/tools/resolvers/agent.server.ts +78 -0
  129. package/src/tools/resolvers/mcp.server.ts +40 -0
@@ -0,0 +1,379 @@
1
+ "use client";
2
+
3
+ import { Button, Textarea } from "@tulip-systems/core/components";
4
+ import { cn } from "@tulip-systems/core/lib";
5
+ import type { ChatStatus } from "ai";
6
+ import { ArrowUp, UploadIcon } from "lucide-react";
7
+ import {
8
+ type ComponentProps,
9
+ createContext,
10
+ type ReactNode,
11
+ useCallback,
12
+ useContext,
13
+ useEffect,
14
+ useMemo,
15
+ useRef,
16
+ } from "react";
17
+
18
+ /**
19
+ * Chat Composer Context
20
+ */
21
+ type ChatComposerContextValue = {
22
+ value: string;
23
+ onValueChange: (value: string) => void;
24
+ submit: () => void;
25
+ textareaRef: React.RefObject<HTMLTextAreaElement | null>;
26
+ disabled: boolean;
27
+ canSubmit: boolean;
28
+ status: ChatStatus;
29
+ statusLabel?: ReactNode;
30
+ };
31
+
32
+ const ChatComposerContext = createContext<ChatComposerContextValue>({} as ChatComposerContextValue);
33
+
34
+ export type ChatComposerProps = Omit<ComponentProps<"form">, "onSubmit"> & {
35
+ value: string;
36
+ onValueChange: (value: string) => void;
37
+ onSubmit: (value: string) => void;
38
+ disabled?: boolean;
39
+ status?: ChatStatus;
40
+ statusLabel?: ReactNode;
41
+ };
42
+
43
+ /**
44
+ * Provides the root composer shell and shared state for all chat composer slots.
45
+ * Use it as the controlled boundary for the current draft value and submit handler.
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * <ChatComposer value={input} onValueChange={setInput} onSubmit={sendMessage}>
50
+ * <ChatComposerInput placeholder="Ask something..." />
51
+ * <ChatComposerToolbar>
52
+ * <ChatComposerActions>
53
+ * <ChatComposerAttachmentButton />
54
+ * </ChatComposerActions>
55
+ * <ChatComposerSubmit />
56
+ * </ChatComposerToolbar>
57
+ * </ChatComposer>
58
+ * ```
59
+ */
60
+ export function ChatComposer({
61
+ value,
62
+ onValueChange,
63
+ onSubmit,
64
+ disabled = false,
65
+ status = "ready",
66
+ statusLabel,
67
+ className,
68
+ children,
69
+ ...props
70
+ }: ChatComposerProps) {
71
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
72
+ const isDisabled = disabled;
73
+ const isSubmitDisabled = disabled || status !== "ready";
74
+ const canSubmit = !isSubmitDisabled && value.trim().length > 0;
75
+
76
+ useEffect(() => {
77
+ adjustTextareaHeight(textareaRef.current);
78
+ });
79
+
80
+ const submit = useCallback(() => {
81
+ const trimmedValue = value.trim();
82
+
83
+ if (!trimmedValue || isSubmitDisabled) {
84
+ return;
85
+ }
86
+
87
+ onSubmit(trimmedValue);
88
+ resetTextareaHeight(textareaRef.current);
89
+ }, [value, isSubmitDisabled, onSubmit]);
90
+
91
+ const contextValue = useMemo(
92
+ () => ({
93
+ value,
94
+ onValueChange,
95
+ submit,
96
+ textareaRef,
97
+ disabled,
98
+ canSubmit,
99
+ status,
100
+ statusLabel,
101
+ }),
102
+ [value, onValueChange, submit, disabled, canSubmit, status, statusLabel],
103
+ );
104
+
105
+ return (
106
+ <ChatComposerContext.Provider value={contextValue}>
107
+ <form className={cn("pointer-events-auto mx-auto w-full max-w-3xl", className)} {...props}>
108
+ <fieldset
109
+ data-disabled={isDisabled ? true : undefined}
110
+ className="relative flex min-h-28 w-full min-w-0 flex-col rounded-xl border border-input/70 bg-background/80 shadow-[0_12px_40px_rgba(0,0,0,0.08)] outline-none backdrop-blur-xl transition-colors focus-within:border-primary/35 focus-within:ring-3 focus-within:ring-ring/50 data-[disabled=true]:opacity-70"
111
+ >
112
+ {children}
113
+ </fieldset>
114
+ </form>
115
+ </ChatComposerContext.Provider>
116
+ );
117
+ }
118
+
119
+ /**
120
+ * Renders the auto-growing textarea connected to the nearest `ChatComposer`.
121
+ * Pressing Enter submits the composer, while Shift+Enter inserts a new line.
122
+ *
123
+ * @example
124
+ * ```tsx
125
+ * <ChatComposerInput placeholder="Ask something..." rows={3} />
126
+ * ```
127
+ */
128
+ export function ChatComposerInput({
129
+ rows = 3,
130
+ className,
131
+ onKeyDown,
132
+ ...props
133
+ }: Omit<ComponentProps<typeof Textarea>, "value" | "onChange" | "disabled">) {
134
+ const { value, onValueChange, submit, textareaRef, disabled } = useContext(ChatComposerContext);
135
+
136
+ return (
137
+ <Textarea
138
+ ref={textareaRef}
139
+ value={value}
140
+ onChange={(event) => {
141
+ onValueChange(event.target.value);
142
+ adjustTextareaHeight(event.target);
143
+ }}
144
+ onKeyDown={(event) => {
145
+ onKeyDown?.(event);
146
+
147
+ if (event.defaultPrevented) {
148
+ return;
149
+ }
150
+
151
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
152
+ event.preventDefault();
153
+ submit();
154
+ }
155
+ }}
156
+ disabled={disabled}
157
+ rows={rows}
158
+ className={cn(
159
+ "max-h-[calc(75dvh)] min-h-20 flex-1 resize-none overflow-hidden rounded-none border-0 bg-transparent px-3 pt-3 pb-0 text-base leading-6 shadow-none ring-0 placeholder:text-muted-foreground/70 focus-visible:ring-0 disabled:bg-transparent dark:bg-transparent dark:disabled:bg-transparent",
160
+ className,
161
+ )}
162
+ {...props}
163
+ />
164
+ );
165
+ }
166
+
167
+ /**
168
+ * Renders the bottom toolbar area for composer actions and the submit button.
169
+ * Place `ChatComposerActions` on the left and `ChatComposerSubmit` on the right.
170
+ *
171
+ * @example
172
+ * ```tsx
173
+ * <ChatComposerToolbar>
174
+ * <ChatComposerActions>
175
+ * <ChatComposerAttachmentButton />
176
+ * </ChatComposerActions>
177
+ * <ChatComposerSubmit />
178
+ * </ChatComposerToolbar>
179
+ * ```
180
+ */
181
+ export function ChatComposerToolbar({ className, ...props }: ComponentProps<"div">) {
182
+ return (
183
+ <div
184
+ className={cn(
185
+ "flex w-full items-center justify-between gap-2 px-2.5 pt-1 pb-2 text-muted-foreground text-sm",
186
+ className,
187
+ )}
188
+ {...props}
189
+ />
190
+ );
191
+ }
192
+
193
+ /**
194
+ * Groups secondary composer controls such as attachments, model selection, or status.
195
+ * It keeps action controls aligned together inside `ChatComposerToolbar`.
196
+ *
197
+ * @example
198
+ * ```tsx
199
+ * <ChatComposerActions>
200
+ * <ChatComposerAttachmentButton />
201
+ * <ChatComposerModelSelect>GPT-5.5</ChatComposerModelSelect>
202
+ * <ChatComposerStatus />
203
+ * </ChatComposerActions>
204
+ * ```
205
+ */
206
+ export function ChatComposerActions({ className, ...props }: ComponentProps<"div">) {
207
+ return <div className={cn("flex min-w-0 items-center gap-2", className)} {...props} />;
208
+ }
209
+
210
+ /**
211
+ * Renders an attachment action button for opening a file picker or context picker.
212
+ * It uses a file-upload icon and `Context toevoegen` accessible label by default.
213
+ *
214
+ * @example
215
+ * ```tsx
216
+ * <ChatComposerAttachmentButton onClick={openFilePicker} />
217
+ * ```
218
+ */
219
+ export function ChatComposerAttachmentButton({
220
+ className,
221
+ disabled,
222
+ ...props
223
+ }: ComponentProps<typeof Button>) {
224
+ const { disabled: composerDisabled } = useContext(ChatComposerContext);
225
+
226
+ return (
227
+ <Button
228
+ aria-label="Bestanden toevoegen"
229
+ type="button"
230
+ size="icon"
231
+ variant="outline"
232
+ disabled={composerDisabled || disabled}
233
+ className={cn("size-8 rounded-lg p-0 shadow-none", className)}
234
+ {...props}
235
+ >
236
+ <UploadIcon className="size-4" />
237
+ <span className="sr-only">Bestanden toevoegen</span>
238
+ </Button>
239
+ );
240
+ }
241
+
242
+ /**
243
+ * Renders a compact model selector trigger inside the composer toolbar.
244
+ * Use it as a simple button or compose it with a popover/select in application code.
245
+ *
246
+ * @example
247
+ * ```tsx
248
+ * <ChatComposerModelSelect onClick={openModelMenu}>GPT-5.5</ChatComposerModelSelect>
249
+ * ```
250
+ */
251
+ export function ChatComposerModelSelect({
252
+ className,
253
+ disabled,
254
+ children,
255
+ ...props
256
+ }: ComponentProps<typeof Button>) {
257
+ const { disabled: composerDisabled } = useContext(ChatComposerContext);
258
+
259
+ return (
260
+ <Button
261
+ type="button"
262
+ variant="outline"
263
+ size="sm"
264
+ disabled={composerDisabled || disabled}
265
+ className={cn("h-8 rounded-lg shadow-none", className)}
266
+ {...props}
267
+ >
268
+ Select model
269
+ </Button>
270
+ );
271
+ }
272
+
273
+ /**
274
+ * Displays the current composer status from `ChatComposer.status` and `statusLabel`.
275
+ * It renders nothing when no label or children are provided.
276
+ *
277
+ * @example
278
+ * ```tsx
279
+ * <ChatComposer status="streaming" statusLabel="Thinking...">
280
+ * <ChatComposerInput />
281
+ * <ChatComposerToolbar>
282
+ * <ChatComposerActions>
283
+ * <ChatComposerStatus />
284
+ * </ChatComposerActions>
285
+ * <ChatComposerSubmit />
286
+ * </ChatComposerToolbar>
287
+ * </ChatComposer>
288
+ * ```
289
+ */
290
+ export function ChatComposerStatus({ className, children, ...props }: ComponentProps<"div">) {
291
+ const { status, statusLabel } = useContext(ChatComposerContext);
292
+ const content = children ?? statusLabel;
293
+
294
+ if (!content) return null;
295
+
296
+ return (
297
+ <div
298
+ className={cn(
299
+ "line-clamp-1 flex items-center gap-2 text-muted-foreground text-xs opacity-70 data-[status=ready]:opacity-100",
300
+ className,
301
+ )}
302
+ data-status={status}
303
+ {...props}
304
+ >
305
+ <span
306
+ className={cn(
307
+ "size-2 rounded-full",
308
+ status === "ready"
309
+ ? "bg-emerald-500"
310
+ : status === "error"
311
+ ? "bg-red-500"
312
+ : "bg-amber-500",
313
+ )}
314
+ />
315
+ <span>{content}</span>
316
+ </div>
317
+ );
318
+ }
319
+
320
+ /**
321
+ * Renders the primary submit button for the current composer draft.
322
+ * It uses an arrow-up icon by default and disables itself while the composer cannot submit.
323
+ *
324
+ * @example
325
+ * ```tsx
326
+ * <ChatComposerSubmit />
327
+ * ```
328
+ */
329
+ export function ChatComposerSubmit({
330
+ className,
331
+ disabled,
332
+ onClick,
333
+ ...props
334
+ }: ComponentProps<typeof Button>) {
335
+ const { canSubmit, submit, status } = useContext(ChatComposerContext);
336
+
337
+ return (
338
+ <Button
339
+ type="button"
340
+ variant="default"
341
+ size="icon"
342
+ disabled={!canSubmit || disabled}
343
+ className={cn("size-8 rounded-full p-0 shadow-none", className)}
344
+ aria-label="Send message"
345
+ onClick={(event) => {
346
+ onClick?.(event);
347
+
348
+ if (!event.defaultPrevented) {
349
+ submit();
350
+ }
351
+ }}
352
+ {...props}
353
+ >
354
+ <ArrowUp className="size-4" />
355
+ <span className="sr-only">{status !== "ready" ? "Wait for response" : "Send message"}</span>
356
+ </Button>
357
+ );
358
+ }
359
+
360
+ /**
361
+ * Helpers
362
+ */
363
+ function adjustTextareaHeight(textarea: HTMLTextAreaElement | null) {
364
+ if (!textarea) {
365
+ return;
366
+ }
367
+
368
+ textarea.style.height = "auto";
369
+ textarea.style.height = `${textarea.scrollHeight + 2}px`;
370
+ }
371
+
372
+ function resetTextareaHeight(textarea: HTMLTextAreaElement | null) {
373
+ if (!textarea) {
374
+ return;
375
+ }
376
+
377
+ textarea.style.height = "auto";
378
+ textarea.style.height = "8rem";
379
+ }
@@ -0,0 +1,166 @@
1
+ "use client";
2
+
3
+ import {
4
+ Message,
5
+ MessageContent as MessageContentPrimitive,
6
+ MessageFooter as MessageFooterPrimitive,
7
+ MessageHeader as MessageHeaderPrimitive,
8
+ } from "@tulip-systems/core/components";
9
+ import { cn } from "@tulip-systems/core/lib";
10
+ import type { UIMessage } from "ai";
11
+ import { type ComponentProps, createContext, useContext, useMemo } from "react";
12
+ import type { ToolClientDefinition } from "../../tools/lib/types.client";
13
+ import { getChatPartKey, isToolPart } from "../lib/parts";
14
+ import type { ChatPart } from "../lib/types";
15
+ import { ChatMessagePart } from "./message-part.client";
16
+
17
+ /**
18
+ * Chat Message Context
19
+ */
20
+ type ChatMessageContextValue = {
21
+ message: UIMessage;
22
+ isUser: boolean;
23
+ visibleParts: ChatPart[];
24
+ };
25
+
26
+ const ChatMessageContext = createContext<ChatMessageContextValue>({} as ChatMessageContextValue);
27
+
28
+ export type ChatMessageProps = ComponentProps<typeof Message> & {
29
+ message: UIMessage;
30
+ };
31
+
32
+ /**
33
+ * Provides the root message container and message context for all chat message slots.
34
+ * Use it with `ChatMessageContent` and `ChatMessageParts` for the default message layout.
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * <ChatMessage message={message}>
39
+ * <ChatMessageContent>
40
+ * <ChatMessageParts toolRenderers={toolRenderers} />
41
+ * </ChatMessageContent>
42
+ * </ChatMessage>
43
+ * ```
44
+ */
45
+ export function ChatMessage({ message, children, ...props }: ChatMessageProps) {
46
+ const isUser = message.role === "user";
47
+ const contextValue = useMemo(() => {
48
+ const visibleParts = message.parts.filter((part) => part.type === "text" || isToolPart(part));
49
+
50
+ return {
51
+ message,
52
+ isUser,
53
+ visibleParts,
54
+ };
55
+ }, [message, isUser]);
56
+
57
+ if (contextValue.visibleParts.length === 0) return null;
58
+
59
+ return (
60
+ <ChatMessageContext.Provider value={contextValue}>
61
+ <Message align={isUser ? "end" : "start"} {...props}>
62
+ {children}
63
+ </Message>
64
+ </ChatMessageContext.Provider>
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Renders the content column for a chat message.
70
+ * Place `ChatMessageHeader`, `ChatMessageParts`, and `ChatMessageFooter` inside it.
71
+ *
72
+ * @example
73
+ * ```tsx
74
+ * <ChatMessageContent>
75
+ * <ChatMessageParts toolRenderers={toolRenderers} />
76
+ * </ChatMessageContent>
77
+ * ```
78
+ */
79
+ export function ChatMessageContent({
80
+ className,
81
+ ...props
82
+ }: ComponentProps<typeof MessageContentPrimitive>) {
83
+ const { isUser } = useContext(ChatMessageContext);
84
+
85
+ return (
86
+ <MessageContentPrimitive
87
+ className={cn(isUser && "ml-auto w-fit max-w-[82%] md:max-w-[68%]", className)}
88
+ {...props}
89
+ />
90
+ );
91
+ }
92
+
93
+ /**
94
+ * Renders a message header slot.
95
+ * If no children are passed, it renders nothing.
96
+ *
97
+ * @example
98
+ * ```tsx
99
+ * <ChatMessageHeader>Assistant</ChatMessageHeader>
100
+ * ```
101
+ */
102
+ export function ChatMessageHeader({ className, children, ...props }: ComponentProps<"div">) {
103
+ const { isUser } = useContext(ChatMessageContext);
104
+
105
+ if (!children) return null;
106
+
107
+ return (
108
+ <MessageHeaderPrimitive className={cn("px-1", isUser && "justify-end", className)} {...props}>
109
+ {children}
110
+ </MessageHeaderPrimitive>
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Renders the visible text and tool parts for the current chat message.
116
+ * Pass `toolRenderers` to customize how known tool calls/results are displayed.
117
+ *
118
+ * @example
119
+ * ```tsx
120
+ * <ChatMessageParts toolRenderers={toolRenderers} />
121
+ * ```
122
+ */
123
+ export function ChatMessageParts({
124
+ toolRenderers,
125
+ className,
126
+ parts,
127
+ ...props
128
+ }: ComponentProps<"div"> & {
129
+ toolRenderers?: ToolClientDefinition[];
130
+ parts?: ChatPart[];
131
+ }) {
132
+ const { message, isUser, visibleParts } = useContext(ChatMessageContext);
133
+ const resolvedParts = parts ?? visibleParts;
134
+
135
+ return (
136
+ <div className={cn("flex max-w-full flex-col gap-2.5", className)} {...props}>
137
+ {resolvedParts.map((part) => (
138
+ <ChatMessagePart
139
+ key={getChatPartKey(message.id, part)}
140
+ part={part}
141
+ isUser={isUser}
142
+ toolRenderers={toolRenderers}
143
+ />
144
+ ))}
145
+ </div>
146
+ );
147
+ }
148
+
149
+ /**
150
+ * Renders a message footer slot.
151
+ * If no children are passed, it renders nothing.
152
+ *
153
+ * @example
154
+ * ```tsx
155
+ * <ChatMessageFooter>Copied</ChatMessageFooter>
156
+ * ```
157
+ */
158
+ export function ChatMessageFooter({ className, children, ...props }: ComponentProps<"div">) {
159
+ if (!children) return null;
160
+
161
+ return (
162
+ <MessageFooterPrimitive className={cn("px-1", className)} {...props}>
163
+ {children}
164
+ </MessageFooterPrimitive>
165
+ );
166
+ }