@assistant-ui/core 0.2.22 → 0.3.0

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 (349) hide show
  1. package/dist/adapters/thread-history.d.ts +8 -0
  2. package/dist/adapters/thread-history.d.ts.map +1 -1
  3. package/dist/internal.d.ts +3 -2
  4. package/dist/internal.js +3 -2
  5. package/dist/model-context/frame/host.d.ts.map +1 -1
  6. package/dist/model-context/frame/host.js +36 -21
  7. package/dist/model-context/frame/host.js.map +1 -1
  8. package/dist/model-context/types.d.ts.map +1 -1
  9. package/dist/model-context/types.js +13 -4
  10. package/dist/model-context/types.js.map +1 -1
  11. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  12. package/dist/react/adapters/LocalStorageThreadListAdapter.js +3 -2
  13. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  14. package/dist/react/client/Tools.d.ts.map +1 -1
  15. package/dist/react/client/Tools.js +40 -55
  16. package/dist/react/client/Tools.js.map +1 -1
  17. package/dist/react/client/interactable-model-context.js +1 -0
  18. package/dist/react/client/interactable-model-context.js.map +1 -1
  19. package/dist/react/interactables-legacy/useAssistantInteractable.js +1 -1
  20. package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -1
  21. package/dist/react/interactables-legacy/useInteractableState.js +4 -3
  22. package/dist/react/interactables-legacy/useInteractableState.js.map +1 -1
  23. package/dist/react/model-context/useAssistantContext.js +1 -1
  24. package/dist/react/model-context/useAssistantContext.js.map +1 -1
  25. package/dist/react/model-context/useAssistantDataUI.js +1 -1
  26. package/dist/react/model-context/useAssistantDataUI.js.map +1 -1
  27. package/dist/react/model-context/useAssistantInstructions.js +1 -1
  28. package/dist/react/model-context/useAssistantInstructions.js.map +1 -1
  29. package/dist/react/model-context/useAssistantTool.js +29 -25
  30. package/dist/react/model-context/useAssistantTool.js.map +1 -1
  31. package/dist/react/model-context/useAssistantToolUI.js +1 -1
  32. package/dist/react/model-context/useAssistantToolUI.js.map +1 -1
  33. package/dist/react/model-context/useAuiToolOverrides.js +1 -1
  34. package/dist/react/model-context/useAuiToolOverrides.js.map +1 -1
  35. package/dist/react/model-context/useInteractable.js +1 -1
  36. package/dist/react/model-context/useInteractable.js.map +1 -1
  37. package/dist/react/model-context/useInteractableState.js +3 -2
  38. package/dist/react/model-context/useInteractableState.js.map +1 -1
  39. package/dist/react/model-context/useInteractableVersions.js +1 -0
  40. package/dist/react/model-context/useInteractableVersions.js.map +1 -1
  41. package/dist/react/model-context/useToolArgsStatus.js +1 -0
  42. package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
  43. package/dist/react/primitive-hooks/useActionBarCopy.js +4 -3
  44. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
  45. package/dist/react/primitive-hooks/useActionBarEdit.js +2 -1
  46. package/dist/react/primitive-hooks/useActionBarEdit.js.map +1 -1
  47. package/dist/react/primitive-hooks/useActionBarFeedback.js +3 -2
  48. package/dist/react/primitive-hooks/useActionBarFeedback.js.map +1 -1
  49. package/dist/react/primitive-hooks/useActionBarReload.js +2 -1
  50. package/dist/react/primitive-hooks/useActionBarReload.js.map +1 -1
  51. package/dist/react/primitive-hooks/useActionBarSpeak.js +2 -1
  52. package/dist/react/primitive-hooks/useActionBarSpeak.js.map +1 -1
  53. package/dist/react/primitive-hooks/useActionBarStopSpeaking.js +2 -1
  54. package/dist/react/primitive-hooks/useActionBarStopSpeaking.js.map +1 -1
  55. package/dist/react/primitive-hooks/useBranchPickerNext.js +2 -1
  56. package/dist/react/primitive-hooks/useBranchPickerNext.js.map +1 -1
  57. package/dist/react/primitive-hooks/useBranchPickerPrevious.js +2 -1
  58. package/dist/react/primitive-hooks/useBranchPickerPrevious.js.map +1 -1
  59. package/dist/react/primitive-hooks/useComposerAddAttachment.js +2 -1
  60. package/dist/react/primitive-hooks/useComposerAddAttachment.js.map +1 -1
  61. package/dist/react/primitive-hooks/useComposerCancel.js +2 -1
  62. package/dist/react/primitive-hooks/useComposerCancel.js.map +1 -1
  63. package/dist/react/primitive-hooks/useComposerDictate.js +2 -1
  64. package/dist/react/primitive-hooks/useComposerDictate.js.map +1 -1
  65. package/dist/react/primitive-hooks/useComposerSend.js +2 -1
  66. package/dist/react/primitive-hooks/useComposerSend.js.map +1 -1
  67. package/dist/react/primitive-hooks/useEditComposerCancel.js +2 -1
  68. package/dist/react/primitive-hooks/useEditComposerCancel.js.map +1 -1
  69. package/dist/react/primitive-hooks/useEditComposerSend.js +2 -1
  70. package/dist/react/primitive-hooks/useEditComposerSend.js.map +1 -1
  71. package/dist/react/primitive-hooks/useMessageBranching.js +3 -2
  72. package/dist/react/primitive-hooks/useMessageBranching.js.map +1 -1
  73. package/dist/react/primitive-hooks/useMessageReload.js +2 -1
  74. package/dist/react/primitive-hooks/useMessageReload.js.map +1 -1
  75. package/dist/react/primitive-hooks/useSuggestionTrigger.js +8 -7
  76. package/dist/react/primitive-hooks/useSuggestionTrigger.js.map +1 -1
  77. package/dist/react/primitive-hooks/useThreadListItemArchive.js +2 -1
  78. package/dist/react/primitive-hooks/useThreadListItemArchive.js.map +1 -1
  79. package/dist/react/primitive-hooks/useThreadListItemDelete.js +2 -1
  80. package/dist/react/primitive-hooks/useThreadListItemDelete.js.map +1 -1
  81. package/dist/react/primitive-hooks/useThreadListItemTrigger.js +2 -1
  82. package/dist/react/primitive-hooks/useThreadListItemTrigger.js.map +1 -1
  83. package/dist/react/primitive-hooks/useThreadListItemUnarchive.js +2 -1
  84. package/dist/react/primitive-hooks/useThreadListItemUnarchive.js.map +1 -1
  85. package/dist/react/primitive-hooks/useThreadListLoadMore.js +2 -1
  86. package/dist/react/primitive-hooks/useThreadListLoadMore.js.map +1 -1
  87. package/dist/react/primitive-hooks/useThreadListNew.js +2 -1
  88. package/dist/react/primitive-hooks/useThreadListNew.js.map +1 -1
  89. package/dist/react/primitive-hooks/useVoice.js +5 -13
  90. package/dist/react/primitive-hooks/useVoice.js.map +1 -1
  91. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +1 -1
  92. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js.map +1 -1
  93. package/dist/react/primitives/composer/ComposerAttachments.js +1 -1
  94. package/dist/react/primitives/composer/ComposerAttachments.js.map +1 -1
  95. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  96. package/dist/react/primitives/composer/ComposerQueue.js +1 -1
  97. package/dist/react/primitives/composer/ComposerQueue.js.map +1 -1
  98. package/dist/react/primitives/message/MessageAttachments.d.ts.map +1 -1
  99. package/dist/react/primitives/message/MessageAttachments.js +1 -1
  100. package/dist/react/primitives/message/MessageAttachments.js.map +1 -1
  101. package/dist/react/primitives/message/MessageParts.js +84 -122
  102. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  103. package/dist/react/primitives/thread/ThreadMessages.js +1 -1
  104. package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
  105. package/dist/react/primitives/thread/ThreadSuggestions.js +1 -1
  106. package/dist/react/primitives/thread/ThreadSuggestions.js.map +1 -1
  107. package/dist/react/primitives/threadList/ThreadListItems.js +1 -1
  108. package/dist/react/primitives/threadList/ThreadListItems.js.map +1 -1
  109. package/dist/react/providers/AttachmentByIndexProvider.js +2 -2
  110. package/dist/react/providers/AttachmentByIndexProvider.js.map +1 -1
  111. package/dist/react/providers/ChainOfThoughtByIndicesProvider.js +1 -1
  112. package/dist/react/providers/ChainOfThoughtByIndicesProvider.js.map +1 -1
  113. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.d.ts.map +1 -1
  114. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js +30 -18
  115. package/dist/react/providers/ChainOfThoughtPartByIndexProvider.js.map +1 -1
  116. package/dist/react/providers/MessageByIdProvider.js +2 -2
  117. package/dist/react/providers/MessageByIdProvider.js.map +1 -1
  118. package/dist/react/providers/MessageByIndexProvider.js +2 -2
  119. package/dist/react/providers/MessageByIndexProvider.js.map +1 -1
  120. package/dist/react/providers/PartByIndexProvider.d.ts.map +1 -1
  121. package/dist/react/providers/PartByIndexProvider.js +30 -18
  122. package/dist/react/providers/PartByIndexProvider.js.map +1 -1
  123. package/dist/react/providers/QueueItemByIndexProvider.js +1 -1
  124. package/dist/react/providers/QueueItemByIndexProvider.js.map +1 -1
  125. package/dist/react/providers/ReadonlyThreadProvider.js +1 -1
  126. package/dist/react/providers/ReadonlyThreadProvider.js.map +1 -1
  127. package/dist/react/providers/SuggestionByIndexProvider.js +1 -1
  128. package/dist/react/providers/SuggestionByIndexProvider.js.map +1 -1
  129. package/dist/react/providers/TextMessagePartProvider.js +1 -0
  130. package/dist/react/providers/TextMessagePartProvider.js.map +1 -1
  131. package/dist/react/providers/ThreadListItemByIndexProvider.js +1 -1
  132. package/dist/react/providers/ThreadListItemByIndexProvider.js.map +1 -1
  133. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +3 -3
  134. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  135. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  136. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +6 -4
  137. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  138. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +31 -1
  139. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  140. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +42 -20
  141. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  142. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts +1 -1
  143. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.d.ts.map +1 -1
  144. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js +1 -1
  145. package/dist/react/runtimes/cloud/CloudFileAttachmentAdapter.js.map +1 -1
  146. package/dist/react/runtimes/cloud/auiV0.d.ts +12 -1
  147. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  148. package/dist/react/runtimes/cloud/auiV0.js +2 -1
  149. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  150. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +75 -73
  151. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  152. package/dist/react/runtimes/createRuntimeExtras.js +1 -1
  153. package/dist/react/runtimes/createRuntimeExtras.js.map +1 -1
  154. package/dist/react/runtimes/external-message-converter.js +1 -1
  155. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  156. package/dist/react/runtimes/useExternalStoreSharedOptions.js +1 -0
  157. package/dist/react/runtimes/useExternalStoreSharedOptions.js.map +1 -1
  158. package/dist/react/runtimes/useLocalRuntime.d.ts +0 -1
  159. package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -1
  160. package/dist/react/types/scopes/tools.d.ts +0 -6
  161. package/dist/react/types/scopes/tools.d.ts.map +1 -1
  162. package/dist/react/utils/groupParts.d.ts +1 -3
  163. package/dist/react/utils/groupParts.d.ts.map +1 -1
  164. package/dist/react/utils/groupParts.js +1 -3
  165. package/dist/react/utils/groupParts.js.map +1 -1
  166. package/dist/runtime/api/assistant-runtime.d.ts +1 -1
  167. package/dist/runtime/api/assistant-runtime.d.ts.map +1 -1
  168. package/dist/runtime/api/assistant-runtime.js +1 -1
  169. package/dist/runtime/api/assistant-runtime.js.map +1 -1
  170. package/dist/runtime/api/attachment-runtime.d.ts +1 -1
  171. package/dist/runtime/api/attachment-runtime.d.ts.map +1 -1
  172. package/dist/runtime/api/attachment-runtime.js +1 -1
  173. package/dist/runtime/api/attachment-runtime.js.map +1 -1
  174. package/dist/runtime/api/composer-runtime.d.ts +2 -2
  175. package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
  176. package/dist/runtime/api/composer-runtime.js +2 -2
  177. package/dist/runtime/api/composer-runtime.js.map +1 -1
  178. package/dist/runtime/api/message-part-runtime.d.ts +4 -4
  179. package/dist/runtime/api/message-part-runtime.d.ts.map +1 -1
  180. package/dist/runtime/api/message-part-runtime.js +3 -3
  181. package/dist/runtime/api/message-part-runtime.js.map +1 -1
  182. package/dist/runtime/api/message-runtime.d.ts +1 -1
  183. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  184. package/dist/runtime/api/message-runtime.js +2 -2
  185. package/dist/runtime/api/message-runtime.js.map +1 -1
  186. package/dist/runtime/api/thread-list-item-runtime.d.ts +1 -1
  187. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  188. package/dist/runtime/api/thread-list-item-runtime.js +2 -2
  189. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  190. package/dist/runtime/api/thread-list-runtime.d.ts +1 -1
  191. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  192. package/dist/runtime/api/thread-list-runtime.js +1 -1
  193. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  194. package/dist/runtime/base/base-thread-runtime-core.d.ts +1 -1
  195. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  196. package/dist/runtime/base/base-thread-runtime-core.js +1 -1
  197. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  198. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts +2 -2
  199. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  200. package/dist/runtime/base/default-edit-composer-runtime-core.js +2 -2
  201. package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
  202. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
  203. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  204. package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -1
  205. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  206. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  207. package/dist/runtime/utils/message-repository.js +4 -2
  208. package/dist/runtime/utils/message-repository.js.map +1 -1
  209. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -1
  210. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  211. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +1 -1
  212. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  213. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  214. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +12 -0
  215. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  216. package/dist/runtimes/internal.d.ts +2 -1
  217. package/dist/runtimes/internal.js +2 -1
  218. package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -0
  219. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  220. package/dist/runtimes/local/local-thread-runtime-core.js +44 -13
  221. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  222. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts +32 -27
  223. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  224. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -0
  225. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  226. package/dist/store/clients/no-op-composer-client.js +1 -0
  227. package/dist/store/clients/no-op-composer-client.js.map +1 -1
  228. package/dist/store/clients/runtime-adapter.js +3 -3
  229. package/dist/store/clients/runtime-adapter.js.map +1 -1
  230. package/dist/store/runtime-clients/thread-list-runtime-client.js +1 -0
  231. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  232. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  233. package/dist/store/runtime-clients/thread-runtime-client.js +17 -15
  234. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  235. package/dist/store/scopes/thread.d.ts +1 -0
  236. package/dist/store/scopes/thread.d.ts.map +1 -1
  237. package/dist/subscribable/subscribable.d.ts +3 -3
  238. package/dist/subscribable/subscribable.d.ts.map +1 -1
  239. package/dist/subscribable/subscribable.js +3 -3
  240. package/dist/subscribable/subscribable.js.map +1 -1
  241. package/package.json +8 -8
  242. package/src/adapters/thread-history.ts +8 -0
  243. package/src/internal.ts +1 -1
  244. package/src/model-context/frame/host.test.ts +47 -0
  245. package/src/model-context/frame/host.ts +46 -19
  246. package/src/model-context/types.ts +16 -6
  247. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +17 -7
  248. package/src/react/client/Tools.ts +0 -7
  249. package/src/react/interactables-legacy/useAssistantInteractable.ts +1 -1
  250. package/src/react/interactables-legacy/useInteractableState.ts +3 -3
  251. package/src/react/model-context/useAssistantContext.ts +1 -1
  252. package/src/react/model-context/useAssistantDataUI.ts +1 -1
  253. package/src/react/model-context/useAssistantInstructions.ts +1 -1
  254. package/src/react/model-context/useAssistantTool.ts +2 -2
  255. package/src/react/model-context/useAssistantToolUI.ts +1 -1
  256. package/src/react/model-context/useAuiToolOverrides.ts +1 -1
  257. package/src/react/model-context/useInteractable.ts +1 -1
  258. package/src/react/model-context/useInteractableState.ts +2 -2
  259. package/src/react/primitive-hooks/useActionBarCopy.test.ts +2 -2
  260. package/src/react/primitive-hooks/useActionBarCopy.ts +3 -3
  261. package/src/react/primitive-hooks/useActionBarEdit.ts +1 -1
  262. package/src/react/primitive-hooks/useActionBarFeedback.ts +2 -2
  263. package/src/react/primitive-hooks/useActionBarReload.ts +1 -1
  264. package/src/react/primitive-hooks/useActionBarSpeak.ts +1 -1
  265. package/src/react/primitive-hooks/useActionBarStopSpeaking.ts +1 -1
  266. package/src/react/primitive-hooks/useBranchPickerNext.ts +1 -1
  267. package/src/react/primitive-hooks/useBranchPickerPrevious.ts +1 -1
  268. package/src/react/primitive-hooks/useComposerAddAttachment.ts +1 -1
  269. package/src/react/primitive-hooks/useComposerCancel.ts +1 -1
  270. package/src/react/primitive-hooks/useComposerDictate.ts +1 -1
  271. package/src/react/primitive-hooks/useComposerSend.ts +1 -1
  272. package/src/react/primitive-hooks/useEditComposerCancel.ts +1 -1
  273. package/src/react/primitive-hooks/useEditComposerSend.ts +1 -1
  274. package/src/react/primitive-hooks/useMessageBranching.ts +2 -2
  275. package/src/react/primitive-hooks/useMessageReload.ts +1 -1
  276. package/src/react/primitive-hooks/useSuggestionTrigger.ts +9 -9
  277. package/src/react/primitive-hooks/useThreadListItemArchive.ts +1 -1
  278. package/src/react/primitive-hooks/useThreadListItemDelete.ts +1 -1
  279. package/src/react/primitive-hooks/useThreadListItemTrigger.ts +1 -1
  280. package/src/react/primitive-hooks/useThreadListItemUnarchive.ts +1 -1
  281. package/src/react/primitive-hooks/useThreadListLoadMore.ts +1 -1
  282. package/src/react/primitive-hooks/useThreadListNew.ts +1 -1
  283. package/src/react/primitive-hooks/useVoice.ts +5 -5
  284. package/src/react/primitives/chainOfThought/ChainOfThoughtParts.tsx +1 -1
  285. package/src/react/primitives/composer/ComposerAttachments.tsx +1 -1
  286. package/src/react/primitives/composer/ComposerQueue.tsx +1 -3
  287. package/src/react/primitives/message/MessageAttachments.tsx +1 -3
  288. package/src/react/primitives/message/MessageParts.tsx +9 -9
  289. package/src/react/primitives/thread/ThreadMessages.tsx +1 -1
  290. package/src/react/primitives/thread/ThreadSuggestions.tsx +1 -1
  291. package/src/react/primitives/threadList/ThreadListItems.tsx +1 -1
  292. package/src/react/providers/AttachmentByIndexProvider.tsx +2 -2
  293. package/src/react/providers/ChainOfThoughtByIndicesProvider.tsx +1 -1
  294. package/src/react/providers/ChainOfThoughtPartByIndexProvider.tsx +2 -19
  295. package/src/react/providers/MessageByIdProvider.tsx +2 -2
  296. package/src/react/providers/MessageByIndexProvider.tsx +2 -2
  297. package/src/react/providers/PartByIndexProvider.tsx +2 -16
  298. package/src/react/providers/QueueItemByIndexProvider.tsx +1 -1
  299. package/src/react/providers/ReadonlyThreadProvider.tsx +1 -1
  300. package/src/react/providers/SuggestionByIndexProvider.tsx +1 -1
  301. package/src/react/providers/ThreadListItemByIndexProvider.tsx +1 -1
  302. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +3 -3
  303. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +20 -6
  304. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.ts +34 -0
  305. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +32 -7
  306. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +47 -16
  307. package/src/react/runtimes/cloud/CloudFileAttachmentAdapter.ts +5 -1
  308. package/src/react/runtimes/cloud/auiV0.ts +14 -0
  309. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +90 -88
  310. package/src/react/runtimes/createRuntimeExtras.test.ts +1 -1
  311. package/src/react/runtimes/createRuntimeExtras.ts +1 -1
  312. package/src/react/runtimes/external-message-converter.test.ts +346 -0
  313. package/src/react/runtimes/external-message-converter.ts +1 -1
  314. package/src/react/runtimes/useLocalRuntime.test.tsx +43 -0
  315. package/src/react/types/scopes/tools.ts +0 -6
  316. package/src/react/utils/groupParts.ts +1 -7
  317. package/src/runtime/api/assistant-runtime.ts +4 -1
  318. package/src/runtime/api/attachment-runtime.ts +8 -2
  319. package/src/runtime/api/composer-runtime.ts +9 -5
  320. package/src/runtime/api/message-part-runtime.ts +10 -3
  321. package/src/runtime/api/message-runtime.ts +7 -2
  322. package/src/runtime/api/thread-list-item-runtime.ts +7 -2
  323. package/src/runtime/api/thread-list-runtime.ts +10 -2
  324. package/src/runtime/base/base-thread-runtime-core.ts +5 -1
  325. package/src/runtime/base/default-edit-composer-runtime-core.ts +14 -2
  326. package/src/runtime/base/default-thread-composer-runtime-core.ts +11 -1
  327. package/src/runtime/utils/message-repository.ts +19 -13
  328. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +4 -1
  329. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +15 -0
  330. package/src/runtimes/internal.ts +3 -0
  331. package/src/runtimes/local/local-thread-runtime-core.test.ts +596 -0
  332. package/src/runtimes/local/local-thread-runtime-core.ts +85 -21
  333. package/src/runtimes/local/should-continue.test.ts +122 -0
  334. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +9 -10
  335. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +38 -32
  336. package/src/store/clients/model-context-client.test.ts +21 -1
  337. package/src/store/clients/runtime-adapter.ts +3 -3
  338. package/src/store/runtime-clients/thread-runtime-client.ts +2 -1
  339. package/src/store/scopes/thread.ts +1 -0
  340. package/src/subscribable/subscribable.ts +16 -8
  341. package/src/tests/MessageRepository.test.ts +12 -0
  342. package/src/tests/RemoteThreadListThreadListRuntimeCore-switchToThread-position.test.ts +121 -0
  343. package/src/tests/auiV0Encode.test.ts +74 -0
  344. package/src/tests/base-composer-runtime-core.test.ts +422 -0
  345. package/src/tests/external-store-thread-list-runtime-core.test.ts +6 -6
  346. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +575 -0
  347. package/src/tests/external-store-thread-runtime-core.test.ts +55 -1
  348. package/src/tests/groupParts.test.ts +3 -48
  349. package/src/tests/OptimisticState-delete-crash.test.ts +0 -208
