@assistant-ui/mcp-docs-server 0.1.39 → 0.2.0

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 (136) hide show
  1. package/.docs/organized/code-examples/waterfall.md +7 -7
  2. package/.docs/organized/code-examples/with-a2a.md +7 -7
  3. package/.docs/organized/code-examples/with-ag-ui.md +8 -8
  4. package/.docs/organized/code-examples/with-ai-sdk-v7.md +9 -9
  5. package/.docs/organized/code-examples/with-artifacts.md +463 -138
  6. package/.docs/organized/code-examples/with-assistant-transport.md +7 -7
  7. package/.docs/organized/code-examples/with-browser-extension.md +6 -6
  8. package/.docs/organized/code-examples/with-chain-of-thought.md +11 -11
  9. package/.docs/organized/code-examples/with-cloud-standalone.md +9 -9
  10. package/.docs/organized/code-examples/with-cloud.md +9 -9
  11. package/.docs/organized/code-examples/with-custom-thread-list.md +9 -9
  12. package/.docs/organized/code-examples/with-elevenlabs-conversational.md +11 -11
  13. package/.docs/organized/code-examples/with-elevenlabs-scribe.md +11 -11
  14. package/.docs/organized/code-examples/with-eve.md +8 -8
  15. package/.docs/organized/code-examples/with-expo.md +26 -46
  16. package/.docs/organized/code-examples/with-external-store.md +7 -7
  17. package/.docs/organized/code-examples/with-ffmpeg.md +9 -9
  18. package/.docs/organized/code-examples/with-generative-ui.md +10 -10
  19. package/.docs/organized/code-examples/with-google-adk.md +8 -8
  20. package/.docs/organized/code-examples/with-heat-graph.md +6 -6
  21. package/.docs/organized/code-examples/with-image-generation.md +9 -9
  22. package/.docs/organized/code-examples/with-interactables.md +9 -9
  23. package/.docs/organized/code-examples/with-langchain.md +9 -9
  24. package/.docs/organized/code-examples/with-langgraph.md +9 -9
  25. package/.docs/organized/code-examples/with-livekit.md +11 -11
  26. package/.docs/organized/code-examples/with-mcp.md +28 -23
  27. package/.docs/organized/code-examples/with-opencode.md +8 -11
  28. package/.docs/organized/code-examples/with-pi.md +14 -14
  29. package/.docs/organized/code-examples/with-react-hook-form.md +10 -10
  30. package/.docs/organized/code-examples/with-react-ink-web.md +5 -5
  31. package/.docs/organized/code-examples/with-react-ink.md +2 -2
  32. package/.docs/organized/code-examples/with-react-router.md +11 -11
  33. package/.docs/organized/code-examples/with-resumable-stream.md +10 -10
  34. package/.docs/organized/code-examples/with-store.md +6 -6
  35. package/.docs/organized/code-examples/with-tanstack.md +9 -9
  36. package/.docs/organized/code-examples/with-tap-runtime.md +7 -7
  37. package/.docs/organized/code-examples/with-virtualized-thread.md +8 -8
  38. package/.docs/raw/docs/(docs)/cli.mdx +2 -0
  39. package/.docs/raw/docs/(docs)/devtools.mdx +7 -2
  40. package/.docs/raw/docs/(reference)/api-reference/generative-ui/a2ui.mdx +40 -0
  41. package/.docs/raw/docs/(reference)/api-reference/generative-ui/index.mdx +3 -0
  42. package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +19 -420
  43. package/.docs/raw/docs/(reference)/api-reference/hooks/state.mdx +4 -1
  44. package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +21 -0
  45. package/.docs/raw/docs/(reference)/api-reference/model-context/context.mdx +1 -9
  46. package/.docs/raw/docs/(reference)/api-reference/primitives/assistant-if.mdx +1 -18
  47. package/.docs/raw/docs/cloud/langgraph.mdx +1 -1
  48. package/.docs/raw/docs/copilots/model-context.mdx +1 -1
  49. package/.docs/raw/docs/copilots/motivation.mdx +1 -1
  50. package/.docs/raw/docs/guides/attachments.mdx +3 -3
  51. package/.docs/raw/docs/guides/branching.mdx +2 -2
  52. package/.docs/raw/docs/guides/context-api.mdx +89 -111
  53. package/.docs/raw/docs/guides/editing.mdx +5 -5
  54. package/.docs/raw/docs/guides/electron.mdx +369 -0
  55. package/.docs/raw/docs/guides/index.mdx +10 -0
  56. package/.docs/raw/docs/guides/quoting.mdx +3 -3
  57. package/.docs/raw/docs/guides/resumable-stream-deployment.mdx +2 -2
  58. package/.docs/raw/docs/ink/hooks.mdx +3 -3
  59. package/.docs/raw/docs/ink/primitives.mdx +14 -7
  60. package/.docs/raw/docs/integrations/auth/better-auth.mdx +2 -2
  61. package/.docs/raw/docs/integrations/auth/clerk.mdx +2 -2
  62. package/.docs/raw/docs/integrations/auth/next-auth.mdx +3 -3
  63. package/.docs/raw/docs/integrations/observability/langsmith.mdx +1 -1
  64. package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +36 -9
  65. package/.docs/raw/docs/migrations/v0-15.mdx +156 -0
  66. package/.docs/raw/docs/primitives/composer.mdx +1 -1
  67. package/.docs/raw/docs/primitives/thread-list.mdx +2 -2
  68. package/.docs/raw/docs/react-native/hooks.mdx +3 -3
  69. package/.docs/raw/docs/react-native/index.mdx +2 -2
  70. package/.docs/raw/docs/react-native/primitives.mdx +39 -5
  71. package/.docs/raw/docs/runtimes/ai-sdk/overview.mdx +3 -3
  72. package/.docs/raw/docs/runtimes/ai-sdk/v4-legacy.mdx +4 -4
  73. package/.docs/raw/docs/runtimes/ai-sdk/v5-legacy.mdx +4 -4
  74. package/.docs/raw/docs/runtimes/ai-sdk/v6-legacy.mdx +4 -4
  75. package/.docs/raw/docs/runtimes/ai-sdk/v7.mdx +2 -2
  76. package/.docs/raw/docs/runtimes/concepts/threads.mdx +10 -10
  77. package/.docs/raw/docs/runtimes/custom/assistant-transport.mdx +2 -2
  78. package/.docs/raw/docs/runtimes/custom/external-store.mdx +1 -1
  79. package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +3 -3
  80. package/.docs/raw/docs/runtimes/langchain.mdx +2 -2
  81. package/.docs/raw/docs/runtimes/langgraph/overview.mdx +1 -1
  82. package/.docs/raw/docs/runtimes/opencode/hooks.mdx +3 -1
  83. package/.docs/raw/docs/tools/a2ui.mdx +107 -0
  84. package/.docs/raw/docs/tools/interactables-legacy.mdx +4 -4
  85. package/.docs/raw/docs/tools/interactables.mdx +3 -3
  86. package/.docs/raw/docs/tools/mcp-apps.mdx +61 -2
  87. package/.docs/raw/docs/tools/user-managed-mcp.mdx +52 -7
  88. package/.docs/raw/docs/ui/follow-up-suggestions.mdx +2 -0
  89. package/.docs/raw/docs/ui/model-selector.mdx +1 -1
  90. package/.docs/raw/docs/ui/part-grouping.mdx +0 -4
  91. package/.docs/raw/docs/ui/reasoning.mdx +1 -1
  92. package/dist/index.d.ts +1 -1
  93. package/dist/index.js +2 -2
  94. package/dist/index.js.map +1 -1
  95. package/dist/prepare-docs/code-examples.js.map +1 -1
  96. package/dist/prepare-docs/prepare.d.ts +1 -1
  97. package/dist/stdio.d.ts +1 -1
  98. package/dist/tools/docs.d.ts +6 -10
  99. package/dist/tools/docs.d.ts.map +1 -1
  100. package/dist/tools/docs.js +2 -2
  101. package/dist/tools/docs.js.map +1 -1
  102. package/dist/tools/examples.d.ts +4 -8
  103. package/dist/tools/examples.d.ts.map +1 -1
  104. package/dist/tools/examples.js +2 -2
  105. package/dist/tools/examples.js.map +1 -1
  106. package/dist/tools/resources.d.ts +1 -1
  107. package/dist/tools/resources.d.ts.map +1 -1
  108. package/dist/tools/resources.js +1 -1
  109. package/dist/tools/resources.js.map +1 -1
  110. package/dist/tools/search.d.ts +4 -10
  111. package/dist/tools/search.d.ts.map +1 -1
  112. package/dist/tools/search.js +2 -2
  113. package/dist/tools/search.js.map +1 -1
  114. package/dist/tools/tests/mcp-test-client.d.ts +15 -0
  115. package/dist/tools/tests/mcp-test-client.d.ts.map +1 -0
  116. package/dist/tools/tests/mcp-test-client.js +68 -0
  117. package/dist/tools/tests/mcp-test-client.js.map +1 -0
  118. package/dist/tools/tests/test-setup.d.ts.map +1 -1
  119. package/dist/tools/xulux-templates.d.ts +9 -23
  120. package/dist/tools/xulux-templates.d.ts.map +1 -1
  121. package/dist/tools/xulux-templates.js +4 -4
  122. package/dist/tools/xulux-templates.js.map +1 -1
  123. package/dist/utils/logger.d.ts.map +1 -1
  124. package/dist/utils/security.js.map +1 -1
  125. package/package.json +4 -3
  126. package/src/index.ts +2 -2
  127. package/src/tools/docs.ts +2 -2
  128. package/src/tools/examples.ts +2 -2
  129. package/src/tools/resources.ts +1 -4
  130. package/src/tools/search.ts +2 -2
  131. package/src/tools/tests/completions.test.ts +40 -26
  132. package/src/tools/tests/integration.test.ts +3 -4
  133. package/src/tools/tests/mcp-protocol.test.ts +160 -175
  134. package/src/tools/tests/mcp-test-client.ts +111 -0
  135. package/src/tools/tests/resources.test.ts +97 -66
  136. package/src/tools/xulux-templates.ts +4 -4
