@assistant-ui/mcp-docs-server 0.1.22 → 0.1.24

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 (75) hide show
  1. package/.docs/organized/code-examples/waterfall.md +801 -0
  2. package/.docs/organized/code-examples/with-ag-ui.md +39 -27
  3. package/.docs/organized/code-examples/with-ai-sdk-v6.md +39 -29
  4. package/.docs/organized/code-examples/with-artifacts.md +467 -0
  5. package/.docs/organized/code-examples/with-assistant-transport.md +32 -25
  6. package/.docs/organized/code-examples/with-chain-of-thought.md +42 -33
  7. package/.docs/organized/code-examples/with-cloud-standalone.md +674 -0
  8. package/.docs/organized/code-examples/with-cloud.md +35 -28
  9. package/.docs/organized/code-examples/with-custom-thread-list.md +35 -28
  10. package/.docs/organized/code-examples/with-elevenlabs-scribe.md +42 -31
  11. package/.docs/organized/code-examples/with-expo.md +2012 -0
  12. package/.docs/organized/code-examples/with-external-store.md +32 -26
  13. package/.docs/organized/code-examples/with-ffmpeg.md +32 -28
  14. package/.docs/organized/code-examples/with-langgraph.md +97 -39
  15. package/.docs/organized/code-examples/with-parent-id-grouping.md +33 -26
  16. package/.docs/organized/code-examples/with-react-hook-form.md +63 -61
  17. package/.docs/organized/code-examples/with-react-router.md +38 -31
  18. package/.docs/organized/code-examples/with-store.md +17 -25
  19. package/.docs/organized/code-examples/with-tanstack.md +36 -26
  20. package/.docs/organized/code-examples/with-tap-runtime.md +11 -25
  21. package/.docs/raw/docs/(docs)/cli.mdx +13 -6
  22. package/.docs/raw/docs/(docs)/guides/attachments.mdx +26 -3
  23. package/.docs/raw/docs/(docs)/guides/chain-of-thought.mdx +5 -5
  24. package/.docs/raw/docs/(docs)/guides/context-api.mdx +53 -52
  25. package/.docs/raw/docs/(docs)/guides/dictation.mdx +0 -2
  26. package/.docs/raw/docs/(docs)/guides/message-timing.mdx +169 -0
  27. package/.docs/raw/docs/(docs)/guides/quoting.mdx +327 -0
  28. package/.docs/raw/docs/(docs)/guides/speech.mdx +0 -1
  29. package/.docs/raw/docs/(docs)/index.mdx +12 -2
  30. package/.docs/raw/docs/(docs)/installation.mdx +8 -2
  31. package/.docs/raw/docs/(docs)/llm.mdx +9 -7
  32. package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar-more.mdx +1 -1
  33. package/.docs/raw/docs/(reference)/api-reference/primitives/action-bar.mdx +2 -2
  34. package/.docs/raw/docs/(reference)/api-reference/primitives/assistant-if.mdx +27 -27
  35. package/.docs/raw/docs/(reference)/api-reference/primitives/composer.mdx +60 -0
  36. package/.docs/raw/docs/(reference)/api-reference/primitives/message-part.mdx +78 -4
  37. package/.docs/raw/docs/(reference)/api-reference/primitives/message.mdx +32 -0
  38. package/.docs/raw/docs/(reference)/api-reference/primitives/selection-toolbar.mdx +61 -0
  39. package/.docs/raw/docs/(reference)/api-reference/primitives/thread.mdx +1 -1
  40. package/.docs/raw/docs/(reference)/legacy/styled/assistant-modal.mdx +1 -6
  41. package/.docs/raw/docs/(reference)/legacy/styled/decomposition.mdx +2 -2
  42. package/.docs/raw/docs/(reference)/legacy/styled/markdown.mdx +1 -6
  43. package/.docs/raw/docs/(reference)/legacy/styled/thread.mdx +1 -5
  44. package/.docs/raw/docs/(reference)/migrations/v0-12.mdx +17 -17
  45. package/.docs/raw/docs/cloud/ai-sdk-assistant-ui.mdx +209 -0
  46. package/.docs/raw/docs/cloud/ai-sdk.mdx +296 -0
  47. package/.docs/raw/docs/cloud/authorization.mdx +178 -79
  48. package/.docs/raw/docs/cloud/{persistence/langgraph.mdx → langgraph.mdx} +2 -2
  49. package/.docs/raw/docs/cloud/overview.mdx +29 -39
  50. package/.docs/raw/docs/react-native/adapters.mdx +118 -0
  51. package/.docs/raw/docs/react-native/custom-backend.mdx +210 -0
  52. package/.docs/raw/docs/react-native/hooks.mdx +364 -0
  53. package/.docs/raw/docs/react-native/index.mdx +332 -0
  54. package/.docs/raw/docs/react-native/primitives.mdx +653 -0
  55. package/.docs/raw/docs/runtimes/ai-sdk/v6.mdx +60 -15
  56. package/.docs/raw/docs/runtimes/assistant-transport.mdx +103 -0
  57. package/.docs/raw/docs/runtimes/custom/external-store.mdx +25 -2
  58. package/.docs/raw/docs/runtimes/data-stream.mdx +1 -3
  59. package/.docs/raw/docs/runtimes/langgraph/index.mdx +113 -9
  60. package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +1 -4
  61. package/.docs/raw/docs/ui/attachment.mdx +4 -2
  62. package/.docs/raw/docs/ui/context-display.mdx +147 -0
  63. package/.docs/raw/docs/ui/message-timing.mdx +92 -0
  64. package/.docs/raw/docs/ui/part-grouping.mdx +1 -1
  65. package/.docs/raw/docs/ui/reasoning.mdx +4 -4
  66. package/.docs/raw/docs/ui/scrollbar.mdx +2 -2
  67. package/.docs/raw/docs/ui/syntax-highlighting.mdx +55 -50
  68. package/.docs/raw/docs/ui/thread.mdx +16 -9
  69. package/dist/index.d.ts +1 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/package.json +3 -3
  72. package/src/tools/tests/integration.test.ts +2 -2
  73. package/src/tools/tests/json-parsing.test.ts +1 -1
  74. package/src/tools/tests/mcp-protocol.test.ts +1 -3
  75. package/.docs/raw/docs/cloud/persistence/ai-sdk.mdx +0 -108
