@timbal-ai/timbal-react 0.2.0 → 0.2.2

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,13 @@ __export(index_exports, {
66
70
  getAccessToken: () => getAccessToken,
67
71
  getRefreshToken: () => getRefreshToken,
68
72
  refreshAccessToken: () => refreshAccessToken,
69
- useSession: () => useSession
73
+ setAccessToken: () => setAccessToken,
74
+ setRefreshToken: () => setRefreshToken,
75
+ useComposerRuntime: () => import_react13.useComposerRuntime,
76
+ useMessageRuntime: () => import_react13.useMessageRuntime,
77
+ useSession: () => useSession,
78
+ useThread: () => import_react13.useThread,
79
+ useThreadRuntime: () => import_react13.useThreadRuntime
70
80
  });
71
81
  module.exports = __toCommonJS(index_exports);
72
82
 
@@ -79,7 +89,9 @@ var import_timbal_sdk = require("@timbal-ai/timbal-sdk");
79
89
  var ACCESS_TOKEN_KEY = "timbal_project_access_token";
80
90
  var REFRESH_TOKEN_KEY = "timbal_project_refresh_token";
81
91
  var getAccessToken = () => localStorage.getItem(ACCESS_TOKEN_KEY);
92
+ var setAccessToken = (token) => localStorage.setItem(ACCESS_TOKEN_KEY, token);
82
93
  var getRefreshToken = () => localStorage.getItem(REFRESH_TOKEN_KEY);
94
+ var setRefreshToken = (token) => localStorage.setItem(REFRESH_TOKEN_KEY, token);
83
95
  var clearTokens = () => {
84
96
  localStorage.removeItem(ACCESS_TOKEN_KEY);
85
97
  localStorage.removeItem(REFRESH_TOKEN_KEY);
@@ -1273,8 +1285,15 @@ var Thread = ({
1273
1285
  maxWidth = "44rem",
1274
1286
  welcome,
1275
1287
  suggestions,
1276
- composerPlaceholder = "Send a message..."
1288
+ composerPlaceholder = "Send a message...",
1289
+ components
1277
1290
  }) => {
1291
+ const WelcomeSlot = components?.Welcome ?? ThreadWelcome;
1292
+ const ComposerSlot = components?.Composer ?? Composer;
1293
+ const UserMessageSlot = components?.UserMessage ?? UserMessage;
1294
+ const AssistantMessageSlot = components?.AssistantMessage ?? AssistantMessage;
1295
+ const EditComposerSlot = components?.EditComposer ?? EditComposer;
1296
+ const ScrollToBottomSlot = components?.ScrollToBottom ?? ThreadScrollToBottom;
1278
1297
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1279
1298
  import_react11.ThreadPrimitive.Root,
1280
1299
  {
@@ -1289,20 +1308,20 @@ var Thread = ({
1289
1308
  turnAnchor: "bottom",
1290
1309
  className: "aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll px-4 pt-4",
1291
1310
  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 }) }),
1311
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(WelcomeSlot, { config: welcome, suggestions }),
1293
1312
  /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1294
1313
  import_react11.ThreadPrimitive.Messages,
1295
1314
  {
1296
1315
  components: {
1297
- UserMessage,
1298
- EditComposer,
1299
- AssistantMessage
1316
+ UserMessage: UserMessageSlot,
1317
+ EditComposer: EditComposerSlot,
1318
+ AssistantMessage: AssistantMessageSlot
1300
1319
  }
1301
1320
  }
1302
1321
  ),