@@ -0,0 +1,575 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import {
3
+ ExternalStoreThreadRuntimeCore,
4
+ hasUpcomingMessage,
5
+ } from "../runtimes/external-store/external-store-thread-runtime-core";
6
+ import type { ExternalStoreAdapter } from "../runtimes/external-store/external-store-adapter";
7
+ import type { ModelContextProvider } from "../model-context/types";
8
+ import type { AppendMessage, ThreadMessage } from "../types/message";
9
+
10
+ const createContextProvider = (): ModelContextProvider => ({
11
+ getModelContext: () => ({}),
12
+ });
13
+
14
+ const createUserMessage = (id: string, text = "Hello"): ThreadMessage =>
15
+ ({
16
+ id,
17
+ role: "user" as const,
18
+ createdAt: new Date(),
19
+ content: [{ type: "text" as const, text }],
20
+ attachments: [],
21
+ metadata: {
22
+ custom: {},
23
+ },
24
+ }) as ThreadMessage;
25
+
26
+ const createAssistantMessage = (id: string, text = "Hi there"): ThreadMessage =>
27
+ ({
28
+ id,
29
+ role: "assistant" as const,
30
+ createdAt: new Date(),
31
+ content: [{ type: "text" as const, text }],
32
+ status: { type: "complete" as const, reason: "stop" as const },
33
+ metadata: {
34
+ unstable_state: null,
35
+ unstable_annotations: [],
36
+ unstable_data: [],
37
+ steps: [],
38
+ custom: {},
39
+ },
40
+ }) as ThreadMessage;
41
+
42
+ const createBaseAdapter = (
43
+ overrides: Partial<ExternalStoreAdapter<ThreadMessage>> = {},
44
+ ): ExternalStoreAdapter<ThreadMessage> => ({
45
+ messages: [],
46
+ onNew: vi.fn(async () => {}),
47
+ ...overrides,
48
+ });
49
+
50
+ describe("ExternalStoreThreadRuntimeCore adapter contract", () => {
51
+ let contextProvider: ModelContextProvider;
52
+
53
+ beforeEach(() => {
54
+ contextProvider = createContextProvider();
55
+ });
56
+
57
+ it("hasUpcomingMessage is true only while running without an assistant tail", () => {
58
+ expect(hasUpcomingMessage(true, [createUserMessage("u1")])).toBe(true);
59
+ expect(hasUpcomingMessage(true, [createAssistantMessage("a1")])).toBe(
60
+ false,
61
+ );
62
+ expect(hasUpcomingMessage(false, [createUserMessage("u1")])).toBe(false);
63
+ expect(hasUpcomingMessage(true, [])).toBe(true);
64
+ });
65
+
66
+ it("derives capabilities from adapter handler presence", () => {
67
+ const bare = new ExternalStoreThreadRuntimeCore(
68
+ contextProvider,
69
+ createBaseAdapter(),
70
+ );
71
+ expect(bare.capabilities.edit).toBe(false);
72
+ expect(bare.capabilities.reload).toBe(false);
73
+ expect(bare.capabilities.cancel).toBe(false);
74
+ expect(bare.capabilities.switchToBranch).toBe(false);
75
+ expect(bare.capabilities.unstable_copy).toBe(true);
76
+
77
+ const full = new ExternalStoreThreadRuntimeCore(
78
+ contextProvider,
79
+ createBaseAdapter({
80
+ onEdit: vi.fn(async () => {}),
81
+ onReload: vi.fn(async () => {}),
82
+ onCancel: vi.fn(async () => {}),
83
+ setMessages: vi.fn(),
84
+ unstable_capabilities: { copy: false },
85
+ }),
86
+ );
87
+ expect(full.capabilities.edit).toBe(true);
88
+ expect(full.capabilities.reload).toBe(true);
89
+ expect(full.capabilities.cancel).toBe(true);
90
+ expect(full.capabilities.switchToBranch).toBe(true);
91
+ expect(full.capabilities.unstable_copy).toBe(false);
92
+ });
93
+
94
+ describe("append", () => {
95
+ it("calls onNew when parentId matches last message id", async () => {
96
+ const onNew = vi.fn(async () => {});
97
+ const messages = [createUserMessage("u1"), createAssistantMessage("a1")];
98
+ const adapter = createBaseAdapter({ messages, onNew });
99
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
100
+
101
+ const appendMessage: AppendMessage = {
102
+ role: "user",
103
+ content: [{ type: "text", text: "Follow up" }],
104
+ attachments: [],
105
+ createdAt: new Date(),
106
+ parentId: "a1",
107
+ sourceId: null,
108
+ runConfig: undefined,
109
+ metadata: { custom: {} },
110
+ } as AppendMessage;
111
+
112
+ await core.append(appendMessage);
113
+ expect(onNew).toHaveBeenCalledWith(appendMessage);
114
+ });
115
+
116
+ it("calls onEdit when parentId does not match last message id", async () => {
117
+ const onEdit = vi.fn(async () => {});
118
+ const onNew = vi.fn(async () => {});
119
+ const messages = [createUserMessage("u1"), createAssistantMessage("a1")];
120
+ const adapter = createBaseAdapter({ messages, onNew, onEdit });
121
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
122
+
123
+ const appendMessage: AppendMessage = {
124
+ role: "user",
125
+ content: [{ type: "text", text: "Edit" }],
126
+ attachments: [],
127
+ createdAt: new Date(),
128
+ parentId: "u1",
129
+ sourceId: null,
130
+ runConfig: undefined,
131
+ metadata: { custom: {} },
132
+ } as AppendMessage;
133
+
134
+ await core.append(appendMessage);
135
+ expect(onEdit).toHaveBeenCalledWith(appendMessage);
136
+ expect(onNew).not.toHaveBeenCalled();
137
+ });
138
+
139
+ it("throws when adapter has no onEdit and parentId differs from head", async () => {
140
+ const messages = [createUserMessage("u1"), createAssistantMessage("a1")];
141
+ const adapter = createBaseAdapter({ messages });
142
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
143
+
144
+ const appendMessage: AppendMessage = {
145
+ role: "user",
146
+ content: [{ type: "text", text: "Edit" }],
147
+ attachments: [],
148
+ createdAt: new Date(),
149
+ parentId: "u1",
150
+ sourceId: null,
151
+ runConfig: undefined,
152
+ metadata: { custom: {} },
153
+ } as AppendMessage;
154
+
155
+ await expect(core.append(appendMessage)).rejects.toThrow(
156
+ "Runtime does not support editing messages.",
157
+ );
158
+ });
159
+ });
160
+
161
+ describe("startRun", () => {
162
+ it("delegates to onReload", async () => {
163
+ const onReload = vi.fn(async () => {});
164
+ const adapter = createBaseAdapter({ onReload });
165
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
166
+
167
+ await core.startRun({ parentId: "msg-1", sourceId: null, runConfig: {} });
168
+ expect(onReload).toHaveBeenCalledWith("msg-1", {
169
+ parentId: "msg-1",
170
+ sourceId: null,
171
+ runConfig: {},
172
+ });
173
+ });
174
+
175
+ it("throws when adapter has no onReload", async () => {
176
+ const adapter = createBaseAdapter();
177
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
178
+
179
+ await expect(
180
+ core.startRun({ parentId: "msg-1", sourceId: null, runConfig: {} }),
181
+ ).rejects.toThrow("Runtime does not support reloading messages.");
182
+ });
183
+ });
184
+
185
+ describe("cancelRun", () => {
186
+ it("delegates to onCancel", () => {
187
+ const onCancel = vi.fn();
188
+ const messages = [createUserMessage("u1"), createAssistantMessage("a1")];
189
+ const adapter = createBaseAdapter({
190
+ messages,
191
+ onCancel,
192
+ setMessages: vi.fn(),
193
+ });
194
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
195
+
196
+ core.cancelRun();
197
+ expect(onCancel).toHaveBeenCalled();
198
+ });
199
+
200
+ it("throws when adapter has no onCancel", () => {
201
+ const adapter = createBaseAdapter();
202
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
203
+
204
+ expect(() => core.cancelRun()).toThrow(
205
+ "Runtime does not support cancelling runs.",
206
+ );
207
+ });
208
+
209
+ it("keeps a partially-streamed optimistic message on cancel", async () => {
210
+ const optimisticAssistant = {
211
+ ...createAssistantMessage("server-msg", "partial answer"),
212
+ status: { type: "running" as const },
213
+ metadata: {
214
+ unstable_state: null,
215
+ unstable_annotations: [],
216
+ unstable_data: [],
217
+ steps: [],
218
+ custom: {},
219
+ isOptimistic: true,
220
+ },
221
+ } as ThreadMessage;
222
+ const setMessages = vi.fn();
223
+ const adapter = createBaseAdapter({
224
+ messages: [createUserMessage("u1"), optimisticAssistant],
225
+ isRunning: true,
226
+ onCancel: vi.fn(),
227
+ setMessages,
228
+ });
229
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
230
+
231
+ core.cancelRun();
232
+
233
+ await new Promise((resolve) => setTimeout(resolve, 0));
234
+ const lastCall = setMessages.mock.lastCall?.[0] as ThreadMessage[];
235
+ expect(lastCall.map((m) => m.id)).toContain("server-msg");
236
+ });
237
+
238
+ it("evicts an empty optimistic head on cancel", async () => {
239
+ const optimisticAssistant = {
240
+ ...createAssistantMessage("server-msg", ""),
241
+ content: [],
242
+ status: { type: "running" as const },
243
+ metadata: {
244
+ unstable_state: null,
245
+ unstable_annotations: [],
246
+ unstable_data: [],
247
+ steps: [],
248
+ custom: {},
249
+ isOptimistic: true,
250
+ },
251
+ } as ThreadMessage;
252
+ const setMessages = vi.fn();
253
+ const adapter = createBaseAdapter({
254
+ messages: [createUserMessage("u1"), optimisticAssistant],
255
+ isRunning: true,
256
+ onCancel: vi.fn(),
257
+ setMessages,
258
+ });
259
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
260
+
261
+ core.cancelRun();
262
+
263
+ await new Promise((resolve) => setTimeout(resolve, 0));
264
+ const lastCall = setMessages.mock.lastCall?.[0] as ThreadMessage[];
265
+ expect(lastCall.map((m) => m.id)).not.toContain("server-msg");
266
+ });
267
+ });
268
+
269
+ describe("optimistic assistant message", () => {
270
+ it("adds optimistic assistant message when running with last user message", () => {
271
+ const userMsg = createUserMessage("u1");
272
+ const adapter = createBaseAdapter({
273
+ messages: [userMsg],
274
+ isRunning: true,
275
+ });
276
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
277
+
278
+ const messages = core.messages;
279
+ expect(messages.length).toBe(2);
280
+ expect(messages[0]!.id).toBe("u1");
281
+ expect(messages[1]!.role).toBe("assistant");
282
+ expect(messages[1]!.content).toEqual([]);
283
+ });
284
+
285
+ it("does not add optimistic message when last message is assistant", () => {
286
+ const adapter = createBaseAdapter({
287
+ messages: [createUserMessage("u1"), createAssistantMessage("a1")],
288
+ isRunning: true,
289
+ });
290
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
291
+
292
+ const messages = core.messages;
293
+ expect(messages.length).toBe(2);
294
+ expect(messages[1]!.role).toBe("assistant");
295
+ expect(messages[1]!.id).toBe("a1");
296
+ });
297
+
298
+ it("does not add optimistic message when not running", () => {
299
+ const adapter = createBaseAdapter({
300
+ messages: [createUserMessage("u1")],
301
+ isRunning: false,
302
+ });
303
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
304
+
305
+ expect(core.messages.length).toBe(1);
306
+ });
307
+ });
308
+
309
+ it("updates isDisabled from adapter", () => {
310
+ const adapter = createBaseAdapter({ isDisabled: true });
311
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
312
+ expect(core.isDisabled).toBe(true);
313
+
314
+ core.__internal_setAdapter(createBaseAdapter({ isDisabled: false }));
315
+ expect(core.isDisabled).toBe(false);
316
+ });
317
+
318
+ it("does not notify subscribers when the same adapter reference is passed", () => {
319
+ const adapter = createBaseAdapter({
320
+ messages: [createUserMessage("u1")],
321
+ });
322
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
323
+ const spy = vi.fn();
324
+ core.subscribe(spy);
325
+ spy.mockClear();
326
+
327
+ core.__internal_setAdapter(adapter);
328
+ expect(spy).not.toHaveBeenCalled();
329
+ });
330
+
331
+ it("notifies subscribers when adapter changes", () => {
332
+ const adapter1 = createBaseAdapter({
333
+ messages: [createUserMessage("u1")],
334
+ });
335
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter1);
336
+
337
+ const callback = vi.fn();
338
+ core.subscribe(callback);
339
+ callback.mockClear();
340
+
341
+ const adapter2 = createBaseAdapter({
342
+ messages: [createUserMessage("u1"), createAssistantMessage("a1")],
343
+ });
344
+ core.__internal_setAdapter(adapter2);
345
+ expect(callback).toHaveBeenCalled();
346
+ });
347
+
348
+ describe("switchToBranch", () => {
349
+ it("throws when setMessages is not provided", () => {
350
+ const adapter = createBaseAdapter({
351
+ messages: [createUserMessage("u1")],
352
+ });
353
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
354
+
355
+ expect(() => core.switchToBranch("u1")).toThrow(
356
+ "Runtime does not support switching branches.",
357
+ );
358
+ });
359
+
360
+ it("silently ignores branch switch while running", () => {
361
+ const setMessages = vi.fn();
362
+ const adapter = createBaseAdapter({
363
+ messages: [createUserMessage("u1"), createAssistantMessage("a1")],
364
+ isRunning: true,
365
+ setMessages,
366
+ });
367
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
368
+
369
+ core.switchToBranch("u1");
370
+ expect(setMessages).not.toHaveBeenCalled();
371
+ });
372
+ });
373
+
374
+ describe("exportExternalState and importExternalState", () => {
375
+ it("delegates export to adapter", () => {
376
+ const state = { key: "value" };
377
+ const adapter = createBaseAdapter({
378
+ onExportExternalState: vi.fn(() => state),
379
+ });
380
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
381
+
382
+ expect(core.exportExternalState()).toEqual(state);
383
+ });
384
+
385
+ it("throws on export when adapter has no onExportExternalState", () => {
386
+ const adapter = createBaseAdapter();
387
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
388
+
389
+ expect(() => core.exportExternalState()).toThrow(
390
+ "Runtime does not support exporting external states.",
391
+ );
392
+ });
393
+
394
+ it("delegates import to adapter", () => {
395
+ const onLoadExternalState = vi.fn();
396
+ const adapter = createBaseAdapter({ onLoadExternalState });
397
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
398
+
399
+ const state = { key: "value" };
400
+ core.importExternalState(state);
401
+ expect(onLoadExternalState).toHaveBeenCalledWith(state);
402
+ });
403
+
404
+ it("throws on import when adapter has no onLoadExternalState", () => {
405
+ const adapter = createBaseAdapter();
406
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
407
+
408
+ expect(() => core.importExternalState({})).toThrow(
409
+ "Runtime does not support importing external states.",
410
+ );
411
+ });
412
+ });
413
+
414
+ describe("unsupported adapter operations", () => {
415
+ it("beginEdit throws when adapter has no onEdit", () => {
416
+ const adapter = createBaseAdapter({
417
+ messages: [createUserMessage("u1")],
418
+ });
419
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
420
+
421
+ expect(() => core.beginEdit("u1")).toThrow(
422
+ "Runtime does not support editing.",
423
+ );
424
+ });
425
+
426
+ it("addToolResult throws when adapter has no onAddToolResult", () => {
427
+ const adapter = createBaseAdapter();
428
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
429
+
430
+ expect(() =>
431
+ core.addToolResult({
432
+ messageId: "m1",
433
+ toolName: "tool",
434
+ toolCallId: "tc1",
435
+ result: {},
436
+ isError: false,
437
+ }),
438
+ ).toThrow("Runtime does not support tool results.");
439
+ });
440
+
441
+ it("resumeRun throws when adapter has no onResume", async () => {
442
+ const adapter = createBaseAdapter();
443
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, adapter);
444
+
445
+ await expect(
446
+ core.resumeRun({ parentId: "msg-1" } as any),
447
+ ).rejects.toThrow("Runtime does not support resuming runs.");
448
+ });
449
+
450
+ it("throws when adapter has neither messages nor messageRepository", () => {
451
+ const adapter = {
452
+ onNew: vi.fn(async () => {}),
453
+ } as unknown as ExternalStoreAdapter<ThreadMessage>;
454
+
455
+ expect(
456
+ () => new ExternalStoreThreadRuntimeCore(contextProvider, adapter),
457
+ ).toThrow(
458
+ "ExternalStoreAdapter must provide either 'messages' or 'messageRepository'",
459
+ );
460
+ });
461
+ });
462
+
463
+ describe("messageRepository incremental sync", () => {
464
+ const u1 = createUserMessage("u1");
465
+ const a1 = createAssistantMessage("a1");
466
+
467
+ const repoAdapter = (
468
+ messages: { message: ThreadMessage; parentId: string | null }[],
469
+ headId: string | null,
470
+ overrides: Partial<ExternalStoreAdapter<ThreadMessage>> = {},
471
+ ): ExternalStoreAdapter<ThreadMessage> => ({
472
+ messageRepository: { headId, messages },
473
+ onNew: vi.fn(async () => {}),
474
+ ...overrides,
475
+ });
476
+
477
+ it("imports the initial repository", () => {
478
+ const core = new ExternalStoreThreadRuntimeCore(
479
+ contextProvider,
480
+ repoAdapter([{ message: u1, parentId: null }], "u1"),
481
+ );
482
+ expect(core.messages.map((m) => m.id)).toEqual(["u1"]);
483
+ });
484
+
485
+ it("adds a message when the repository is replaced", () => {
486
+ const core = new ExternalStoreThreadRuntimeCore(
487
+ contextProvider,
488
+ repoAdapter([{ message: u1, parentId: null }], "u1"),
489
+ );
490
+ core.__internal_setAdapter(
491
+ repoAdapter(
492
+ [
493
+ { message: u1, parentId: null },
494
+ { message: a1, parentId: "u1" },
495
+ ],
496
+ "a1",
497
+ ),
498
+ );
499
+ expect(core.messages.map((m) => m.id)).toEqual(["u1", "a1"]);
500
+ });
501
+
502
+ it("deletes a message no longer present in the new repository", () => {
503
+ const core = new ExternalStoreThreadRuntimeCore(
504
+ contextProvider,
505
+ repoAdapter(
506
+ [
507
+ { message: u1, parentId: null },
508
+ { message: a1, parentId: "u1" },
509
+ ],
510
+ "a1",
511
+ ),
512
+ );
513
+ expect(core.messages.map((m) => m.id)).toEqual(["u1", "a1"]);
514
+
515
+ core.__internal_setAdapter(
516
+ repoAdapter([{ message: u1, parentId: null }], "u1"),
517
+ );
518
+ expect(core.messages.map((m) => m.id)).toEqual(["u1"]);
519
+ });
520
+
521
+ it("matches a fresh import when built incrementally", () => {
522
+ const incremental = new ExternalStoreThreadRuntimeCore(
523
+ contextProvider,
524
+ repoAdapter([{ message: u1, parentId: null }], "u1"),
525
+ );
526
+ incremental.__internal_setAdapter(
527
+ repoAdapter(
528
+ [
529
+ { message: u1, parentId: null },
530
+ { message: a1, parentId: "u1" },
531
+ ],
532
+ "a1",
533
+ ),
534
+ );
535
+ const fresh = new ExternalStoreThreadRuntimeCore(
536
+ contextProvider,
537
+ repoAdapter(
538
+ [
539
+ { message: u1, parentId: null },
540
+ { message: a1, parentId: "u1" },
541
+ ],
542
+ "a1",
543
+ ),
544
+ );
545
+ expect(incremental.messages.map((m) => m.id)).toEqual(
546
+ fresh.messages.map((m) => m.id),
547
+ );
548
+ });
549
+
550
+ it("coalesces an isRunning flip on an unchanged repository reference", () => {
551
+ const messageRepository = {
552
+ headId: "u1",
553
+ messages: [{ message: u1, parentId: null }],
554
+ };
555
+ const onNew = vi.fn(async () => {});
556
+ const core = new ExternalStoreThreadRuntimeCore(contextProvider, {
557
+ messageRepository,
558
+ onNew,
559
+ isRunning: false,
560
+ });
561
+ expect(core.messages.map((m) => m.id)).toEqual(["u1"]);
562
+
563
+ core.__internal_setAdapter({ messageRepository, onNew, isRunning: true });
564
+ expect(core.messages.length).toBe(2);
565
+ expect(core.messages[1]!.role).toBe("assistant");
566
+
567
+ core.__internal_setAdapter({
568
+ messageRepository,
569
+ onNew,
570
+ isRunning: false,
571
+ });
572
+ expect(core.messages.map((m) => m.id)).toEqual(["u1"]);
573
+ });
574
+ });
575
+ });
@@ -343,7 +343,7 @@ describe("ExternalStoreThreadRuntimeCore - optimistic message reconciliation", (
343
343
  );
344
344
 
345
345
  // Simulates onEdit/onReload producing a new branch under the same parent;
346
- // the prior branch must survive (regression guard for #4131).
346
+ // the prior branch must survive.
347
347
  runtime.__internal_setAdapter(
348
348
  makeStore({
349
349
  messages: [u, { id: "a2", role: "assistant", text: "second" }],
@@ -353,6 +353,60 @@ describe("ExternalStoreThreadRuntimeCore - optimistic message reconciliation", (
353
353
 
354
354
  expect(childrenOf(runtime, "u")).toEqual(["a1", "a2"]);
355
355
  });
356
+
357
+ it("warns and keeps the last occurrence when the messages array repeats an id", () => {
358
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
359
+ try {
360
+ const runtime = new ExternalStoreThreadRuntimeCore(
361
+ mockContextProvider,
362
+ makeStore({
363
+ messages: [
364
+ { id: "u", role: "user", text: "hi" },
365
+ { id: "a", role: "assistant", text: "stale" },
366
+ { id: "a", role: "assistant", text: "fresh" },
367
+ ] satisfies Raw[],
368
+ convertMessage,
369
+ }),
370
+ );
371
+
372
+ expect(runtime.messages.map((m) => m.id)).toEqual(["u", "a"]);
373
+ expect(runtime.messages[1]!.content[0]).toMatchObject({
374
+ type: "text",
375
+ text: "fresh",
376
+ });
377
+ expect(warn).toHaveBeenCalledTimes(1);
378
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('"a"'));
379
+ } finally {
380
+ warn.mockRestore();
381
+ }
382
+ });
383
+
384
+ it("reparents the message after a dropped duplicate onto the preceding message", () => {
385
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
386
+ try {
387
+ const runtime = new ExternalStoreThreadRuntimeCore(
388
+ mockContextProvider,
389
+ makeStore({
390
+ messages: [
391
+ { id: "u", role: "user", text: "hi" },
392
+ { id: "dup", role: "assistant", text: "stale" },
393
+ { id: "u2", role: "user", text: "again" },
394
+ { id: "dup", role: "assistant", text: "fresh" },
395
+ ] satisfies Raw[],
396
+ convertMessage,
397
+ }),
398
+ );
399
+
400
+ expect(runtime.messages.map((m) => m.id)).toEqual(["u", "u2", "dup"]);
401
+ const parentOf = (id: string) =>
402
+ runtime.export().messages.find((m) => m.message.id === id)!.parentId;
403
+ expect(parentOf("u")).toBeNull();
404
+ expect(parentOf("u2")).toBe("u");
405
+ expect(parentOf("dup")).toBe("u2");
406
+ } finally {
407
+ warn.mockRestore();
408
+ }
409
+ });
356
410
  });
357
411
 
358
412
  describe("ExternalStoreThreadRuntimeCore - branch change callback", () => {