@@ -4,45 +4,43 @@
4
4
 
5
5
  ```typescript
6
6
  import { openai } from "@ai-sdk/openai";
7
- import {
8
- streamText,
9
- convertToModelMessages,
10
- tool,
11
- stepCountIs,
12
- zodSchema,
13
- } from "ai";
7
+ import { convertToModelMessages, stepCountIs, streamText } from "ai";
14
8
  import type { UIMessage } from "ai";
15
- import { z } from "zod";
9
+ import {
10
+ AISDKToolkit,
11
+ unstable_injectInteractableContext,
12
+ type AISDKToolkitToolsOptions,
13
+ } from "@assistant-ui/react-ai-sdk";
14
+ import toolkit from "../../toolkit";
16
15
 
17
16
  export const maxDuration = 30;
18
17
 
18
+ const aiToolkit = new AISDKToolkit({ toolkit });
19
+
20
+ type FrontendToolDefs = NonNullable<AISDKToolkitToolsOptions["frontend"]>;
21
+
19
22
  export async function POST(req: Request) {
20
- const { messages }: { messages: UIMessage[] } = await req.json();
23
+ const {
24
+ messages,
25
+ tools: clientTools,
26
+ }: {
27
+ messages: UIMessage[];
28
+ tools?: FrontendToolDefs;
29
+ } = await req.json();
30
+
31
+ const tools = await aiToolkit.tools({
32
+ ...(clientTools && { frontend: clientTools }),
33
+ });
21
34
 
22
35
  const result = streamText({
23
36
  model: openai("gpt-5.4-nano"),
24
37
  system:
25
- "You are a helpful assistant that can generate HTML code. When the user asks you to create any visual content, UI, or webpage, use the render_html tool to render it in the browser. Always use the render_html tool for HTML output rather than showing code in a code block.",
26
- messages: await convertToModelMessages(messages),
38
+ "You create functional HTML artifacts. When the user requests a webpage, interface, visualization, or other visual content, call the artifact tool with a short title and a complete HTML document. When an artifact already exists and the user requests a change, call update_artifact with its id and only the fields that changed. Do not put artifact HTML in a code block.",
39
+ messages: await convertToModelMessages(
40
+ unstable_injectInteractableContext(messages),
41
+ ),
27
42
  stopWhen: stepCountIs(10),
28
- tools: {
29
- render_html: tool({
30
- description:
31
- "Render HTML code in the user's browser. Use this whenever the user asks for HTML, a webpage, a UI component, or any visual content.",
32
- inputSchema: zodSchema(
33
- z.object({
34
- code: z
35
- .string()
36
- .describe(
37
- "The complete HTML code to render, including inline CSS and JavaScript if needed.",
38
- ),
39
- }),
40
- ),
41
- execute: async () => {
42
- return { success: true };
43
- },
44
- }),
45
- },
43
+ tools,
46
44
  });
47
45
 
48
46
  return result.toUIMessageStreamResponse();
@@ -50,6 +48,391 @@ export async function POST(req: Request) {
50
48
 
51
49
  ```
