@assistant-ui/core 0.3.17 → 0.3.18

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 (284) hide show
  1. package/dist/adapters/attachment.d.ts.map +1 -1
  2. package/dist/adapters/attachment.js +5 -4
  3. package/dist/adapters/attachment.js.map +1 -1
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/internal.d.ts +3 -2
  7. package/dist/internal.js +3 -2
  8. package/dist/model-context/frame/host.d.ts.map +1 -1
  9. package/dist/model-context/frame/host.js +10 -4
  10. package/dist/model-context/frame/host.js.map +1 -1
  11. package/dist/model-context/frame/provider.d.ts +2 -0
  12. package/dist/model-context/frame/provider.d.ts.map +1 -1
  13. package/dist/model-context/frame/provider.js +30 -7
  14. package/dist/model-context/frame/provider.js.map +1 -1
  15. package/dist/model-context/interactable-composer-metadata.d.ts.map +1 -1
  16. package/dist/model-context/interactable-composer-metadata.js +16 -9
  17. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  18. package/dist/model-context/registry.d.ts.map +1 -1
  19. package/dist/model-context/registry.js +3 -5
  20. package/dist/model-context/registry.js.map +1 -1
  21. package/dist/model-context/types.d.ts.map +1 -1
  22. package/dist/model-context/types.js +5 -4
  23. package/dist/model-context/types.js.map +1 -1
  24. package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
  25. package/dist/react/client/InMemoryThreadList.js +62 -56
  26. package/dist/react/client/InMemoryThreadList.js.map +1 -1
  27. package/dist/react/client/Interactables.d.ts.map +1 -1
  28. package/dist/react/client/Interactables.js +24 -27
  29. package/dist/react/client/Interactables.js.map +1 -1
  30. package/dist/react/client/RemoteThreadList.d.ts +1 -0
  31. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  32. package/dist/react/client/RemoteThreadList.js +151 -141
  33. package/dist/react/client/RemoteThreadList.js.map +1 -1
  34. package/dist/react/client/Tools.d.ts.map +1 -1
  35. package/dist/react/client/Tools.js +18 -16
  36. package/dist/react/client/Tools.js.map +1 -1
  37. package/dist/react/interactables-legacy/Interactables.js +242 -121
  38. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  39. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -1
  40. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +11 -9
  41. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -1
  42. package/dist/react/model-context/makeAssistantTool.d.ts +2 -2
  43. package/dist/react/model-context/makeAssistantTool.js +1 -1
  44. package/dist/react/model-context/makeAssistantTool.js.map +1 -1
  45. package/dist/react/model-context/useAssistantTool.d.ts +2 -2
  46. package/dist/react/model-context/useAssistantTool.js +1 -1
  47. package/dist/react/model-context/useAssistantTool.js.map +1 -1
  48. package/dist/react/model-context/useToolArgsStatus.d.ts.map +1 -1
  49. package/dist/react/model-context/useToolArgsStatus.js +2 -1
  50. package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
  51. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  52. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +16 -8
  53. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  54. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +5 -1
  55. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  56. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +103 -103
  57. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  58. package/dist/react/runtimes/RemoteThreadResource.d.ts +1 -0
  59. package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
  60. package/dist/react/runtimes/RemoteThreadResource.js +13 -11
  61. package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
  62. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +4 -1
  63. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  64. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +401 -34
  65. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  66. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -1
  67. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +2 -1
  68. package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -1
  69. package/dist/react/runtimes/useExternalStoreRuntime.js +75 -46
  70. package/dist/react/runtimes/useExternalStoreRuntime.js.map +1 -1
  71. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
  72. package/dist/react/runtimes/useRemoteThreadListRuntime.js +63 -23
  73. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  74. package/dist/runtime/api/thread-list-item-runtime.d.ts +7 -3
  75. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  76. package/dist/runtime/api/thread-list-item-runtime.js +2 -2
  77. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  78. package/dist/runtime/api/thread-list-runtime.d.ts +3 -0
  79. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  80. package/dist/runtime/api/thread-list-runtime.js +18 -14
  81. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  82. package/dist/runtime/api/thread-runtime.d.ts +4 -0
  83. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  84. package/dist/runtime/api/thread-runtime.js +1 -1
  85. package/dist/runtime/api/thread-runtime.js.map +1 -1
  86. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  87. package/dist/runtime/base/base-composer-runtime-core.js +16 -12
  88. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  89. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  90. package/dist/runtime/base/base-thread-runtime-core.js +6 -5
  91. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  92. package/dist/runtime/interfaces/composer-runtime-core.d.ts +12 -5
  93. package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
  94. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +4 -1
  95. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  96. package/dist/runtime/utils/external-message-conversion.js +1 -1
  97. package/dist/runtime/utils/external-message-conversion.js.map +1 -1
  98. package/dist/runtime/utils/message-repository.d.ts +1 -0
  99. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  100. package/dist/runtime/utils/message-repository.js +15 -6
  101. package/dist/runtime/utils/message-repository.js.map +1 -1
  102. package/dist/runtime/utils/thread-message-like.js +1 -1
  103. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  104. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  105. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +21 -27
  106. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  107. package/dist/runtimes/internal.d.ts +2 -2
  108. package/dist/runtimes/internal.js +2 -2
  109. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +18 -1
  110. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  111. package/dist/runtimes/remote-thread-list/remote-thread-state.js +130 -42
  112. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  113. package/dist/runtimes/remote-thread-list/title-generation.d.ts +49 -0
  114. package/dist/runtimes/remote-thread-list/title-generation.d.ts.map +1 -0
  115. package/dist/runtimes/remote-thread-list/title-generation.js +197 -0
  116. package/dist/runtimes/remote-thread-list/title-generation.js.map +1 -0
  117. package/dist/runtimes/remote-thread-list/title.d.ts +3 -1
  118. package/dist/runtimes/remote-thread-list/title.d.ts.map +1 -1
  119. package/dist/runtimes/remote-thread-list/title.js +9 -1
  120. package/dist/runtimes/remote-thread-list/title.js.map +1 -1
  121. package/dist/runtimes/remote-thread-list/types.d.ts +10 -0
  122. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  123. package/dist/store/clients/external-thread.d.ts.map +1 -1
  124. package/dist/store/clients/external-thread.js +19 -3
  125. package/dist/store/clients/external-thread.js.map +1 -1
  126. package/dist/store/clients/model-context-client.d.ts.map +1 -1
  127. package/dist/store/clients/model-context-client.js +2 -1
  128. package/dist/store/clients/model-context-client.js.map +1 -1
  129. package/dist/store/clients/single-thread-list.d.ts.map +1 -1
  130. package/dist/store/clients/single-thread-list.js +1 -0
  131. package/dist/store/clients/single-thread-list.js.map +1 -1
  132. package/dist/store/clients/suggestions.d.ts.map +1 -1
  133. package/dist/store/clients/suggestions.js +5 -4
  134. package/dist/store/clients/suggestions.js.map +1 -1
  135. package/dist/store/runtime-clients/composer-runtime-client.d.ts +1 -0
  136. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  137. package/dist/store/runtime-clients/composer-runtime-client.js +97 -61
  138. package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
  139. package/dist/store/runtime-clients/message-runtime-client.d.ts +1 -0
  140. package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
  141. package/dist/store/runtime-clients/message-runtime-client.js +221 -144
  142. package/dist/store/runtime-clients/message-runtime-client.js.map +1 -1
  143. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  144. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
  145. package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
  146. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  147. package/dist/store/runtime-clients/thread-list-runtime-client.js +54 -52
  148. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  149. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  150. package/dist/store/runtime-clients/thread-runtime-client.js +193 -121
  151. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  152. package/dist/store/scopes/composer.d.ts +10 -2
  153. package/dist/store/scopes/composer.d.ts.map +1 -1
  154. package/dist/store/scopes/message.d.ts +30 -1
  155. package/dist/store/scopes/message.d.ts.map +1 -1
  156. package/dist/store/scopes/thread-list-item.d.ts +2 -2
  157. package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
  158. package/dist/store/scopes/thread.d.ts +8 -0
  159. package/dist/store/scopes/thread.d.ts.map +1 -1
  160. package/dist/store/scopes/threads.d.ts +1 -0
  161. package/dist/store/scopes/threads.d.ts.map +1 -1
  162. package/dist/types/attachment.d.ts +2 -1
  163. package/dist/types/attachment.d.ts.map +1 -1
  164. package/dist/types/attachment.js +2 -1
  165. package/dist/types/attachment.js.map +1 -1
  166. package/dist/utils/abortable-iterable.d.ts +19 -0
  167. package/dist/utils/abortable-iterable.d.ts.map +1 -0
  168. package/dist/utils/abortable-iterable.js +72 -0
  169. package/dist/utils/abortable-iterable.js.map +1 -0
  170. package/dist/utils/composite-context-provider.d.ts.map +1 -1
  171. package/dist/utils/composite-context-provider.js +20 -2
  172. package/dist/utils/composite-context-provider.js.map +1 -1
  173. package/dist/utils/record.d.ts +11 -0
  174. package/dist/utils/record.d.ts.map +1 -0
  175. package/dist/utils/record.js +12 -0
  176. package/dist/utils/record.js.map +1 -0
  177. package/package.json +10 -10
  178. package/src/adapters/attachment.test.ts +50 -1
  179. package/src/adapters/attachment.ts +7 -4
  180. package/src/index.ts +1 -0
  181. package/src/internal.ts +4 -0
  182. package/src/model-context/frame/host.test.ts +24 -0
  183. package/src/model-context/frame/host.ts +10 -4
  184. package/src/model-context/frame/provider.test.ts +71 -0
  185. package/src/model-context/frame/provider.ts +37 -7
  186. package/src/model-context/interactable-composer-metadata.test.ts +28 -0
  187. package/src/model-context/interactable-composer-metadata.ts +9 -3
  188. package/src/model-context/registry.test.ts +12 -0
  189. package/src/model-context/registry.ts +3 -2
  190. package/src/model-context/types.test.ts +25 -0
  191. package/src/model-context/types.ts +9 -4
  192. package/src/react/RuntimeAdapter.test.ts +34 -0
  193. package/src/react/client/InMemoryThreadList.ts +1 -0
  194. package/src/react/client/Interactables.test.ts +28 -0
  195. package/src/react/client/Interactables.ts +15 -13
  196. package/src/react/client/RemoteThreadList.background.test.ts +148 -6
  197. package/src/react/client/RemoteThreadList.concurrent.test.tsx +42 -1
  198. package/src/react/client/RemoteThreadList.test.ts +345 -3
  199. package/src/react/client/RemoteThreadList.ts +166 -110
  200. package/src/react/client/Tools.ts +22 -11
  201. package/src/react/interactables-legacy/Interactables.ts +16 -16
  202. package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +9 -10
  203. package/src/react/model-context/makeAssistantTool.ts +2 -2
  204. package/src/react/model-context/useAssistantTool.ts +2 -2
  205. package/src/react/model-context/useToolArgsStatus.test.tsx +30 -0
  206. package/src/react/model-context/useToolArgsStatus.ts +2 -1
  207. package/src/react/runtimes/RemoteThreadListHookInstanceManager.destroy.test.tsx +172 -0
  208. package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +33 -0
  209. package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +7 -1
  210. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +28 -5
  211. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.load-error.test.ts +132 -0
  212. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.switch-delete.test.ts +105 -0
  213. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +136 -111
  214. package/src/react/runtimes/RemoteThreadResource.test.ts +9 -0
  215. package/src/react/runtimes/RemoteThreadResource.ts +19 -12
  216. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +483 -6
  217. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +504 -68
  218. package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +15 -0
  219. package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +1 -0
  220. package/src/react/runtimes/useExternalStoreRuntime.lifecycle.test.tsx +41 -0
  221. package/src/react/runtimes/useExternalStoreRuntime.ts +10 -4
  222. package/src/react/runtimes/useLocalRuntime.test.tsx +20 -0
  223. package/src/react/runtimes/useRemoteThreadListRuntime.load-error.test.tsx +108 -0
  224. package/src/react/runtimes/useRemoteThreadListRuntime.ts +33 -0
  225. package/src/runtime/api/message-runtime.test.ts +75 -0
  226. package/src/runtime/api/thread-list-item-runtime.ts +10 -3
  227. package/src/runtime/api/thread-list-runtime.ts +21 -11
  228. package/src/runtime/api/thread-runtime.test.ts +78 -0
  229. package/src/runtime/api/thread-runtime.ts +8 -1
  230. package/src/runtime/base/base-composer-runtime-core.ts +36 -8
  231. package/src/runtime/base/base-thread-runtime-core.test.ts +55 -1
  232. package/src/runtime/base/base-thread-runtime-core.ts +6 -4
  233. package/src/runtime/interfaces/composer-runtime-core.ts +10 -5
  234. package/src/runtime/interfaces/thread-list-runtime-core.ts +5 -1
  235. package/src/runtime/utils/external-message-conversion.test.ts +68 -0
  236. package/src/runtime/utils/external-message-conversion.ts +1 -1
  237. package/src/runtime/utils/message-repository.ts +29 -17
  238. package/src/runtime/utils/thread-message-like.ts +1 -1
  239. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +18 -13
  240. package/src/runtimes/internal.ts +1 -0
  241. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +128 -2
  242. package/src/runtimes/remote-thread-list/remote-thread-state.ts +182 -42
  243. package/src/runtimes/remote-thread-list/title-generation.test.ts +602 -0
  244. package/src/runtimes/remote-thread-list/title-generation.ts +329 -0
  245. package/src/runtimes/remote-thread-list/title.test.ts +62 -0
  246. package/src/runtimes/remote-thread-list/title.ts +13 -0
  247. package/src/runtimes/remote-thread-list/types.ts +10 -0
  248. package/src/store/clients/external-thread.ts +48 -3
  249. package/src/store/clients/model-context-client.ts +5 -1
  250. package/src/store/clients/single-thread-list.ts +1 -0
  251. package/src/store/clients/suggestions.test.ts +40 -0
  252. package/src/store/clients/suggestions.ts +13 -10
  253. package/src/store/runtime-clients/composer-runtime-client.ts +42 -9
  254. package/src/store/runtime-clients/message-runtime-client.ts +54 -6
  255. package/src/store/runtime-clients/thread-list-item-runtime-client.ts +4 -2
  256. package/src/store/runtime-clients/thread-list-runtime-client.ts +1 -0
  257. package/src/store/runtime-clients/thread-runtime-client.ts +58 -10
  258. package/src/store/scopes/composer.ts +16 -4
  259. package/src/store/scopes/message.ts +14 -0
  260. package/src/store/scopes/thread-list-item.ts +5 -2
  261. package/src/store/scopes/thread.ts +4 -0
  262. package/src/store/scopes/threads.ts +1 -0
  263. package/src/tests/MessageRepository.test.ts +116 -0
  264. package/src/tests/OptimisticState-list-race.test.ts +1 -0
  265. package/src/tests/RemoteThreadListThreadListRuntimeCore-initialize-adapter-change.test.ts +38 -0
  266. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +47 -0
  267. package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +89 -0
  268. package/src/tests/attachment-adapters.test.ts +14 -0
  269. package/src/tests/base-composer-runtime-core.test.ts +39 -0
  270. package/src/tests/external-store-thread-list-runtime-core.test.ts +83 -0
  271. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +27 -18
  272. package/src/tests/external-thread-attachments.test.tsx +139 -0
  273. package/src/tests/external-thread-parity.test.tsx +23 -0
  274. package/src/tests/remote-thread-list-isLoading.test.ts +1 -0
  275. package/src/tests/thread-list-runtime-getLoadThreadsPromise.test.ts +1 -0
  276. package/src/tests/thread-switch-events.test.tsx +78 -0
  277. package/src/tests/tools-scope-migration.test.tsx +28 -0
  278. package/src/types/attachment.ts +4 -0
  279. package/src/utils/abortable-iterable.test.ts +213 -0
  280. package/src/utils/abortable-iterable.ts +93 -0
  281. package/src/utils/composite-context-provider.test.ts +25 -0
  282. package/src/utils/composite-context-provider.ts +23 -2
  283. package/src/utils/record.test.ts +48 -0
  284. package/src/utils/record.ts +11 -0
