@assistant-ui/react 0.15.24 → 0.15.26

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 (204) hide show
  1. package/dist/hooks/useToolCallElapsed.d.ts +3 -1
  2. package/dist/hooks/useToolCallElapsed.d.ts.map +1 -1
  3. package/dist/hooks/useToolCallElapsed.js +8 -8
  4. package/dist/hooks/useToolCallElapsed.js.map +1 -1
  5. package/dist/index.d.ts +39 -7
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -3
  8. package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  9. package/dist/mcp-apps/McpAppRenderer.js +3 -2
  10. package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
  11. package/dist/mcp-apps/app-frame.d.ts.map +1 -1
  12. package/dist/mcp-apps/app-frame.js +56 -46
  13. package/dist/mcp-apps/app-frame.js.map +1 -1
  14. package/dist/mcp-apps/csp.d.ts +4 -0
  15. package/dist/mcp-apps/csp.d.ts.map +1 -0
  16. package/dist/mcp-apps/csp.js +93 -0
  17. package/dist/mcp-apps/csp.js.map +1 -0
  18. package/dist/mcp-apps/types.d.ts +1 -0
  19. package/dist/mcp-apps/types.d.ts.map +1 -1
  20. package/dist/mcp-apps/types.js.map +1 -1
  21. package/dist/model-context/makeAssistantVisible.d.ts +1 -0
  22. package/dist/model-context/makeAssistantVisible.d.ts.map +1 -1
  23. package/dist/model-context/makeAssistantVisible.js +31 -12
  24. package/dist/model-context/makeAssistantVisible.js.map +1 -1
  25. package/dist/primitives/assistantModal/AssistantModalRoot.d.ts +1 -0
  26. package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
  27. package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
  28. package/dist/primitives/attachment.d.ts +3 -1
  29. package/dist/primitives/attachment.d.ts.map +1 -1
  30. package/dist/primitives/attachment.js.map +1 -1
  31. package/dist/primitives/composer/ComposerCancel.d.ts +5 -1
  32. package/dist/primitives/composer/ComposerCancel.d.ts.map +1 -1
  33. package/dist/primitives/composer/ComposerCancel.js +14 -1
  34. package/dist/primitives/composer/ComposerCancel.js.map +1 -1
  35. package/dist/primitives/composer/ComposerInput.d.ts +15 -1
  36. package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
  37. package/dist/primitives/composer/ComposerInput.js +5 -4
  38. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  39. package/dist/primitives/composer/ComposerRoot.d.ts.map +1 -1
  40. package/dist/primitives/composer/ComposerRoot.js +24 -19
  41. package/dist/primitives/composer/ComposerRoot.js.map +1 -1
  42. package/dist/primitives/composer/composerInputSelector.d.ts +3 -0
  43. package/dist/primitives/composer/composerInputSelector.d.ts.map +1 -0
  44. package/dist/primitives/composer/composerInputSelector.js +7 -0
  45. package/dist/primitives/composer/composerInputSelector.js.map +1 -0
  46. package/dist/primitives/composer/trigger/TriggerPopover.d.ts +11 -11
  47. package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
  48. package/dist/primitives/composer/trigger/TriggerPopover.js +62 -60
  49. package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
  50. package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts +8 -8
  51. package/dist/primitives/composer/trigger/TriggerPopoverAction.d.ts.map +1 -1
  52. package/dist/primitives/composer/trigger/TriggerPopoverAction.js +6 -6
  53. package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
  54. package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts +3 -3
  55. package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts.map +1 -1
  56. package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
  57. package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts +9 -9
  58. package/dist/primitives/composer/trigger/TriggerPopoverDirective.d.ts.map +1 -1
  59. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +7 -7
  60. package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
  61. package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts +5 -5
  62. package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts.map +1 -1
  63. package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
  64. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.d.ts +8 -8
  65. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js +8 -8
  66. package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
  67. package/dist/primitives/composer/trigger/index.d.ts +1 -1
  68. package/dist/primitives/composer/useComposerCancelWithFocus.d.ts +2 -0
  69. package/dist/primitives/composer/useComposerCancelWithFocus.d.ts.map +1 -0
  70. package/dist/primitives/composer/useComposerCancelWithFocus.js +42 -0
  71. package/dist/primitives/composer/useComposerCancelWithFocus.js.map +1 -0
  72. package/dist/primitives/composer.d.ts +55 -15
  73. package/dist/primitives/composer.d.ts.map +1 -1
  74. package/dist/primitives/composer.js +8 -1
  75. package/dist/primitives/composer.js.map +1 -1
  76. package/dist/primitives/message/MessagePartsGrouped.d.ts +8 -1
  77. package/dist/primitives/message/MessagePartsGrouped.d.ts.map +1 -1
  78. package/dist/primitives/message/MessagePartsGrouped.js +58 -278
  79. package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
  80. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.d.ts.map +1 -1
  81. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js +5 -2
  82. package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
  83. package/dist/primitives/thread/ThreadLoadEarlier.d.ts +19 -0
  84. package/dist/primitives/thread/ThreadLoadEarlier.d.ts.map +1 -0
  85. package/dist/primitives/thread/ThreadLoadEarlier.js +36 -0
  86. package/dist/primitives/thread/ThreadLoadEarlier.js.map +1 -0
  87. package/dist/primitives/thread/ThreadMessages.d.ts +1 -1
  88. package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
  89. package/dist/primitives/thread/ThreadMessages.js +2 -2
  90. package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
  91. package/dist/primitives/thread/ThreadViewport.js +4 -3
  92. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  93. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js +2 -2
  94. package/dist/primitives/thread/topAnchor/useTopAnchorReserve.js.map +1 -1
  95. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  96. package/dist/primitives/thread/useThreadViewportAutoScroll.js +195 -17
  97. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  98. package/dist/primitives/thread.d.ts +2 -0
  99. package/dist/primitives/thread.d.ts.map +1 -1
  100. package/dist/primitives/thread.js +5 -2
  101. package/dist/primitives/thread.js.map +1 -1
  102. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  103. package/dist/sandbox-host/SandboxHost.js +9 -3
  104. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  105. package/dist/unstable/useComposerInput.d.ts +7 -4
  106. package/dist/unstable/useComposerInput.d.ts.map +1 -1
  107. package/dist/unstable/useComposerInput.js +4 -4
  108. package/dist/unstable/useComposerInput.js.map +1 -1
  109. package/dist/unstable/useComposerInputHistory.d.ts +3 -2
  110. package/dist/unstable/useComposerInputHistory.d.ts.map +1 -1
  111. package/dist/unstable/useComposerInputHistory.js +2 -2
  112. package/dist/unstable/useComposerInputHistory.js.map +1 -1
  113. package/dist/unstable/useLiveCompletionAdapter.d.ts +3 -2
  114. package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -1
  115. package/dist/unstable/useLiveCompletionAdapter.js +8 -7
  116. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  117. package/dist/unstable/useMentionAdapter.d.ts +12 -3
  118. package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
  119. package/dist/unstable/useMentionAdapter.js +2 -2
  120. package/dist/unstable/useMentionAdapter.js.map +1 -1
  121. package/dist/unstable/useMessageStallDetection.d.ts +4 -2
  122. package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
  123. package/dist/unstable/useMessageStallDetection.js +12 -8
  124. package/dist/unstable/useMessageStallDetection.js.map +1 -1
  125. package/dist/unstable/useSlashCommandAdapter.d.ts +5 -2
  126. package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
  127. package/dist/unstable/useSlashCommandAdapter.js +5 -4
  128. package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
  129. package/dist/unstable/webmcp/useWebMcpProvider.d.ts +4 -0
  130. package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -1
  131. package/dist/unstable/webmcp/useWebMcpProvider.js +2 -0
  132. package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -1
  133. package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
  134. package/dist/utils/getSelectionMessageId.js +31 -8
  135. package/dist/utils/getSelectionMessageId.js.map +1 -1
  136. package/dist/utils/hooks/useOnResizeContent.d.ts.map +1 -1
  137. package/dist/utils/hooks/useOnResizeContent.js +2 -22
  138. package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
  139. package/dist/utils/smooth/useSmooth.js +6 -6
  140. package/dist/utils/smooth/useSmooth.js.map +1 -1
  141. package/dist/utils/useIsomorphicLayoutEffect.d.ts +3 -0
  142. package/dist/utils/useIsomorphicLayoutEffect.d.ts.map +1 -0
  143. package/dist/utils/useIsomorphicLayoutEffect.js +6 -0
  144. package/dist/utils/useIsomorphicLayoutEffect.js.map +1 -0
  145. package/package.json +12 -11
  146. package/src/hooks/useToolCallElapsed.test.tsx +37 -0
  147. package/src/hooks/useToolCallElapsed.ts +10 -7
  148. package/src/index.ts +47 -5
  149. package/src/mcp-apps/McpAppRenderer.tsx +3 -9
  150. package/src/mcp-apps/app-frame.test.tsx +38 -0
  151. package/src/mcp-apps/app-frame.tsx +5 -8
  152. package/src/mcp-apps/csp.test.ts +112 -0
  153. package/src/mcp-apps/csp.ts +124 -0
  154. package/src/mcp-apps/types.ts +1 -0
  155. package/src/model-context/frame/AssistantFrame.test.ts +45 -0
  156. package/src/model-context/frame/SPEC_AssistantFrame.md +1 -0
  157. package/src/model-context/makeAssistantVisible.test.tsx +315 -5
  158. package/src/model-context/makeAssistantVisible.tsx +43 -7
  159. package/src/primitives/assistantModal/AssistantModalRoot.tsx +1 -0
  160. package/src/primitives/attachment.ts +4 -1
  161. package/src/primitives/composer/ComposerCancel.focus.test.tsx +307 -0
  162. package/src/primitives/composer/ComposerCancel.ts +8 -1
  163. package/src/primitives/composer/ComposerInput.tsx +20 -2
  164. package/src/primitives/composer/ComposerRoot.tsx +6 -5
  165. package/src/primitives/composer/composerInputSelector.ts +4 -0
  166. package/src/primitives/composer/trigger/TriggerPopover.tsx +11 -11
  167. package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +10 -13
  168. package/src/primitives/composer/trigger/TriggerPopoverCategories.tsx +2 -2
  169. package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +11 -14
  170. package/src/primitives/composer/trigger/TriggerPopoverExports.test.ts +61 -0
  171. package/src/primitives/composer/trigger/TriggerPopoverItems.tsx +3 -3
  172. package/src/primitives/composer/trigger/TriggerPopoverRootContext.tsx +8 -8
  173. package/src/primitives/composer/useComposerCancelWithFocus.ts +57 -0
  174. package/src/primitives/composer.ts +70 -15
  175. package/src/primitives/message/MessagePartsGrouped.test.tsx +106 -2
  176. package/src/primitives/message/MessagePartsGrouped.tsx +36 -134
  177. package/src/primitives/message/MessageRoot.test.tsx +125 -2
  178. package/src/primitives/selectionToolbar/SelectionToolbarRoot.test.tsx +20 -12
  179. package/src/primitives/selectionToolbar/SelectionToolbarRoot.tsx +6 -2
  180. package/src/primitives/thread/ThreadLoadEarlier.tsx +35 -0
  181. package/src/primitives/thread/ThreadMessages.tsx +1 -0
  182. package/src/primitives/thread/ThreadViewport.tsx +3 -3
  183. package/src/primitives/thread/topAnchor/useTopAnchorReserve.ts +2 -2
  184. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +922 -68
  185. package/src/primitives/thread/useThreadViewportAutoScroll.ts +330 -56
  186. package/src/primitives/thread.ts +2 -0
  187. package/src/sandbox-host/SandboxHost.test.tsx +34 -0
  188. package/src/sandbox-host/SandboxHost.tsx +11 -12
  189. package/src/tests/toolCallTiming.test.tsx +19 -2
  190. package/src/unstable/useComposerInput.ts +7 -4
  191. package/src/unstable/useComposerInputHistory.ts +3 -2
  192. package/src/unstable/useLiveCompletionAdapter.ts +9 -14
  193. package/src/unstable/useMentionAdapter.ts +12 -3
  194. package/src/unstable/useMessageStallDetection.test.tsx +40 -0
  195. package/src/unstable/useMessageStallDetection.ts +14 -8
  196. package/src/unstable/useSlashCommandAdapter.ts +8 -4
  197. package/src/unstable/webmcp/useWebMcpProvider.ts +4 -0
  198. package/src/utils/getSelectionMessageId.test.ts +231 -0
  199. package/src/utils/getSelectionMessageId.ts +45 -7
  200. package/src/utils/hooks/useOnResizeContent.ts +2 -29
  201. package/src/utils/smooth/useSmooth.test.tsx +16 -5
  202. package/src/utils/smooth/useSmooth.ts +7 -7
  203. package/src/utils/useIsomorphicLayoutEffect.test.tsx +21 -0
  204. package/src/utils/useIsomorphicLayoutEffect.ts +9 -0