52
50
 
51
+ ## app/artifact-state.ts
52
+
53
+ ```typescript
54
+ import { z } from "zod";
55
+
56
+ export const artifactSchema = z.object({
57
+ title: z.string().describe("A short title for the artifact."),
58
+ code: z
59
+ .string()
60
+ .describe(
61
+ "A complete HTML document with inline CSS and JavaScript when needed.",
62
+ ),
63
+ });
64
+
65
+ export type ArtifactState = z.infer<typeof artifactSchema>;
66
+
67
+ export const emptyArtifact: ArtifactState = {
68
+ title: "Untitled artifact",
69
+ code: "",
70
+ };
71
+
72
+ export const artifactDescription =
73
+ "Create an interactive HTML artifact when the user asks for a webpage, interface, visualization, or other visual content. Provide a short title and a complete HTML document.";
74
+
75
+ ```
76
+
77
+ ## app/artifact-surface.tsx
78
+
79
+ ```tsx
80
+ "use client";
81
+
82
+ import {
83
+ createContext,
84
+ useContext,
85
+ useEffect,
86
+ useMemo,
87
+ useRef,
88
+ useState,
89
+ type ReactNode,
90
+ } from "react";
91
+ import {
92
+ unstable_useInteractable,
93
+ unstable_useInteractableVersions,
94
+ useAuiState,
95
+ type Unstable_InteractableToolRenderProps,
96
+ } from "@assistant-ui/react";
97
+ import {
98
+ ArrowLeftIcon,
99
+ CheckIcon,
100
+ Code2Icon,
101
+ EyeIcon,
102
+ FileCode2Icon,
103
+ HistoryIcon,
104
+ Loader2Icon,
105
+ PanelRightOpenIcon,
106
+ SparklesIcon,
107
+ XIcon,
108
+ } from "lucide-react";
109
+ import {
110
+ artifactDescription,
111
+ artifactSchema,
112
+ emptyArtifact,
113
+ type ArtifactState,
114
+ } from "./artifact-state";
115
+
116
+ type ArtifactSurfaceTarget = {
117
+ id: string;
118
+ name: "artifact";
119
+ messageId: string;
120
+ };
121
+
122
+ type ArtifactSurfaceContextValue = {
123
+ active: ArtifactSurfaceTarget | null;
124
+ open: (target: ArtifactSurfaceTarget) => void;
125
+ close: () => void;
126
+ returnToOrigin: () => void;
127
+ };
128
+
129
+ const ArtifactSurfaceContext =
130
+ createContext<ArtifactSurfaceContextValue | null>(null);
131
+
132
+ export function ArtifactSurfaceProvider({ children }: { children: ReactNode }) {
133
+ const [active, setActive] = useState<ArtifactSurfaceTarget | null>(null);
134
+
135
+ const value = useMemo<ArtifactSurfaceContextValue>(
136
+ () => ({
137
+ active,
138
+ open: setActive,
139
+ close: () => setActive(null),
140
+ returnToOrigin: () => {
141
+ const messageId = active?.messageId;
142
+ setActive(null);
143
+ if (!messageId) return;
144
+ requestAnimationFrame(() => {
145
+ const message = Array.from(
146
+ document.querySelectorAll<HTMLElement>("[data-message-id]"),
147
+ ).find((element) => element.dataset.messageId === messageId);
148
+ message?.scrollIntoView({ behavior: "smooth", block: "center" });
149
+ });
150
+ },
151
+ }),
152
+ [active],
153
+ );
154
+
155
+ return (
156
+ <ArtifactSurfaceContext.Provider value={value}>
157
+ {children}
158
+ </ArtifactSurfaceContext.Provider>
159
+ );
160
+ }
161
+
162
+ export function useArtifactSurface() {
163
+ const value = useContext(ArtifactSurfaceContext);
164
+ if (!value) {
165
+ throw new Error(
166
+ "useArtifactSurface must be used within ArtifactSurfaceProvider",
167
+ );
168
+ }
169
+ return value;
170
+ }
171
+
172
+ export function ArtifactTrigger({
173
+ state,
174
+ version,
175
+ id,
176
+ streaming,
177
+ }: Unstable_InteractableToolRenderProps<ArtifactState>) {
178
+ const messageId = useAuiState((s) => s.message.id);
179
+ const { active, open } = useArtifactSurface();
180
+ const displayState = version && !version.isLatest ? version.state : state;
181
+ const title = displayState.title || "Untitled artifact";
182
+ const lineCount = displayState.code
183
+ ? displayState.code.split(/\r?\n/).length
184
+ : 0;
185
+ const isOpen = active?.id === id;
186
+
187
+ return (
188
+ <div className="bg-card my-3 overflow-hidden rounded-xl border shadow-[0_10px_30px_-22px_rgba(24,24,27,0.5)]">
189
+ <div className="bg-muted text-muted-foreground flex items-center gap-2 border-b px-3 py-2 font-mono text-[10px] tracking-[0.16em] uppercase">
190
+ <FileCode2Icon className="size-3.5" />
191
+ HTML artifact
192
+ {streaming ? (
193
+ <span className="ml-auto inline-flex items-center gap-1.5 text-amber-700">
194
+ <Loader2Icon className="size-3 animate-spin" />
195
+ Writing
196
+ </span>
197
+ ) : (
198
+ <span className="ml-auto inline-flex items-center gap-1.5 text-emerald-700">
199
+ <CheckIcon className="size-3" />
200
+ Ready
201
+ </span>
202
+ )}
203
+ </div>
204
+ <div className="flex items-center gap-4 p-3">
205
+ <div className="min-w-0 flex-1">
206
+ <p className="truncate text-sm font-semibold">{title}</p>
207
+ <p className="text-muted-foreground mt-0.5 text-xs">
208
+ {lineCount > 0 ? `${lineCount} lines` : "Preparing document"}
209
+ {version && !version.isLatest ? " · historical version" : ""}
210
+ </p>
211
+ </div>
212
+ <button
213
+ type="button"
214
+ data-testid="artifact-trigger"
215
+ disabled={streaming}
216
+ onClick={() =>
217
+ open({
218
+ id,
219
+ name: "artifact",
220
+ messageId,
221
+ })
222
+ }
223
+ className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-9 shrink-0 items-center gap-2 rounded-lg px-3 text-xs font-medium transition-colors disabled:cursor-wait disabled:opacity-50"
224
+ >
225
+ <PanelRightOpenIcon className="size-3.5" />
226
+ {isOpen ? "In workbench" : "Open workbench"}
227
+ </button>
228
+ </div>
229
+ </div>
230
+ );
231
+ }
232
+
233
+ export function ArtifactSurface() {
234
+ const { active } = useArtifactSurface();
235
+ if (!active) return null;
236
+ return <ArtifactWorkbench key={active.id} target={active} />;
237
+ }
238
+
239
+ function ArtifactWorkbench({ target }: { target: ArtifactSurfaceTarget }) {
240
+ const [state, { setState }] = unstable_useInteractable("artifact", {
241
+ id: target.id,
242
+ description: artifactDescription,
243
+ stateSchema: artifactSchema,
244
+ initialState: emptyArtifact,
245
+ });
246
+ const versions = unstable_useInteractableVersions<ArtifactState>(
247
+ target.id,
248
+ target.name,
249
+ );
250
+ const { close, returnToOrigin } = useArtifactSurface();
251
+ const [view, setView] = useState<"preview" | "code">("preview");
252
+ const [historyOpen, setHistoryOpen] = useState(false);
253
+ const historyRef = useRef<HTMLDivElement>(null);
254
+
255
+ useEffect(() => {
256
+ if (!historyOpen) return;
257
+ const dismissPointer = (event: PointerEvent) => {
258
+ if (!historyRef.current?.contains(event.target as Node)) {
259
+ setHistoryOpen(false);
260
+ }
261
+ };
262
+ const dismissKey = (event: KeyboardEvent) => {
263
+ if (event.key === "Escape") setHistoryOpen(false);
264
+ };
265
+ const dismissBlur = () => setHistoryOpen(false);
266
+ document.addEventListener("pointerdown", dismissPointer);
267
+ document.addEventListener("keydown", dismissKey);
268
+ window.addEventListener("blur", dismissBlur);
269
+ return () => {
270
+ document.removeEventListener("pointerdown", dismissPointer);
271
+ document.removeEventListener("keydown", dismissKey);
272
+ window.removeEventListener("blur", dismissBlur);
273
+ };
274
+ }, [historyOpen]);
275
+
276
+ return (
277
+ <aside
278
+ data-testid="artifact-surface"
279
+ className="bg-muted fixed inset-0 z-30 flex min-w-0 flex-1 flex-col md:static md:z-0 md:border-l"
280
+ >
281
+ <header className="border-border bg-background/90 flex h-14 shrink-0 items-center gap-3 border-b px-3 backdrop-blur md:px-5">
282
+ <div className="flex min-w-0 items-center gap-2">
283
+ <span className="grid size-7 shrink-0 place-items-center rounded-lg bg-amber-300 text-zinc-950 shadow-[inset_0_0_0_1px_rgba(24,24,27,0.12)]">
284
+ <SparklesIcon className="size-3.5" />
285
+ </span>
286
+ <div className="min-w-0">
287
+ <p className="truncate text-sm font-semibold">
288
+ {state.title || "Untitled artifact"}
289
+ </p>
290
+ <p className="text-muted-foreground font-mono text-[9px] tracking-[0.14em] uppercase">
291
+ Opened from conversation
292
+ </p>
293
+ </div>
294
+ </div>
295
+
296
+ <div className="ml-auto flex items-center gap-1.5">
297
+ <div className="relative" ref={historyRef}>
298
+ <button
299
+ type="button"
300
+ data-testid="artifact-version-select"
301
+ aria-label="Artifact version history"
302
+ aria-expanded={historyOpen}
303
+ onClick={() => setHistoryOpen((open) => !open)}
304
+ className="border-border bg-card text-foreground hover:bg-muted inline-flex h-8 items-center gap-1.5 rounded-md border px-2.5 text-xs font-medium"
305
+ >
306
+ <HistoryIcon className="size-3.5" />
307
+ {versions.length} {versions.length === 1 ? "version" : "versions"}
308
+ </button>
309
+ {historyOpen && (
310
+ <div className="border-border bg-card absolute top-9 right-0 z-10 flex w-44 flex-col overflow-hidden rounded-md border py-1 shadow-lg">
311
+ {versions.map((version, index) => (
312
+ <button
313
+ key={`${version.origin}-${index}`}
314
+ type="button"
315
+ onClick={() => {
316
+ version.restore();
317
+ setHistoryOpen(false);
318
+ }}
319
+ className="text-foreground hover:bg-muted flex items-center justify-between px-3 py-1.5 text-left text-xs"
320
+ >
321
+ <span>
322
+ v{index + 1} ·{" "}
323
+ {version.origin === "user-edit" ? "you" : "assistant"}
324
+ </span>
325
+ {index === versions.length - 1 && (
326
+ <span className="text-muted-foreground font-mono text-[9px] uppercase">
327
+ latest
328
+ </span>
329
+ )}
330
+ </button>
331
+ ))}
332
+ </div>
333
+ )}
334
+ </div>
335
+ <button
336
+ type="button"
337
+ data-testid="artifact-return"
338
+ aria-label="Return to conversation"
339
+ onClick={returnToOrigin}
340
+ className="border-border bg-card text-foreground hover:bg-muted inline-flex h-8 items-center gap-1.5 rounded-md border px-2.5 text-xs font-medium"
341
+ >
342
+ <ArrowLeftIcon className="size-3.5" />
343
+ <span className="hidden sm:inline">Conversation</span>
344
+ </button>
345
+ <button
346
+ type="button"
347
+ data-testid="artifact-close"
348
+ onClick={close}
349
+ className="text-muted-foreground hover:bg-muted hover:text-foreground grid size-8 place-items-center rounded-md"
350
+ aria-label="Close artifact"
351
+ >
352
+ <XIcon className="size-4" />
353
+ </button>
354
+ </div>
355
+ </header>
356
+
357
+ <div className="flex min-h-0 flex-1 flex-col p-2 md:p-4">
358
+ <div className="border-border bg-card flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border shadow-[0_24px_70px_-44px_rgba(24,24,27,0.55)]">
359
+ <div className="bg-muted/80 flex shrink-0 items-center gap-1 border-b p-1.5">
360
+ <button
361
+ type="button"
362
+ aria-pressed={view === "preview"}
363
+ onClick={() => setView("preview")}
364
+ className={`inline-flex h-8 items-center gap-2 rounded-md px-3 text-xs font-medium transition-colors ${
365
+ view === "preview"
366
+ ? "bg-card text-foreground ring-border shadow-sm ring-1"
367
+ : "text-muted-foreground hover:text-foreground"
368
+ }`}
369
+ >
370
+ <EyeIcon className="size-3.5" />
371
+ Preview
372
+ </button>
373
+ <button
374
+ type="button"
375
+ aria-pressed={view === "code"}
376
+ onClick={() => setView("code")}
377
+ className={`inline-flex h-8 items-center gap-2 rounded-md px-3 text-xs font-medium transition-colors ${
378
+ view === "code"
379
+ ? "bg-card text-foreground ring-border shadow-sm ring-1"
380
+ : "text-muted-foreground hover:text-foreground"
381
+ }`}
382
+ >
383
+ <Code2Icon className="size-3.5" />
384
+ Source
385
+ </button>
386
+ <span className="text-muted-foreground ml-auto pr-2 font-mono text-[9px] tracking-[0.14em] uppercase">
387
+ Live document
388
+ </span>
389
+ </div>
390
+
391
+ {view === "preview" ? (
392
+ <iframe
393
+ data-testid="artifact-preview"
394
+ title={`Preview of ${state.title || "artifact"}`}
395
+ srcDoc={state.code}
396
+ sandbox="allow-scripts allow-forms"
397
+ className="min-h-0 flex-1 bg-white"
398
+ />
399
+ ) : (
400
+ <div className="flex min-h-0 flex-1 flex-col">
401
+ <input
402
+ aria-label="Artifact title"
403
+ value={state.title}
404
+ onChange={(event) =>
405
+ setState((previous) => ({
406
+ ...previous,
407
+ title: event.target.value,
408
+ }))
409
+ }
410
+ className="placeholder:text-muted-foreground h-11 shrink-0 border-b px-4 text-sm font-semibold outline-none"
411
+ placeholder="Artifact title"
412
+ />
413
+ <textarea
414
+ data-testid="artifact-code-editor"
415
+ aria-label="Artifact HTML source"
416
+ value={state.code}
417
+ onChange={(event) =>
418
+ setState((previous) => ({
419
+ ...previous,
420
+ code: event.target.value,
421
+ }))
422
+ }
423
+ spellCheck={false}
424
+ className="min-h-0 flex-1 resize-none bg-zinc-950 p-4 font-mono text-[13px] leading-6 text-zinc-100 outline-none selection:bg-amber-300 selection:text-zinc-950"
425
+ />
426
+ </div>
427
+ )}
428
+ </div>
429
+ </div>
430
+ </aside>
431
+ );
432
+ }
433
+
434
+ ```
435
+
53
436
  ## app/globals.css
