@timbal-ai/timbal-react 0.2.0 → 0.2.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @timbal-ai/timbal-react
2
2
 
3
- React components and runtime for building Timbal chat UIs. Provides a streaming chat interface that connects to Timbal workforce agents out of the box.
3
+ React components and runtime for building Timbal chat UIs. Drop in a single component to get a fully-featured streaming chat interface connected to a Timbal workforce agent.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,15 +10,15 @@ npm install @timbal-ai/timbal-react
10
10
  bun add @timbal-ai/timbal-react
11
11
  ```
12
12
 
13
- ### Peer dependencies
13
+ **Peer dependencies:**
14
14
 
15
15
  ```bash
16
16
  npm install react react-dom @assistant-ui/react @timbal-ai/timbal-sdk
17
17
  ```
18
18
 
19
- ### Required: Tailwind setup
19
+ ### Tailwind setup
20
20
 
21
- The package ships pre-built class names that Tailwind must scan. Add this `@source` line to your CSS entry file — **without it the components will be unstyled**:
21
+ The package ships pre-built Tailwind class names. Add this `@source` line to your CSS entry file — **without it the components will be unstyled**:
22
22
 
23
23
  ```css
24
24
  /* src/index.css */
@@ -29,7 +29,7 @@ The package ships pre-built class names that Tailwind must scan. Add this `@sour
29
29
 
30
30
  > Adjust the path if your CSS file lives at a different depth relative to `node_modules`.
31
31
 
32
- ### Required: CSS imports
32
+ ### CSS imports
33
33
 
34
34
  Import these stylesheets once in your app entry:
35
35
 
@@ -41,11 +41,11 @@ import "katex/dist/katex.min.css";
41
41
 
42
42
  ---
43
43
 
44
- ## Usage
44
+ ## Quick start
45
45
 
46
- ### One-liner
46
+ ### Basic usage
47
47
 
48
- The simplest way to embed a chat UI. `TimbalChat` handles the runtime and the thread in one component:
48
+ `TimbalChat` is a single component that handles everything runtime, streaming, messages, and the composer:
49
49
 
50
50
  ```tsx
51
51
  import { TimbalChat } from "@timbal-ai/timbal-react";
@@ -59,7 +59,9 @@ export default function App() {
59
59
  }
60
60
  ```
61
61
 
62
- #### With welcome screen and suggestions
62
+ > `TimbalChat` requires a fixed height parent. Use `height: "100vh"` or `flex-1 min-h-0` depending on your layout.
63
+
64
+ ### Welcome screen and suggestions
63
65
 
64
66
  ```tsx
65
67
  <TimbalChat