@@ -12,6 +12,7 @@ import {
12
12
  useTriggerPopoverAriaProps,
13
13
  } from "../primitives/composer/useComposerInputState";
14
14
 
15
+ /** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */
15
16
  export type Unstable_UseComposerInputOptions = {
16
17
  /**
17
18
  * Disables the input in addition to the composer's own disabled sources
@@ -21,6 +22,7 @@ export type Unstable_UseComposerInputOptions = {
21
22
  disabled?: boolean | undefined;
22
23
  };
23
24
 
25
+ /** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */
24
26
  export type Unstable_ComposerInput = {
25
27
  /** Current composer text, or `""` when the composer is not editing. */
26
28
  value: string;
@@ -49,8 +51,6 @@ export type Unstable_ComposerInput = {
49
51
  };
50
52
 
51
53
  /**
52
- * @deprecated Under active development and might change without notice.
53
- *
54
54
  * Headless bridge to the composer's text value and send action, for building a
55
55
  * custom composer input without `ComposerPrimitive.Input`. It is a thin bridge,
56
56
  * not a second input: it does not own keyboard behavior, autosize, IME or
@@ -58,6 +58,8 @@ export type Unstable_ComposerInput = {
58
58
  * state. Spread `unstable_useTriggerPopoverAriaProps()` onto your element for
59
59
  * trigger-popover combobox semantics.
60
60
  *
61
+ * @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release.
62
+ *
61
63
  * @example
62
64
  * ```tsx
63
65
  * const { value, setText, send, isDisabled, canSend } = unstable_useComposerInput();
@@ -104,17 +106,18 @@ export function unstable_useComposerInput(
104
106
  return { value, setText, send, isDisabled, canSend };
105
107
  }
106
108
 
109
+ /** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */
107
110
  export type Unstable_TriggerPopoverAriaProps = TriggerPopoverAriaProps;
108
111
 
109
112
  /**
110
- * @deprecated Under active development and might change without notice.
111
- *
112
113
  * ARIA combobox attributes for the focused element (typically the composer
113
114
  * input) describing the open trigger popover, per the WAI-ARIA editable
114
115
  * combobox pattern. Returns an empty object outside a `TriggerPopoverRoot` or
115
116
  * when no popover is open. Spread these last so they take precedence over any
116
117
  * matching ARIA props you set yourself, mirroring `ComposerPrimitive.Input`.
117
118
  *
119
+ * @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release.
120
+ *
118
121
  * @example
119
122
  * ```tsx
120
123
  * const aria = unstable_useTriggerPopoverAriaProps();
@@ -14,6 +14,7 @@ import type { ThreadMessage } from "@assistant-ui/core";
14
14
  import { getThreadMessageText } from "@assistant-ui/core/internal";
15
15
  import { useTriggerPopoverRootContextOptional } from "../primitives/composer/trigger/TriggerPopoverRootContext";
16
16
 
17
+ /** @deprecated Experimental since 2026-06-11. Not scheduled for removal; the API may change in any release. */
17
18
  export type Unstable_ComposerInputHistory = {
18
19
  /** Keydown handler to spread onto `ComposerPrimitive.Input`. */
19
20
  onKeyDown: KeyboardEventHandler<HTMLTextAreaElement>;
@@ -45,8 +46,6 @@ const isOnLastLine = (value: string, caret: number): boolean =>
45
46
  !value.slice(caret).includes("\n");
46
47
 
47
48
  /**
48
- * @deprecated Under active development and might change without notice.
49
- *
50
49
  * Terminal-style input history for the thread composer: ArrowUp on an
51
50
  * empty draft recalls previously sent user messages (newest first),
52
51
  * ArrowDown steps back toward the newest and finally restores the draft
@@ -58,6 +57,8 @@ const isOnLastLine = (value: string, caret: number): boolean =>
58
57
  * to modifier keys, and to consumer handlers that already called
59
58
  * `preventDefault`. It is inert on edit composers.
60
59
  *
60
+ * @deprecated Experimental since 2026-06-11. Not scheduled for removal; the API may change in any release.
61
+ *
61
62
  * @example
62
63
  * ```tsx
63
64
  * const history = unstable_useComposerInputHistory();
@@ -1,18 +1,13 @@
1
1
  "use client";
2
2
 
3
- import {
4
- useCallback,
5
- useEffect,
6
- useLayoutEffect,
7
- useMemo,
8
- useRef,
9
- useState,
10
- } from "react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { useIsomorphicLayoutEffect } from "../utils/useIsomorphicLayoutEffect";
11
5
  import type {
12
6
  Unstable_TriggerAdapter,
13
7
  Unstable_TriggerItem,
14
8
  } from "@assistant-ui/core";
15
9
 
10
+ /** @deprecated Experimental since 2026-06-15. Not scheduled for removal; the API may change in any release. */
16
11
  export type Unstable_UseLiveCompletionAdapterOptions = {
17
12
  /**
18
13
  * Fetches the items for a query from an async source. Called debounced; the
@@ -35,8 +30,6 @@ export type Unstable_UseLiveCompletionAdapterOptions = {
35
30
  const NO_QUERY = "\u0000";
36
31
 
37
32
  /**
38
- * @deprecated Under active development and may change without notice.
39
- *
40
33
  * Bridges an async completion source (a server search, a gateway RPC) into the
41
34
  * synchronous `Unstable_TriggerAdapter` that `ComposerTriggerPopover` consumes.
42
35
  * `search(query)` returns the last fetched items synchronously and schedules a
@@ -47,6 +40,8 @@ const NO_QUERY = "\u0000";
47
40
  * `isLoading` is `true` while a fetch is in flight. Pass it to the popover's
48
41
  * `isLoading` prop to render a loading state.
49
42
  *
43
+ * @deprecated Experimental since 2026-06-15. Not scheduled for removal; the API may change in any release.
44
+ *
50
45
  * @example
51
46
  * ```tsx
52
47
  * const mentions = unstable_useLiveCompletionAdapter({
@@ -75,7 +70,7 @@ export function unstable_useLiveCompletionAdapter(
75
70
 
76
71
  const fetcherRef = useRef(fetcher);
77
72
  // The debounce timer must only observe fetchers from committed renders.
78
- useLayoutEffect(() => {
73
+ useIsomorphicLayoutEffect(() => {
79
74
  fetcherRef.current = fetcher;
80
75
  }, [fetcher]);
81
76
 
@@ -143,7 +138,7 @@ export function unstable_useLiveCompletionAdapter(
143
138
  );
144
139
 
145
140
  const scheduleFetchRef = useRef(scheduleFetch);
146
- useLayoutEffect(() => {
141
+ useIsomorphicLayoutEffect(() => {
147
142
  scheduleFetchRef.current = scheduleFetch;
148
143
  }, [scheduleFetch]);
149
144
 
@@ -156,7 +151,7 @@ export function unstable_useLiveCompletionAdapter(
156
151
  }, [cancelTimer, rearmPendingRetry]);
157
152
 
158
153
  const cacheKeyRef = useRef(cacheKey);
159
- useLayoutEffect(() => {
154
+ useIsomorphicLayoutEffect(() => {
160
155
  if (cacheKeyRef.current === cacheKey) return;
161
156
  cacheKeyRef.current = cacheKey;
162
157
  invalidatePending();
@@ -175,7 +170,7 @@ export function unstable_useLiveCompletionAdapter(
175
170
 
176
171
  // Render-time searches can outlive an abandoned render, so they only arm
177
172
  // request work after this hook commits.
178
- useLayoutEffect(() => {
173
+ useIsomorphicLayoutEffect(() => {
179
174
  inactiveRef.current = false;
180
175
  const deferredQuery = deferredQueryRef.current;
181
176
  deferredQueryRef.current = null;
@@ -17,9 +17,14 @@ import {
17
17
  type ModelContextSnapshotSource,
18
18
  } from "./useModelContextSnapshot";
19
19
 
20
- /** Icon component shape consumed by `ComposerTriggerPopover`'s `iconMap`. */
20
+ /**
21
+ * Icon component shape consumed by `ComposerTriggerPopover`'s `iconMap`.
22
+ *
23
+ * @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release.
24
+ */
21
25
  export type Unstable_IconComponent = FC<{ className?: string }>;
22
26
 
27
+ /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
23
28
  export type Unstable_Mention = {
24
29
  readonly id: string;
25
30
  readonly type: string;
@@ -30,12 +35,14 @@ export type Unstable_Mention = {
30
35
  readonly metadata?: ReadonlyJSONObject | undefined;
31
36
  };
32
37
 
38
+ /** @deprecated Experimental since 2026-03-16. Not scheduled for removal; the API may change in any release. */
33
39
  export type Unstable_MentionCategory = {
34
40
  readonly id: string;
35
41
  readonly label: string;
36
42
  readonly items: readonly Unstable_Mention[];
37
43
  };
38
44
 
45
+ /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
39
46
  export type Unstable_ModelContextToolsOptions = {
40
47
  /**
41
48
  * Wrap tools in a dedicated category. Selects drill-down mode on its own
@@ -48,6 +55,7 @@ export type Unstable_ModelContextToolsOptions = {
48
55
  readonly icon?: string;
49
56
  };
50
57
 
58
+ /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
51
59
  export type Unstable_UseMentionAdapterOptions = {
52
60
  /**
53
61
  * Flat mention list. Ignored when `categories` is set, and keeps the
@@ -78,6 +86,7 @@ export type Unstable_UseMentionAdapterOptions = {
78
86
  readonly fallbackIcon?: Unstable_IconComponent;
79
87
  };
80
88
 
89
+ /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
81
90
  export type Unstable_MentionDirective = {
82
91
  readonly formatter: Unstable_DirectiveFormatter;
83
92
  readonly onInserted?: ((item: Unstable_TriggerItem) => void) | undefined;
@@ -120,12 +129,12 @@ const toolMentionSource: ModelContextSnapshotSource<
120
129
  };
121
130
 
122
131
  /**
123
- * @deprecated Under active development and might change without notice.
124
- *
125
132
  * Creates a spreadable `{ adapter, directive }` bundle for `@` mentions.
126
133
  * Supports tools registered in model context, explicit items, or both —
127
134
  * flat or categorized.
128
135
  *
136
+ * @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release.
137
+ *
129
138
  * @example
130
139
  * ```tsx
131
140
  * const mention = unstable_useMentionAdapter();
@@ -0,0 +1,40 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { renderToString } from "react-dom/server";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+
6
+ const { message } = vi.hoisted(() => ({
7
+ message: {
8
+ status: { type: "running" as const },
9
+ content: [{ type: "text" as const, text: "streaming" }],
10
+ },
11
+ }));
12
+
13
+ vi.mock("@assistant-ui/store", async (importOriginal) => ({
14
+ ...(await importOriginal<typeof import("@assistant-ui/store")>()),
15
+ useAuiState: (selector: (state: { message: typeof message }) => unknown) =>
16
+ selector({ message }),
17
+ }));
18
+
19
+ vi.mock("@assistant-ui/store/internal", async (importOriginal) => ({
20
+ ...(await importOriginal<typeof import("@assistant-ui/store/internal")>()),
21
+ useShallowSelector: <T,>(selector: T) => selector,
22
+ }));
23
+
24
+ import { unstable_useMessageStallDetection } from "./useMessageStallDetection";
25
+
26
+ describe("unstable_useMessageStallDetection", () => {
27
+ afterEach(() => {
28
+ vi.restoreAllMocks();
29
+ });
30
+
31
+ it("does not read the wall clock during server render", () => {
32
+ const nowSpy = vi.spyOn(Date, "now");
33
+ const Probe = () => (
34
+ <span>{String(unstable_useMessageStallDetection().stalled)}</span>
35
+ );
36
+
37
+ expect(renderToString(<Probe />)).toBe("<span>false</span>");
38
+ expect(nowSpy).not.toHaveBeenCalled();
39
+ });
40
+ });
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react";
4
4
  import { useAuiState } from "@assistant-ui/store";
5
5
  import { useShallowSelector } from "@assistant-ui/store/internal";
6
6
 
7
+ /** @deprecated Experimental since 2026-06-13. Not scheduled for removal; the API may change in any release. */
7
8
  export type Unstable_MessageStallDetectionOptions = {
8
9
  /**
9
10
  * Milliseconds of unchanged message content before the message counts as
@@ -13,6 +14,7 @@ export type Unstable_MessageStallDetectionOptions = {
13
14
  thresholdMs?: number | undefined;
14
15
  };
15
16
 
17
+ /** @deprecated Experimental since 2026-06-13. Not scheduled for removal; the API may change in any release. */
16
18
  export type Unstable_MessageStallDetection = {
17
19
  /** True while the message is running and its content has not changed for at least `thresholdMs`. */
18
20
  stalled: boolean;
@@ -21,8 +23,6 @@ export type Unstable_MessageStallDetection = {
21
23
  };
22
24
 
23
25
  /**
24
- * @deprecated Under active development and might change without notice.
25
- *
26
26
  * Detects mid-run output stalls on the current message: while the message is
27
27
  * running, watches its text, reasoning, and tool-argument values plus tool-result
28
28
  * availability and reports a stall once they stop changing for `thresholdMs`.
@@ -30,6 +30,8 @@ export type Unstable_MessageStallDetection = {
30
30
  * provider stalls, after the first tokens have already streamed.
31
31
  *
32
32
  * Must be used inside a message scope.
33
+ *
34
+ * @deprecated Experimental since 2026-06-13. Not scheduled for removal; the API may change in any release.
33
35
  */
34
36
  export function unstable_useMessageStallDetection(
35
37
  options?: Unstable_MessageStallDetectionOptions,
@@ -57,9 +59,9 @@ export function unstable_useMessageStallDetection(
57
59
  );
58
60
 
59
61
  const running = activity[0] === true;
60
- const lastActivityRef = useRef(Date.now());
62
+ const lastActivityRef = useRef(0);
61
63
  const [stalled, setStalled] = useState(false);
62
- const [, setTick] = useState(0);
64
+ const [now, setNow] = useState(0);
63
65
 
64
66
  useEffect(() => {
65
67
  if (!running) return undefined;
@@ -73,26 +75,30 @@ export function unstable_useMessageStallDetection(
73
75
  return undefined;
74
76
  }
75
77
 
78
+ const stall = () => {
79
+ setNow(Date.now());
80
+ setStalled(true);
81
+ };
76
82
  const sinceActivity = Date.now() - lastActivityRef.current;
77
83
  if (sinceActivity >= thresholdMs) {
78
- setStalled(true);
84
+ stall();
79
85
  return undefined;
80
86
  }
81
87
 
82
88
  setStalled(false);
83
- const id = setTimeout(() => setStalled(true), thresholdMs - sinceActivity);
89
+ const id = setTimeout(stall, thresholdMs - sinceActivity);
84
90
  return () => clearTimeout(id);
85
91
  }, [running, activity, thresholdMs]);
86
92
 
87
93
  useEffect(() => {
88
94
  if (!stalled) return undefined;
89
- const id = setInterval(() => setTick((t) => t + 1), 1000);
95
+ const id = setInterval(() => setNow(Date.now()), 1000);
90
96
  return () => clearInterval(id);
91
97
  }, [stalled]);
92
98
 
93
99
  if (!stalled) return { stalled: false, stalledForMs: 0 };
94
100
  return {
95
101
  stalled: true,
96
- stalledForMs: Math.max(0, Date.now() - lastActivityRef.current),
102
+ stalledForMs: Math.max(0, now - lastActivityRef.current),
97
103
  };
98
104
  }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { useLayoutEffect, useMemo, useRef } from "react";
3
+ import { useMemo, useRef } from "react";
4
+ import { useIsomorphicLayoutEffect } from "../utils/useIsomorphicLayoutEffect";
4
5
  import type {
5
6
  Unstable_TriggerAdapter,
6
7
  Unstable_TriggerItem,
@@ -8,6 +9,7 @@ import type {
8
9
  import type { Unstable_IconComponent } from "./useMentionAdapter";
9
10
  import { matchesTriggerItemQuery } from "../primitives/composer/trigger/matchesTriggerItemQuery";
10
11
 
12
+ /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
11
13
  export type Unstable_SlashCommand = {
12
14
  readonly id: string;
13
15
  readonly label?: string | undefined;
@@ -16,6 +18,7 @@ export type Unstable_SlashCommand = {
16
18
  readonly execute: () => void;
17
19
  };
18
20
 
21
+ /** @deprecated Experimental since 2026-04-06. Not scheduled for removal; the API may change in any release. */
19
22
  export type Unstable_UseSlashCommandAdapterOptions = {
20
23
  readonly commands: readonly Unstable_SlashCommand[];
21
24
  /** Strip the trigger text from the composer after executing. @default false */
@@ -26,19 +29,20 @@ export type Unstable_UseSlashCommandAdapterOptions = {
26
29
  readonly fallbackIcon?: Unstable_IconComponent;
27
30
  };
28
31
 
32
+ /** @deprecated Experimental since 2026-04-15. Not scheduled for removal; the API may change in any release. */
29
33
  export type Unstable_SlashCommandAction = {
30
34
  readonly onExecute: (item: Unstable_TriggerItem) => void;
31
35
  readonly removeOnExecute?: boolean | undefined;
32
36
  };
33
37
 
34
38
  /**
35
- * @deprecated Under active development and may change without notice.
36
- *
37
39
  * Bundles slash command definitions (with inline `execute` callbacks) into
38
40
  * `{adapter, action}` that plug directly into `ComposerTriggerPopover`.
39
41
  * `execute` stays in the hook closure and is never attached to the returned
40
42
  * `TriggerItem`, keeping items serializable.
41
43
  *
44
+ * @deprecated Experimental since 2026-04-06. Not scheduled for removal; the API may change in any release.
45
+ *
42
46
  * @example
43
47
  * ```tsx
44
48
  * const slash = unstable_useSlashCommandAdapter({
@@ -71,7 +75,7 @@ export function unstable_useSlashCommandAdapter(
71
75
  : nextItems;
72
76
 
73
77
  // The adapter's callbacks must only observe commands from committed renders.
74
- useLayoutEffect(() => {
78
+ useIsomorphicLayoutEffect(() => {
75
79
  commandsRef.current = commands;
76
80
  committedItemsRef.current = items;
77
81
  }, [commands, items]);
@@ -20,10 +20,12 @@ import {
20
20
  type ModelContextSnapshotSource,
21
21
  } from "../useModelContextSnapshot";
22
22
 
23
+ /** @deprecated Experimental since 2026-08-29. Not scheduled for removal; the API may change in any release. */
23
24
  export type Unstable_WebMcpProviderOptions = {
24
25
  filter?: (name: string, tool: Tool<any, any>) => boolean;
25
26
  };
26
27
 
28
+ /** @deprecated Experimental since 2026-08-29. Not scheduled for removal; the API may change in any release. */
27
29
  export type Unstable_WebMcpProviderResult = {
28
30
  status: "unsupported" | "active";
29
31
  registeredToolNames: readonly string[];
@@ -128,6 +130,8 @@ const useWebMcpRegistry = ({
128
130
  * Returns `status: "unsupported"` when the page exposes no
129
131
  * `document.modelContext` (or `navigator.modelContext`), and the sorted names
130
132
  * of the tools currently registered with the host.
133
+ *
134
+ * @deprecated Experimental since 2026-08-29. Not scheduled for removal; the API may change in any release.
131
135
  */
132
136
  export const unstable_useWebMcpProvider = (
133
137
  options: Unstable_WebMcpProviderOptions = {},
@@ -30,6 +30,237 @@ afterEach(() => {
30
30
  });
31
31
 
32
32
  describe("getSelectionMessageId", () => {
33
+ it.each([false, true])(
34
+ "accepts an end at the next message's first text offset zero (backward: %s)",
35
+ (backward) => {
36
+ document.body.innerHTML =
37
+ '<div data-message-id="message-1"><p id="first" data-aui-quote-selectable>first text</p></div><div data-message-id="message-2"><p data-aui-quote-selectable><span id="second">second text</span></p></div>';
38
+ const first = textNode("#first");
39
+ const second = textNode("#second");
40
+ const selection = selectText(first);
41
+ if (backward) {
42
+ selection.setBaseAndExtent(second, 0, first, 1);
43
+ } else {
44
+ selection.setBaseAndExtent(first, 1, second, 0);
45
+ }
46
+ const endpoints = [
47
+ selection.anchorNode,
48
+ selection.anchorOffset,
49
+ selection.focusNode,
50
+ selection.focusOffset,
51
+ ];
52
+ const text = selection.toString();
53
+
54
+ expect(getSelectionMessageId(selection)).toBe("message-1");
55
+ expect(selection.toString()).toBe(text);
56
+ expect([
57
+ selection.anchorNode,
58
+ selection.anchorOffset,
59
+ selection.focusNode,
60
+ selection.focusOffset,
61
+ ]).toEqual(endpoints);
62
+ },
63
+ );
64
+
65
+ it("accepts an end at the next message element's offset zero", () => {
66
+ document.body.innerHTML =
67
+ '<div data-message-id="message-1"><p id="first" data-aui-quote-selectable>first text</p></div><div id="second" data-message-id="message-2"><p>second text</p></div>';
68
+ const selection = selectText(textNode("#first"));
69
+ const second = document.querySelector("#second");
70
+ assert(second);
71
+ selection.getRangeAt(0).setEnd(second, 0);
72
+
73
+ expect(getSelectionMessageId(selection)).toBe("message-1");
74
+ });
75
+
76
+ it.each([false, true])(
77
+ "accepts a zero-offset end inside an excluded next message (backward: %s)",
78
+ (backward) => {
79
+ document.body.innerHTML =
80
+ '<div data-message-id="message-1"><p id="first" data-aui-quote-selectable>first text</p></div><div data-message-id="message-2" data-aui-quote-selectable="false"><p><span id="second">second text</span></p></div>';
81
+ const first = textNode("#first");
82
+ const second = textNode("#second");
83
+ const selection = selectText(first);
84
+ selection.setBaseAndExtent(
85
+ backward ? second : first,
86
+ 0,
87
+ backward ? first : second,
88
+ 0,
89
+ );
90
+ const endpoints = [
91
+ selection.anchorNode,
92
+ selection.anchorOffset,
93
+ selection.focusNode,
94
+ selection.focusOffset,
95
+ ];
96
+ const text = selection.toString();
97
+
98
+ expect(getSelectionMessageId(selection)).toBe("message-1");
99
+ expect(selection.toString()).toBe(text);
100
+ expect([
101
+ selection.anchorNode,
102
+ selection.anchorOffset,
103
+ selection.focusNode,
104
+ selection.focusOffset,
105
+ ]).toEqual(endpoints);
106
+ },
107
+ );
108
+
109
+ it.each(["<span></span>", "<span> </span>", "<span>excluded text</span>"])(
110
+ "rejects selected content inheriting the next message's exclusion: %s",
111
+ (prefix) => {
112
+ document.body.innerHTML = `<div data-message-id="message-1"><p id="first" data-aui-quote-selectable>first text</p></div><div data-message-id="message-2" data-aui-quote-selectable="false">${prefix}<p id="second">second text</p></div>`;
113
+ const first = textNode("#first");
114
+ const second = textNode("#second");
115
+ const selection = selectText(first);
116
+ for (const backward of [false, true]) {
117
+ selection.setBaseAndExtent(
118
+ backward ? second : first,
119
+ 0,
120
+ backward ? first : second,
121
+ 0,
122
+ );
123
+ expect(getSelectionMessageId(selection)).toBeNull();
124
+ }
125
+ },
126
+ );
127
+
128
+ it.each([
129
+ { footer: "", separator: "", prefix: "<span></span>" },
130
+ { footer: "", separator: "\n ", prefix: "" },
131
+ {
132
+ footer: '<div class="footer"><button><svg></svg></button></div>',
133
+ separator: "",
134
+ prefix: "",
135
+ },
136
+ {
137
+ footer: "<div><span></span></div>",
138
+ separator: "\n",
139
+ prefix: "<span></span>\n",
140
+ },
141
+ ])(
142
+ "skips empty chrome and whitespace at the end: %j",
143
+ ({ footer, separator, prefix }) => {
144
+ document.body.innerHTML = `<div data-message-id="message-1"><p id="first" data-aui-quote-selectable>first text</p>${footer}</div>${separator}<div data-message-id="message-2">${prefix}<p id="second">second text</p></div>`;
145
+ const first = textNode("#first");
146
+ const second = textNode("#second");
147
+ const selection = selectText(first);
148
+ for (const backward of [false, true]) {
149
+ selection.setBaseAndExtent(
150
+ backward ? second : first,
151
+ backward ? 0 : 1,
152
+ backward ? first : second,
153
+ backward ? 1 : 0,
154
+ );
155
+ const endpoints = [
156
+ selection.anchorNode,
157
+ selection.anchorOffset,
158
+ selection.focusNode,
159
+ selection.focusOffset,
160
+ ];
161
+ const text = selection.toString();
162
+ expect(getSelectionMessageId(selection)).toBe("message-1");
163
+ expect(selection.toString()).toBe(text);
164
+ expect([
165
+ selection.anchorNode,
166
+ selection.anchorOffset,
167
+ selection.focusNode,
168
+ selection.focusOffset,
169
+ ]).toEqual(endpoints);
170
+ }
171
+ },
172
+ );
173
+
174
+ it.each([
175
+ { footer: "<footer>footer text</footer>", prefix: "" },
176
+ { footer: "", prefix: "<header>next message header</header>" },
177
+ {
178
+ footer: '<div data-aui-quote-selectable="false"><span></span>\n</div>',
179
+ prefix: "",
180
+ },
181
+ {
182
+ footer: "",
183
+ prefix: '<div data-aui-quote-selectable="false"><span></span>\n</div>',
184
+ },
185
+ ])(
186
+ "does not trim selected content or exclusions: %j",
187
+ ({ footer, prefix }) => {
188
+ document.body.innerHTML = `<div data-message-id="message-1"><p id="first" data-aui-quote-selectable>first text</p>${footer}</div>\n<div data-message-id="message-2">${prefix}<p id="second">second text</p></div>`;
189
+ const selection = selectText(textNode("#first"));
190
+ selection.getRangeAt(0).setEnd(textNode("#second"), 0);
191
+ expect(getSelectionMessageId(selection)).toBeNull();
192
+ },
193
+ );
194
+
195
+ it("does not normalize past the selection start", () => {
196
+ document.body.innerHTML =
197
+ '<div data-message-id="message-1"><p id="first">first text</p><span></span></div><div data-message-id="message-2"><p id="second">second text</p></div>';
198
+ const first = textNode("#first");
199
+ const selection = selectText(first);
200
+ selection.setBaseAndExtent(first, first.length, textNode("#second"), 0);
201
+ expect(getSelectionMessageId(selection)).toBeNull();
202
+ });
203
+
204
+ it.each(["<span>\n </span>", "<span></span>"])(
205
+ "preserves inherited exclusions after a nested opt-in: %s",
206
+ (suffix) => {
207
+ document.body.innerHTML = `<div data-message-id="message-1"><div data-aui-quote-selectable="false"><p id="first" data-aui-quote-selectable>first text</p>${suffix}</div></div><div data-message-id="message-2"><p id="second">second text</p></div>`;
208
+ const first = textNode("#first");
209
+ const second = textNode("#second");
210
+ const selection = selectText(first);
211
+ expect(getSelectionMessageId(selection)).toBe("message-1");
212
+ for (const backward of [false, true]) {
213
+ selection.setBaseAndExtent(
214
+ backward ? second : first,
215
+ 0,
216
+ backward ? first : second,
217
+ 0,
218
+ );
219
+ expect(getSelectionMessageId(selection)).toBeNull();
220
+ }
221
+ },
222
+ );
223
+
224
+ it("rejects even one selected character in the next message", () => {
225
+ document.body.innerHTML =
226
+ '<div data-message-id="message-1"><p id="first">first text</p></div><div data-message-id="message-2"><p id="second">second text</p></div>';
227
+ const selection = selectText(textNode("#first"));
228
+ selection.getRangeAt(0).setEnd(textNode("#second"), 1);
229
+
230
+ expect(getSelectionMessageId(selection)).toBeNull();
231
+ });
232
+
233
+ it.each(["", '<span aria-hidden="true"></span>'])(
234
+ "rejects an empty excluded subtree before a zero-offset end with suffix %s",
235
+ (suffix) => {
236
+ document.body.innerHTML = `<div data-message-id="message-1" data-aui-quote-selectable><p id="first">first text</p><span data-aui-quote-selectable="false"></span>${suffix}</div><div data-message-id="message-2"><p id="second">second text</p></div>`;
237
+ const selection = selectText(textNode("#first"));
238
+ selection.getRangeAt(0).setEnd(textNode("#second"), 0);
239
+
240
+ expect(getSelectionMessageId(selection)).toBeNull();
241
+ },
242
+ );
243
+
244
+ it("rejects an excluded subtree preceding the next message's text", () => {
245
+ document.body.innerHTML =
246
+ '<div data-message-id="message-1"><p id="first">first text</p></div><div data-message-id="message-2"><span data-aui-quote-selectable="false"></span><p id="second">second text</p></div>';
247
+ const selection = selectText(textNode("#first"));
248
+ selection.getRangeAt(0).setEnd(textNode("#second"), 0);
249
+
250
+ expect(getSelectionMessageId(selection)).toBeNull();
251
+ });
252
+
253
+ it("still checks thread ownership after adjusting a zero-offset end", () => {
254
+ document.body.innerHTML =
255
+ '<div id="first-thread"><div data-message-id="message-1"><p id="first">first text</p></div><div data-message-id="message-2"><p id="second">second text</p></div></div><div id="second-thread"></div>';
256
+ const selection = selectText(textNode("#first"));
257
+ selection.getRangeAt(0).setEnd(textNode("#second"), 0);
258
+ const root = document.querySelector("#second-thread");
259
+ assert(root);
260
+
261
+ expect(getSelectionMessageId(selection, root)).toBeNull();
262
+ });
263
+
33
264
  it("rejects a message outside the supplied thread root", () => {
34
265
  document.body.innerHTML = `
35
266
  <div id="first-thread">