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

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 (127) 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 +73 -57
  7. package/.docs/organized/code-examples/with-browser-extension.md +7 -7
  8. package/.docs/organized/code-examples/with-chain-of-thought.md +44 -93
  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-eve.md +343 -0
  15. package/.docs/organized/code-examples/with-expo.md +943 -940
  16. package/.docs/organized/code-examples/with-external-store.md +5 -5
  17. package/.docs/organized/code-examples/with-ffmpeg.md +8 -11
  18. package/.docs/organized/code-examples/with-generative-ui.md +33 -309
  19. package/.docs/organized/code-examples/with-google-adk.md +5 -5
  20. package/.docs/organized/code-examples/with-heat-graph.md +4 -4
  21. package/.docs/organized/code-examples/with-image-generation.md +7 -7
  22. package/.docs/organized/code-examples/with-interactables.md +169 -341
  23. package/.docs/organized/code-examples/with-langchain.md +7 -7
  24. package/.docs/organized/code-examples/with-langgraph.md +23 -160
  25. package/.docs/organized/code-examples/with-livekit.md +10 -10
  26. package/.docs/organized/code-examples/with-mcp.md +7 -7
  27. package/.docs/organized/code-examples/with-opencode.md +106 -580
  28. package/.docs/organized/code-examples/with-pi.md +2046 -0
  29. package/.docs/organized/code-examples/with-react-hook-form.md +16 -9
  30. package/.docs/organized/code-examples/with-react-ink-web.md +691 -0
  31. package/.docs/organized/code-examples/with-react-ink.md +29 -17
  32. package/.docs/organized/code-examples/with-react-router.md +12 -12
  33. package/.docs/organized/code-examples/with-resumable-stream.md +8 -8
  34. package/.docs/organized/code-examples/with-store.md +14 -10
  35. package/.docs/organized/code-examples/with-tanstack.md +21 -7
  36. package/.docs/organized/code-examples/with-tap-runtime.md +5 -5
  37. package/.docs/organized/code-examples/with-virtualized-thread.md +676 -0
  38. package/.docs/raw/docs/(docs)/architecture.mdx +13 -12
  39. package/.docs/raw/docs/(docs)/cli.mdx +4 -2
  40. package/.docs/raw/docs/(docs)/devtools.mdx +25 -2
  41. package/.docs/raw/docs/(docs)/index.mdx +5 -2
  42. package/.docs/raw/docs/(docs)/installation.mdx +5 -2
  43. package/.docs/raw/docs/(reference)/api-reference/external-store/message-conversion.mdx +16 -16
  44. package/.docs/raw/docs/(reference)/api-reference/hooks/composer-triggers.mdx +73 -42
  45. package/.docs/raw/docs/(reference)/api-reference/hooks/model-context.mdx +3 -20
  46. package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +234 -123
  47. package/.docs/raw/docs/(reference)/api-reference/integrations/eve.mdx +51 -0
  48. package/.docs/raw/docs/(reference)/api-reference/integrations/index.mdx +4 -1
  49. package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +70 -38
  50. package/.docs/raw/docs/(reference)/api-reference/model-context/context.mdx +1 -1
  51. package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar.mdx +30 -0
  52. package/.docs/raw/docs/(reference)/api-reference/primitives/attachment.mdx +7 -0
  53. package/.docs/raw/docs/(reference)/api-reference/primitives/branch-picker.mdx +26 -0
  54. package/.docs/raw/docs/(reference)/api-reference/primitives/chain-of-thought.mdx +15 -0
  55. package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +103 -0
  56. package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +16 -0
  57. package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +56 -0
  58. package/.docs/raw/docs/(reference)/api-reference/primitives/queue-item.mdx +12 -0
  59. package/.docs/raw/docs/(reference)/api-reference/primitives/selection-toolbar.mdx +12 -0
  60. package/.docs/raw/docs/(reference)/api-reference/primitives/suggestion.mdx +14 -0
  61. package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +57 -1
  62. package/.docs/raw/docs/(reference)/api-reference/tools/index.mdx +6 -0
  63. package/.docs/raw/docs/(reference)/api-reference/tools/interactables-legacy.mdx +55 -0
  64. package/.docs/raw/docs/(reference)/api-reference/tools/interactables.mdx +151 -0
  65. package/.docs/raw/docs/(reference)/api-reference/tools/rendering.mdx +17 -38
  66. package/.docs/raw/docs/(reference)/api-reference/tools/status.mdx +22 -0
  67. package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +20 -27
  68. package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +40 -25
  69. package/.docs/raw/docs/guides/chain-of-thought.mdx +1 -1
  70. package/.docs/raw/docs/guides/headless-composer-input.mdx +113 -0
  71. package/.docs/raw/docs/guides/index.mdx +3 -0
  72. package/.docs/raw/docs/guides/input-history.mdx +55 -0
  73. package/.docs/raw/docs/guides/latex.mdx +28 -22
  74. package/.docs/raw/docs/guides/mentions.mdx +32 -7
  75. package/.docs/raw/docs/guides/slash-commands.mdx +3 -6
  76. package/.docs/raw/docs/guides/speech.mdx +5 -7
  77. package/.docs/raw/docs/guides/virtualization.mdx +133 -0
  78. package/.docs/raw/docs/guides/voice.mdx +3 -2
  79. package/.docs/raw/docs/ink/hooks.mdx +2 -2
  80. package/.docs/raw/docs/integrations/attachments/custom-adapter.mdx +4 -12
  81. package/.docs/raw/docs/integrations/auth/better-auth.mdx +7 -10
  82. package/.docs/raw/docs/integrations/auth/clerk.mdx +3 -9
  83. package/.docs/raw/docs/integrations/auth/next-auth.mdx +3 -8
  84. package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +8 -5
  85. package/.docs/raw/docs/integrations/index.mdx +5 -12
  86. package/.docs/raw/docs/integrations/observability/helicone.mdx +3 -4
  87. package/.docs/raw/docs/integrations/observability/langfuse.mdx +3 -5
  88. package/.docs/raw/docs/integrations/observability/langsmith.mdx +3 -2
  89. package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +4 -3
  90. package/.docs/raw/docs/primitives/composer.mdx +8 -0
  91. package/.docs/raw/docs/primitives/thread.mdx +24 -0
  92. package/.docs/raw/docs/react-native/hooks.mdx +1 -1
  93. package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +22 -3
  94. package/.docs/raw/docs/runtimes/concepts/stability.mdx +1 -1
  95. package/.docs/raw/docs/runtimes/custom/data-stream.mdx +1 -1
  96. package/.docs/raw/docs/runtimes/custom/external-store.mdx +54 -0
  97. package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +108 -4
  98. package/.docs/raw/docs/runtimes/eve/overview.mdx +100 -0
  99. package/.docs/raw/docs/runtimes/eve/quickstart.mdx +143 -0
  100. package/.docs/raw/docs/runtimes/langchain.mdx +386 -18
  101. package/.docs/raw/docs/runtimes/langgraph/quickstart.mdx +1 -1
  102. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-1.mdx +1 -1
  103. package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +12 -6
  104. package/.docs/raw/docs/tools/defining-tools.mdx +4 -0
  105. package/.docs/raw/docs/tools/interactables-legacy.mdx +410 -0
  106. package/.docs/raw/docs/tools/interactables.mdx +892 -223
  107. package/.docs/raw/docs/tools/mcp.mdx +4 -4
  108. package/.docs/raw/docs/tools/multi-agent.mdx +5 -0
  109. package/.docs/raw/docs/tools/tool-ui.mdx +37 -1
  110. package/.docs/raw/docs/ui/composer-trigger-popover.mdx +2 -0
  111. package/.docs/raw/docs/ui/dot-matrix.mdx +133 -0
  112. package/.docs/raw/docs/ui/file.mdx +1 -1
  113. package/.docs/raw/docs/ui/model-selector.mdx +219 -52
  114. package/.docs/raw/docs/ui/number-roll.mdx +154 -0
  115. package/.docs/raw/docs/ui/part-grouping.mdx +38 -0
  116. package/.docs/raw/docs/ui/reasoning.mdx +3 -3
  117. package/.docs/raw/docs/ui/streamdown.mdx +2 -0
  118. package/.docs/raw/docs/ui/syntax-highlighting.mdx +1 -1
  119. package/.docs/raw/docs/ui/thread.mdx +52 -0
  120. package/.docs/raw/docs/ui/tool-fallback.mdx +2 -2
  121. package/.docs/raw/docs/utilities/react-o11y.mdx +92 -6
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +18 -2
  124. package/dist/index.js.map +1 -1
  125. package/package.json +4 -4
  126. package/src/index.ts +14 -6
  127. package/src/tools/tests/mcp-protocol.test.ts +9 -0