@@ -45,9 +45,7 @@ For `useChatRuntime`, attachments work automatically without additional configur
45
45
  ```tsx title="/app/MyRuntimeProvider.tsx"
46
46
  import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
47
47
 
48
- const runtime = useChatRuntime({
49
- api: "/api/chat",
50
- });
48
+ const runtime = useChatRuntime();
51
49
  ```
52
50
 
53
51
  <Callout type="info">
@@ -479,6 +477,31 @@ class ValidatedImageAdapter implements AttachmentAdapter {
479
477
  }
480
478
  ```
481
479
 
480
+ ### External Source Attachments
481
+
482
+ Add attachments from external sources (URLs, API data, CMS references) without needing a `File` object or an `AttachmentAdapter`:
483
+
484
+ ```tsx
485
+ const aui = useAui();
486
+
487
+ // Add an attachment from an external source
488
+ await aui.composer().addAttachment({
489
+ name: "report.pdf",
490
+ contentType: "application/pdf",
491
+ content: [{ type: "text", text: "Extracted document content..." }],
492
+ });
493
+
494
+ // Optionally provide id and type
495
+ await aui.composer().addAttachment({
496
+ id: "cms-doc-123",
497
+ type: "document",
498
+ name: "Product Spec",
499
+ content: [{ type: "text", text: "Product specification content..." }],
500
+ });
501
+ ```
502
+
503
+ External attachments are added as complete attachments directly — they skip the `AttachmentAdapter` entirely and can be removed without one.
504
+
482
505
  ### Multiple File Selection
483
506
 
484
507
  Enable multi-file selection with custom limits:
@@ -46,7 +46,7 @@ const ChainOfThought: FC = () => {
46
46
  <ChainOfThoughtPrimitive.AccordionTrigger className="flex w-full cursor-pointer items-center gap-2 px-4 py-2 font-medium text-sm hover:bg-muted/50">
47
47
  Thinking
48
48
  </ChainOfThoughtPrimitive.AccordionTrigger>
49
- <AuiIf condition={({ chainOfThought }) => !chainOfThought.collapsed}>
49
+ <AuiIf condition={(s) => !s.chainOfThought.collapsed}>
50
50
  <ChainOfThoughtPrimitive.Parts
51
51
  components={{ Reasoning, tools: { Fallback: ToolFallback } }}
52
52
  />
@@ -110,7 +110,7 @@ A button that toggles the collapsed/expanded state. Collapsed by default.
110
110
  Renders the grouped parts when expanded (nothing when collapsed).
111
111
 
112
112
  ```tsx