1303
1322
  /* @__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 })
1323
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ScrollToBottomSlot, {}),
1324
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ComposerSlot, { placeholder: composerPlaceholder })
1306
1325
  ] })
1307
1326
  ]
1308
1327
  }
@@ -1322,7 +1341,7 @@ var ThreadScrollToBottom = () => {
1322
1341
  ) });
1323
1342
  };
1324
1343
  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: [
1344
+ 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
1345
  /* @__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
1346
  /* @__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
1347
  /* @__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 +1365,7 @@ var ThreadWelcome = ({ config, suggestions }) => {
1346
1365
  /* @__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
1366
  ] }) }),
1348
1367
  suggestions && suggestions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ThreadSuggestions, { suggestions })
1349
- ] });
1368
+ ] }) });
1350
1369
  };
1351
1370
  var ThreadSuggestions = ({ suggestions }) => {
1352
1371
  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,9 +1553,19 @@ function TimbalChat({
1534
1553
  return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(TimbalRuntimeProvider, { workforceId, baseUrl, fetch: fetch2, children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(Thread, { ...threadProps }) });
1535
1554
  }
1536
1555
 
1556
+ // src/index.ts
1557
+ var import_react13 = require("@assistant-ui/react");
1558
+
1537
1559
  // src/auth/provider.tsx
1538
1560
  var import_react12 = require("react");
1539
1561
  var import_jsx_runtime14 = require("react/jsx-runtime");
1562
+ function isInsideIframe() {
1563
+ try {
1564
+ return typeof window !== "undefined" && window.self !== window.top;
1565
+ } catch {
1566
+ return true;
1567
+ }
1568
+ }
1540
1569
  var SessionContext = (0, import_react12.createContext)(void 0);
1541
1570
  var useSession = () => {
1542
1571
  const context = (0, import_react12.useContext)(SessionContext);
@@ -1551,6 +1580,7 @@ var SessionProvider = ({
1551
1580
  }) => {
1552
1581
  const [user, setUser] = (0, import_react12.useState)(null);
1553
1582
  const [loading, setLoading] = (0, import_react12.useState)(enabled);
1583
+ const [embedded] = (0, import_react12.useState)(isInsideIframe);
1554
1584
  (0, import_react12.useEffect)(() => {
1555
1585
  if (!enabled) {
1556
1586
  setLoading(false);
@@ -1583,13 +1613,35 @@ var SessionProvider = ({
1583
1613
  if (ignore) return;
1584
1614
  clearTokens();
1585
1615
  }
1586
- setLoading(false);
1616
+ if (!ignore && !embedded) {
1617
+ setLoading(false);
1618
+ }
1587
1619
  };
1588
1620
  restoreSession();
1621
+ let messageCleanup;
1622
+ if (embedded) {
1623
+ const handleMessage = async (event) => {
1624
+ if (ignore) return;
1625
+ if (event.data?.type !== "timbal:auth" || !event.data.token) return;
1626
+ setAccessToken(event.data.token);
1627
+ if (event.data.refreshToken) {
1628
+ setRefreshToken(event.data.refreshToken);
1629
+ }
1630
+ const u = await fetchCurrentUser();
1631
+ if (!ignore) {
1632
+ setUser(u);
1633
+ setLoading(false);
1634
+ }
1635
+ };
1636
+ window.addEventListener("message", handleMessage);
1637
+ window.parent.postMessage({ type: "timbal:request-session" }, "*");
1638
+ messageCleanup = () => window.removeEventListener("message", handleMessage);
1639
+ }
1589
1640
  return () => {
1590
1641
  ignore = true;
1642
+ messageCleanup?.();
1591
1643
  };
1592
- }, [enabled]);
1644
+ }, [enabled, embedded]);
1593
1645
  const logout = (0, import_react12.useCallback)(() => {
1594
1646
  clearTokens();
1595
1647
  setUser(null);
@@ -1607,6 +1659,7 @@ var SessionProvider = ({
1607
1659
  user,
1608
1660
  loading,
1609
1661
  isAuthenticated: !!user,
1662
+ isEmbedded: embedded,
1610
1663
  logout
1611
1664
  },
1612
1665
  children
@@ -1622,14 +1675,14 @@ var AuthGuard = ({
1622
1675
  requireAuth = false,
1623
1676
  enabled = true
1624
1677
  }) => {
1625
- const { isAuthenticated, loading } = useSession();
1678
+ const { isAuthenticated, loading, isEmbedded } = useSession();
1626
1679
  if (!enabled) {
1627
1680
  return children;
1628
1681
  }
1629
1682
  if (loading) {
1630
1683
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "flex items-center justify-center h-screen", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_lucide_react6.Loader2, { className: "w-8 h-8 animate-spin" }) });
1631
1684
  }
1632
- if (requireAuth && !isAuthenticated) {
1685
+ if (requireAuth && !isAuthenticated && !isEmbedded) {
1633
1686
  const returnTo = encodeURIComponent(
1634
1687
  window.location.pathname + window.location.search
1635
1688
  );
@@ -1640,6 +1693,7 @@ var AuthGuard = ({
1640
1693
  };
1641
1694
  // Annotate the CommonJS export names for ESM import in node:
1642
1695
  0 && (module.exports = {
1696
+ ActionBarPrimitive,
1643
1697
  AuthGuard,
1644
1698
  Avatar,
1645
1699
  AvatarFallback,
@@ -1647,6 +1701,7 @@ var AuthGuard = ({
1647
1701
  Button,
1648
1702
  ComposerAddAttachment,
1649
1703
  ComposerAttachments,
1704
+ ComposerPrimitive,
1650
1705
  Dialog,
1651
1706
  DialogClose,
1652
1707
  DialogContent,
@@ -1655,10 +1710,12 @@ var AuthGuard = ({
1655
1710
  DialogTitle,
1656
1711
  DialogTrigger,
1657
1712
  MarkdownText,
1713
+ MessagePrimitive,
1658
1714
  SessionProvider,
1659
1715
  Shimmer,
1660
1716
  SyntaxHighlighter,
1661
1717
  Thread,
1718
+ ThreadPrimitive,
1662
1719
  TimbalChat,
1663
1720
  TimbalRuntimeProvider,
1664
1721
  ToolFallback,
@@ -1676,5 +1733,11 @@ var AuthGuard = ({
1676
1733
  getAccessToken,
1677
1734
  getRefreshToken,
1678
1735
  refreshAccessToken,
1679
- useSession
1736
+ setAccessToken,
1737
+ setRefreshToken,
1738
+ useComposerRuntime,
1739
+ useMessageRuntime,
1740
+ useSession,
1741
+ useThread,
1742
+ useThreadRuntime
1680
1743
  });
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
 
@@ -79,6 +102,7 @@ interface SessionContextType {
79
102
  user: Session | null;
80
103
  loading: boolean;
81
104
  isAuthenticated: boolean;
105
+ isEmbedded: boolean;
82
106
  logout: () => void;
83
107
  }
84
108
  declare const useSession: () => SessionContextType;
@@ -98,7 +122,9 @@ interface AuthGuardProps {
98
122
  declare const AuthGuard: React__default.FC<AuthGuardProps>;
99
123
 
100
124
  declare const getAccessToken: () => string | null;
125
+ declare const setAccessToken: (token: string) => void;
101
126
  declare const getRefreshToken: () => string | null;
127
+ declare const setRefreshToken: (token: string) => void;
102
128
  declare const clearTokens: () => void;
103
129
  declare const refreshAccessToken: () => Promise<boolean>;
104
130
  declare const authFetch: (url: string, options?: RequestInit) => Promise<Response>;
@@ -136,4 +162,4 @@ declare const Shimmer: React.MemoExoticComponent<({ children, as: Component, cla
136
162
 
137
163
  declare function cn(...inputs: ClassValue[]): string;
138
164
 
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 };
165
+ 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, setAccessToken, setRefreshToken, 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
 
@@ -79,6 +102,7 @@ interface SessionContextType {
79
102
  user: Session | null;
80
103
  loading: boolean;
81
104
  isAuthenticated: boolean;
105
+ isEmbedded: boolean;
82
106
  logout: () => void;
83
107
  }
84
108
  declare const useSession: () => SessionContextType;
@@ -98,7 +122,9 @@ interface AuthGuardProps {
98
122
  declare const AuthGuard: React__default.FC<AuthGuardProps>;
99
123
 
100
124
  declare const getAccessToken: () => string | null;
125
+ declare const setAccessToken: (token: string) => void;
101
126
  declare const getRefreshToken: () => string | null;
127
+ declare const setRefreshToken: (token: string) => void;
102
128
  declare const clearTokens: () => void;
103
129
  declare const refreshAccessToken: () => Promise<boolean>;
104
130
  declare const authFetch: (url: string, options?: RequestInit) => Promise<Response>;
@@ -136,4 +162,4 @@ declare const Shimmer: React.MemoExoticComponent<({ children, as: Component, cla
136
162
 
137
163
  declare function cn(...inputs: ClassValue[]): string;
138
164
 
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 };
165
+ 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, setAccessToken, setRefreshToken, useSession };
package/dist/index.esm.js CHANGED
@@ -10,7 +10,9 @@ import { parseSSELine } from "@timbal-ai/timbal-sdk";
10
10
  var ACCESS_TOKEN_KEY = "timbal_project_access_token";
11
11
  var REFRESH_TOKEN_KEY = "timbal_project_refresh_token";
12
12
  var getAccessToken = () => localStorage.getItem(ACCESS_TOKEN_KEY);
13
+ var setAccessToken = (token) => localStorage.setItem(ACCESS_TOKEN_KEY, token);
13
14
  var getRefreshToken = () => localStorage.getItem(REFRESH_TOKEN_KEY);
15
+ var setRefreshToken = (token) => localStorage.setItem(REFRESH_TOKEN_KEY, token);
14
16
  var clearTokens = () => {
15
17
  localStorage.removeItem(ACCESS_TOKEN_KEY);
16
18
  localStorage.removeItem(REFRESH_TOKEN_KEY);
@@ -1236,8 +1238,15 @@ var Thread = ({
1236
1238
  maxWidth = "44rem",
1237
1239
  welcome,
1238
1240
  suggestions,
1239
- composerPlaceholder = "Send a message..."
1241
+ composerPlaceholder = "Send a message...",
1242
+ components
1240
1243
  }) => {
1244
+ const WelcomeSlot = components?.Welcome ?? ThreadWelcome;
1245
+ const ComposerSlot = components?.Composer ?? Composer;
1246
+ const UserMessageSlot = components?.UserMessage ?? UserMessage;
1247
+ const AssistantMessageSlot = components?.AssistantMessage ?? AssistantMessage;
1248
+ const EditComposerSlot = components?.EditComposer ?? EditComposer;
1249
+ const ScrollToBottomSlot = components?.ScrollToBottom ?? ThreadScrollToBottom;
1241
1250
  return /* @__PURE__ */ jsx12(
1242
1251
  ThreadPrimitive.Root,
1243
1252
  {
@@ -1252,20 +1261,20 @@ var Thread = ({
1252
1261
  turnAnchor: "bottom",
1253
1262
  className: "aui-thread-viewport relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll px-4 pt-4",
1254
1263
  children: [
1255
- /* @__PURE__ */ jsx12(AuiIf, { condition: (s) => s.thread.isEmpty, children: /* @__PURE__ */ jsx12(ThreadWelcome, { config: welcome, suggestions }) }),
1264
+ /* @__PURE__ */ jsx12(WelcomeSlot, { config: welcome, suggestions }),
1256
1265
  /* @__PURE__ */ jsx12(
1257
1266
  ThreadPrimitive.Messages,
1258
1267
  {
1259
1268
  components: {
1260
- UserMessage,
1261
- EditComposer,
1262
- AssistantMessage
1269
+ UserMessage: UserMessageSlot,
1270
+ EditComposer: EditComposerSlot,
1271
+ AssistantMessage: AssistantMessageSlot
1263
1272
  }
1264
1273
  }
1265
1274
  ),
1266
1275
  /* @__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 })
1276
+ /* @__PURE__ */ jsx12(ScrollToBottomSlot, {}),
1277
+ /* @__PURE__ */ jsx12(ComposerSlot, { placeholder: composerPlaceholder })
1269
1278
  ] })
1270
1279
  ]
1271
1280
  }