54
437
 
55
438
  ```css
@@ -211,90 +594,44 @@ export default function RootLayout({
211
594
  import { Thread } from "@/components/assistant-ui/thread";
212
595
  import {
213
596
  AssistantRuntimeProvider,
214
- Tools,
215
- useAui,
216
- useAuiState,
217
597
  AuiProvider,
218
598
  Suggestions,
599
+ Tools,
600
+ unstable_Interactables,
601
+ useAui,
219
602
  } from "@assistant-ui/react";
220
603
  import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
221
- import type { ToolCallMessagePart } from "@assistant-ui/react";
222
- import { CodeIcon, EyeIcon } from "lucide-react";
223
- import { useState } from "react";
604
+ import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
605
+ import {
606
+ ArtifactSurface,
607
+ ArtifactSurfaceProvider,
608
+ useArtifactSurface,
609
+ } from "./artifact-surface";
224
610
  import toolkit from "./toolkit";
225
611
 
226
- function ArtifactsView() {
227
- const [tab, setTab] = useState<"source" | "preview">("source");
228
-
229
- const lastToolCall = useAuiState((s) => {
230
- const messages = s.thread.messages;
231
- return messages
232
- .flatMap((m) =>
233
- m.content.filter(
234
- (c): c is ToolCallMessagePart =>
235
- c.type === "tool-call" && c.toolName === "render_html",
236
- ),
237
- )
238
- .at(-1);
239
- });
240
-
241
- const code = lastToolCall?.args.code as string | undefined;
242
- const isComplete = lastToolCall?.result !== undefined;
243
-
244
- if (!code) return null;
612
+ function ArtifactExperience() {
613
+ const { active } = useArtifactSurface();
245
614
 
246
615
  return (
247
- <div className="flex flex-grow basis-full justify-stretch p-3">
248
- <div className="flex h-full w-full flex-col overflow-hidden rounded-lg border">
249
- <div className="flex border-b">
250
- <button
251
- type="button"
252
- onClick={() => setTab("source")}
253
- className={`inline-flex flex-1 items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors ${
254
- tab === "source"
255
- ? "bg-background text-foreground"
256
- : "text-muted-foreground hover:text-foreground"
257
- }`}
258
- >
259
- <CodeIcon className="size-4" />
260
- Source Code
261
- </button>
262
- <button
263
- type="button"
264
- onClick={() => isComplete && setTab("preview")}
265
- disabled={!isComplete}
266
- className={`inline-flex flex-1 items-center justify-center gap-2 px-4 py-2.5 text-sm font-medium transition-colors ${
267
- !isComplete
268
- ? "cursor-not-allowed opacity-50"
269
- : tab === "preview"
270
- ? "bg-background text-foreground"
271
- : "text-muted-foreground hover:text-foreground"
272
- }`}
273
- >
274
- <EyeIcon className="size-4" />
275
- Preview
276
- </button>
277
- </div>
278
- {tab === "source" || !isComplete ? (
279
- <div className="h-full overflow-y-auto px-4 py-2 font-mono text-sm break-words whitespace-pre-line">
280
- {code}
281
- </div>
282
- ) : (
283
- <div className="flex h-full flex-grow px-4 py-2">
284
- <iframe
285
- className="h-full w-full"
286
- title="Artifact Preview"
287
- srcDoc={code}
288
- />
289
- </div>
290
- )}
291
- </div>
292
- </div>
616
+ <main className="bg-background flex h-full min-h-0 overflow-hidden">
617
+ <section
618
+ className={
619
+ active
620
+ ? "bg-background hidden min-w-0 md:block md:w-[min(44vw,42rem)] md:shrink-0"
621
+ : "bg-background min-w-0 flex-1"
622
+ }
623
+ >
624
+ <Thread />
625
+ </section>
626
+ <ArtifactSurface />
627
+ </main>
293
628
  );
294
629
  }
295
630
 
296
- function ThreadWithSuggestions() {
631
+ function ArtifactScopes() {
297
632
  const aui = useAui({
633
+ tools: Tools({ toolkit }),
634
+ unstable_interactables: unstable_Interactables(),
298
635
  suggestions: Suggestions([
299
636
  {
300
637
  title: "Build a landing page",
@@ -310,27 +647,24 @@ function ThreadWithSuggestions() {
310
647
  },
311
648
  ]),
312
649
  });
650
+
313
651
  return (
314
652
  <AuiProvider value={aui}>
315
- <Thread />
653
+ <ArtifactSurfaceProvider>
654
+ <ArtifactExperience />
655
+ </ArtifactSurfaceProvider>
316
656
  </AuiProvider>
317
657
  );
318
658
  }
319
659
 
320
660
  export default function Home() {
321
- const runtime = useChatRuntime();
322
- const aui = useAui({
323
- tools: Tools({ toolkit }),
661
+ const runtime = useChatRuntime({
662
+ sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
324
663
  });
325
664
 
326
665
  return (
327
- <AssistantRuntimeProvider aui={aui} runtime={runtime}>
328
- <main className="flex h-full justify-stretch">
329
- <div className="flex-grow basis-full">
330
- <ThreadWithSuggestions />
331
- </div>
332
- <ArtifactsView />
333
- </main>
666
+ <AssistantRuntimeProvider runtime={runtime}>
667
+ <ArtifactScopes />
334
668
  </AssistantRuntimeProvider>
335
669
  );
336
670
  }
@@ -342,25 +676,16 @@ export default function Home() {
342
676
  ```tsx
