@btst/stack 1.4.0 → 1.4.1

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 (40) hide show
  1. package/dist/node_modules/.pnpm/@radix-ui_react-accordion@1.2.12_@types_react-dom@19.2.3_@types_react@19.2.6__@types_re_947719a27ff11ec6f09710dd9e85efc5/node_modules/@radix-ui/react-accordion/dist/index.cjs +321 -0
  2. package/dist/node_modules/.pnpm/@radix-ui_react-accordion@1.2.12_@types_react-dom@19.2.3_@types_react@19.2.6__@types_re_947719a27ff11ec6f09710dd9e85efc5/node_modules/@radix-ui/react-accordion/dist/index.mjs +306 -0
  3. package/dist/node_modules/.pnpm/@radix-ui_react-collapsible@1.1.12_@types_react-dom@19.2.3_@types_react@19.2.6__@types__d025a77f62ee83ca6bd8b0ea1f9de738/node_modules/@radix-ui/react-collapsible/dist/index.cjs +168 -0
  4. package/dist/node_modules/.pnpm/@radix-ui_react-collapsible@1.1.12_@types_react-dom@19.2.3_@types_react@19.2.6__@types__d025a77f62ee83ca6bd8b0ea1f9de738/node_modules/@radix-ui/react-collapsible/dist/index.mjs +146 -0
  5. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-interface.cjs +29 -3
  6. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-interface.mjs +29 -3
  7. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-layout.cjs +16 -3
  8. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-layout.mjs +16 -3
  9. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-message.cjs +35 -3
  10. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/chat-message.mjs +35 -3
  11. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/tool-call-display.cjs +123 -0
  12. package/dist/packages/better-stack/src/plugins/ai-chat/client/components/tool-call-display.mjs +121 -0
  13. package/dist/packages/ui/src/components/accordion.cjs +67 -0
  14. package/dist/packages/ui/src/components/accordion.mjs +62 -0
  15. package/dist/plugins/ai-chat/client/components/index.cjs +2 -0
  16. package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
  17. package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
  18. package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
  19. package/dist/plugins/ai-chat/client/components/index.mjs +1 -0
  20. package/dist/plugins/ai-chat/client/index.cjs +2 -0
  21. package/dist/plugins/ai-chat/client/index.d.cts +5 -176
  22. package/dist/plugins/ai-chat/client/index.d.mts +5 -176
  23. package/dist/plugins/ai-chat/client/index.d.ts +5 -176
  24. package/dist/plugins/ai-chat/client/index.mjs +1 -0
  25. package/dist/plugins/blog/client/components/shared/markdown-content-styles.css +6 -0
  26. package/dist/shared/stack.DaOcgmrM.d.cts +323 -0
  27. package/dist/shared/stack.DaOcgmrM.d.mts +323 -0
  28. package/dist/shared/stack.DaOcgmrM.d.ts +323 -0
  29. package/package.json +1 -1
  30. package/src/plugins/ai-chat/client/components/chat-interface.tsx +41 -2
  31. package/src/plugins/ai-chat/client/components/chat-layout.tsx +16 -1
  32. package/src/plugins/ai-chat/client/components/chat-message.tsx +59 -3
  33. package/src/plugins/ai-chat/client/components/index.ts +2 -0
  34. package/src/plugins/ai-chat/client/components/tool-call-display.tsx +197 -0
  35. package/src/plugins/ai-chat/client/index.ts +12 -1
  36. package/src/plugins/ai-chat/client/overrides.ts +71 -0
  37. package/src/plugins/blog/client/components/shared/markdown-content-styles.css +6 -0
  38. package/dist/shared/stack.DorMi9CZ.d.cts +0 -80
  39. package/dist/shared/stack.DorMi9CZ.d.mts +0 -80
  40. package/dist/shared/stack.DorMi9CZ.d.ts +0 -80
