@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
@@ -8,12 +8,9 @@ Mentions let users type `@` in the composer to open a popover picker, select an
8
8
 
9
9
  ## How It Works
10
10
 
11
- ```
12
- User types "@" → Trigger detected → Adapter provides categories/items
13
- ↓
14
- Directive inserted ← User selects item from popover
15
- ↓
16
- Message sent with ":tool[Label]{name=id}" in text
11
+ ```mermaid
12
+ flowchart LR
13
+ type["User types @"] --> trigger["Trigger detected"] --> adapter["Adapter provides<br/>categories / items"] --> select["User selects item<br/>from popover"] --> insert["Directive inserted"] --> sent["Message sent with<br/>:tool[Label]{name=id} text"]
17
14
  ```
18
15
 
19
16
  The mention system has three layers:
@@ -85,7 +82,35 @@ const myAdapter: Unstable_TriggerAdapter = {
85
82
 
86
83
  ### Async Mention Search
87
84
 
88
- The adapter interface is synchronous, but the data it reads can come from any async source. Load results into React state (or a query cache) and read the current snapshot inside the adapter methods. The adapter re-creates on each render, so the popover always sees the latest results.
85
+ The built-in `unstable_useLiveCompletionAdapter` wraps an async fetcher with debouncing, stale-request cancellation (results for an outdated query are dropped), and a single-entry cache. Its `search` returns the last results synchronously and schedules a debounced fetch when the query changes; when results arrive the returned `adapter` re-creates, which re-runs the popover lookup so the fresh items render. It also reports `isLoading`, which you pass to the popover to show a loading state.
86
+
87
+ ```tsx
88
+ import {
89
+ unstable_useLiveCompletionAdapter,
90
+ unstable_defaultDirectiveFormatter,
91
+ } from "@assistant-ui/react";
92
+ import { ComposerTriggerPopover } from "@/components/assistant-ui/composer-trigger-popover";
93
+
94
+ function MentionPopover() {
95
+ const mentions = unstable_useLiveCompletionAdapter({
96
+ fetcher: async (query) => {
97
+ const users = await fetchUsers(query);
98
+ return users.map((u) => ({ id: u.id, type: "user", label: u.name }));
99
+ },
100
+ });
101
+
102
+ return (
103
+ <ComposerTriggerPopover
104
+ char="@"
105
+ adapter={mentions.adapter}
106
+ isLoading={mentions.isLoading}
107
+ directive={{ formatter: unstable_defaultDirectiveFormatter }}
108
+ />
109
+ );
110
+ }
111
+ ```
112
+
113
+ The adapter interface is itself synchronous, so you can also wire async data by hand when you need a custom cache, no debounce, or an existing query client. Load results into React state (or a query cache) and read the current snapshot inside the adapter methods. The adapter re-creates on each render, so the popover always sees the latest results.
89
114
 
90
115
  **With React state and `useEffect`:**
91
116
 
@@ -8,12 +8,9 @@ Slash commands let users type `/` in the composer to open a popover, browse avai
8
8
 
9
9
  ## How It Works
10
10
 
11
- ```
12
- User types "/" → Trigger detected → Adapter provides commands
13
- ↓
14
- Callback fired ← User selects command from popover
15
- ↓
16
- Directive chip left in composer (or removed if removeOnExecute)
11
+ ```mermaid
12
+ flowchart LR
13
+ type["User types /"] --> trigger["Trigger detected"] --> adapter["Adapter provides<br/>commands"] --> select["User selects command<br/>from popover"] --> callback["Callback fired"] --> chip["Directive chip left in composer<br/>(or removed if removeOnExecute)"]
17
14
  ```
18
15
 
