@assistant-ui/mcp-docs-server 0.1.32 → 0.1.33

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 (112) hide show
  1. package/.docs/organized/code-examples/waterfall.md +15 -17
  2. package/.docs/organized/code-examples/with-a2a.md +19 -19
  3. package/.docs/organized/code-examples/with-ag-ui.md +25 -20
  4. package/.docs/organized/code-examples/with-ai-sdk-v6.md +6 -4
  5. package/.docs/organized/code-examples/with-artifacts.md +34 -28
  6. package/.docs/organized/code-examples/with-assistant-transport.md +59 -47
  7. package/.docs/organized/code-examples/with-browser-extension.md +17 -5
  8. package/.docs/organized/code-examples/with-chain-of-thought.md +376 -78
  9. package/.docs/organized/code-examples/with-cloud-standalone.md +20 -19
  10. package/.docs/organized/code-examples/with-cloud.md +6 -4
  11. package/.docs/organized/code-examples/with-custom-thread-list.md +9 -7
  12. package/.docs/organized/code-examples/with-elevenlabs-conversational.md +18 -16
  13. package/.docs/organized/code-examples/with-elevenlabs-scribe.md +9 -8
  14. package/.docs/organized/code-examples/with-expo.md +23 -17
  15. package/.docs/organized/code-examples/with-external-store.md +4 -2
  16. package/.docs/organized/code-examples/with-ffmpeg.md +317 -280
  17. package/.docs/organized/code-examples/with-generative-ui.md +1018 -214
  18. package/.docs/organized/code-examples/with-google-adk.md +4 -2
  19. package/.docs/organized/code-examples/with-heat-graph.md +8 -6
  20. package/.docs/organized/code-examples/with-image-generation.md +19 -17
  21. package/.docs/organized/code-examples/with-interactables.md +312 -234
  22. package/.docs/organized/code-examples/with-langchain.md +9 -7
  23. package/.docs/organized/code-examples/with-langgraph.md +82 -78
  24. package/.docs/organized/code-examples/with-livekit.md +19 -17
  25. package/.docs/organized/code-examples/with-mcp.md +13 -7
  26. package/.docs/organized/code-examples/with-opencode.md +61 -61
  27. package/.docs/organized/code-examples/with-react-hook-form.md +14 -12
  28. package/.docs/organized/code-examples/with-react-ink.md +2 -2
  29. package/.docs/organized/code-examples/with-react-router.md +11 -9
  30. package/.docs/organized/code-examples/with-resumable-stream.md +10 -9
  31. package/.docs/organized/code-examples/with-store.md +14 -12
  32. package/.docs/organized/code-examples/with-tanstack.md +8 -9
  33. package/.docs/organized/code-examples/with-tap-runtime.md +21 -19
  34. package/.docs/raw/docs/(docs)/architecture.mdx +43 -2
  35. package/.docs/raw/docs/(docs)/cli.mdx +1 -2
  36. package/.docs/raw/docs/(docs)/copilots/model-context.mdx +34 -26
  37. package/.docs/raw/docs/(docs)/copilots/motivation.mdx +32 -26
  38. package/.docs/raw/docs/(docs)/installation.mdx +1 -1
  39. package/.docs/raw/docs/(reference)/api-reference/adapters/persistence.mdx +5 -1
  40. package/.docs/raw/docs/(reference)/api-reference/external-store/message-conversion.mdx +14 -14
  41. package/.docs/raw/docs/(reference)/api-reference/external-store/runtime.mdx +11 -1
  42. package/.docs/raw/docs/(reference)/api-reference/generative-ui/index.mdx +18 -0
  43. package/.docs/raw/docs/(reference)/api-reference/generative-ui/rendering.mdx +33 -0
  44. package/.docs/raw/docs/(reference)/api-reference/generative-ui/spec.mdx +45 -0
  45. package/.docs/raw/docs/(reference)/api-reference/hooks/composer-triggers.mdx +41 -41
  46. package/.docs/raw/docs/(reference)/api-reference/hooks/model-context.mdx +19 -1
  47. package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +122 -122
  48. package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +32 -2
  49. package/.docs/raw/docs/(reference)/api-reference/model-context/context.mdx +6 -0
  50. package/.docs/raw/docs/(reference)/api-reference/overview.mdx +4 -6
  51. package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +30 -0
  52. package/.docs/raw/docs/(reference)/api-reference/tools/component-tools.mdx +20 -3
  53. package/.docs/raw/docs/(reference)/api-reference/tools/index.mdx +8 -8
  54. package/.docs/raw/docs/(reference)/api-reference/tools/rendering.mdx +52 -4
  55. package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +6 -7
  56. package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +58 -46
  57. package/.docs/raw/docs/cloud/ai-sdk.mdx +0 -2
  58. package/.docs/raw/docs/guides/chain-of-thought.mdx +7 -9
  59. package/.docs/raw/docs/guides/context-api.mdx +2 -1
  60. package/.docs/raw/docs/guides/index.mdx +3 -12
  61. package/.docs/raw/docs/guides/mentions.mdx +4 -4
  62. package/.docs/raw/docs/guides/slash-commands.mdx +1 -1
  63. package/.docs/raw/docs/guides/suggestions.mdx +1 -1
  64. package/.docs/raw/docs/ink/hooks.mdx +98 -85
  65. package/.docs/raw/docs/ink/migration.mdx +1 -1
  66. package/.docs/raw/docs/ink/primitives.mdx +2 -2
  67. package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +1 -1
  68. package/.docs/raw/docs/integrations/index.mdx +2 -2
  69. package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +17 -2
  70. package/.docs/raw/docs/migrations/toolkit-tools.mdx +226 -0
  71. package/.docs/raw/docs/primitives/chain-of-thought.mdx +10 -16
  72. package/.docs/raw/docs/primitives/message.mdx +9 -10
  73. package/.docs/raw/docs/react-native/hooks.mdx +57 -82
  74. package/.docs/raw/docs/react-native/migration.mdx +1 -1
  75. package/.docs/raw/docs/react-native/primitives.mdx +2 -2
  76. package/.docs/raw/docs/runtimes/a2a/client-and-hooks.mdx +3 -0
  77. package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +122 -1
  78. package/.docs/raw/docs/runtimes/concepts/threads.mdx +7 -1
  79. package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +18 -7
  80. package/.docs/raw/docs/runtimes/google-adk/hooks.mdx +108 -38
  81. package/.docs/raw/docs/runtimes/langgraph/generative-ui.mdx +2 -2
  82. package/.docs/raw/docs/runtimes/langgraph/streaming.mdx +1 -1
  83. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +64 -50
  84. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +98 -86
  85. package/.docs/raw/docs/tools/backend.mdx +136 -0
  86. package/.docs/raw/docs/tools/defining-tools.mdx +413 -0
  87. package/.docs/raw/docs/tools/dynamic-tools.mdx +110 -0
  88. package/.docs/raw/docs/tools/generative-ui.mdx +214 -0
  89. package/.docs/raw/docs/tools/index.mdx +76 -0
  90. package/.docs/raw/docs/{guides → tools}/interactables.mdx +1 -1
  91. package/.docs/raw/docs/{integrations/tools → tools}/mcp.mdx +77 -50
  92. package/.docs/raw/docs/{guides → tools}/multi-agent.mdx +17 -19
  93. package/.docs/raw/docs/tools/tool-ui.mdx +967 -0
  94. package/.docs/raw/docs/{integrations/tools/react-mcp.mdx → tools/user-managed-mcp.mdx} +3 -3
  95. package/.docs/raw/docs/ui/directive-text.mdx +3 -3
  96. package/.docs/raw/docs/ui/mcp-config.mdx +4 -4
  97. package/.docs/raw/docs/ui/part-grouping.mdx +84 -50
  98. package/.docs/raw/docs/ui/reasoning.mdx +4 -5
  99. package/.docs/raw/docs/ui/tool-group.mdx +5 -6
  100. package/.docs/raw/docs/utilities/heat-graph.mdx +1 -1
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/prepare-docs/code-examples.d.ts.map +1 -1
  103. package/dist/prepare-docs/copy-raw.d.ts.map +1 -1
  104. package/dist/utils/paths.d.ts.map +1 -1
  105. package/package.json +3 -3
  106. package/.docs/organized/code-examples/with-parent-id-grouping.md +0 -596
  107. package/.docs/raw/docs/(docs)/copilots/make-assistant-tool-ui.mdx +0 -151
  108. package/.docs/raw/docs/(docs)/copilots/make-assistant-tool.mdx +0 -230
  109. package/.docs/raw/docs/guides/generative-ui.mdx +0 -142
  110. package/.docs/raw/docs/guides/tool-ui.mdx +0 -858
  111. package/.docs/raw/docs/guides/tools.mdx +0 -736
  112. /package/.docs/raw/docs/{guides → tools}/mcp-apps.mdx +0 -0