@@ -0,0 +1,676 @@
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.Unstable_MessageById
304
+ >["components"];
305
+
306
+ type MessageRow = {
307
+ id: string;
308
+ role: "user" | "assistant" | "system";
309
+ };
310
+
311
+ type Turn = { id: string; messageIds: string[] };
312
+
313
+ const useThreadMessageRows = (): readonly MessageRow[] => {
314
+ const prevRowsRef = useRef<readonly MessageRow[]>([]);
315
+
316
+ return useAuiState((s) => {
317
+ const messages = s.thread.messages;
318
+ const prev = prevRowsRef.current;
319
+ if (
320
+ prev.length === messages.length &&
321
+ prev.every((row, index) => {
322
+ const message = messages[index]!;
323
+ return row.id === message.id && row.role === message.role;
324
+ })
325
+ ) {
326
+ return prev;
327
+ }
328
+
329
+ const next = messages.map(({ id, role }) => ({ id, role }));
330
+ prevRowsRef.current = next;
331
+ return next;
332
+ });
333
+ };
334
+
335
+ const buildTurns = (messages: readonly MessageRow[]): Turn[] => {
336
+ if (messages.length === 0) return [];
337
+ const turns: Turn[] = [];
338
+ for (const { id, role } of messages) {
339
+ const last = turns.at(-1);
340
+ if (role === "user" || !last) turns.push({ id, messageIds: [id] });
341
+ else last.messageIds.push(id);
342
+ }
343
+ return turns;
344
+ };
345
+
346
+ const UserMessage: FC = () => (
347
+ <MessagePrimitive.Root
348
+ data-role="user"
349
+ className="bg-muted ml-auto w-fit max-w-[80%] rounded-xl px-4 py-2"
350
+ >
351
+ <MessagePrimitive.Parts />
352
+ </MessagePrimitive.Root>
353
+ );
354
+
355
+ const AssistantMessage: FC = () => (
356
+ <MessagePrimitive.Root
357
+ data-role="assistant"
358
+ className="text-foreground leading-relaxed"
359
+ >
360
+ <MessagePrimitive.Parts components={{ Text: MarkdownText }} />
361
+ </MessagePrimitive.Root>
362
+ );
363
+
364
+ const MESSAGE_COMPONENTS: MessageComponents = { UserMessage, AssistantMessage };
365
+
366
+ const Composer: FC = () => (
367
+ <ComposerPrimitive.Root className="border-border bg-background focus-within:ring-ring flex items-end rounded-xl border shadow-sm focus-within:ring-1">
368
+ <ComposerPrimitive.Input
369
+ placeholder="Send a message to watch the thread follow the stream"
370
+ className="placeholder:text-muted-foreground max-h-40 flex-1 resize-none bg-transparent px-4 py-3 text-sm outline-none"
371
+ rows={1}
372
+ autoFocus
373
+ />
374
+ <ComposerPrimitive.Send
375
+ aria-label="Send"
376
+ className="text-muted-foreground hover:text-foreground m-2 rounded-lg p-2 transition-colors disabled:opacity-40"
377
+ >
378
+ <SendHorizontalIcon className="size-4" />
379
+ </ComposerPrimitive.Send>
380
+ </ComposerPrimitive.Root>
381
+ );
382
+
383
+ export const VirtualizedThread: FC = () => {
384
+ const messages = useThreadMessageRows();
385
+ const isRunning = useAuiState((s) => s.thread.isRunning);
386
+ const turns = useMemo(() => buildTurns(messages), [messages]);
387
+
388
+ const scrollerRef = useRef<HTMLDivElement>(null);
389
+ const contentRef = useRef<HTMLDivElement>(null);
390
+ const stickyRef = useRef(true);
391
+ const [isAtBottom, setIsAtBottom] = useState(true);
392
+
393
+ const virtualizer = useVirtualizer({
394
+ count: turns.length,
395
+ estimateSize: () => ESTIMATED_TURN_HEIGHT,
396
+ getItemKey: (index) => turns[index]!.id,
397
+ getScrollElement: () => scrollerRef.current,
398
+ initialRect: { height: 800, width: 800 },
399
+ overscan: 4,
400
+ scrollToFn: (offset, _options, instance) => {
401
+ const el = instance.scrollElement;
402
+ if (!el) return;
403
+ if (stickyRef.current) {
404
+ const maxScroll = el.scrollHeight - el.clientHeight;
405
+ if (
406
+ maxScroll - el.scrollTop <= AT_BOTTOM_THRESHOLD &&
407
+ offset < maxScroll
408
+ )
409
+ return;
410
+ }
411
+ el.scrollTo(0, offset);
412
+ },
413
+ });
414
+
415
+ const jumpToBottom = useCallback(() => {
416
+ stickyRef.current = true;
417
+ if (turns.length > 0)
418
+ virtualizer.scrollToIndex(turns.length - 1, { align: "end" });
419
+ requestAnimationFrame(() => {
420
+ const el = scrollerRef.current;
421
+ if (el && stickyRef.current) el.scrollTop = el.scrollHeight;
422
+ });
423
+ }, [turns.length, virtualizer]);
424
+
425
+ useEffect(() => {
426
+ const el = scrollerRef.current;
427
+ if (!el) return;
428
+ let lastScrollTop = el.scrollTop;
429
+ let lastScrollHeight = el.scrollHeight;
430
+ let lastClientHeight = el.clientHeight;
431
+ const onScroll = () => {
432
+ const atBottom =
433
+ el.scrollHeight - el.scrollTop - el.clientHeight <= AT_BOTTOM_THRESHOLD;
434
+ if (atBottom) {
435
+ stickyRef.current = true;
436
+ } else if (
437
+ el.scrollTop < lastScrollTop &&
438
+ el.scrollHeight === lastScrollHeight &&
439
+ Math.abs(el.clientHeight - lastClientHeight) <= 1
440
+ ) {
441
+ stickyRef.current = false;
442
+ }
443
+ lastScrollTop = el.scrollTop;
444
+ lastScrollHeight = el.scrollHeight;
445
+ lastClientHeight = el.clientHeight;
446
+ setIsAtBottom(atBottom);
447
+ };
448
+ const onWheel = (e: WheelEvent) => {
449
+ if (e.deltaY < 0) stickyRef.current = false;
450
+ };
451
+ const disarm = () => {
452
+ stickyRef.current = false;
453
+ };
454
+ el.addEventListener("scroll", onScroll, { passive: true });
455
+ el.addEventListener("wheel", onWheel, { passive: true });
456
+ el.addEventListener("touchmove", disarm, { passive: true });
457
+ return () => {
458
+ el.removeEventListener("scroll", onScroll);
459
+ el.removeEventListener("wheel", onWheel);
460
+ el.removeEventListener("touchmove", disarm);
461
+ };
462
+ }, []);
463
+
464
+ useEffect(() => {
465
+ const el = scrollerRef.current;
466
+ const content = contentRef.current;
467
+ if (!el || !content) return;
468
+ const observer = new ResizeObserver(() => {
469
+ if (stickyRef.current) el.scrollTop = el.scrollHeight;
470
+ });
471
+ observer.observe(content);
472
+ return () => observer.disconnect();
473
+ }, []);
474
+
475
+ const prevIsRunningRef = useRef(false);
476
+ useLayoutEffect(() => {
477
+ if (isRunning && !prevIsRunningRef.current) jumpToBottom();
478
+ prevIsRunningRef.current = isRunning;
479
+ }, [isRunning, jumpToBottom]);
480
+
481
+ const didInitialJumpRef = useRef(false);
482
+ useLayoutEffect(() => {
483
+ if (didInitialJumpRef.current || turns.length === 0) return;
484
+ didInitialJumpRef.current = true;
485
+ jumpToBottom();
486
+ }, [turns.length, jumpToBottom]);
487
+
488
+ const items = virtualizer.getVirtualItems();
489
+ const paddingTop = items[0]?.start ?? 0;
490
+ const paddingBottom = Math.max(
491
+ 0,
492
+ virtualizer.getTotalSize() - (items.at(-1)?.end ?? 0),
493
+ );
494
+
495
+ return (
496
+ <ThreadPrimitive.Root className="bg-background flex h-full flex-col">
497
+ <div
498
+ ref={scrollerRef}
499
+ className="flex-1 overflow-y-auto overscroll-contain"
500
+ >
501
+ <div ref={contentRef} className="mx-auto w-full max-w-3xl px-4 pt-4">
502
+ <div style={{ paddingTop, paddingBottom }}>
503
+ {items.map((item) => (
504
+ <div
505
+ key={item.key}
506
+ data-index={item.index}
507
+ ref={virtualizer.measureElement}
508
+ className="flex flex-col gap-4 py-3"
509
+ >
510
+ {turns[item.index]!.messageIds.map((messageId) => (
511
+ <ThreadPrimitive.Unstable_MessageById
512
+ key={messageId}
513
+ messageId={messageId}
514
+ components={MESSAGE_COMPONENTS}
515
+ />
516
+ ))}
517
+ </div>
518
+ ))}
519
+ </div>
520
+ </div>
521
+ </div>
522
+ <div className="relative mx-auto w-full max-w-3xl px-4 pb-4">
523
+ {!isAtBottom && (
524
+ <button
525
+ type="button"
526
+ onClick={jumpToBottom}
527
+ aria-label="Scroll to bottom"
528
+ 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"
529
+ >
530
+ <ArrowDownIcon className="size-4" />
531
+ </button>
532
+ )}
533
+ <Composer />
534
+ </div>
535
+ </ThreadPrimitive.Root>
536
+ );
537
+ };
538
+
539
+ ```
540
+
541
+ ## components.json
542
+
543
+ ```json
544
+ {
545
+ "$schema": "https://ui.shadcn.com/schema.json",
546
+ "style": "new-york",
547
+ "rsc": true,
548
+ "tsx": true,
549
+ "tailwind": {
550
+ "config": "",
551
+ "css": "app/globals.css",
552
+ "baseColor": "zinc",
553
+ "cssVariables": true,
554
+ "prefix": ""
555
+ },
556
+ "aliases": {
557
+ "components": "@/components",
558
+ "utils": "@/lib/utils",
559
+ "ui": "@/components/ui",
560
+ "lib": "@/lib",
561
+ "hooks": "@/hooks"
562
+ },
563
+ "iconLibrary": "lucide",
564
+ "registries": {
565
+ "@assistant-ui": "https://r.assistant-ui.com/{name}.json"
566
+ }
567
+ }
568
+
569
+ ```
570
+
571
+ ## next.config.ts
572
+
573
+ ```typescript
574
+ import { withAui } from "@assistant-ui/next";
575
+ import type { NextConfig } from "next";
576
+
577
+ const nextConfig: NextConfig = {
578
+ /* config options here */
579
+ };
580
+
581
+ export default withAui(nextConfig);
582
+
583
+ ```
584
+
585
+ ## package.json
586
+
587
+ ```json
588
+ {
589
+ "name": "with-virtualized-thread",
590
+ "version": "0.0.0",
591
+ "private": true,
592
+ "type": "module",
593
+ "scripts": {
594
+ "dev": "next dev",
595
+ "build": "next build",
596
+ "start": "next start"
597
+ },
598
+ "dependencies": {
599
+ "@assistant-ui/react": "workspace:*",
600
+ "@assistant-ui/react-markdown": "workspace:*",
601
+ "@assistant-ui/ui": "workspace:*",
602
+ "@tanstack/react-virtual": "^3.14.3",
603
+ "class-variance-authority": "^0.7.1",
604
+ "clsx": "^2.1.1",
605
+ "lucide-react": "^1.21.0",
606
+ "next": "^16.2.9",
607
+ "react": "^19.2.7",
608
+ "react-dom": "^19.2.7",
609
+ "tailwind-merge": "^3.6.0"
610
+ },
611
+ "devDependencies": {
612
+ "@assistant-ui/next": "workspace:*",
613
+ "@assistant-ui/x-buildutils": "workspace:*",
614
+ "@tailwindcss/postcss": "^4.3.1",
615
+ "@types/node": "^26.0.0",
616
+ "@types/react": "^19.2.17",
617
+ "@types/react-dom": "^19.2.3",
618
+ "postcss": "^8.5.15",
619
+ "tailwindcss": "^4.3.1",
620
+ "tw-animate-css": "^1.4.0",
621
+ "typescript": "^6.0.3"
622
+ }
623
+ }
624
+
625
+ ```
626
+
627
+ ## README.md
628
+
629
+ ```markdown
630
+ # Virtualized thread example
631
+
632
+ 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.
633
+
634
+ ## Quick start
635
+
636
+ ```sh
637
+ pnpm install
638
+ pnpm --filter with-virtualized-thread dev
639
+ ```
640
+
641
+ ## What the example demonstrates
642
+
643
+ | Concern | File |
644
+ | --- | --- |
645
+ | Virtualizer over user turns, padding spacers, `measureElement` | `app/VirtualizedThread.tsx` |
646
+ | Id-keyed per-message rendering through `ThreadPrimitive.Unstable_MessageById` | `app/VirtualizedThread.tsx` |
647
+ | Sticky-bottom auto-follow with a user-scroll disarm guard | `app/VirtualizedThread.tsx` |
648
+ | External store runtime with seeded messages and a streaming tail | `app/MyRuntimeProvider.tsx` |
649
+ | Deterministic synthetic thread content | `app/seed-messages.ts` |
650
+
651
+ See the [thread virtualization guide](https://assistant-ui.com/docs/guides/virtualization) for the full walkthrough.
652
+
653
+ ```
654
+
655
+ ## tsconfig.json
656
+
657
+ ```json
658
+ {
659
+ "extends": "@assistant-ui/x-buildutils/ts/next",
660
+ "compilerOptions": {
661
+ "paths": {
662
+ "@/*": ["./*"],
663
+ "@/components/assistant-ui/*": [
664
+ "../../packages/ui/src/components/assistant-ui/*"
665
+ ],
666
+ "@/components/ui/*": ["../../packages/ui/src/components/ui/*"],
667
+ "@/lib/utils": ["../../packages/ui/src/lib/utils"],
668
+ "@assistant-ui/ui/*": ["../../packages/ui/src/*"]
669
+ }
670
+ },
671
+ "include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
672
+ "exclude": ["node_modules"]
673
+ }
674
+
675
+ ```
676
+
@@ -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