19
16
  The slash command system is built on the same [trigger popover architecture](#trigger-popover-architecture) as mentions. A slash command declares its behavior with a `<TriggerPopover.Action>` sub-primitive whose `onExecute` callback fires when an item is chosen.
@@ -57,24 +57,22 @@ const runtime = useChatRuntime({
57
57
  The default action bar does not include a speech button. Add `ActionBarPrimitive.Speak` and `ActionBarPrimitive.StopSpeaking` to your assistant message action bar:
58
58
 
59
59
  ```tsx
60
- import { ActionBarPrimitive, useMessageTTS } from "@assistant-ui/react";
60
+ import { ActionBarPrimitive, AuiIf } from "@assistant-ui/react";
61
61
  import { AudioLinesIcon, StopCircleIcon } from "lucide-react";
62
62
 
63
63
  const AssistantActionBar = () => {
64
- const isSpeaking = useMessageTTS();
65
-
66
64
  return (
67
65
  <ActionBarPrimitive.Root>
68
- {!isSpeaking && (
66
+ <AuiIf condition={(s) => s.message.speech == null}>
69
67
  <ActionBarPrimitive.Speak>
70
68
  <AudioLinesIcon />
71
69
  </ActionBarPrimitive.Speak>
72
- )}
73
- {isSpeaking && (
70
+ </AuiIf>
71
+ <AuiIf condition={(s) => s.message.speech != null}>
74
72
  <ActionBarPrimitive.StopSpeaking>
75
73
  <StopCircleIcon />
76
74
  </ActionBarPrimitive.StopSpeaking>
77
- )}
75
+ </AuiIf>
78
76
  <ActionBarPrimitive.Copy />
79
77
  </ActionBarPrimitive.Root>
80
78
  );
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Thread Virtualization
3
- description: Render very long threads with @tanstack/react-virtual and ThreadPrimitive.MessageByIndex.
3
+ description: Render very long threads with @tanstack/react-virtual, with ThreadPrimitive.Unstable_MessageById and ThreadPrimitive.MessageByIndex.
4
4
  platforms: ["react"]
5
5
  ---
6
6
 
@@ -10,9 +10,35 @@ Virtualization mounts only the messages near the viewport and represents the res
10
10
 
11
11
  Probably not. The default kit already renders message bodies with `content-visibility: auto` and `contain-intrinsic-size`, which skips paint work for off-screen messages, and `ThreadPrimitive.Viewport` handles auto-scroll. That covers typical threads. Reach for virtualization when React mount and update cost itself becomes the bottleneck: threads with hundreds to thousands of messages, or very heavy per-message content, where typing latency degrades because every message stays mounted.
12
12
 
13
- ## Rendering messages by index
13
+ ## Rendering Messages By Id
14
14
 
15
- `ThreadPrimitive.MessageByIndex` renders a single message at a given index and memoizes on the index plus per-field `components` identity. Pass a module-level components object so the memo holds and a streaming delta only re-renders the message it belongs to:
15
+ For virtualized rows, use message ids as the row identity. `unstable_useThreadMessageIds` returns the thread's ids with stable array identity across content-only updates, and `ThreadPrimitive.Unstable_MessageById` renders one message with the same `components` surface as `MessageByIndex`. Unknown ids render `null`, which is useful when a virtual row unmounts after a message was removed.
16
+
17
+ ```tsx
18
+ const MESSAGE_COMPONENTS = { UserMessage, AssistantMessage };
19
+
20
+ const messageIds = unstable_useThreadMessageIds();
21
+
22
+ return messageIds.map((messageId) => (
23
+ <ThreadPrimitive.Unstable_MessageById
24
+ key={messageId}
25
+ messageId={messageId}
26
+ components={MESSAGE_COMPONENTS}
27
+ />
28
+ ));
29
+ ```
30
+
31
+ <Callout type="warn">
32
+ `unstable_useThreadMessageIds` and `ThreadPrimitive.Unstable_MessageById` are
33
+ experimental and may change in any release.
34
+ </Callout>
35
+
36
+ ## Rendering Messages By Index
37
+
38
+ `ThreadPrimitive.MessageByIndex` is still supported and is the smaller API when
39
+ you already have a stable index from a fixed-order list. It renders a single
40
+ message at that index and memoizes on the index plus the per-field `components`
41
+ identity:
16
42
 
17
43
  ```tsx
18
44
  const MESSAGE_COMPONENTS = { UserMessage, AssistantMessage };
@@ -20,15 +46,59 @@ const MESSAGE_COMPONENTS = { UserMessage, AssistantMessage };
20
46
  <ThreadPrimitive.MessageByIndex index={index} components={MESSAGE_COMPONENTS} />;
21
47
  ```
22
48
 
49
+ For virtualizers, prefer the id-based API above when you can. Index rows are more
50
+ fragile when messages are inserted, removed, reordered, or when a virtual row
51
+ briefly outlives the item it was created for.
52
+
23
53
  ## Grouping into turns
24
54
 
25
- Virtualizing per user turn (a user message plus the responses that follow it) gives the virtualizer stable, meaningfully sized items. Subscribe to a single signature string so the selector returns a primitive and the turn array only rebuilds when membership actually changes:
55
+ The example virtualizes per user turn (a user message plus the responses that follow it), which gives the virtualizer stable, meaningfully sized items. Keep the id and role together in a stable row shape so the turn array only rebuilds when message membership or roles change:
26
56
 
27
57
  ```tsx
28
- const signature = useAuiState((s) =>
29
- s.thread.messages.map((m, i) => `${i}:${m.role}:${m.id}`).join("\n"),
58
+ type MessageRow = {
59
+ id: string;
60
+ role: "user" | "assistant" | "system";
61
+ };
62
+
63
+ const useThreadMessageRows = (): readonly MessageRow[] => {
64
+ const prevRowsRef = useRef<readonly MessageRow[]>([]);
65
+
66
+ return useAuiState((s) => {
67
+ const messages = s.thread.messages;
68
+ const prev = prevRowsRef.current;
69
+ if (
70
+ prev.length === messages.length &&
71
+ prev.every((row, index) => {
72
+ const message = messages[index]!;
73
+ return row.id === message.id && row.role === message.role;
74
+ })
75
+ ) {
76
+ return prev;
77
+ }
78
+
79
+ const next = messages.map(({ id, role }) => ({ id, role }));
80
+ prevRowsRef.current = next;
81
+ return next;
82
+ });
83
+ };
84
+
85
+ const messageRows = useThreadMessageRows();
86
+ const turns = useMemo(
87
+ () => buildTurns(messageRows),
88
+ [messageRows],
30
89
  );
31
- const turns = useMemo(() => buildTurns(signature), [signature]);
90
+ ```
91
+
92
+ Then each virtual turn renders the ids it owns:
93
+
94
+ ```tsx
95
+ {turn.messageIds.map((messageId) => (
96
+ <ThreadPrimitive.Unstable_MessageById
97
+ key={messageId}
98
+ messageId={messageId}
99
+ components={MESSAGE_COMPONENTS}
100
+ />
101
+ ))}
32
102
  ```
33
103
 
34
104
  ## Padding spacers, not absolute positioning
@@ -156,8 +156,9 @@ class MyVoiceAdapter implements RealtimeVoiceAdapter {
156
156
 
157
157
  The session status follows the same pattern as other adapters:
158
158
 
159
- ```
160
- starting → running → ended
159
+ ```mermaid
160
+ flowchart LR
161
+ starting["starting"] --> running["running"] --> ended["ended"]
161
162
  ```
162
163
 
163
164
  The `ended` status includes a `reason`:
@@ -9,18 +9,10 @@ For the adapter contract itself, see [adapters](/docs/runtimes/concepts/adapters
9
9
 
10
10
  ## How it works
11
11
 
12
- ```
13
- composer add ──► POST /api/upload (presign) ──► PUT to object storage
14
- │
15
- ▼
16
- PendingAttachment with the public URL
17
- │
18
- composer send ◄────────────────────────────────────┘
19
- │
20
- └─► send() emits a content part with the URL
21
- │
22
- ▼
23
- AI SDK passes URL to the model
12
+ ```mermaid
13
+ flowchart LR
14
+ add["composer add"] --> presign["POST /api/upload<br/>(presign)"] --> put["PUT to object storage"] --> pending["PendingAttachment<br/>with the public URL"]
15
+ pending --> send["composer send"] --> part["send() emits a<br/>content part with the URL"] --> model["AI SDK passes<br/>URL to the model"]
24
16
  ```
25
17
 
26
18
  Three ideas to internalize before reading the code:
@@ -9,16 +9,13 @@ This page covers the non-cloud path. AssistantCloud users should see [cloud auth
9
9
 
10
10
  ## How it works
11
11
 
12
- ```
13
- browser ──► /api/auth/[...all] ──► better-auth handlers
14
- /api/chat │
15
- /api/threads/* │
16
- │ │
17
- ▼ ▼
18
- auth.api.getSession({ headers }) ──► session.user.id
19
- │
20
- ▼
21
- scope queries by userId
12
+ ```mermaid
13
+ flowchart LR
14
+ browser["browser"] --> routes["/api/auth/[...all]<br/>/api/chat<br/>/api/threads/*"]
15
+ routes --> handlers["better-auth handlers"]
16
+ routes --> session["auth.api.getSession({ headers })"]
17
+ handlers --> session
18
+ session --> uid["session.user.id"] --> scope["scope queries by userId"]
22
19
  ```
23
20
 
24
21
  Three integration points:
@@ -9,15 +9,9 @@ If you use AssistantCloud, see [cloud authorization](/docs/cloud/authorization)
9
9
 
10
10
  ## How it works
11
11
 
12
- ```
13
- browser ──► clerkMiddleware (proxy.ts) ──► /api/chat
14
- /api/threads/*
15
- │
16
- ▼
17
- auth() returns { userId }
18
- │
19
- ▼
20
- scope queries by userId
12
+ ```mermaid
13
+ flowchart LR
14
+ browser["browser"] --> mw["clerkMiddleware<br/>(proxy.ts)"] --> routes["/api/chat<br/>/api/threads/*"] --> auth["auth() returns { userId }"] --> scope["scope queries by userId"]
21
15
  ```
22
16
 
23
17
  Three places Clerk touches the integration:
@@ -9,14 +9,9 @@ If you use AssistantCloud, see [cloud authorization](/docs/cloud) instead; cloud
9
9
 
10
10
  ## How it works
11
11
 
12
- ```
13
- browser ──► /api/chat ──► auth() returns session
14
- /api/threads/* │
15
- ▼
16
- scope queries by session.user.id
17
- │
18
- ▼
19
- database
12
+ ```mermaid
13
+ flowchart LR
14
+ browser["browser"] --> routes["/api/chat<br/>/api/threads/*"] --> auth["auth() returns session"] --> scope["scope queries by<br/>session.user.id"] --> db["database"]
20
15
  ```
21
16
 
22
17
  Three places auth touches the integration:
@@ -13,11 +13,14 @@ If you arrived here looking to wire up your first chat: jump to [AI SDK v6 quick
13
13
 
14
14
  ## Where it slots in
15
15
 
16
- ```
17
- client ──► useChatRuntime (react-ai-sdk) ──► /api/chat (streamText) ──► provider
18
- │
19
- └─ thread state, tool calls, attachments,
20
- speech / dictation / feedback adapters
16
+ ```mermaid
17
+ flowchart LR
18
+ client["client"]
19
+ runtime["useChatRuntime (react-ai-sdk)<br/>(thread state · tool calls · attachments<br/>speech · dictation · feedback adapters)"]
20
+ api["/api/chat<br/>(streamText)"]
21
+ provider["provider"]
22
+
23
+ client --> runtime --> api --> provider
21
24
  ```
22
25
 
23
26
  `@assistant-ui/react-ai-sdk` wraps the AI SDK's `useChat` hook and exposes it as an assistant-ui runtime. The runtime owns conversation state on the client; your `/api/chat` route returns a UI message stream from `streamText`. Everything else (tools, attachments, observability, gateways, custom persistence) layers on top of this base.
@@ -15,18 +15,11 @@ Integrations are wiring guides for using third-party services with assistant-ui,
15
15
 
16
16
  ## Where integrations slot in
17
17
 
18
- ```
19
- client ──► your API route ──► LLM provider
20
- │ ▲
21
- │ │
22
- agent │ │ observability
23
- frameworks │ │ proxies
24
- (e.g. │ │ (e.g. Helicone,
25
- Mastra) │ │ Langfuse)
26
- ▼ │
27
- run on the server, │
28
- then forward calls ──────┘
29
- to the provider
18
+ ```mermaid
19
+ flowchart LR
20
+ client["client"] --> route["your API route"] --> provider["LLM provider"]
21
+ route -->|"agent frameworks (e.g. Mastra)"| server["run on the server,<br/>then forward calls"]
22
+ server -->|"observability proxies (e.g. Helicone, Langfuse)"| provider
30
23
  ```
31
24
 
32
25
  Integrations live on the server. **Agent frameworks** like Mastra take over the API route. **Gateways** swap the upstream provider URL. **Observability** logs or traces every call. **Auth** gates the route and scopes per-user data. **Persistence** and **attachments** are adapter recipes for storing chat data outside the default in-memory path.
@@ -11,10 +11,9 @@ Helicone is independent of which assistant-ui runtime you use. It slots in at th
11
11
 
12
12
  ## How it works
13
13
 
14
- ```
15
- your server ──► Helicone proxy ──► OpenAI / Anthropic / etc.
16
- │
17
- └─ logs request, response, tokens, cost
14
+ ```mermaid
15
+ flowchart LR
16
+ server["your server"] --> proxy["Helicone proxy<br/>(logs request, response, tokens, cost)"] --> provider["OpenAI / Anthropic / etc."]
18
17
  ```
19
18
 
20
19
  Calls pass through Helicone's edge before reaching the upstream provider. The proxy is transparent: response shape and streaming behavior are unchanged, you just gain a dashboard of every call.
@@ -13,11 +13,9 @@ Pick Langfuse when you want to see the agent's full call tree inside a single tu
13
13
 
14
14
  ## How it works
15
15
 
16
- ```
17
- your route ──► AI SDK streamText (with experimental_telemetry)
18
- │
19
- ▼
20
- OpenTelemetry SDK ──► LangfuseSpanProcessor ──► Langfuse
16
+ ```mermaid
17
+ flowchart LR
18
+ route["your route"] --> stream["AI SDK streamText<br/>(experimental_telemetry)"] --> otel["OpenTelemetry SDK"] --> proc["LangfuseSpanProcessor"] --> langfuse["Langfuse"]
21
19
  ```
22
20
 
23
21
  Langfuse subscribes to OpenTelemetry spans the AI SDK already emits when telemetry is enabled. No proxy, no wrapping; the SDK ships spans and Langfuse renders them.
@@ -15,8 +15,9 @@ This page covers the **AI SDK** path. If you use [`@assistant-ui/react-langgraph
15
15
 
16
16
  LangSmith provides a wrapper around the `ai` namespace. You call `wrapAISDK(ai)`, get back the same exports (`generateText`, `streamText`, `generateObject`, `streamObject`), and use those in place of the originals. Every call is then traced.
17
17
 
18
- ```
19
- your route ──► wrapped streamText ──► LangSmith client ──► LangSmith
18
+ ```mermaid
19
+ flowchart LR
20
+ route["your route"] --> stream["wrapped streamText"] --> client["LangSmith client"] --> langsmith["LangSmith"]
20
21
  ```
21
22
 
22
23
  ## Setup
@@ -18,9 +18,10 @@ If you're rolling your own auth, replace `auth()` calls with whatever your stack
18
18
 
19
19
  ## How it works
20
20
 
21
- ```
22
- client ──► /api/threads/* (RemoteThreadListAdapter) ──► threads table
23
- /api/messages/* (ThreadHistoryAdapter) ──► messages table
21
+ ```mermaid
22
+ flowchart LR
23
+ client["client"] --> threads["/api/threads/*<br/>(RemoteThreadListAdapter)"] --> tt["threads table"]
24
+ client --> messages["/api/messages/*<br/>(ThreadHistoryAdapter)"] --> mt["messages table"]
24
25
  ```
25
26
 
26
27
  Two adapters, two tables:
@@ -103,6 +103,14 @@ import { ComposerPrimitive } from "@assistant-ui/react";
103
103
 
104
104
  The primitive's behavior (keyboard handling, disabled state, form submission) is merged onto your element. Your styles, your component, primitive wiring.
105
105
 
106
+ <Callout type="info">
107
+ Own the input DOM entirely, such as a `contentEditable` surface or editor
108
+ library that cannot be expressed through `asChild` or `render`? See
109
+ [Headless Composer Input](/docs/guides/headless-composer-input) for the
110
+ unstable hook that supplies composer text and send gating without
111
+ `ComposerPrimitive.Input`.
112
+ </Callout>
113
+
106
114
  ### Unstable Trigger Popovers
107
115
 
108
116
  Composer includes an unstable **trigger popover** system for character-triggered popovers (e.g. `@` for mentions, `/` for slash commands). Multiple triggers coexist under a single `TriggerPopoverRoot`.
@@ -319,6 +319,30 @@ Renders a single message at a specific index in the thread.
319
319
 
320
320
  <PrimitivesTypeTable type="ThreadPrimitiveMessageByIndexProps" parameters={ThreadPrimitiveDocs.MessageByIndex.props} />
321
321
 
322
+ ### Unstable_MessageById
323
+
324
+ Renders a single message by id with the same `components` surface as
325
+ `MessageByIndex`. Pair it with `unstable_useThreadMessageIds` for virtualized or
326
+ custom message lists that should stay attached to messages across reordering and
327
+ windowing. Unknown ids render `null`.
328
+
329
+ ```tsx
330
+ const messageIds = unstable_useThreadMessageIds();
331
+
332
+ messageIds.map((messageId) => (
333
+ <ThreadPrimitive.Unstable_MessageById
334
+ key={messageId}
335
+ messageId={messageId}
336
+ components={{ Message: MyMessage }}
337
+ />
338
+ ));
339
+ ```
340
+
341
+ <Callout type="warn">
342
+ `unstable_useThreadMessageIds` and `ThreadPrimitive.Unstable_MessageById` are
343
+ experimental and may change in any release.
344
+ </Callout>
345
+
322
346
  ### ScrollToBottom
323
347
 
324
348
  Scrolls the viewport to the bottom. Automatically disabled when already at the bottom. Renders a `<button>` element unless `asChild` is set.
@@ -68,6 +68,8 @@ time, the same way a live run never stores them.
68
68
 
69
69
  `fromAgUiMessages` reconstructs the text, reasoning, and tool calls of each message. Multimodal user input (`image`, `audio`, `video`, and `document` parts, as well as legacy `binary` parts) is restored as attachments on the user message, so a backend that persists multimodal messages shows them again on reload and re-sends them on the next run. Legacy `binary` parts that only reference a file id are not restored.
70
70
 
71
+ An assistant message whose tool call has no matching tool result is reconstructed with `requires-action` status, the same status the runtime derives for a pending tool call, so a reloaded human-in-the-loop call (for example an `ask_user` tool) is actionable rather than stuck. This matches how every other external-store runtime surfaces a pending tool call on reload. The AG-UI wire snapshot carries no run outcome, so a tool call that a successful run intentionally left without a result is also surfaced as actionable.
72
+
71
73
  ## Thread list (experimental)
72
74
 
73
75
  <Callout type="warn">
@@ -131,6 +133,25 @@ await runtime.unstable_submitInterruptResponses(
131
133
  );
132
134
  ```
133
135
 
136
+ ### Steering away from an interrupt
137
+
138
+ When the user ignores the interrupt UI and just sends a new message, use the `useAgUiSteerAway` hook. Every open interrupt defaults to `status: "cancelled"`, the new message is appended, and the run resumes with `resume: ResumeEntry[]` on the wire. With no pending interrupts it behaves like a normal append.
139
+
140
+ ```tsx
141
+ const steerAway = useAgUiSteerAway();
142
+
143
+ // the user typed a new message instead of answering the interrupt
144
+ await steerAway("actually, let's do something else");
145
+ ```
146
+
147
+ The message accepts a plain string or a partial `AppendMessage` (the parent defaults to the current head, which is the interrupted assistant message). Pass `responses` to override the status of specific interrupts; the rest still default to cancelled.
148
+
149
+ ```tsx
150
+ await steerAway("continue without the file", [
151
+ { interruptId: "tool-1", status: "resolved", payload: { approved: true } },
152
+ ]);
153
+ ```
154
+
134
155
  ## Supported events
135
156
 
136
157
  The runtime parses the AG-UI event stream and maps each event type to assistant-ui state.
@@ -299,6 +299,54 @@ runtime.thread.import(repo);
299
299
 
300
300
  Each message must have an explicit `id` and `parentId`; messages with the same `parentId` create branches. Parents must appear before children in the array.
301
301
 
302
+ ### Exporting a snapshot
303
+
304
+ `thread.import()` has a counterpart, `thread.export()`, which captures the current thread (including its full branch tree) as a serializable `ExportedMessageRepository`. Use it to persist a conversation and re-import it later:
305
+
306
+ ```tsx
307
+ // capture the current thread as a serializable snapshot
308
+ const repo = runtime.thread.export();
309
+ await saveToBackend(JSON.stringify(repo));
310
+
311
+ // later, restore it into a runtime
312
+ runtime.thread.import(repo);
313
+ ```
314
+
315
+ The exported shape round-trips through `thread.import()` directly, so the same value is both your persistence format and what you load back.
316
+
317
+ ### Persisting branch selection
318
+
319
+ If you store the full branch tree outside assistant-ui, persist the selected branch head too and pass it back as `messageRepository.headId`. `setMessages` still performs the branch switch; `unstable_onBranchChange` is an additional signal that fires after an explicit `switchToBranch` action, such as a BranchPicker click.
320
+
321
+ ```tsx
322
+ const runtime = useExternalStoreRuntime({
323
+ messageRepository: {
324
+ messages: storedMessages,
325
+ headId: selectedHeadId,
326
+ },
327
+ setMessages: (messages) => {
328
+ setVisibleMessages(messages);
329
+ },
330
+ unstable_onBranchChange: ({ headId, visibleMessageIds }) => {
331
+ saveSelectedBranch({
332
+ headId,
333
+ visibleMessageIds,
334
+ });
335
+ },
336
+ onNew,
337
+ });
338
+ ```
339
+
340
+ `headId` is the canonical persisted head of the visible branch. Optimistic or transient message ids are not surfaced there. `visibleMessageIds` is the currently visible path in order, which can include an optimistic leaf while `headId` points to its persisted ancestor.
341
+
342
+ The callback only fires for explicit branch switches, and consecutive switches that resolve to the same canonical head are de-duped. It does not fire on adapter resync, `messageRepository` reset, append, edit/regenerate, content-only updates, or while the thread is running.
343
+
344
+ <Callout type="warn">
345
+ `unstable_onBranchChange` is under active development and may change without
346
+ notice. It complements `setMessages`; it does not enable branch switching by
347
+ itself.
348
+ </Callout>
349
+
302
350
  ## Tool calling
303
351
 
304
352
  Handle tool results by updating the matching tool-call entry:
@@ -717,6 +765,12 @@ useExternalStoreRuntime({
717
765
  type: "(messages: readonly T[]) => void",
718
766
  description: "Update messages (required for branch switching).",
719
767
  },
768
+ {
769
+ name: "unstable_onBranchChange",
770
+ type: "(event: ExternalStoreBranchChange) => void",
771
+ description:
772
+ "Called after an explicit branch switch with the canonical persisted head id and visible message path. Complements setMessages and is unstable.",
773
+ },
720
774
  {
721
775
  name: "onEdit",
722
776
  type: "(message: AppendMessage) => Promise<void>",