@@ -3,46 +3,132 @@
3
3
  ## app/api/chat/route.ts
4
4
 
5
5
  ```typescript
6
- import { openai } from "@ai-sdk/openai";
6
+ import { createOpenAI } from "@ai-sdk/openai";
7
7
  import {
8
8
  streamText,
9
9
  convertToModelMessages,
10
10
  tool,
11
11
  stepCountIs,
12
12
  zodSchema,
13
+ createUIMessageStream,
14
+ JsonToSseTransformStream,
13
15
  } from "ai";
14
- import type { UIMessage } from "ai";
16
+ import type { UIMessage, UIMessageStreamWriter } from "ai";
15
17
  import { frontendTools } from "@assistant-ui/react-ai-sdk";
16
18
  import { z } from "zod";
17
19
 
18
20
  export const maxDuration = 30;
19
21
 
22
+ type SearchResult = { id: string; url: string; title: string };
23
+
24
+ const searchSources = (query: string): SearchResult[] => {
25
+ const q = query.toLowerCase();
26
+ if (q.includes("fibonacci")) {
27
+ return [
28
+ {
29
+ id: "src-fib-1",
30
+ url: "https://en.wikipedia.org/wiki/Fibonacci_number",
31
+ title: "Fibonacci number (Wikipedia)",
32
+ },
33
+ {
34
+ id: "src-fib-2",
35
+ url: "https://oeis.org/A000045",
36
+ title: "OEIS A000045: Fibonacci numbers",
37
+ },
38
+ ];
39
+ }
40
+ if (
41
+ q.includes("renewable") ||
42
+ q.includes("energy") ||
43
+ q.includes("climate")
44
+ ) {
45
+ return [
46
+ {
47
+ id: "src-iea",
48
+ url: "https://www.iea.org/reports/renewables-2024",
49
+ title: "IEA Renewables 2024",
50
+ },
51
+ {
52
+ id: "src-irena",
53
+ url: "https://www.irena.org/Publications",
54
+ title: "IRENA Publications",
55
+ },
56
+ ];
57
+ }
58
+ return [
59
+ {
60
+ id: "src-wiki",
61
+ url: `https://en.wikipedia.org/wiki/Special:Search?search=${encodeURIComponent(query)}`,
62
+ title: "Wikipedia search results",
63
+ },
64
+ {
65
+ id: "src-web",
66
+ url: `https://duckduckgo.com/?q=${encodeURIComponent(query)}`,
67
+ title: `Web results for: ${query}`,
68
+ },
69
+ ];
70
+ };
71
+
20
72
  export async function POST(req: Request) {
21
73
  const {
22
74
  messages,
23
75
  tools,
24
76
  }: { messages: UIMessage[]; tools: Record<string, any> } = await req.json();
25
77
 
78
+ const uiStream = createUIMessageStream({
79
+ execute: async ({ writer }) => {
80
+ if (!process.env.OPENAI_API_KEY) {
81
+ await streamFallback(writer, messages);
82
+ return;
83
+ }
84
+ await streamModel(writer, messages, tools);
85
+ },
86
+ });
87
+
88
+ return new Response(
89
+ uiStream
90
+ .pipeThrough(new JsonToSseTransformStream())
91
+ .pipeThrough(new TextEncoderStream()),
92
+ {
93
+ headers: {
94
+ "Content-Type": "text/event-stream",
95
+ "Cache-Control": "no-cache",
96
+ Connection: "keep-alive",
97
+ },
98
+ },
99
+ );
100
+ }
101
+
102
+ async function streamModel(
103
+ writer: UIMessageStreamWriter,
104
+ messages: UIMessage[],
105
+ frontendToolDefs: Record<string, any>,
106
+ ) {
107
+ const toolNameByCall = new Map<string, string>();
108
+ const openai = createOpenAI({
109
+ apiKey: process.env.OPENAI_API_KEY!,
110
+ ...(process.env.OPENAI_BASE_URL && {
111
+ baseURL: process.env.OPENAI_BASE_URL,
112
+ }),
113
+ });
114
+
26
115
  const result = streamText({
27
- // Use a reasoning model to showcase chain of thought
116
+ // Reasoning model so the chain-of-thought group has real content.
28
117
  model: openai("gpt-5.4-mini"),
29
118
  messages: await convertToModelMessages(messages),
30
119
  stopWhen: stepCountIs(10),
31
120
  tools: {
32
- // Frontend tools registered via makeAssistantTool (e.g. execute_js)
33
- ...frontendTools(tools),
34
-
35
- // Backend-only tools
121
+ ...frontendTools(frontendToolDefs),
36
122
  get_current_weather: tool({
37
123
  description: "Get the current weather for a city",
38
- inputSchema: zodSchema(
39
- z.object({
40
- city: z.string(),
41
- }),
42
- ),
43
- execute: async ({ city }) => {
44
- return `The weather in ${city} is sunny, 72°F`;
45
- },
124
+ inputSchema: zodSchema(z.object({ city: z.string() })),
125
+ execute: async ({ city }) => `The weather in ${city} is sunny, 72°F`,
126
+ }),
127
+ search_web: tool({
128
+ description:
129
+ "Search the web for citations on a topic. Returns a list of source URLs and titles the assistant should consult.",
130
+ inputSchema: zodSchema(z.object({ query: z.string() })),
131
+ execute: async ({ query }) => ({ sources: searchSources(query) }),
46
132
  }),
47
133
  },
48
134
  providerOptions: {
@@ -53,9 +139,106 @@ export async function POST(req: Request) {
53
139
  },
54
140
  });
55
141
 
56
- return result.toUIMessageStreamResponse({ sendReasoning: true });
142
+ // Forward model chunks, then synthesize `source-url` parts from each
143
+ // `search_web` tool output so the UI renders them as `source` parts. OpenAI
144
+ // does not natively emit `source-url` chunks without web search, so the route
145
+ // produces them deterministically from the tool result.
146
+ for await (const chunk of result.toUIMessageStream({ sendReasoning: true })) {
147
+ writer.write(chunk);
148
+
149
+ if (chunk.type === "tool-input-available") {
150
+ toolNameByCall.set(chunk.toolCallId, chunk.toolName);
151
+ }
152
+ if (
153
+ chunk.type === "tool-output-available" &&
154
+ toolNameByCall.get(chunk.toolCallId) === "search_web"
155
+ ) {
156
+ const output = chunk.output as { sources?: SearchResult[] };
157
+ for (const source of output.sources ?? []) {
158
+ writer.write({
159
+ type: "source-url",
160
+ sourceId: `${source.id}-${crypto.randomUUID()}`,
161
+ url: source.url,
162
+ title: source.title,
163
+ });
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ async function streamFallback(
170
+ writer: UIMessageStreamWriter,
171
+ messages: UIMessage[],
172
+ ) {
173
+ const lastUserText =
174
+ messages
175
+ .filter((m) => m.role === "user")
176
+ .at(-1)
177
+ ?.parts.flatMap((p) => (p.type === "text" ? [p.text] : []))
178
+ .join(" ") ?? "your question";
179
+
180
+ const messageId = `msg-${crypto.randomUUID()}`;
181
+ const reasoningId = `r-${crypto.randomUUID()}`;
182
+ const textId = `t-${crypto.randomUUID()}`;
183
+ const toolCallId = `call-${crypto.randomUUID()}`;
184
+ const sources = searchSources(lastUserText);
185
+
186
+ writer.write({ type: "start", messageId });
187
+ writer.write({ type: "start-step" });
188
+
189
+ writer.write({ type: "reasoning-start", id: reasoningId });
190
+ const reasoning =
191
+ "OPENAI_API_KEY is not set, so this response is generated deterministically by the route. " +
192
+ "A real run emits the same chunk shapes from the model: a reasoning step, " +
193
+ "a search_web tool call, source-url parts derived from the tool output, " +
194
+ "and a final answer that summarises the sources.";
195
+ for (const word of reasoning.match(/\S+\s*|\s+/g) ?? [reasoning]) {
196
+ writer.write({ type: "reasoning-delta", id: reasoningId, delta: word });
197
+ await sleep(15);
198
+ }
199
+ writer.write({ type: "reasoning-end", id: reasoningId });
200
+
201
+ writer.write({
202
+ type: "tool-input-available",
203
+ toolCallId,
204
+ toolName: "search_web",
205
+ input: { query: lastUserText },
206
+ });
207
+ await sleep(150);
208
+ writer.write({
209
+ type: "tool-output-available",
210
+ toolCallId,
211
+ output: { sources },
212
+ });
213
+
214
+ for (const source of sources) {
215
+ writer.write({
216
+ type: "source-url",
217
+ sourceId: `${source.id}-${crypto.randomUUID()}`,
218
+ url: source.url,
219
+ title: source.title,
220
+ });
221
+ }
222
+
223
+ writer.write({ type: "text-start", id: textId });
224
+ const answer =
225
+ `\n\nMock answer for: "${lastUserText}". ` +
226
+ "The badges above are structured `source` message parts emitted as " +
227
+ "`source-url` chunks, not URLs scraped from text. Set `OPENAI_API_KEY` " +
228
+ "to switch to a real model run.";
229
+ for (const word of answer.match(/\S+\s*|\s+/g) ?? [answer]) {
230
+ writer.write({ type: "text-delta", id: textId, delta: word });
231
+ await sleep(15);
232
+ }
233
+ writer.write({ type: "text-end", id: textId });
234
+
235
+ writer.write({ type: "finish-step" });
236
+ writer.write({ type: "finish" });
57
237
  }
58
238
 
239
+ const sleep = (ms: number) =>
240
+ new Promise<void>((resolve) => setTimeout(resolve, ms));
241
+
59
242
  ```
60
243
 
61
244
  ## app/globals.css
@@ -194,7 +377,8 @@ import "./globals.css";
194
377
 
195
378
  export const metadata: Metadata = {
196
379
  title: "Chain of Thought Example",
197
- description: "Example using MessagePrimitive.GroupedParts with AI SDK",
380
+ description:
381
+ "Reasoning, tool calls, and source citations rendered as grouped message parts.",
198
382
  };
199
383
 
200
384
  export default function RootLayout({
@@ -218,10 +402,12 @@ export default function RootLayout({
218
402
 
219
403
  import { type FC, type PropsWithChildren, useState } from "react";
220
404
  import { MarkdownText } from "@/components/assistant-ui/markdown-text";
405
+ import { Sources } from "@/components/assistant-ui/sources";
221
406
  import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
222
407
  import { Button } from "@/components/ui/button";
223
408
  import {
224
409
  ComposerPrimitive,
410
+ groupPartByType,
225
411
  MessagePrimitive,
226
412
  SuggestionPrimitive,
227
413
  ThreadPrimitive,
@@ -238,7 +424,7 @@ import {
238
424
  export const MyThread: FC = () => {
239
425
  return (
240
426
  <ThreadPrimitive.Root
241
- className="flex h-full flex-col bg-background"
427
+ className="bg-background flex h-full flex-col"
242
428
  style={{ ["--thread-max-width" as string]: "44rem" }}
243
429
  >
244
430
  <ThreadPrimitive.Viewport className="flex flex-1 flex-col overflow-y-scroll scroll-smooth px-4 pt-8">
@@ -278,7 +464,9 @@ const ThreadWelcome: FC = () => {
278
464
  return (
279
465
  <div className="mx-auto flex w-full max-w-(--thread-max-width) grow flex-col justify-center gap-6">
280
466
  <div>
281
- <h1 className="font-semibold text-2xl">What should we calculate?</h1>
467
+ <h1 className="text-2xl font-semibold">
468
+ What should we think through?
469
+ </h1>
282
470
  </div>
283
471
  <div className="flex flex-wrap gap-2">
284
472
  <ThreadPrimitive.Suggestions>
@@ -295,7 +483,7 @@ const ThreadSuggestionItem: FC = () => {
295
483
  <Button
296
484
  type="button"
297
485
  variant="ghost"
298
- className="h-auto w-full flex-col items-start justify-start gap-1 rounded-2xl border bg-background px-4 py-3 text-start text-sm hover:bg-muted sm:w-[calc(50%-0.25rem)]"
486
+ className="bg-background hover:bg-muted h-auto w-full flex-col items-start justify-start gap-1 rounded-2xl border px-4 py-3 text-start text-sm sm:w-[calc(50%-0.25rem)]"
299
487
  >
300
488
  <SuggestionPrimitive.Title className="font-medium" />
301
489
  <SuggestionPrimitive.Description className="text-muted-foreground empty:hidden" />
@@ -308,7 +496,7 @@ const UserMessage: FC = () => {
308
496
  return (
309
497
  <MessagePrimitive.Root className="mx-auto w-full max-w-(--thread-max-width) py-3">
310
498
  <div className="flex justify-end">
311
- <div className="max-w-[80%] rounded-2xl bg-primary px-4 py-2 text-primary-foreground">
499
+ <div className="bg-primary text-primary-foreground max-w-[80%] rounded-2xl px-4 py-2">
312
500
  <MessagePrimitive.Parts>
313
501
  {({ part }) => {
314
502
  if (part.type === "text") return <Text {...part} />;
@@ -326,13 +514,11 @@ const AssistantMessage: FC = () => {
326
514
  <MessagePrimitive.Root className="mx-auto w-full max-w-(--thread-max-width) py-3">
327
515
  <div className="flex flex-col gap-2 px-2 leading-relaxed">
328
516
  <MessagePrimitive.GroupedParts
329
- groupBy={(part) => {
330
- if (part.type === "reasoning")
331
- return ["group-chainOfThought", "group-reasoning"];
332
- if (part.type === "tool-call")
333
- return ["group-chainOfThought", "group-tool"];
334
- return null;
335
- }}
517
+ groupBy={groupPartByType({
518
+ reasoning: ["group-chainOfThought", "group-reasoning"],
519
+ "tool-call": ["group-chainOfThought", "group-tool"],
520
+ source: ["group-sources"],
521
+ })}
336
522
  >
337
523
  {({ part, children }) => {
338
524
  switch (part.type) {
@@ -344,12 +530,16 @@ const AssistantMessage: FC = () => {
344
530
  return (
345
531
  <PartLayout label="Taking action">{children}</PartLayout>
346
532
  );
533
+ case "group-sources":
534
+ return <SourcesLayout>{children}</SourcesLayout>;
347
535
  case "text":
348
536
  return <MarkdownText />;
349
537
  case "reasoning":
350
538
  return <Reasoning {...part} />;
351
539
  case "tool-call":
352
540
  return <ToolCall {...part} />;
541
+ case "source":
542
+ return <Sources {...part} />;
353
543
  default:
354
544
  return null;
355
545
  }
@@ -367,7 +557,7 @@ const ChainOfThoughtGroup: FC<PropsWithChildren> = ({ children }) => {
367
557
  <div className="my-2 rounded-lg border">
368
558
  <button
369
559
  type="button"
370
- className="flex w-full cursor-pointer items-center gap-2 px-4 py-2 font-medium text-sm hover:bg-muted/50"
560
+ className="hover:bg-muted/50 flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm font-medium"
371
561
  onClick={() => setOpen((o) => !o)}
372
562
  >
373
563
  {open ? (
@@ -382,6 +572,15 @@ const ChainOfThoughtGroup: FC<PropsWithChildren> = ({ children }) => {
382
572
  );
383
573
  };
384
574
 
575
+ const SourcesLayout: FC<PropsWithChildren> = ({ children }) => {
576
+ return (
577
+ <div className="flex flex-wrap items-center gap-1.5">
578
+ <span className="text-muted-foreground mr-1 text-xs">Sources</span>
579
+ {children}
580
+ </div>
581
+ );
582
+ };
583
+
385
584
  const PartLayout: FC<PropsWithChildren<{ label: string }>> = ({
386
585
  children,
387
586
  label,
@@ -392,7 +591,7 @@ const PartLayout: FC<PropsWithChildren<{ label: string }>> = ({
392
591
  <div className="border-t">
393
592
  <button
394
593
  type="button"
395
- className="flex w-full cursor-pointer items-center gap-2 px-4 py-1.5 text-muted-foreground text-xs hover:bg-muted/50"
594
+ className="text-muted-foreground hover:bg-muted/50 flex w-full cursor-pointer items-center gap-2 px-4 py-1.5 text-xs"
396
595
  onClick={() => setOpen((o) => !o)}
397
596
  >
398
597
  {open ? (
@@ -409,7 +608,7 @@ const PartLayout: FC<PropsWithChildren<{ label: string }>> = ({
409
608
 
410
609
  const Reasoning: FC<{ text: string }> = ({ text }) => {
411
610
  return (
412
- <p className="whitespace-pre-wrap px-4 py-2 text-muted-foreground text-sm italic">
611
+ <p className="text-muted-foreground px-4 py-2 text-sm whitespace-pre-wrap italic">
413
612
  {text}
414
613
  </p>
415
614
  );
@@ -436,10 +635,10 @@ const Composer: FC = () => {
436
635
  const isRunning = useThread((state) => state.isRunning);
437
636
 
438
637
  return (
439
- <ComposerPrimitive.Root className="flex w-full flex-col rounded-2xl border border-input bg-background px-1 pt-2 outline-none transition-shadow has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-2 has-[textarea:focus-visible]:ring-ring/20">
638
+ <ComposerPrimitive.Root className="border-input bg-background has-[textarea:focus-visible]:border-ring has-[textarea:focus-visible]:ring-ring/20 flex w-full flex-col rounded-2xl border px-1 pt-2 transition-shadow outline-none has-[textarea:focus-visible]:ring-2">
440
639
  <ComposerPrimitive.Input
441
640
  placeholder="Send a message..."
442
- className="mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none placeholder:text-muted-foreground"
641
+ className="placeholder:text-muted-foreground mb-1 max-h-32 min-h-14 w-full resize-none bg-transparent px-4 pt-2 pb-3 text-sm outline-none"
443
642
  rows={1}
444
643
  autoFocus
445
644
  />
@@ -482,7 +681,8 @@ const Composer: FC = () => {
482
681
 
483
682
  import {
484
683
  AssistantRuntimeProvider,
485
- makeAssistantTool,
684
+ Tools,
685
+ type Toolkit,
486
686
  useAui,
487
687
  AuiProvider,
488
688
  Suggestions,
@@ -492,53 +692,56 @@ import { z } from "zod";
492
692
  import { MyThread } from "./MyThread";
493
693
  import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
494
694
 
495
- const ExecuteJsTool = makeAssistantTool({
496
- toolName: "execute_js",
497
- description: "Execute JavaScript code and return the result",
498
- parameters: z.object({
499
- code: z.string().describe("The JavaScript code to execute"),
500
- }),
501
- execute: async ({ code }) => {
502
- try {
503
- // biome-ignore lint/security/noGlobalEval: example code
504
- const result = eval(code);
505
- return { success: true, result: String(result) };
506
- } catch (e) {
507
- return { success: false, error: String(e) };
508
- }
695
+ const toolkit = {
696
+ execute_js: {
697
+ type: "frontend",
698
+ description: "Execute JavaScript code and return the result",
699
+ parameters: z.object({
700
+ code: z.string().describe("The JavaScript code to execute"),
701
+ }),
702
+ execute: async ({ code }) => {
703
+ try {
704
+ const result = eval(code);
705
+ return { success: true, result: String(result) };
706
+ } catch (e) {
707
+ return { success: false, error: String(e) };
708
+ }
709
+ },
710
+ render: ({ args, result, status }) => (
711
+ <div className="bg-muted/30 my-2 rounded-lg border p-4 text-sm">
712
+ <p className="mb-1 font-semibold">execute_js</p>
713
+ <pre className="bg-background rounded p-2 font-mono text-xs whitespace-pre-wrap">
714
+ {args.code}
715
+ </pre>
716
+ {status.type !== "running" && result && (
717
+ <div className="mt-2 border-t pt-2">
718
+ <p className="text-muted-foreground font-semibold">
719
+ {result.success ? "Result:" : "Error:"}
720
+ </p>
721
+ <pre className="font-mono text-xs whitespace-pre-wrap">
722
+ {result.success ? result.result : result.error}
723
+ </pre>
724
+ </div>
725
+ )}
726
+ </div>
727
+ ),
509
728
  },
510
- render: ({ args, result, status }) => (
511
- <div className="my-2 rounded-lg border bg-muted/30 p-4 text-sm">
512
- <p className="mb-1 font-semibold">execute_js</p>
513
- <pre className="whitespace-pre-wrap rounded bg-background p-2 font-mono text-xs">
514
- {args.code}
515
- </pre>
516
- {status.type !== "running" && result && (
517
- <div className="mt-2 border-t pt-2">
518
- <p className="font-semibold text-muted-foreground">
519
- {result.success ? "Result:" : "Error:"}
520
- </p>
521
- <pre className="whitespace-pre-wrap font-mono text-xs">
522
- {result.success ? result.result : result.error}
523
- </pre>
524
- </div>
525
- )}
526
- </div>
527
- ),
528
- });
729
+ } satisfies Toolkit;
529
730
 
530
731
  function MyThreadWithSuggestions() {
531
732
  const aui = useAui({
532
733
  suggestions: Suggestions([
533
734
  {
534
735
  title: "Calculate Fibonacci(20)",
535
- label: "step by step with code",
536
- prompt: "Calculate the 20th Fibonacci number using JavaScript.",
736
+ label: "with chain of thought",
737
+ prompt:
738
+ "Calculate the 20th Fibonacci number using JavaScript and show your reasoning.",
537
739
  },
538
740
  {
539
- title: "What is 2^16?",
540
- label: "show your reasoning",
541
- prompt: "What is 2 to the power of 16? Show your work step by step.",
741
+ title: "Research with citations",
742
+ label: "show sources",
743
+ prompt:
744
+ "Research recent developments in renewable energy and cite your sources.",
542
745
  },
543
746
  ]),
544
747
  });
@@ -553,10 +756,12 @@ export default function Home() {
553
756
  const runtime = useChatRuntime({
554
757
  sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
555
758
  });
759
+ const aui = useAui({
760
+ tools: Tools({ toolkit }),
761
+ });
556
762
 
557
763
  return (
558
- <AssistantRuntimeProvider runtime={runtime}>
559
- <ExecuteJsTool />
764
+ <AssistantRuntimeProvider aui={aui} runtime={runtime}>
560
765
  <div className="h-full">
561
766
  <MyThreadWithSuggestions />
562
767
  </div>
@@ -599,12 +804,13 @@ export default function Home() {
599
804
  ## next.config.js
600
805
 
601
806
  ```javascript
807
+ import { withAui } from "@assistant-ui/next";
602
808
  /** @type {import('next').NextConfig} */
603
809
  const nextConfig = {
604
810
  transpilePackages: ["@assistant-ui/react", "@assistant-ui/react-ai-sdk"],
605
811
  };
606
812
 
607
- export default nextConfig;
813
+ export default withAui(nextConfig);
608
814
 
609
815
  ```
610
816
 
@@ -622,15 +828,15 @@ export default nextConfig;
622
828
  "start": "next start"
623
829
  },
624
830
  "dependencies": {
625
- "@ai-sdk/openai": "^3.0.64",
831
+ "@ai-sdk/openai": "^3.0.67",
626
832
  "@assistant-ui/react": "workspace:*",
627
833
  "@assistant-ui/react-ai-sdk": "workspace:*",
628
834
  "@assistant-ui/react-markdown": "workspace:*",
629
835
  "@assistant-ui/ui": "workspace:*",
630
- "ai": "^6.0.188",
836
+ "ai": "^6.0.193",
631
837
  "class-variance-authority": "^0.7.1",
632
838
  "clsx": "^2.1.1",
633
- "lucide-react": "^1.16.0",
839
+ "lucide-react": "^1.17.0",
634
840
  "next": "^16.2.6",
635
841
  "react": "^19.2.6",
636
842
  "react-dom": "^19.2.6",
@@ -638,6 +844,7 @@ export default nextConfig;
638
844
  "zod": "^4.4.3"
639
845
  },
640
846
  "devDependencies": {
847
+ "@assistant-ui/next": "workspace:*",
641
848
  "@assistant-ui/x-buildutils": "workspace:*",
642
849
  "@tailwindcss/postcss": "^4.3.0",
643
850
  "@types/node": "^25.9.1",
@@ -652,6 +859,97 @@ export default nextConfig;
652
859
 
653
860
  ```
654
861
 
862
+ ## README.md
863
+
864
+ ```markdown
865
+ # Chain of Thought Example
866
+
867
+ A real chat flow that renders reasoning, tool calls, and source citations as grouped message parts. Demonstrates `MessagePrimitive.GroupedParts` with a nested `groupBy` path, plus structured `source` parts emitted from a search tool.
868
+
869
+ ## Quick start
870
+
871
+ ### Using the CLI
872
+
873
+ ```bash
874
+ npx assistant-ui@latest create my-app --example with-chain-of-thought
875
+ cd my-app
876
+ ```
877
+
878
+ ### Environment variables
879
+
880
+ `OPENAI_API_KEY` is optional. With the key set the route runs a real reasoning model and a `search_web` tool; without it the route streams a deterministic mock response with the same chunk shapes, so the UI is fully demonstrable offline.
881
+
882
+ Create `.env.local`:
883
+
884
+ ```
885
+ OPENAI_API_KEY=sk-...
886
+ ```
887
+
888
+ ### Run
889
+
890
+ ```bash
891
+ npm install
892
+ npm run dev
893
+ ```
894
+
895
+ Open [http://localhost:3000](http://localhost:3000).
896
+
897
+ ## What the example demonstrates
898
+
899
+ | Concern | Where it lives |
900
+ |---|---|
901
+ | Real chat flow with `useChatRuntime` | `app/page.tsx` |
902
+ | Reasoning, tool calls, source citations streamed as message parts | `app/api/chat/route.ts` |
903
+ | Nested adjacency-based grouping with `MessagePrimitive.GroupedParts` | `app/MyThread.tsx` |
904
+ | `Sources` component rendering `source` parts | `app/MyThread.tsx` |
905
+ | Fallback path that runs without an API key | `app/api/chat/route.ts` (`streamFallback`) |
906
+
907
+ ## How parts are grouped
908
+
909
+ The thread groups three part types under one collapsible "Thinking" section, then renders source badges in a row below it. The grouping is configured on `MessagePrimitive.GroupedParts`:
910
+
911
+ ```tsx
912
+ <MessagePrimitive.GroupedParts
913
+ groupBy={(part) => {
914
+ if (part.type === "reasoning") return ["group-chainOfThought", "group-reasoning"];
915
+ if (part.type === "tool-call") return ["group-chainOfThought", "group-tool"];
916
+ if (part.type === "source") return ["group-chainOfThought", "group-sources"];
917
+ return null;
918
+ }}
919
+ >
920
+ ```
921
+
922
+ Each part is mapped to a nested group key path. Adjacent parts that share a path are coalesced and rendered through the matching `group-*` case in the render function. `null` leaves a part ungrouped.
923
+
924
+ This is the canonical pattern. For non-adjacent clustering (parts that share an identifier but appear at different positions in the message), see [`/docs/ui/part-grouping`](https://www.assistant-ui.com/docs/ui/part-grouping).
925
+
926
+ ## How sources are emitted
927
+
928
+ The route emits structured `source-url` chunks; `@assistant-ui/react-ai-sdk` converts them to `source` message parts that the `Sources` component renders as badges. They are not URLs scraped from assistant text.
929
+
930
+ ```ts
931
+ writer.write({
932
+ type: "source-url",
933
+ sourceId: "src-1",
934
+ url: "https://example.com/report",
935
+ title: "Example report",
936
+ });
937
+ ```
938
+
939
+ In the real path the `search_web` tool returns sources in its output, and the route writes one `source-url` chunk per result immediately after the `tool-output-available` chunk. In the fallback path the same shape is written directly. The UI rendering is identical in both modes.
940
+
941
+ ## Why these features live together
942
+
943
+ Chain-of-thought, tool calls, and citation sources are the parts of an assistant turn a user typically wants to inspect together. Grouping them under one collapsible keeps the message tidy by default and reveals the full reasoning trail on demand.
944
+
945
+ ## Related documentation
946
+
947
+ - [Message Part Grouping](https://www.assistant-ui.com/docs/ui/part-grouping)
948
+ - [Sources component](https://www.assistant-ui.com/docs/ui/sources)
949
+ - [AI SDK v6 runtime](https://www.assistant-ui.com/docs/runtimes/ai-sdk/v6)
950
+
951
+ ```
952
+
655
953
  ## tsconfig.json
656
954
 
657
955
  ```json