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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/.docs/organized/code-examples/waterfall.md +1 -1
  2. package/.docs/organized/code-examples/with-a2a.md +2 -2
  3. package/.docs/organized/code-examples/with-ag-ui.md +2 -2
  4. package/.docs/organized/code-examples/with-ai-sdk-v6.md +4 -4
  5. package/.docs/organized/code-examples/with-artifacts.md +4 -4
  6. package/.docs/organized/code-examples/with-assistant-transport.md +70 -54
  7. package/.docs/organized/code-examples/with-browser-extension.md +4 -4
  8. package/.docs/organized/code-examples/with-chain-of-thought.md +40 -89
  9. package/.docs/organized/code-examples/with-cloud-standalone.md +4 -4
  10. package/.docs/organized/code-examples/with-cloud.md +4 -4
  11. package/.docs/organized/code-examples/with-custom-thread-list.md +4 -4
  12. package/.docs/organized/code-examples/with-elevenlabs-conversational.md +6 -6
  13. package/.docs/organized/code-examples/with-elevenlabs-scribe.md +6 -6
  14. package/.docs/organized/code-examples/with-eve.md +343 -0
  15. package/.docs/organized/code-examples/with-expo.md +909 -932
  16. package/.docs/organized/code-examples/with-external-store.md +2 -2
  17. package/.docs/organized/code-examples/with-ffmpeg.md +5 -8
  18. package/.docs/organized/code-examples/with-generative-ui.md +5 -5
  19. package/.docs/organized/code-examples/with-google-adk.md +2 -2
  20. package/.docs/organized/code-examples/with-heat-graph.md +1 -1
  21. package/.docs/organized/code-examples/with-image-generation.md +4 -4
  22. package/.docs/organized/code-examples/with-interactables.md +166 -338
  23. package/.docs/organized/code-examples/with-langchain.md +4 -4
  24. package/.docs/organized/code-examples/with-langgraph.md +20 -157
  25. package/.docs/organized/code-examples/with-livekit.md +6 -6
  26. package/.docs/organized/code-examples/with-mcp.md +4 -4
  27. package/.docs/organized/code-examples/with-opencode.md +3 -3
  28. package/.docs/organized/code-examples/with-pi.md +9 -7
  29. package/.docs/organized/code-examples/with-react-hook-form.md +13 -6
  30. package/.docs/organized/code-examples/with-react-ink-web.md +5 -5
  31. package/.docs/organized/code-examples/with-react-ink.md +1 -1
  32. package/.docs/organized/code-examples/with-react-router.md +10 -10
  33. package/.docs/organized/code-examples/with-resumable-stream.md +5 -5
  34. package/.docs/organized/code-examples/with-store.md +1 -1
  35. package/.docs/organized/code-examples/with-tanstack.md +5 -5
  36. package/.docs/organized/code-examples/with-tap-runtime.md +2 -2
  37. package/.docs/organized/code-examples/with-virtualized-thread.md +44 -24
  38. package/.docs/raw/docs/(docs)/cli.mdx +4 -2
  39. package/.docs/raw/docs/(docs)/devtools.mdx +25 -2
  40. package/.docs/raw/docs/(docs)/index.mdx +5 -2
  41. package/.docs/raw/docs/(docs)/installation.mdx +5 -2
  42. package/.docs/raw/docs/(reference)/api-reference/external-store/message-conversion.mdx +16 -16
  43. package/.docs/raw/docs/(reference)/api-reference/hooks/composer-triggers.mdx +73 -42
  44. package/.docs/raw/docs/(reference)/api-reference/hooks/model-context.mdx +3 -20
  45. package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +234 -166
  46. package/.docs/raw/docs/(reference)/api-reference/integrations/eve.mdx +51 -0
  47. package/.docs/raw/docs/(reference)/api-reference/integrations/index.mdx +4 -1
  48. package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +70 -38
  49. package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar.mdx +30 -0
  50. package/.docs/raw/docs/(reference)/api-reference/primitives/attachment.mdx +7 -0
  51. package/.docs/raw/docs/(reference)/api-reference/primitives/branch-picker.mdx +26 -0
  52. package/.docs/raw/docs/(reference)/api-reference/primitives/chain-of-thought.mdx +15 -0
  53. package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +103 -0
  54. package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +16 -0
  55. package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +56 -0
  56. package/.docs/raw/docs/(reference)/api-reference/primitives/queue-item.mdx +12 -0
  57. package/.docs/raw/docs/(reference)/api-reference/primitives/selection-toolbar.mdx +12 -0
  58. package/.docs/raw/docs/(reference)/api-reference/primitives/suggestion.mdx +14 -0
  59. package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +57 -1
  60. package/.docs/raw/docs/(reference)/api-reference/tools/index.mdx +6 -0
  61. package/.docs/raw/docs/(reference)/api-reference/tools/interactables-legacy.mdx +55 -0
  62. package/.docs/raw/docs/(reference)/api-reference/tools/interactables.mdx +151 -0
  63. package/.docs/raw/docs/(reference)/api-reference/tools/rendering.mdx +15 -15
  64. package/.docs/raw/docs/(reference)/api-reference/tools/toolkits.mdx +20 -20
  65. package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +25 -9
  66. package/.docs/raw/docs/guides/headless-composer-input.mdx +113 -0
  67. package/.docs/raw/docs/guides/latex.mdx +28 -22
  68. package/.docs/raw/docs/guides/mentions.mdx +32 -7
  69. package/.docs/raw/docs/guides/slash-commands.mdx +3 -6
  70. package/.docs/raw/docs/guides/speech.mdx +5 -7
  71. package/.docs/raw/docs/guides/virtualization.mdx +77 -7
  72. package/.docs/raw/docs/guides/voice.mdx +3 -2
  73. package/.docs/raw/docs/integrations/attachments/custom-adapter.mdx +4 -12
  74. package/.docs/raw/docs/integrations/auth/better-auth.mdx +7 -10
  75. package/.docs/raw/docs/integrations/auth/clerk.mdx +3 -9
  76. package/.docs/raw/docs/integrations/auth/next-auth.mdx +3 -8
  77. package/.docs/raw/docs/integrations/frameworks/ai-sdk.mdx +8 -5
  78. package/.docs/raw/docs/integrations/index.mdx +5 -12
  79. package/.docs/raw/docs/integrations/observability/helicone.mdx +3 -4
  80. package/.docs/raw/docs/integrations/observability/langfuse.mdx +3 -5
  81. package/.docs/raw/docs/integrations/observability/langsmith.mdx +3 -2
  82. package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +4 -3
  83. package/.docs/raw/docs/primitives/composer.mdx +8 -0
  84. package/.docs/raw/docs/primitives/thread.mdx +24 -0
  85. package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +21 -0
  86. package/.docs/raw/docs/runtimes/custom/external-store.mdx +54 -0
  87. package/.docs/raw/docs/runtimes/eve/overview.mdx +100 -0
  88. package/.docs/raw/docs/runtimes/eve/quickstart.mdx +143 -0
  89. package/.docs/raw/docs/runtimes/langchain.mdx +386 -18
  90. package/.docs/raw/docs/runtimes/langgraph/quickstart.mdx +1 -1
  91. package/.docs/raw/docs/runtimes/langgraph/tutorial/part-1.mdx +1 -1
  92. package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +6 -0
  93. package/.docs/raw/docs/tools/defining-tools.mdx +4 -0
  94. package/.docs/raw/docs/tools/interactables-legacy.mdx +410 -0
  95. package/.docs/raw/docs/tools/interactables.mdx +892 -223
  96. package/.docs/raw/docs/tools/mcp.mdx +4 -4
  97. package/.docs/raw/docs/tools/multi-agent.mdx +5 -0
  98. package/.docs/raw/docs/ui/composer-trigger-popover.mdx +2 -0
  99. package/.docs/raw/docs/ui/file.mdx +1 -1
  100. package/.docs/raw/docs/ui/part-grouping.mdx +38 -0
  101. package/.docs/raw/docs/utilities/react-o11y.mdx +92 -6
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +18 -2
  104. package/dist/index.js.map +1 -1
  105. package/package.json +4 -4
  106. package/src/index.ts +14 -6
  107. package/src/tools/tests/mcp-protocol.test.ts +9 -0
