@assistant-ui/core 0.3.18 → 0.3.19

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 (274) hide show
  1. package/dist/adapters/speech.d.ts.map +1 -1
  2. package/dist/adapters/speech.js +13 -3
  3. package/dist/adapters/speech.js.map +1 -1
  4. package/dist/adapters/thread-history.d.ts +4 -0
  5. package/dist/adapters/thread-history.d.ts.map +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js.map +1 -1
  8. package/dist/internal.d.ts +1 -1
  9. package/dist/model-context/frame/host.d.ts.map +1 -1
  10. package/dist/model-context/frame/host.js +26 -11
  11. package/dist/model-context/frame/host.js.map +1 -1
  12. package/dist/model-context/interactable-composer-metadata.js +11 -9
  13. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  14. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  15. package/dist/react/adapters/LocalStorageThreadListAdapter.js +45 -7
  16. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  17. package/dist/react/client/DataRenderers.d.ts.map +1 -1
  18. package/dist/react/client/DataRenderers.js +20 -15
  19. package/dist/react/client/DataRenderers.js.map +1 -1
  20. package/dist/react/client/Interactables.d.ts.map +1 -1
  21. package/dist/react/client/Interactables.js +11 -5
  22. package/dist/react/client/Interactables.js.map +1 -1
  23. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  24. package/dist/react/client/RemoteThreadList.js +3 -2
  25. package/dist/react/client/RemoteThreadList.js.map +1 -1
  26. package/dist/react/client/interactable-model-context.d.ts.map +1 -1
  27. package/dist/react/client/interactable-model-context.js +3 -2
  28. package/dist/react/client/interactable-model-context.js.map +1 -1
  29. package/dist/react/index.d.ts +2 -2
  30. package/dist/react/index.js +2 -2
  31. package/dist/react/interactables-legacy/useAssistantInteractable.d.ts.map +1 -1
  32. package/dist/react/interactables-legacy/useAssistantInteractable.js +3 -0
  33. package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -1
  34. package/dist/react/model-context/useInteractable.d.ts.map +1 -1
  35. package/dist/react/model-context/useInteractable.js +4 -1
  36. package/dist/react/model-context/useInteractable.js.map +1 -1
  37. package/dist/react/model-context/useInteractableVersions.js +1 -1
  38. package/dist/react/primitives/message/MessageGroupedParts.d.ts +11 -2
  39. package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -1
  40. package/dist/react/primitives/message/MessageGroupedParts.js +3 -2
  41. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -1
  42. package/dist/react/primitives/part/PartMessages.d.ts +1 -0
  43. package/dist/react/primitives/part/PartMessages.d.ts.map +1 -1
  44. package/dist/react/primitives/part/PartMessages.js +1 -0
  45. package/dist/react/primitives/part/PartMessages.js.map +1 -1
  46. package/dist/react/primitives/thread/ThreadMessages.d.ts.map +1 -1
  47. package/dist/react/primitives/thread/ThreadMessages.js +6 -5
  48. package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
  49. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  50. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +2 -1
  51. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  52. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +2 -24
  53. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  54. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +135 -374
  55. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  56. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +1 -0
  57. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
  58. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +22 -2
  59. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
  60. package/dist/react/runtimes/cloud/auiV0.d.ts +40 -15
  61. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  62. package/dist/react/runtimes/cloud/auiV0.js +62 -15
  63. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  64. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts +2 -1
  65. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -1
  66. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +4 -0
  67. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -1
  68. package/dist/react/runtimes/cloud/sdkIdentity.d.ts +6 -0
  69. package/dist/react/runtimes/cloud/sdkIdentity.d.ts.map +1 -0
  70. package/dist/react/runtimes/cloud/sdkIdentity.js +9 -0
  71. package/dist/react/runtimes/cloud/sdkIdentity.js.map +1 -0
  72. package/dist/react/runtimes/external-message-converter.d.ts +12 -2
  73. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  74. package/dist/react/runtimes/external-message-converter.js +26 -61
  75. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  76. package/dist/react/runtimes/useRemoteThreadListRuntime.js +1 -1
  77. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  78. package/dist/react/utils/groupParts.d.ts +6 -1
  79. package/dist/react/utils/groupParts.d.ts.map +1 -1
  80. package/dist/react/utils/groupParts.js +6 -0
  81. package/dist/react/utils/groupParts.js.map +1 -1
  82. package/dist/react/utils/useJSONEqual.d.ts +3 -1
  83. package/dist/react/utils/useJSONEqual.d.ts.map +1 -1
  84. package/dist/react/utils/useJSONEqual.js +6 -1
  85. package/dist/react/utils/useJSONEqual.js.map +1 -1
  86. package/dist/react/utils/useJSONSchemaDependency.d.ts +7 -0
  87. package/dist/react/utils/useJSONSchemaDependency.d.ts.map +1 -0
  88. package/dist/react/utils/useJSONSchemaDependency.js +20 -0
  89. package/dist/react/utils/useJSONSchemaDependency.js.map +1 -0
  90. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  91. package/dist/runtime/api/thread-runtime.js +2 -5
  92. package/dist/runtime/api/thread-runtime.js.map +1 -1
  93. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -0
  94. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  95. package/dist/runtime/base/base-composer-runtime-core.js +46 -10
  96. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  97. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -0
  98. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  99. package/dist/runtime/base/base-thread-runtime-core.js +155 -64
  100. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  101. package/dist/runtime/interfaces/thread-runtime-core.d.ts +6 -0
  102. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  103. package/dist/runtime/queue/message-queue.d.ts +5 -0
  104. package/dist/runtime/queue/message-queue.d.ts.map +1 -1
  105. package/dist/runtime/queue/message-queue.js +61 -6
  106. package/dist/runtime/queue/message-queue.js.map +1 -1
  107. package/dist/runtime/utils/auto-status.d.ts +6 -2
  108. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  109. package/dist/runtime/utils/auto-status.js +12 -4
  110. package/dist/runtime/utils/auto-status.js.map +1 -1
  111. package/dist/runtime/utils/external-message-conversion.d.ts +16 -4
  112. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -1
  113. package/dist/runtime/utils/external-message-conversion.js +48 -12
  114. package/dist/runtime/utils/external-message-conversion.js.map +1 -1
  115. package/dist/runtime/utils/message-repository.js +3 -3
  116. package/dist/runtime/utils/message-repository.js.map +1 -1
  117. package/dist/runtime/utils/thread-message-like.d.ts +2 -1
  118. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  119. package/dist/runtime/utils/thread-message-like.js +4 -2
  120. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  121. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  122. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +8 -1
  123. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  124. package/dist/runtimes/internal.d.ts +1 -1
  125. package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -1
  126. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  127. package/dist/runtimes/local/local-thread-runtime-core.js +4 -1
  128. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  129. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  130. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -1
  131. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  132. package/dist/store/clients/external-thread.d.ts.map +1 -1
  133. package/dist/store/clients/external-thread.js +241 -200
  134. package/dist/store/clients/external-thread.js.map +1 -1
  135. package/dist/store/clients/thread-tasks.d.ts +13 -0
  136. package/dist/store/clients/thread-tasks.d.ts.map +1 -0
  137. package/dist/store/clients/thread-tasks.js +69 -0
  138. package/dist/store/clients/thread-tasks.js.map +1 -0
  139. package/dist/store/index.d.ts +3 -2
  140. package/dist/store/index.js +3 -3
  141. package/dist/store/primitive-predicates.d.ts.map +1 -1
  142. package/dist/store/primitive-predicates.js +4 -4
  143. package/dist/store/primitive-predicates.js.map +1 -1
  144. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  145. package/dist/store/runtime-clients/thread-runtime-client.js +176 -128
  146. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  147. package/dist/store/scope-registration.d.ts +2 -0
  148. package/dist/store/scope-registration.d.ts.map +1 -1
  149. package/dist/store/scopes/task.d.ts +36 -0
  150. package/dist/store/scopes/task.d.ts.map +1 -0
  151. package/dist/store/scopes/task.js +0 -0
  152. package/dist/store/scopes/thread.d.ts +20 -0
  153. package/dist/store/scopes/thread.d.ts.map +1 -1
  154. package/dist/subscribable/subscribable.d.ts +2 -1
  155. package/dist/subscribable/subscribable.d.ts.map +1 -1
  156. package/dist/subscribable/subscribable.js +23 -15
  157. package/dist/subscribable/subscribable.js.map +1 -1
  158. package/dist/types/index.d.ts +2 -2
  159. package/dist/types/message.d.ts +8 -1
  160. package/dist/types/message.d.ts.map +1 -1
  161. package/dist/types/message.js.map +1 -1
  162. package/dist/utils/abortable-iterable.d.ts.map +1 -1
  163. package/dist/utils/abortable-iterable.js +30 -14
  164. package/dist/utils/abortable-iterable.js.map +1 -1
  165. package/dist/utils/getGroupStatus.d.ts +18 -2
  166. package/dist/utils/getGroupStatus.d.ts.map +1 -1
  167. package/dist/utils/getGroupStatus.js +32 -7
  168. package/dist/utils/getGroupStatus.js.map +1 -1
  169. package/dist/utils/wire-media.d.ts.map +1 -1
  170. package/dist/utils/wire-media.js +2 -2
  171. package/dist/utils/wire-media.js.map +1 -1
  172. package/package.json +6 -6
  173. package/src/adapters/speech.test.ts +236 -0
  174. package/src/adapters/speech.ts +17 -10
  175. package/src/adapters/thread-history.ts +3 -0
  176. package/src/index.ts +1 -0
  177. package/src/model-context/frame/host.test.ts +113 -1
  178. package/src/model-context/frame/host.ts +39 -12
  179. package/src/model-context/interactable-composer-metadata.test.ts +30 -0
  180. package/src/model-context/interactable-composer-metadata.ts +10 -10
  181. package/src/react/adapters/LocalStorageThreadListAdapter.test.ts +244 -0
  182. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +104 -7
  183. package/src/react/client/DataRenderers.test.tsx +45 -0
  184. package/src/react/client/DataRenderers.ts +13 -9
  185. package/src/react/client/Interactables.test.ts +44 -1
  186. package/src/react/client/Interactables.ts +7 -1
  187. package/src/react/client/RemoteThreadList.test.ts +41 -1
  188. package/src/react/client/RemoteThreadList.ts +15 -2
  189. package/src/react/client/interactable-model-context.test.ts +44 -0
  190. package/src/react/client/interactable-model-context.ts +3 -2
  191. package/src/react/index.ts +5 -1
  192. package/src/react/interactables-legacy/useAssistantInteractable.test.tsx +132 -0
  193. package/src/react/interactables-legacy/useAssistantInteractable.ts +10 -1
  194. package/src/react/model-context/useInteractable.test.tsx +177 -0
  195. package/src/react/model-context/useInteractable.ts +3 -0
  196. package/src/react/primitive-hooks/useActionBarCopy.test.ts +0 -1
  197. package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +0 -1
  198. package/src/react/primitives/message/MessageGroupedParts.test.tsx +96 -0
  199. package/src/react/primitives/message/MessageGroupedParts.tsx +15 -4
  200. package/src/react/primitives/part/PartMessages.test.tsx +104 -1
  201. package/src/react/primitives/part/PartMessages.tsx +1 -0
  202. package/src/react/primitives/thread/ThreadMessages.test.tsx +142 -0
  203. package/src/react/primitives/thread/ThreadMessages.tsx +8 -5
  204. package/src/react/providers/ReadonlyThreadProvider.test.tsx +56 -0
  205. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +7 -1
  206. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +20 -14
  207. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +221 -1
  208. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +166 -512
  209. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.test.ts +77 -0
  210. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +27 -1
  211. package/src/react/runtimes/cloud/auiV0.ts +160 -25
  212. package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +21 -0
  213. package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +7 -1
  214. package/src/react/runtimes/cloud/sdkIdentity.ts +9 -0
  215. package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +18 -4
  216. package/src/react/runtimes/external-message-converter.test.ts +136 -1
  217. package/src/react/runtimes/external-message-converter.ts +39 -93
  218. package/src/react/runtimes/useRemoteThreadListRuntime.ts +1 -1
  219. package/src/react/utils/groupParts.ts +12 -1
  220. package/src/react/utils/useJSONEqual.ts +9 -0
  221. package/src/react/utils/useJSONSchemaDependency.ts +22 -0
  222. package/src/runtime/api/thread-runtime.test.ts +58 -0
  223. package/src/runtime/api/thread-runtime.ts +2 -4
  224. package/src/runtime/base/base-composer-runtime-core.ts +69 -16
  225. package/src/runtime/base/base-thread-runtime-core.test.ts +1008 -1
  226. package/src/runtime/base/base-thread-runtime-core.ts +222 -83
  227. package/src/runtime/interfaces/thread-runtime-core.ts +6 -0
  228. package/src/runtime/queue/message-queue.ts +68 -6
  229. package/src/runtime/utils/auto-status.test.ts +112 -1
  230. package/src/runtime/utils/auto-status.ts +31 -5
  231. package/src/runtime/utils/external-message-conversion.test.ts +134 -0
  232. package/src/runtime/utils/external-message-conversion.ts +122 -31
  233. package/src/runtime/utils/message-repository.test.ts +49 -0
  234. package/src/runtime/utils/message-repository.ts +3 -3
  235. package/src/runtime/utils/thread-message-like.test.ts +52 -0
  236. package/src/runtime/utils/thread-message-like.ts +4 -0
  237. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +31 -1
  238. package/src/runtimes/local/local-thread-runtime-core.test.ts +21 -0
  239. package/src/runtimes/local/local-thread-runtime-core.ts +11 -1
  240. package/src/runtimes/tool-invocations/EDGE_CASES.md +2 -6
  241. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +133 -0
  242. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +10 -6
  243. package/src/store/clients/external-thread.ts +19 -1
  244. package/src/store/clients/thread-tasks.test.ts +274 -0
  245. package/src/store/clients/thread-tasks.ts +138 -0
  246. package/src/store/index.ts +8 -0
  247. package/src/store/primitive-predicates.test.ts +48 -5
  248. package/src/store/primitive-predicates.ts +7 -2
  249. package/src/store/runtime-clients/thread-runtime-client.ts +31 -1
  250. package/src/store/scope-registration.ts +2 -0
  251. package/src/store/scopes/task.ts +34 -0
  252. package/src/store/scopes/thread.ts +16 -0
  253. package/src/store/thread-tasks.test.tsx +209 -0
  254. package/src/subscribable/subscribable.test.ts +156 -1
  255. package/src/subscribable/subscribable.ts +25 -15
  256. package/src/tests/MessageRepository.test.ts +1 -5
  257. package/src/tests/auiV0Encode.test.ts +300 -0
  258. package/src/tests/base-composer-runtime-core-send.test.ts +51 -0
  259. package/src/tests/base-composer-runtime-core.test.ts +314 -2
  260. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +151 -0
  261. package/src/tests/external-thread-attachments.test.tsx +69 -0
  262. package/src/tests/external-thread-parity.test.tsx +34 -0
  263. package/src/tests/groupParts.test.ts +71 -0
  264. package/src/tests/message-queue.test.ts +253 -0
  265. package/src/tests/remote-thread-list-reactive-threadId.test.ts +51 -0
  266. package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +43 -0
  267. package/src/types/index.ts +1 -0
  268. package/src/types/message.ts +8 -0
  269. package/src/utils/abortable-iterable.test.ts +115 -0
  270. package/src/utils/abortable-iterable.ts +34 -23
  271. package/src/utils/getGroupStatus.test.ts +70 -4
  272. package/src/utils/getGroupStatus.ts +35 -12
  273. package/src/utils/wire-media.test.ts +10 -0
  274. package/src/utils/wire-media.ts +6 -2