@@ -0,0 +1,323 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { UIMessage } from 'ai';
3
+ import { ComponentType, FormEvent } from 'react';
4
+
5
+ /**
6
+ * AI Chat plugin localization strings
7
+ */
8
+ interface AiChatLocalization {
9
+ CHAT_PLACEHOLDER: string;
10
+ CHAT_SEND_BUTTON: string;
11
+ CHAT_EMPTY_STATE: string;
12
+ CHAT_LOADING: string;
13
+ CHAT_ERROR: string;
14
+ CHAT_RETRY: string;
15
+ CHAT_GENERIC_ERROR_TITLE: string;
16
+ CHAT_GENERIC_ERROR_MESSAGE: string;
17
+ CHAT_PAGE_NOT_FOUND_TITLE: string;
18
+ CHAT_PAGE_NOT_FOUND_DESCRIPTION: string;
19
+ SIDEBAR_TITLE: string;
20
+ SIDEBAR_NEW_CHAT: string;
21
+ SIDEBAR_NO_CONVERSATIONS: string;
22
+ SIDEBAR_SEARCH_PLACEHOLDER: string;
23
+ CONVERSATION_RENAME: string;
24
+ CONVERSATION_RENAME_PLACEHOLDER: string;
25
+ CONVERSATION_RENAME_SAVE: string;
26
+ CONVERSATION_RENAME_CANCEL: string;
27
+ CONVERSATION_DELETE: string;
28
+ CONVERSATION_DELETE_CONFIRM_TITLE: string;
29
+ CONVERSATION_DELETE_CONFIRM_DESCRIPTION: string;
30
+ CONVERSATION_DELETE_CONFIRM_BUTTON: string;
31
+ CONVERSATION_DELETE_CANCEL: string;
32
+ IMAGE_UPLOAD_BUTTON: string;
33
+ IMAGE_UPLOAD_UPLOADING: string;
34
+ IMAGE_UPLOAD_ERROR_NOT_IMAGE: string;
35
+ IMAGE_UPLOAD_ERROR_TOO_LARGE: string;
36
+ IMAGE_UPLOAD_SUCCESS: string;
37
+ IMAGE_UPLOAD_FAILURE: string;
38
+ FILE_UPLOAD_BUTTON: string;
39
+ FILE_UPLOAD_ERROR_TOO_LARGE: string;
40
+ FILE_UPLOAD_SUCCESS: string;
41
+ FILE_UPLOAD_FAILURE: string;
42
+ TIME_JUST_NOW: string;
43
+ TIME_MINUTES_AGO: string;
44
+ TIME_HOURS_AGO: string;
45
+ TIME_YESTERDAY: string;
46
+ TIME_DAYS_AGO: string;
47
+ MESSAGE_COPY: string;
48
+ MESSAGE_COPIED: string;
49
+ MESSAGE_RETRY: string;
50
+ MESSAGE_EDIT: string;
51
+ MESSAGE_SAVE: string;
52
+ MESSAGE_CANCEL: string;
53
+ A11Y_USER_MESSAGE: string;
54
+ A11Y_ASSISTANT_MESSAGE: string;
55
+ A11Y_COPY_CODE: string;
56
+ A11Y_CODE_COPIED: string;
57
+ }
58
+
59
+ /**
60
+ * Plugin mode for AI Chat
61
+ * - 'authenticated': Conversations persisted with userId (default)
62
+ * - 'public': Stateless chat, no persistence (ideal for public chatbots)
63
+ */
64
+ type AiChatMode = "authenticated" | "public";
65
+ /**
66
+ * State of a tool call execution
67
+ */
68
+ type ToolCallState = "input-streaming" | "input-available" | "output-available" | "output-error";
69
+ /**
70
+ * Props passed to custom tool call renderer components
71
+ */
72
+ interface ToolCallProps<TInput = unknown, TOutput = unknown> {
73
+ /** Unique identifier for this tool call */
74
+ toolCallId: string;
75
+ /** Name of the tool being called */
76
+ toolName: string;
77
+ /** Current state of the tool call execution */
78
+ state: ToolCallState;
79
+ /** Input arguments passed to the tool (may be partial during streaming) */
80
+ input: TInput | undefined;
81
+ /** Output from the tool (only available when state is 'output-available') */
82
+ output: TOutput | undefined;
83
+ /** Error message (only available when state is 'output-error') */
84
+ errorText?: string;
85
+ /** Whether the tool call is currently in progress */
86
+ isLoading: boolean;
87
+ }
88
+ /**
89
+ * A component that renders a custom UI for a specific tool call.
90
+ * Return `null` to fall back to the default tool call accordion.
91
+ */
92
+ type ToolCallRenderer<TInput = unknown, TOutput = unknown> = ComponentType<ToolCallProps<TInput, TOutput>>;
93
+ /**
94
+ * Allowed file type categories for uploads
95
+ */
96
+ type AllowedFileType = "image" | "text" | "pdf" | "markdown" | "csv" | "json";
97
+ /**
98
+ * Default allowed file types (images only for best AI model compatibility)
99
+ * Consumers can expand this by passing allowedFileTypes in overrides
100
+ */
101
+ declare const DEFAULT_ALLOWED_FILE_TYPES: AllowedFileType[];
102
+ /**
103
+ * Context passed to lifecycle hooks
104
+ */
105
+ interface RouteContext {
106
+ /** Current route path */
107
+ path: string;
108
+ /** Route parameters (e.g., { id: "abc123" }) */
109
+ params?: Record<string, string>;
110
+ /** Whether rendering on server (true) or client (false) */
111
+ isSSR: boolean;
112
+ /** Additional context properties */
113
+ [key: string]: any;
114
+ }
115
+ /**
116
+ * Overridable components and functions for the AI Chat plugin
117
+ *
118
+ * External consumers can provide their own implementations of these
119
+ * to customize the behavior for their framework (Next.js, React Router, etc.)
120
+ */
121
+ interface AiChatPluginOverrides {
122
+ /**
123
+ * Plugin mode - should match backend config
124
+ * @default 'authenticated'
125
+ */
126
+ mode?: AiChatMode;
127
+ /**
128
+ * API base URL
129
+ */
130
+ apiBaseURL: string;
131
+ /**
132
+ * API base path
133
+ */
134
+ apiBasePath: string;
135
+ /**
136
+ * Navigation function for programmatic navigation
137
+ */
138
+ navigate: (path: string) => void | Promise<void>;
139
+ /**
140
+ * Refresh function to invalidate server-side cache (e.g., Next.js router.refresh())
141
+ */
142
+ refresh?: () => void | Promise<void>;
143
+ /**
144
+ * Link component for navigation
145
+ */
146
+ Link?: ComponentType<React.ComponentProps<"a"> & Record<string, any>>;
147
+ /**
148
+ * Image component for displaying images
149
+ */
150
+ Image?: ComponentType<React.ImgHTMLAttributes<HTMLImageElement> & Record<string, any>>;
151
+ /**
152
+ * Function used to upload a file and return its URL.
153
+ * Called for images, PDFs, text files, and other supported file types.
154
+ */
155
+ uploadFile?: (file: File) => Promise<string>;
156
+ /**
157
+ * Allowed file types for upload.
158
+ * By default, all types are enabled: image, text, pdf, markdown, csv, json
159
+ * Set to empty array to disable file uploads entirely.
160
+ * @default ['image', 'text', 'pdf', 'markdown', 'csv', 'json']
161
+ */
162
+ allowedFileTypes?: AllowedFileType[];
163
+ /**
164
+ * Localization object for the AI Chat plugin
165
+ */
166
+ localization?: Partial<AiChatLocalization>;
167
+ /**
168
+ * Optional headers to pass with API requests (e.g., for SSR auth)
169
+ */
170
+ headers?: HeadersInit;
171
+ /**
172
+ * Whether to show the attribution
173
+ * @default true
174
+ */
175
+ showAttribution?: boolean;
176
+ /**
177
+ * Suggested prompts to display in the empty chat state.
178
+ * When provided, these appear as clickable buttons that populate the input field.
179
+ *
180
+ * @example
181
+ * ```tsx
182
+ * chatSuggestions: [
183
+ * "What can you help me with?",
184
+ * "Tell me about your features",
185
+ * "How do I get started?",
186
+ * ]
187
+ * ```
188
+ */
189
+ chatSuggestions?: string[];
190
+ /**
191
+ * Custom renderers for tool calls. Keys should match tool names.
192
+ * Each renderer receives ToolCallProps and can return custom UI.
193
+ *
194
+ * @example
195
+ * ```tsx
196
+ * toolRenderers: {
197
+ * getWeather: ({ toolName, input, output, state, isLoading }) => (
198
+ * <WeatherCard location={input?.location} weather={output} loading={isLoading} />
199
+ * ),
200
+ * searchDocs: ({ input, output, isLoading }) => (
201
+ * <SearchResults query={input?.query} results={output} loading={isLoading} />
202
+ * ),
203
+ * }
204
+ * ```
205
+ */
206
+ toolRenderers?: Record<string, ToolCallRenderer>;
207
+ /**
208
+ * Called when a route is rendered
209
+ * @param routeName - Name of the route (e.g., 'chat', 'chatConversation')
210
+ * @param context - Route context with path, params, etc.
211
+ */
212
+ onRouteRender?: (routeName: string, context: RouteContext) => void | Promise<void>;
213
+ /**
214
+ * Called when a route encounters an error
215
+ * @param routeName - Name of the route
216
+ * @param error - The error that occurred
217
+ * @param context - Route context
218
+ */
219
+ onRouteError?: (routeName: string, error: Error, context: RouteContext) => void | Promise<void>;
220
+ /**
221
+ * Called before the chat page is rendered
222
+ * Return false to prevent rendering (e.g., for authorization)
223
+ * @param context - Route context
224
+ */
225
+ onBeforeChatPageRendered?: (context: RouteContext) => boolean;
226
+ /**
227
+ * Called before a conversation page is rendered
228
+ * Return false to prevent rendering (e.g., for authorization)
229
+ * @param id - The conversation ID
230
+ * @param context - Route context
231
+ */
232
+ onBeforeConversationPageRendered?: (id: string, context: RouteContext) => boolean;
233
+ }
234
+
235
+ interface ChatInterfaceProps {
236
+ apiPath?: string;
237
+ initialMessages?: UIMessage[];
238
+ id?: string;
239
+ /** Variant: 'full' for full-page layout, 'widget' for embedded widget */
240
+ variant?: "full" | "widget";
241
+ className?: string;
242
+ /** Called whenever messages change (for persistence). Only fires in public mode. */
243
+ onMessagesChange?: (messages: UIMessage[]) => void;
244
+ }
245
+ declare function ChatInterface({ apiPath, initialMessages, id, variant, className, onMessagesChange, }: ChatInterfaceProps): react_jsx_runtime.JSX.Element;
246
+
247
+ interface ChatLayoutProps {
248
+ /** API base URL */
249
+ apiBaseURL: string;
250
+ /** API base path */
251
+ apiBasePath: string;
252
+ /** Current conversation ID (if viewing existing conversation) */
253
+ conversationId?: string;
254
+ /** Layout mode: 'full' for full page with sidebar, 'widget' for embeddable widget */
255
+ layout?: "full" | "widget";
256
+ /** Additional class name for the container */
257
+ className?: string;
258
+ /** Whether to show the sidebar (default: true for full layout) */
259
+ showSidebar?: boolean;
260
+ /** Height of the widget (only applies to widget layout) */
261
+ widgetHeight?: string | number;
262
+ /** Initial messages to populate the chat (useful for localStorage persistence in public mode) */
263
+ initialMessages?: UIMessage[];
264
+ /** Called whenever messages change (for persistence). Only fires in public mode. */
265
+ onMessagesChange?: (messages: UIMessage[]) => void;
266
+ }
267
+ /**
268
+ * ChatLayout component that provides a full-page chat experience with sidebar
269
+ * or a compact widget mode for embedding.
270
+ */
271
+ declare function ChatLayout({ apiBaseURL, apiBasePath, conversationId, layout, className, showSidebar, widgetHeight, initialMessages, onMessagesChange, }: ChatLayoutProps): react_jsx_runtime.JSX.Element;
272
+
273
+ interface ChatSidebarProps {
274
+ currentConversationId?: string;
275
+ onNewChat?: () => void;
276
+ className?: string;
277
+ }
278
+ declare function ChatSidebar({ currentConversationId, onNewChat, className, }: ChatSidebarProps): react_jsx_runtime.JSX.Element;
279
+
280
+ interface ChatMessageProps {
281
+ message: UIMessage;
282
+ isStreaming?: boolean;
283
+ variant?: "default" | "compact";
284
+ /** Callback when user wants to retry/regenerate an AI response */
285
+ onRetry?: () => void;
286
+ /** Callback when user edits their message - receives the new text */
287
+ onEdit?: (newText: string) => void;
288
+ /** Whether retry is currently in progress */
289
+ isRetrying?: boolean;
290
+ }
291
+ declare function ChatMessage({ message, isStreaming, variant, onRetry, onEdit, isRetrying, }: ChatMessageProps): react_jsx_runtime.JSX.Element;
292
+
293
+ /** Represents an attached file with metadata */
294
+ interface AttachedFile {
295
+ /** Data URL or uploaded URL */
296
+ url: string;
297
+ /** MIME type of the file */
298
+ mediaType: string;
299
+ /** Original filename */
300
+ filename: string;
301
+ }
302
+ interface ChatInputProps {
303
+ input?: string;
304
+ handleInputChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
305
+ handleSubmit: (e: FormEvent<HTMLFormElement>, files?: AttachedFile[]) => void;
306
+ isLoading: boolean;
307
+ placeholder?: string;
308
+ variant?: "default" | "compact";
309
+ /** Callback when files are attached (for controlled mode) */
310
+ onFilesAttached?: (files: AttachedFile[]) => void;
311
+ /** Attached files (for controlled mode) */
312
+ attachedFiles?: AttachedFile[];
313
+ }
314
+ declare function ChatInput({ input, handleInputChange, handleSubmit, isLoading, placeholder, variant, onFilesAttached, attachedFiles: controlledFiles, }: ChatInputProps): react_jsx_runtime.JSX.Element;
315
+
316
+ /**
317
+ * Default tool call display component.
318
+ * Shows an accordion with tool name, status, inputs, and outputs.
319
+ */
320
+ declare function ToolCallDisplay({ toolCallId, toolName, state, input, output, errorText, isLoading, }: ToolCallProps): react_jsx_runtime.JSX.Element;
321
+
322
+ export { ChatInterface as C, DEFAULT_ALLOWED_FILE_TYPES as D, ChatLayout as e, ChatSidebar as g, ChatMessage as h, ChatInput as i, ToolCallDisplay as j };
323
+ export type { AiChatMode as A, ToolCallProps as T, AiChatPluginOverrides as a, AllowedFileType as b, ToolCallState as c, ToolCallRenderer as d, ChatLayoutProps as f };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btst/stack",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "A composable, plugin-based library for building full-stack applications.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,6 +28,8 @@ interface ChatInterfaceProps {
28
28
  /** Variant: 'full' for full-page layout, 'widget' for embedded widget */
29
29
  variant?: "full" | "widget";
30
30
  className?: string;
31
+ /** Called whenever messages change (for persistence). Only fires in public mode. */
32
+ onMessagesChange?: (messages: UIMessage[]) => void;
31
33
  }