@@ -4,17 +4,16 @@
4
4
 
5
5
  ```typescript
6
6
  import { openai } from "@ai-sdk/openai";
7
- import {
8
- streamText,
9
- convertToModelMessages,
10
- stepCountIs,
11
- jsonSchema,
12
- } from "ai";
7
+ import { streamText, convertToModelMessages, stepCountIs } from "ai";
13
8
  import type { UIMessage } from "ai";
9
+ import {
10
+ frontendTools,
11
+ unstable_injectInteractableContext,
12
+ } from "@assistant-ui/react-ai-sdk";
14
13
 
15
14
  export const maxDuration = 30;
16
15
 
17
- type ToolDef = { description?: string; parameters: Record<string, unknown> };
16
+ type FrontendTools = Parameters<typeof frontendTools>[0];
18
17
 
19
18
  export async function POST(req: Request) {
20
19
  const {
@@ -24,30 +23,19 @@ export async function POST(req: Request) {
24
23
  }: {
25
24
  messages: UIMessage[];
26
25
  system?: string;
27
- tools?: Record<string, ToolDef>;
26
+ tools?: FrontendTools;
28
27
  } = await req.json();
29
28
 
30
- // Convert client-defined tools (forwarded from model context) to AI SDK format.
31
- // These have no `execute` — they are frontend tools executed on the client
32
- // via Tools({ toolkit }) / useAssistantInteractable.
33
- const tools = clientTools
34
- ? Object.fromEntries(
35
- Object.entries(clientTools).map(([name, def]) => [
36
- name,
37
- {
38
- description: def.description ?? "",
39
- inputSchema: jsonSchema(def.parameters),
40
- },
41
- ]),
42
- )
43
- : undefined;
29
+ const modelMessages = await convertToModelMessages(
30
+ unstable_injectInteractableContext(messages),
31
+ );
44
32
 
45
33
  const result = streamText({
46
34
  model: openai("gpt-5.4-nano"),
47
- messages: await convertToModelMessages(messages),
35
+ messages: modelMessages,
48
36
  stopWhen: stepCountIs(10),
49
37
  ...(system ? { system } : {}),
50
- ...(tools ? { tools } : {}),
38
+ ...(clientTools ? { tools: frontendTools(clientTools) } : {}),
51
39
  } as Parameters<typeof streamText>[0]);
52
40
 
53
41
  return result.toUIMessageStreamResponse();
@@ -214,25 +202,15 @@ export default function RootLayout({
214
202
  ```tsx