@@ -8,7 +8,7 @@ import type {
8
8
  MessagePartStatus,
9
9
  ToolCallMessagePartStatus,
10
10
  } from "../../../types/message";
11
- import { getGroupStatus } from "../../../utils/getGroupStatus";
11
+ import { getGroupSummary } from "../../../utils/getGroupStatus";
12
12
  import {
13
13
  buildGroupTree,
14
14
  GROUPBY_MEMO_KEY,
@@ -18,6 +18,14 @@ import {
18
18
  import { MessagePartChildren, type EnrichedPartState } from "./MessageParts";
19
19
 
20
20
  export namespace MessagePrimitiveGroupedParts {
21
+ /** Per status tallies over the group's `indices`; they sum to `indices.length`. */
22
+ export type GroupCounts = {
23
+ readonly running: number;
24
+ readonly complete: number;
25
+ readonly incomplete: number;
26
+ readonly requiresAction: number;
27
+ };
28
+
21
29
  /**
22
30
  * A coalesced group of adjacent parts. Surfaced through the same
23
31
  * `{ part }` channel as a leaf {@link EnrichedPartState} so consumers
@@ -28,6 +36,8 @@ export namespace MessagePrimitiveGroupedParts {
28
36
  export type GroupPart<TKey extends `group-${string}` = `group-${string}`> = {
29
37
  readonly type: TKey;
30
38
  readonly status: MessagePartStatus | ToolCallMessagePartStatus;
39
+ /** Per status tallies over `indices`. */
40
+ readonly counts: GroupCounts;
31
41
  readonly indices: readonly number[];
32
42
  };
33
43
 
@@ -85,10 +95,10 @@ export namespace MessagePrimitiveGroupedParts {
85
95
  * `switch (part.type)` can tell groups apart from real part types.
86
96
  *
87
97
  * **Prefer {@link groupPartByType}** for the common case of mapping by
88
- * `part.type` — it ships a stable memo fingerprint so the tree
98
+ * `part.type` or tool name — it ships a stable memo fingerprint so the tree
89
99
  * survives unrelated re-renders. Use an inline function only when
90
100
  * the helper isn't expressive enough (e.g. branching on
91
- * `part.toolName` or part metadata).
101
+ * `part.parentId` or part metadata).
92
102
  *
93
103
  * The second argument is a {@link GroupByContext} carrying the tool-UI
94
104
  * registry, for grouping that depends on it (e.g. standalone tool calls).
@@ -194,10 +204,11 @@ const renderNode = <TKey extends `group-${string}`>(
194
204
  );
195
205
  }