113
- <AuiIf condition={({ chainOfThought }) => !chainOfThought.collapsed}>
113
+ <AuiIf condition={(s) => !s.chainOfThought.collapsed}>
114
114
  <ChainOfThoughtPrimitive.Parts
115
115
  components={{
116
116
  Reasoning,
@@ -140,10 +140,10 @@ import { ChevronDownIcon, ChevronRightIcon } from "lucide-react";
140
140
  const ChainOfThoughtAccordionTrigger = () => {
141
141
  return (
142
142
  <ChainOfThoughtPrimitive.AccordionTrigger className="flex w-full cursor-pointer items-center gap-2 px-4 py-2 text-sm">
143
- <AuiIf condition={({ chainOfThought }) => chainOfThought.collapsed}>
143
+ <AuiIf condition={(s) => s.chainOfThought.collapsed}>
144
144
  <ChevronRightIcon className="size-4" />
145
145
  </AuiIf>
146
- <AuiIf condition={({ chainOfThought }) => !chainOfThought.collapsed}>
146
+ <AuiIf condition={(s) => !s.chainOfThought.collapsed}>
147
147
  <ChevronDownIcon className="size-4" />
148
148
  </AuiIf>
149
149
  Thinking
@@ -154,7 +154,7 @@ const ChainOfThoughtAccordionTrigger = () => {
154
154
 
155
155
  ## Full Example
156
156
 
157
- See the complete [with-chain-of-thought example](https://github.com/Yonom/assistant-ui/tree/main/examples/with-chain-of-thought) for a working implementation with tool calls and reasoning.
157
+ See the complete [with-chain-of-thought example](https://github.com/assistant-ui/assistant-ui/tree/main/examples/with-chain-of-thought) for a working implementation with tool calls and reasoning.
158
158
 
159
159
  ## Related Guides
160
160
 
@@ -50,10 +50,10 @@ assistant-ui organizes state into **scopes** - logical boundaries that provide a
50
50
  // Inside a message component
51
51
  function MessageButton() {
52
52
  // ✅ Available: message scope (current message)
53
- const role = useAuiState(({ message }) => message.role);
53
+ const role = useAuiState((s) => s.message.role);
54
54
 
55
55
  // ✅ Available: thread scope (parent)
56
- const isRunning = useAuiState(({ thread }) => thread.isRunning);
56
+ const isRunning = useAuiState((s) => s.thread.isRunning);
57
57
  }
58
58
  ```
59
59
 
@@ -76,14 +76,14 @@ Read state reactively with automatic re-renders when values change. This hook wo
76
76
  import { useAuiState } from "@assistant-ui/react";
77
77
 
78
78
  // Basic usage - extract a single property
79
- const role = useAuiState(({ message }) => message.role); // "user" | "assistant"
80
- const isRunning = useAuiState(({ thread }) => thread.isRunning); // boolean
79
+ const role = useAuiState((s) => s.message.role); // "user" | "assistant"
80
+ const isRunning = useAuiState((s) => s.thread.isRunning); // boolean
81
81
 
82
82
  // Access nested data
83
83
  const attachmentCount = useAuiState(
84
- ({ composer }) => composer.attachments.length,
84
+ (s) => s.composer.attachments.length,
85
85
  );
86
- const lastMessage = useAuiState(({ thread }) => thread.messages.at(-1));
86
+ const lastMessage = useAuiState((s) => s.thread.messages.at(-1));
87
87
  ```
88
88
 
89
89
  The selector function receives all available scopes for your component's location and should return a specific value. The component re-renders only when that returned value changes.
@@ -93,25 +93,25 @@ The selector function receives all available scopes for your component's locatio
93
93
  ```tsx
94
94
  // Access multiple scopes
95
95
  const canSend = useAuiState(
96
- ({ thread, composer }) => !thread.isRunning && composer.text.length > 0,
96
+ (s) => !s.thread.isRunning && s.composer.text.length > 0,
97
97
  );
98
98
 
99
99
  // Compute derived state
100
- const messageCount = useAuiState(({ thread }) => thread.messages.length);
100
+ const messageCount = useAuiState((s) => s.thread.messages.length);
101
101
  ```
102
102
 
103
103
  **Important:** Never create new objects in selectors. Return primitive values or stable references to avoid infinite re-renders.
104
104
 
105
105
  ```tsx
106
106
  // ❌ Bad - creates new object every time
107
- const data = useAuiState(({ message }) => ({
108
- role: message.role,
109
- content: message.content,
107
+ const data = useAuiState((s) => ({
108
+ role: s.message.role,
109
+ content: s.message.content,
110
110
  }));
111
111
 
112
112
  // ✅ Good - returns stable values
113
- const role = useAuiState(({ message }) => message.role);
114
- const content = useAuiState(({ message }) => message.content);
113
+ const role = useAuiState((s) => s.message.role);
114
+ const content = useAuiState((s) => s.message.content);
115
115
  ```
116
116
 
117
117
  ### useAui
@@ -191,7 +191,8 @@ api.part().getState();
191
191
  aui.composer().send();
192
192
  aui.composer().setText(text);
193
193
  aui.composer().setRole(role);
194
- aui.composer().addAttachment(file);
194
+ aui.composer().addAttachment(file); // File object
195
+ aui.composer().addAttachment({ name, content }); // external source
195
196
  aui.composer().clearAttachments();
196
197
  aui.composer().reset();
197
198
  aui.composer().getState();
@@ -335,7 +336,7 @@ const threadItem = aui.threads().item({ id: "thread_123" });
335
336
 
336
337
  ```tsx
337
338
  function RunIndicator() {
338
- const isRunning = useAuiState(({ thread }) => thread.isRunning);
339
+ const isRunning = useAuiState((s) => s.thread.isRunning);
339
340
 
340
341
  if (!isRunning) return null;
341
342
  return <div>Assistant is thinking...</div>;
@@ -361,8 +362,8 @@ function CopyButton() {
361
362
  ```tsx
362
363
  function SmartComposer() {
363
364
  const aui = useAui();
364
- const isRunning = useAuiState(({ thread }) => thread.isRunning);
365
- const text = useAuiState(({ composer }) => composer.text);
365
+ const isRunning = useAuiState((s) => s.thread.isRunning);
366
+ const text = useAuiState((s) => s.composer.text);
366
367
 
367
368
  const canSend = !isRunning && text.length > 0;
368
369
 
@@ -424,11 +425,11 @@ For most use cases, this behavior is intuitive. In advanced scenarios where you
424
425
  ```tsx
425
426
  // ❌ Expensive computation in selector (runs on every store update)
426
427
  const result = useAuiState(
427
- ({ thread }) => thread.messages.filter((m) => m.role === "user").length,
428
+ (s) => s.thread.messages.filter((m) => m.role === "user").length,
428
429
  );
429
430
 
430
431
  // ✅ Memoize expensive computations
431
- const messages = useAuiState(({ thread }) => thread.messages);
432
+ const messages = useAuiState((s) => s.thread.messages);
432
433
  const userCount = useMemo(
433
434
  () => messages.filter((m) => m.role === "user").length,
434
435
  [messages],
@@ -439,20 +440,20 @@ const userCount = useMemo(
439
440
 
440
441
  ```tsx
441
442
  // ❌ Subscribes to entire thread state
442
- const thread = useAuiState(({ thread }) => thread);
443
+ const thread = useAuiState((s) => s.thread);
443
444
 
444
445
  // ✅ Subscribe only to needed values
445
- const isRunning = useAuiState(({ thread }) => thread.isRunning);
446
+ const isRunning = useAuiState((s) => s.thread.isRunning);
446
447
  ```
447
448
 
448
449
  ## API Reference
449
450
 
450
451
  ### Hooks
451
452
 
452
- | Hook | Purpose | Returns |
453
- | ----------------------------------- | -------------------------- | -------------- |
453
+ | Hook | Purpose | Returns |
454
+ | ----------------------------- | -------------------------- | -------------- |
454
455
  | `useAuiState(selector)` | Subscribe to state changes | Selected value |
455
- | `useAui()` | Get API instance | API object |
456
+ | `useAui()` | Get API instance | API object |
456
457
  | `useAuiEvent(event, handler)` | Subscribe to events | void |
457
458
 
458
459
  ### Scope States
@@ -469,28 +470,28 @@ const isRunning = useAuiState(({ thread }) => thread.isRunning);
469
470
 
470
471
  ### Available Actions by Scope
471
472
 
472
- | Scope | Actions | Use Cases |
473
- | -------------- | --------------------------------------------------------------------- | ----------------------------------------- |
474
- | ThreadList | `switchToNewThread()`, `switchToThread(id)`, `getState()` | Thread navigation and creation |
475
- | ThreadListItem | `switchTo()`, `rename(title)`, `archive()`, `unarchive()`, `delete()` | Thread management operations |
476
- | Thread | `append(message)`, `startRun()`, `cancelRun()`, `switchToNewThread()` | Message handling and conversation control |
477
- | Message | `reload()`, `speak()`, `stopSpeaking()`, `submitFeedback(feedback)` | Message interactions and regeneration |
478
- | Composer | `send()`, `setText(text)`, `addAttachment(file)`, `reset()` | Text input and message composition |
479
- | Part | `addResult(result)`, `getState()` | Tool call result handling |
480
- | Attachment | `remove()`, `getState()` | File management |
473
+ | Scope | Actions | Use Cases |
474
+ | -------------- | ------------------------------------------------------------------------- | ----------------------------------------- |
475
+ | ThreadList | `switchToNewThread()`, `switchToThread(id)`, `getState()` | Thread navigation and creation |
476
+ | ThreadListItem | `switchTo()`, `rename(title)`, `archive()`, `unarchive()`, `delete()` | Thread management operations |
477
+ | Thread | `append(message)`, `startRun()`, `cancelRun()`, `switchToNewThread()` | Message handling and conversation control |
478
+ | Message | `reload()`, `speak()`, `stopSpeaking()`, `submitFeedback(feedback)` | Message interactions and regeneration |
479
+ | Composer | `send()`, `setText(text)`, `addAttachment(file \| attachment)`, `reset()` | Text input and message composition |
480
+ | Part | `addResult(result)`, `getState()` | Tool call result handling |
481
+ | Attachment | `remove()`, `getState()` | File management |
481
482
 
482
483
  ### Common Events
483
484
 
484
- | Event | Description |
485
- | -------------------------------- | ----------------------------- |
486
- | `thread.runStart` | Assistant starts generating |
487
- | `thread.runEnd` | Assistant finishes generating |
488
- | `thread.initialize` | Thread is initialized |
489
- | `thread.modelContextUpdate` | Model context is updated |
490
- | `composer.send` | Message is sent |
491
- | `composer.attachmentAdd` | Attachment added to composer |
492
- | `threadListItem.switchedTo` | Switched to a thread |
493
- | `threadListItem.switchedAway` | Switched away from a thread |
485
+ | Event | Description |
486
+ | ----------------------------- | ----------------------------- |
487
+ | `thread.runStart` | Assistant starts generating |
488
+ | `thread.runEnd` | Assistant finishes generating |
489
+ | `thread.initialize` | Thread is initialized |
490
+ | `thread.modelContextUpdate` | Model context is updated |
491
+ | `composer.send` | Message is sent |
492
+ | `composer.attachmentAdd` | Attachment added to composer |
493
+ | `threadListItem.switchedTo` | Switched to a thread |
494
+ | `threadListItem.switchedAway` | Switched away from a thread |
494
495
 
495
496
  ## Troubleshooting
496
497
 
@@ -500,14 +501,14 @@ const isRunning = useAuiState(({ thread }) => thread.isRunning);
500
501
 
501
502
  ```tsx
502
503
  // ❌ This will throw if not inside a message component
503
- const role = useAuiState(({ message }) => message.role);
504
+ const role = useAuiState((s) => s.message.role);
504
505
 
505
506
  // ✅ Check scope availability first
506
507
  function SafeMessageButton() {
507
508
  const aui = useAui();
508
509
 
509
- const role = useAuiState(({ message }) =>
510
- api.message.source !== undefined ? message.role : "none",
510
+ const role = useAuiState((s) =>
511
+ api.message.source !== undefined ? s.message.role : "none",
511
512
  );
512
513
 
513
514
  return <div>Role: {role}</div>;
@@ -518,14 +519,14 @@ function SafeMessageButton() {
518
519
 
519
520
  ```tsx
520
521
  // ❌ Creating new objects in selectors causes infinite re-renders
521
- const data = useAuiState(({ message }) => ({
522
- role: message.role,
523
- content: message.content, // New object every time!
522
+ const data = useAuiState((s) => ({
523
+ role: s.message.role,
524
+ content: s.message.content, // New object every time!
524
525
  }));
525
526
 
526
527
  // ✅ Return primitive values or use separate selectors
527
- const role = useAuiState(({ message }) => message.role);
528
- const content = useAuiState(({ message }) => message.content);
528
+ const role = useAuiState((s) => s.message.role);
529
+ const content = useAuiState((s) => s.message.content);
529
530
  ```
530
531
 
531
532
  **"Scope resolution failed" / Stale scope references**
@@ -553,7 +554,7 @@ useEffect(() => {
553
554
 
554
555
  ```tsx
555
556
  // Read state
556
- const value = useAuiState(({ scope }) => scope.property);
557
+ const value = useAuiState((s) => s.scope.property);
557
558
 
558
559
  // Perform action
559
560
  const aui = useAui();
@@ -22,7 +22,6 @@ The `WebSpeechDictationAdapter` is supported in Chrome, Edge, and Safari. Check
22
22
  import { WebSpeechDictationAdapter } from "@assistant-ui/react";
23
23
 
24
24
  const runtime = useChatRuntime({
25
- api: "/api/chat",
26
25
  adapters: {
27
26
  dictation: new WebSpeechDictationAdapter({
28
27
  // Optional configuration
@@ -345,7 +344,6 @@ export class ElevenLabsScribeAdapter implements DictationAdapter {
345
344
 
346
345
  ```tsx
347
346
  const runtime = useChatRuntime({
348
- api: "/api/chat",
349
347
  adapters: {
350
348
  dictation: new ElevenLabsScribeAdapter({
351
349
  tokenEndpoint: "/api/scribe-token",
@@ -0,0 +1,169 @@
1
+ ---
2
+ title: Message Timing
3
+ description: Display stream timing metadata like duration, tokens per second, and time to first token.
4
+ ---
5
+
6
+ Display stream performance metrics — duration, tokens per second, TTFT — on assistant messages.
7
+
8
+ <Callout type="info">
9
+ The [`MessageTiming`](/docs/ui/message-timing) registry component provides a ready-made badge + popover UI. This guide covers the underlying `useMessageTiming()` hook for custom implementations and runtime-specific setup.
10
+ </Callout>
11
+
12
+ ## Reading Timing Data
13
+
14
+ Use `useMessageTiming()` inside a message component to access timing data:
15
+
16
+ ```tsx
17
+ import { useMessageTiming } from "@assistant-ui/react";
18
+
19
+ const MessageTimingDisplay: FC = () => {
20
+ const timing = useMessageTiming();
21
+ if (!timing?.totalStreamTime) return null;
22
+
23
+ const formatMs = (ms: number) =>
24
+ ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(2)}s`;
25
+
26
+ return (
27
+ <span className="text-xs text-muted-foreground">
28
+ {formatMs(timing.totalStreamTime)}
29
+ {timing.tokensPerSecond !== undefined &&
30
+ ` · ${timing.tokensPerSecond.toFixed(1)} tok/s`}
31
+ </span>
32
+ );
33
+ };
34
+ ```
35
+
36
+ Place it inside `MessagePrimitive.Root`, typically near the action bar:
37
+
38
+ ```tsx {8}
39
+ const AssistantMessage: FC = () => {
40
+ return (
41
+ <MessagePrimitive.Root>
42
+ <MessagePrimitive.Parts components={{ ... }} />
43
+ <ActionBarPrimitive.Root>
44
+ <ActionBarPrimitive.Copy />
45
+ <ActionBarPrimitive.Reload />
46
+ <MessageTimingDisplay />
47
+ </ActionBarPrimitive.Root>
48
+ </MessagePrimitive.Root>
49
+ );
50
+ };
51
+ ```
52
+
53
+ ### `useMessageTiming()` Return Fields
54
+
55
+ | Field | Type | Description |
56
+ |-------|------|-------------|
57
+ | `streamStartTime` | `number` | Unix timestamp when stream started |
58
+ | `firstTokenTime` | `number?` | Time to first text token (ms) |
59
+ | `totalStreamTime` | `number?` | Total stream duration (ms) |
60
+ | `tokenCount` | `number?` | Real or estimated output token count |
61
+ | `tokensPerSecond` | `number?` | Throughput (tokens/sec); see [accuracy note](#ai-sdk-usechatruntime) |
62
+ | `totalChunks` | `number` | Total stream chunks received |
63
+ | `toolCallCount` | `number` | Number of tool calls |
64
+
65
+ ## Runtime Support
66
+
67
+ | Runtime | Supported | Notes |
68
+ |---------|:-:|-------|
69
+ | DataStream | Yes | Automatic via `AssistantMessageAccumulator` |
70
+ | AI SDK (`useChatRuntime`) | Yes | Automatic via client-side tracking |
71
+ | Local (`useLocalRuntime`) | Yes | Pass timing in `ChatModelRunResult.metadata` |
72
+ | ExternalStore | Yes | Pass timing in `ThreadMessageLike.metadata` |
73
+ | LangGraph | No | Not yet implemented |
74
+ | AG-UI | No | Not yet implemented |
75
+
76
+ ### DataStream
77
+
78
+ Timing is tracked automatically inside `AssistantMessageAccumulator`. No setup required.
79
+
80
+ ```tsx
81
+ import { useDataStreamRuntime } from "@assistant-ui/react-data-stream";
82
+
83
+ const runtime = useDataStreamRuntime({ api: "/api/chat" });
84
+ // useMessageTiming() works out of the box
85
+ ```
86
+
87
+ ### AI SDK (`useChatRuntime`)
88
+
89
+ Timing is tracked automatically on the client side by observing streaming state transitions and content changes. Timing is finalized when each stream completes.
90
+
91
+ <Callout type="warn">
92
+ `tokenCount` and `tokensPerSecond` are **estimated** using a 4 characters per token approximation — real token counts from the server are not extracted. This can overcount significantly for short messages.
93
+ </Callout>
94
+
95
+ ```tsx
96
+ import { useChatRuntime } from "@assistant-ui/react-ai-sdk";
97
+
98
+ const runtime = useChatRuntime();
99
+ // useMessageTiming() works out of the box
100
+ ```
101
+
102
+ ### Local (`useLocalRuntime`)
103
+
104
+ Pass timing in the `metadata` field of your `ChatModelRunResult`:
105
+
106
+ ```tsx
107
+ import type { ChatModelAdapter } from "@assistant-ui/react";
108
+
109
+ const myAdapter: ChatModelAdapter = {
110
+ async run({ messages, abortSignal }) {
111
+ const startTime = Date.now();
112
+ const result = await callMyAPI(messages, abortSignal);
113
+ const totalStreamTime = Date.now() - startTime;
114
+
115
+ return {
116
+ content: [{ type: "text", text: result.text }],
117
+ metadata: {
118
+ timing: {
119
+ streamStartTime: startTime,
120
+ totalStreamTime,
121
+ tokenCount: result.usage?.completionTokens,
122
+ tokensPerSecond:
123
+ result.usage?.completionTokens
124
+ ? result.usage.completionTokens / (totalStreamTime / 1000)
125
+ : undefined,
126
+ totalChunks: 1,
127
+ toolCallCount: 0,
128
+ },
129
+ },
130
+ };
131
+ },
132
+ };
133
+ ```
134
+
135
+ ### ExternalStore (`useExternalStoreRuntime`)
136
+
137
+ Pass timing in the `metadata.timing` field of your `ThreadMessageLike` messages:
138
+
139
+ ```tsx
140
+ import type { ThreadMessageLike } from "@assistant-ui/react";
141
+
142
+ const message: ThreadMessageLike = {
143
+ role: "assistant",
144
+ content: [{ type: "text", text: fullText }],
145
+ metadata: {
146
+ timing: {
147
+ streamStartTime: startTime,
148
+ firstTokenTime,
149
+ totalStreamTime,
150
+ tokenCount,
151
+ tokensPerSecond,
152
+ totalChunks: chunks,
153
+ toolCallCount: 0,
154
+ },
155
+ },
156
+ };
157
+ ```
158
+
159
+ ## API Reference
160
+
161
+ ### `useMessageTiming()`
162
+
163
+ ```tsx
164
+ const timing: MessageTiming | undefined = useMessageTiming();
165
+ ```
166
+
167
+ Returns timing metadata for the current assistant message, or `undefined` for non-assistant messages or when no timing data is available.
168
+
169
+ Must be used inside a `MessagePrimitive.Root` context.