@@ -76,7 +78,7 @@ export default function App() {
76
78
  />
77
79
  ```
78
80
 
79
- #### With a custom placeholder and width
81
+ ### Placeholder and width
80
82
 
81
83
  ```tsx
82
84
  <TimbalChat
@@ -87,9 +89,9 @@ export default function App() {
87
89
  />
88
90
  ```
89
91
 
90
- #### Switching agents dynamically
92
+ ### Switching agents dynamically
91
93
 
92
- Use `key` to reset the chat when the workforce changes:
94
+ Pass `key` to fully reset the chat when the workforce changes:
93
95
 
94
96
  ```tsx
95
97
  const [workforceId, setWorkforceId] = useState("agent-a");
@@ -104,9 +106,9 @@ const [workforceId, setWorkforceId] = useState("agent-a");
104
106
 
105
107
  ---
106
108
 
107
- ### Compose manually
109
+ ## Splitting the runtime and UI
108
110
 
109
- Use `TimbalRuntimeProvider` + `Thread` separately when you need to place the runtime above the chat UI — for example, to build a custom header that reads or controls chat state:
111
+ `TimbalChat` is a convenience wrapper around `TimbalRuntimeProvider` + `Thread`. Use them separately when you need to place the runtime above the chat — for example, to build a custom header that reads or controls chat state:
110
112
 
111
113
  ```tsx
112
114
  import { TimbalRuntimeProvider, Thread } from "@timbal-ai/timbal-react";
@@ -126,7 +128,7 @@ export default function App() {
126
128
  }
127
129
  ```
128
130
 
129
- #### With a custom API base URL
131
+ ### Custom API base URL
130
132
 
131
133
  Useful when your API is mounted at a subpath (e.g. behind a reverse proxy):
132
134
 
@@ -136,20 +138,15 @@ Useful when your API is mounted at a subpath (e.g. behind a reverse proxy):
136
138
  </TimbalRuntimeProvider>
137
139
  ```
138
140
 
139
- #### With a custom fetch function
141
+ ### Custom fetch function
140
142
 
141
143
  Pass your own `fetch` to add headers, inject tokens, or proxy requests:
142
144
 
143
145
  ```tsx
144
- import { TimbalRuntimeProvider, Thread } from "@timbal-ai/timbal-react";
145
-
146
146
  const myFetch: typeof fetch = (url, options) => {
147
147
  return fetch(url, {
148
148
  ...options,
149
- headers: {
150
- ...options?.headers,
151
- "X-My-Header": "value",
152
- },
149
+ headers: { ...options?.headers, "X-My-Header": "value" },
153
150
  });
154
151
  };
155
152
 
@@ -160,30 +157,168 @@ const myFetch: typeof fetch = (url, options) => {
160
157
 
161
158
  ---
162
159
 
163
- ### `TimbalChat` / `Thread` props
160
+ ## Customizing the UI
161
+
162
+ Use the `components` prop on `TimbalChat` or `Thread` to replace any part of the interface while keeping everything else as the default.
163
+
164
+ ### Available slots
165
+
166
+ | Slot | Props forwarded | Default |
167
+ |---|---|---|
168
+ | `UserMessage` | none | built-in user bubble |
169
+ | `AssistantMessage` | none | built-in assistant bubble |
170
+ | `EditComposer` | none | built-in inline edit composer |
171
+ | `Composer` | `placeholder` | built-in composer bar |
172
+ | `Welcome` | `config`, `suggestions` | built-in welcome screen |
173
+ | `ScrollToBottom` | none | built-in scroll button |
174
+
175
+ Custom slot components read their data via hooks — no props are passed automatically except where noted above.
176
+
177
+ ### Custom user message
178
+
179
+ ```tsx
180
+ import { TimbalChat, MessagePrimitive } from "@timbal-ai/timbal-react";
181
+
182
+ const CompactUserMessage = () => (
183
+ <MessagePrimitive.Root className="flex justify-end px-4 py-2">
184
+ <div className="bg-primary text-primary-foreground rounded-2xl px-4 py-2 text-sm max-w-[75%]">
185
+ <MessagePrimitive.Parts />
186
+ </div>
187
+ </MessagePrimitive.Root>
188
+ );
189
+
190
+ <TimbalChat workforceId="..." components={{ UserMessage: CompactUserMessage }} />
191
+ ```
192
+
193
+ ### Custom composer
194
+
195
+ The `Composer` slot receives `placeholder` from the `composerPlaceholder` prop:
196
+
197
+ ```tsx
198
+ import { TimbalChat, ComposerPrimitive } from "@timbal-ai/timbal-react";
199
+
200
+ const MinimalComposer = ({ placeholder }: { placeholder?: string }) => (
201
+ <ComposerPrimitive.Root className="flex items-center gap-2 border rounded-full px-4 py-2">
202
+ <ComposerPrimitive.Input
203
+ placeholder={placeholder ?? "Type here..."}
204
+ className="flex-1 bg-transparent text-sm outline-none"
205
+ rows={1}
206
+ />
207
+ <ComposerPrimitive.Send className="text-primary font-medium text-sm">
208
+ Send
209
+ </ComposerPrimitive.Send>
210
+ </ComposerPrimitive.Root>
211
+ );
212
+
213
+ <TimbalChat workforceId="..." components={{ Composer: MinimalComposer }} />
214
+ ```
215
+
216
+ ### Custom welcome screen
217
+
218
+ The `Welcome` slot is always mounted and controls its own visibility. Use `useThread` to replicate the default "show only when the thread is empty" behaviour:
219
+
220
+ ```tsx
221
+ import { TimbalChat, useThread, useThreadRuntime, type ThreadWelcomeProps } from "@timbal-ai/timbal-react";
222
+
223
+ const BrandedWelcome = ({ suggestions }: ThreadWelcomeProps) => {
224
+ const isEmpty = useThread((s) => s.isEmpty);
225
+ const runtime = useThreadRuntime();
226
+ if (!isEmpty) return null;
227
+ return (
228
+ <div className="flex flex-col items-center justify-center h-full gap-4">
229
+ <img src="/logo.svg" className="h-12" />
230
+ <h2 className="text-xl font-semibold">Welcome to Acme AI</h2>
231
+ <div className="flex gap-2 flex-wrap justify-center">
232
+ {suggestions?.map((s) => (
233
+ <button
234
+ key={s.title}
235
+ onClick={() => runtime.append({ role: "user", content: [{ type: "text", text: s.title }] })}
236
+ className="border rounded-full px-4 py-1.5 text-sm hover:bg-muted"
237
+ >
238
+ {s.title}
239
+ </button>
240
+ ))}
241
+ </div>
242
+ </div>
243
+ );
244
+ };
245
+
246
+ <TimbalChat
247
+ workforceId="..."
248
+ suggestions={[{ title: "Get started" }, { title: "Show me an example" }]}
249
+ components={{ Welcome: BrandedWelcome }}
250
+ />
251
+ ```
252
+
253
+ ### Mixing slots
254
+
255
+ Override any combination — slots are independent of each other:
256
+
257
+ ```tsx
258
+ <TimbalChat
259
+ workforceId="..."
260
+ components={{
261
+ UserMessage: CompactUserMessage,
262
+ Composer: MinimalComposer,
263
+ }}
264
+ />
265
+ ```
266
+
267
+ ### Hooks and primitives
268
+
269
+ These are re-exported from `@assistant-ui/react` for use inside custom slot components:
270
+
271
+ | Export | Use inside |
272
+ |---|---|
273
+ | `ThreadPrimitive` | Any slot |
274
+ | `MessagePrimitive` | `UserMessage`, `AssistantMessage`, `EditComposer` |
275
+ | `ComposerPrimitive` | `Composer`, `EditComposer` |
276
+ | `ActionBarPrimitive` | `UserMessage`, `AssistantMessage` |
277
+ | `useThread` | Any slot — subscribe to thread state (e.g. `isRunning`, `isEmpty`) |
278
+ | `useThreadRuntime` | Any slot — call actions (e.g. `runtime.append(...)`) |
279
+ | `useMessageRuntime` | `UserMessage`, `AssistantMessage` — edit, reload, branch |
280
+ | `useComposerRuntime` | `Composer`, `EditComposer` — access composer state |
281
+
282
+ ---
283
+
284
+ ## API reference
285
+
286
+ ### `TimbalChat` props
287
+
288
+ `TimbalChat` accepts all `TimbalRuntimeProvider` props plus all `Thread` props.
164
289
 
165
290
  | Prop | Type | Default | Description |
166
291
  |---|---|---|---|
292
+ | `workforceId` | `string` | **required** | ID of the workforce to stream from |
293
+ | `baseUrl` | `string` | `"/api"` | Base URL for API calls. Posts to `{baseUrl}/workforce/{workforceId}/stream` |
294
+ | `fetch` | `(url, options?) => Promise<Response>` | `authFetch` | Custom fetch. Defaults to the built-in auth-aware fetch (Bearer token + auto-refresh) |
167
295
  | `welcome.heading` | `string` | `"How can I help you today?"` | Welcome screen heading |
168
296
  | `welcome.subheading` | `string` | `"Send a message to start a conversation."` | Welcome screen subheading |
169
297
  | `suggestions` | `{ title: string; description?: string }[]` | — | Suggestion chips on the welcome screen |
170
298
  | `composerPlaceholder` | `string` | `"Send a message..."` | Composer input placeholder |
299
+ | `components` | `ThreadComponents` | — | Override individual UI slots |
171
300
  | `maxWidth` | `string` | `"44rem"` | Max width of the message column |
172
301
  | `className` | `string` | — | Extra classes on the root element |
173
302
 
303
+ ### `Thread` props
304
+
305
+ Same as `TimbalChat` minus `workforceId`, `baseUrl`, and `fetch` (those live on `TimbalRuntimeProvider`).
306
+
174
307
  ### `TimbalRuntimeProvider` props
175
308
 
176
309
  | Prop | Type | Default | Description |
177
310
  |---|---|---|---|
178
- | `workforceId` | `string` | | ID of the workforce to stream from |
179
- | `baseUrl` | `string` | `"/api"` | Base URL for API calls. Posts to `{baseUrl}/workforce/{workforceId}/stream` |
180
- | `fetch` | `(url, options?) => Promise<Response>` | `authFetch` | Custom fetch function. Defaults to the built-in auth-aware fetch (Bearer token + auto-refresh) |
311
+ | `workforceId` | `string` | **required** | ID of the workforce to stream from |
312
+ | `baseUrl` | `string` | `"/api"` | Base URL for API calls |
313
+ | `fetch` | `(url, options?) => Promise<Response>` | `authFetch` | Custom fetch function |
181
314
 
182
315
  ---
183
316
 
184
317
  ## Auth
185
318
 
186
- The package includes a session/auth system backed by localStorage tokens. The API is expected to expose `/api/auth/login`, `/api/auth/logout`, and `/api/auth/refresh`.
319
+ The package includes an optional session/auth system backed by localStorage tokens. The API is expected to expose `/api/auth/login`, `/api/auth/logout`, and `/api/auth/refresh`.
320
+
321
+ Auth is **opt-in** — it only activates when `VITE_TIMBAL_PROJECT_ID` is set in your environment.
187
322
 
188
323
  ### Setup
189
324
 
@@ -195,7 +330,6 @@ import { SessionProvider, AuthGuard, TooltipProvider } from "@timbal-ai/timbal-r
195
330
  import { BrowserRouter, Routes, Route } from "react-router-dom";
196
331
  import Home from "./pages/Home";
197
332
 
198
- // Auth is opt-in — only active when VITE_TIMBAL_PROJECT_ID is set
199
333
  const isAuthEnabled = !!import.meta.env.VITE_TIMBAL_PROJECT_ID;
200
334
 
201
335
  export default function App() {
@@ -215,20 +349,7 @@ export default function App() {
215
349
  }
216
350
  ```
217
351
 
218
- When `enabled` is `false` (no project ID configured), both `SessionProvider` and `AuthGuard` are transparent — no redirects, no API calls.
219
-
220
- ### `SessionProvider` props
221
-
222
- | Prop | Type | Default | Description |
223
- |---|---|---|---|
224
- | `enabled` | `boolean` | `true` | When `false`, session is always `null` and no API calls are made |
225
-
226
- ### `AuthGuard` props
227
-
228
- | Prop | Type | Default | Description |
229
- |---|---|---|---|
230
- | `requireAuth` | `boolean` | `false` | Redirect to login if not authenticated |
231
- | `enabled` | `boolean` | `true` | When `false`, renders children unconditionally |
352
+ When `enabled` is `false`, both `SessionProvider` and `AuthGuard` are transparent — no redirects, no API calls.
232
353
 
233
354
  ### `useSession` hook
234
355
 
@@ -239,9 +360,7 @@ import { useSession } from "@timbal-ai/timbal-react";
239
360
 
240
361
  function Header() {
241
362
  const { user, isAuthenticated, loading, logout } = useSession();
242
-
243
363
  if (loading) return null;
244
-
245
364
  return (
246
365
  <header>
247
366
  {isAuthenticated ? (
@@ -259,25 +378,30 @@ function Header() {
259
378
 
260
379
  ### `authFetch`
261
380
 
262
- A drop-in replacement for `fetch` that attaches the Bearer token from localStorage and auto-refreshes on 401:
381
+ A drop-in replacement for `fetch` that attaches the Bearer token from localStorage and auto-refreshes on 401. It's also the default `fetch` used by `TimbalRuntimeProvider`, so you only need to import it directly for your own API calls (e.g. loading workforce lists):
263
382
 
264
383
  ```tsx
265
384
  import { authFetch } from "@timbal-ai/timbal-react";
266
385
 
267
- // Fetch a list of workforce agents
268
386
  const res = await authFetch("/api/workforce");
269
387
  if (res.ok) {
270
388
  const agents = await res.json();
271
389
  }
272
390
  ```
273
391
 
274
- It's also the default `fetch` used by `TimbalRuntimeProvider` — you only need to import it directly for your own API calls (e.g. loading workforce lists, metadata, etc.).
392
+ ### Auth prop reference
393
+
394
+ | Component | Prop | Type | Default | Description |
395
+ |---|---|---|---|---|
396
+ | `SessionProvider` | `enabled` | `boolean` | `true` | When `false`, session is always `null` and no API calls are made |
397
+ | `AuthGuard` | `requireAuth` | `boolean` | `false` | Redirect to login if not authenticated |
398
+ | `AuthGuard` | `enabled` | `boolean` | `true` | When `false`, renders children unconditionally |
275
399
 
276
400
  ---
277
401
 
278
- ## Components
402
+ ## Other exports
279
403
 
280
- All components accept `className` for Tailwind overrides.
404
+ ### Components
281
405
 
282
406
  | Export | Description |
283
407
  |---|---|
@@ -306,12 +430,7 @@ A complete page with agent switching, auth, and a custom header:
306
430
  // src/pages/Home.tsx
307
431
  import { useEffect, useState } from "react";
308
432
  import type { WorkforceItem } from "@timbal-ai/timbal-sdk";
309
- import {
310
- TimbalChat,
311
- Button,
312
- authFetch,
313
- useSession,
314
- } from "@timbal-ai/timbal-react";
433
+ import { TimbalChat, Button, authFetch, useSession } from "@timbal-ai/timbal-react";
315
434
  import { LogOut } from "lucide-react";
316
435
 
317
436
  const isAuthEnabled = !!import.meta.env.VITE_TIMBAL_PROJECT_ID;
@@ -368,7 +487,6 @@ export default function Home() {
368
487
  Install via a local path reference:
369
488
 
370
489
  ```json
371
- // package.json
372
490
  {
373
491
  "dependencies": {
374
492
  "@timbal-ai/timbal-react": "file:../../timbal-react"
@@ -378,7 +496,7 @@ Install via a local path reference:
378
496
 
379
497
  Adjust the relative path to where `timbal-react` lives on your machine.
380
498
 
381
- After editing source files, rebuild the package:
499
+ After editing source files, rebuild:
382
500
 
383
501
  ```bash
384
502
  cd timbal-react
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ ActionBarPrimitive: () => import_react13.ActionBarPrimitive,
33
34
  AuthGuard: () => AuthGuard,
34
35
  Avatar: () => Avatar,
35
36
  AvatarFallback: () => AvatarFallback,
@@ -37,6 +38,7 @@ __export(index_exports, {
37
38
  Button: () => Button,
38
39
  ComposerAddAttachment: () => ComposerAddAttachment,
39
40
  ComposerAttachments: () => ComposerAttachments,
41
+ ComposerPrimitive: () => import_react13.ComposerPrimitive,
40
42
  Dialog: () => Dialog,
41
43
  DialogClose: () => DialogClose,
42
44
  DialogContent: () => DialogContent,
@@ -45,10 +47,12 @@ __export(index_exports, {
45
47
  DialogTitle: () => DialogTitle,
46
48
  DialogTrigger: () => DialogTrigger,
47
49
  MarkdownText: () => MarkdownText,
50
+ MessagePrimitive: () => import_react13.MessagePrimitive,
48
51
  SessionProvider: () => SessionProvider,
49
52
  Shimmer: () => Shimmer,
50
53
  SyntaxHighlighter: () => syntax_highlighter_default,
51
54
  Thread: () => Thread,
55
+ ThreadPrimitive: () => import_react13.ThreadPrimitive,
52
56
  TimbalChat: () => TimbalChat,
53
57
  TimbalRuntimeProvider: () => TimbalRuntimeProvider,
54
58
  ToolFallback: () => ToolFallback,
@@ -66,7 +70,11 @@ __export(index_exports, {
66
70
  getAccessToken: () => getAccessToken,
67
71
  getRefreshToken: () => getRefreshToken,
68
72
  refreshAccessToken: () => refreshAccessToken,
69
- useSession: () => useSession
73
+ useComposerRuntime: () => import_react13.useComposerRuntime,
74
+ useMessageRuntime: () => import_react13.useMessageRuntime,
75
+ useSession: () => useSession,
76
+ useThread: () => import_react13.useThread,
77
+ useThreadRuntime: () => import_react13.useThreadRuntime
70
78
  });
71
79
  module.exports = __toCommonJS(index_exports);
72
80
 
@@ -1273,8 +1281,15 @@ var Thread = ({
1273
1281
  maxWidth = "44rem",
1274
1282
  welcome,
1275
1283
  suggestions,
1276
- composerPlaceholder = "Send a message..."
1284
+ composerPlaceholder = "Send a message...",
1285
+ components
1277
1286
  }) => {
1287
+ const WelcomeSlot = components?.Welcome ?? ThreadWelcome;
1288
+ const ComposerSlot = components?.Composer ?? Composer;
1289
+ const UserMessageSlot = components?.UserMessage ?? UserMessage;
1290
+ const AssistantMessageSlot = components?.AssistantMessage ?? AssistantMessage;
1291
+ const EditComposerSlot = components?.EditComposer ?? EditComposer;
1292
+ const ScrollToBottomSlot = components?.ScrollToBottom ?? ThreadScrollToBottom;
1278
1293
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1279
1294
  import_react11.ThreadPrimitive.Root,
1280
1295
  {
@@ -1289,20 +1304,20 @@ var Thread = ({
1289
1304
  turnAnchor: "bottom",
1290
1305
  className: "aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll px-4 pt-4",
1291
1306
  children: [
1292
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react11.AuiIf, { condition: (s) => s.thread.isEmpty, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadWelcome, { config: welcome, suggestions }) }),
1307
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(WelcomeSlot, { config: welcome, suggestions }),
1293
1308
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1294
1309
  import_react11.ThreadPrimitive.Messages,
1295
1310
  {
1296
1311
  components: {
1297
- UserMessage,
1298
- EditComposer,
1299
- AssistantMessage
1312
+ UserMessage: UserMessageSlot,
1313
+ EditComposer: EditComposerSlot,
1314
+ AssistantMessage: AssistantMessageSlot
1300
1315
  }
1301
1316
  }
1302
1317
  ),
1303
1318
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_react11.ThreadPrimitive.ViewportFooter, { className: "aui-thread-viewport-footer sticky bottom-0 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4 md:pb-6", children: [
1304
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadScrollToBottom, {}),
1305
- /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(Composer, { placeholder: composerPlaceholder })
1319
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ScrollToBottomSlot, {}),
1320
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ComposerSlot, { placeholder: composerPlaceholder })
1306
1321
  ] })
1307
1322
  ]
1308
1323
  }
@@ -1322,7 +1337,7 @@ var ThreadScrollToBottom = () => {
1322
1337
  ) });
1323
1338
  };
1324
1339
  var ThreadWelcome = ({ config, suggestions }) => {
1325
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col", children: [
1340
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_react11.AuiIf, { condition: (s) => s.thread.isEmpty, children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col", children: [
1326
1341
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "aui-thread-welcome-center flex w-full grow flex-col items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "aui-thread-welcome-message flex size-full flex-col items-center justify-center px-4 text-center", children: [
1327
1342
  /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "fade-in animate-in fill-mode-both relative mb-6 flex size-14 items-center justify-center duration-300", children: [
1328
1343
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "animate-ai-ring-glow absolute inset-0 rounded-2xl bg-gradient-to-br from-primary/15 to-primary/5 ring-1 ring-primary/15" }),
@@ -1346,7 +1361,7 @@ var ThreadWelcome = ({ config, suggestions }) => {
1346
1361
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { className: "aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-muted-foreground mt-2 delay-75 duration-200", children: config?.subheading ?? "Send a message to start a conversation." })
1347
1362
  ] }) }),
1348
1363
  suggestions && suggestions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadSuggestions, { suggestions })
1349
- ] });
1364
+ ] }) });
1350
1365
  };
1351
1366
  var ThreadSuggestions = ({ suggestions }) => {
1352
1367
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4", children: suggestions.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadSuggestionItem, { title: s.title, description: s.description }, i)) });
@@ -1534,6 +1549,9 @@ function TimbalChat({
1534
1549
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(TimbalRuntimeProvider, { workforceId, baseUrl, fetch: fetch2, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Thread, { ...threadProps }) });
1535
1550
  }
1536
1551
 
1552
+ // src/index.ts
1553
+ var import_react13 = require("@assistant-ui/react");
1554
+
1537
1555
  // src/auth/provider.tsx
1538
1556
  var import_react12 = require("react");
1539
1557
  var import_jsx_runtime14 = require("react/jsx-runtime");
@@ -1640,6 +1658,7 @@ var AuthGuard = ({
1640
1658
  };
1641
1659
  // Annotate the CommonJS export names for ESM import in node:
1642
1660
  0 && (module.exports = {
1661
+ ActionBarPrimitive,
1643
1662
  AuthGuard,
1644
1663
  Avatar,
1645
1664
  AvatarFallback,
@@ -1647,6 +1666,7 @@ var AuthGuard = ({
1647
1666
  Button,
1648
1667
  ComposerAddAttachment,
1649
1668
  ComposerAttachments,
1669
+ ComposerPrimitive,
1650
1670
  Dialog,
1651
1671
  DialogClose,
1652
1672
  DialogContent,
@@ -1655,10 +1675,12 @@ var AuthGuard = ({
1655
1675
  DialogTitle,
1656
1676
  DialogTrigger,
1657
1677
  MarkdownText,
1678
+ MessagePrimitive,
1658
1679
  SessionProvider,
1659
1680
  Shimmer,
1660
1681
  SyntaxHighlighter,
1661
1682
  Thread,
1683
+ ThreadPrimitive,
1662
1684
  TimbalChat,
1663
1685
  TimbalRuntimeProvider,
1664
1686
  ToolFallback,
@@ -1676,5 +1698,9 @@ var AuthGuard = ({
1676
1698
  getAccessToken,
1677
1699
  getRefreshToken,
1678
1700
  refreshAccessToken,
1679
- useSession
1701
+ useComposerRuntime,
1702
+ useMessageRuntime,
1703
+ useSession,
1704
+ useThread,
1705
+ useThreadRuntime
1680
1706
  });
package/dist/index.d.cts CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
- import React__default, { ReactNode, FC, ComponentPropsWithRef, ElementType } from 'react';
3
+ import React__default, { ReactNode, FC, ComponentType, ComponentPropsWithRef, ElementType } from 'react';
4
4
  import { ToolCallMessagePartComponent } from '@assistant-ui/react';
5
+ export { ActionBarPrimitive, ComposerPrimitive, MessagePrimitive, ThreadPrimitive, useComposerRuntime, useMessageRuntime, useThread, useThreadRuntime } from '@assistant-ui/react';
5
6
  import * as class_variance_authority_types from 'class-variance-authority/types';
6
7
  import { VariantProps } from 'class-variance-authority';
7
8
  import { SyntaxHighlighterProps } from '@assistant-ui/react-markdown';
@@ -34,6 +35,26 @@ interface ThreadWelcomeConfig {
34
35
  heading?: string;
35
36
  subheading?: string;
36
37
  }
38
+ interface ThreadWelcomeProps {
39
+ config?: ThreadWelcomeConfig;
40
+ suggestions?: ThreadSuggestion[];
41
+ }
42
+ interface ThreadComponents {
43
+ /** Replace the user message bubble. Access message content via `MessagePrimitive.Parts`. */
44
+ UserMessage?: ComponentType;
45
+ /** Replace the assistant message bubble. Access message content via `MessagePrimitive.Parts`. */
46
+ AssistantMessage?: ComponentType;
47
+ /** Replace the inline edit composer. */
48
+ EditComposer?: ComponentType;
49
+ /** Replace the composer (input bar). Receives `placeholder` from `composerPlaceholder`. */
50
+ Composer?: ComponentType<{
51
+ placeholder?: string;
52
+ }>;
53
+ /** Replace the welcome / empty state. Receives `config` and `suggestions` props. Controls its own visibility — use `useThread(s => s.isEmpty)` to replicate the default behaviour. */
54
+ Welcome?: ComponentType<ThreadWelcomeProps>;
55
+ /** Replace the scroll-to-bottom button. */
56
+ ScrollToBottom?: ComponentType;
57
+ }
37
58
  interface ThreadProps {
38
59
  className?: string;
39
60
  /** Max width of the message column. Default: "44rem" */
@@ -44,6 +65,8 @@ interface ThreadProps {
44
65
  suggestions?: ThreadSuggestion[];
45
66
  /** Composer input placeholder. Default: "Send a message..." */
46
67
  composerPlaceholder?: string;
68
+ /** Override individual UI slots while keeping the rest as defaults. */
69
+ components?: ThreadComponents;
47
70
  }
48
71
  declare const Thread: FC<ThreadProps>;
49
72
 
@@ -136,4 +159,4 @@ declare const Shimmer: React.MemoExoticComponent<({ children, as: Component, cla
136
159
 
137
160
  declare function cn(...inputs: ClassValue[]): string;
138
161
 
139
- export { AuthGuard, Avatar, AvatarFallback, AvatarImage, Button, ComposerAddAttachment, ComposerAttachments, Dialog, DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, MarkdownText, SessionProvider, Shimmer, ShikiSyntaxHighlighter as SyntaxHighlighter, type TextShimmerProps, Thread, type ThreadProps, type ThreadSuggestion, type ThreadWelcomeConfig, TimbalChat, type TimbalChatProps, TimbalRuntimeProvider, type TimbalRuntimeProviderProps, ToolFallback, Tooltip, TooltipContent, TooltipIconButton, type TooltipIconButtonProps, TooltipProvider, TooltipTrigger, UserMessageAttachments, authFetch, buttonVariants, clearTokens, cn, fetchCurrentUser, getAccessToken, getRefreshToken, refreshAccessToken, useSession };
162
+ export { AuthGuard, Avatar, AvatarFallback, AvatarImage, Button, ComposerAddAttachment, ComposerAttachments, Dialog, DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, MarkdownText, SessionProvider, Shimmer, ShikiSyntaxHighlighter as SyntaxHighlighter, type TextShimmerProps, Thread, type ThreadComponents, type ThreadProps, type ThreadSuggestion, type ThreadWelcomeConfig, type ThreadWelcomeProps, TimbalChat, type TimbalChatProps, TimbalRuntimeProvider, type TimbalRuntimeProviderProps, ToolFallback, Tooltip, TooltipContent, TooltipIconButton, type TooltipIconButtonProps, TooltipProvider, TooltipTrigger, UserMessageAttachments, authFetch, buttonVariants, clearTokens, cn, fetchCurrentUser, getAccessToken, getRefreshToken, refreshAccessToken, useSession };
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
- import React__default, { ReactNode, FC, ComponentPropsWithRef, ElementType } from 'react';
3
+ import React__default, { ReactNode, FC, ComponentType, ComponentPropsWithRef, ElementType } from 'react';
4
4
  import { ToolCallMessagePartComponent } from '@assistant-ui/react';
5
+ export { ActionBarPrimitive, ComposerPrimitive, MessagePrimitive, ThreadPrimitive, useComposerRuntime, useMessageRuntime, useThread, useThreadRuntime } from '@assistant-ui/react';
5
6
  import * as class_variance_authority_types from 'class-variance-authority/types';
6
7
  import { VariantProps } from 'class-variance-authority';
7
8
  import { SyntaxHighlighterProps } from '@assistant-ui/react-markdown';
@@ -34,6 +35,26 @@ interface ThreadWelcomeConfig {
34
35
  heading?: string;
35
36
  subheading?: string;
36
37
  }
38
+ interface ThreadWelcomeProps {
39
+ config?: ThreadWelcomeConfig;
40
+ suggestions?: ThreadSuggestion[];
41
+ }
42
+ interface ThreadComponents {
43
+ /** Replace the user message bubble. Access message content via `MessagePrimitive.Parts`. */
44
+ UserMessage?: ComponentType;
45
+ /** Replace the assistant message bubble. Access message content via `MessagePrimitive.Parts`. */
46
+ AssistantMessage?: ComponentType;
47
+ /** Replace the inline edit composer. */
48
+ EditComposer?: ComponentType;
49
+ /** Replace the composer (input bar). Receives `placeholder` from `composerPlaceholder`. */
50
+ Composer?: ComponentType<{
51
+ placeholder?: string;
52
+ }>;
53
+ /** Replace the welcome / empty state. Receives `config` and `suggestions` props. Controls its own visibility — use `useThread(s => s.isEmpty)` to replicate the default behaviour. */
54
+ Welcome?: ComponentType<ThreadWelcomeProps>;
55
+ /** Replace the scroll-to-bottom button. */
56
+ ScrollToBottom?: ComponentType;
57
+ }
37
58
  interface ThreadProps {
38
59
  className?: string;
39
60
  /** Max width of the message column. Default: "44rem" */
@@ -44,6 +65,8 @@ interface ThreadProps {
44
65
  suggestions?: ThreadSuggestion[];
45
66
  /** Composer input placeholder. Default: "Send a message..." */
46
67
  composerPlaceholder?: string;
68
+ /** Override individual UI slots while keeping the rest as defaults. */
69
+ components?: ThreadComponents;
47
70
  }
48
71
  declare const Thread: FC<ThreadProps>;
49
72
 
@@ -136,4 +159,4 @@ declare const Shimmer: React.MemoExoticComponent<({ children, as: Component, cla
136
159
 
137
160
  declare function cn(...inputs: ClassValue[]): string;
138
161
 
139
- export { AuthGuard, Avatar, AvatarFallback, AvatarImage, Button, ComposerAddAttachment, ComposerAttachments, Dialog, DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, MarkdownText, SessionProvider, Shimmer, ShikiSyntaxHighlighter as SyntaxHighlighter, type TextShimmerProps, Thread, type ThreadProps, type ThreadSuggestion, type ThreadWelcomeConfig, TimbalChat, type TimbalChatProps, TimbalRuntimeProvider, type TimbalRuntimeProviderProps, ToolFallback, Tooltip, TooltipContent, TooltipIconButton, type TooltipIconButtonProps, TooltipProvider, TooltipTrigger, UserMessageAttachments, authFetch, buttonVariants, clearTokens, cn, fetchCurrentUser, getAccessToken, getRefreshToken, refreshAccessToken, useSession };
162
+ export { AuthGuard, Avatar, AvatarFallback, AvatarImage, Button, ComposerAddAttachment, ComposerAttachments, Dialog, DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, MarkdownText, SessionProvider, Shimmer, ShikiSyntaxHighlighter as SyntaxHighlighter, type TextShimmerProps, Thread, type ThreadComponents, type ThreadProps, type ThreadSuggestion, type ThreadWelcomeConfig, type ThreadWelcomeProps, TimbalChat, type TimbalChatProps, TimbalRuntimeProvider, type TimbalRuntimeProviderProps, ToolFallback, Tooltip, TooltipContent, TooltipIconButton, type TooltipIconButtonProps, TooltipProvider, TooltipTrigger, UserMessageAttachments, authFetch, buttonVariants, clearTokens, cn, fetchCurrentUser, getAccessToken, getRefreshToken, refreshAccessToken, useSession };
package/dist/index.esm.js CHANGED
@@ -1236,8 +1236,15 @@ var Thread = ({
1236
1236
  maxWidth = "44rem",
1237
1237
  welcome,
1238
1238
  suggestions,
1239
- composerPlaceholder = "Send a message..."
1239
+ composerPlaceholder = "Send a message...",
1240
+ components
1240
1241
  }) => {
1242
+ const WelcomeSlot = components?.Welcome ?? ThreadWelcome;
1243
+ const ComposerSlot = components?.Composer ?? Composer;
1244
+ const UserMessageSlot = components?.UserMessage ?? UserMessage;
1245
+ const AssistantMessageSlot = components?.AssistantMessage ?? AssistantMessage;
1246
+ const EditComposerSlot = components?.EditComposer ?? EditComposer;
1247
+ const ScrollToBottomSlot = components?.ScrollToBottom ?? ThreadScrollToBottom;
1241
1248
  return /* @__PURE__ */ jsx12(
1242
1249
  ThreadPrimitive.Root,
1243
1250
  {
@@ -1252,20 +1259,20 @@ var Thread = ({
1252
1259
  turnAnchor: "bottom",
1253
1260
  className: "aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll px-4 pt-4",
1254
1261
  children: [
1255
- /* @__PURE__ */ jsx12(AuiIf, { condition: (s) => s.thread.isEmpty, children: /* @__PURE__ */ jsx12(ThreadWelcome, { config: welcome, suggestions }) }),
1262
+ /* @__PURE__ */ jsx12(WelcomeSlot, { config: welcome, suggestions }),
1256
1263
  /* @__PURE__ */ jsx12(
1257
1264
  ThreadPrimitive.Messages,
1258
1265
  {
1259
1266
  components: {
1260
- UserMessage,
1261
- EditComposer,
1262
- AssistantMessage
1267
+ UserMessage: UserMessageSlot,
1268
+ EditComposer: EditComposerSlot,
1269
+ AssistantMessage: AssistantMessageSlot
1263
1270
  }
1264
1271
  }
1265
1272
  ),
1266
1273
  /* @__PURE__ */ jsxs7(ThreadPrimitive.ViewportFooter, { className: "aui-thread-viewport-footer sticky bottom-0 mx-auto mt-auto flex w-full max-w-(--thread-max-width) flex-col gap-4 overflow-visible rounded-t-3xl bg-background pb-4 md:pb-6", children: [
1267
- /* @__PURE__ */ jsx12(ThreadScrollToBottom, {}),
1268
- /* @__PURE__ */ jsx12(Composer, { placeholder: composerPlaceholder })
1274
+ /* @__PURE__ */ jsx12(ScrollToBottomSlot, {}),
1275
+ /* @__PURE__ */ jsx12(ComposerSlot, { placeholder: composerPlaceholder })
1269
1276
  ] })
1270
1277
  ]
1271
1278
  }
@@ -1285,7 +1292,7 @@ var ThreadScrollToBottom = () => {
1285
1292
  ) });
1286
1293
  };
1287
1294
  var ThreadWelcome = ({ config, suggestions }) => {
1288
- return /* @__PURE__ */ jsxs7("div", { className: "aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col", children: [
1295
+ return /* @__PURE__ */ jsx12(AuiIf, { condition: (s) => s.thread.isEmpty, children: /* @__PURE__ */ jsxs7("div", { className: "aui-thread-welcome-root mx-auto my-auto flex w-full max-w-(--thread-max-width) grow flex-col", children: [
1289
1296
  /* @__PURE__ */ jsx12("div", { className: "aui-thread-welcome-center flex w-full grow flex-col items-center justify-center", children: /* @__PURE__ */ jsxs7("div", { className: "aui-thread-welcome-message flex size-full flex-col items-center justify-center px-4 text-center", children: [
1290
1297
  /* @__PURE__ */ jsxs7("div", { className: "fade-in animate-in fill-mode-both relative mb-6 flex size-14 items-center justify-center duration-300", children: [
1291
1298
  /* @__PURE__ */ jsx12("div", { className: "animate-ai-ring-glow absolute inset-0 rounded-2xl bg-gradient-to-br from-primary/15 to-primary/5 ring-1 ring-primary/15" }),
@@ -1309,7 +1316,7 @@ var ThreadWelcome = ({ config, suggestions }) => {
1309
1316
  /* @__PURE__ */ jsx12("p", { className: "aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-muted-foreground mt-2 delay-75 duration-200", children: config?.subheading ?? "Send a message to start a conversation." })
1310
1317
  ] }) }),
1311
1318
  suggestions && suggestions.length > 0 && /* @__PURE__ */ jsx12(ThreadSuggestions, { suggestions })
1312
- ] });
1319
+ ] }) });
1313
1320
  };
1314
1321
  var ThreadSuggestions = ({ suggestions }) => {
1315
1322
  return /* @__PURE__ */ jsx12("div", { className: "aui-thread-welcome-suggestions grid w-full @md:grid-cols-2 gap-2 pb-4", children: suggestions.map((s, i) => /* @__PURE__ */ jsx12(ThreadSuggestionItem, { title: s.title, description: s.description }, i)) });
@@ -1497,6 +1504,18 @@ function TimbalChat({
1497
1504
  return /* @__PURE__ */ jsx13(TimbalRuntimeProvider, { workforceId, baseUrl, fetch: fetch2, children: /* @__PURE__ */ jsx13(Thread, { ...threadProps }) });
1498
1505
  }
1499
1506
 
1507
+ // src/index.ts
1508
+ import {
1509
+ ThreadPrimitive as ThreadPrimitive2,
1510
+ MessagePrimitive as MessagePrimitive3,
1511
+ ComposerPrimitive as ComposerPrimitive3,
1512
+ ActionBarPrimitive as ActionBarPrimitive2,
1513
+ useThread,
1514
+ useThreadRuntime as useThreadRuntime2,
1515
+ useMessageRuntime,
1516
+ useComposerRuntime
1517
+ } from "@assistant-ui/react";
1518
+
1500
1519
  // src/auth/provider.tsx
1501
1520
  import {
1502
1521
  createContext,
@@ -1608,6 +1627,7 @@ var AuthGuard = ({
1608
1627
  return children;
1609
1628
  };
1610
1629
  export {
1630
+ ActionBarPrimitive2 as ActionBarPrimitive,
1611
1631
  AuthGuard,
1612
1632
  Avatar,
1613
1633
  AvatarFallback,
@@ -1615,6 +1635,7 @@ export {
1615
1635
  Button,
1616
1636
  ComposerAddAttachment,
1617
1637
  ComposerAttachments,
1638
+ ComposerPrimitive3 as ComposerPrimitive,
1618
1639
  Dialog,
1619
1640
  DialogClose,
1620
1641
  DialogContent,
@@ -1623,10 +1644,12 @@ export {
1623
1644
  DialogTitle,
1624
1645
  DialogTrigger,
1625
1646
  MarkdownText,
1647
+ MessagePrimitive3 as MessagePrimitive,
1626
1648
  SessionProvider,
1627
1649
  Shimmer,
1628
1650
  syntax_highlighter_default as SyntaxHighlighter,
1629
1651
  Thread,
1652
+ ThreadPrimitive2 as ThreadPrimitive,
1630
1653
  TimbalChat,
1631
1654
  TimbalRuntimeProvider,
1632
1655
  ToolFallback,
@@ -1644,5 +1667,9 @@ export {
1644
1667
  getAccessToken,
1645
1668
  getRefreshToken,
1646
1669
  refreshAccessToken,
1647
- useSession
1670
+ useComposerRuntime,
1671
+ useMessageRuntime,
1672
+ useSession,
1673
+ useThread,
1674
+ useThreadRuntime2 as useThreadRuntime
1648
1675
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timbal-ai/timbal-react",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "React components and runtime for building Timbal chat UIs",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -22,6 +22,8 @@
22
22
  "build": "tsup",
23
23
  "build:watch": "tsup --watch",
24
24
  "clean": "rm -rf dist",
25
+ "test": "bun test",
26
+ "test:watch": "bun test --watch",
25
27
  "typecheck": "tsc --noEmit",
26
28
  "prepublishOnly": "bun run build && bun run typecheck"
27
29
  },
@@ -52,9 +54,13 @@
52
54
  },
53
55
  "devDependencies": {
54
56
  "@assistant-ui/react": "^0.12.10",
57
+ "@testing-library/jest-dom": "^6.9.1",
58
+ "@testing-library/react": "^16.3.2",
59
+ "@testing-library/user-event": "^14.6.1",
55
60
  "@timbal-ai/timbal-sdk": "0.4.9",
56
61
  "@types/react": "^19.2.4",
57
62
  "@types/react-dom": "^19.2.3",
63
+ "happy-dom": "^20.8.9",
58
64
  "react": "^19.2.0",
59
65
  "react-dom": "^19.2.0",
60
66
  "tsup": "^8.5.0",