215
203
  "use client";
216
204
 
217
- import {
218
- useRef,
219
- useState,
220
- useCallback,
221
- useEffect,
222
- type Dispatch,
223
- type SetStateAction,
224
- } from "react";
205
+ import { useCallback } from "react";
225
206
  import { Thread } from "@/components/assistant-ui/thread";
226
207
  import {
227
208
  AssistantRuntimeProvider,
228
209
  AuiProvider,
229
- Interactables,
210
+ unstable_Interactables,
230
211
  Suggestions,
231
- Tools,
232
212
  useAui,
233
- useAuiToolOverrides,
234
- useAssistantInteractable,
235
- useInteractableState,
213
+ unstable_useInteractable,
236
214
  } from "@assistant-ui/react";
237
215
  import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
238
216
  import { lastAssistantMessageIsCompleteWithToolCalls } from "ai";
@@ -247,34 +225,29 @@ import {
247
225
  } from "lucide-react";
248
226
  import {
249
227
  type NoteState,
250
- type TaskBoardState,
251
228
  noteInitialState,
252
- noteSchema,
229
+ notesInitialState,
230
+ notesSchema,
253
231
  taskBoardInitialState,
254
232
  taskBoardSchema,
255
233
  } from "./state";
256
- import toolkit from "./toolkits";
257
234
 