32
34
 
33
35
  export function ChatInterface({
@@ -36,6 +38,7 @@ export function ChatInterface({
36
38
  id,
37
39
  variant = "full",
38
40
  className,
41
+ onMessagesChange,
39
42
  }: ChatInterfaceProps) {
40
43
  const {
41
44
  navigate,
@@ -45,6 +48,7 @@ export function ChatInterface({
45
48
  headers,
46
49
  mode,
47
50
  showAttribution,
51
+ chatSuggestions,
48
52
  } = usePluginOverrides<AiChatPluginOverrides, Partial<AiChatPluginOverrides>>(
49
53
  "ai-chat",
50
54
  { showAttribution: true },
@@ -108,6 +112,15 @@ export function ChatInterface({
108
112
  // Ref to track edit operation with messages to use
109
113
  const editMessagesRef = useRef<UIMessage[] | null>(null);
110
114
 
115
+ // Track if we've finished initializing messages
116
+ // This prevents onMessagesChange from firing with an empty array before initialMessages are loaded
117
+ // Without this guard, the effect would fire on mount with [], overwriting any saved messages
118
+ const [isMessagesInitialized, setIsMessagesInitialized] = useState(
119
+ () =>
120
+ // Start as initialized if there are no initialMessages to load
121
+ !initialMessages || initialMessages.length === 0,
122
+ );
123
+
111
124
  // Memoize the transport to prevent recreation on every render
112
125
  const transport = useMemo(
113
126
  () =>
@@ -240,6 +253,8 @@ export function ChatInterface({
240
253
  messages.length === 0
241
254
  ) {
242
255
  setMessages(initialMessages);
256
+ // Mark as initialized - this is batched with setMessages so both take effect in the same render
257
+ setIsMessagesInitialized(true);
243
258
  }
244
259
  }, [initialMessages, setMessages, messages.length]);
245
260
 
@@ -259,6 +274,14 @@ export function ChatInterface({
259
274
  }
260
275
  }, [messages]);
261
276
 
277
+ // Notify parent when messages change (for persistence in public mode)
278
+ // Only fire after initialization to prevent overwriting saved messages with an empty array
279
+ useEffect(() => {
280
+ if (isPublicMode && onMessagesChange && isMessagesInitialized) {
281
+ onMessagesChange(messages);
282
+ }
283
+ }, [messages, isPublicMode, onMessagesChange, isMessagesInitialized]);
284
+
262
285
  const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
263
286
  setInput(e.target.value);
264
287
  };
@@ -382,8 +405,24 @@ export function ChatInterface({
382
405
  )}
383
406
  >
384
407
  {messages.length === 0 ? (
385
- <div className="flex flex-col items-center justify-center h-full min-h-[300px] text-muted-foreground">
386
- <p>{localization.CHAT_EMPTY_STATE}</p>
408
+ <div className="flex flex-col h-full min-h-[300px]">
409
+ <div className="flex-1 flex items-center justify-center text-muted-foreground">
410
+ <p>{localization.CHAT_EMPTY_STATE}</p>
411
+ </div>
412
+ {chatSuggestions && chatSuggestions.length > 0 && (
413
+ <div className="flex flex-wrap justify-center gap-2 pb-4 max-w-md mx-auto">
414
+ {chatSuggestions.map((suggestion, index) => (
415
+ <button
416
+ key={index}
417
+ type="button"
418
+ onClick={() => setInput(suggestion)}
419
+ className="px-3 py-2 text-sm rounded-lg border border-border bg-background hover:bg-accent hover:text-accent-foreground transition-colors text-foreground"
420
+ >
421
+ {suggestion}
422
+ </button>
423
+ ))}
424
+ </div>
425
+ )}
387
426
  </div>
388
427
  ) : (
389
428
  messages.map((m, index) => (
@@ -11,6 +11,7 @@ import { Menu, PanelLeftClose, PanelLeft } from "lucide-react";
11
11
  import { cn } from "@workspace/ui/lib/utils";
12
12
  import { ChatSidebar } from "./chat-sidebar";
13
13
  import { ChatInterface } from "./chat-interface";
14
+ import type { UIMessage } from "ai";
14
15
 
15
16
  export interface ChatLayoutProps {
16
17
  /** API base URL */
@@ -27,6 +28,10 @@ export interface ChatLayoutProps {
27
28
  showSidebar?: boolean;
28
29
  /** Height of the widget (only applies to widget layout) */
29
30
  widgetHeight?: string | number;
31
+ /** Initial messages to populate the chat (useful for localStorage persistence in public mode) */
32
+ initialMessages?: UIMessage[];
33
+ /** Called whenever messages change (for persistence). Only fires in public mode. */
34
+ onMessagesChange?: (messages: UIMessage[]) => void;
30
35
  }
31
36
 
32
37
  /**
@@ -41,6 +46,8 @@ export function ChatLayout({
41
46
  className,
42
47
  showSidebar = true,
43
48
  widgetHeight = "600px",
49
+ initialMessages,
50
+ onMessagesChange,
44
51
  }: ChatLayoutProps) {
45
52
  const [sidebarOpen, setSidebarOpen] = useState(true);
46
53
  const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
@@ -67,7 +74,13 @@ export function ChatLayout({
67
74
  )}
68
75
  style={{ height: widgetHeight }}
69
76
  >
70
- <ChatInterface apiPath={apiPath} id={conversationId} variant="widget" />
77
+ <ChatInterface
78
+ apiPath={apiPath}
79
+ id={conversationId}
80
+ variant="widget"
81
+ initialMessages={initialMessages}
82
+ onMessagesChange={onMessagesChange}
83
+ />
71
84
  </div>
72
85
  );
73
86
  }
@@ -153,6 +166,8 @@ export function ChatLayout({
153
166
  apiPath={apiPath}
154
167
  id={conversationId}
155
168
  variant="full"
169
+ initialMessages={initialMessages}
170
+ onMessagesChange={onMessagesChange}
156
171
  />
157
172
  </div>
158
173
  </div>
@@ -20,6 +20,7 @@ import {
20
20
  X,
21
21
  } from "lucide-react";
22
22
  import type { UIMessage } from "ai";
23
+ import { isToolUIPart, getToolName } from "ai";
23
24
  import {
24
25
  useMemo,
25
26
  useState,
@@ -28,8 +29,13 @@ import {
28
29
  type ComponentType,
29
30
  } from "react";
30
31
  import { usePluginOverrides } from "@btst/stack/context";
31
- import type { AiChatPluginOverrides } from "../overrides";
32
+ import type {
33
+ AiChatPluginOverrides,
34
+ ToolCallProps,
35
+ ToolCallState,
36
+ } from "../overrides";
32
37
  import { AI_CHAT_LOCALIZATION } from "../localization";
38
+ import { ToolCallDisplay } from "./tool-call-display";
33
39
 
34
40
  // Import shared markdown + syntax highlighting styles (same pattern as blog plugin)
35
41
  import "@workspace/ui/markdown-content.css";
@@ -131,6 +137,7 @@ export function ChatMessage({
131
137
  Link,
132
138
  Image,
133
139
  localization: customLocalization,
140
+ toolRenderers,
134
141
  } = usePluginOverrides<AiChatPluginOverrides, Partial<AiChatPluginOverrides>>(
135
142
  "ai-chat",
136
143
  {},
@@ -233,6 +240,14 @@ export function ChatMessage({
233
240
  return [];
234
241
  }, [message.parts]);
235
242
 
243
+ // Extract tool call parts from message (AI SDK v5 format: type is "tool-{toolName}")
244
+ const toolParts = useMemo(() => {
245
+ if (message.parts && Array.isArray(message.parts)) {
246
+ return message.parts.filter((part: any) => isToolUIPart(part));
247
+ }
248
+ return [];
249
+ }, [message.parts]);
250
+
236
251
  // Use remend to complete partial markdown when streaming
237
252
  const displayContent = useMemo(() => {
238
253
  if (!textContent) return "";
@@ -370,7 +385,7 @@ export function ChatMessage({
370
385
  )}
371
386
  </div>
372
387
  ) : (
373
- // Assistant messages: rendered markdown + files + images
388
+ // Assistant messages: rendered markdown + files + images + tool calls
374
389
  <div className="wrap-break-word space-y-2">
375
390
  {/* Any attached files (non-images) */}
376
391
  {fileParts.length > 0 && (
@@ -407,6 +422,45 @@ export function ChatMessage({
407
422
  ))}
408
423
  </div>
409
424
  )}
425
+ {/* Tool calls */}
426
+ {toolParts.length > 0 && (
427
+ <div className="space-y-2">
428
+ {toolParts.map((part: any) => {
429
+ const toolName = getToolName(part);
430
+ const toolCallId = part.toolCallId;
431
+ const state = part.state as ToolCallState;
432
+ const input = part.input;
433
+ const output = part.output;
434
+ const errorText = part.errorText;
435
+ const isLoading =
436
+ state === "input-streaming" ||
437
+ state === "input-available";
438
+
439
+ const toolCallProps: ToolCallProps = {
440
+ toolCallId,
441
+ toolName,
442
+ state,
443
+ input,
444
+ output,
445
+ errorText,
446
+ isLoading,
447
+ };
448
+
449
+ // Check if there's a custom renderer for this tool
450
+ const CustomRenderer = toolRenderers?.[toolName];
451
+ if (CustomRenderer) {
452
+ return (
453
+ <CustomRenderer key={toolCallId} {...toolCallProps} />
454
+ );
455
+ }
456
+
457
+ // Use default tool call display
458
+ return (
459
+ <ToolCallDisplay key={toolCallId} {...toolCallProps} />
460
+ );
461
+ })}
462
+ </div>
463
+ )}
410
464
  {/* Text content */}
411
465
  {displayContent ? (
412
466
  <MarkdownContent
@@ -415,7 +469,9 @@ export function ChatMessage({
415
469
  LinkComponent={Link ?? DefaultLink}
416
470
  ImageComponent={ImageComponent}
417
471
  />
418
- ) : imageParts.length === 0 && fileParts.length === 0 ? (
472
+ ) : imageParts.length === 0 &&
473
+ fileParts.length === 0 &&
474
+ toolParts.length === 0 ? (
419
475
  <span className="text-muted-foreground">...</span>
420
476
  ) : null}
421
477
  </div>
@@ -1,8 +1,10 @@
1
1
  export { ChatInterface } from "./chat-interface";
2
2
  export { ChatLayout } from "./chat-layout";
3
+ export type { ChatLayoutProps } from "./chat-layout";
3
4
  export { ChatSidebar } from "./chat-sidebar";
4
5
  export { ChatMessage } from "./chat-message";
5
6
  export { ChatInput } from "./chat-input";
7
+ export { ToolCallDisplay } from "./tool-call-display";
6
8
 
7
9
  // Page components
8
10
  export { ChatPageComponent } from "./pages/chat-page";