@assistant-ui/react 0.15.14 → 0.15.16

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 (210) hide show
  1. package/dist/assistant-transport.d.ts +32 -0
  2. package/dist/assistant-transport.d.ts.map +1 -0
  3. package/dist/assistant-transport.js +27 -0
  4. package/dist/assistant-transport.js.map +1 -0
  5. package/dist/context/providers/MessageProvider.js +1 -1
  6. package/dist/devtools/DevToolsHooks.d.ts +0 -2
  7. package/dist/devtools/DevToolsHooks.d.ts.map +1 -1
  8. package/dist/devtools/DevToolsHooks.js +10 -12
  9. package/dist/devtools/DevToolsHooks.js.map +1 -1
  10. package/dist/hooks/useMessageQuote.js +1 -1
  11. package/dist/hooks/useToolCallElapsed.js +1 -1
  12. package/dist/index.d.ts +4 -7
  13. package/dist/index.js +6 -8
  14. package/dist/legacy-runtime/AssistantRuntimeProvider.js +1 -1
  15. package/dist/mcp-apps/McpAppRenderer.js +1 -1
  16. package/dist/mcp-apps/McpAppsRemoteHost.d.ts.map +1 -1
  17. package/dist/mcp-apps/McpAppsRemoteHost.js +18 -2
  18. package/dist/mcp-apps/McpAppsRemoteHost.js.map +1 -1
  19. package/dist/mcp-apps/bridge.d.ts.map +1 -1
  20. package/dist/mcp-apps/bridge.js +33 -24
  21. package/dist/mcp-apps/bridge.js.map +1 -1
  22. package/dist/model-context/makeAssistantVisible.js +1 -1
  23. package/dist/primitives/actionBar/ActionBarCopy.js +1 -1
  24. package/dist/primitives/actionBar/ActionBarExportMarkdown.d.ts.map +1 -1
  25. package/dist/primitives/actionBar/ActionBarExportMarkdown.js +5 -2
  26. package/dist/primitives/actionBar/ActionBarExportMarkdown.js.map +1 -1
  27. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +1 -1
  28. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +1 -1
  29. package/dist/primitives/actionBar/ActionBarStopSpeaking.d.ts.map +1 -1
  30. package/dist/primitives/actionBar/ActionBarStopSpeaking.js +24 -37
  31. package/dist/primitives/actionBar/ActionBarStopSpeaking.js.map +1 -1
  32. package/dist/primitives/actionBar/useActionBarFloatStatus.js +1 -1
  33. package/dist/primitives/assistantModal/AssistantModalRoot.js +1 -1
  34. package/dist/primitives/attachment/AttachmentName.js +1 -1
  35. package/dist/primitives/attachment/AttachmentRemove.js +1 -1
  36. package/dist/primitives/attachment/AttachmentThumb.js +1 -1
  37. package/dist/primitives/branchPicker/BranchPickerCount.js +1 -1
  38. package/dist/primitives/branchPicker/BranchPickerNumber.js +1 -1
  39. package/dist/primitives/branchPicker/BranchPickerRoot.d.ts.map +1 -1
  40. package/dist/primitives/branchPicker/BranchPickerRoot.js +20 -15
  41. package/dist/primitives/branchPicker/BranchPickerRoot.js.map +1 -1
  42. package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +1 -1
  43. package/dist/primitives/composer/ComposerAddAttachment.js +1 -1
  44. package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -1
  45. package/dist/primitives/composer/ComposerDictationTranscript.d.ts +2 -2
  46. package/dist/primitives/composer/ComposerDictationTranscript.js +3 -3
  47. package/dist/primitives/composer/ComposerDictationTranscript.js.map +1 -1
  48. package/dist/primitives/composer/ComposerInput.js +2 -2
  49. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  50. package/dist/primitives/composer/ComposerQuote.js +1 -1
  51. package/dist/primitives/composer/ComposerRoot.js +1 -1
  52. package/dist/primitives/composer/ComposerStopDictation.js +1 -1
  53. package/dist/primitives/composer/trigger/TriggerPopover.js +1 -1
  54. package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
  55. package/dist/primitives/composer/trigger/triggerSelectionResource.js +24 -20
  56. package/dist/primitives/composer/trigger/triggerSelectionResource.js.map +1 -1
  57. package/dist/primitives/composer/useComposerInputState.js +1 -1
  58. package/dist/primitives/message/MessageIf.js +1 -1
  59. package/dist/primitives/message/MessagePartsGrouped.js +1 -1
  60. package/dist/primitives/message/MessageRoot.js +1 -1
  61. package/dist/primitives/message.js +1 -1
  62. package/dist/primitives/queueItem/QueueItemRemove.js +1 -1
  63. package/dist/primitives/queueItem/QueueItemSteer.js +1 -1
  64. package/dist/primitives/queueItem/QueueItemText.js +1 -1
  65. package/dist/primitives/selectionToolbar/SelectionToolbarQuote.js +1 -1
  66. package/dist/primitives/suggestion/SuggestionDescription.js +1 -1
  67. package/dist/primitives/suggestion/SuggestionTitle.js +1 -1
  68. package/dist/primitives/suggestion/SuggestionTrigger.js +1 -1
  69. package/dist/primitives/thread/ThreadIf.js +1 -1
  70. package/dist/primitives/thread/ThreadRoot.d.ts +3 -0
  71. package/dist/primitives/thread/ThreadRoot.d.ts.map +1 -1
  72. package/dist/primitives/thread/ThreadRoot.js +44 -9
  73. package/dist/primitives/thread/ThreadRoot.js.map +1 -1
  74. package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
  75. package/dist/primitives/thread/ThreadViewport.js +2 -13
  76. package/dist/primitives/thread/ThreadViewport.js.map +1 -1
  77. package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
  78. package/dist/primitives/thread/useThreadViewportAutoScroll.js +23 -8
  79. package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
  80. package/dist/primitives/threadList/ThreadListNew.js +1 -1
  81. package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
  82. package/dist/sandbox-host/SandboxHost.js +6 -1
  83. package/dist/sandbox-host/SandboxHost.js.map +1 -1
  84. package/dist/unstable/useComposerInput.d.ts +2 -2
  85. package/dist/unstable/useComposerInput.js +1 -1
  86. package/dist/unstable/useComposerInput.js.map +1 -1
  87. package/dist/unstable/useComposerInputHistory.js +2 -2
  88. package/dist/unstable/useComposerInputHistory.js.map +1 -1
  89. package/dist/unstable/useLiveCompletionAdapter.d.ts +5 -0
  90. package/dist/unstable/useLiveCompletionAdapter.d.ts.map +1 -1
  91. package/dist/unstable/useLiveCompletionAdapter.js +16 -3
  92. package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
  93. package/dist/unstable/useMentionAdapter.d.ts +1 -1
  94. package/dist/utils/invokeCallbackSafely.d.ts +5 -0
  95. package/dist/utils/invokeCallbackSafely.d.ts.map +1 -0
  96. package/dist/utils/invokeCallbackSafely.js +15 -0
  97. package/dist/utils/invokeCallbackSafely.js.map +1 -0
  98. package/dist/utils/smooth/SmoothContext.d.ts +6 -6
  99. package/dist/utils/smooth/SmoothContext.js +1 -1
  100. package/dist/utils/smooth/useSmooth.d.ts +3 -0
  101. package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
  102. package/dist/utils/smooth/useSmooth.js +16 -1
  103. package/dist/utils/smooth/useSmooth.js.map +1 -1
  104. package/dist/utils/useToolArgsFieldStatus.js +1 -1
  105. package/package.json +10 -10
  106. package/src/assistant-transport.ts +94 -0
  107. package/src/devtools/DevToolsHooks.test.ts +40 -0
  108. package/src/devtools/DevToolsHooks.ts +10 -15
  109. package/src/index.ts +8 -4
  110. package/src/mcp-apps/McpAppsRemoteHost.test.ts +29 -3
  111. package/src/mcp-apps/McpAppsRemoteHost.ts +42 -7
  112. package/src/mcp-apps/bridge.test.ts +153 -0
  113. package/src/mcp-apps/bridge.ts +42 -30
  114. package/src/model-context/frame/SPEC_AssistantFrame.md +1 -1
  115. package/src/primitives/actionBar/ActionBarExportMarkdown.test.tsx +67 -0
  116. package/src/primitives/actionBar/ActionBarExportMarkdown.tsx +3 -1
  117. package/src/primitives/actionBar/ActionBarStopSpeaking.tsx +0 -9
  118. package/src/primitives/branchPicker/BranchPickerRoot.tsx +5 -3
  119. package/src/primitives/composer/ComposerDictationTranscript.tsx +2 -2
  120. package/src/primitives/composer/ComposerInput.tsx +1 -1
  121. package/src/primitives/composer/trigger/triggerSelectionResource.test.ts +135 -0
  122. package/src/primitives/composer/trigger/triggerSelectionResource.ts +2 -0
  123. package/src/primitives/thread/ThreadRoot.test.tsx +251 -0
  124. package/src/primitives/thread/ThreadRoot.tsx +33 -0
  125. package/src/primitives/thread/ThreadViewport.tsx +1 -8
  126. package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +152 -0
  127. package/src/primitives/thread/useThreadViewportAutoScroll.ts +26 -19
  128. package/src/sandbox-host/SandboxHost.test.tsx +90 -0
  129. package/src/sandbox-host/SandboxHost.tsx +8 -2
  130. package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +24 -3
  131. package/src/tests/RemoteThreadListRuntime.reloadMainThread.test.tsx +1 -2
  132. package/src/tests/augmentations.test.ts +49 -0
  133. package/src/tests/in-memory-thread-list.test.tsx +1 -1
  134. package/src/tests/threadMessageById.test.tsx +61 -1
  135. package/src/unstable/useComposerInput.ts +2 -2
  136. package/src/unstable/useComposerInputHistory.test.tsx +4 -4
  137. package/src/unstable/useComposerInputHistory.ts +1 -1
  138. package/src/unstable/useLiveCompletionAdapter.test.tsx +157 -0
  139. package/src/unstable/useLiveCompletionAdapter.ts +41 -17
  140. package/src/utils/invokeCallbackSafely.ts +11 -0
  141. package/src/utils/smooth/useSmooth.test.tsx +110 -0
  142. package/src/utils/smooth/useSmooth.ts +20 -6
  143. package/dist/client/InMemoryThreadList.d.ts +0 -12
  144. package/dist/client/InMemoryThreadList.d.ts.map +0 -1
  145. package/dist/client/InMemoryThreadList.js +0 -374
  146. package/dist/client/InMemoryThreadList.js.map +0 -1
  147. package/dist/context/react/utils/ensureBinding.d.ts +0 -5
  148. package/dist/context/react/utils/ensureBinding.d.ts.map +0 -1
  149. package/dist/context/react/utils/ensureBinding.js +0 -24
  150. package/dist/context/react/utils/ensureBinding.js.map +0 -1
  151. package/dist/context/react/utils/useRuntimeState.d.ts +0 -13
  152. package/dist/context/react/utils/useRuntimeState.d.ts.map +0 -1
  153. package/dist/context/react/utils/useRuntimeState.js +0 -33
  154. package/dist/context/react/utils/useRuntimeState.js.map +0 -1
  155. package/dist/legacy-runtime/cloud/auiV0.d.ts +0 -119
  156. package/dist/legacy-runtime/cloud/auiV0.d.ts.map +0 -1
  157. package/dist/legacy-runtime/cloud/auiV0.js +0 -137
  158. package/dist/legacy-runtime/cloud/auiV0.js.map +0 -1
  159. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.d.ts +0 -16
  160. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.d.ts.map +0 -1
  161. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.js +0 -36
  162. package/dist/legacy-runtime/cloud/useCloudThreadListRuntime.js.map +0 -1
  163. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts +0 -17
  164. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts.map +0 -1
  165. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js +0 -55
  166. package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js.map +0 -1
  167. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts +0 -11
  168. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.d.ts.map +0 -1
  169. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js +0 -165
  170. package/dist/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.js.map +0 -1
  171. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts +0 -15
  172. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts.map +0 -1
  173. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +0 -81
  174. package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js.map +0 -1
  175. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts +0 -126
  176. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.d.ts.map +0 -1
  177. package/dist/legacy-runtime/runtime-cores/assistant-transport/types.js +0 -0
  178. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts +0 -14
  179. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +0 -1
  180. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +0 -290
  181. package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +0 -1
  182. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.d.ts +0 -7
  183. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.d.ts.map +0 -1
  184. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js +0 -25
  185. package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js.map +0 -1
  186. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.d.ts +0 -7
  187. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.d.ts.map +0 -1
  188. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js +0 -27
  189. package/dist/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.js.map +0 -1
  190. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.d.ts +0 -2
  191. package/dist/legacy-runtime/runtime-cores/assistant-transport/utils.js +0 -2
  192. package/src/client/InMemoryThreadList.ts +0 -247
  193. package/src/context/react/utils/ensureBinding.ts +0 -46
  194. package/src/context/react/utils/useRuntimeState.ts +0 -46
  195. package/src/legacy-runtime/cloud/auiV0.ts +0 -331
  196. package/src/legacy-runtime/cloud/useCloudThreadListRuntime.ts +0 -31
  197. package/src/legacy-runtime/runtime-cores/assistant-transport/commandQueue.ts +0 -68
  198. package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.test.ts +0 -455
  199. package/src/legacy-runtime/runtime-cores/assistant-transport/replayBoundaryStream.ts +0 -172
  200. package/src/legacy-runtime/runtime-cores/assistant-transport/runManager.ts +0 -113
  201. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-delivery.test.tsx +0 -79
  202. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-parentid.test.tsx +0 -97
  203. package/src/legacy-runtime/runtime-cores/assistant-transport/transport-scheduling.test.ts +0 -307
  204. package/src/legacy-runtime/runtime-cores/assistant-transport/types.ts +0 -170
  205. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransport.spec.md +0 -141
  206. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx +0 -585
  207. package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +0 -476
  208. package/src/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.ts +0 -20
  209. package/src/legacy-runtime/runtime-cores/assistant-transport/useLatestRef.ts +0 -9
  210. package/src/legacy-runtime/runtime-cores/assistant-transport/utils.ts +0 -1
