@assistant-ui/mcp-docs-server 0.1.34 → 0.1.35

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 (64) hide show
  1. package/.docs/organized/code-examples/waterfall.md +4 -4
  2. package/.docs/organized/code-examples/with-a2a.md +5 -5
  3. package/.docs/organized/code-examples/with-ag-ui.md +6 -6
  4. package/.docs/organized/code-examples/with-ai-sdk-v6.md +7 -7
  5. package/.docs/organized/code-examples/with-artifacts.md +7 -7
  6. package/.docs/organized/code-examples/with-assistant-transport.md +5 -5
  7. package/.docs/organized/code-examples/with-browser-extension.md +5 -5
  8. package/.docs/organized/code-examples/with-chain-of-thought.md +8 -8
  9. package/.docs/organized/code-examples/with-cloud-standalone.md +7 -7
  10. package/.docs/organized/code-examples/with-cloud.md +7 -7
  11. package/.docs/organized/code-examples/with-custom-thread-list.md +7 -7
  12. package/.docs/organized/code-examples/with-elevenlabs-conversational.md +9 -9
  13. package/.docs/organized/code-examples/with-elevenlabs-scribe.md +9 -9
  14. package/.docs/organized/code-examples/with-expo.md +43 -17
  15. package/.docs/organized/code-examples/with-external-store.md +5 -5
  16. package/.docs/organized/code-examples/with-ffmpeg.md +7 -7
  17. package/.docs/organized/code-examples/with-generative-ui.md +32 -308
  18. package/.docs/organized/code-examples/with-google-adk.md +5 -5
  19. package/.docs/organized/code-examples/with-heat-graph.md +4 -4
  20. package/.docs/organized/code-examples/with-image-generation.md +7 -7
  21. package/.docs/organized/code-examples/with-interactables.md +7 -7
  22. package/.docs/organized/code-examples/with-langchain.md +7 -7
  23. package/.docs/organized/code-examples/with-langgraph.md +6 -6
  24. package/.docs/organized/code-examples/with-livekit.md +9 -9
  25. package/.docs/organized/code-examples/with-mcp.md +7 -7
  26. package/.docs/organized/code-examples/with-opencode.md +106 -580
  27. package/.docs/organized/code-examples/with-pi.md +2044 -0
  28. package/.docs/organized/code-examples/with-react-hook-form.md +8 -8
  29. package/.docs/organized/code-examples/with-react-ink-web.md +691 -0
  30. package/.docs/organized/code-examples/with-react-ink.md +28 -16
  31. package/.docs/organized/code-examples/with-react-router.md +5 -5
  32. package/.docs/organized/code-examples/with-resumable-stream.md +8 -8
  33. package/.docs/organized/code-examples/with-store.md +14 -10
  34. package/.docs/organized/code-examples/with-tanstack.md +18 -4
  35. package/.docs/organized/code-examples/with-tap-runtime.md +5 -5
  36. package/.docs/organized/code-examples/with-virtualized-thread.md +656 -0
  37. package/.docs/raw/docs/(docs)/architecture.mdx +13 -12
  38. package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +44 -1
  39. package/.docs/raw/docs/(reference)/api-reference/model-context/context.mdx +1 -1
  40. package/.docs/raw/docs/(reference)/api-reference/tools/rendering.mdx +2 -23
  41. package/.docs/raw/docs/(reference)/api-reference/tools/status.mdx +22 -0
  42. package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +0 -7
  43. package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +20 -21
  44. package/.docs/raw/docs/guides/chain-of-thought.mdx +1 -1
  45. package/.docs/raw/docs/guides/index.mdx +3 -0
  46. package/.docs/raw/docs/guides/input-history.mdx +55 -0
  47. package/.docs/raw/docs/guides/virtualization.mdx +63 -0
  48. package/.docs/raw/docs/ink/hooks.mdx +2 -2
  49. package/.docs/raw/docs/react-native/hooks.mdx +1 -1
  50. package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +1 -3
  51. package/.docs/raw/docs/runtimes/concepts/stability.mdx +1 -1
  52. package/.docs/raw/docs/runtimes/custom/data-stream.mdx +1 -1
  53. package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +108 -4
  54. package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +6 -6
  55. package/.docs/raw/docs/tools/tool-ui.mdx +37 -1
  56. package/.docs/raw/docs/ui/dot-matrix.mdx +133 -0
  57. package/.docs/raw/docs/ui/model-selector.mdx +219 -52
  58. package/.docs/raw/docs/ui/number-roll.mdx +154 -0
  59. package/.docs/raw/docs/ui/reasoning.mdx +3 -3
  60. package/.docs/raw/docs/ui/streamdown.mdx +2 -0
  61. package/.docs/raw/docs/ui/syntax-highlighting.mdx +1 -1
  62. package/.docs/raw/docs/ui/thread.mdx +52 -0
  63. package/.docs/raw/docs/ui/tool-fallback.mdx +2 -2
  64. package/package.json +3 -3