343
677
  "use generative";
344
678
 
345
- import { defineToolkit, externalTool } from "@assistant-ui/react";
346
- import { TerminalIcon } from "lucide-react";
347
- import { z } from "zod";
679
+ import { defineToolkit, unstable_interactableTool } from "@assistant-ui/react";
680
+ import { ArtifactTrigger } from "./artifact-surface";
681
+ import { artifactDescription, artifactSchema } from "./artifact-state";
348
682
 
349
683
  export default defineToolkit({
350
- render_html: {
351
- parameters: z.object({
352
- code: z.string(),
353
- }),
354
- execute: externalTool(),
355
- render: () => {
356
- return (
357
- <div className="bg-primary text-primary-foreground my-2 inline-flex items-center gap-2 rounded-full border px-4 py-2">
358
- <TerminalIcon className="size-4" />
359
- render_html(&#123; code: &quot;...&quot; &#125;)
360
- </div>
361
- );
362
- },
363
- },
684
+ artifact: unstable_interactableTool({
685
+ description: artifactDescription,
686
+ stateSchema: artifactSchema,
687
+ render: (props) => <ArtifactTrigger {...props} />,
688
+ }),
364
689
  });
365
690
 
366
691
  ```
@@ -426,30 +751,30 @@ export default withAui(nextConfig);
426
751
  "start": "next start"
427
752
  },
428
753
  "dependencies": {
429
- "@ai-sdk/openai": "^4.0.14",
754
+ "@ai-sdk/openai": "^4.0.20",
430
755
  "@assistant-ui/react": "workspace:*",
431
756
  "@assistant-ui/react-ai-sdk": "workspace:*",
432
757
  "@assistant-ui/react-markdown": "workspace:*",
433
758
  "@assistant-ui/ui": "workspace:*",
434
- "ai": "^7.0.28",
759
+ "ai": "^7.0.37",
435
760
  "class-variance-authority": "^0.7.1",
436
761
  "clsx": "^2.1.1",
437
- "lucide-react": "^1.24.0",
438
- "next": "^16.2.10",
439
- "react": "^19.2.7",
440
- "react-dom": "^19.2.7",
762
+ "lucide-react": "^1.26.0",
763
+ "next": "^16.2.11",
764
+ "react": "^19.2.8",
765
+ "react-dom": "^19.2.8",
441
766
  "tailwind-merge": "^3.6.0",
442
767
  "zod": "^4.4.3"
443
768
  },
444
769
  "devDependencies": {
445
770
  "@assistant-ui/next": "workspace:*",
446
771
  "@assistant-ui/x-buildutils": "workspace:*",
447
- "@tailwindcss/postcss": "^4.3.2",
772
+ "@tailwindcss/postcss": "^4.3.3",
448
773
  "@types/node": "^26.1.1",
449
774
  "@types/react": "^19.2.17",
450
775
  "@types/react-dom": "^19.2.3",
451
- "postcss": "^8.5.19",
452
- "tailwindcss": "^4.3.2",
776
+ "postcss": "^8.5.23",
777
+ "tailwindcss": "^4.3.3",
453
778
  "tw-animate-css": "^1.4.0",
454
779
  "typescript": "npm:@typescript/typescript6@^6.0.2",
455
780
  "typescript-7": "npm:typescript@^7.0.2"