@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
@@ -14,6 +14,10 @@ import {
14
14
  } from "ai";
15
15
  import type { UIMessage } from "ai";
16
16
  import { z } from "zod";
17
+ import {
18
+ renderGuiToolDescription,
19
+ renderGuiToolInputSchema,
20
+ } from "../../../lib/render-gui-tool";
17
21
 
18
22
  export const maxDuration = 30;
19
23
 
@@ -51,6 +55,14 @@ export async function POST(req: Request) {
51
55
  tools: {
52
56
  ...frontendToolDefs,
53
57
 
58
+ render_gui: tool({
59
+ description: renderGuiToolDescription,
60
+ inputSchema: zodSchema(renderGuiToolInputSchema),
61
+ execute: async (input) => ({
62
+ spec: input.spec,
63
+ }),
64
+ }),
65
+
54
66
  // Backend tool: generate chart data
55
67
  generate_chart: tool({
56
68
  description:
@@ -231,6 +243,65 @@ export async function POST(req: Request) {
231
243
 
232
244
  ```
233
245
 
246
+ ## app/gui-chat/page.tsx
247
+
248
+ ```tsx
249
+ "use client";
250
+
251
+ import { ExampleNav } from "@/components/example-nav";
252
+ import { GuiThread } from "@/components/gui-thread";
253
+ import {
254
+ AssistantRuntimeProvider,
255
+ Suggestions,
256
+ useAssistantInstructions,
257
+ useAui,
258
+ } from "@assistant-ui/react";
259
+ import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
260
+ import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
261
+ import { renderGuiChatInstructions } from "@/lib/render-gui-tool";
262
+
263
+ const GuiChatInstructions = () => {
264
+ useAssistantInstructions(renderGuiChatInstructions);
265
+ return null;
266
+ };
267
+
268
+ export default function GuiChatPage() {
269
+ const runtime = useChatRuntime({
270
+ sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
271
+ });
272
+
273
+ const aui = useAui({
274
+ suggestions: Suggestions([
275
+ {
276
+ title: "Welcome card",
277
+ label: "with a Get started button",
278
+ prompt:
279
+ "Show me a welcome card with a Get started button using render_gui.",
280
+ },
281
+ {
282
+ title: "Stats dashboard",
283
+ label: "revenue and users",
284
+ prompt:
285
+ "Use render_gui to show a stats card with Revenue $124k and Active Users 8.2k.",
286
+ },
287
+ ]),
288
+ });
289
+
290
+ return (
291
+ <AssistantRuntimeProvider aui={aui} runtime={runtime}>
292
+ <GuiChatInstructions />
293
+ <div className="flex h-full flex-col">
294
+ <ExampleNav />
295
+ <main className="min-h-0 flex-1">
296
+ <GuiThread />
297
+ </main>
298
+ </div>
299
+ </AssistantRuntimeProvider>
300
+ );
301
+ }
302
+
303
+ ```
304
+
234
305
  ## app/layout.tsx
235
306
 
236
307
  ```tsx
@@ -266,8 +337,9 @@ import { Thread } from "@/components/assistant-ui/thread";
266
337
  import {
267
338
  AssistantRuntimeProvider,
268
339
  Suggestions,
340
+ Tools,
341
+ type Toolkit,
269
342
  useAui,
270
- useAssistantTool,
271
343
  } from "@assistant-ui/react";
272
344
  import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
273
345
  import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
@@ -276,11 +348,11 @@ import { ChartToolUI } from "@/components/chart-tool-ui";
276
348
  import { DatePickerToolUI } from "@/components/date-picker-tool-ui";
277
349
  import { ContactFormToolUI } from "@/components/contact-form-tool-ui";
278
350
  import { LocationToolUI } from "@/components/location-tool-ui";
351
+ import { ExampleNav } from "@/components/example-nav";
279
352
 
280
- // Register frontend tool schemas (no execute — resolved via addResult in the UI)
281
- function FrontendTools() {
282
- useAssistantTool({
283
- toolName: "select_date",
353
+ const toolkit = {
354
+ select_date: {
355
+ type: "human",
284
356
  description:
285
357
  "Ask the user to select a date. Use this when you need to collect a date (e.g. for scheduling, booking, deadlines).",
286
358
  parameters: z.object({
@@ -288,10 +360,10 @@ function FrontendTools() {
288
360
  minDate: z.string().optional().describe("Minimum date (ISO string)"),
289
361
  maxDate: z.string().optional().describe("Maximum date (ISO string)"),
290
362
  }),
291
- });
292
-
293
- useAssistantTool({
294
- toolName: "collect_contact",
363
+ render: DatePickerToolUI,
364
+ },
365
+ collect_contact: {
366
+ type: "human",
295
367
  description:
296
368
  "Collect contact information from the user. Use this when you need the user's name, email, or phone number.",
297
369
  parameters: z.object({
@@ -300,10 +372,17 @@ function FrontendTools() {
300
372
  .array(z.enum(["name", "email", "phone"]))
301
373
  .describe("Which fields to collect"),
302
374
  }),
303
- });
304
-
305
- return null;
306
- }
375
+ render: ContactFormToolUI,
376
+ },
377
+ generate_chart: {
378
+ type: "backend",
379
+ render: ChartToolUI,
380
+ },
381
+ show_location: {
382
+ type: "backend",
383
+ render: LocationToolUI,
384
+ },
385
+ } satisfies Toolkit;
307
386
 
308
387
  export default function Home() {
309
388
  const runtime = useChatRuntime({
@@ -311,6 +390,7 @@ export default function Home() {
311
390
  });
312
391
 
313
392
  const aui = useAui({
393
+ tools: Tools({ toolkit }),
314
394
  suggestions: Suggestions([
315
395
  {
316
396
  title: "Show a bar chart",
@@ -339,16 +419,12 @@ export default function Home() {
339
419
 
340
420
  return (
341
421
  <AssistantRuntimeProvider aui={aui} runtime={runtime}>
342
- {/* Frontend tools: register schemas, resolved via addResult in UI */}
343
- <FrontendTools />
344
- {/* Tool UIs: render components for each tool call */}
345
- <ChartToolUI />
346
- <LocationToolUI />
347
- <DatePickerToolUI />
348
- <ContactFormToolUI />
349
- <main className="h-full">
350
- <Thread />
351
- </main>
422
+ <div className="flex h-full flex-col">
423
+ <ExampleNav />
424
+ <main className="min-h-0 flex-1">
425
+ <Thread />
426
+ </main>
427
+ </div>
352
428
  </AssistantRuntimeProvider>
353
429
  );
354
430
  }
@@ -361,13 +437,11 @@ export default function Home() {
361
437
  "use client";
362
438
 
363
439
  import { GenerativeUIRender, type GenerativeUISpec } from "@assistant-ui/react";
364
- import { componentsAllowlist } from "@/components/gui";
365
-
366
- const UnknownComponentFallback = ({ component }: { component: string }) => (
367
- <span className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
368
- unknown component: {component}
369
- </span>
370
- );
440
+ import {
441
+ UnknownComponentFallback,
442
+ componentsAllowlist,
443
+ } from "@/components/gui";
444
+ import { ExampleNav } from "@/components/example-nav";
371
445
 
372
446
  /**
373
447
  * Self-contained demo of the GenerativeUI primitive.
@@ -433,23 +507,26 @@ const exampleSpec: GenerativeUISpec = {
433
507
 
434
508
  export default function GenerativeUIPrimitivePage() {
435
509
  return (
436
- <main className="mx-auto flex h-full max-w-2xl flex-col gap-6 p-8">
437
- <header>
438
- <h1 className="font-bold text-2xl">MessagePrimitive.GenerativeUI</h1>
439
- <p className="mt-1 text-muted-foreground text-sm">
440
- Agent-described React UI rendered from a JSON spec via a
441
- consumer-provided component allowlist.
442
- </p>
443
- </header>
444
-
445
- <section className="flex flex-col gap-4">
446
- <GenerativeUIRender
447
- spec={exampleSpec}
448
- components={componentsAllowlist}
449
- Fallback={UnknownComponentFallback}
450
- />
451
- </section>
452
- </main>
510
+ <div className="flex h-full flex-col">
511
+ <ExampleNav />
512
+ <main className="mx-auto flex h-full max-w-2xl flex-col gap-6 overflow-y-auto p-8">
513
+ <header>
514
+ <h1 className="text-2xl font-bold">MessagePrimitive.GenerativeUI</h1>
515
+ <p className="text-muted-foreground mt-1 text-sm">
516
+ Agent-described React UI rendered from a JSON spec via a
517
+ consumer-provided component allowlist.
518
+ </p>
519
+ </header>
520
+
521
+ <section className="flex flex-col gap-4">
522
+ <GenerativeUIRender
523
+ spec={exampleSpec}
524
+ components={componentsAllowlist}
525
+ Fallback={UnknownComponentFallback}
526
+ />
527
+ </section>
528
+ </main>
529
+ </div>
453
530
  );
454
531
  }
455
532
 
@@ -485,12 +562,257 @@ export default function GenerativeUIPrimitivePage() {
485
562
 
486
563
  ```
487
564
 
565
+ ## components/assistant-message-gui.tsx
566
+
567
+ ```tsx
568
+ "use client";
569
+
570
+ import { MarkdownText } from "@/components/assistant-ui/markdown-text";
571
+ import {
572
+ Reasoning,
573
+ ReasoningContent,
574
+ ReasoningRoot,
575
+ ReasoningText,
576
+ ReasoningTrigger,
577
+ } from "@/components/assistant-ui/reasoning";
578
+ import { ToolFallback } from "@/components/assistant-ui/tool-fallback";
579
+ import {
580
+ ToolGroupContent,
581
+ ToolGroupRoot,
582
+ ToolGroupTrigger,
583
+ } from "@/components/assistant-ui/tool-group";
584
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
585
+ import {
586
+ UnknownComponentFallback,
587
+ componentsAllowlist,
588
+ } from "@/components/gui";
589
+ import {
590
+ RENDER_GUI_TOOL_NAME,
591
+ isLeakedRenderGuiText,
592
+ parseRenderGuiResult,
593
+ } from "@/lib/render-gui-tool";
594
+ import { cn } from "@/lib/utils";
595
+ import type { GenerativeUISpec } from "@assistant-ui/react";
596
+ import {
597
+ ActionBarPrimitive,
598
+ AuiIf,
599
+ BranchPickerPrimitive,
600
+ ErrorPrimitive,
601
+ getMcpAppFromToolPart,
602
+ MessagePrimitive,
603
+ useAuiState,
604
+ } from "@assistant-ui/react";
605
+ import {
606
+ CheckIcon,
607
+ ChevronLeftIcon,
608
+ ChevronRightIcon,
609
+ CopyIcon,
610
+ RefreshCwIcon,
611
+ } from "lucide-react";
612
+ import { type FC, useEffect, useRef } from "react";
613
+
614
+ type AssistantMessageGuiProps = {
615
+ /** Set false in dev to surface the dropped-bridge warning. */
616
+ bridgeEnabled?: boolean;
617
+ };
618
+
619
+ const AllowlistedGenerativeUI = ({ spec }: { spec?: GenerativeUISpec }) => (
620
+ <MessagePrimitive.GenerativeUI
621
+ spec={spec}
622
+ components={componentsAllowlist}
623
+ Fallback={UnknownComponentFallback}
624
+ />
625
+ );
626
+
627
+ const useDroppedGenerativeUIWarning = (bridgeEnabled: boolean) => {
628
+ const content = useAuiState((s) => s.message.content);
629
+ const warned = useRef(false);
630
+
631
+ useEffect(() => {
632
+ if (
633
+ process.env.NODE_ENV === "production" ||
634
+ bridgeEnabled ||
635
+ warned.current
636
+ ) {
637
+ return;
638
+ }
639
+
640
+ const hasRenderGui = content.some(
641
+ (part) =>
642
+ part.type === "tool-call" &&
643
+ part.toolName === RENDER_GUI_TOOL_NAME &&
644
+ part.result != null &&
645
+ parseRenderGuiResult(part.result) != null,
646
+ );
647
+
648
+ const hasUnhandledGenerativeUI = content.some(
649
+ (part) => part.type === "generative-ui",
650
+ );
651
+
652
+ if (hasRenderGui || hasUnhandledGenerativeUI) {
653
+ warned.current = true;
654
+ console.warn(
655
+ "[generative-ui] Message contains render_gui or generative-ui parts but the bridge is disabled. " +
656
+ "Enable the bridge in assistant-message-gui.tsx or wire MessagePrimitive.GenerativeUI.",
657
+ );
658
+ }
659
+ }, [bridgeEnabled, content]);
660
+ };
661
+
662
+ export const AssistantMessageGui: FC<AssistantMessageGuiProps> = ({
663
+ bridgeEnabled = true,
664
+ }) => {
665
+ useDroppedGenerativeUIWarning(bridgeEnabled);
666
+
667
+ const hasBridgedRenderGui = useAuiState((s) =>
668
+ s.message.content.some(
669
+ (part) =>
670
+ part.type === "tool-call" &&
671
+ part.toolName === RENDER_GUI_TOOL_NAME &&
672
+ parseRenderGuiResult(part.result) != null,
673
+ ),
674
+ );
675
+
676
+ const ACTION_BAR_PT = "pt-1.5";
677
+ const ACTION_BAR_HEIGHT = `-mb-7.5 min-h-7.5 ${ACTION_BAR_PT}`;
678
+
679
+ return (
680
+ <MessagePrimitive.Root
681
+ data-slot="aui_assistant-message-root"
682
+ data-role="assistant"
683
+ className="fade-in slide-in-from-bottom-1 animate-in relative duration-150"
684
+ >
685
+ <div
686
+ data-slot="aui_assistant-message-content"
687
+ // [contain-intrinsic-size:auto_24px] fixes issue #4104, don't change without checking for regressions
688
+ className="text-foreground px-2 leading-relaxed wrap-break-word [contain-intrinsic-size:auto_24px] [content-visibility:auto]"
689
+ >
690
+ <MessagePrimitive.GroupedParts
691
+ groupBy={(part) => {
692
+ if (part.type === "reasoning") {
693
+ return ["group-chainOfThought", "group-reasoning"];
694
+ }
695
+ if (part.type === "tool-call") {
696
+ if (getMcpAppFromToolPart(part)) return [];
697
+ if (part.toolName === RENDER_GUI_TOOL_NAME) return [];
698
+ return ["group-chainOfThought", "group-tool"];
699
+ }
700
+ return [];
701
+ }}
702
+ >
703
+ {({ part, children }) => {
704
+ switch (part.type) {
705
+ case "group-chainOfThought":
706
+ return <div data-slot="aui_chain-of-thought">{children}</div>;
707
+ case "group-reasoning": {
708
+ const running = part.status.type === "running";
709
+ return (
710
+ <ReasoningRoot defaultOpen={running}>
711
+ <ReasoningTrigger active={running} />
712
+ <ReasoningContent aria-busy={running}>
713
+ <ReasoningText>{children}</ReasoningText>
714
+ </ReasoningContent>
715
+ </ReasoningRoot>
716
+ );
717
+ }
718
+ case "group-tool":
719
+ return (
720
+ <ToolGroupRoot>
721
+ <ToolGroupTrigger
722
+ count={part.indices.length}
723
+ active={part.status.type === "running"}
724
+ />
725
+ <ToolGroupContent>{children}</ToolGroupContent>
726
+ </ToolGroupRoot>
727
+ );
728
+ case "text":
729
+ if (
730
+ bridgeEnabled &&
731
+ hasBridgedRenderGui &&
732
+ isLeakedRenderGuiText(part.text)
733
+ ) {
734
+ return null;
735
+ }
736
+ return <MarkdownText />;
737
+ case "reasoning":
738
+ return <Reasoning {...part} />;
739
+ case "generative-ui":
740
+ return bridgeEnabled ? <AllowlistedGenerativeUI /> : null;
741
+ case "tool-call": {
742
+ if (bridgeEnabled && part.toolName === RENDER_GUI_TOOL_NAME) {
743
+ const spec = parseRenderGuiResult(part.result);
744
+ if (spec) return <AllowlistedGenerativeUI spec={spec} />;
745
+ }
746
+ return part.toolUI ?? <ToolFallback {...part} />;
747
+ }
748
+ default:
749
+ return null;
750
+ }
751
+ }}
752
+ </MessagePrimitive.GroupedParts>
753
+ <MessagePrimitive.Error>
754
+ <ErrorPrimitive.Root className="aui-message-error-root border-destructive bg-destructive/10 text-destructive dark:bg-destructive/5 mt-2 rounded-md border p-3 text-sm dark:text-red-200">
755
+ <ErrorPrimitive.Message className="aui-message-error-message line-clamp-2" />
756
+ </ErrorPrimitive.Root>
757
+ </MessagePrimitive.Error>
758
+ </div>
759
+
760
+ <div
761
+ data-slot="aui_assistant-message-footer"
762
+ className={cn("ms-2 flex items-center", ACTION_BAR_HEIGHT)}
763
+ >
764
+ <BranchPickerPrimitive.Root
765
+ hideWhenSingleBranch
766
+ className="aui-branch-picker-root text-muted-foreground -ms-2 me-2 inline-flex items-center text-xs"
767
+ >
768
+ <BranchPickerPrimitive.Previous asChild>
769
+ <TooltipIconButton tooltip="Previous">
770
+ <ChevronLeftIcon />
771
+ </TooltipIconButton>
772
+ </BranchPickerPrimitive.Previous>
773
+ <span className="aui-branch-picker-state font-medium">
774
+ <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
775
+ </span>
776
+ <BranchPickerPrimitive.Next asChild>
777
+ <TooltipIconButton tooltip="Next">
778
+ <ChevronRightIcon />
779
+ </TooltipIconButton>
780
+ </BranchPickerPrimitive.Next>
781
+ </BranchPickerPrimitive.Root>
782
+ <ActionBarPrimitive.Root
783
+ hideWhenRunning
784
+ autohide="not-last"
785
+ className="aui-assistant-action-bar-root text-muted-foreground -ms-1 flex gap-1"
786
+ >
787
+ <ActionBarPrimitive.Copy asChild>
788
+ <TooltipIconButton tooltip="Copy">
789
+ <AuiIf condition={(s) => s.message.isCopied}>
790
+ <CheckIcon />
791
+ </AuiIf>
792
+ <AuiIf condition={(s) => !s.message.isCopied}>
793
+ <CopyIcon />
794
+ </AuiIf>
795
+ </TooltipIconButton>
796
+ </ActionBarPrimitive.Copy>
797
+ <ActionBarPrimitive.Reload asChild>
798
+ <TooltipIconButton tooltip="Refresh">
799
+ <RefreshCwIcon />
800
+ </TooltipIconButton>
801
+ </ActionBarPrimitive.Reload>
802
+ </ActionBarPrimitive.Root>
803
+ </div>
804
+ </MessagePrimitive.Root>
805
+ );
806
+ };
807
+
808
+ ```
809
+
488
810
  ## components/chart-tool-ui.tsx
489
811
 
490
812
  ```tsx
491
813
  "use client";
492
814
 
493
- import { makeAssistantToolUI } from "@assistant-ui/react";
815
+ import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
494
816
  import {
495
817
  BarChart,
496
818
  Bar,
@@ -528,13 +850,12 @@ const COLORS = [
528
850
  "oklch(0.769 0.188 70.08)",
529
851
  ];
530
852
 
531
- export const ChartToolUI = makeAssistantToolUI<ChartArgs, ChartResult>({
532
- toolName: "generate_chart",
533
- render: function ChartUI({ args, status }) {
853
+ export const ChartToolUI: ToolCallMessagePartComponent<ChartArgs, ChartResult> =
854
+ function ChartUI({ args, status }) {
534
855
  if (status.type === "running" && !args.data?.length) {
535
856
  return (
536
857
  <div className="flex items-center gap-2 rounded-lg border p-4">
537
- <Loader2Icon className="size-4 animate-spin text-muted-foreground" />
858
+ <Loader2Icon className="text-muted-foreground size-4 animate-spin" />
538
859
  <span className="text-muted-foreground text-sm">
539
860
  Generating chart...
540
861
  </span>
@@ -547,7 +868,7 @@ export const ChartToolUI = makeAssistantToolUI<ChartArgs, ChartResult>({
547
868
 
548
869
  return (
549
870
  <div className="my-2 rounded-lg border p-4">
550
- <h3 className="mb-3 text-center font-semibold text-sm">{title}</h3>
871
+ <h3 className="mb-3 text-center text-sm font-semibold">{title}</h3>
551
872
  <div className="h-[280px] w-full">
552
873
  <ResponsiveContainer width="100%" height="100%">
553
874
  {type === "bar" ? (
@@ -620,8 +941,7 @@ export const ChartToolUI = makeAssistantToolUI<ChartArgs, ChartResult>({
620
941
  </div>
621
942
  </div>
622
943
  );
623
- },
624
- });
944
+ };
625
945
 
626
946
  ```
627
947
 
@@ -631,7 +951,7 @@ export const ChartToolUI = makeAssistantToolUI<ChartArgs, ChartResult>({
631
951
  "use client";
632
952
 
633
953
  import { useState } from "react";
634
- import { makeAssistantToolUI } from "@assistant-ui/react";
954
+ import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
635
955
  import { UserIcon, CheckCircle2Icon } from "lucide-react";
636
956
 
637
957
  type ContactFormArgs = {
@@ -645,86 +965,79 @@ type ContactFormResult = {
645
965
  phone?: string;
646
966
  };
647
967
 
648
- export const ContactFormToolUI = makeAssistantToolUI<
968
+ export const ContactFormToolUI: ToolCallMessagePartComponent<
649
969
  ContactFormArgs,
650
970
  ContactFormResult
651
- >({
652
- toolName: "collect_contact",
653
- render: function ContactFormUI({ args, result, addResult }) {
654
- const [form, setForm] = useState<ContactFormResult>({});
655
-
656
- if (result) {
657
- return (
658
- <div className="my-2 rounded-lg border border-green-200 bg-green-50 p-3">
659
- <div className="mb-1 flex items-center gap-2">
660
- <CheckCircle2Icon className="size-4 text-green-600" />
661
- <span className="font-medium text-green-800 text-sm">
662
- Contact info collected
663
- </span>
664
- </div>
665
- <div className="ml-6 space-y-0.5 text-green-700 text-xs">
666
- {result.name && <p>Name: {result.name}</p>}
667
- {result.email && <p>Email: {result.email}</p>}
668
- {result.phone && <p>Phone: {result.phone}</p>}
669
- </div>
670
- </div>
671
- );
672
- }
673
-
674
- const fields = args.fields ?? ["name", "email", "phone"];
675
- const isValid = fields.every((f) => form[f]?.trim());
971
+ > = function ContactFormUI({ args, result, addResult }) {
972
+ const [form, setForm] = useState<ContactFormResult>({});
676
973
 
974
+ if (result) {
677
975
  return (
678
- <div className="my-2 rounded-lg border p-4">
679
- <div className="mb-3 flex items-center gap-2">
680
- <UserIcon className="size-4 text-muted-foreground" />
681
- <span className="font-medium text-sm">{args.prompt}</span>
976
+ <div className="my-2 rounded-lg border border-green-200 bg-green-50 p-3">
977
+ <div className="mb-1 flex items-center gap-2">
978
+ <CheckCircle2Icon className="size-4 text-green-600" />
979
+ <span className="text-sm font-medium text-green-800">
980
+ Contact info collected
981
+ </span>
682
982
  </div>
683
- <div className="space-y-2">
684
- {fields.includes("name") && (
685
- <input
686
- type="text"
687
- placeholder="Name"
688
- value={form.name ?? ""}
689
- onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))}
690
- className="w-full rounded-md border bg-background px-3 py-2 text-sm"
691
- />
692
- )}
693
- {fields.includes("email") && (
694
- <input
695
- type="email"
696
- placeholder="Email"
697
- value={form.email ?? ""}
698
- onChange={(e) =>
699
- setForm((p) => ({ ...p, email: e.target.value }))
700
- }
701
- className="w-full rounded-md border bg-background px-3 py-2 text-sm"
702
- />
703
- )}
704
- {fields.includes("phone") && (
705
- <input
706
- type="tel"
707
- placeholder="Phone"
708
- value={form.phone ?? ""}
709
- onChange={(e) =>
710
- setForm((p) => ({ ...p, phone: e.target.value }))
711
- }
712
- className="w-full rounded-md border bg-background px-3 py-2 text-sm"
713
- />
714
- )}
715
- <button
716
- type="button"
717
- disabled={!isValid}
718
- onClick={() => addResult(form)}
719
- className="w-full rounded-md bg-primary px-4 py-2 font-medium text-primary-foreground text-sm disabled:opacity-50"
720
- >
721
- Submit
722
- </button>
983
+ <div className="ml-6 space-y-0.5 text-xs text-green-700">
984
+ {result.name && <p>Name: {result.name}</p>}
985
+ {result.email && <p>Email: {result.email}</p>}
986
+ {result.phone && <p>Phone: {result.phone}</p>}
723
987
  </div>
724
988
  </div>
725
989
  );
726
- },
727
- });
990
+ }
991
+
992
+ const fields = args.fields ?? ["name", "email", "phone"];
993
+ const isValid = fields.every((f) => form[f]?.trim());
994
+
995
+ return (
996
+ <div className="my-2 rounded-lg border p-4">
997
+ <div className="mb-3 flex items-center gap-2">
998
+ <UserIcon className="text-muted-foreground size-4" />
999
+ <span className="text-sm font-medium">{args.prompt}</span>
1000
+ </div>
1001
+ <div className="space-y-2">
1002
+ {fields.includes("name") && (
1003
+ <input
1004
+ type="text"
1005
+ placeholder="Name"
1006
+ value={form.name ?? ""}
1007
+ onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))}
1008
+ className="bg-background w-full rounded-md border px-3 py-2 text-sm"
1009
+ />
1010
+ )}
1011
+ {fields.includes("email") && (
1012
+ <input
1013
+ type="email"
1014
+ placeholder="Email"
1015
+ value={form.email ?? ""}
1016
+ onChange={(e) => setForm((p) => ({ ...p, email: e.target.value }))}
1017
+ className="bg-background w-full rounded-md border px-3 py-2 text-sm"
1018
+ />
1019
+ )}
1020
+ {fields.includes("phone") && (
1021
+ <input
1022
+ type="tel"
1023
+ placeholder="Phone"
1024
+ value={form.phone ?? ""}
1025
+ onChange={(e) => setForm((p) => ({ ...p, phone: e.target.value }))}
1026
+ className="bg-background w-full rounded-md border px-3 py-2 text-sm"
1027
+ />
1028
+ )}
1029
+ <button
1030
+ type="button"
1031
+ disabled={!isValid}
1032
+ onClick={() => addResult(form)}
1033
+ className="bg-primary text-primary-foreground w-full rounded-md px-4 py-2 text-sm font-medium disabled:opacity-50"
1034
+ >
1035
+ Submit
1036
+ </button>
1037
+ </div>
1038
+ </div>
1039
+ );
1040
+ };
728
1041
 
729
1042
  ```
730
1043
 
@@ -734,7 +1047,7 @@ export const ContactFormToolUI = makeAssistantToolUI<
734
1047
  "use client";
735
1048
 
736
1049
  import { useState } from "react";
737
- import { makeAssistantToolUI } from "@assistant-ui/react";
1050
+ import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
738
1051
  import { CalendarIcon, CheckCircle2Icon } from "lucide-react";
739
1052
 
740
1053
  type DatePickerArgs = {
@@ -747,53 +1060,378 @@ type DatePickerResult = {
747
1060
  date: string;
748
1061
  };
749
1062
 
750
- export const DatePickerToolUI = makeAssistantToolUI<
1063
+ export const DatePickerToolUI: ToolCallMessagePartComponent<
751
1064
  DatePickerArgs,
752
1065
  DatePickerResult
753
- >({
754
- toolName: "select_date",
755
- render: function DatePickerUI({ args, result, addResult }) {
756
- const [value, setValue] = useState("");
1066
+ > = function DatePickerUI({ args, result, addResult }) {
1067
+ const [value, setValue] = useState("");
757
1068
 
758
- if (result) {
759
- return (
760
- <div className="my-2 flex items-center gap-2 rounded-lg border border-green-200 bg-green-50 p-3">
761
- <CheckCircle2Icon className="size-4 text-green-600" />
762
- <span className="text-green-800 text-sm">
763
- Selected: {new Date(result.date).toLocaleDateString()}
764
- </span>
1069
+ if (result) {
1070
+ return (
1071
+ <div className="my-2 flex items-center gap-2 rounded-lg border border-green-200 bg-green-50 p-3">
1072
+ <CheckCircle2Icon className="size-4 text-green-600" />
1073
+ <span className="text-sm text-green-800">
1074
+ Selected: {new Date(result.date).toLocaleDateString()}
1075
+ </span>
1076
+ </div>
1077
+ );
1078
+ }
1079
+
1080
+ return (
1081
+ <div className="my-2 rounded-lg border p-4">
1082
+ <div className="mb-3 flex items-center gap-2">
1083
+ <CalendarIcon className="text-muted-foreground size-4" />
1084
+ <span className="text-sm font-medium">{args.prompt}</span>
1085
+ </div>
1086
+ <div className="flex items-center gap-2">
1087
+ <input
1088
+ type="date"
1089
+ value={value}
1090
+ min={args.minDate}
1091
+ max={args.maxDate}
1092
+ onChange={(e) => setValue(e.target.value)}
1093
+ className="bg-background rounded-md border px-3 py-2 text-sm"
1094
+ />
1095
+ <button
1096
+ type="button"
1097
+ disabled={!value}
1098
+ onClick={() => addResult({ date: new Date(value).toISOString() })}
1099
+ className="bg-primary text-primary-foreground rounded-md px-4 py-2 text-sm font-medium disabled:opacity-50"
1100
+ >
1101
+ Confirm
1102
+ </button>
1103
+ </div>
1104
+ </div>
1105
+ );
1106
+ };
1107
+
1108
+ ```
1109
+
1110
+ ## components/example-nav.tsx
1111
+
1112
+ ```tsx
1113
+ import Link from "next/link";
1114
+
1115
+ const links = [
1116
+ { href: "/", label: "Tool UI demo" },
1117
+ { href: "/primitive", label: "Static primitive" },
1118
+ { href: "/gui-chat", label: "GUI chat" },
1119
+ ] as const;
1120
+
1121
+ export function ExampleNav() {
1122
+ return (
1123
+ <nav className="bg-background flex flex-wrap items-center gap-2 border-b px-4 py-2 text-sm">
1124
+ <span className="text-muted-foreground font-medium">Examples:</span>
1125
+ {links.map(({ href, label }) => (
1126
+ <Link
1127
+ key={href}
1128
+ href={href}
1129
+ className="text-foreground hover:bg-muted rounded-md px-2 py-1"
1130
+ >
1131
+ {label}
1132
+ </Link>
1133
+ ))}
1134
+ </nav>
1135
+ );
1136
+ }
1137
+
1138
+ ```
1139
+
1140
+ ## components/gui-thread.tsx
1141
+
1142
+ ```tsx
1143
+ import {
1144
+ ComposerAddAttachment,
1145
+ ComposerAttachments,
1146
+ UserMessageAttachments,
1147
+ } from "@/components/assistant-ui/attachment";
1148
+ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
1149
+ import { Button } from "@/components/ui/button";
1150
+ import { cn } from "@/lib/utils";
1151
+ import {
1152
+ ActionBarPrimitive,
1153
+ AuiIf,
1154
+ BranchPickerPrimitive,
1155
+ ComposerPrimitive,
1156
+ MessagePrimitive,
1157
+ SuggestionPrimitive,
1158
+ ThreadPrimitive,
1159
+ useAuiState,
1160
+ } from "@assistant-ui/react";
1161
+ import {
1162
+ ArrowDownIcon,
1163
+ ArrowUpIcon,
1164
+ ChevronLeftIcon,
1165
+ ChevronRightIcon,
1166
+ PencilIcon,
1167
+ SquareIcon,
1168
+ } from "lucide-react";
1169
+ import type { FC } from "react";
1170
+ import { AssistantMessageGui } from "@/components/assistant-message-gui";
1171
+
1172
+ /** Thread fork: swaps the assistant message renderer for the generative-ui bridge. */
1173
+ export const GuiThread: FC = () => {
1174
+ return (
1175
+ <ThreadPrimitive.Root
1176
+ className="aui-root aui-thread-root bg-background @container flex h-full flex-col"
1177
+ style={{
1178
+ ["--thread-max-width" as string]: "44rem",
1179
+ ["--composer-radius" as string]: "24px",
1180
+ ["--composer-padding" as string]: "10px",
1181
+ }}
1182
+ >
1183
+ <ThreadPrimitive.Viewport
1184
+ turnAnchor="top"
1185
+ data-slot="aui_thread-viewport"
1186
+ className="relative flex flex-1 flex-col overflow-x-auto overflow-y-scroll scroll-smooth"
1187
+ >
1188
+ <div className="mx-auto flex w-full max-w-(--thread-max-width) flex-1 flex-col px-4 pt-4">
1189
+ <AuiIf condition={(s) => s.thread.isEmpty}>
1190
+ <ThreadWelcome />
1191
+ </AuiIf>
1192
+
1193
+ <div
1194
+ data-slot="aui_message-group"
1195
+ className="mb-10 flex flex-col gap-y-8 empty:hidden"
1196
+ >
1197
+ <ThreadPrimitive.Messages>
1198
+ {() => <ThreadMessage />}
1199
+ </ThreadPrimitive.Messages>
1200
+ </div>
1201
+
1202
+ <ThreadPrimitive.ViewportFooter className="aui-thread-viewport-footer bg-background sticky bottom-0 mt-auto flex flex-col gap-4 overflow-visible rounded-t-(--composer-radius) pb-4 md:pb-6">
1203
+ <ThreadScrollToBottom />
1204
+ <Composer />
1205
+ </ThreadPrimitive.ViewportFooter>
765
1206
  </div>
766
- );
767
- }
1207
+ </ThreadPrimitive.Viewport>
1208
+ </ThreadPrimitive.Root>
1209
+ );
1210
+ };
768
1211
 
769
- return (
770
- <div className="my-2 rounded-lg border p-4">
771
- <div className="mb-3 flex items-center gap-2">
772
- <CalendarIcon className="size-4 text-muted-foreground" />
773
- <span className="font-medium text-sm">{args.prompt}</span>
1212
+ const ThreadMessage: FC = () => {
1213
+ const role = useAuiState((s) => s.message.role);
1214
+ const isEditing = useAuiState((s) => s.message.composer.isEditing);
1215
+
1216
+ if (isEditing) return <EditComposer />;
1217
+ if (role === "user") return <UserMessage />;
1218
+ return <AssistantMessageGui />;
1219
+ };
1220
+
1221
+ const ThreadScrollToBottom: FC = () => {
1222
+ return (
1223
+ <ThreadPrimitive.ScrollToBottom asChild>
1224
+ <TooltipIconButton
1225
+ tooltip="Scroll to bottom"
1226
+ variant="outline"
1227
+ className="aui-thread-scroll-to-bottom dark:border-border dark:bg-background dark:hover:bg-accent absolute -top-12 z-10 self-center rounded-full p-4 disabled:invisible"
1228
+ >
1229
+ <ArrowDownIcon />
1230
+ </TooltipIconButton>
1231
+ </ThreadPrimitive.ScrollToBottom>
1232
+ );
1233
+ };
1234
+
1235
+ const ThreadWelcome: FC = () => {
1236
+ return (
1237
+ <div className="aui-thread-welcome-root my-auto flex grow flex-col">
1238
+ <div className="aui-thread-welcome-center flex w-full grow flex-col items-center justify-center">
1239
+ <div className="aui-thread-welcome-message flex size-full flex-col justify-center px-4">
1240
+ <h1 className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-2xl font-semibold duration-200">
1241
+ Compose UI from a spec
1242
+ </h1>
1243
+ <p className="aui-thread-welcome-message-inner fade-in slide-in-from-bottom-1 animate-in fill-mode-both text-muted-foreground text-xl delay-75 duration-200">
1244
+ Ask the agent to call render_gui with Card, Button, and Stack.
1245
+ </p>
774
1246
  </div>
775
- <div className="flex items-center gap-2">
776
- <input
777
- type="date"
778
- value={value}
779
- min={args.minDate}
780
- max={args.maxDate}
781
- onChange={(e) => setValue(e.target.value)}
782
- className="rounded-md border bg-background px-3 py-2 text-sm"
1247
+ </div>
1248
+ <ThreadSuggestions />
1249
+ </div>
1250
+ );
1251
+ };
1252
+
1253
+ const ThreadSuggestions: FC = () => {
1254
+ return (
1255
+ <div className="aui-thread-welcome-suggestions grid w-full gap-2 pb-4 @md:grid-cols-2">
1256
+ <ThreadPrimitive.Suggestions>
1257
+ {() => <ThreadSuggestionItem />}
1258
+ </ThreadPrimitive.Suggestions>
1259
+ </div>
1260
+ );
1261
+ };
1262
+
1263
+ const ThreadSuggestionItem: FC = () => {
1264
+ return (
1265
+ <div className="aui-thread-welcome-suggestion-display fade-in slide-in-from-bottom-2 animate-in fill-mode-both duration-200 nth-[n+3]:hidden @md:nth-[n+3]:block">
1266
+ <SuggestionPrimitive.Trigger send asChild>
1267
+ <Button
1268
+ variant="ghost"
1269
+ className="aui-thread-welcome-suggestion bg-background hover:bg-muted h-auto w-full flex-wrap items-start justify-start gap-1 rounded-3xl border px-4 py-3 text-start text-sm transition-colors @md:flex-col"
1270
+ >
1271
+ <SuggestionPrimitive.Title className="aui-thread-welcome-suggestion-text-1 font-medium" />
1272
+ <SuggestionPrimitive.Description className="aui-thread-welcome-suggestion-text-2 text-muted-foreground empty:hidden" />
1273
+ </Button>
1274
+ </SuggestionPrimitive.Trigger>
1275
+ </div>
1276
+ );
1277
+ };
1278
+
1279
+ const Composer: FC = () => {
1280
+ return (
1281
+ <ComposerPrimitive.Root className="aui-composer-root relative flex w-full flex-col">
1282
+ <ComposerPrimitive.AttachmentDropzone asChild>
1283
+ <div
1284
+ data-slot="aui_composer-shell"
1285
+ className="bg-background focus-within:border-ring/75 focus-within:ring-ring/20 data-[dragging=true]:border-ring data-[dragging=true]:bg-accent/50 flex w-full flex-col gap-2 rounded-(--composer-radius) border p-(--composer-padding) transition-shadow focus-within:ring-2 data-[dragging=true]:border-dashed"
1286
+ >
1287
+ <ComposerAttachments />
1288
+ <ComposerPrimitive.Input
1289
+ placeholder="Send a message..."
1290
+ className="aui-composer-input placeholder:text-muted-foreground/80 max-h-32 min-h-10 w-full resize-none bg-transparent px-1.75 py-1 text-sm outline-none"
1291
+ rows={1}
1292
+ autoFocus
1293
+ aria-label="Message input"
783
1294
  />
784
- <button
1295
+ <ComposerAction />
1296
+ </div>
1297
+ </ComposerPrimitive.AttachmentDropzone>
1298
+ </ComposerPrimitive.Root>
1299
+ );
1300
+ };
1301
+
1302
+ const ComposerAction: FC = () => {
1303
+ return (
1304
+ <div className="aui-composer-action-wrapper relative flex items-center justify-between">
1305
+ <ComposerAddAttachment />
1306
+ <AuiIf condition={(s) => !s.thread.isRunning}>
1307
+ <ComposerPrimitive.Send asChild>
1308
+ <TooltipIconButton
1309
+ tooltip="Send message"
1310
+ side="bottom"
1311
+ type="button"
1312
+ variant="default"
1313
+ size="icon"
1314
+ className="aui-composer-send size-8 rounded-full"
1315
+ aria-label="Send message"
1316
+ >
1317
+ <ArrowUpIcon className="aui-composer-send-icon size-4" />
1318
+ </TooltipIconButton>
1319
+ </ComposerPrimitive.Send>
1320
+ </AuiIf>
1321
+ <AuiIf condition={(s) => s.thread.isRunning}>
1322
+ <ComposerPrimitive.Cancel asChild>
1323
+ <Button
785
1324
  type="button"
786
- disabled={!value}
787
- onClick={() => addResult({ date: new Date(value).toISOString() })}
788
- className="rounded-md bg-primary px-4 py-2 font-medium text-primary-foreground text-sm disabled:opacity-50"
1325
+ variant="default"
1326
+ size="icon"
1327
+ className="aui-composer-cancel size-8 rounded-full"
1328
+ aria-label="Stop generating"
789
1329
  >
790
- Confirm
791
- </button>
1330
+ <SquareIcon className="aui-composer-cancel-icon size-3 fill-current" />
1331
+ </Button>
1332
+ </ComposerPrimitive.Cancel>
1333
+ </AuiIf>
1334
+ </div>
1335
+ );
1336
+ };
1337
+
1338
+ const UserMessage: FC = () => {
1339
+ return (
1340
+ <MessagePrimitive.Root
1341
+ data-slot="aui_user-message-root"
1342
+ className="fade-in slide-in-from-bottom-1 animate-in grid auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 duration-150 [contain-intrinsic-size:auto_60px] [content-visibility:auto] [&:where(>*)]:col-start-2"
1343
+ data-role="user"
1344
+ >
1345
+ <UserMessageAttachments />
1346
+
1347
+ <div className="aui-user-message-content-wrapper relative col-start-2 min-w-0">
1348
+ <div className="aui-user-message-content peer bg-muted text-foreground rounded-2xl px-4 py-2.5 wrap-break-word empty:hidden">
1349
+ <MessagePrimitive.Parts />
1350
+ </div>
1351
+ <div className="aui-user-action-bar-wrapper absolute start-0 top-1/2 -translate-x-full -translate-y-1/2 pe-2 peer-empty:hidden rtl:translate-x-full">
1352
+ <UserActionBar />
792
1353
  </div>
793
1354
  </div>
794
- );
795
- },
796
- });
1355
+
1356
+ <BranchPicker
1357
+ data-slot="aui_user-branch-picker"
1358
+ className="col-span-full col-start-1 row-start-3 -me-1 justify-end"
1359
+ />
1360
+ </MessagePrimitive.Root>
1361
+ );
1362
+ };
1363
+
1364
+ const UserActionBar: FC = () => {
1365
+ return (
1366
+ <ActionBarPrimitive.Root
1367
+ hideWhenRunning
1368
+ autohide="not-last"
1369
+ className="aui-user-action-bar-root flex flex-col items-end"
1370
+ >
1371
+ <ActionBarPrimitive.Edit asChild>
1372
+ <TooltipIconButton tooltip="Edit" className="aui-user-action-edit p-4">
1373
+ <PencilIcon />
1374
+ </TooltipIconButton>
1375
+ </ActionBarPrimitive.Edit>
1376
+ </ActionBarPrimitive.Root>
1377
+ );
1378
+ };
1379
+
1380
+ const EditComposer: FC = () => {
1381
+ return (
1382
+ <MessagePrimitive.Root
1383
+ data-slot="aui_edit-composer-wrapper"
1384
+ className="flex flex-col px-2"
1385
+ >
1386
+ <ComposerPrimitive.Root className="aui-edit-composer-root bg-muted ms-auto flex w-full max-w-[85%] flex-col rounded-2xl">
1387
+ <ComposerPrimitive.Input
1388
+ className="aui-edit-composer-input text-foreground min-h-14 w-full resize-none bg-transparent p-4 text-sm outline-none"
1389
+ autoFocus
1390
+ />
1391
+ <div className="aui-edit-composer-footer mx-3 mb-3 flex items-center gap-2 self-end">
1392
+ <ComposerPrimitive.Cancel asChild>
1393
+ <Button variant="ghost" size="sm">
1394
+ Cancel
1395
+ </Button>
1396
+ </ComposerPrimitive.Cancel>
1397
+ <ComposerPrimitive.Send asChild>
1398
+ <Button size="sm">Update</Button>
1399
+ </ComposerPrimitive.Send>
1400
+ </div>
1401
+ </ComposerPrimitive.Root>
1402
+ </MessagePrimitive.Root>
1403
+ );
1404
+ };
1405
+
1406
+ const BranchPicker: FC<BranchPickerPrimitive.Root.Props> = ({
1407
+ className,
1408
+ ...rest
1409
+ }) => {
1410
+ return (
1411
+ <BranchPickerPrimitive.Root
1412
+ hideWhenSingleBranch
1413
+ className={cn(
1414
+ "aui-branch-picker-root text-muted-foreground -ms-2 me-2 inline-flex items-center text-xs",
1415
+ className,
1416
+ )}
1417
+ {...rest}
1418
+ >
1419
+ <BranchPickerPrimitive.Previous asChild>
1420
+ <TooltipIconButton tooltip="Previous">
1421
+ <ChevronLeftIcon />
1422
+ </TooltipIconButton>
1423
+ </BranchPickerPrimitive.Previous>
1424
+ <span className="aui-branch-picker-state font-medium">
1425
+ <BranchPickerPrimitive.Number /> / <BranchPickerPrimitive.Count />
1426
+ </span>
1427
+ <BranchPickerPrimitive.Next asChild>
1428
+ <TooltipIconButton tooltip="Next">
1429
+ <ChevronRightIcon />
1430
+ </TooltipIconButton>
1431
+ </BranchPickerPrimitive.Next>
1432
+ </BranchPickerPrimitive.Root>
1433
+ );
1434
+ };
797
1435
 
798
1436
  ```
799
1437
 
@@ -809,13 +1447,23 @@ import type { ComponentType, PropsWithChildren } from "react";
809
1447
  * The agent's JSON spec can reference any of these by name.
810
1448
  */
811
1449
 
1450
+ export const UnknownComponentFallback = ({
1451
+ component,
1452
+ }: {
1453
+ component: string;
1454
+ }) => (
1455
+ <span className="bg-muted rounded px-1.5 py-0.5 font-mono text-xs">
1456
+ unknown component: {component}
1457
+ </span>
1458
+ );
1459
+
812
1460
  const Card: ComponentType<
813
1461
  PropsWithChildren<{ title?: string; description?: string }>
814
1462
  > = ({ title, description, children }) => (
815
- <div className="rounded-xl border bg-card p-4 shadow-sm">
816
- {title ? <div className="font-semibold text-base">{title}</div> : null}
1463
+ <div className="bg-card rounded-xl border p-4 shadow-sm">
1464
+ {title ? <div className="text-base font-semibold">{title}</div> : null}
817
1465
  {description ? (
818
- <div className="mt-1 text-muted-foreground text-sm">{description}</div>
1466
+ <div className="text-muted-foreground mt-1 text-sm">{description}</div>
819
1467
  ) : null}
820
1468
  {children ? <div className="mt-3">{children}</div> : null}
821
1469
  </div>
@@ -835,7 +1483,7 @@ const Button: ComponentType<
835
1483
  return (
836
1484
  <button
837
1485
  type="button"
838
- className={`mt-2 inline-flex items-center rounded-md px-3 py-1.5 font-medium text-sm ${cls}`}
1486
+ className={`mt-2 inline-flex items-center rounded-md px-3 py-1.5 text-sm font-medium ${cls}`}
839
1487
  onClick={() => {
840
1488
  if (onClickPrompt) {
841
1489
  // In a richer integration this would call composer.setText / send.
@@ -877,9 +1525,9 @@ const Stat: ComponentType<{ label: string; value: string | number }> = ({
877
1525
  label,
878
1526
  value,
879
1527
  }) => (
880
- <div className="flex flex-col rounded-lg border bg-muted/40 p-3">
1528
+ <div className="bg-muted/40 flex flex-col rounded-lg border p-3">
881
1529
  <span className="text-muted-foreground text-xs">{label}</span>
882
- <span className="font-semibold text-xl">{value}</span>
1530
+ <span className="text-xl font-semibold">{value}</span>
883
1531
  </div>
884
1532
  );
885
1533
 
@@ -899,7 +1547,7 @@ export const componentsAllowlist = {
899
1547
  ```tsx
900
1548
  "use client";
901
1549
 
902
- import { makeAssistantToolUI } from "@assistant-ui/react";
1550
+ import type { ToolCallMessagePartComponent } from "@assistant-ui/react";
903
1551
  import { MapPinIcon, Loader2Icon } from "lucide-react";
904
1552
 
905
1553
  type LocationArgs = {
@@ -913,60 +1561,199 @@ type LocationResult = {
913
1561
  success: boolean;
914
1562
  };
915
1563
 
916
- export const LocationToolUI = makeAssistantToolUI<LocationArgs, LocationResult>(
917
- {
918
- toolName: "show_location",
919
- render: function LocationUI({ args, status }) {
920
- if (status.type === "running" && !args.lat) {
921
- return (
922
- <div className="flex items-center gap-2 rounded-lg border p-4">
923
- <Loader2Icon className="size-4 animate-spin text-muted-foreground" />
924
- <span className="text-muted-foreground text-sm">
925
- Loading location...
926
- </span>
927
- </div>
928
- );
929
- }
1564
+ export const LocationToolUI: ToolCallMessagePartComponent<
1565
+ LocationArgs,
1566
+ LocationResult
1567
+ > = function LocationUI({ args, status }) {
1568
+ if (status.type === "running" && (args.lat == null || args.lng == null)) {
1569
+ return (
1570
+ <div className="flex items-center gap-2 rounded-lg border p-4">
1571
+ <Loader2Icon className="text-muted-foreground size-4 animate-spin" />
1572
+ <span className="text-muted-foreground text-sm">
1573
+ Loading location...
1574
+ </span>
1575
+ </div>
1576
+ );
1577
+ }
930
1578
 
931
- const { name, address, lat, lng } = args;
932
- if (!lat || !lng) return null;
1579
+ const { name, address, lat, lng } = args;
1580
+ if (lat == null || lng == null) return null;
933
1581
 
934
- const mapSrc = `https://www.openstreetmap.org/export/embed.html?bbox=${lng - 0.01},${lat - 0.01},${lng + 0.01},${lat + 0.01}&layer=mapnik&marker=${lat},${lng}`;
1582
+ const mapSrc = `https://www.openstreetmap.org/export/embed.html?bbox=${lng - 0.01},${lat - 0.01},${lng + 0.01},${lat + 0.01}&layer=mapnik&marker=${lat},${lng}`;
935
1583
 
936
- return (
937
- <div className="my-2 overflow-hidden rounded-lg border">
938
- <div className="flex items-center gap-2 border-b px-3 py-2">
939
- <MapPinIcon className="size-4 text-muted-foreground" />
940
- <div>
941
- <p className="font-medium text-sm">{name}</p>
942
- {address && (
943
- <p className="text-muted-foreground text-xs">{address}</p>
944
- )}
945
- </div>
946
- </div>
947
- <iframe
948
- title={`Map of ${name}`}
949
- src={mapSrc}
950
- className="h-[200px] w-full border-0"
951
- />
1584
+ return (
1585
+ <div className="my-2 overflow-hidden rounded-lg border">
1586
+ <div className="flex items-center gap-2 border-b px-3 py-2">
1587
+ <MapPinIcon className="text-muted-foreground size-4" />
1588
+ <div>
1589
+ <p className="text-sm font-medium">{name}</p>
1590
+ {address && (
1591
+ <p className="text-muted-foreground text-xs">{address}</p>
1592
+ )}
952
1593
  </div>
953
- );
954
- },
955
- },
1594
+ </div>
1595
+ <iframe
1596
+ title={`Map of ${name}`}
1597
+ src={mapSrc}
1598
+ className="h-[200px] w-full border-0"
1599
+ />
1600
+ </div>
1601
+ );
1602
+ };
1603
+
1604
+ ```
1605
+
1606
+ ## lib/render-gui-tool.test.ts
1607
+
1608
+ ```typescript
1609
+ import { describe, expect, it } from "vitest";
1610
+ import {
1611
+ generativeUISpecSchema,
1612
+ isLeakedRenderGuiText,
1613
+ parseRenderGuiResult,
1614
+ renderGuiToolResultSchema,
1615
+ } from "./render-gui-tool";
1616
+
1617
+ describe("render-gui-tool", () => {
1618
+ it("accepts a valid Card + Button spec", () => {
1619
+ const spec = {
1620
+ root: {
1621
+ component: "Card",
1622
+ props: { title: "Welcome" },
1623
+ children: [
1624
+ {
1625
+ component: "Button",
1626
+ props: { label: "Get started", variant: "primary" },
1627
+ },
1628
+ ],
1629
+ },
1630
+ };
1631
+
1632
+ expect(generativeUISpecSchema.parse(spec)).toEqual(spec);
1633
+ expect(
1634
+ parseRenderGuiResult(renderGuiToolResultSchema.parse({ spec })),
1635
+ ).toEqual(spec);
1636
+ });
1637
+
1638
+ it("rejects a spec with empty component name", () => {
1639
+ expect(() =>
1640
+ generativeUISpecSchema.parse({
1641
+ root: { component: "", props: { title: "Bad" } },
1642
+ }),
1643
+ ).toThrow();
1644
+ });
1645
+
1646
+ it("returns undefined for malformed tool results", () => {
1647
+ expect(parseRenderGuiResult({ notSpec: true })).toBeUndefined();
1648
+ expect(parseRenderGuiResult(null)).toBeUndefined();
1649
+ });
1650
+
1651
+ it("detects leaked render_gui tool result text", () => {
1652
+ const payload = renderGuiToolResultSchema.parse({
1653
+ spec: {
1654
+ root: {
1655
+ component: "Card",
1656
+ children: [{ component: "Text", children: ["Hello"] }],
1657
+ },
1658
+ },
1659
+ });
1660
+
1661
+ expect(isLeakedRenderGuiText(JSON.stringify(payload, null, 2))).toBe(true);
1662
+ expect(
1663
+ isLeakedRenderGuiText(
1664
+ "```json\n" + JSON.stringify(payload, null, 2) + "\n```",
1665
+ ),
1666
+ ).toBe(true);
1667
+ expect(isLeakedRenderGuiText("Here is your card.")).toBe(false);
1668
+ });
1669
+ });
1670
+
1671
+ ```
1672
+
1673
+ ## lib/render-gui-tool.ts
1674
+
1675
+ ```typescript
1676
+ import { z } from "zod";
1677
+ import type { GenerativeUISpec } from "@assistant-ui/react";
1678
+
1679
+ export const RENDER_GUI_TOOL_NAME = "render_gui" as const;
1680
+
1681
+ const generativeUINodeSchema: z.ZodType<unknown> = z.lazy(() =>
1682
+ z.union([
1683
+ z.string(),
1684
+ z.object({
1685
+ component: z.string().min(1),
1686
+ props: z.record(z.string(), z.unknown()).optional(),
1687
+ children: z.array(generativeUINodeSchema).optional(),
1688
+ key: z.string().optional(),
1689
+ }),
1690
+ ]),
956
1691
  );
957
1692
 
1693
+ export const generativeUISpecSchema = z.object({
1694
+ root: z.union([
1695
+ generativeUINodeSchema,
1696
+ z.array(generativeUINodeSchema).min(1),
1697
+ ]),
1698
+ });
1699
+
1700
+ export const renderGuiToolInputSchema = z.object({
1701
+ spec: generativeUISpecSchema,
1702
+ });
1703
+
1704
+ export const renderGuiToolResultSchema = renderGuiToolInputSchema;
1705
+
1706
+ export const parseRenderGuiResult = (
1707
+ result: unknown,
1708
+ ): GenerativeUISpec | undefined => {
1709
+ const parsed = renderGuiToolResultSchema.safeParse(result);
1710
+ return parsed.success ? (parsed.data.spec as GenerativeUISpec) : undefined;
1711
+ };
1712
+
1713
+ export const renderGuiToolDescription =
1714
+ "Compose inline UI from the allowlisted component library (Card, Text, Button, Stack, Stat, Heading). " +
1715
+ "Pass a JSON spec with a root node tree. Use for dashboards, status panels, and structured layouts — not for user input forms (use interactive tool UI instead).";
1716
+
1717
+ export const renderGuiChatInstructions =
1718
+ "When the user asks for UI, call render_gui with the spec. " +
1719
+ "Do not paste JSON, code blocks, or the tool result in your text reply — the client renders the UI from the tool output automatically. " +
1720
+ "Keep any text reply brief or omit it when the UI alone answers the request.";
1721
+
1722
+ const stripMarkdownJsonFence = (text: string): string => {
1723
+ const trimmed = text.trim();
1724
+ const match = trimmed.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```\s*$/i);
1725
+ return match ? match[1]!.trim() : trimmed;
1726
+ };
1727
+
1728
+ /** Text echo of a render_gui tool result/spec — hide when the bridge already rendered it. */
1729
+ export const isLeakedRenderGuiText = (text: string): boolean => {
1730
+ const candidate = stripMarkdownJsonFence(text);
1731
+ if (!candidate.startsWith("{")) return false;
1732
+
1733
+ try {
1734
+ const parsed = JSON.parse(candidate);
1735
+ if (renderGuiToolResultSchema.safeParse(parsed).success) return true;
1736
+ if (generativeUISpecSchema.safeParse(parsed).success) return true;
1737
+ } catch {
1738
+ return false;
1739
+ }
1740
+
1741
+ return false;
1742
+ };
1743
+
958
1744
  ```
959
1745
 
960
1746
  ## next.config.ts
961
1747
 
962
1748
  ```typescript
1749
+ import { withAui } from "@assistant-ui/next";
963
1750
  import type { NextConfig } from "next";
964
1751
 
965
1752
  const nextConfig: NextConfig = {
966
1753
  /* config options here */
967
1754
  };
968
1755
 
969
- export default nextConfig;
1756
+ export default withAui(nextConfig);
970
1757
 
971
1758
  ```
972
1759
 
@@ -981,17 +1768,18 @@ export default nextConfig;
981
1768
  "scripts": {
982
1769
  "dev": "next dev",
983
1770
  "build": "next build",
984
- "start": "next start"
1771
+ "start": "next start",
1772
+ "test": "vitest run"
985
1773
  },
986
1774
  "dependencies": {
987
- "@ai-sdk/openai": "^3.0.64",
1775
+ "@ai-sdk/openai": "^3.0.67",
988
1776
  "@assistant-ui/react": "workspace:*",
989
1777
  "@assistant-ui/react-ai-sdk": "workspace:*",
990
1778
  "@assistant-ui/ui": "workspace:*",
991
- "ai": "^6.0.188",
1779
+ "ai": "^6.0.193",
992
1780
  "class-variance-authority": "^0.7.1",
993
1781
  "clsx": "^2.1.1",
994
- "lucide-react": "^1.16.0",
1782
+ "lucide-react": "^1.17.0",
995
1783
  "next": "^16.2.6",
996
1784
  "react": "^19.2.6",
997
1785
  "react-dom": "^19.2.6",
@@ -1000,6 +1788,7 @@ export default nextConfig;
1000
1788
  "zod": "^4.4.3"
1001
1789
  },
1002
1790
  "devDependencies": {
1791
+ "@assistant-ui/next": "workspace:*",
1003
1792
  "@assistant-ui/x-buildutils": "workspace:*",
1004
1793
  "@tailwindcss/postcss": "^4.3.0",
1005
1794
  "@types/node": "^25.9.1",
@@ -1008,7 +1797,8 @@ export default nextConfig;
1008
1797
  "postcss": "^8.5.15",
1009
1798
  "tailwindcss": "^4.3.0",
1010
1799
  "tw-animate-css": "^1.4.0",
1011
- "typescript": "^6.0.3"
1800
+ "typescript": "^6.0.3",
1801
+ "vitest": "^4.1.7"
1012
1802
  }
1013
1803
  }
1014
1804
 
@@ -1036,3 +1826,17 @@ export default nextConfig;
1036
1826
 
1037
1827
  ```
1038
1828
 
1829
+ ## vitest.config.ts
1830
+
1831
+ ```typescript
1832
+ import { defineConfig } from "vitest/config";
1833
+
1834
+ export default defineConfig({
1835
+ test: {
1836
+ environment: "node",
1837
+ include: ["lib/**/*.test.ts"],
1838
+ },
1839
+ });
1840
+
1841
+ ```
1842
+