@@ -0,0 +1,656 @@
1
+ # Example: with-virtualized-thread
2
+
3
+ ## app/globals.css
4
+
5
+ ```css
6
+ @import "tailwindcss";
7
+ @import "tw-animate-css";
8
+
9
+ @source "../../../packages/ui/src";
10
+
11
+ @custom-variant dark (&:is(.dark *));
12
+
13
+ @theme inline {
14
+ --radius-sm: calc(var(--radius) - 4px);
15
+ --radius-md: calc(var(--radius) - 2px);
16
+ --radius-lg: var(--radius);
17
+ --radius-xl: calc(var(--radius) + 4px);
18
+ --color-background: var(--background);
19
+ --color-foreground: var(--foreground);
20
+ --color-card: var(--card);
21
+ --color-card-foreground: var(--card-foreground);
22
+ --color-popover: var(--popover);
23
+ --color-popover-foreground: var(--popover-foreground);
24
+ --color-primary: var(--primary);
25
+ --color-primary-foreground: var(--primary-foreground);
26
+ --color-secondary: var(--secondary);
27
+ --color-secondary-foreground: var(--secondary-foreground);
28
+ --color-muted: var(--muted);
29
+ --color-muted-foreground: var(--muted-foreground);
30
+ --color-accent: var(--accent);
31
+ --color-accent-foreground: var(--accent-foreground);
32
+ --color-destructive: var(--destructive);
33
+ --color-border: var(--border);
34
+ --color-input: var(--input);
35
+ --color-ring: var(--ring);
36
+ --color-chart-1: var(--chart-1);
37
+ --color-chart-2: var(--chart-2);
38
+ --color-chart-3: var(--chart-3);
39
+ --color-chart-4: var(--chart-4);
40
+ --color-chart-5: var(--chart-5);
41
+ --color-sidebar: var(--sidebar);
42
+ --color-sidebar-foreground: var(--sidebar-foreground);
43
+ --color-sidebar-primary: var(--sidebar-primary);
44
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
45
+ --color-sidebar-accent: var(--sidebar-accent);
46
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
47
+ --color-sidebar-border: var(--sidebar-border);
48
+ --color-sidebar-ring: var(--sidebar-ring);
49
+ }
50
+
51
+ :root {
52
+ --radius: 0.625rem;
53
+ --background: oklch(1 0 0);
54
+ --foreground: oklch(0.141 0.005 285.823);
55
+ --card: oklch(1 0 0);
56
+ --card-foreground: oklch(0.141 0.005 285.823);
57
+ --popover: oklch(1 0 0);
58
+ --popover-foreground: oklch(0.141 0.005 285.823);
59
+ --primary: oklch(0.21 0.006 285.885);
60
+ --primary-foreground: oklch(0.985 0 0);
61
+ --secondary: oklch(0.967 0.001 286.375);
62
+ --secondary-foreground: oklch(0.21 0.006 285.885);
63
+ --muted: oklch(0.967 0.001 286.375);
64
+ --muted-foreground: oklch(0.552 0.016 285.938);
65
+ --accent: oklch(0.967 0.001 286.375);
66
+ --accent-foreground: oklch(0.21 0.006 285.885);
67
+ --destructive: oklch(0.577 0.245 27.325);
68
+ --border: oklch(0.92 0.004 286.32);
69
+ --input: oklch(0.92 0.004 286.32);
70
+ --ring: oklch(0.705 0.015 286.067);
71
+ --chart-1: oklch(0.646 0.222 41.116);
72
+ --chart-2: oklch(0.6 0.118 184.704);
73
+ --chart-3: oklch(0.398 0.07 227.392);
74
+ --chart-4: oklch(0.828 0.189 84.429);
75
+ --chart-5: oklch(0.769 0.188 70.08);
76
+ --sidebar: oklch(0.985 0 0);
77
+ --sidebar-foreground: oklch(0.141 0.005 285.823);
78
+ --sidebar-primary: oklch(0.21 0.006 285.885);
79
+ --sidebar-primary-foreground: oklch(0.985 0 0);
80
+ --sidebar-accent: oklch(0.967 0.001 286.375);
81
+ --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
82
+ --sidebar-border: oklch(0.92 0.004 286.32);
83
+ --sidebar-ring: oklch(0.705 0.015 286.067);
84
+ }
85
+
86
+ .dark {
87
+ --background: oklch(0.141 0.005 285.823);
88
+ --foreground: oklch(0.985 0 0);
89
+ --card: oklch(0.21 0.006 285.885);
90
+ --card-foreground: oklch(0.985 0 0);
91
+ --popover: oklch(0.21 0.006 285.885);
92
+ --popover-foreground: oklch(0.985 0 0);
93
+ --primary: oklch(0.92 0.004 286.32);
94
+ --primary-foreground: oklch(0.21 0.006 285.885);
95
+ --secondary: oklch(0.274 0.006 286.033);
96
+ --secondary-foreground: oklch(0.985 0 0);
97
+ --muted: oklch(0.274 0.006 286.033);
98
+ --muted-foreground: oklch(0.705 0.015 286.067);
99
+ --accent: oklch(0.274 0.006 286.033);
100
+ --accent-foreground: oklch(0.985 0 0);
101
+ --destructive: oklch(0.704 0.191 22.216);
102
+ --border: oklch(1 0 0 / 10%);
103
+ --input: oklch(1 0 0 / 15%);
104
+ --ring: oklch(0.552 0.016 285.938);
105
+ --chart-1: oklch(0.488 0.243 264.376);
106
+ --chart-2: oklch(0.696 0.17 162.48);
107
+ --chart-3: oklch(0.769 0.188 70.08);
108
+ --chart-4: oklch(0.627 0.265 303.9);
109
+ --chart-5: oklch(0.645 0.246 16.439);
110
+ --sidebar: oklch(0.21 0.006 285.885);
111
+ --sidebar-foreground: oklch(0.985 0 0);
112
+ --sidebar-primary: oklch(0.488 0.243 264.376);
113
+ --sidebar-primary-foreground: oklch(0.985 0 0);
114
+ --sidebar-accent: oklch(0.274 0.006 286.033);
115
+ --sidebar-accent-foreground: oklch(0.985 0 0);
116
+ --sidebar-border: oklch(1 0 0 / 10%);
117
+ --sidebar-ring: oklch(0.552 0.016 285.938);
118
+ }
119
+
120
+ @layer base {
121
+ * {
122
+ @apply border-border outline-ring/50;
123
+ }
124
+ body {
125
+ @apply bg-background text-foreground;
126
+ }
127
+ }
128
+
129
+ ```
130
+
131
+ ## app/layout.tsx
132
+
133
+ ```tsx
134
+ import type { Metadata } from "next";
135
+ import { MyRuntimeProvider } from "@/app/MyRuntimeProvider";
136
+
137
+ import "./globals.css";
138
+
139
+ export const metadata: Metadata = {
140
+ title: "assistant-ui virtualized thread",
141
+ description: "Long threads with @tanstack/react-virtual",
142
+ };
143
+
144
+ export default function RootLayout({
145
+ children,
146
+ }: Readonly<{
147
+ children: React.ReactNode;
148
+ }>) {
149
+ return (
150
+ <MyRuntimeProvider>
151
+ <html lang="en" className="h-dvh">
152
+ <body className="h-dvh font-sans">{children}</body>
153
+ </html>
154
+ </MyRuntimeProvider>
155
+ );
156
+ }
157
+
158
+ ```
159
+
160
+ ## app/MyRuntimeProvider.tsx
161
+
162
+ ```tsx
163
+ "use client";
164
+
165
+ import {
166
+ AssistantRuntimeProvider,
167
+ type AppendMessage,
168
+ type ThreadMessageLike,
169
+ useExternalStoreRuntime,
170
+ } from "@assistant-ui/react";
171
+ import { useRef, useState, type ReactNode } from "react";
172
+ import { generateSeedMessages, REPLY_CHUNKS } from "./seed-messages";
173
+
174
+ const convertMessage = (message: ThreadMessageLike) => message;
175
+
176
+ export function MyRuntimeProvider({ children }: { children: ReactNode }) {
177
+ const [messages, setMessages] =
178
+ useState<readonly ThreadMessageLike[]>(generateSeedMessages);
179
+ const [isRunning, setIsRunning] = useState(false);
180
+ const nextIdRef = useRef(0);
181
+
182
+ const onNew = async (message: AppendMessage) => {
183
+ if (message.content[0]?.type !== "text")
184
+ throw new Error("Only text content is supported");
185
+ const userText = message.content[0].text;
186
+ const id = `reply-${nextIdRef.current++}`;
187
+
188
+ setMessages((current) => [
189
+ ...current,
190
+ { id: `${id}-user`, role: "user", content: userText },
191
+ ]);
192
+ setIsRunning(true);
193
+ try {
194
+ for (let i = 1; i <= REPLY_CHUNKS.length; i++) {
195
+ await new Promise((resolve) => setTimeout(resolve, 40));
196
+ const content = REPLY_CHUNKS.slice(0, i).join("");
197
+ setMessages((current) =>
198
+ current.at(-1)?.id === id
199
+ ? [...current.slice(0, -1), { id, role: "assistant", content }]
200
+ : [...current, { id, role: "assistant", content }],
201
+ );
202
+ }
203
+ } finally {
204
+ setIsRunning(false);
205
+ }
206
+ };
207
+
208
+ const runtime = useExternalStoreRuntime<ThreadMessageLike>({
209
+ messages,
210
+ setMessages,
211
+ isRunning,
212
+ onNew,
213
+ convertMessage,
214
+ });
215
+
216
+ return (
217
+ <AssistantRuntimeProvider runtime={runtime}>
218
+ {children}
219
+ </AssistantRuntimeProvider>
220
+ );
221
+ }
222
+
223
+ ```
224
+
225
+ ## app/page.tsx
226
+
227
+ ```tsx
228
+ "use client";
229
+
230
+ import { VirtualizedThread } from "@/app/VirtualizedThread";
231
+
232
+ export default function Home() {
233
+ return (
234
+ <main className="h-dvh">
235
+ <VirtualizedThread />
236
+ </main>
237
+ );
238
+ }
239
+
240
+ ```
241
+
242
+ ## app/seed-messages.ts
243
+
244
+ ```typescript
245
+ import type { ThreadMessageLike } from "@assistant-ui/react";
246
+
247
+ const SEED_COUNT = 500;
248
+
249
+ const SNIPPETS = [
250
+ "Virtualization keeps the DOM small by only mounting the messages near the viewport. Everything else is represented by empty space, so scrolling through thousands of messages stays cheap.",
251
+ "Here is a list of things to keep in mind:\n\n- only mount what is visible\n- measure real heights after mount\n- keep React keys stable across index shifts",
252
+ "```ts\nconst virtualizer = useVirtualizer({\n count: turns.length,\n estimateSize: () => 200,\n getScrollElement: () => scrollerRef.current,\n});\n```",
253
+ "Padding spacers keep the mounted items in normal document flow, which means CSS like `position: sticky` and the regular kit styling keep working inside each message.",
254
+ "Auto-follow works by observing the content size with a ResizeObserver and re-pinning the scroller to the bottom while the user has not scrolled away.",
255
+ ];
256
+
257
+ export const generateSeedMessages = (): ThreadMessageLike[] =>
258
+ Array.from({ length: SEED_COUNT }, (_, i) => ({
259
+ id: `seed-${i}`,
260
+ role: i % 2 === 0 ? ("user" as const) : ("assistant" as const),
261
+ content:
262
+ i % 2 === 0
263
+ ? `Question ${i / 2 + 1}: tell me more about long threads.`
264
+ : SNIPPETS.slice(0, (i % 3) + 1).join("\n\n"),
265
+ }));
266
+
267
+ export const REPLY_CHUNKS =
268
+ "Sure! This reply is streamed chunk by chunk so you can watch the thread follow the newest tokens. Scroll up at any point and the view stops following until you return to the bottom or press the scroll to bottom button. The reply is intentionally long enough to grow past a single viewport so the auto-follow behavior is easy to observe while it streams in."
269
+ .split(" ")
270
+ .map((word) => `${word} `);
271
+
272
+ ```
273
+
274
+ ## app/VirtualizedThread.tsx
275
+
276
+ ```tsx
277
+ "use client";
278
+
279
+ import { MarkdownText } from "@/components/assistant-ui/markdown-text";
280
+ import {
281
+ ComposerPrimitive,
282
+ MessagePrimitive,
283
+ ThreadPrimitive,
284
+ useAuiState,
285
+ } from "@assistant-ui/react";
286
+ import { useVirtualizer } from "@tanstack/react-virtual";
287
+ import { ArrowDownIcon, SendHorizontalIcon } from "lucide-react";
288
+ import {
289
+ type ComponentProps,
290
+ type FC,
291
+ useCallback,
292
+ useEffect,
293
+ useLayoutEffect,
294
+ useMemo,
295
+ useRef,
296
+ useState,
297
+ } from "react";
298
+
299
+ const ESTIMATED_TURN_HEIGHT = 200;
300
+ const AT_BOTTOM_THRESHOLD = 4;
301
+
302
+ type MessageComponents = ComponentProps<
303
+ typeof ThreadPrimitive.MessageByIndex
304
+ >["components"];
305
+
306
+ type Turn = { id: string; indices: number[] };
307
+
308
+ const buildTurns = (signature: string): Turn[] => {
309
+ if (!signature) return [];
310
+ const turns: Turn[] = [];
311
+ for (const row of signature.split("\n")) {
312
+ const firstColon = row.indexOf(":");
313
+ const secondColon = row.indexOf(":", firstColon + 1);
314
+ const index = Number(row.slice(0, firstColon));
315
+ const role = row.slice(firstColon + 1, secondColon);
316
+ const id = row.slice(secondColon + 1);
317
+ const last = turns.at(-1);
318
+ if (role === "user" || !last) turns.push({ id, indices: [index] });
319
+ else last.indices.push(index);
320
+ }
321
+ return turns;
322
+ };
323
+
324
+ const UserMessage: FC = () => (
325
+ <MessagePrimitive.Root
326
+ data-role="user"
327
+ className="bg-muted ml-auto w-fit max-w-[80%] rounded-xl px-4 py-2"
328
+ >
329
+ <MessagePrimitive.Parts />
330
+ </MessagePrimitive.Root>
331
+ );
332
+
333
+ const AssistantMessage: FC = () => (
334
+ <MessagePrimitive.Root
335
+ data-role="assistant"
336
+ className="text-foreground leading-relaxed"
337
+ >
338
+ <MessagePrimitive.Parts components={{ Text: MarkdownText }} />
339
+ </MessagePrimitive.Root>
340
+ );
341
+
342
+ const MESSAGE_COMPONENTS: MessageComponents = { UserMessage, AssistantMessage };
343
+
344
+ const Composer: FC = () => (
345
+ <ComposerPrimitive.Root className="border-border bg-background focus-within:ring-ring flex items-end rounded-xl border shadow-sm focus-within:ring-1">
346
+ <ComposerPrimitive.Input
347
+ placeholder="Send a message to watch the thread follow the stream"
348
+ className="placeholder:text-muted-foreground max-h-40 flex-1 resize-none bg-transparent px-4 py-3 text-sm outline-none"
349
+ rows={1}
350
+ autoFocus
351
+ />
352
+ <ComposerPrimitive.Send
353
+ aria-label="Send"
354
+ className="text-muted-foreground hover:text-foreground m-2 rounded-lg p-2 transition-colors disabled:opacity-40"
355
+ >
356
+ <SendHorizontalIcon className="size-4" />
357
+ </ComposerPrimitive.Send>
358
+ </ComposerPrimitive.Root>
359
+ );
360
+
361
+ export const VirtualizedThread: FC = () => {
362
+ const signature = useAuiState((s) =>
363
+ s.thread.messages.map((m, i) => `${i}:${m.role}:${m.id}`).join("\n"),
364
+ );
365
+ const isRunning = useAuiState((s) => s.thread.isRunning);
366
+ const turns = useMemo(() => buildTurns(signature), [signature]);
367
+
368
+ const scrollerRef = useRef<HTMLDivElement>(null);
369
+ const contentRef = useRef<HTMLDivElement>(null);
370
+ const stickyRef = useRef(true);
371
+ const [isAtBottom, setIsAtBottom] = useState(true);
372
+
373
+ const virtualizer = useVirtualizer({
374
+ count: turns.length,
375
+ estimateSize: () => ESTIMATED_TURN_HEIGHT,
376
+ getItemKey: (index) => turns[index]!.id,
377
+ getScrollElement: () => scrollerRef.current,
378
+ initialRect: { height: 800, width: 800 },
379
+ overscan: 4,
380
+ scrollToFn: (offset, _options, instance) => {
381
+ const el = instance.scrollElement;
382
+ if (!el) return;
383
+ if (stickyRef.current) {
384
+ const maxScroll = el.scrollHeight - el.clientHeight;
385
+ if (
386
+ maxScroll - el.scrollTop <= AT_BOTTOM_THRESHOLD &&
387
+ offset < maxScroll
388
+ )
389
+ return;
390
+ }
391
+ el.scrollTo(0, offset);
392
+ },
393
+ });
394
+
395
+ const jumpToBottom = useCallback(() => {
396
+ stickyRef.current = true;
397
+ if (turns.length > 0)
398
+ virtualizer.scrollToIndex(turns.length - 1, { align: "end" });
399
+ requestAnimationFrame(() => {
400
+ const el = scrollerRef.current;
401
+ if (el && stickyRef.current) el.scrollTop = el.scrollHeight;
402
+ });
403
+ }, [turns.length, virtualizer]);
404
+
405
+ useEffect(() => {
406
+ const el = scrollerRef.current;
407
+ if (!el) return;
408
+ let lastScrollTop = el.scrollTop;
409
+ let lastScrollHeight = el.scrollHeight;
410
+ let lastClientHeight = el.clientHeight;
411
+ const onScroll = () => {
412
+ const atBottom =
413
+ el.scrollHeight - el.scrollTop - el.clientHeight <= AT_BOTTOM_THRESHOLD;
414
+ if (atBottom) {
415
+ stickyRef.current = true;
416
+ } else if (
417
+ el.scrollTop < lastScrollTop &&
418
+ el.scrollHeight === lastScrollHeight &&
419
+ Math.abs(el.clientHeight - lastClientHeight) <= 1
420
+ ) {
421
+ stickyRef.current = false;
422
+ }
423
+ lastScrollTop = el.scrollTop;
424
+ lastScrollHeight = el.scrollHeight;
425
+ lastClientHeight = el.clientHeight;
426
+ setIsAtBottom(atBottom);
427
+ };
428
+ const onWheel = (e: WheelEvent) => {
429
+ if (e.deltaY < 0) stickyRef.current = false;
430
+ };
431
+ const disarm = () => {
432
+ stickyRef.current = false;
433
+ };
434
+ el.addEventListener("scroll", onScroll, { passive: true });
435
+ el.addEventListener("wheel", onWheel, { passive: true });
436
+ el.addEventListener("touchmove", disarm, { passive: true });
437
+ return () => {
438
+ el.removeEventListener("scroll", onScroll);
439
+ el.removeEventListener("wheel", onWheel);
440
+ el.removeEventListener("touchmove", disarm);
441
+ };
442
+ }, []);
443
+
444
+ useEffect(() => {
445
+ const el = scrollerRef.current;
446
+ const content = contentRef.current;
447
+ if (!el || !content) return;
448
+ const observer = new ResizeObserver(() => {
449
+ if (stickyRef.current) el.scrollTop = el.scrollHeight;
450
+ });
451
+ observer.observe(content);
452
+ return () => observer.disconnect();
453
+ }, []);
454
+
455
+ const prevIsRunningRef = useRef(false);
456
+ useLayoutEffect(() => {
457
+ if (isRunning && !prevIsRunningRef.current) jumpToBottom();
458
+ prevIsRunningRef.current = isRunning;
459
+ }, [isRunning, jumpToBottom]);
460
+
461
+ const didInitialJumpRef = useRef(false);
462
+ useLayoutEffect(() => {
463
+ if (didInitialJumpRef.current || turns.length === 0) return;
464
+ didInitialJumpRef.current = true;
465
+ jumpToBottom();
466
+ }, [turns.length, jumpToBottom]);
467
+
468
+ const items = virtualizer.getVirtualItems();
469
+ const paddingTop = items[0]?.start ?? 0;
470
+ const paddingBottom = Math.max(
471
+ 0,
472
+ virtualizer.getTotalSize() - (items.at(-1)?.end ?? 0),
473
+ );
474
+
475
+ return (
476
+ <ThreadPrimitive.Root className="bg-background flex h-full flex-col">
477
+ <div
478
+ ref={scrollerRef}
479
+ className="flex-1 overflow-y-auto overscroll-contain"
480
+ >
481
+ <div ref={contentRef} className="mx-auto w-full max-w-3xl px-4 pt-4">
482
+ <div style={{ paddingTop, paddingBottom }}>
483
+ {items.map((item) => (
484
+ <div
485
+ key={item.key}
486
+ data-index={item.index}
487
+ ref={virtualizer.measureElement}
488
+ className="flex flex-col gap-4 py-3"
489
+ >
490
+ {turns[item.index]!.indices.map((index) => (
491
+ <ThreadPrimitive.MessageByIndex
492
+ key={index}
493
+ index={index}
494
+ components={MESSAGE_COMPONENTS}
495
+ />
496
+ ))}
497
+ </div>
498
+ ))}
499
+ </div>
500
+ </div>
501
+ </div>
502
+ <div className="relative mx-auto w-full max-w-3xl px-4 pb-4">
503
+ {!isAtBottom && (
504
+ <button
505
+ type="button"
506
+ onClick={jumpToBottom}
507
+ aria-label="Scroll to bottom"
508
+ className="border-border bg-background hover:bg-muted absolute -top-12 left-1/2 -translate-x-1/2 rounded-full border p-2 shadow-sm transition-colors"
509
+ >
510
+ <ArrowDownIcon className="size-4" />
511
+ </button>
512
+ )}
513
+ <Composer />
514
+ </div>
515
+ </ThreadPrimitive.Root>
516
+ );
517
+ };
518
+
519
+ ```
520
+
521
+ ## components.json
522
+
523
+ ```json
524
+ {
525
+ "$schema": "https://ui.shadcn.com/schema.json",
526
+ "style": "new-york",
527
+ "rsc": true,
528
+ "tsx": true,
529
+ "tailwind": {
530
+ "config": "",
531
+ "css": "app/globals.css",
532
+ "baseColor": "zinc",
533
+ "cssVariables": true,
534
+ "prefix": ""
535
+ },
536
+ "aliases": {
537
+ "components": "@/components",
538
+ "utils": "@/lib/utils",
539
+ "ui": "@/components/ui",
540
+ "lib": "@/lib",
541
+ "hooks": "@/hooks"
542
+ },
543
+ "iconLibrary": "lucide",
544
+ "registries": {
545
+ "@assistant-ui": "https://r.assistant-ui.com/{name}.json"
546
+ }
547
+ }
548
+
549
+ ```
550
+
551
+ ## next.config.ts
552
+
553
+ ```typescript
554
+ import { withAui } from "@assistant-ui/next";
555
+ import type { NextConfig } from "next";
556
+
557
+ const nextConfig: NextConfig = {
558
+ /* config options here */
559
+ };
560
+
561
+ export default withAui(nextConfig);
562
+
563
+ ```
564
+
565
+ ## package.json
566
+
567
+ ```json
568
+ {
569
+ "name": "with-virtualized-thread",
570
+ "version": "0.0.0",
571
+ "private": true,
572
+ "type": "module",
573
+ "scripts": {
574
+ "dev": "next dev",
575
+ "build": "next build",
576
+ "start": "next start"
577
+ },
578
+ "dependencies": {
579
+ "@assistant-ui/react": "workspace:*",
580
+ "@assistant-ui/react-markdown": "workspace:*",
581
+ "@assistant-ui/ui": "workspace:*",
582
+ "@tanstack/react-virtual": "^3.14.2",
583
+ "class-variance-authority": "^0.7.1",
584
+ "clsx": "^2.1.1",
585
+ "lucide-react": "^1.18.0",
586
+ "next": "^16.2.9",
587
+ "react": "^19.2.7",
588
+ "react-dom": "^19.2.7",
589
+ "tailwind-merge": "^3.6.0"
590
+ },
591
+ "devDependencies": {
592
+ "@assistant-ui/next": "workspace:*",
593
+ "@assistant-ui/x-buildutils": "workspace:*",
594
+ "@tailwindcss/postcss": "^4.3.1",
595
+ "@types/node": "^25.9.3",
596
+ "@types/react": "^19.2.17",
597
+ "@types/react-dom": "^19.2.3",
598
+ "postcss": "^8.5.15",
599
+ "tailwindcss": "^4.3.1",
600
+ "tw-animate-css": "^1.4.0",
601
+ "typescript": "^6.0.3"
602
+ }
603
+ }
604
+
605
+ ```
606
+
607
+ ## README.md
608
+
609
+ ```markdown
610
+ # Virtualized thread example
611
+
612
+ Renders a 500-message thread with `@tanstack/react-virtual` so only the turns near the viewport are mounted. The runtime is a self-contained external store with a fake streaming reply, so the example runs fully offline with no environment variables.
613
+
614
+ ## Quick start
615
+
616
+ ```sh
617
+ pnpm install
618
+ pnpm --filter with-virtualized-thread dev
619
+ ```
620
+
621
+ ## What the example demonstrates
622
+
623
+ | Concern | File |
624
+ | --- | --- |
625
+ | Virtualizer over user turns, padding spacers, `measureElement` | `app/VirtualizedThread.tsx` |
626
+ | Per-message rendering through `ThreadPrimitive.MessageByIndex` | `app/VirtualizedThread.tsx` |
627
+ | Sticky-bottom auto-follow with a user-scroll disarm guard | `app/VirtualizedThread.tsx` |
628
+ | External store runtime with seeded messages and a streaming tail | `app/MyRuntimeProvider.tsx` |
629
+ | Deterministic synthetic thread content | `app/seed-messages.ts` |
630
+
631
+ See the [thread virtualization guide](https://assistant-ui.com/docs/guides/virtualization) for the full walkthrough.
632
+
633
+ ```
634
+
635
+ ## tsconfig.json
636
+
637
+ ```json
638
+ {
639
+ "extends": "@assistant-ui/x-buildutils/ts/next",
640
+ "compilerOptions": {
641
+ "paths": {
642
+ "@/*": ["./*"],
643
+ "@/components/assistant-ui/*": [
644
+ "../../packages/ui/src/components/assistant-ui/*"
645
+ ],
646
+ "@/components/ui/*": ["../../packages/ui/src/components/ui/*"],
647
+ "@/lib/utils": ["../../packages/ui/src/lib/utils"],
648
+ "@assistant-ui/ui/*": ["../../packages/ui/src/*"]
649
+ }
650
+ },
651
+ "include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
652
+ "exclude": ["node_modules"]
653
+ }
654
+
655
+ ```
656
+
@@ -7,18 +7,19 @@ import { Sparkles, PanelsTopLeft, Database, Terminal } from "lucide-react";
7
7
 
8
8
  ## assistant-ui is built on these main pillars:
9
9
 
10
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
11
- <Card title='1. Frontend components'>
12
- Shadcn UI chat components with built-in state management
13
- </Card>
14
-
15
- <Card title='2. Runtime'>
16
- State management layer connecting UI to LLMs and backend services
17
- </Card>
18
-
19
- <Card title='3. Assistant Cloud'>
20
- Hosted service for thread persistence, history, and user management
21
- </Card>
10
+ <div className="not-prose grid grid-cols-1 gap-4 md:grid-cols-3">
11
+ <Card
12
+ title="1. Frontend components"
13
+ description="Shadcn UI chat components with built-in state management"
14
+ />
15
+ <Card
16
+ title="2. Runtime"
17
+ description="State management layer connecting UI to LLMs and backend services"
18
+ />
19
+ <Card
20
+ title="3. Assistant Cloud"
21
+ description="Hosted service for thread persistence, history, and user management"
22
+ />
22
23
  </div>
23
24
 
24
25