196
206
 
197
- const status = getGroupStatus(parts, node.indices);
207
+ const { status, counts } = getGroupSummary(parts, node.indices);
198
208
  const groupPart: MessagePrimitiveGroupedParts.GroupPart<TKey> = {
199
209
  type: node.key as TKey,
200
210
  status,
211
+ counts,
201
212
  indices: node.indices,
202
213
  };
203
214
 
@@ -1,4 +1,13 @@
1
- import { expectTypeOf } from "vitest";
1
+ // @vitest-environment jsdom
2
+
3
+ import { cleanup, render, screen } from "@testing-library/react";
4
+ import { useAuiState } from "@assistant-ui/store";
5
+ import { afterEach, describe, expect, expectTypeOf, it } from "vitest";
6
+ import type { ThreadMessage } from "../../../types/message";
7
+ import { AssistantRuntimeProvider } from "../../AssistantRuntimeProvider";
8
+ import { useExternalStoreRuntime } from "../../runtimes/useExternalStoreRuntime";
9
+ import { MessagePrimitiveParts } from "../message/MessageParts";
10
+ import { ThreadPrimitiveMessages } from "../thread/ThreadMessages";
2
11
  import { PartPrimitiveMessages } from "./PartMessages";
3
12
 
4
13
  const checkPartMessagesProps = () => {
@@ -15,3 +24,97 @@ const checkPartMessagesProps = () => {
15
24
  </PartPrimitiveMessages>;
16
25
  };
17
26
  expectTypeOf(checkPartMessagesProps).toEqualTypeOf<() => void>();
27
+
28
+ const nestedAssistant: ThreadMessage = {
29
+ id: "nested-assistant",
30
+ createdAt: new Date(0),
31
+ role: "assistant",
32
+ content: [],
33
+ status: { type: "running" },
34
+ metadata: {
35
+ unstable_state: {},
36
+ unstable_annotations: [],
37
+ unstable_data: [],
38
+ steps: [],
39
+ custom: {},
40
+ },
41
+ };
42
+
43
+ const NestedThreadStatus = () => {
44
+ const isRunning = useAuiState((s) => s.thread.isRunning);
45
+ return (
46
+ <output data-testid="nested-thread-running">{String(isRunning)}</output>
47
+ );
48
+ };
49
+
50
+ const OuterMessageStatus = () => {
51
+ const status = useAuiState((s) => s.message.status?.type);
52
+ return <output data-testid="outer-message-status">{status}</output>;
53
+ };
54
+
55
+ afterEach(cleanup);
56
+
57
+ describe("PartPrimitiveMessages", () => {
58
+ it("keeps the outer message running and reports the nested thread as running after the outer run ends", () => {
59
+ const App = () => {
60
+ const runtime = useExternalStoreRuntime({
61
+ messages: [
62
+ {
63
+ id: "outer-assistant",
64
+ createdAt: new Date(0),
65
+ role: "assistant" as const,
66
+ content: [
67
+ {
68
+ type: "tool-call" as const,
69
+ toolCallId: "delegate-1",
70
+ toolName: "delegate",
71
+ args: {},
72
+ argsText: "",
73
+ messages: [nestedAssistant],
74
+ },
75
+ ],
76
+ metadata: {
77
+ unstable_state: {},
78
+ unstable_annotations: [],
79
+ unstable_data: [],
80
+ steps: [],
81
+ custom: {},
82
+ },
83
+ },
84
+ ],
85
+ isRunning: false,
86
+ convertMessage: (message) => message,
87
+ onNew: async () => {},
88
+ });
89
+ return (
90
+ <AssistantRuntimeProvider runtime={runtime}>
91
+ <ThreadPrimitiveMessages>
92
+ {() => (
93
+ <>
94
+ <OuterMessageStatus />
95
+ <MessagePrimitiveParts>
96
+ {({ part }) =>
97
+ part.type === "tool-call" ? (
98
+ <PartPrimitiveMessages>
99
+ {() => <NestedThreadStatus />}
100
+ </PartPrimitiveMessages>
101
+ ) : null
102
+ }
103
+ </MessagePrimitiveParts>
104
+ </>
105
+ )}
106
+ </ThreadPrimitiveMessages>
107
+ </AssistantRuntimeProvider>
108
+ );
109
+ };
110
+
111
+ render(<App />);
112
+
113
+ expect(screen.getByTestId("outer-message-status").textContent).toBe(
114
+ "running",
115
+ );
116
+ expect(screen.getByTestId("nested-thread-running").textContent).toBe(
117
+ "true",
118
+ );
119
+ });
120
+ });
@@ -36,6 +36,7 @@ const usePartMessages = (): readonly ThreadMessage[] | undefined => {
36
36
  * This primitive reads `messages` from the current tool call part in the PartScope
37
37
  * and renders them using a readonly thread context. All existing message and part
38
38
  * primitives work inside, and parent tool UI registrations are inherited.
39
+ * The nested thread reports `isRunning` while its trailing assistant message is running.
39
40
  *
40
41
  * @example
41
42
  * ```tsx
@@ -0,0 +1,142 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { cleanup, render, screen, waitFor } from "@testing-library/react";
4
+ import { AuiProvider, useAui } from "@assistant-ui/store";
5
+ import { type FC, useState } from "react";
6
+ import { afterEach, describe, expect, it } from "vitest";
7
+ import {
8
+ ExternalThread,
9
+ type ExternalThreadProps,
10
+ } from "../../../store/clients/external-thread";
11
+ import type { MessageState } from "../../../store";
12
+ import { AssistantRuntimeProvider } from "../../AssistantRuntimeProvider";
13
+ import { useExternalStoreRuntime } from "../../runtimes/useExternalStoreRuntime";
14
+ import { ThreadPrimitiveMessages } from "./ThreadMessages";
15
+
16
+ const message = (id: string, role: "user" | "assistant") =>
17
+ ({
18
+ id,
19
+ createdAt: new Date(0),
20
+ role,
21
+ content: [{ type: "text", text: id }],
22
+ attachments: [],
23
+ metadata: {
24
+ custom: {},
25
+ },
26
+ ...(role === "assistant" ? { status: { type: "complete" } } : undefined),
27
+ }) as ExternalThreadProps["messages"][number];
28
+
29
+ const StatefulMessage = ({ message }: { message: MessageState }) => {
30
+ const [initialId] = useState(message.id);
31
+ return <span data-role={message.role}>{`${initialId}:${message.id}`}</span>;
32
+ };
33
+
34
+ const renderMessages = (messages: ExternalThreadProps["messages"]) => {
35
+ const App: FC<{ messages: ExternalThreadProps["messages"] }> = ({
36
+ messages,
37
+ }) => {
38
+ const aui = useAui({ thread: ExternalThread({ messages }) });
39
+ return (
40
+ <AuiProvider value={aui}>
41
+ <ThreadPrimitiveMessages>
42
+ {({ message }) => <StatefulMessage message={message} />}
43
+ </ThreadPrimitiveMessages>
44
+ </AuiProvider>
45
+ );
46
+ };
47
+ const result = render(<App messages={messages} />);
48
+ return {
49
+ rerender: (nextMessages: ExternalThreadProps["messages"]) =>
50
+ result.rerender(<App messages={nextMessages} />),
51
+ };
52
+ };
53
+
54
+ const renderExternalStoreMessages = (
55
+ messages: ExternalThreadProps["messages"],
56
+ isRunning: boolean,
57
+ ) => {
58
+ const App: FC<{
59
+ messages: ExternalThreadProps["messages"];
60
+ isRunning: boolean;
61
+ }> = ({ messages, isRunning }) => {
62
+ const runtime = useExternalStoreRuntime({
63
+ messages,
64
+ isRunning,
65
+ convertMessage: (message) => message,
66
+ onNew: async () => {},
67
+ });
68
+ return (
69
+ <AssistantRuntimeProvider runtime={runtime}>
70
+ <ThreadPrimitiveMessages>
71
+ {({ message }) => <StatefulMessage message={message} />}
72
+ </ThreadPrimitiveMessages>
73
+ </AssistantRuntimeProvider>
74
+ );
75
+ };
76
+ const result = render(<App messages={messages} isRunning={isRunning} />);
77
+ return {
78
+ container: result.container,
79
+ rerender: (
80
+ nextMessages: ExternalThreadProps["messages"],
81
+ nextIsRunning: boolean,
82
+ ) =>
83
+ result.rerender(
84
+ <App messages={nextMessages} isRunning={nextIsRunning} />,
85
+ ),
86
+ };
87
+ };
88
+
89
+ describe("ThreadPrimitiveMessages", () => {
90
+ afterEach(cleanup);
91
+
92
+ it("keeps component state with the surviving message after removal", () => {
93
+ const first = message("first", "user");
94
+ const second = message("second", "assistant");
95
+ const thread = renderMessages([first, second]);
96
+
97
+ thread.rerender([second]);
98
+
99
+ expect(screen.queryByText("second:second")).not.toBeNull();
100
+ expect(screen.queryByText("first:second")).toBeNull();
101
+ });
102
+
103
+ it("keeps component state with messages after prepending history", () => {
104
+ const first = message("first", "user");
105
+ const second = message("second", "assistant");
106
+ const thread = renderMessages([first, second]);
107
+
108
+ thread.rerender([message("zeroth", "assistant"), first, second]);
109
+
110
+ expect(screen.queryByText("first:first")).not.toBeNull();
111
+ expect(screen.queryByText("second:second")).not.toBeNull();
112
+ expect(screen.queryByText("first:zeroth")).toBeNull();
113
+ expect(screen.queryByText("second:first")).toBeNull();
114
+ });
115
+
116
+ it("remounts a message when its runtime ID changes", () => {
117
+ const thread = renderMessages([message("client-id", "assistant")]);
118
+
119
+ thread.rerender([message("server-id", "assistant")]);
120
+
121
+ expect(screen.queryByText("server-id:server-id")).not.toBeNull();
122
+ expect(screen.queryByText("client-id:server-id")).toBeNull();
123
+ });
124
+
125
+ it("remounts an optimistic placeholder as the real assistant message", async () => {
126
+ const userMessage = message("user-id", "user");
127
+ const thread = renderExternalStoreMessages([userMessage], true);
128
+ const optimisticRow = thread.container.querySelector(
129
+ '[data-role="assistant"]',
130
+ );
131
+
132
+ expect(optimisticRow).not.toBeNull();
133
+ expect(optimisticRow?.textContent).not.toBe("assistant-id:assistant-id");
134
+
135
+ thread.rerender([userMessage, message("assistant-id", "assistant")], false);
136
+
137
+ await waitFor(() => {
138
+ expect(screen.queryByText("assistant-id:assistant-id")).not.toBeNull();
139
+ });
140
+ expect(optimisticRow?.isConnected).toBe(false);
141
+ });
142
+ });
@@ -6,6 +6,7 @@ import {
6
6
  useMemo,
7
7
  } from "react";
8
8
  import { RenderChildrenWithAccessor, useAuiState } from "@assistant-ui/store";
9
+ import { useShallowSelector } from "@assistant-ui/store/internal";
9
10
  import { MessageByIndexProvider } from "../../providers/MessageByIndexProvider";
10
11
  import { MessageByIdProvider } from "../../providers/MessageByIdProvider";
11
12
  import type { MessageState } from "../../../store";
@@ -241,12 +242,14 @@ ThreadPrimitiveUnstable_MessageById.displayName =
241
242
  const ThreadPrimitiveMessagesInner: FC<{
242
243
  children: (value: { message: MessageState }) => ReactNode;
243
244
  }> = ({ children }) => {
244
- const messagesLength = useAuiState((s) => s.thread.messages.length);
245
+ const messageIds = useAuiState(
246
+ useShallowSelector((s) => s.thread.messages.map((message) => message.id)),
247
+ );
245
248
 
246
249
  return useMemo(() => {
247
- if (messagesLength === 0) return null;
248
- return Array.from({ length: messagesLength }, (_, index) => (
249
- <MessageByIndexProvider key={index} index={index}>
250
+ if (messageIds.length === 0) return null;
251
+ return messageIds.map((messageId, index) => (
252
+ <MessageByIndexProvider key={messageId} index={index}>
250
253
  <RenderChildrenWithAccessor
251
254
  getItemState={(aui) => aui.thread.message({ index }).getState()}
252
255
  >
@@ -260,7 +263,7 @@ const ThreadPrimitiveMessagesInner: FC<{
260
263
  </RenderChildrenWithAccessor>
261
264
  </MessageByIndexProvider>
262
265
  ));
263
- }, [messagesLength, children]);
266
+ }, [messageIds, children]);
264
267
  };
265
268
 
266
269
  /**
@@ -0,0 +1,56 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { cleanup, render, screen, waitFor } from "@testing-library/react";
4
+ import { useAuiState } from "@assistant-ui/store";
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+ import type { ThreadMessage } from "../../types/message";
7
+ import { ReadonlyThreadProvider } from "./ReadonlyThreadProvider";
8
+
9
+ const messages = (status: "running" | "complete"): readonly ThreadMessage[] => [
10
+ {
11
+ id: "nested-assistant",
12
+ createdAt: new Date(0),
13
+ role: "assistant",
14
+ content: [],
15
+ status:
16
+ status === "running"
17
+ ? { type: "running" }
18
+ : { type: "complete", reason: "unknown" },
19
+ metadata: {
20
+ unstable_state: {},
21
+ unstable_annotations: [],
22
+ unstable_data: [],
23
+ steps: [],
24
+ custom: {},
25
+ },
26
+ },
27
+ ];
28
+
29
+ const ThreadStatus = () => {
30
+ const isRunning = useAuiState((s) => s.thread.isRunning);
31
+ return <output data-testid="thread-running">{String(isRunning)}</output>;
32
+ };
33
+
34
+ afterEach(cleanup);
35
+
36
+ describe("ReadonlyThreadProvider", () => {
37
+ it("reports isRunning from the trailing assistant message", async () => {
38
+ const result = render(
39
+ <ReadonlyThreadProvider messages={messages("running")}>
40
+ <ThreadStatus />
41
+ </ReadonlyThreadProvider>,
42
+ );
43
+
44
+ expect(screen.getByTestId("thread-running").textContent).toBe("true");
45
+
46
+ result.rerender(
47
+ <ReadonlyThreadProvider messages={messages("complete")}>
48
+ <ThreadStatus />
49
+ </ReadonlyThreadProvider>,
50
+ );
51
+
52
+ await waitFor(() => {
53
+ expect(screen.getByTestId("thread-running").textContent).toBe("false");
54
+ });
55
+ });
56
+ });
@@ -58,6 +58,7 @@ import { useAui } from "@assistant-ui/store";
58
58
  import type { ModelContextProvider } from "../../model-context/types";
59
59
  import { RuntimeAdapterProvider } from "./RuntimeAdapterProvider";
60
60
  import { useStableRuntimeAdapters } from "./useRuntimeAdapters";
61
+ import { invokeUserCallback } from "../../utils/invoke-user-callback";
61
62
 
62
63
  const threadNotFoundError = (threadIdOrRemoteId: string, action: string) =>
63
64
  new Error(`Thread "${threadIdOrRemoteId}" not found while ${action}.`);
@@ -573,7 +574,12 @@ export class RemoteThreadListThreadListRuntimeCore
573
574
  if (this._lastNotifiedThreadId === threadId) return;
574
575
  this._lastNotifiedThreadId = threadId;
575
576
  if (emit) {
576
- this._options.onThreadIdChange?.(threadId);
577
+ invokeUserCallback(
578
+ "assistant-ui",
579
+ "onThreadIdChange",
580
+ this._options.onThreadIdChange,
581
+ threadId,
582
+ );
577
583
  }
578
584
  }
579
585
 
@@ -28,8 +28,8 @@ describe("extractAuiV0", () => {
28
28
  it("reports a terminal message once with its usage", () => {
29
29
  const result = extractAuiV0(auiV0Message({ type: "complete" }));
30
30
  expect(result?.status).toBe("completed");
31
- expect(result?.inputTokens).toBe(1);
32
- expect(result?.outputTokens).toBe(2);
31
+ expect(result?.usage?.inputTokens).toBe(1);
32
+ expect(result?.usage?.outputTokens).toBe(2);
33
33
  });
34
34
 
35
35
  it("sums step usage reported under the AI SDK v7 token details", () => {
@@ -57,22 +57,28 @@ describe("extractAuiV0", () => {
57
57
  },
58
58
  });
59
59
 
60
- expect(result?.inputTokens).toBe(15);
61
- expect(result?.outputTokens).toBe(6);
62
- expect(result?.cachedInputTokens).toBe(9);
63
- expect(result?.reasoningTokens).toBe(2);
60
+ expect(result?.usage).toEqual({
61
+ inputTokens: 15,
62
+ outputTokens: 6,
63
+ cachedInputTokens: 9,
64
+ reasoningTokens: 2,
65
+ });
64
66
  expect(result?.steps).toEqual([
65
67
  {
66
- input_tokens: 10,
67
- output_tokens: 4,
68
- reasoning_tokens: 1,
69
- cached_input_tokens: 6,
68
+ usage: {
69
+ inputTokens: 10,
70
+ outputTokens: 4,
71
+ inputTokenDetails: { cacheReadTokens: 6 },
72
+ outputTokenDetails: { reasoningTokens: 1 },
73
+ },
70
74
  },
71
75
  {
72
- input_tokens: 5,
73
- output_tokens: 2,
74
- reasoning_tokens: 1,
75
- cached_input_tokens: 3,
76
+ usage: {
77
+ inputTokens: 5,
78
+ outputTokens: 2,
79
+ inputTokenDetails: { cacheReadTokens: 3 },
80
+ outputTokenDetails: { reasoningTokens: 1 },
81
+ },
76
82
  },
77
83
  ]);
78
84
  });