@@ -14,7 +14,7 @@ declare const useSmoothStatus: {
14
14
  readonly error?: unknown;
15
15
  } | {
16
16
  readonly type: "requires-action";
17
- readonly reason: "interrupt";
17
+ readonly reason: "tool-calls" | "interrupt";
18
18
  };
19
19
  <TSelected>(selector: (state: {
20
20
  readonly type: "running";
@@ -26,7 +26,7 @@ declare const useSmoothStatus: {
26
26
  readonly error?: unknown;
27
27
  } | {
28
28
  readonly type: "requires-action";
29
- readonly reason: "interrupt";
29
+ readonly reason: "tool-calls" | "interrupt";
30
30
  }) => TSelected): TSelected;
31
31
  (options: {
32
32
  optional: true;
@@ -40,7 +40,7 @@ declare const useSmoothStatus: {
40
40
  readonly error?: unknown;
41
41
  } | {
42
42
  readonly type: "requires-action";
43
- readonly reason: "interrupt";
43
+ readonly reason: "tool-calls" | "interrupt";
44
44
  } | null;
45
45
  <TSelected>(options: {
46
46
  optional: true;
@@ -54,7 +54,7 @@ declare const useSmoothStatus: {
54
54
  readonly error?: unknown;
55
55
  } | {
56
56
  readonly type: "requires-action";
57
- readonly reason: "interrupt";
57
+ readonly reason: "tool-calls" | "interrupt";
58
58
  }) => TSelected;
59
59
  }): TSelected | null;
60
60
  }, useSmoothStatusStore: {
@@ -68,7 +68,7 @@ declare const useSmoothStatus: {
68
68
  readonly error?: unknown;
69
69
  } | {
70
70
  readonly type: "requires-action";
71
- readonly reason: "interrupt";
71
+ readonly reason: "tool-calls" | "interrupt";
72
72
  }>;
73
73
  (options: {
74
74
  optional: true;
@@ -82,7 +82,7 @@ declare const useSmoothStatus: {
82
82
  readonly error?: unknown;
83
83
  } | {
84
84
  readonly type: "requires-action";
85
- readonly reason: "interrupt";
85
+ readonly reason: "tool-calls" | "interrupt";
86
86
  }> | null;
87
87
  };
88
88
  //#endregion
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { createContextStoreHook } from "../../context/react/utils/createContextStoreHook.js";
3
- import { useAui } from "@assistant-ui/store";
4
3
  import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
4
+ import { useAui } from "@assistant-ui/store";
5
5
  import { createContext, forwardRef, useContext, useState } from "@assistant-ui/tap/react-shim";
6
6
  import { create } from "zustand";
7
7
  import { jsx } from "react/jsx-runtime";
@@ -37,6 +37,9 @@ type SmoothOptions = {
37
37
  * `true` uses the default rate, and a {@link SmoothOptions} object tunes
38
38
  * the reveal. Returns the part state with `text` replaced by the revealed
39
39
  * prefix and `status` reporting `running` until the reveal catches up.
40
+ * If the source settles before any character has been revealed, the text
41
+ * is committed immediately so a missed animation frame cannot leave an
42
+ * empty bubble.
40
43
  *
41
44
  * The reveal auto-disables under `prefers-reduced-motion: reduce`,
42
45
  * committing the full text immediately; this takes precedence over an
@@ -1 +1 @@
1
- {"version":3,"file":"useSmooth.d.ts","names":[],"sources":["../../../src/utils/smooth/useSmooth.ts"],"mappings":";;;;;KAkBY;;;;;;EAMV;;;;;;EAMA;;;;;EAKA;;;;;;;;EAQA;;;;;;;;;;;;;;;;;;;;;;cA6GW,YACX,OAAO,oBAAoB,kBAAkB,uBAC7C,mBAAkB,kBACjB,oBAAoB,kBAAkB"}
1
+ {"version":3,"file":"useSmooth.d.ts","names":[],"sources":["../../../src/utils/smooth/useSmooth.ts"],"mappings":";;;;;KAkBY;;;;;;EAMV;;;;;;EAMA;;;;;EAKA;;;;;;;;EAQA;;;;;;;;;;;;;;;;;;;;;;;;;cAgHW,YACX,OAAO,oBAAoB,kBAAkB,uBAC7C,mBAAkB,kBACjB,oBAAoB,kBAAkB"}
@@ -66,6 +66,9 @@ const positiveOr = (value, fallback) => value !== void 0 && value > 0 ? value :
66
66
  * `true` uses the default rate, and a {@link SmoothOptions} object tunes
67
67
  * the reveal. Returns the part state with `text` replaced by the revealed
68
68
  * prefix and `status` reporting `running` until the reveal catches up.
69
+ * If the source settles before any character has been revealed, the text
70
+ * is committed immediately so a missed animation frame cannot leave an
71
+ * empty bubble.
69
72
  *
70
73
  * The reveal auto-disables under `prefers-reduced-motion: reduce`,
71
74
  * committing the full text immediately; this takes precedence over an
@@ -147,17 +150,29 @@ const useSmooth = (state, smooth = false) => {
147
150
  animatorRef.currentText = text;
148
151
  animatorRef.targetText = text;
149
152
  animatorRef.stop();
153
+ setText(text);
150
154
  }
151
155
  return;
152
156
  }
153
157
  animatorRef.targetText = text;
158
+ if (state.status.type !== "running") {
159
+ if (animatorRef.currentText === "") {
160
+ animatorRef.currentText = text;
161
+ animatorRef.stop();
162
+ setText(text);
163
+ return;
164
+ }
165
+ animatorRef.start();
166
+ return;
167
+ }
154
168
  animatorRef.start();
155
169
  }, [
156
170
  animatorRef,
157
171
  enabled,
158
172
  text,
159
173
  state.status.type,
160
- part
174
+ part,
175
+ setText
161
176
  ]);
162
177
  useEffect(() => {
163
178
  return () => {
@@ -1 +1 @@
1
- {"version":3,"file":"useSmooth.js","names":["useEffect","useMemo","useRef","useState","useAui","useAuiState","MessagePartStatus","ReasoningMessagePart","TextMessagePart","MessagePartState","useCallbackRef","useMediaQuery","useSmoothStatusStore","writableStore","SmoothOptions","drainMs","maxCharIntervalMs","maxCharsPerFrame","minCommitMs","DEFAULT_DRAIN_MS","DEFAULT_MAX_CHAR_INTERVAL_MS","TextStreamAnimator","animationFrameId","lastUpdateTime","Date","now","lastCommitTime","targetText","Infinity","currentText","setText","newText","constructor","start","animate","stop","cancelAnimationFrame","currentTime","deltaTime","timeToConsume","remainingChars","length","baseTimePerChar","Math","min","frameLimit","charsToAdd","requestAnimationFrame","slice","isComplete","SMOOTH_STATUS","Object","freeze","type","positiveOr","value","fallback","undefined","useSmooth","state","smooth","text","reduceMotion","options","enabled","displayedText","setDisplayedText","status","aui","part","prevPart","setPrevPart","startsWith","smoothStatusStore","optional","target","setState","animatorRef","animatorPartRef","partChanged","current"],"sources":["../../../src/utils/smooth/useSmooth.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport type {\n MessagePartStatus,\n ReasoningMessagePart,\n TextMessagePart,\n MessagePartState,\n} from \"@assistant-ui/core\";\nimport { useCallbackRef } from \"@radix-ui/react-use-callback-ref\";\nimport { useMediaQuery } from \"../hooks/useMediaQuery\";\nimport { useSmoothStatusStore } from \"./SmoothContext\";\nimport { writableStore } from \"../../context/ReadonlyStore\";\n\n/**\n * Tuning options for the smooth text streaming animation.\n */\nexport type SmoothOptions = {\n /**\n * Target time in milliseconds to drain the backlog of unrevealed\n * characters. Larger values reveal long backlogs more gradually.\n * @default 250\n */\n drainMs?: number | undefined;\n /**\n * Maximum time in milliseconds between revealed characters, i.e. the\n * slowest reveal rate when the backlog is short.\n * @default 5\n */\n maxCharIntervalMs?: number | undefined;\n /**\n * Maximum number of characters revealed per animation frame.\n * @default Infinity\n */\n maxCharsPerFrame?: number | undefined;\n /**\n * Minimum time in milliseconds between committed updates. The reveal keeps\n * advancing every frame, but the visible text (and the downstream re-render\n * and markdown re-parse it triggers) is committed at most once per interval.\n * The final frame always commits. `0` commits every frame.\n * @default 0\n */\n minCommitMs?: number | undefined;\n};\n\nconst DEFAULT_DRAIN_MS = 250;\nconst DEFAULT_MAX_CHAR_INTERVAL_MS = 5;\n\nclass TextStreamAnimator {\n private animationFrameId: number | null = null;\n private lastUpdateTime: number = Date.now();\n public lastCommitTime: number = 0;\n\n public targetText: string = \"\";\n public drainMs: number = DEFAULT_DRAIN_MS;\n public maxCharIntervalMs: number = DEFAULT_MAX_CHAR_INTERVAL_MS;\n public maxCharsPerFrame: number = Infinity;\n public minCommitMs: number = 0;\n\n public currentText: string;\n private setText: (newText: string) => void;\n\n constructor(currentText: string, setText: (newText: string) => void) {\n this.currentText = currentText;\n this.setText = setText;\n }\n\n start() {\n if (this.animationFrameId !== null) return;\n this.lastUpdateTime = Date.now();\n this.animate();\n }\n\n stop() {\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n }\n\n private animate = () => {\n const currentTime = Date.now();\n const deltaTime = currentTime - this.lastUpdateTime;\n let timeToConsume = deltaTime;\n\n const remainingChars = this.targetText.length - this.currentText.length;\n const baseTimePerChar = Math.min(\n this.maxCharIntervalMs,\n this.drainMs / remainingChars,\n );\n\n const frameLimit = Math.min(remainingChars, this.maxCharsPerFrame);\n let charsToAdd = 0;\n while (timeToConsume >= baseTimePerChar && charsToAdd < frameLimit) {\n charsToAdd++;\n timeToConsume -= baseTimePerChar;\n }\n // A cap-limited frame must not bank its surplus time, or the next\n // frame would burst past the cap.\n if (charsToAdd === frameLimit && frameLimit === this.maxCharsPerFrame) {\n timeToConsume = 0;\n }\n\n if (charsToAdd !== remainingChars) {\n this.animationFrameId = requestAnimationFrame(this.animate);\n } else {\n this.animationFrameId = null;\n }\n if (charsToAdd === 0) return;\n\n this.currentText = this.targetText.slice(\n 0,\n this.currentText.length + charsToAdd,\n );\n this.lastUpdateTime = currentTime - timeToConsume;\n\n const isComplete = charsToAdd === remainingChars;\n if (isComplete || currentTime - this.lastCommitTime >= this.minCommitMs) {\n this.lastCommitTime = currentTime;\n this.setText(this.currentText);\n }\n };\n}\n\nconst SMOOTH_STATUS: MessagePartStatus = Object.freeze({\n type: \"running\",\n});\n\nconst positiveOr = (value: number | undefined, fallback: number): number =>\n value !== undefined && value > 0 ? value : fallback;\n\n/**\n * Animates streamed message part text with a typewriter-style reveal.\n *\n * Takes the current part state and a `smooth` argument: `false` disables,\n * `true` uses the default rate, and a {@link SmoothOptions} object tunes\n * the reveal. Returns the part state with `text` replaced by the revealed\n * prefix and `status` reporting `running` until the reveal catches up.\n *\n * The reveal auto-disables under `prefers-reduced-motion: reduce`,\n * committing the full text immediately; this takes precedence over an\n * explicit `smooth={true}`.\n *\n * @example\n * ```tsx\n * const { text, status } = useSmooth(useMessagePartText(), {\n * drainMs: 500,\n * maxCharsPerFrame: 30,\n * });\n * ```\n */\nexport const useSmooth = (\n state: MessagePartState & (TextMessagePart | ReasoningMessagePart),\n smooth: boolean | SmoothOptions = false,\n): MessagePartState & (TextMessagePart | ReasoningMessagePart) => {\n const { text } = state;\n const reduceMotion = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n const options =\n typeof smooth === \"object\" && smooth !== null ? smooth : undefined;\n const enabled = smooth !== false && smooth !== null && !reduceMotion;\n const drainMs = positiveOr(options?.drainMs, DEFAULT_DRAIN_MS);\n const maxCharIntervalMs = positiveOr(\n options?.maxCharIntervalMs,\n DEFAULT_MAX_CHAR_INTERVAL_MS,\n );\n const maxCharsPerFrame = positiveOr(options?.maxCharsPerFrame, Infinity);\n const minCommitMs = positiveOr(options?.minCommitMs, 0);\n\n const [displayedText, setDisplayedText] = useState(\n state.status.type === \"running\" ? \"\" : text,\n );\n\n // Render-phase resync on part flip or text discontinuity, so the\n // first paint after a thread switch never shows the previous\n // part's text (#4051). `displayedText` is already a prefix of\n // `text` during normal streaming, so use it as the previous-text\n // reference instead of carrying separate state — avoids the\n // double render per streaming token. Read part identity through\n // `useAuiState` so we actually subscribe to its changes instead\n // of relying on a render-time proxy reference that may be stable\n // across thread swaps.\n const aui = useAui();\n const part = useAuiState(() => aui.part);\n const [prevPart, setPrevPart] = useState(part);\n if (part !== prevPart || !text.startsWith(displayedText)) {\n setPrevPart(part);\n setDisplayedText(state.status.type === \"running\" ? \"\" : text);\n }\n\n const smoothStatusStore = useSmoothStatusStore({ optional: true });\n const setText = useCallbackRef((text: string) => {\n setDisplayedText(text);\n if (smoothStatusStore) {\n const target =\n displayedText !== text || state.status.type === \"running\"\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n });\n\n // TODO this is hacky\n useEffect(() => {\n if (smoothStatusStore) {\n const target =\n enabled && (displayedText !== text || state.status.type === \"running\")\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n }, [smoothStatusStore, enabled, text, displayedText, state.status]);\n\n const [animatorRef] = useState<TextStreamAnimator>(\n new TextStreamAnimator(displayedText, setText),\n );\n\n useEffect(() => {\n animatorRef.drainMs = drainMs;\n animatorRef.maxCharIntervalMs = maxCharIntervalMs;\n animatorRef.maxCharsPerFrame = maxCharsPerFrame;\n animatorRef.minCommitMs = minCommitMs;\n }, [animatorRef, drainMs, maxCharIntervalMs, maxCharsPerFrame, minCommitMs]);\n\n const animatorPartRef = useRef(part);\n useEffect(() => {\n if (!enabled) {\n animatorRef.stop();\n return;\n }\n\n // Discontinuity: part flipped, or new text breaks continuation\n // of the animator's current target. Either case requires\n // resetting the cursor — without the part check, a new part\n // whose text happens to share a prefix with the previous target\n // would keep the stale cursor and flicker.\n const partChanged = animatorPartRef.current !== part;\n animatorPartRef.current = part;\n if (partChanged || !text.startsWith(animatorRef.targetText)) {\n if (state.status.type === \"running\") {\n animatorRef.currentText = \"\";\n animatorRef.targetText = text;\n animatorRef.lastCommitTime = 0;\n animatorRef.start();\n } else {\n animatorRef.currentText = text;\n animatorRef.targetText = text;\n animatorRef.stop();\n }\n return;\n }\n\n animatorRef.targetText = text;\n animatorRef.start();\n }, [animatorRef, enabled, text, state.status.type, part]);\n\n useEffect(() => {\n return () => {\n animatorRef.stop();\n };\n }, [animatorRef]);\n\n return useMemo(\n () =>\n enabled\n ? {\n ...state,\n text: displayedText,\n status: text === displayedText ? state.status : SMOOTH_STATUS,\n }\n : state,\n [enabled, displayedText, state, text],\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useSmooth.js","names":["useEffect","useMemo","useRef","useState","useAui","useAuiState","MessagePartStatus","ReasoningMessagePart","TextMessagePart","MessagePartState","useCallbackRef","useMediaQuery","useSmoothStatusStore","writableStore","SmoothOptions","drainMs","maxCharIntervalMs","maxCharsPerFrame","minCommitMs","DEFAULT_DRAIN_MS","DEFAULT_MAX_CHAR_INTERVAL_MS","TextStreamAnimator","animationFrameId","lastUpdateTime","Date","now","lastCommitTime","targetText","Infinity","currentText","setText","newText","constructor","start","animate","stop","cancelAnimationFrame","currentTime","deltaTime","timeToConsume","remainingChars","length","baseTimePerChar","Math","min","frameLimit","charsToAdd","requestAnimationFrame","slice","isComplete","SMOOTH_STATUS","Object","freeze","type","positiveOr","value","fallback","undefined","useSmooth","state","smooth","text","reduceMotion","options","enabled","displayedText","setDisplayedText","status","aui","part","prevPart","setPrevPart","startsWith","smoothStatusStore","optional","target","setState","animatorRef","animatorPartRef","partChanged","current"],"sources":["../../../src/utils/smooth/useSmooth.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport type {\n MessagePartStatus,\n ReasoningMessagePart,\n TextMessagePart,\n MessagePartState,\n} from \"@assistant-ui/core\";\nimport { useCallbackRef } from \"@radix-ui/react-use-callback-ref\";\nimport { useMediaQuery } from \"../hooks/useMediaQuery\";\nimport { useSmoothStatusStore } from \"./SmoothContext\";\nimport { writableStore } from \"../../context/ReadonlyStore\";\n\n/**\n * Tuning options for the smooth text streaming animation.\n */\nexport type SmoothOptions = {\n /**\n * Target time in milliseconds to drain the backlog of unrevealed\n * characters. Larger values reveal long backlogs more gradually.\n * @default 250\n */\n drainMs?: number | undefined;\n /**\n * Maximum time in milliseconds between revealed characters, i.e. the\n * slowest reveal rate when the backlog is short.\n * @default 5\n */\n maxCharIntervalMs?: number | undefined;\n /**\n * Maximum number of characters revealed per animation frame.\n * @default Infinity\n */\n maxCharsPerFrame?: number | undefined;\n /**\n * Minimum time in milliseconds between committed updates. The reveal keeps\n * advancing every frame, but the visible text (and the downstream re-render\n * and markdown re-parse it triggers) is committed at most once per interval.\n * The final frame always commits. `0` commits every frame.\n * @default 0\n */\n minCommitMs?: number | undefined;\n};\n\nconst DEFAULT_DRAIN_MS = 250;\nconst DEFAULT_MAX_CHAR_INTERVAL_MS = 5;\n\nclass TextStreamAnimator {\n private animationFrameId: number | null = null;\n private lastUpdateTime: number = Date.now();\n public lastCommitTime: number = 0;\n\n public targetText: string = \"\";\n public drainMs: number = DEFAULT_DRAIN_MS;\n public maxCharIntervalMs: number = DEFAULT_MAX_CHAR_INTERVAL_MS;\n public maxCharsPerFrame: number = Infinity;\n public minCommitMs: number = 0;\n\n public currentText: string;\n private setText: (newText: string) => void;\n\n constructor(currentText: string, setText: (newText: string) => void) {\n this.currentText = currentText;\n this.setText = setText;\n }\n\n start() {\n if (this.animationFrameId !== null) return;\n this.lastUpdateTime = Date.now();\n this.animate();\n }\n\n stop() {\n if (this.animationFrameId !== null) {\n cancelAnimationFrame(this.animationFrameId);\n this.animationFrameId = null;\n }\n }\n\n private animate = () => {\n const currentTime = Date.now();\n const deltaTime = currentTime - this.lastUpdateTime;\n let timeToConsume = deltaTime;\n\n const remainingChars = this.targetText.length - this.currentText.length;\n const baseTimePerChar = Math.min(\n this.maxCharIntervalMs,\n this.drainMs / remainingChars,\n );\n\n const frameLimit = Math.min(remainingChars, this.maxCharsPerFrame);\n let charsToAdd = 0;\n while (timeToConsume >= baseTimePerChar && charsToAdd < frameLimit) {\n charsToAdd++;\n timeToConsume -= baseTimePerChar;\n }\n // A cap-limited frame must not bank its surplus time, or the next\n // frame would burst past the cap.\n if (charsToAdd === frameLimit && frameLimit === this.maxCharsPerFrame) {\n timeToConsume = 0;\n }\n\n if (charsToAdd !== remainingChars) {\n this.animationFrameId = requestAnimationFrame(this.animate);\n } else {\n this.animationFrameId = null;\n }\n if (charsToAdd === 0) return;\n\n this.currentText = this.targetText.slice(\n 0,\n this.currentText.length + charsToAdd,\n );\n this.lastUpdateTime = currentTime - timeToConsume;\n\n const isComplete = charsToAdd === remainingChars;\n if (isComplete || currentTime - this.lastCommitTime >= this.minCommitMs) {\n this.lastCommitTime = currentTime;\n this.setText(this.currentText);\n }\n };\n}\n\nconst SMOOTH_STATUS: MessagePartStatus = Object.freeze({\n type: \"running\",\n});\n\nconst positiveOr = (value: number | undefined, fallback: number): number =>\n value !== undefined && value > 0 ? value : fallback;\n\n/**\n * Animates streamed message part text with a typewriter-style reveal.\n *\n * Takes the current part state and a `smooth` argument: `false` disables,\n * `true` uses the default rate, and a {@link SmoothOptions} object tunes\n * the reveal. Returns the part state with `text` replaced by the revealed\n * prefix and `status` reporting `running` until the reveal catches up.\n * If the source settles before any character has been revealed, the text\n * is committed immediately so a missed animation frame cannot leave an\n * empty bubble.\n *\n * The reveal auto-disables under `prefers-reduced-motion: reduce`,\n * committing the full text immediately; this takes precedence over an\n * explicit `smooth={true}`.\n *\n * @example\n * ```tsx\n * const { text, status } = useSmooth(useMessagePartText(), {\n * drainMs: 500,\n * maxCharsPerFrame: 30,\n * });\n * ```\n */\nexport const useSmooth = (\n state: MessagePartState & (TextMessagePart | ReasoningMessagePart),\n smooth: boolean | SmoothOptions = false,\n): MessagePartState & (TextMessagePart | ReasoningMessagePart) => {\n const { text } = state;\n const reduceMotion = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n const options =\n typeof smooth === \"object\" && smooth !== null ? smooth : undefined;\n const enabled = smooth !== false && smooth !== null && !reduceMotion;\n const drainMs = positiveOr(options?.drainMs, DEFAULT_DRAIN_MS);\n const maxCharIntervalMs = positiveOr(\n options?.maxCharIntervalMs,\n DEFAULT_MAX_CHAR_INTERVAL_MS,\n );\n const maxCharsPerFrame = positiveOr(options?.maxCharsPerFrame, Infinity);\n const minCommitMs = positiveOr(options?.minCommitMs, 0);\n\n const [displayedText, setDisplayedText] = useState(\n state.status.type === \"running\" ? \"\" : text,\n );\n\n // Render-phase resync on part flip or text discontinuity, so the\n // first paint after a thread switch never shows the previous\n // part's text (#4051). `displayedText` is already a prefix of\n // `text` during normal streaming, so use it as the previous-text\n // reference instead of carrying separate state — avoids the\n // double render per streaming token. Read part identity through\n // `useAuiState` so we actually subscribe to its changes instead\n // of relying on a render-time proxy reference that may be stable\n // across thread swaps.\n const aui = useAui();\n const part = useAuiState(() => aui.part);\n const [prevPart, setPrevPart] = useState(part);\n if (part !== prevPart || !text.startsWith(displayedText)) {\n setPrevPart(part);\n setDisplayedText(state.status.type === \"running\" ? \"\" : text);\n }\n\n const smoothStatusStore = useSmoothStatusStore({ optional: true });\n const setText = useCallbackRef((text: string) => {\n setDisplayedText(text);\n if (smoothStatusStore) {\n const target =\n displayedText !== text || state.status.type === \"running\"\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n });\n\n // TODO this is hacky\n useEffect(() => {\n if (smoothStatusStore) {\n const target =\n enabled && (displayedText !== text || state.status.type === \"running\")\n ? SMOOTH_STATUS\n : state.status;\n writableStore(smoothStatusStore).setState(target, true);\n }\n }, [smoothStatusStore, enabled, text, displayedText, state.status]);\n\n const [animatorRef] = useState<TextStreamAnimator>(\n new TextStreamAnimator(displayedText, setText),\n );\n\n useEffect(() => {\n animatorRef.drainMs = drainMs;\n animatorRef.maxCharIntervalMs = maxCharIntervalMs;\n animatorRef.maxCharsPerFrame = maxCharsPerFrame;\n animatorRef.minCommitMs = minCommitMs;\n }, [animatorRef, drainMs, maxCharIntervalMs, maxCharsPerFrame, minCommitMs]);\n\n const animatorPartRef = useRef(part);\n useEffect(() => {\n if (!enabled) {\n animatorRef.stop();\n return;\n }\n\n const partChanged = animatorPartRef.current !== part;\n animatorPartRef.current = part;\n // A new part whose text shares a prefix with the previous target would\n // keep the stale cursor and flicker without this reset.\n if (partChanged || !text.startsWith(animatorRef.targetText)) {\n if (state.status.type === \"running\") {\n animatorRef.currentText = \"\";\n animatorRef.targetText = text;\n animatorRef.lastCommitTime = 0;\n animatorRef.start();\n } else {\n animatorRef.currentText = text;\n animatorRef.targetText = text;\n animatorRef.stop();\n setText(text);\n }\n return;\n }\n\n animatorRef.targetText = text;\n if (state.status.type !== \"running\") {\n // No character has painted. A pending frame that never runs would\n // leave an empty bubble after settle.\n if (animatorRef.currentText === \"\") {\n animatorRef.currentText = text;\n animatorRef.stop();\n setText(text);\n return;\n }\n animatorRef.start();\n return;\n }\n\n animatorRef.start();\n }, [animatorRef, enabled, text, state.status.type, part, setText]);\n\n useEffect(() => {\n return () => {\n animatorRef.stop();\n };\n }, [animatorRef]);\n\n return useMemo(\n () =>\n enabled\n ? {\n ...state,\n text: displayedText,\n status: text === displayedText ? state.status : SMOOTH_STATUS,\n }\n : state,\n [enabled, displayedText, state, text],\n );\n};\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { useAuiState } from "@assistant-ui/store";
2
1
  import { c } from "@assistant-ui/tap/react-shim/compiler-runtime";
2
+ import { useAuiState } from "@assistant-ui/store";
3
3
  import { getPartialJsonObjectFieldState } from "assistant-stream/utils";
4
4
  //#region src/utils/useToolArgsFieldStatus.ts
5
5
  const COMPLETE_STATUS = { type: "complete" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react",
3
- "version": "0.15.14",
3
+ "version": "0.15.16",
4
4
  "description": "Open-source TypeScript/React library for building production-grade AI chat experiences",
5
5
  "keywords": [
6
6
  "ai",
@@ -55,9 +55,9 @@
55
55
  ],
56
56
  "sideEffects": false,
57
57
  "dependencies": {
58
- "@assistant-ui/core": "^0.3.13",
59
- "@assistant-ui/store": "^0.3.9",
60
- "@assistant-ui/tap": "^0.9.12",
58
+ "@assistant-ui/core": "^0.3.15",
59
+ "@assistant-ui/store": "^0.3.10",
60
+ "@assistant-ui/tap": "^0.9.14",
61
61
  "@radix-ui/primitive": "^1.1.7",
62
62
  "@radix-ui/react-collection": "^1.1.15",
63
63
  "@radix-ui/react-compose-refs": "^1.1.5",
@@ -66,13 +66,13 @@
66
66
  "@radix-ui/react-use-callback-ref": "^1.1.4",
67
67
  "@radix-ui/react-use-controllable-state": "^1.2.6",
68
68
  "@radix-ui/react-use-escape-keydown": "^1.1.5",
69
- "assistant-cloud": "^0.1.40",
70
- "assistant-stream": "^0.3.37",
69
+ "assistant-cloud": "^0.1.41",
70
+ "assistant-stream": "^0.3.39",
71
71
  "radix-ui": "^1.6.7",
72
72
  "react-textarea-autosize": "^8.5.9",
73
73
  "safe-content-frame": "^0.0.27",
74
74
  "zod": "^4.4.3",
75
- "zustand": "^5.0.14"
75
+ "zustand": "^5.0.15"
76
76
  },
77
77
  "peerDependencies": {
78
78
  "@types/react": "*",
@@ -91,15 +91,15 @@
91
91
  "devDependencies": {
92
92
  "@testing-library/react": "^16.3.2",
93
93
  "@types/json-schema": "^7.0.15",
94
- "@types/node": "^26.1.2",
94
+ "@types/node": "^26.2.0",
95
95
  "@types/react": "^19.2.18",
96
96
  "@types/react-dom": "^19.2.4",
97
97
  "jsdom": "^30.0.1",
98
98
  "react": "^19.2.8",
99
99
  "react-dom": "^19.2.8",
100
100
  "vitest": "^4.1.10",
101
- "@assistant-ui/vite": "0.0.12",
102
- "@assistant-ui/x-buildutils": "0.0.22"
101
+ "@assistant-ui/vite": "0.0.14",
102
+ "@assistant-ui/x-buildutils": "0.0.23"
103
103
  },
104
104
  "publishConfig": {
105
105
  "access": "public",
@@ -0,0 +1,94 @@
1
+ "use client";
2
+
3
+ import type {
4
+ AssistantRuntime,
5
+ UserCommands as CoreUserCommands,
6
+ UserExternalState as CoreUserExternalState,
7
+ } from "@assistant-ui/core";
8
+ import {
9
+ useAssistantTransportRuntime as useCoreAssistantTransportRuntime,
10
+ useAssistantTransportSendCommand as useCoreAssistantTransportSendCommand,
11
+ useAssistantTransportState as useCoreAssistantTransportState,
12
+ } from "@assistant-ui/core/react";
13
+ import type {
14
+ AssistantTransportCommand as CoreAssistantTransportCommand,
15
+ AssistantTransportConnectionMetadata as CoreAssistantTransportConnectionMetadata,
16
+ AssistantTransportOptions as CoreAssistantTransportOptions,
17
+ AssistantTransportProtocol,
18
+ SendCommandsRequestBody as CoreSendCommandsRequestBody,
19
+ } from "@assistant-ui/core/react";
20
+ import type { UserCommands, UserExternalState } from "./augmentations";
21
+
22
+ export type { AssistantTransportProtocol };
23
+
24
+ export type SendCommandsRequestBody = Omit<
25
+ CoreSendCommandsRequestBody,
26
+ "commands"
27
+ > & {
28
+ commands: AssistantTransportCommand[];
29
+ };
30
+
31
+ export type AssistantTransportCommand =
32
+ | Exclude<CoreAssistantTransportCommand, CoreUserCommands>
33
+ | UserCommands;
34
+
35
+ export type AssistantTransportConnectionMetadata = Omit<
36
+ CoreAssistantTransportConnectionMetadata,
37
+ "pendingCommands"
38
+ > & {
39
+ pendingCommands: AssistantTransportCommand[];
40
+ };
41
+
42
+ export type AssistantTransportStateConverter<T> = (
43
+ state: T,
44
+ connectionMetadata: AssistantTransportConnectionMetadata,
45
+ ) => ReturnType<CoreAssistantTransportOptions<T>["converter"]>;
46
+
47
+ export type AssistantTransportOptions<T> = Omit<
48
+ CoreAssistantTransportOptions<T>,
49
+ "converter" | "prepareSendCommandsRequest" | "onError" | "onCancel"
50
+ > & {
51
+ converter: AssistantTransportStateConverter<T>;
52
+ prepareSendCommandsRequest?: (
53
+ body: SendCommandsRequestBody,
54
+ ) => Record<string, unknown> | Promise<Record<string, unknown>>;
55
+ onError?: (
56
+ error: Error,
57
+ params: {
58
+ commands: AssistantTransportCommand[];
59
+ updateState: (updater: (state: T) => T) => void;
60
+ },
61
+ ) => void | Promise<void>;
62
+ onCancel?: (params: {
63
+ commands: AssistantTransportCommand[];
64
+ updateState: (updater: (state: T) => T) => void;
65
+ error?: Error;
66
+ }) => void;
67
+ };
68
+
69
+ export const useAssistantTransportRuntime = <T>(
70
+ options: AssistantTransportOptions<T>,
71
+ ): AssistantRuntime => {
72
+ return useCoreAssistantTransportRuntime(
73
+ options as CoreAssistantTransportOptions<T>,
74
+ );
75
+ };
76
+
77
+ export const useAssistantTransportSendCommand = () => {
78
+ const send = useCoreAssistantTransportSendCommand();
79
+ return (command: AssistantTransportCommand) => {
80
+ send(command as CoreAssistantTransportCommand);
81
+ };
82
+ };
83
+
84
+ export function useAssistantTransportState(): UserExternalState;
85
+ export function useAssistantTransportState<T>(
86
+ selector: (state: UserExternalState) => T,
87
+ ): T;
88
+ export function useAssistantTransportState<T>(
89
+ selector: (state: UserExternalState) => T = (state) => state as T,
90
+ ): T | UserExternalState {
91
+ return useCoreAssistantTransportState(
92
+ selector as (state: CoreUserExternalState) => T,
93
+ );
94
+ }
@@ -0,0 +1,40 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { DevToolsHooks, DevToolsProviderApi } from "./DevToolsHooks";
3
+
4
+ describe("DevToolsHooks", () => {
5
+ afterEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+
9
+ it("notifies every listener when one throws", () => {
10
+ const error = new Error("listener failed");
11
+ const consoleError = vi
12
+ .spyOn(console, "error")
13
+ .mockImplementation(() => {});
14
+ const laterListener = vi.fn();
15
+ const unsubscribeThrowing = DevToolsHooks.subscribe(() => {
16
+ throw error;
17
+ });
18
+ const unsubscribeLater = DevToolsHooks.subscribe(laterListener);
19
+ let unregister = () => {};
20
+
21
+ try {
22
+ unregister = DevToolsProviderApi.register({});
23
+ const apiId = [...DevToolsHooks.getApis().keys()].at(-1);
24
+ expect(apiId).toBeDefined();
25
+
26
+ DevToolsHooks.clearEventLogs(apiId!);
27
+
28
+ expect(laterListener).toHaveBeenCalledTimes(2);
29
+ expect(consoleError).toHaveBeenCalledTimes(2);
30
+ expect(consoleError).toHaveBeenCalledWith(
31
+ "[assistant-ui] DevTools listener threw an error",
32
+ error,
33
+ );
34
+ } finally {
35
+ unsubscribeThrowing();
36
+ unsubscribeLater();
37
+ unregister();
38
+ }
39
+ });
40
+ });
@@ -1,4 +1,5 @@
1
1
  import type { Unsubscribe } from "@assistant-ui/core";
2
+ import { notifyEventListeners } from "@assistant-ui/core/internal";
2
3
  import type { AssistantClient } from "@assistant-ui/store";
3
4
 
4
5
  export interface EventLog {
@@ -54,6 +55,10 @@ const getHook = (): DevToolsHook => {
54
55
  return newHook;
55
56
  };
56
57
 
58
+ const notifyListeners = (apiId: number): void => {
59
+ notifyEventListeners(getHook().listeners, apiId, "DevTools");
60
+ };
61
+
57
62
  export class DevToolsHooks {
58
63
  static subscribe(listener: () => void): Unsubscribe {
59
64
  const hook = getHook();
@@ -69,17 +74,12 @@ export class DevToolsHooks {
69
74
  if (!entry) return;
70
75
 
71
76
  entry.logs = [];
72
- DevToolsHooks.notifyListeners(apiId);
77
+ notifyListeners(apiId);
73
78
  }
74
79
 
75
80
  static getApis(): Map<number, DevToolsApiEntry> {
76
81
  return getHook().apis;
77
82
  }
78
-
79
- private static notifyListeners(apiId: number): void {
80
- const hook = getHook();
81
- hook.listeners.forEach((listener) => listener(apiId));
82
- }
83
83
  }
84
84
 
85
85
  export class DevToolsProviderApi {
@@ -116,15 +116,15 @@ export class DevToolsProviderApi {
116
116
  );
117
117
  }
118
118
 
119
- DevToolsProviderApi.notifyListeners(apiId);
119
+ notifyListeners(apiId);
120
120
  });
121
121
 
122
122
  const stateUnsubscribe = aui.subscribe?.(() => {
123
- DevToolsProviderApi.notifyListeners(apiId);
123
+ notifyListeners(apiId);
124
124
  });
125
125
 
126
126
  hook.apis.set(apiId, entry);
127
- DevToolsProviderApi.notifyListeners(apiId);
127
+ notifyListeners(apiId);
128
128
 
129
129
  return () => {
130
130
  const hook = getHook();
@@ -136,12 +136,7 @@ export class DevToolsProviderApi {
136
136
 
137
137
  hook.apis.delete(apiId);
138
138
 
139
- DevToolsProviderApi.notifyListeners(apiId);
139
+ notifyListeners(apiId);
140
140
  };
141
141
  }
142
-
143
- private static notifyListeners(apiId: number): void {
144
- const hook = getHook();
145
- hook.listeners.forEach((listener) => listener(apiId));
146
- }
147
142
  }
package/src/index.ts CHANGED
@@ -43,7 +43,7 @@ export type {
43
43
  ThreadListItemState,
44
44
  } from "@assistant-ui/core";
45
45
 
46
- export { useCloudThreadListRuntime } from "./legacy-runtime/cloud/useCloudThreadListRuntime";
46
+ export { useCloudThreadListRuntime } from "@assistant-ui/core/react";
47
47
  export { AssistantCloud } from "assistant-cloud";
48
48
 
49
49
  // --- adapters/attachment ---
@@ -107,13 +107,13 @@ export {
107
107
  useAssistantTransportRuntime,
108
108
  useAssistantTransportSendCommand,
109
109
  useAssistantTransportState,
110
- } from "./legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime";
110
+ } from "./assistant-transport";
111
111
  export type {
112
112
  AssistantTransportConnectionMetadata,
113
113
  AssistantTransportCommand,
114
114
  AssistantTransportProtocol,
115
115
  SendCommandsRequestBody,
116
- } from "./legacy-runtime/runtime-cores/assistant-transport/types";
116
+ } from "./assistant-transport";
117
117
 
118
118
  // --- core ---
119
119
  export type {
@@ -430,7 +430,11 @@ export {
430
430
  export {
431
431
  InMemoryThreadList,
432
432
  type InMemoryThreadListProps,
433
- } from "./client/InMemoryThreadList";
433
+ } from "@assistant-ui/core/store";
434
+ export {
435
+ RemoteThreadList,
436
+ type RemoteThreadListProps,
437
+ } from "@assistant-ui/core/store";
434
438
  export { SingleThreadList } from "@assistant-ui/core/store";
435
439
 
436
440
  export * as INTERNAL from "./internal";
@@ -41,9 +41,15 @@ describe("McpAppsRemoteHost", () => {
41
41
  });
42
42
 
43
43
  it("posts serverId params verbatim for tool calls and resource loads", async () => {
44
- const fetch = vi.fn(async () =>
45
- Response.json({ content: [{ type: "text", text: "ok" }] }),
46
- ) as unknown as typeof globalThis.fetch;
44
+ const fetch = vi.fn(async (_input, init) => {
45
+ const request = JSON.parse(String(init?.body)) as { method: string };
46
+ return request.method === "mcp-apps/read-resource"
47
+ ? Response.json({
48
+ uri: "ui://example/search",
49
+ html: "<main>Search</main>",
50
+ })
51
+ : Response.json({ content: [{ type: "text", text: "ok" }] });
52
+ }) as unknown as typeof globalThis.fetch;
47
53
  const root = mount(fetch);
48
54
 
49
55
  try {
@@ -123,4 +129,24 @@ describe("McpAppsRemoteHost", () => {
123
129
  root.unmount();
124
130
  }
125
131
  });
132
+
133
+ it.each([{}, { uri: " ", html: "" }])(
134
+ "rejects malformed successful resource responses",
135
+ async (response) => {
136
+ const fetch = vi.fn(async () =>
137
+ Response.json(response),
138
+ ) as unknown as typeof globalThis.fetch;
139
+ const root = mount(fetch);
140
+
141
+ try {
142
+ await expect(
143
+ root.getValue().loadResource({ uri: "ui://example/search" }),
144
+ ).rejects.toThrow(
145
+ /Invalid MCP App host response "mcp-apps\/read-resource" from "\/api\/mcp-apps"/,
146
+ );
147
+ } finally {
148
+ root.unmount();
149
+ }
150
+ },
151
+ );
126
152
  });
@@ -39,6 +39,33 @@ const readErrorBody = async (res: Response): Promise<string | undefined> => {
39
39
  }
40
40
  };
41
41
 
42
+ const invalidMcpAppResource = (options: McpAppsRemoteHostOptions): never => {
43
+ throw new Error(
44
+ `Invalid MCP App host response "mcp-apps/read-resource" from "${options.url}": expected a resource with non-empty string "uri" and "html" fields`,
45
+ );
46
+ };
47
+
48
+ const parseMcpAppResource = (
49
+ value: unknown,
50
+ options: McpAppsRemoteHostOptions,
51
+ ): McpAppResource => {
52
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
53
+ return invalidMcpAppResource(options);
54
+ }
55
+
56
+ const resource = value as Record<string, unknown>;
57
+ if (
58
+ typeof resource.uri !== "string" ||
59
+ resource.uri.trim() === "" ||
60
+ typeof resource.html !== "string" ||
61
+ resource.html.trim() === ""
62
+ ) {
63
+ return invalidMcpAppResource(options);
64
+ }
65
+
66
+ return value as McpAppResource;
67
+ };
68
+
42
69
  async function postToHost(
43
70
  options: McpAppsRemoteHostOptions,
44
71
  method: string,
@@ -63,7 +90,14 @@ async function postToHost(
63
90
  `MCP App host request "${method}" to "${options.url}" failed with ${status}${body ? `: ${body}` : ""}`,
64
91
  );
65
92
  }
66
- return res.json();
93
+ try {
94
+ return await res.json();
95
+ } catch (cause) {
96
+ throw new Error(
97
+ `Invalid MCP App host response "${method}" from "${options.url}": expected valid JSON`,
98
+ { cause },
99
+ );
100
+ }
67
101
  }
68
102
 
69
103
  /**
@@ -91,12 +125,13 @@ const useMcpAppsRemoteHost = (
91
125
  };
92
126
  };
93
127
  return {
94
- loadResource: (params) =>
95
- postToHost(
96
- getCurrentOptions(),
97
- "mcp-apps/read-resource",
98
- params,
99
- ) as Promise<McpAppResource>,
128
+ loadResource: async (params) => {
129
+ const options = getCurrentOptions();
130
+ return parseMcpAppResource(
131
+ await postToHost(options, "mcp-apps/read-resource", params),
132
+ options,
133
+ );
134
+ },
100
135
  callTool: (params) =>
101
136
  postToHost(getCurrentOptions(), "tools/call", params),
102
137
  readResource: (params) =>