@@ -1,5 +1,5 @@
1
1
  import type { Unsubscribe } from "../../types/unsubscribe";
2
- import { useMemo, useEffect } from "react";
2
+ import { useMemo, useEffect, useRef } from "react";
3
3
  import { useResource, resource, withKey } from "@assistant-ui/tap";
4
4
  import type { ClientOutput } from "@assistant-ui/store";
5
5
  import { useAssistantEmit, useClientLookup } from "@assistant-ui/store/client";
@@ -59,28 +59,48 @@ const useComposerClient = ({
59
59
  threadIdRef,
60
60
  messageIdRef,
61
61
  runtime,
62
+ isSuggestion,
62
63
  }: {
63
64
  threadIdRef: { current: string };
64
65
  messageIdRef?: { current: string };
65
66
  runtime: ComposerRuntime;
67
+ isSuggestion?: ((text: string) => boolean) | undefined;
66
68
  }): ClientOutput<"composer"> => {
67
69
  const runtimeState = useSubscribable(runtime);
68
70
  const emit = useAssistantEmit();
71
+ const pendingSuggestion = useRef(false);
69
72
 
70
73
  // Bind composer events to event manager
71
74
  useEffect(() => {
72
75
  const unsubscribers: Unsubscribe[] = [];
73
76
 
74
77
  // Subscribe to composer events
75
- for (const event of ["send", "attachmentAdd"] as const) {
76
- const unsubscribe = runtime.unstable_on(event, () => {
77
- emit(`composer.${event}`, {
78
+ const sendUnsubscribe = runtime.unstable_on("send", (payload) => {
79
+ const suggestion = pendingSuggestion.current;
80
+ pendingSuggestion.current = false;
81
+ emit("composer.send", {
82
+ threadId: threadIdRef.current,
83
+ ...(messageIdRef && { messageId: messageIdRef.current }),
84
+ chars: payload.chars,
85
+ attachments: payload.attachments,
86
+ ...(suggestion ? { suggestion: true } : undefined),
87
+ });
88
+ });
89
+ unsubscribers.push(sendUnsubscribe);
90
+
91
+ const attachmentUnsubscribe = runtime.unstable_on(
92
+ "attachmentAdd",
93
+ (payload) => {
94
+ emit("composer.attachmentAdd", {
78
95
  threadId: threadIdRef.current,
79
96
  ...(messageIdRef && { messageId: messageIdRef.current }),
97
+ ...(payload.contentType
98
+ ? { contentType: payload.contentType }
99
+ : undefined),
80
100
  });
81
- });
82
- unsubscribers.push(unsubscribe);
83
- }
101
+ },
102
+ );
103
+ unsubscribers.push(attachmentUnsubscribe);
84
104
 
85
105
  unsubscribers.push(
86
106
  runtime.unstable_on("attachmentAddError", (payload) => {
@@ -91,6 +111,9 @@ const useComposerClient = ({
91
111
  ...(payload.attachmentId && { attachmentId: payload.attachmentId }),
92
112
  reason: payload.reason,
93
113
  message: payload.message,
114
+ ...(payload.contentType
115
+ ? { contentType: payload.contentType }
116
+ : undefined),
94
117
  });
95
118
  }),
96
119
  );
@@ -153,8 +176,18 @@ const useComposerClient = ({
153
176
  addAttachment: runtime.addAttachment,
154
177
  reset: runtime.reset,
155
178
  clearAttachments: runtime.clearAttachments,
156
- send: runtime.send,
157
- cancel: runtime.cancel,
179
+ send: (options) => {
180
+ const state = runtime.getState();
181
+ pendingSuggestion.current =
182
+ state.canSend && (isSuggestion?.(state.text) ?? false);
183
+ runtime.send(options);
184
+ },
185
+ cancel: () => {
186
+ if (!messageIdRef && runtime.getState().canCancel) {
187
+ emit("composer.cancel", { threadId: threadIdRef.current });
188
+ }
189
+ runtime.cancel();
190
+ },
158
191
  beginEdit:
159
192
  (runtime as EditComposerRuntime).beginEdit ??
160
193
  (() => {
@@ -1,7 +1,11 @@
1
- import { useMemo, useState } from "react";
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import { useResource, withKey, resource } from "@assistant-ui/tap";
3
3
  import type { ClientOutput } from "@assistant-ui/store";
4
- import { useClientLookup, useClientResource } from "@assistant-ui/store/client";
4
+ import {
5
+ useAssistantEmit,
6
+ useClientLookup,
7
+ useClientResource,
8
+ } from "@assistant-ui/store/client";
5
9
  import type { MessageRuntime } from "../../runtime/api/message-runtime";
6
10
  import { useSubscribable } from "./useSubscribable";
7
11
  import { liveRef } from "./liveRef";
@@ -47,11 +51,14 @@ const MessagePartByIndex = resource(useMessagePartByIndex);
47
51
  const useMessageClient = ({
48
52
  runtime,
49
53
  threadIdRef,
54
+ threadId,
50
55
  }: {
51
56
  runtime: MessageRuntime;
52
57
  threadIdRef: { current: string };
58
+ threadId: string;
53
59
  }): ClientOutput<"message"> => {
54
60
  const runtimeState = useSubscribable(runtime);
61
+ const emit = useAssistantEmit();
55
62
 
56
63
  const [isCopiedState, setIsCopied] = useState(false);
57
64
  const [isHoveringState, setIsHovering] = useState(false);
@@ -60,6 +67,33 @@ const useMessageClient = ({
60
67
  () => liveRef(() => runtime.getState().id),
61
68
  [runtime],
62
69
  );
70
+ const previousStatus = useRef(runtimeState.status);
71
+ const emitMessageEvent = (
72
+ event:
73
+ | "message.reload"
74
+ | "message.speak"
75
+ | "message.branchSwitched"
76
+ | "message.copied",
77
+ ) => {
78
+ emit(event, { threadId, messageId: runtime.getState().id });
79
+ };
80
+
81
+ useEffect(() => {
82
+ const status = runtimeState.status;
83
+ const previous = previousStatus.current;
84
+ previousStatus.current = status;
85
+ if (
86
+ status?.type === "incomplete" &&
87
+ status.reason === "error" &&
88
+ (previous?.type !== "incomplete" || previous.reason !== "error")
89
+ ) {
90
+ emit("message.error", {
91
+ threadId,
92
+ messageId: runtimeState.id,
93
+ reason: "error",
94
+ });
95
+ }
96
+ }, [runtimeState.status, runtimeState.id, emit, threadId]);
63
97
 
64
98
  const composer = useClientResource(
65
99
  ComposerClient({
@@ -114,11 +148,20 @@ const useMessageClient = ({
114
148
  composer: () => composer.methods,
115
149
 
116
150
  delete: () => runtime.delete(),
117
- reload: (config) => runtime.reload(config),
118
- speak: () => runtime.speak(),
151
+ reload: (config) => {
152
+ emitMessageEvent("message.reload");
153
+ return runtime.reload(config);
154
+ },
155
+ speak: () => {
156
+ emitMessageEvent("message.speak");
157
+ return runtime.speak();
158
+ },
119
159
  stopSpeaking: () => runtime.stopSpeaking(),
120
160
  submitFeedback: (feedback) => runtime.submitFeedback(feedback),
121
- switchToBranch: (options) => runtime.switchToBranch(options),
161
+ switchToBranch: (options) => {
162
+ emitMessageEvent("message.branchSwitched");
163
+ return runtime.switchToBranch(options);
164
+ },
122
165
  getCopyText: () => runtime.unstable_getCopyText(),
123
166
  part: (selector) => {
124
167
  if ("index" in selector) {
@@ -136,7 +179,12 @@ const useMessageClient = ({
136
179
  }
137
180
  },
138
181
 
139
- setIsCopied,
182
+ setIsCopied: (value) => {
183
+ if (value) {
184
+ emitMessageEvent("message.copied");
185
+ }
186
+ setIsCopied(value);
187
+ },
140
188
  setIsHovering,
141
189
 
142
190
  __internal_getRuntime: () => runtime,
@@ -52,8 +52,10 @@ const useThreadListItemClient = ({
52
52
  unarchive: () =>
53
53
  handleThreadListAction("unarchive", () => runtime.unarchive()),
54
54
  delete: () => handleThreadListAction("delete", () => runtime.delete()),
55
- generateTitle: () =>
56
- handleThreadListAction("generate title", () => runtime.generateTitle()),
55
+ generateTitle: (options) =>
56
+ handleThreadListAction("generate title", () =>
57
+ runtime.generateTitle(options),
58
+ ),
57
59
  initialize: runtime.initialize,
58
60
  detach: runtime.detach,
59
61
  __internal_getRuntime: () => runtime,
@@ -82,6 +82,7 @@ const useThreadListClient = ({
82
82
  mainThreadId: runtimeState.mainThreadId,
83
83
  newThreadId: runtimeState.newThreadId ?? null,
84
84
  isLoading: runtimeState.isLoading,
85
+ loadError: runtimeState.loadError,
85
86
  isLoadingMore: runtimeState.isLoadingMore,
86
87
  hasMore: runtimeState.hasMore,
87
88
  threadIds: runtimeState.threadIds,
@@ -1,7 +1,10 @@
1
1
  import type { Unsubscribe } from "../../types/unsubscribe";
2
2
  import type { ThreadRuntimeEventType } from "../../runtime/interfaces/thread-runtime-core";
3
- import type { ThreadRuntime } from "../../runtime/api/thread-runtime";
4
- import { useMemo, useEffect, type RefObject } from "react";
3
+ import type {
4
+ CreateAppendMessage,
5
+ ThreadRuntime,
6
+ } from "../../runtime/api/thread-runtime";
7
+ import { useMemo, useEffect, useCallback, type RefObject } from "react";
5
8
  import { useResource, resource, withKey } from "@assistant-ui/tap";
6
9
  import { liveRef } from "./liveRef";
7
10
  import type { ClientOutput } from "@assistant-ui/store";
@@ -20,17 +23,21 @@ const useMessageClientById = ({
20
23
  runtime,
21
24
  id,
22
25
  threadIdRef,
26
+ threadId,
23
27
  }: {
24
28
  runtime: ThreadRuntime;
25
29
  id: string;
26
30
  threadIdRef: RefObject<string>;
31
+ threadId: string;
27
32
  }) => {
28
33
  const messageRuntime = useMemo(
29
34
  () => runtime.getMessageById(id),
30
35
  [runtime, id],
31
36
  );
32
37
 
33
- return useResource(MessageClient({ runtime: messageRuntime, threadIdRef }));
38
+ return useResource(
39
+ MessageClient({ runtime: messageRuntime, threadIdRef, threadId }),
40
+ );
34
41
  };
35
42
 
36
43
  const MessageClientById = resource(useMessageClientById);
@@ -72,11 +79,24 @@ const useThreadClient = ({
72
79
  () => liveRef(() => runtime.getState()!.threadId),
73
80
  [runtime],
74
81
  );
82
+ const emitThreadEvent = (
83
+ event: "thread.cancelRun" | "thread.voiceStarted",
84
+ ) => {
85
+ emit(event, { threadId: runtime.getState()!.threadId });
86
+ };
87
+ const isSuggestion = useCallback(
88
+ (text: string) =>
89
+ runtime
90
+ .getState()!
91
+ .suggestions.some((suggestion) => suggestion.prompt === text),
92
+ [runtime],
93
+ );
75
94
 
76
95
  const composer = useClientResource(
77
96
  ComposerClient({
78
97
  runtime: runtime.composer,
79
98
  threadIdRef,
99
+ isSuggestion,
80
100
  }),
81
101
  );
82
102
  const suggestions = useClientResource(
@@ -84,11 +104,16 @@ const useThreadClient = ({
84
104
  );
85
105
  const messages = useClientLookup(
86
106
  runtimeState.messages.map((m) =>
87
- withKey(m.id, MessageClientById({ runtime, id: m.id, threadIdRef }), [
88
- runtime,
107
+ withKey(
89
108
  m.id,
90
- threadIdRef,
91
- ]),
109
+ MessageClientById({
110
+ runtime,
111
+ id: m.id,
112
+ threadIdRef,
113
+ threadId: runtimeState.threadId,
114
+ }),
115
+ [runtime, m.id, threadIdRef, runtimeState.threadId],
116
+ ),
92
117
  ),
93
118
  );
94
119
 
@@ -114,18 +139,41 @@ const useThreadClient = ({
114
139
  getState: () => state,
115
140
  composer: () => composer.methods,
116
141
  suggestions: () => suggestions.methods,
117
- append: runtime.append,
142
+ append: (message) => {
143
+ const appended: Exclude<CreateAppendMessage, string> =
144
+ typeof message === "string"
145
+ ? { content: [{ type: "text", text: message }] }
146
+ : message;
147
+ if ((appended.role ?? "user") === "user") {
148
+ const text = appended.content
149
+ .map((part) => (part.type === "text" ? part.text : ""))
150
+ .join("");
151
+ emit("composer.send", {
152
+ threadId: runtime.getState()!.threadId,
153
+ chars: text.length,
154
+ attachments: appended.attachments?.length ?? 0,
155
+ ...(isSuggestion(text) ? { suggestion: true } : undefined),
156
+ });
157
+ }
158
+ runtime.append(message);
159
+ },
118
160
  deleteMessage: runtime.deleteMessage,
119
161
  startRun: runtime.startRun,
120
162
  resumeRun: runtime.resumeRun,
121
163
  importExternalState: runtime.importExternalState,
122
- cancelRun: runtime.cancelRun,
164
+ cancelRun: () => {
165
+ if (runtimeState.isRunning) emitThreadEvent("thread.cancelRun");
166
+ runtime.cancelRun();
167
+ },
123
168
  getModelContext: runtime.getModelContext,
124
169
  export: runtime.export,
125
170
  import: runtime.import,
126
171
  reset: runtime.reset,
127
172
  stopSpeaking: runtime.stopSpeaking,
128
- connectVoice: runtime.connectVoice,
173
+ connectVoice: () => {
174
+ runtime.connectVoice();
175
+ emitThreadEvent("thread.voiceStarted");
176
+ },
129
177
  disconnectVoice: runtime.disconnectVoice,
130
178
  getVoiceVolume: runtime.getVoiceVolume,
131
179
  subscribeVoiceVolume: runtime.subscribeVoiceVolume,
@@ -109,22 +109,34 @@ export type ComposerMeta = {
109
109
 
110
110
  export type ComposerEvents = {
111
111
  /**
112
- * The user sent the composer contents. `messageId` is set when the send
113
- * came from an edit composer.
112
+ * The user sent a message, from the composer or through `thread.append`.
113
+ * `messageId` is set when the send came from an edit composer.
114
114
  */
115
- "composer.send": { threadId: string; messageId?: string };
115
+ "composer.send": {
116
+ threadId: string;
117
+ messageId?: string;
118
+ chars: number;
119
+ attachments: number;
120
+ suggestion?: boolean;
121
+ };
116
122
  /**
117
123
  * An attachment was added to the composer. `messageId` is set when the
118
124
  * attachment was added to an edit composer.
119
125
  */
120
- "composer.attachmentAdd": { threadId: string; messageId?: string };
126
+ "composer.attachmentAdd": {
127
+ threadId: string;
128
+ messageId?: string;
129
+ contentType?: string;
130
+ };
121
131
  "composer.attachmentAddError": {
122
132
  threadId: string;
123
133
  messageId?: string;
124
134
  attachmentId?: string;
125
135
  reason: AttachmentAddErrorReason;
126
136
  message: string;
137
+ contentType?: string;
127
138
  };
139
+ "composer.cancel": { threadId: string };
128
140
  };
129
141
 
130
142
  export type ComposerClientSchema = {
@@ -66,7 +66,21 @@ export type MessageMeta = {
66
66
  query: { type: "id"; id: string } | { type: "index"; index: number };
67
67
  };
68
68
 
69
+ export type MessageEvents = {
70
+ /** The user asked for this assistant message to be generated again. */
71
+ "message.reload": { threadId: string; messageId: string };
72
+ /** The user moved to another branch at this message. */
73
+ "message.branchSwitched": { threadId: string; messageId: string };
74
+ /** The user copied this message. */
75
+ "message.copied": { threadId: string; messageId: string };
76
+ /** Speech synthesis started for this message. */
77
+ "message.speak": { threadId: string; messageId: string };
78
+ /** This message entered an error status. */
79
+ "message.error": { threadId: string; messageId: string; reason: "error" };
80
+ };
81
+
69
82
  export type MessageClientSchema = {
70
83
  methods: MessageMethods;
71
84
  meta: MessageMeta;
85
+ events: MessageEvents;
72
86
  };
@@ -1,4 +1,7 @@
1
- import type { ThreadListItemRuntime } from "../../runtime/api/thread-list-item-runtime";
1
+ import type {
2
+ ThreadListItemGenerateTitleOptions,
3
+ ThreadListItemRuntime,
4
+ } from "../../runtime/api/thread-list-item-runtime";
2
5
  import type { ThreadListItemStatus } from "../../runtime/interfaces/thread-list-runtime-core";
3
6
 
4
7
  export type ThreadListItemState = {
@@ -26,7 +29,7 @@ export type ThreadListItemMethods = {
26
29
  archive(): void;
27
30
  unarchive(): void;
28
31
  delete(): void;
29
- generateTitle(): void;
32
+ generateTitle(options?: ThreadListItemGenerateTitleOptions): void;
30
33
  initialize(): Promise<{ remoteId: string; externalId: string | undefined }>;
31
34
  detach(): void;
32
35
  __internal_getRuntime?(): ThreadListItemRuntime;
@@ -155,6 +155,10 @@ export type ThreadEvents = {
155
155
  * state.
156
156
  */
157
157
  "thread.runEnd": { threadId: string };
158
+ /** The user stopped the run in progress on this thread. */
159
+ "thread.cancelRun": { threadId: string };
160
+ /** The user started a voice session on this thread. */
161
+ "thread.voiceStarted": { threadId: string };
158
162
  /**
159
163
  * The thread transitioned from new to initialized. Fires before the first
160
164
  * message is added, so read thread state via `useAuiState` rather than
@@ -9,6 +9,7 @@ export type ThreadsState = {
9
9
  readonly mainThreadId: string;
10
10
  readonly newThreadId: string | null;
11
11
  readonly isLoading: boolean;
12
+ readonly loadError: unknown;
12
13
  readonly isLoadingMore: boolean;
13
14
  readonly hasMore: boolean;
14
15
  readonly threadIds: readonly string[];
@@ -240,6 +240,80 @@ describe("MessageRepository", () => {
240
240
  expect(messages2.map((m) => m.id)).toEqual(["parent-id", "branch2-id"]);
241
241
  });
242
242
 
243
+ it("keeps a nested branch selected when revisiting its ancestor", () => {
244
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "r" }));
245
+ repository.addOrUpdateMessage("r", createTestMessage({ id: "a" }));
246
+ repository.addOrUpdateMessage("a", createTestMessage({ id: "x" }));
247
+ repository.addOrUpdateMessage("r", createTestMessage({ id: "b" }));
248
+ repository.addOrUpdateMessage("b", createTestMessage({ id: "y" }));
249
+
250
+ repository.switchToBranch("y");
251
+ expect(repository.getMessages().map((m) => m.id)).toEqual([
252
+ "r",
253
+ "b",
254
+ "y",
255
+ ]);
256
+
257
+ repository.switchToBranch("r");
258
+ expect(repository.getMessages().map((m) => m.id)).toEqual([
259
+ "r",
260
+ "b",
261
+ "y",
262
+ ]);
263
+
264
+ repository.switchToBranch("x");
265
+ repository.switchToBranch("r");
266
+ expect(repository.getMessages().map((m) => m.id)).toEqual([
267
+ "r",
268
+ "a",
269
+ "x",
270
+ ]);
271
+ });
272
+
273
+ it("keeps the selected root branch when deletion selects from the root", () => {
274
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "a" }));
275
+ repository.addOrUpdateMessage("a", createTestMessage({ id: "x" }));
276
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "b" }));
277
+ repository.addOrUpdateMessage("b", createTestMessage({ id: "y" }));
278
+
279
+ repository.switchToBranch("y");
280
+ repository.deleteMessage("y", null);
281
+
282
+ expect(repository.getMessages().map((m) => m.id)).toEqual(["b"]);
283
+ });
284
+
285
+ it("keeps the selected root branch after relinking a message", () => {
286
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "a" }));
287
+ repository.addOrUpdateMessage("a", createTestMessage({ id: "m" }));
288
+ repository.addOrUpdateMessage("m", createTestMessage({ id: "x" }));
289
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "b" }));
290
+ repository.addOrUpdateMessage("b", createTestMessage({ id: "m" }));
291
+
292
+ expect(repository.getMessages().map((m) => m.id)).toEqual([
293
+ "b",
294
+ "m",
295
+ "x",
296
+ ]);
297
+
298
+ repository.deleteMessage("x", null);
299
+
300
+ expect(repository.getMessages().map((m) => m.id)).toEqual(["b", "m"]);
301
+ });
302
+
303
+ it("keeps the selected branch when relinking an off-branch message", () => {
304
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "a" }));
305
+ repository.addOrUpdateMessage("a", createTestMessage({ id: "m" }));
306
+ repository.addOrUpdateMessage("a", createTestMessage({ id: "n" }));
307
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "b" }));
308
+ repository.addOrUpdateMessage("b", createTestMessage({ id: "n" }));
309
+
310
+ expect(repository.getMessages().map((m) => m.id)).toEqual(["a", "m"]);
311
+
312
+ repository.deleteMessage("m", null);
313
+
314
+ expect(repository.getMessages().map((m) => m.id)).toEqual(["a"]);
315
+ });
316
+
243
317
  it("should operate on a long message history without overflowing the stack", () => {
244
318
  const messageCount = 30_000;
245
319
  const messages = createLongBranchMessages(messageCount);
@@ -486,6 +560,31 @@ describe("MessageRepository", () => {
486
560
  ).toBe("parent-id");
487
561
  });
488
562
 
563
+ it("round-trips after reparenting under a later-added ancestor", () => {
564
+ const messageA = createTestMessage({ id: "A" });
565
+ repository.addOrUpdateMessage(null, messageA);
566
+ repository.addOrUpdateMessage("A", createTestMessage({ id: "B" }));
567
+ repository.addOrUpdateMessage("A", createTestMessage({ id: "C" }));
568
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "X" }));
569
+ repository.addOrUpdateMessage("X", messageA);
570
+
571
+ const exported = repository.export();
572
+ expect(exported.messages.map((m) => m.message.id)).toEqual([
573
+ "X",
574
+ "A",
575
+ "B",
576
+ "C",
577
+ ]);
578
+
579
+ const restored = new MessageRepository();
580
+ restored.import(exported);
581
+
582
+ expect(restored.headId).toBe("B");
583
+ expect(restored.getMessages().map((m) => m.id)).toEqual(["X", "A", "B"]);
584
+ restored.switchToBranch("C");
585
+ expect(restored.getMessages().map((m) => m.id)).toEqual(["X", "A", "C"]);
586
+ });
587
+
489
588
  it("should import repository state", () => {
490
589
  const parent = createTestMessage({ id: "parent-id" });
491
590
  const child = createTestMessage({ id: "child-id" });
@@ -941,5 +1040,22 @@ describe("MessageRepository", () => {
941
1040
 
942
1041
  expect(repository.headId).toBe("C");
943
1042
  });
1043
+
1044
+ it("should advance the head when re-parenting onto the current head", () => {
1045
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "a" }));
1046
+ repository.addOrUpdateMessage("a", createTestMessage({ id: "h" }));
1047
+ repository.addOrUpdateMessage(null, createTestMessage({ id: "b" }));
1048
+ repository.addOrUpdateMessage("b", createTestMessage({ id: "c" }));
1049
+
1050
+ repository.addOrUpdateMessage("h", createTestMessage({ id: "c" }));
1051
+
1052
+ expect(repository.getMessages().map((m) => m.id)).toEqual([
1053
+ "a",
1054
+ "h",
1055
+ "c",
1056
+ ]);
1057
+ expect(repository.headId).toBe("c");
1058
+ expect(repository.getMessage("c").parentId).toBe("h");
1059
+ });
944
1060
  });
945
1061
  });
@@ -22,6 +22,7 @@ import { deferred } from "./remote-thread-list-test-helpers";
22
22
 
23
23
  const EMPTY_STATE: RemoteThreadState = {
24
24
  isLoading: false,
25
+ loadError: undefined,
25
26
  isLoadingMore: false,
26
27
  cursor: undefined,
27
28
  newThreadId: undefined,
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ createCore,
4
+ deferred,
5
+ makeAdapter,
6
+ } from "./remote-thread-list-test-helpers";
7
+
8
+ type InitializeResult = { remoteId: string; externalId: string };
9
+
10
+ describe("RemoteThreadListThreadListRuntimeCore initialize", () => {
11
+ it("keeps the initialization task on the promoted slot when the adapter changes mid-flight", async () => {
12
+ const initializing = deferred<InitializeResult>();
13
+ const core = createCore(
14
+ makeAdapter({ initialize: vi.fn(() => initializing.promise) }),
15
+ );
16
+
17
+ await core.switchToNewThread();
18
+ const localId = core.newThreadId!;
19
+ const pending = core.initialize(localId);
20
+
21
+ // An adapter swap advances the generation without resetting the store, so
22
+ // the completion still applies its optimistic transform while `then`
23
+ // declines to reconcile against the retired adapter.
24
+ core.__internal_setOptions({
25
+ adapter: makeAdapter(),
26
+ runtimeHook: () => ({}) as never,
27
+ });
28
+
29
+ initializing.resolve({ remoteId: "remote-1", externalId: "external-1" });
30
+ await expect(pending).rejects.toThrow();
31
+
32
+ const item = core.getItemById(localId);
33
+ expect(item?.status).toBe("regular");
34
+ await expect(
35
+ item?.status === "new" ? undefined : item?.initializeTask,
36
+ ).resolves.toEqual({ remoteId: "remote-1", externalId: "external-1" });
37
+ });
38
+ });