@@ -1285,7 +1294,7 @@ var ThreadScrollToBottom = () => {
1285
1294
  ) });
1286
1295
  };
1287
1296
  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: [
1297
+ 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
1298
  /* @__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
1299
  /* @__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
1300
  /* @__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 +1318,7 @@ var ThreadWelcome = ({ config, suggestions }) => {
1309
1318
  /* @__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
1319
  ] }) }),
1311
1320
  suggestions && suggestions.length > 0 && /* @__PURE__ */ jsx12(ThreadSuggestions, { suggestions })
1312
- ] });
1321
+ ] }) });
1313
1322
  };
1314
1323
  var ThreadSuggestions = ({ suggestions }) => {
1315
1324
  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 +1506,18 @@ function TimbalChat({
1497
1506
  return /* @__PURE__ */ jsx13(TimbalRuntimeProvider, { workforceId, baseUrl, fetch: fetch2, children: /* @__PURE__ */ jsx13(Thread, { ...threadProps }) });
1498
1507
  }
1499
1508
 
1509
+ // src/index.ts
1510
+ import {
1511
+ ThreadPrimitive as ThreadPrimitive2,
1512
+ MessagePrimitive as MessagePrimitive3,
1513
+ ComposerPrimitive as ComposerPrimitive3,
1514
+ ActionBarPrimitive as ActionBarPrimitive2,
1515
+ useThread,
1516
+ useThreadRuntime as useThreadRuntime2,
1517
+ useMessageRuntime,
1518
+ useComposerRuntime
1519
+ } from "@assistant-ui/react";
1520
+
1500
1521
  // src/auth/provider.tsx
1501
1522
  import {
1502
1523
  createContext,
@@ -1506,6 +1527,13 @@ import {
1506
1527
  useState as useState5
1507
1528
  } from "react";
1508
1529
  import { jsx as jsx14 } from "react/jsx-runtime";
1530
+ function isInsideIframe() {
1531
+ try {
1532
+ return typeof window !== "undefined" && window.self !== window.top;
1533
+ } catch {
1534
+ return true;
1535
+ }
1536
+ }
1509
1537
  var SessionContext = createContext(void 0);
1510
1538
  var useSession = () => {
1511
1539
  const context = useContext(SessionContext);
@@ -1520,6 +1548,7 @@ var SessionProvider = ({
1520
1548
  }) => {
1521
1549
  const [user, setUser] = useState5(null);
1522
1550
  const [loading, setLoading] = useState5(enabled);
1551
+ const [embedded] = useState5(isInsideIframe);
1523
1552
  useEffect4(() => {
1524
1553
  if (!enabled) {
1525
1554
  setLoading(false);
@@ -1552,13 +1581,35 @@ var SessionProvider = ({
1552
1581
  if (ignore) return;
1553
1582
  clearTokens();
1554
1583
  }
1555
- setLoading(false);
1584
+ if (!ignore && !embedded) {
1585
+ setLoading(false);
1586
+ }
1556
1587
  };
1557
1588
  restoreSession();
1589
+ let messageCleanup;
1590
+ if (embedded) {
1591
+ const handleMessage = async (event) => {
1592
+ if (ignore) return;
1593
+ if (event.data?.type !== "timbal:auth" || !event.data.token) return;
1594
+ setAccessToken(event.data.token);
1595
+ if (event.data.refreshToken) {
1596
+ setRefreshToken(event.data.refreshToken);
1597
+ }
1598
+ const u = await fetchCurrentUser();
1599
+ if (!ignore) {
1600
+ setUser(u);
1601
+ setLoading(false);
1602
+ }
1603
+ };
1604
+ window.addEventListener("message", handleMessage);
1605
+ window.parent.postMessage({ type: "timbal:request-session" }, "*");
1606
+ messageCleanup = () => window.removeEventListener("message", handleMessage);
1607
+ }
1558
1608
  return () => {
1559
1609
  ignore = true;
1610
+ messageCleanup?.();
1560
1611
  };
1561
- }, [enabled]);
1612
+ }, [enabled, embedded]);
1562
1613
  const logout = useCallback2(() => {
1563
1614
  clearTokens();
1564
1615
  setUser(null);
@@ -1576,6 +1627,7 @@ var SessionProvider = ({
1576
1627
  user,
1577
1628
  loading,
1578
1629
  isAuthenticated: !!user,
1630
+ isEmbedded: embedded,
1579
1631
  logout
1580
1632
  },
1581
1633
  children
@@ -1591,14 +1643,14 @@ var AuthGuard = ({
1591
1643
  requireAuth = false,
1592
1644
  enabled = true
1593
1645
  }) => {
1594
- const { isAuthenticated, loading } = useSession();
1646
+ const { isAuthenticated, loading, isEmbedded } = useSession();
1595
1647
  if (!enabled) {
1596
1648
  return children;
1597
1649
  }
1598
1650
  if (loading) {
1599
1651
  return /* @__PURE__ */ jsx15("div", { className: "flex items-center justify-center h-screen", children: /* @__PURE__ */ jsx15(Loader2, { className: "w-8 h-8 animate-spin" }) });
1600
1652
  }
1601
- if (requireAuth && !isAuthenticated) {
1653
+ if (requireAuth && !isAuthenticated && !isEmbedded) {
1602
1654
  const returnTo = encodeURIComponent(
1603
1655
  window.location.pathname + window.location.search
1604
1656
  );
@@ -1608,6 +1660,7 @@ var AuthGuard = ({
1608
1660
  return children;
1609
1661
  };
1610
1662
  export {
1663
+ ActionBarPrimitive2 as ActionBarPrimitive,
1611
1664
  AuthGuard,
1612
1665
  Avatar,
1613
1666
  AvatarFallback,
@@ -1615,6 +1668,7 @@ export {
1615
1668
  Button,
1616
1669
  ComposerAddAttachment,
1617
1670
  ComposerAttachments,
1671
+ ComposerPrimitive3 as ComposerPrimitive,
1618
1672
  Dialog,
1619
1673
  DialogClose,
1620
1674
  DialogContent,
@@ -1623,10 +1677,12 @@ export {
1623
1677
  DialogTitle,
1624
1678
  DialogTrigger,
1625
1679
  MarkdownText,
1680
+ MessagePrimitive3 as MessagePrimitive,
1626
1681
  SessionProvider,
1627
1682
  Shimmer,
1628
1683
  syntax_highlighter_default as SyntaxHighlighter,
1629
1684
  Thread,
1685
+ ThreadPrimitive2 as ThreadPrimitive,
1630
1686
  TimbalChat,
1631
1687
  TimbalRuntimeProvider,
1632
1688
  ToolFallback,
@@ -1644,5 +1700,11 @@ export {
1644
1700
  getAccessToken,
1645
1701
  getRefreshToken,
1646
1702
  refreshAccessToken,
1647
- useSession
1703
+ setAccessToken,
1704
+ setRefreshToken,
1705
+ useComposerRuntime,
1706
+ useMessageRuntime,
1707
+ useSession,
1708
+ useThread,
1709
+ useThreadRuntime2 as useThreadRuntime
1648
1710
  };
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.2",
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",