258
235
  function TaskBoard() {
259
- const id = useAssistantInteractable("taskBoard", {
260
- description:
261
- "A task board showing the user's tasks. Use the manage_tasks tool (not update_taskBoard) to add/toggle/remove/clear tasks.",
262
- stateSchema: taskBoardSchema,
263
- initialState: taskBoardInitialState,
264
- });
265
- const [state, { setState, isPending }] = useInteractableState<TaskBoardState>(
266
- id,
267
- taskBoardInitialState,
236
+ const [state, { setState, isPending }] = unstable_useInteractable(
237
+ "taskBoard",
238
+ {
239
+ description:
240
+ "A task board showing the user's tasks. Use update_taskBoard with tasks.add/update/remove/clear to manage tasks. New tasks need a title and done=false.",
241
+ stateSchema: taskBoardSchema,
242
+ initialState: taskBoardInitialState,
243
+ },
268
244
  );
269
245
 
270
- const aui = useAui({ tools: Tools({ toolkit }) });
271
-
272
246
  const doneCount = state.tasks.filter((t) => t.done).length;
273
247
 
274
248
  return (
275
- <AuiProvider value={aui}>
276
- <TaskBoardToolOverrides setState={setState} />
277
- <div className="flex flex-col">
249
+ <>
250
+ <div className="flex h-full flex-col">
278
251
  <div className="flex items-center gap-2 border-b px-4 py-3">
279
252
  <ListTodoIcon className="text-muted-foreground size-4" />
280
253
  <span className="text-sm font-semibold">Task Board</span>
@@ -294,9 +267,9 @@ function TaskBoard() {
294
267
  </p>
295
268
  ) : (
296
269
  <ul className="space-y-1">
297
- {state.tasks.map((task) => (
270
+ {state.tasks.map((task, index) => (
298
271
  <li
299
- key={task.id}
272
+ key={task.id ?? `streaming-${index}`}
300
273
  className="group hover:bg-muted flex items-center gap-2 rounded-lg px-3 py-2 transition-colors"
301
274
  >
302
275
  <button
@@ -309,6 +282,9 @@ function TaskBoard() {
309
282
  }))
310
283
  }
311
284
  className="shrink-0"
285
+ aria-label={
286
+ task.done ? "Mark task undone" : "Mark task done"
287
+ }
312
288
  >
313
289
  {task.done ? (
314
290
  <CheckCircle2Icon className="text-primary size-4" />
@@ -329,6 +305,7 @@ function TaskBoard() {
329
305
  }))
330
306
  }
331
307
  className="shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
308
+ aria-label={`Remove task ${task.title}`}
332
309
  >
333
310
  <Trash2Icon className="text-muted-foreground hover:text-destructive size-3.5" />
334
311
  </button>
@@ -338,59 +315,11 @@ function TaskBoard() {
338
315
  )}
339
316
  </div>
340
317
  </div>
341
- </AuiProvider>
318
+ </>
342
319
  );
343
320
  }
344
321
 
345
- function TaskBoardToolOverrides({
346
- setState,
347
- }: {
348
- setState: Dispatch<SetStateAction<TaskBoardState>>;
349
- }) {
350
- useAuiToolOverrides({
351
- manage_tasks: {
352
- execute: async (args) => {
353
- switch (args.action) {
354
- case "add": {
355
- const id = crypto.randomUUID();
356
- setState((prev) => ({
357
- tasks: [
358
- ...prev.tasks,
359
- { id, title: args.title ?? "Untitled", done: false },
360
- ],
361
- }));
362
- return { success: true, id };
363
- }
364
- case "toggle": {
365
- if (!args.id) return { success: false, error: "id is required" };
366
- setState((prev) => ({
367
- tasks: prev.tasks.map((t) =>
368
- t.id === args.id ? { ...t, done: !t.done } : t,
369
- ),
370
- }));
371
- return { success: true };
372
- }
373
- case "remove": {
374
- if (!args.id) return { success: false, error: "id is required" };
375
- setState((prev) => ({
376
- tasks: prev.tasks.filter((t) => t.id !== args.id),
377
- }));
378
- return { success: true };
379
- }
380
- case "clear": {
381
- setState({ tasks: [] });
382
- return { success: true };
383
- }
384
- default:
385
- return { success: false, error: "Unknown action" };
386
- }
387
- },
388
- },
389
- });
390
- return null;
391
- }
392
-
393
- const COLORS: Record<string, string> = {
322
+ const COLORS: Record<NoteState["color"], string> = {
394
323
  yellow: "bg-yellow-100 border-yellow-300",
395
324
  blue: "bg-blue-100 border-blue-300",
396
325
  green: "bg-green-100 border-green-300",
@@ -398,62 +327,41 @@ const COLORS: Record<string, string> = {
398
327
  };
399
328
 
400
329
  function NoteCard({
401
- noteId,
402
- selectedId,
330
+ note,
331
+ selected,
403
332
  onSelect,
404
333
  onRemove,
405
334
  }: {
406
- noteId: string;
407
- selectedId: string | null;
335
+ note: NoteState;
336
+ selected: boolean;
408
337
  onSelect: (id: string) => void;
409
338
  onRemove: (id: string) => void;
410
339
  }) {
411
- useAssistantInteractable("note", {
412
- id: noteId,
413
- description:
414
- "A sticky note. The AI can partially update any field (title, content, color) without resending the others.",
415
- stateSchema: noteSchema,
416
- initialState: noteInitialState,
417
- selected: selectedId === noteId,
418
- });
419
- const [state, { setSelected }] = useInteractableState<NoteState>(
420
- noteId,
421
- noteInitialState,
422
- );
423
-
424
- const isSelected = selectedId === noteId;
425
-
426
- const handleClick = () => {
427
- onSelect(noteId);
428
- setSelected(true);
429
- };
430
-
431
340
  return (
432
- <div
433
- role="button"
434
- tabIndex={0}
435
- onClick={handleClick}
436
- onKeyDown={(e) => (e.key === "Enter" || e.key === " ") && handleClick()}
437
- className={`group relative flex w-full cursor-pointer flex-col gap-1 rounded-lg border-2 p-3 text-left transition-all ${COLORS[state.color] ?? COLORS.yellow} ${isSelected ? "ring-primary ring-2 ring-offset-1" : "hover:shadow-sm"}`}
438
- >
439
- {isSelected && (
440
- <span className="bg-primary text-primary-foreground absolute top-1.5 right-2 rounded px-1.5 py-0.5 text-[10px] leading-none font-medium">
441
- SELECTED
341
+ <div className="group relative">
342
+ <button
343
+ type="button"
344
+ onClick={() => onSelect(note.id)}
345
+ className={`flex w-full cursor-pointer flex-col gap-1 rounded-lg border-2 p-3 text-left transition-all ${COLORS[note.color] ?? COLORS.yellow} ${selected ? "ring-primary ring-2 ring-offset-1" : "hover:shadow-sm"}`}
346
+ aria-pressed={selected}
347
+ >
348
+ {selected && (
349
+ <span className="bg-primary text-primary-foreground absolute top-1.5 right-2 rounded px-1.5 py-0.5 text-[10px] leading-none font-medium">
350
+ SELECTED
351
+ </span>
352
+ )}
353
+ <span className="pr-16 text-sm font-semibold text-zinc-800">
354
+ {note.title}
355
+ </span>
356
+ <span className="line-clamp-3 text-xs text-zinc-600">
357
+ {note.content || "Empty note"}
442
358
  </span>
443
- )}
444
- <span className="pr-16 text-sm font-semibold text-zinc-800">
445
- {state.title}
446
- </span>
447
- <span className="line-clamp-3 text-xs text-zinc-600">
448
- {state.content || "Empty note"}
449
- </span>
359
+ </button>
450
360
  <button
451
361
  type="button"
452
- onClick={(e) => {
453
- e.stopPropagation();
454
- onRemove(noteId);
455
- }}
362
+ onClick={() => onRemove(note.id)}
456
363
  className="absolute right-2 bottom-2 rounded p-0.5 opacity-0 transition-opacity group-hover:opacity-100 hover:bg-black/10"
364
+ aria-label={`Remove note ${note.title}`}
457
365
  >
458
366
  <Trash2Icon className="size-3 text-zinc-500" />
459
367
  </button>
@@ -461,79 +369,69 @@ function NoteCard({
461
369
  );
462
370
  }
463
371
 
464
- const NOTE_IDS_KEY = "interactables-example-note-ids";
465
-
466
- function loadNoteIds(): string[] {
467
- try {
468
- const saved = localStorage.getItem(NOTE_IDS_KEY);
469
- return saved ? JSON.parse(saved) : [];
470
- } catch {
471
- return [];
472
- }
473
- }
474
-
475
372
  function NotesPanel() {
476
- const [noteIds, setNoteIds] = useState<string[]>([]);
477
- const [selectedId, setSelectedId] = useState<string | null>(null);
478
- const hydratedRef = useRef(false);
479
-
480
- useEffect(() => {
481
- if (!hydratedRef.current) {
482
- hydratedRef.current = true;
483
- const saved = loadNoteIds();
484
- if (saved.length > 0) setNoteIds(saved);
485
- return;
486
- }
487
- localStorage.setItem(NOTE_IDS_KEY, JSON.stringify(noteIds));
488
- }, [noteIds]);
489
-
490
- const aui = useAui({ tools: Tools({ toolkit }) });
373
+ const [state, { setState }] = unstable_useInteractable("notes", {
374
+ description:
375
+ "The sticky-note collection and selected note id. Use update_notes with notes.add/update/remove/clear to manage notes, and set selectedId to change focus. New notes need a title, content, and color.",
376
+ stateSchema: notesSchema,
377
+ initialState: notesInitialState,
378
+ });
491
379
 
492
- const handleSelect = useCallback((id: string) => {
493
- setSelectedId(id);
494
- }, []);
380
+ const handleAdd = useCallback(() => {
381
+ const id = `note-${crypto.randomUUID()}`;
382
+ setState((prev) => ({
383
+ notes: [...prev.notes, { id, ...noteInitialState }],
384
+ selectedId: id,
385
+ }));
386
+ }, [setState]);
387
+
388
+ const handleSelect = useCallback(
389
+ (id: string) => {
390
+ setState((prev) => ({ ...prev, selectedId: id }));
391
+ },
392
+ [setState],
393
+ );
495
394
 
496
- const handleRemove = useCallback((id: string) => {
497
- setNoteIds((prev) => prev.filter((n) => n !== id));
498
- setSelectedId((prev) => (prev === id ? null : prev));
499
- }, []);
395
+ const handleRemove = useCallback(
396
+ (id: string) => {
397
+ setState((prev) => ({
398
+ notes: prev.notes.filter((note) => note.id !== id),
399
+ selectedId: prev.selectedId === id ? null : prev.selectedId,
400
+ }));
401
+ },
402
+ [setState],
403
+ );
500
404
 
501
405
  return (
502
- <AuiProvider value={aui}>
503
- <NotesToolOverrides
504
- setNoteIds={setNoteIds}
505
- setSelectedId={setSelectedId}
506
- />
507
- <div className="flex flex-col">
406
+ <>
407
+ <div className="flex h-full flex-col">
508
408
  <div className="flex items-center gap-2 border-b px-4 py-3">
509
409
  <StickyNoteIcon className="text-muted-foreground size-4" />
510
410
  <span className="text-sm font-semibold">Notes</span>
511
411
  <span className="text-muted-foreground ml-auto text-xs">
512
- {noteIds.length}
412
+ {state.notes.length}
513
413
  </span>
514
414
  <button
515
415
  type="button"
516
- onClick={() => {
517
- const id = `note-${crypto.randomUUID()}`;
518
- setNoteIds((prev) => [...prev, id]);
519
- }}
416
+ onClick={handleAdd}
520
417
  className="hover:bg-muted rounded p-1 transition-colors"
418
+ aria-label="Add note"
521
419
  >
522
420
  <PlusIcon className="text-muted-foreground size-3.5" />
523
421
  </button>
524
422
  </div>
525
423
  <div className="flex-1 overflow-y-auto p-3">
526
- {noteIds.length === 0 ? (
424
+ {state.notes.length === 0 ? (
527
425
  <p className="text-muted-foreground py-6 text-center text-xs">
528
426
  No notes yet. Ask the assistant!
529
427
  </p>
530
428
  ) : (
531
429
  <div className="grid gap-2">
532
- {noteIds.map((noteId) => (
430
+ {state.notes.map((note, index) => (
533
431
  <NoteCard
534
- key={noteId}
535
- noteId={noteId}
536
- selectedId={selectedId}
432
+ key={note.id ?? `streaming-${index}`}
433
+ note={note}
434
+ selected={state.selectedId === note.id}
537
435
  onSelect={handleSelect}
538
436
  onRemove={handleRemove}
539
437
  />
@@ -542,74 +440,32 @@ function NotesPanel() {
542
440
  )}
543
441
  </div>
544
442
  </div>
545
- </AuiProvider>
443
+ </>
546
444
  );
547
445
  }
548
446
 
549
- function NotesToolOverrides({
550
- setNoteIds,
551
- setSelectedId,
552
- }: {
553
- setNoteIds: Dispatch<SetStateAction<string[]>>;
554
- setSelectedId: Dispatch<SetStateAction<string | null>>;
555
- }) {
556
- useAuiToolOverrides({
557
- manage_notes: {
558
- execute: async (args) => {
559
- switch (args.action) {
560
- case "add": {
561
- const id = `note-${crypto.randomUUID()}`;
562
- setNoteIds((prev) => [...prev, id]);
563
- return { success: true, noteId: id };
564
- }
565
- case "remove": {
566
- if (args.noteId) {
567
- setNoteIds((prev) => prev.filter((id) => id !== args.noteId));
568
- }
569
- return { success: true };
570
- }
571
- case "clear": {
572
- setNoteIds([]);
573
- setSelectedId(null);
574
- return { success: true };
575
- }
576
- default:
577
- return { success: false, error: "Unknown action" };
578
- }
579
- },
580
- },
581
- });
582
- return null;
583
- }
584
-
585
447
  const STORAGE_KEY = "interactables-example";
586
448
 
587
- function useInteractablePersistence(aui: ReturnType<typeof useAui>) {
588
- useEffect(() => {
589
- const saved = localStorage.getItem(STORAGE_KEY);
590
- if (saved) {
591
- try {
592
- aui.interactables().importState(JSON.parse(saved));
593
- } catch {
594
- // ignore malformed data
595
- }
449
+ // Module-level so the adapter identity is stable across renders.
450
+ const persistenceAdapter = {
451
+ load: () => {
452
+ try {
453
+ const saved = localStorage.getItem(STORAGE_KEY);
454
+ return saved ? JSON.parse(saved) : undefined;
455
+ } catch {
456
+ return undefined;
596
457
  }
458
+ },
459
+ save: (state: unknown) => {
460
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
461
+ },
462
+ };
597
463
 
598
- aui.interactables().setPersistenceAdapter({
599
- save: (state) => {
600
- localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
601
- },
602
- });
603
- }, [aui]);
604
- }
605
-
606
- export default function Home() {
607
- const runtime = useChatRuntime({
608
- sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
609
- });
610
-
464
+ function InteractablesExample() {
611
465
  const aui = useAui({
612
- interactables: Interactables(),
466
+ unstable_interactables: unstable_Interactables({
467
+ persistence: persistenceAdapter,
468
+ }),
613
469
  suggestions: Suggestions([
614
470
  {
615
471
  title: "Add 3 tasks",
@@ -630,23 +486,33 @@ export default function Home() {
630
486
  ]),
631
487
  });
632
488
 
633
- useInteractablePersistence(aui);
634
-
635
489
  return (
636
- <AssistantRuntimeProvider aui={aui} runtime={runtime}>
637
- <main className="flex h-full">
638
- <div className="flex-1">
490
+ <AuiProvider value={aui}>
491
+ <main className="flex h-full min-h-0">
492
+ <div className="min-w-0 flex-1">
639
493
  <Thread />
640
494
  </div>
641
495
  <div className="flex w-80 flex-col border-l">
642
- <div className="flex-1 overflow-y-auto">
496
+ <div className="min-h-0 flex-1 overflow-y-auto">
643
497
  <NotesPanel />
644
498
  </div>
645
- <div className="flex-1 overflow-y-auto border-t">
499
+ <div className="min-h-0 flex-1 overflow-y-auto border-t">
646
500
  <TaskBoard />
647
501
  </div>
648
502
  </div>
649
503
  </main>
504
+ </AuiProvider>
505
+ );
506
+ }
507
+
508
+ export default function Home() {
509
+ const runtime = useChatRuntime({
510
+ sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
511
+ });
512
+
513
+ return (
514
+ <AssistantRuntimeProvider runtime={runtime}>
515
+ <InteractablesExample />
650
516
  </AssistantRuntimeProvider>
651
517
  );
652
518
  }
@@ -664,7 +530,7 @@ export type TaskBoardState = { tasks: Task[] };
664
530
  export const taskBoardSchema = z.object({
665
531
  tasks: z.array(
666
532
  z.object({
667
- id: z.string(),
533
+ id: z.string().describe("Stable task id, unique within this board."),
668
534
  title: z.string(),
669
535
  done: z.boolean(),
670
536
  }),
@@ -673,69 +539,34 @@ export const taskBoardSchema = z.object({
673
539
 
674
540
  export const taskBoardInitialState: TaskBoardState = { tasks: [] };
675
541
 
676
- export const manageTasksParameters = z.object({
677
- action: z.enum(["add", "toggle", "remove", "clear"]),
678
- title: z.string().optional(),
679
- id: z.string().optional(),
680
- });
681
-
682
- export type ManageTasksArgs = z.infer<typeof manageTasksParameters>;
683
-
684
- export type NoteState = { title: string; content: string; color: string };
542
+ export const noteColorSchema = z.enum(["yellow", "blue", "green", "pink"]);
685
543
 
686
544
  export const noteSchema = z.object({
545
+ id: z.string().describe("Stable note id, unique within this collection."),
687
546
  title: z.string(),
688
547
  content: z.string(),
689
- color: z.enum(["yellow", "blue", "green", "pink"]),
548
+ color: noteColorSchema,
690
549
  });
691
550
 
692
- export const noteInitialState: NoteState = {
551
+ export type NoteState = z.infer<typeof noteSchema>;
552
+
553
+ export const noteInitialState: Omit<NoteState, "id"> = {
693
554
  title: "New Note",
694
555
  content: "",
695
556
  color: "yellow",
696
557
  };
697
558
 
698
- export const manageNotesParameters = z.object({
699
- action: z.enum(["add", "remove", "clear"]),
700
- noteId: z.string().optional(),
559
+ export const notesSchema = z.object({
560
+ notes: z.array(noteSchema),
561
+ selectedId: z.string().nullable(),
701
562
  });
702
563
 
703
- export type ManageNotesArgs = z.infer<typeof manageNotesParameters>;
704
-
705
- ```
706
-
707
- ## app/toolkits.ts
564
+ export type NotesState = z.infer<typeof notesSchema>;
708
565
 
709
- ```typescript
710
- "use generative";
711
-
712
- import { defineToolkit, stubTool } from "@assistant-ui/react";
713
- import { manageNotesParameters, manageTasksParameters } from "./state";
714
-
715
- export default defineToolkit({
716
- manage_tasks: {
717
- description:
718
- 'Manage tasks on the task board. Actions: "add" (requires title), "toggle" (requires id), "remove" (requires id), "clear" (no extra fields).',
719
- parameters: manageTasksParameters,
720
- // The generative compiler strips this stub; TaskBoardToolOverrides provides the real executor.
721
- execute: stubTool(),
722
- renderText: {
723
- running: ({ args }) => `Updating tasks: ${args.action}`,
724
- complete: "Tasks updated",
725
- },
726
- },
727
- manage_notes: {
728
- description:
729
- 'Manage sticky notes. Actions: "add" (creates a new note, returns its id), "remove" (requires noteId), "clear" (removes all notes). After adding, use the update_note_{id} tool to set its content.',
730
- parameters: manageNotesParameters,
731
- // The generative compiler strips this stub; NotesToolOverrides provides the real executor.
732
- execute: stubTool(),
733
- renderText: {
734
- running: ({ args }) => `Updating notes: ${args.action}`,
735
- complete: "Notes updated",
736
- },
737
- },
738
- });
566
+ export const notesInitialState: NotesState = {
567
+ notes: [],
568
+ selectedId: null,
569
+ };
739
570
 
740
571
  ```
741
572
 
@@ -772,14 +603,13 @@ export default defineToolkit({
772
603
  ## next.config.ts
773
604
 
774
605
  ```typescript
775
- import { withAui } from "@assistant-ui/next";
776
606
  import type { NextConfig } from "next";
777
607
 
778
608
  const nextConfig: NextConfig = {
779
609
  /* config options here */
780
610
  };
781
611
 
782
- export default withAui(nextConfig);
612
+ export default nextConfig;
783
613
 
784
614
  ```
785
615
 
@@ -797,14 +627,14 @@ export default withAui(nextConfig);
797
627
  "start": "next start"
798
628
  },
799
629
  "dependencies": {
800
- "@ai-sdk/openai": "^3.0.71",
630
+ "@ai-sdk/openai": "^3.0.73",
801
631
  "@assistant-ui/react": "workspace:*",
802
632
  "@assistant-ui/react-ai-sdk": "workspace:*",
803
633
  "@assistant-ui/ui": "workspace:*",
804
- "ai": "^6.0.204",
634
+ "ai": "^6.0.208",
805
635
  "class-variance-authority": "^0.7.1",
806
636
  "clsx": "^2.1.1",
807
- "lucide-react": "^1.18.0",
637
+ "lucide-react": "^1.21.0",
808
638
  "next": "^16.2.9",
809
639
  "react": "^19.2.7",
810
640
  "react-dom": "^19.2.7",
@@ -812,10 +642,9 @@ export default withAui(nextConfig);
812
642
  "zod": "^4.4.3"
813
643
  },
814
644
  "devDependencies": {
815
- "@assistant-ui/next": "workspace:*",
816
645
  "@assistant-ui/x-buildutils": "workspace:*",
817
646
  "@tailwindcss/postcss": "^4.3.1",
818
- "@types/node": "^25.9.3",
647
+ "@types/node": "^26.0.0",
819
648
  "@types/react": "^19.2.17",
820
649
  "@types/react-dom": "^19.2.3",
821
650
  "postcss": "^8.5.15",
@@ -836,16 +665,16 @@ Demonstrates **interactable components** — persistent UI components whose stat
836
665
 
837
666
  ## Features Demonstrated
838
667
 
839
- ### Task Board (single instance + custom tool)
840
- - `useInteractable("taskBoard", config)` — registers a single interactable
841
- - `Tools({ toolkit })` — custom tool for incremental add/toggle/remove/clear
842
- - Auto-generated `update_taskBoard` tool with **partial updates** (AI only sends changed fields)
668
+ ### Task Board
669
+ - `unstable_useInteractable("taskBoard", config)` — registers a single interactable
670
+ - Auto-generated `update_taskBoard` tool with partial scalar updates and array operations
671
+ - Collection edits go through `tasks.add`, `tasks.update`, `tasks.remove`, and `tasks.clear` on the generated update tool
843
672
 
844
- ### Sticky Notes (multi-instance + selection + partial updates)
845
- - Multiple `<NoteCard>` components each call `useInteractable("note", { id: noteId, ... })`
846
- - **Multi-instance**: each note gets its own `update_note_{id}` tool automatically
847
- - **Selection**: click a note to select it; AI sees `(SELECTED)` in system prompt and prioritizes it
848
- - **Partial updates**: AI can change just `{ color: "pink" }` without resending title and content
673
+ ### Sticky Notes
674
+ - `unstable_useInteractable("notes", config)` — registers the sticky-note collection and `selectedId` as one state object
675
+ - Auto-generated `update_notes` manages both note collection edits and selected-note focus
676
+ - **Selection**: click a note to set `selectedId`; the AI can change focus by updating the same normal state field
677
+ - **Partial updates**: AI can change just one note field with `notes.update: [{ id, color: "pink" }]`
849
678
 
850
679
  ## Getting Started
851
680
 
@@ -865,12 +694,11 @@ Open [http://localhost:3000](http://localhost:3000) to see the example.
865
694
 
866
695
  ## Key Concepts
867
696
 
868
- - **`Interactables()`** — scope resource registered via `useAui`
869
- - **`useInteractable(name, config)`** — returns `[state, setState, { id, setSelected }]`
697
+ - **`unstable_Interactables({ persistence })`** — scope resource registered via `useAui`, with a `load`/`save` adapter
698
+ - **`unstable_useInteractable(name, config)`** — returns `[state, { id, setState, isPending, error, flush }]`
870
699
  - **Partial updates** — auto-generated tools use partial schemas; AI only sends changed fields
871
- - **Multi-instance** — same `name`, different `id`; tools named `update_{name}_{id}`
872
- - **Selection** — `setSelected(true)` marks a component as focused for the AI
873
- - **`Tools({ toolkit })`** — custom frontend tools for fine-grained control
700
+ - **Array operations** — array fields support generated `add`, `update`, `remove`, `clear`, and `set` operations on the same `update_{name}` tool
701
+ - **Selection as state** — focused UI state lives in the interactable's normal schema (`selectedId`), not in a separate manage tool
874
702
  - **`sendAutomaticallyWhen`** — auto-sends follow-up messages when tool calls complete
875
703
 
876
704
  ```