@assistant-ui/core 0.2.18 → 0.2.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 (228) hide show
  1. package/dist/index.d.ts +4 -2
  2. package/dist/index.js +3 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/internal.d.ts +2 -1
  5. package/dist/internal.js +2 -1
  6. package/dist/model-context/interactable-composer-metadata.d.ts +105 -0
  7. package/dist/model-context/interactable-composer-metadata.d.ts.map +1 -0
  8. package/dist/model-context/interactable-composer-metadata.js +259 -0
  9. package/dist/model-context/interactable-composer-metadata.js.map +1 -0
  10. package/dist/model-context/registry.d.ts.map +1 -1
  11. package/dist/model-context/registry.js +1 -0
  12. package/dist/model-context/registry.js.map +1 -1
  13. package/dist/model-context/types.d.ts +6 -0
  14. package/dist/model-context/types.d.ts.map +1 -1
  15. package/dist/model-context/types.js +4 -0
  16. package/dist/model-context/types.js.map +1 -1
  17. package/dist/react/AssistantProvider.d.ts +1 -1
  18. package/dist/react/AssistantProvider.js +1 -1
  19. package/dist/react/AssistantRuntimeProvider.d.ts +1 -1
  20. package/dist/react/RuntimeAdapter.js +1 -1
  21. package/dist/react/client/Interactables.d.ts +8 -2
  22. package/dist/react/client/Interactables.d.ts.map +1 -1
  23. package/dist/react/client/Interactables.js +190 -94
  24. package/dist/react/client/Interactables.js.map +1 -1
  25. package/dist/react/client/Tools.js +1 -1
  26. package/dist/react/client/interactable-model-context.d.ts +9 -6
  27. package/dist/react/client/interactable-model-context.d.ts.map +1 -1
  28. package/dist/react/client/interactable-model-context.js +154 -32
  29. package/dist/react/client/interactable-model-context.js.map +1 -1
  30. package/dist/react/index.d.ts +15 -6
  31. package/dist/react/index.js +12 -5
  32. package/dist/react/interactables-legacy/Interactables.d.ts +12 -0
  33. package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -0
  34. package/dist/react/interactables-legacy/Interactables.js +264 -0
  35. package/dist/react/interactables-legacy/Interactables.js.map +1 -0
  36. package/dist/react/interactables-legacy/interactable-model-context.d.ts +12 -0
  37. package/dist/react/interactables-legacy/interactable-model-context.d.ts.map +1 -0
  38. package/dist/react/interactables-legacy/interactable-model-context.js +54 -0
  39. package/dist/react/interactables-legacy/interactable-model-context.js.map +1 -0
  40. package/dist/react/interactables-legacy/scopes.d.ts +58 -0
  41. package/dist/react/interactables-legacy/scopes.d.ts.map +1 -0
  42. package/dist/react/interactables-legacy/scopes.js +0 -0
  43. package/dist/react/interactables-legacy/useAssistantInteractable.d.ts +31 -0
  44. package/dist/react/interactables-legacy/useAssistantInteractable.d.ts.map +1 -0
  45. package/dist/react/{model-context → interactables-legacy}/useAssistantInteractable.js +6 -2
  46. package/dist/react/interactables-legacy/useAssistantInteractable.js.map +1 -0
  47. package/dist/react/interactables-legacy/useInteractableState.d.ts +21 -0
  48. package/dist/react/interactables-legacy/useInteractableState.d.ts.map +1 -0
  49. package/dist/react/interactables-legacy/useInteractableState.js +84 -0
  50. package/dist/react/interactables-legacy/useInteractableState.js.map +1 -0
  51. package/dist/react/model-context/interactableTool.d.ts +53 -0
  52. package/dist/react/model-context/interactableTool.d.ts.map +1 -0
  53. package/dist/react/model-context/interactableTool.js +188 -0
  54. package/dist/react/model-context/interactableTool.js.map +1 -0
  55. package/dist/react/model-context/useAssistantContext.js +1 -1
  56. package/dist/react/model-context/useAssistantDataUI.js +1 -1
  57. package/dist/react/model-context/useAssistantInstructions.js +1 -1
  58. package/dist/react/model-context/useAssistantTool.js +1 -1
  59. package/dist/react/model-context/useAssistantToolUI.js +1 -1
  60. package/dist/react/model-context/useAuiToolOverrides.js +1 -1
  61. package/dist/react/model-context/useInteractable.d.ts +73 -0
  62. package/dist/react/model-context/useInteractable.d.ts.map +1 -0
  63. package/dist/react/model-context/useInteractable.js +100 -0
  64. package/dist/react/model-context/useInteractable.js.map +1 -0
  65. package/dist/react/model-context/useInteractableState.d.ts +8 -5
  66. package/dist/react/model-context/useInteractableState.d.ts.map +1 -1
  67. package/dist/react/model-context/useInteractableState.js +48 -48
  68. package/dist/react/model-context/useInteractableState.js.map +1 -1
  69. package/dist/react/model-context/useInteractableVersions.d.ts +19 -0
  70. package/dist/react/model-context/useInteractableVersions.d.ts.map +1 -0
  71. package/dist/react/model-context/useInteractableVersions.js +51 -0
  72. package/dist/react/model-context/useInteractableVersions.js.map +1 -0
  73. package/dist/react/primitive-hooks/useThreadMessageIds.d.ts +15 -0
  74. package/dist/react/primitive-hooks/useThreadMessageIds.d.ts.map +1 -0
  75. package/dist/react/primitive-hooks/useThreadMessageIds.js +26 -0
  76. package/dist/react/primitive-hooks/useThreadMessageIds.js.map +1 -0
  77. package/dist/react/primitive-hooks/useVoice.js +1 -1
  78. package/dist/react/primitives/chainOfThought/ChainOfThoughtParts.js +1 -1
  79. package/dist/react/primitives/composer/ComposerAttachments.js +1 -1
  80. package/dist/react/primitives/composer/ComposerQueue.js +1 -1
  81. package/dist/react/primitives/generativeUI/GenerativeUI.js +1 -1
  82. package/dist/react/primitives/message/MessageAttachments.js +1 -1
  83. package/dist/react/primitives/message/MessageGroupedParts.js +1 -1
  84. package/dist/react/primitives/message/MessageParts.js +1 -1
  85. package/dist/react/primitives/message/MessageQuote.js +1 -1
  86. package/dist/react/primitives/part/PartMessages.d.ts +5 -2
  87. package/dist/react/primitives/part/PartMessages.d.ts.map +1 -1
  88. package/dist/react/primitives/part/PartMessages.js +8 -8
  89. package/dist/react/primitives/part/PartMessages.js.map +1 -1
  90. package/dist/react/primitives/thread/ThreadMessages.d.ts +31 -1
  91. package/dist/react/primitives/thread/ThreadMessages.d.ts.map +1 -1
  92. package/dist/react/primitives/thread/ThreadMessages.js +64 -2
  93. package/dist/react/primitives/thread/ThreadMessages.js.map +1 -1
  94. package/dist/react/primitives/thread/ThreadSuggestions.js +1 -1
  95. package/dist/react/primitives/threadList/ThreadListItems.js +1 -1
  96. package/dist/react/providers/MessageByIdProvider.d.ts +9 -0
  97. package/dist/react/providers/MessageByIdProvider.d.ts.map +1 -0
  98. package/dist/react/providers/MessageByIdProvider.js +57 -0
  99. package/dist/react/providers/MessageByIdProvider.js.map +1 -0
  100. package/dist/react/providers/ReadonlyThreadProvider.js +1 -1
  101. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +1 -1
  102. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +3 -0
  103. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  104. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +16 -1
  105. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  106. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +1 -1
  107. package/dist/react/runtimes/cloud/auiV0.js +1 -1
  108. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +1 -1
  109. package/dist/react/runtimes/createRuntimeExtras.d.ts +20 -0
  110. package/dist/react/runtimes/createRuntimeExtras.d.ts.map +1 -0
  111. package/dist/react/runtimes/createRuntimeExtras.js +43 -0
  112. package/dist/react/runtimes/createRuntimeExtras.js.map +1 -0
  113. package/dist/react/runtimes/useLocalRuntime.js +1 -1
  114. package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
  115. package/dist/react/runtimes/useRemoteThreadListRuntime.js +6 -2
  116. package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
  117. package/dist/react/runtimes/useStreamingTiming.d.ts +26 -0
  118. package/dist/react/runtimes/useStreamingTiming.d.ts.map +1 -0
  119. package/dist/react/runtimes/useStreamingTiming.js +43 -0
  120. package/dist/react/runtimes/useStreamingTiming.js.map +1 -0
  121. package/dist/react/types/scopes/interactables.d.ts +40 -24
  122. package/dist/react/types/scopes/interactables.d.ts.map +1 -1
  123. package/dist/react/types/store-augmentation.d.ts +3 -1
  124. package/dist/react/types/store-augmentation.d.ts.map +1 -1
  125. package/dist/react/utils/useJSONEqual.d.ts +11 -0
  126. package/dist/react/utils/useJSONEqual.d.ts.map +1 -0
  127. package/dist/react/utils/useJSONEqual.js +30 -0
  128. package/dist/react/utils/useJSONEqual.js.map +1 -0
  129. package/dist/runtime/base/base-composer-runtime-core.d.ts +5 -0
  130. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  131. package/dist/runtime/base/base-composer-runtime-core.js +13 -0
  132. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  133. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  134. package/dist/runtime/base/default-edit-composer-runtime-core.js +6 -1
  135. package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
  136. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  137. package/dist/runtime/base/default-thread-composer-runtime-core.js +4 -1
  138. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  139. package/dist/runtime/utils/message-repository.d.ts +1 -0
  140. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  141. package/dist/runtime/utils/message-repository.js +6 -3
  142. package/dist/runtime/utils/message-repository.js.map +1 -1
  143. package/dist/runtime/utils/streaming-timing.d.ts +59 -0
  144. package/dist/runtime/utils/streaming-timing.d.ts.map +1 -0
  145. package/dist/runtime/utils/streaming-timing.js +76 -0
  146. package/dist/runtime/utils/streaming-timing.js.map +1 -0
  147. package/dist/runtime/utils/thread-message-like.js +2 -1
  148. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  149. package/dist/runtimes/external-store/external-store-adapter.d.ts +26 -1
  150. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  151. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +9 -0
  152. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  153. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +19 -0
  154. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  155. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  156. package/dist/runtimes/local/local-thread-runtime-core.js +1 -1
  157. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  158. package/dist/runtimes/remote-thread-list/types.d.ts +12 -0
  159. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  160. package/dist/store/clients/suggestions.js +1 -1
  161. package/dist/store/clients/thread-message-client.js +1 -1
  162. package/dist/store/runtime-clients/composer-runtime-client.js +1 -1
  163. package/dist/store/runtime-clients/message-runtime-client.js +1 -1
  164. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
  165. package/dist/store/runtime-clients/thread-runtime-client.js +1 -1
  166. package/dist/tests/remote-thread-list-test-helpers.d.ts +1 -1
  167. package/dist/tests/remote-thread-list-test-helpers.d.ts.map +1 -1
  168. package/dist/tests/remote-thread-list-test-helpers.js +3 -2
  169. package/dist/tests/remote-thread-list-test-helpers.js.map +1 -1
  170. package/package.json +9 -9
  171. package/src/index.ts +17 -0
  172. package/src/internal.ts +7 -0
  173. package/src/model-context/interactable-composer-metadata.test.ts +550 -0
  174. package/src/model-context/interactable-composer-metadata.ts +395 -0
  175. package/src/model-context/registry.ts +5 -0
  176. package/src/model-context/types.ts +12 -0
  177. package/src/react/client/Interactables.test.ts +358 -0
  178. package/src/react/client/Interactables.ts +339 -86
  179. package/src/react/client/interactable-model-context.test.ts +418 -0
  180. package/src/react/client/interactable-model-context.ts +252 -58
  181. package/src/react/index.ts +72 -3
  182. package/src/react/interactables-legacy/Interactables.ts +332 -0
  183. package/src/react/interactables-legacy/interactable-model-context.test.ts +54 -0
  184. package/src/react/interactables-legacy/interactable-model-context.ts +80 -0
  185. package/src/react/interactables-legacy/scopes.ts +66 -0
  186. package/src/react/{model-context → interactables-legacy}/useAssistantInteractable.ts +10 -1
  187. package/src/react/interactables-legacy/useInteractableState.ts +67 -0
  188. package/src/react/model-context/interactableTool.tsx +175 -0
  189. package/src/react/model-context/useInteractable.ts +233 -0
  190. package/src/react/model-context/useInteractableState.ts +31 -23
  191. package/src/react/model-context/useInteractableVersions.ts +48 -0
  192. package/src/react/primitive-hooks/useThreadMessageIds.ts +27 -0
  193. package/src/react/primitives/part/PartMessages.test.tsx +17 -0
  194. package/src/react/primitives/part/PartMessages.tsx +13 -8
  195. package/src/react/primitives/thread/ThreadMessages.tsx +67 -0
  196. package/src/react/providers/MessageByIdProvider.tsx +23 -0
  197. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +21 -1
  198. package/src/react/runtimes/createRuntimeExtras.test.ts +58 -0
  199. package/src/react/runtimes/createRuntimeExtras.ts +68 -0
  200. package/src/react/runtimes/createRuntimeExtras.use.test.ts +50 -0
  201. package/src/react/runtimes/useRemoteThreadListRuntime.ts +13 -1
  202. package/src/react/runtimes/useStreamingTiming.ts +68 -0
  203. package/src/react/types/scopes/interactables.ts +45 -23
  204. package/src/react/types/store-augmentation.ts +4 -2
  205. package/src/react/utils/useJSONEqual.ts +20 -0
  206. package/src/runtime/base/base-composer-runtime-core.ts +13 -0
  207. package/src/runtime/base/default-edit-composer-runtime-core.ts +18 -1
  208. package/src/runtime/base/default-thread-composer-runtime-core.ts +11 -1
  209. package/src/runtime/utils/message-repository.ts +11 -7
  210. package/src/runtime/utils/streaming-timing.test.ts +301 -0
  211. package/src/runtime/utils/streaming-timing.ts +161 -0
  212. package/src/runtime/utils/thread-message-like.ts +2 -1
  213. package/src/runtimes/external-store/external-store-adapter.ts +28 -0
  214. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +33 -1
  215. package/src/runtimes/local/local-thread-runtime-core.ts +4 -1
  216. package/src/runtimes/remote-thread-list/types.ts +13 -0
  217. package/src/tests/MessageRepository.test.ts +2 -0
  218. package/src/tests/composer-can-send.test.ts +1 -0
  219. package/src/tests/default-edit-composer-runtime-core.test.ts +78 -1
  220. package/src/tests/default-thread-composer-runtime-core.test.ts +82 -0
  221. package/src/tests/external-store-thread-runtime-core.test.ts +337 -0
  222. package/src/tests/groupParts.test.ts +32 -0
  223. package/src/tests/remote-thread-list-reactive-threadId.test.ts +90 -0
  224. package/src/tests/remote-thread-list-test-helpers.ts +2 -1
  225. package/src/tests/thread-message-like.test.ts +50 -0
  226. package/dist/react/model-context/useAssistantInteractable.d.ts +0 -22
  227. package/dist/react/model-context/useAssistantInteractable.d.ts.map +0 -1
  228. package/dist/react/model-context/useAssistantInteractable.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"default-thread-composer-runtime-core.js","names":["AppendMessage","AttachmentAdapter","DictationAdapter","SendOptions","ThreadComposerRuntimeCore","ThreadRuntimeCore","EMPTY_QUEUE_ITEMS","QueueItemState","BaseComposerRuntimeCore","DefaultThreadComposerRuntimeCore","_canCancel","canCancel","canSend","isEmpty","runtime","isSendDisabled","queue","getQueueItems","steerQueueItem","queueItemId","removeQueueItem","getAttachmentAdapter","adapters","attachments","getDictationAdapter","dictation","constructor","Omit","connect","lastIsSendDisabled","lastQueue","subscribe","changed","capabilities","cancel","_notifySubscribers","handleSend","message","options","append","parentId","messages","at","id","sourceId","startRun","steer","handleCancel","cancelRun"],"sources":["../../../src/runtime/base/default-thread-composer-runtime-core.ts"],"sourcesContent":["import type { AppendMessage } from \"../../types/message\";\nimport type { AttachmentAdapter } from \"../../adapters/attachment\";\nimport type { DictationAdapter } from \"../../adapters/speech\";\nimport type {\n SendOptions,\n ThreadComposerRuntimeCore,\n} from \"../interfaces/composer-runtime-core\";\nimport type { ThreadRuntimeCore } from \"../interfaces/thread-runtime-core\";\nimport {\n EMPTY_QUEUE_ITEMS,\n type QueueItemState,\n} from \"../../store/scopes/queue-item\";\nimport { BaseComposerRuntimeCore } from \"./base-composer-runtime-core\";\n\nexport class DefaultThreadComposerRuntimeCore\n extends BaseComposerRuntimeCore\n implements ThreadComposerRuntimeCore\n{\n private _canCancel = false;\n public get canCancel() {\n return this._canCancel;\n }\n\n public get canSend() {\n return !this.isEmpty && !this.runtime.isSendDisabled;\n }\n\n public override get queue(): readonly QueueItemState[] {\n return this.runtime.getQueueItems?.() ?? EMPTY_QUEUE_ITEMS;\n }\n\n public override steerQueueItem(queueItemId: string): void {\n this.runtime.steerQueueItem?.(queueItemId);\n }\n\n public override removeQueueItem(queueItemId: string): void {\n this.runtime.removeQueueItem?.(queueItemId);\n }\n\n protected getAttachmentAdapter() {\n return this.runtime.adapters?.attachments;\n }\n\n protected getDictationAdapter() {\n return this.runtime.adapters?.dictation;\n }\n\n constructor(\n private runtime: Omit<ThreadRuntimeCore, \"composer\"> & {\n adapters?:\n | {\n attachments?: AttachmentAdapter | undefined;\n dictation?: DictationAdapter | undefined;\n }\n | undefined;\n },\n ) {\n super();\n this.connect();\n }\n\n public connect() {\n let lastIsSendDisabled = this.runtime.isSendDisabled;\n let lastQueue = this.queue;\n return this.runtime.subscribe(() => {\n let changed = false;\n if (this.canCancel !== this.runtime.capabilities.cancel) {\n this._canCancel = this.runtime.capabilities.cancel;\n changed = true;\n }\n if (lastIsSendDisabled !== this.runtime.isSendDisabled) {\n lastIsSendDisabled = this.runtime.isSendDisabled;\n changed = true;\n }\n if (lastQueue !== this.queue) {\n lastQueue = this.queue;\n changed = true;\n }\n if (changed) this._notifySubscribers();\n });\n }\n\n public async handleSend(\n message: Omit<AppendMessage, \"parentId\" | \"sourceId\">,\n options?: SendOptions,\n ) {\n this.runtime.append({\n ...(message as AppendMessage),\n parentId: this.runtime.messages.at(-1)?.id ?? null,\n sourceId: null,\n startRun: options?.startRun,\n steer: options?.steer,\n });\n }\n\n public async handleCancel() {\n this.runtime.cancelRun();\n }\n}\n"],"mappings":";;;AAcA,IAAaS,mCAAb,cACUD,wBAEV;CA+BYM;CA9BV,aAAqB;CACrB,IAAWH,YAAY;EACrB,OAAO,KAAKD;CACd;CAEA,IAAWE,UAAU;EACnB,OAAO,CAAC,KAAKC,WAAW,CAAC,KAAKC,QAAQC;CACxC;CAEA,IAAoBC,QAAmC;EACrD,OAAO,KAAKF,QAAQG,gBAAgB,KAAKX;CAC3C;CAEA,eAA+Ba,aAA2B;EACxD,KAAKL,QAAQI,iBAAiBC,WAAW;CAC3C;CAEA,gBAAgCA,aAA2B;EACzD,KAAKL,QAAQM,kBAAkBD,WAAW;CAC5C;CAEA,uBAAiC;EAC/B,OAAO,KAAKL,QAAQQ,UAAUC;CAChC;CAEA,sBAAgC;EAC9B,OAAO,KAAKT,QAAQQ,UAAUG;CAChC;CAEAC,YACE,SAQA;EACA,MAAM;EATEZ,KAAAA,UAAAA;EAUR,KAAKc,QAAQ;CACf;CAEA,UAAiB;EACf,IAAIC,qBAAqB,KAAKf,QAAQC;EACtC,IAAIe,YAAY,KAAKd;EACrB,OAAO,KAAKF,QAAQiB,gBAAgB;GAClC,IAAIC,UAAU;GACd,IAAI,KAAKrB,cAAc,KAAKG,QAAQmB,aAAaC,QAAQ;IACvD,KAAKxB,aAAa,KAAKI,QAAQmB,aAAaC;IAC5CF,UAAU;GACZ;GACA,IAAIH,uBAAuB,KAAKf,QAAQC,gBAAgB;IACtDc,qBAAqB,KAAKf,QAAQC;IAClCiB,UAAU;GACZ;GACA,IAAIF,cAAc,KAAKd,OAAO;IAC5Bc,YAAY,KAAKd;IACjBgB,UAAU;GACZ;GACA,IAAIA,SAAS,KAAKG,mBAAmB;EACvC,CAAC;CACH;CAEA,MAAaC,WACXC,SACAC,SACA;EACA,KAAKxB,QAAQyB,OAAO;GAClB,GAAIF;GACJG,UAAU,KAAK1B,QAAQ2B,SAASC,GAAG,EAAE,CAAC,EAAEC,MAAM;GAC9CC,UAAU;GACVC,UAAUP,SAASO;GACnBC,OAAOR,SAASQ;EAClB,CAAC;CACH;CAEA,MAAaC,eAAe;EAC1B,KAAKjC,QAAQkC,UAAU;CACzB;AACF"}
1
+ {"version":3,"file":"default-thread-composer-runtime-core.js","names":["AppendMessage","AttachmentAdapter","DictationAdapter","SendOptions","ThreadComposerRuntimeCore","ThreadRuntimeCore","EMPTY_QUEUE_ITEMS","QueueItemState","BaseComposerRuntimeCore","gateInteractableComposerMetadata","DefaultThreadComposerRuntimeCore","_canCancel","canCancel","canSend","isEmpty","runtime","isSendDisabled","queue","getQueueItems","steerQueueItem","queueItemId","removeQueueItem","getAttachmentAdapter","adapters","attachments","getDictationAdapter","dictation","constructor","Omit","connect","lastIsSendDisabled","lastQueue","subscribe","changed","capabilities","cancel","_notifySubscribers","handleSend","message","options","composerMetadata","getModelContext","unstable_composerMetadata","messages","enriched","enrichWithComposerMetadata","append","parentId","at","id","sourceId","startRun","steer","handleCancel","cancelRun"],"sources":["../../../src/runtime/base/default-thread-composer-runtime-core.ts"],"sourcesContent":["import type { AppendMessage } from \"../../types/message\";\nimport type { AttachmentAdapter } from \"../../adapters/attachment\";\nimport type { DictationAdapter } from \"../../adapters/speech\";\nimport type {\n SendOptions,\n ThreadComposerRuntimeCore,\n} from \"../interfaces/composer-runtime-core\";\nimport type { ThreadRuntimeCore } from \"../interfaces/thread-runtime-core\";\nimport {\n EMPTY_QUEUE_ITEMS,\n type QueueItemState,\n} from \"../../store/scopes/queue-item\";\nimport { BaseComposerRuntimeCore } from \"./base-composer-runtime-core\";\nimport { gateInteractableComposerMetadata } from \"../../model-context/interactable-composer-metadata\";\n\nexport class DefaultThreadComposerRuntimeCore\n extends BaseComposerRuntimeCore\n implements ThreadComposerRuntimeCore\n{\n private _canCancel = false;\n public get canCancel() {\n return this._canCancel;\n }\n\n public get canSend() {\n return !this.isEmpty && !this.runtime.isSendDisabled;\n }\n\n public override get queue(): readonly QueueItemState[] {\n return this.runtime.getQueueItems?.() ?? EMPTY_QUEUE_ITEMS;\n }\n\n public override steerQueueItem(queueItemId: string): void {\n this.runtime.steerQueueItem?.(queueItemId);\n }\n\n public override removeQueueItem(queueItemId: string): void {\n this.runtime.removeQueueItem?.(queueItemId);\n }\n\n protected getAttachmentAdapter() {\n return this.runtime.adapters?.attachments;\n }\n\n protected getDictationAdapter() {\n return this.runtime.adapters?.dictation;\n }\n\n constructor(\n private runtime: Omit<ThreadRuntimeCore, \"composer\"> & {\n adapters?:\n | {\n attachments?: AttachmentAdapter | undefined;\n dictation?: DictationAdapter | undefined;\n }\n | undefined;\n },\n ) {\n super();\n this.connect();\n }\n\n public connect() {\n let lastIsSendDisabled = this.runtime.isSendDisabled;\n let lastQueue = this.queue;\n return this.runtime.subscribe(() => {\n let changed = false;\n if (this.canCancel !== this.runtime.capabilities.cancel) {\n this._canCancel = this.runtime.capabilities.cancel;\n changed = true;\n }\n if (lastIsSendDisabled !== this.runtime.isSendDisabled) {\n lastIsSendDisabled = this.runtime.isSendDisabled;\n changed = true;\n }\n if (lastQueue !== this.queue) {\n lastQueue = this.queue;\n changed = true;\n }\n if (changed) this._notifySubscribers();\n });\n }\n\n public async handleSend(\n message: Omit<AppendMessage, \"parentId\" | \"sourceId\">,\n options?: SendOptions,\n ) {\n // Merge provider-contributed metadata onto the outgoing user message\n // (same metadata.custom append path quotes ride). The interactables gate\n // runs here because it needs thread history, unavailable to the provider.\n const composerMetadata = gateInteractableComposerMetadata(\n this.runtime.getModelContext().unstable_composerMetadata,\n this.runtime.messages,\n );\n const enriched = this.enrichWithComposerMetadata(message, composerMetadata);\n\n this.runtime.append({\n ...(enriched as AppendMessage),\n parentId: this.runtime.messages.at(-1)?.id ?? null,\n sourceId: null,\n startRun: options?.startRun,\n steer: options?.steer,\n });\n }\n\n public async handleCancel() {\n this.runtime.cancelRun();\n }\n}\n"],"mappings":";;;;AAeA,IAAaU,mCAAb,cACUF,wBAEV;CA+BYO;CA9BV,aAAqB;CACrB,IAAWH,YAAY;EACrB,OAAO,KAAKD;CACd;CAEA,IAAWE,UAAU;EACnB,OAAO,CAAC,KAAKC,WAAW,CAAC,KAAKC,QAAQC;CACxC;CAEA,IAAoBC,QAAmC;EACrD,OAAO,KAAKF,QAAQG,gBAAgB,KAAKZ;CAC3C;CAEA,eAA+Bc,aAA2B;EACxD,KAAKL,QAAQI,iBAAiBC,WAAW;CAC3C;CAEA,gBAAgCA,aAA2B;EACzD,KAAKL,QAAQM,kBAAkBD,WAAW;CAC5C;CAEA,uBAAiC;EAC/B,OAAO,KAAKL,QAAQQ,UAAUC;CAChC;CAEA,sBAAgC;EAC9B,OAAO,KAAKT,QAAQQ,UAAUG;CAChC;CAEAC,YACE,SAQA;EACA,MAAM;EATEZ,KAAAA,UAAAA;EAUR,KAAKc,QAAQ;CACf;CAEA,UAAiB;EACf,IAAIC,qBAAqB,KAAKf,QAAQC;EACtC,IAAIe,YAAY,KAAKd;EACrB,OAAO,KAAKF,QAAQiB,gBAAgB;GAClC,IAAIC,UAAU;GACd,IAAI,KAAKrB,cAAc,KAAKG,QAAQmB,aAAaC,QAAQ;IACvD,KAAKxB,aAAa,KAAKI,QAAQmB,aAAaC;IAC5CF,UAAU;GACZ;GACA,IAAIH,uBAAuB,KAAKf,QAAQC,gBAAgB;IACtDc,qBAAqB,KAAKf,QAAQC;IAClCiB,UAAU;GACZ;GACA,IAAIF,cAAc,KAAKd,OAAO;IAC5Bc,YAAY,KAAKd;IACjBgB,UAAU;GACZ;GACA,IAAIA,SAAS,KAAKG,mBAAmB;EACvC,CAAC;CACH;CAEA,MAAaC,WACXC,SACAC,SACA;EAIA,MAAMC,mBAAmB/B,iCACvB,KAAKM,QAAQ0B,gBAAgB,CAAC,CAACC,2BAC/B,KAAK3B,QAAQ4B,QACf;EACA,MAAMC,WAAW,KAAKC,2BAA2BP,SAASE,gBAAgB;EAE1E,KAAKzB,QAAQ+B,OAAO;GAClB,GAAIF;GACJG,UAAU,KAAKhC,QAAQ4B,SAASK,GAAG,EAAE,CAAC,EAAEC,MAAM;GAC9CC,UAAU;GACVC,UAAUZ,SAASY;GACnBC,OAAOb,SAASa;EAClB,CAAC;CACH;CAEA,MAAaC,eAAe;EAC1B,KAAKtC,QAAQuC,UAAU;CACzB;AACF"}
@@ -32,6 +32,7 @@ declare class MessageRepository {
32
32
  private performOp;
33
33
  private _messages;
34
34
  get headId(): string | null;
35
+ get canonicalHeadId(): string | null;
35
36
  getMessages(headId?: string): readonly ThreadMessage[];
36
37
  addOrUpdateMessage(parentId: string | null, message: ThreadMessage): void;
37
38
  getMessage(messageId: string): {
@@ -1 +1 @@
1
- {"version":3,"file":"message-repository.d.ts","names":[],"sources":["../../../src/runtime/utils/message-repository.ts"],"mappings":";;;;KAOY,6BAAA;EACV,OAAA,EAAS,aAAA;EACT,QAAA;EACA,SAAA,GAAY,SAAS;AAAA;AAAA,KAGX,yBAAA;EACV,MAAA;EACA,QAAA,EAAU,KAAA;IACR,OAAA,EAAS,aAAA;IACT,QAAA;IACA,SAAA,GAAY,SAAA;EAAA;AAAA;AAAA,cAIH,yBAAA;iCAEU,iBAAA,OAClB,yBAAA;;IAmBC,OAAA,EAAS,iBAAA;IACT,QAAA;EAAA,KACC,OAAA;IACS,MAAA;EAAA,MACX,yBAAA;AAAA;AAAA,cAyDQ,iBAAA;EAAA,QACH,QAAA;EAAA,QACA,IAAA;EAAA,QACA,IAAA;EAAA,QAKA,YAAA;EAAA,QAUA,SAAA;EAAA,QAwDA,SAAA;EAAA,IAQJ,MAAA;EAIJ,WAAA,CAAY,MAAA,qBAAe,aAAA;EAuB3B,kBAAA,CAAmB,QAAA,iBAAyB,OAAA,EAAS,aAAA;EAiCrD,UAAA,CAAW,SAAA;;;;;EAcX,aAAA,CAAc,SAAA,UAAmB,aAAA;EAsCjC,WAAA,CAAY,SAAA;;;;;;;;;UAmBJ,gCAAA;EA+BR,cAAA,CAAe,SAAA;EAkBf,SAAA,CAAU,SAAA;EAgDV,KAAA;EAUA,MAAA,IAAU,yBAAA;EAyBV,MAAA;IAAS,MAAA;IAAQ;EAAA,GAAY,yBAAA;AAAA"}
1
+ {"version":3,"file":"message-repository.d.ts","names":[],"sources":["../../../src/runtime/utils/message-repository.ts"],"mappings":";;;;KAOY,6BAAA;EACV,OAAA,EAAS,aAAA;EACT,QAAA;EACA,SAAA,GAAY,SAAS;AAAA;AAAA,KAGX,yBAAA;EACV,MAAA;EACA,QAAA,EAAU,KAAA;IACR,OAAA,EAAS,aAAA;IACT,QAAA;IACA,SAAA,GAAY,SAAA;EAAA;AAAA;AAAA,cAIH,yBAAA;iCAEU,iBAAA,OAClB,yBAAA;;IAmBC,OAAA,EAAS,iBAAA;IACT,QAAA;EAAA,KACC,OAAA;IACS,MAAA;EAAA,MACX,yBAAA;AAAA;AAAA,cAyDQ,iBAAA;EAAA,QACH,QAAA;EAAA,QACA,IAAA;EAAA,QACA,IAAA;EAAA,QAKA,YAAA;EAAA,QAUA,SAAA;EAAA,QAwDA,SAAA;EAAA,IAQJ,MAAA;EAAA,IAIA,eAAA;EAUJ,WAAA,CAAY,MAAA,qBAAe,aAAA;EAuB3B,kBAAA,CAAmB,QAAA,iBAAyB,OAAA,EAAS,aAAA;EAiCrD,UAAA,CAAW,SAAA;;;;;EAcX,aAAA,CAAc,SAAA,UAAmB,aAAA;EAsCjC,WAAA,CAAY,SAAA;EA7RS;;;;;;;;EAAA,QAgTb,gCAAA;EA+BR,cAAA,CAAe,SAAA;EAkBf,SAAA,CAAU,SAAA;EAgDV,KAAA;EAUA,MAAA,IAAU,yBAAA;EAmBV,MAAA;IAAS,MAAA;IAAQ;EAAA,GAAY,yBAAA;AAAA"}
@@ -87,6 +87,11 @@ var MessageRepository = class {
87
87
  get headId() {
88
88
  return this.head?.current.id ?? null;
89
89
  }
90
+ get canonicalHeadId() {
91
+ let head = this.head;
92
+ while (head?.current.metadata?.isOptimistic) head = head.prev;
93
+ return head?.current.id ?? null;
94
+ }
90
95
  getMessages(headId) {
91
96
  if (headId === void 0 || headId === this.head?.current.id) return this._messages.value;
92
97
  const headMessage = this.messages.get(headId);
@@ -222,10 +227,8 @@ var MessageRepository = class {
222
227
  parentId: message.prev?.current.id ?? null
223
228
  });
224
229
  }
225
- let head = this.head;
226
- while (head?.current.metadata?.isOptimistic) head = head.prev;
227
230
  return {
228
- headId: head?.current.id ?? null,
231
+ headId: this.canonicalHeadId,
229
232
  messages: exportItems
230
233
  };
231
234
  }
@@ -1 +1 @@
1
- {"version":3,"file":"message-repository.js","names":["ThreadMessage","RunConfig","generateId","ThreadMessageLike","getAutoStatus","fromThreadMessageLike","ExportedMessageRepositoryItem","message","parentId","runConfig","ExportedMessageRepository","headId","messages","Array","fromArray","conv","map","m","undefined","idx","id","fromBranchableArray","items","options","fallbackStatus","Error","RepositoryParent","children","next","RepositoryMessage","prev","current","level","findHead","CachedValue","_value","T","constructor","func","value","dirty","MessageRepository","Map","head","root","updateLevels","newLevel","childId","childMessage","get","performOp","newParent","child","operation","parentOrRoot","newParentOrRoot","filter","fallbackId","at","fallback","_messages","getMessages","headMessage","addOrUpdateMessage","existingItem","newItem","set","getMessage","messageId","index","deleteMessage","replacementId","replacement","delete","getBranches","evictOffBranchOptimisticMessages","previousHead","currentHead","onHeadBranch","Set","add","stale","has","metadata","isOptimistic","push","switchToBranch","prevOrRoot","resetHead","clear","length","deleteDescendants","msg","export","exportItems","import"],"sources":["../../../src/runtime/utils/message-repository.ts"],"sourcesContent":["import type { ThreadMessage } from \"../../types/message\";\nimport type { RunConfig } from \"../../types/message\";\nimport { generateId } from \"../../utils/id\";\nimport type { ThreadMessageLike } from \"./thread-message-like\";\nimport { getAutoStatus } from \"./auto-status\";\nimport { fromThreadMessageLike } from \"./thread-message-like\";\n\nexport type ExportedMessageRepositoryItem = {\n message: ThreadMessage;\n parentId: string | null;\n runConfig?: RunConfig;\n};\n\nexport type ExportedMessageRepository = {\n headId?: string | null;\n messages: Array<{\n message: ThreadMessage;\n parentId: string | null;\n runConfig?: RunConfig;\n }>;\n};\n\nexport const ExportedMessageRepository = {\n fromArray: (\n messages: readonly ThreadMessageLike[],\n ): ExportedMessageRepository => {\n const conv = messages.map((m) =>\n fromThreadMessageLike(\n m,\n generateId(),\n getAutoStatus(false, false, false, false, undefined),\n ),\n );\n\n return {\n messages: conv.map((m, idx) => ({\n parentId: idx > 0 ? conv[idx - 1]!.id : null,\n message: m,\n })),\n };\n },\n\n fromBranchableArray: (\n items: readonly {\n message: ThreadMessageLike;\n parentId: string | null;\n }[],\n options?: { headId?: string | null },\n ): ExportedMessageRepository => {\n const fallbackStatus = getAutoStatus(false, false, false, false, undefined);\n return {\n ...(options?.headId !== undefined\n ? { headId: options.headId }\n : undefined),\n messages: items.map(({ message, parentId }) => {\n if (!message.id) {\n throw new Error(\n \"ExportedMessageRepository.fromBranchableArray: Each message must have an 'id' field set.\",\n );\n }\n return {\n parentId,\n message: fromThreadMessageLike(message, message.id, fallbackStatus),\n };\n }),\n };\n },\n};\n\ntype RepositoryParent = {\n children: string[];\n next: RepositoryMessage | null;\n};\n\ntype RepositoryMessage = RepositoryParent & {\n prev: RepositoryMessage | null;\n current: ThreadMessage;\n level: number;\n};\n\nconst findHead = (\n message: RepositoryMessage | RepositoryParent,\n): RepositoryMessage | null => {\n if (message.next) return findHead(message.next);\n if (\"current\" in message) return message;\n return null;\n};\n\nclass CachedValue<T> {\n private _value: T | null = null;\n\n constructor(private func: () => T) {}\n\n get value() {\n if (this._value === null) {\n this._value = this.func();\n }\n return this._value;\n }\n\n dirty() {\n this._value = null;\n }\n}\n\nexport class MessageRepository {\n private messages = new Map<string, RepositoryMessage>();\n private head: RepositoryMessage | null = null;\n private root: RepositoryParent = {\n children: [],\n next: null,\n };\n\n private updateLevels(message: RepositoryMessage, newLevel: number) {\n message.level = newLevel;\n for (const childId of message.children) {\n const childMessage = this.messages.get(childId);\n if (childMessage) {\n this.updateLevels(childMessage, newLevel + 1);\n }\n }\n }\n\n private performOp(\n newParent: RepositoryMessage | null,\n child: RepositoryMessage,\n operation: \"cut\" | \"link\" | \"relink\",\n ) {\n const parentOrRoot = child.prev ?? this.root;\n const newParentOrRoot = newParent ?? this.root;\n\n if (operation === \"relink\" && parentOrRoot === newParentOrRoot) return;\n\n if (operation !== \"link\") {\n parentOrRoot.children = parentOrRoot.children.filter(\n (m) => m !== child.current.id,\n );\n\n if (parentOrRoot.next === child) {\n const fallbackId = parentOrRoot.children.at(-1);\n const fallback = fallbackId ? this.messages.get(fallbackId) : null;\n if (fallback === undefined) {\n throw new Error(\n \"MessageRepository(performOp/cut): Fallback sibling message not found. This is likely an internal bug in assistant-ui.\",\n );\n }\n parentOrRoot.next = fallback;\n }\n }\n\n if (operation !== \"cut\") {\n for (\n let current: RepositoryMessage | null = newParent;\n current;\n current = current.prev\n ) {\n if (current.current.id === child.current.id) {\n throw new Error(\n \"MessageRepository(performOp/link): A message with the same id already exists in the parent tree. This error occurs if the same message id is found multiple times. This is likely an internal bug in assistant-ui.\",\n );\n }\n }\n\n newParentOrRoot.children = [\n ...newParentOrRoot.children,\n child.current.id,\n ];\n\n if (findHead(child) === this.head || newParentOrRoot.next === null) {\n newParentOrRoot.next = child;\n }\n\n child.prev = newParent;\n\n const newLevel = newParent ? newParent.level + 1 : 0;\n this.updateLevels(child, newLevel);\n }\n }\n\n private _messages = new CachedValue<readonly ThreadMessage[]>(() => {\n const messages = new Array<ThreadMessage>((this.head?.level ?? -1) + 1);\n for (let current = this.head; current; current = current.prev) {\n messages[current.level] = current.current;\n }\n return messages;\n });\n\n get headId() {\n return this.head?.current.id ?? null;\n }\n\n getMessages(headId?: string) {\n if (headId === undefined || headId === this.head?.current.id) {\n return this._messages.value;\n }\n\n const headMessage = this.messages.get(headId);\n if (!headMessage) {\n throw new Error(\n \"MessageRepository(getMessages): Head message not found. This is likely an internal bug in assistant-ui.\",\n );\n }\n\n const messages = new Array<ThreadMessage>(headMessage.level + 1);\n for (\n let current: RepositoryMessage | null = headMessage;\n current;\n current = current.prev\n ) {\n messages[current.level] = current.current;\n }\n return messages;\n }\n\n addOrUpdateMessage(parentId: string | null, message: ThreadMessage) {\n const existingItem = this.messages.get(message.id);\n const prev = parentId ? this.messages.get(parentId) : null;\n if (prev === undefined)\n throw new Error(\n \"MessageRepository(addOrUpdateMessage): Parent message not found. This is likely an internal bug in assistant-ui.\",\n );\n\n if (existingItem) {\n existingItem.current = message;\n this.performOp(prev, existingItem, \"relink\");\n this._messages.dirty();\n return;\n }\n\n const newItem: RepositoryMessage = {\n prev,\n current: message,\n next: null,\n children: [],\n level: prev ? prev.level + 1 : 0,\n };\n\n this.messages.set(message.id, newItem);\n this.performOp(prev, newItem, \"link\");\n\n if (this.head === prev) {\n this.head = newItem;\n }\n\n this._messages.dirty();\n }\n\n getMessage(messageId: string) {\n const message = this.messages.get(messageId);\n if (!message)\n throw new Error(\n \"MessageRepository(updateMessage): Message not found. This is likely an internal bug in assistant-ui.\",\n );\n\n return {\n parentId: message.prev?.current.id ?? null,\n message: message.current,\n index: message.level,\n };\n }\n\n deleteMessage(messageId: string, replacementId?: string | null | undefined) {\n const message = this.messages.get(messageId);\n\n if (!message)\n throw new Error(\n \"MessageRepository(deleteMessage): Message not found. This is likely an internal bug in assistant-ui.\",\n );\n\n const replacement =\n replacementId === undefined\n ? message.prev\n : replacementId === null\n ? null\n : this.messages.get(replacementId);\n if (replacement === undefined)\n throw new Error(\n \"MessageRepository(deleteMessage): Replacement not found. This is likely an internal bug in assistant-ui.\",\n );\n\n for (const child of message.children) {\n const childMessage = this.messages.get(child);\n if (!childMessage)\n throw new Error(\n \"MessageRepository(deleteMessage): Child message not found. This is likely an internal bug in assistant-ui.\",\n );\n this.performOp(replacement, childMessage, \"relink\");\n }\n\n this.performOp(null, message, \"cut\");\n this.messages.delete(messageId);\n\n if (this.head === message) {\n this.head = findHead(replacement ?? this.root);\n }\n\n this._messages.dirty();\n }\n\n getBranches(messageId: string) {\n const message = this.messages.get(messageId);\n if (!message)\n throw new Error(\n \"MessageRepository(getBranches): Message not found. This is likely an internal bug in assistant-ui.\",\n );\n\n const { children } = message.prev ?? this.root;\n return children;\n }\n\n /**\n * Evicts optimistic messages (`metadata.isOptimistic`) the head just moved\n * away from. Since eviction runs on every head move, the only optimistic\n * messages in the repository live on the branch the head previously pointed\n * at — so we walk just that branch rather than the whole repository. Keeps a\n * client→server id swap from leaving a phantom sibling, and drops off-branch\n * placeholders.\n */\n private evictOffBranchOptimisticMessages(\n previousHead: RepositoryMessage | null,\n currentHead: RepositoryMessage | null,\n ) {\n if (!previousHead) return;\n\n const onHeadBranch = new Set<string>();\n for (let current = currentHead; current; current = current.prev) {\n onHeadBranch.add(current.current.id);\n }\n\n const stale: string[] = [];\n for (\n let current: RepositoryMessage | null = previousHead;\n current;\n current = current.prev\n ) {\n // Stop at the first node shared with the current head branch: every\n // ancestor above it is shared too, so nothing further can be off-branch.\n if (onHeadBranch.has(current.current.id)) break;\n if (current.current.metadata?.isOptimistic) {\n stale.push(current.current.id);\n }\n }\n\n for (const id of stale) {\n // A prior deletion may have already removed this node.\n if (this.messages.has(id)) this.deleteMessage(id);\n }\n }\n\n switchToBranch(messageId: string) {\n const message = this.messages.get(messageId);\n if (!message)\n throw new Error(\n \"MessageRepository(switchToBranch): Branch not found. This is likely an internal bug in assistant-ui.\",\n );\n\n const previousHead = this.head;\n const prevOrRoot = message.prev ?? this.root;\n prevOrRoot.next = message;\n\n this.head = findHead(message);\n\n this.evictOffBranchOptimisticMessages(previousHead, this.head);\n\n this._messages.dirty();\n }\n\n resetHead(messageId: string | null) {\n if (messageId === null) {\n this.clear();\n return;\n }\n\n const message = this.messages.get(messageId);\n if (!message)\n throw new Error(\n \"MessageRepository(resetHead): Branch not found. This is likely an internal bug in assistant-ui.\",\n );\n\n const previousHead = this.head;\n\n if (message.children.length > 0) {\n const deleteDescendants = (msg: RepositoryMessage) => {\n for (const childId of msg.children) {\n const childMessage = this.messages.get(childId);\n if (childMessage) {\n deleteDescendants(childMessage);\n this.messages.delete(childId);\n }\n }\n };\n deleteDescendants(message);\n\n message.children = [];\n message.next = null;\n }\n\n this.head = message;\n for (\n let current: RepositoryMessage | null = message;\n current;\n current = current.prev\n ) {\n if (current.prev) {\n current.prev.next = current;\n } else {\n this.root.next = current;\n }\n }\n\n this.evictOffBranchOptimisticMessages(previousHead, this.head);\n\n this._messages.dirty();\n }\n\n clear(): void {\n this.messages.clear();\n this.head = null;\n this.root = {\n children: [],\n next: null,\n };\n this._messages.dirty();\n }\n\n export(): ExportedMessageRepository {\n const exportItems: ExportedMessageRepository[\"messages\"] = [];\n\n // Optimistic messages are ephemeral and never persisted. They're always\n // leaf nodes, so skipping them can't orphan a persisted child.\n for (const [, message] of this.messages) {\n if (message.current.metadata?.isOptimistic) continue;\n exportItems.push({\n message: message.current,\n parentId: message.prev?.current.id ?? null,\n });\n }\n\n // The head may itself be optimistic; walk up to the nearest persisted ancestor.\n let head = this.head;\n while (head?.current.metadata?.isOptimistic) {\n head = head.prev;\n }\n\n return {\n headId: head?.current.id ?? null,\n messages: exportItems,\n };\n }\n\n import({ headId, messages }: ExportedMessageRepository) {\n for (const { message, parentId } of messages) {\n this.addOrUpdateMessage(parentId, message);\n }\n\n this.resetHead(headId ?? messages.at(-1)?.message.id ?? null);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"message-repository.js","names":["ThreadMessage","RunConfig","generateId","ThreadMessageLike","getAutoStatus","fromThreadMessageLike","ExportedMessageRepositoryItem","message","parentId","runConfig","ExportedMessageRepository","headId","messages","Array","fromArray","conv","map","m","undefined","idx","id","fromBranchableArray","items","options","fallbackStatus","Error","RepositoryParent","children","next","RepositoryMessage","prev","current","level","findHead","CachedValue","_value","T","constructor","func","value","dirty","MessageRepository","Map","head","root","updateLevels","newLevel","childId","childMessage","get","performOp","newParent","child","operation","parentOrRoot","newParentOrRoot","filter","fallbackId","at","fallback","_messages","canonicalHeadId","metadata","isOptimistic","getMessages","headMessage","addOrUpdateMessage","existingItem","newItem","set","getMessage","messageId","index","deleteMessage","replacementId","replacement","delete","getBranches","evictOffBranchOptimisticMessages","previousHead","currentHead","onHeadBranch","Set","add","stale","has","push","switchToBranch","prevOrRoot","resetHead","clear","length","deleteDescendants","msg","export","exportItems","import"],"sources":["../../../src/runtime/utils/message-repository.ts"],"sourcesContent":["import type { ThreadMessage } from \"../../types/message\";\nimport type { RunConfig } from \"../../types/message\";\nimport { generateId } from \"../../utils/id\";\nimport type { ThreadMessageLike } from \"./thread-message-like\";\nimport { getAutoStatus } from \"./auto-status\";\nimport { fromThreadMessageLike } from \"./thread-message-like\";\n\nexport type ExportedMessageRepositoryItem = {\n message: ThreadMessage;\n parentId: string | null;\n runConfig?: RunConfig;\n};\n\nexport type ExportedMessageRepository = {\n headId?: string | null;\n messages: Array<{\n message: ThreadMessage;\n parentId: string | null;\n runConfig?: RunConfig;\n }>;\n};\n\nexport const ExportedMessageRepository = {\n fromArray: (\n messages: readonly ThreadMessageLike[],\n ): ExportedMessageRepository => {\n const conv = messages.map((m) =>\n fromThreadMessageLike(\n m,\n generateId(),\n getAutoStatus(false, false, false, false, undefined),\n ),\n );\n\n return {\n messages: conv.map((m, idx) => ({\n parentId: idx > 0 ? conv[idx - 1]!.id : null,\n message: m,\n })),\n };\n },\n\n fromBranchableArray: (\n items: readonly {\n message: ThreadMessageLike;\n parentId: string | null;\n }[],\n options?: { headId?: string | null },\n ): ExportedMessageRepository => {\n const fallbackStatus = getAutoStatus(false, false, false, false, undefined);\n return {\n ...(options?.headId !== undefined\n ? { headId: options.headId }\n : undefined),\n messages: items.map(({ message, parentId }) => {\n if (!message.id) {\n throw new Error(\n \"ExportedMessageRepository.fromBranchableArray: Each message must have an 'id' field set.\",\n );\n }\n return {\n parentId,\n message: fromThreadMessageLike(message, message.id, fallbackStatus),\n };\n }),\n };\n },\n};\n\ntype RepositoryParent = {\n children: string[];\n next: RepositoryMessage | null;\n};\n\ntype RepositoryMessage = RepositoryParent & {\n prev: RepositoryMessage | null;\n current: ThreadMessage;\n level: number;\n};\n\nconst findHead = (\n message: RepositoryMessage | RepositoryParent,\n): RepositoryMessage | null => {\n if (message.next) return findHead(message.next);\n if (\"current\" in message) return message;\n return null;\n};\n\nclass CachedValue<T> {\n private _value: T | null = null;\n\n constructor(private func: () => T) {}\n\n get value() {\n if (this._value === null) {\n this._value = this.func();\n }\n return this._value;\n }\n\n dirty() {\n this._value = null;\n }\n}\n\nexport class MessageRepository {\n private messages = new Map<string, RepositoryMessage>();\n private head: RepositoryMessage | null = null;\n private root: RepositoryParent = {\n children: [],\n next: null,\n };\n\n private updateLevels(message: RepositoryMessage, newLevel: number) {\n message.level = newLevel;\n for (const childId of message.children) {\n const childMessage = this.messages.get(childId);\n if (childMessage) {\n this.updateLevels(childMessage, newLevel + 1);\n }\n }\n }\n\n private performOp(\n newParent: RepositoryMessage | null,\n child: RepositoryMessage,\n operation: \"cut\" | \"link\" | \"relink\",\n ) {\n const parentOrRoot = child.prev ?? this.root;\n const newParentOrRoot = newParent ?? this.root;\n\n if (operation === \"relink\" && parentOrRoot === newParentOrRoot) return;\n\n if (operation !== \"link\") {\n parentOrRoot.children = parentOrRoot.children.filter(\n (m) => m !== child.current.id,\n );\n\n if (parentOrRoot.next === child) {\n const fallbackId = parentOrRoot.children.at(-1);\n const fallback = fallbackId ? this.messages.get(fallbackId) : null;\n if (fallback === undefined) {\n throw new Error(\n \"MessageRepository(performOp/cut): Fallback sibling message not found. This is likely an internal bug in assistant-ui.\",\n );\n }\n parentOrRoot.next = fallback;\n }\n }\n\n if (operation !== \"cut\") {\n for (\n let current: RepositoryMessage | null = newParent;\n current;\n current = current.prev\n ) {\n if (current.current.id === child.current.id) {\n throw new Error(\n \"MessageRepository(performOp/link): A message with the same id already exists in the parent tree. This error occurs if the same message id is found multiple times. This is likely an internal bug in assistant-ui.\",\n );\n }\n }\n\n newParentOrRoot.children = [\n ...newParentOrRoot.children,\n child.current.id,\n ];\n\n if (findHead(child) === this.head || newParentOrRoot.next === null) {\n newParentOrRoot.next = child;\n }\n\n child.prev = newParent;\n\n const newLevel = newParent ? newParent.level + 1 : 0;\n this.updateLevels(child, newLevel);\n }\n }\n\n private _messages = new CachedValue<readonly ThreadMessage[]>(() => {\n const messages = new Array<ThreadMessage>((this.head?.level ?? -1) + 1);\n for (let current = this.head; current; current = current.prev) {\n messages[current.level] = current.current;\n }\n return messages;\n });\n\n get headId() {\n return this.head?.current.id ?? null;\n }\n\n get canonicalHeadId() {\n // Optimistic messages are ephemeral, so persisted callers need the nearest\n // non-optimistic ancestor rather than the raw head.\n let head = this.head;\n while (head?.current.metadata?.isOptimistic) {\n head = head.prev;\n }\n return head?.current.id ?? null;\n }\n\n getMessages(headId?: string) {\n if (headId === undefined || headId === this.head?.current.id) {\n return this._messages.value;\n }\n\n const headMessage = this.messages.get(headId);\n if (!headMessage) {\n throw new Error(\n \"MessageRepository(getMessages): Head message not found. This is likely an internal bug in assistant-ui.\",\n );\n }\n\n const messages = new Array<ThreadMessage>(headMessage.level + 1);\n for (\n let current: RepositoryMessage | null = headMessage;\n current;\n current = current.prev\n ) {\n messages[current.level] = current.current;\n }\n return messages;\n }\n\n addOrUpdateMessage(parentId: string | null, message: ThreadMessage) {\n const existingItem = this.messages.get(message.id);\n const prev = parentId ? this.messages.get(parentId) : null;\n if (prev === undefined)\n throw new Error(\n \"MessageRepository(addOrUpdateMessage): Parent message not found. This is likely an internal bug in assistant-ui.\",\n );\n\n if (existingItem) {\n existingItem.current = message;\n this.performOp(prev, existingItem, \"relink\");\n this._messages.dirty();\n return;\n }\n\n const newItem: RepositoryMessage = {\n prev,\n current: message,\n next: null,\n children: [],\n level: prev ? prev.level + 1 : 0,\n };\n\n this.messages.set(message.id, newItem);\n this.performOp(prev, newItem, \"link\");\n\n if (this.head === prev) {\n this.head = newItem;\n }\n\n this._messages.dirty();\n }\n\n getMessage(messageId: string) {\n const message = this.messages.get(messageId);\n if (!message)\n throw new Error(\n \"MessageRepository(updateMessage): Message not found. This is likely an internal bug in assistant-ui.\",\n );\n\n return {\n parentId: message.prev?.current.id ?? null,\n message: message.current,\n index: message.level,\n };\n }\n\n deleteMessage(messageId: string, replacementId?: string | null | undefined) {\n const message = this.messages.get(messageId);\n\n if (!message)\n throw new Error(\n \"MessageRepository(deleteMessage): Message not found. This is likely an internal bug in assistant-ui.\",\n );\n\n const replacement =\n replacementId === undefined\n ? message.prev\n : replacementId === null\n ? null\n : this.messages.get(replacementId);\n if (replacement === undefined)\n throw new Error(\n \"MessageRepository(deleteMessage): Replacement not found. This is likely an internal bug in assistant-ui.\",\n );\n\n for (const child of message.children) {\n const childMessage = this.messages.get(child);\n if (!childMessage)\n throw new Error(\n \"MessageRepository(deleteMessage): Child message not found. This is likely an internal bug in assistant-ui.\",\n );\n this.performOp(replacement, childMessage, \"relink\");\n }\n\n this.performOp(null, message, \"cut\");\n this.messages.delete(messageId);\n\n if (this.head === message) {\n this.head = findHead(replacement ?? this.root);\n }\n\n this._messages.dirty();\n }\n\n getBranches(messageId: string) {\n const message = this.messages.get(messageId);\n if (!message)\n throw new Error(\n \"MessageRepository(getBranches): Message not found. This is likely an internal bug in assistant-ui.\",\n );\n\n const { children } = message.prev ?? this.root;\n return children;\n }\n\n /**\n * Evicts optimistic messages (`metadata.isOptimistic`) the head just moved\n * away from. Since eviction runs on every head move, the only optimistic\n * messages in the repository live on the branch the head previously pointed\n * at — so we walk just that branch rather than the whole repository. Keeps a\n * client→server id swap from leaving a phantom sibling, and drops off-branch\n * placeholders.\n */\n private evictOffBranchOptimisticMessages(\n previousHead: RepositoryMessage | null,\n currentHead: RepositoryMessage | null,\n ) {\n if (!previousHead) return;\n\n const onHeadBranch = new Set<string>();\n for (let current = currentHead; current; current = current.prev) {\n onHeadBranch.add(current.current.id);\n }\n\n const stale: string[] = [];\n for (\n let current: RepositoryMessage | null = previousHead;\n current;\n current = current.prev\n ) {\n // Stop at the first node shared with the current head branch: every\n // ancestor above it is shared too, so nothing further can be off-branch.\n if (onHeadBranch.has(current.current.id)) break;\n if (current.current.metadata?.isOptimistic) {\n stale.push(current.current.id);\n }\n }\n\n for (const id of stale) {\n // A prior deletion may have already removed this node.\n if (this.messages.has(id)) this.deleteMessage(id);\n }\n }\n\n switchToBranch(messageId: string) {\n const message = this.messages.get(messageId);\n if (!message)\n throw new Error(\n \"MessageRepository(switchToBranch): Branch not found. This is likely an internal bug in assistant-ui.\",\n );\n\n const previousHead = this.head;\n const prevOrRoot = message.prev ?? this.root;\n prevOrRoot.next = message;\n\n this.head = findHead(message);\n\n this.evictOffBranchOptimisticMessages(previousHead, this.head);\n\n this._messages.dirty();\n }\n\n resetHead(messageId: string | null) {\n if (messageId === null) {\n this.clear();\n return;\n }\n\n const message = this.messages.get(messageId);\n if (!message)\n throw new Error(\n \"MessageRepository(resetHead): Branch not found. This is likely an internal bug in assistant-ui.\",\n );\n\n const previousHead = this.head;\n\n if (message.children.length > 0) {\n const deleteDescendants = (msg: RepositoryMessage) => {\n for (const childId of msg.children) {\n const childMessage = this.messages.get(childId);\n if (childMessage) {\n deleteDescendants(childMessage);\n this.messages.delete(childId);\n }\n }\n };\n deleteDescendants(message);\n\n message.children = [];\n message.next = null;\n }\n\n this.head = message;\n for (\n let current: RepositoryMessage | null = message;\n current;\n current = current.prev\n ) {\n if (current.prev) {\n current.prev.next = current;\n } else {\n this.root.next = current;\n }\n }\n\n this.evictOffBranchOptimisticMessages(previousHead, this.head);\n\n this._messages.dirty();\n }\n\n clear(): void {\n this.messages.clear();\n this.head = null;\n this.root = {\n children: [],\n next: null,\n };\n this._messages.dirty();\n }\n\n export(): ExportedMessageRepository {\n const exportItems: ExportedMessageRepository[\"messages\"] = [];\n\n // Optimistic messages are ephemeral and never persisted. They're always\n // leaf nodes, so skipping them can't orphan a persisted child.\n for (const [, message] of this.messages) {\n if (message.current.metadata?.isOptimistic) continue;\n exportItems.push({\n message: message.current,\n parentId: message.prev?.current.id ?? null,\n });\n }\n\n return {\n headId: this.canonicalHeadId,\n messages: exportItems,\n };\n }\n\n import({ headId, messages }: ExportedMessageRepository) {\n for (const { message, parentId } of messages) {\n this.addOrUpdateMessage(parentId, message);\n }\n\n this.resetHead(headId ?? messages.at(-1)?.message.id ?? null);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ import { MessageTiming } from "../../types/message.js";
2
+
3
+ //#region src/runtime/utils/streaming-timing.d.ts
4
+ /**
5
+ * Shape-specific accessors that adapt {@link stepStreamingTiming} /
6
+ * {@link useStreamingTiming} to a runtime's message list. Each adapter
7
+ * provides three pure functions over its own message type; the timing state
8
+ * machine itself lives once in core.
9
+ */
10
+ type StreamingTimingAccessors<TMessage> = {
11
+ /** Resolve the id of the last assistant message, or `undefined` if none. */readonly getAssistantMessageId: (messages: readonly TMessage[]) => string | undefined; /** Total text length (incl. reasoning/thinking) of the assistant message. */
12
+ readonly getTextLength: (messages: readonly TMessage[], messageId: string) => number; /** Number of tool calls recorded on the assistant message. */
13
+ readonly getToolCallCount: (messages: readonly TMessage[], messageId: string) => number;
14
+ };
15
+ type StreamingTimingOptions = {
16
+ /**
17
+ * Estimate token count from a text length. Defaults to `Math.ceil(n / 4)`.
18
+ * Adapters with real usage data can override this.
19
+ */
20
+ readonly estimateTokens?: (textLength: number) => number;
21
+ };
22
+ /**
23
+ * Mutable tracking state for a single in-flight assistant message. Kept
24
+ * between updates by the caller (a ref in the React hook); `null` means no
25
+ * message is being tracked.
26
+ *
27
+ * `totalChunks` counts content growth deltas observed while streaming. The
28
+ * final delta that lands in the same update as the `isRunning -> false`
29
+ * transition is reconciled at finalize, so `totalChunks` and the token
30
+ * estimate (derived from final length) count the same content.
31
+ */
32
+ type StreamingTimingState = {
33
+ readonly messageId: string;
34
+ readonly startTime: number;
35
+ readonly firstTokenTime?: number;
36
+ readonly lastContentLength: number;
37
+ readonly totalChunks: number;
38
+ };
39
+ /**
40
+ * Advance the client-side streaming timing tracker by one update.
41
+ *
42
+ * Pure: given the previous state and the current `(messages, isRunning)`
43
+ * snapshot, returns the next state plus any `MessageTiming` finalized this
44
+ * update (empty when streaming is still in flight). The caller owns the
45
+ * state (e.g. a ref) and merges finalized timings into its store.
46
+ *
47
+ * At finalize the text length is recomputed from the final messages, and any
48
+ * final growth delta that landed in the same update as the
49
+ * `isRunning -> false` transition is reconciled, so the token estimate and
50
+ * `totalChunks` count the same content (the per-adapter hooks this replaces
51
+ * reuse a stale growth delta and miss that last chunk).
52
+ */
53
+ declare const stepStreamingTiming: <TMessage>(state: StreamingTimingState | null, messages: readonly TMessage[], isRunning: boolean, accessors: StreamingTimingAccessors<TMessage>, options: StreamingTimingOptions | undefined, now?: () => number) => {
54
+ readonly state: StreamingTimingState | null;
55
+ readonly timings: Record<string, MessageTiming>;
56
+ };
57
+ //#endregion
58
+ export { StreamingTimingAccessors, StreamingTimingOptions, StreamingTimingState, stepStreamingTiming };
59
+ //# sourceMappingURL=streaming-timing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming-timing.d.ts","names":[],"sources":["../../../src/runtime/utils/streaming-timing.ts"],"mappings":";;;;;AAQA;;;;KAAY,wBAAA;EAYW,qFAVZ,qBAAA,GACP,QAAA,WAAmB,QAAA,2BASQ;EAAA,SANpB,aAAA,GACP,QAAA,WAAmB,QAAA,IACnB,SAAA,qBANO;EAAA,SASA,gBAAA,GACP,QAAA,WAAmB,QAAA,IACnB,SAAA;AAAA;AAAA,KAIQ,sBAAA;EAVW;;;;EAAA,SAeZ,cAAA,IAAkB,UAAkB;AAAA;;;AAT1B;AAIrB;;;;AAK+C;AAa/C;;KAAY,oBAAA;EAAA,SACD,SAAA;EAAA,SACA,SAAA;EAAA,SACA,cAAA;EAAA,SACA,iBAAA;EAAA,SACA,WAAA;AAAA;;AAAW;AAuCtB;;;;;;;;;;;;cAAa,mBAAA,aACX,KAAA,EAAO,oBAAA,SACP,QAAA,WAAmB,QAAA,IACnB,SAAA,WACA,SAAA,EAAW,wBAAA,CAAyB,QAAA,GACpC,OAAA,EAAS,sBAAA,cACT,GAAA;EAAA,SAES,KAAA,EAAO,oBAAA;EAAA,SACP,OAAA,EAAS,MAAA,SAAe,aAAA;AAAA"}
@@ -0,0 +1,76 @@
1
+ //#region src/runtime/utils/streaming-timing.ts
2
+ const defaultEstimateTokens = (textLength) => Math.ceil(textLength / 4);
3
+ /**
4
+ * Apply one content growth delta to `state`, recording the first-token time
5
+ * on the first growth and bumping the chunk count. Returns `state` unchanged
6
+ * when `len` has not grown.
7
+ */
8
+ const applyGrowth = (state, len, now) => {
9
+ if (len <= state.lastContentLength) return state;
10
+ return {
11
+ ...state,
12
+ firstTokenTime: state.firstTokenTime ?? now() - state.startTime,
13
+ lastContentLength: len,
14
+ totalChunks: state.totalChunks + 1
15
+ };
16
+ };
17
+ /**
18
+ * Advance the client-side streaming timing tracker by one update.
19
+ *
20
+ * Pure: given the previous state and the current `(messages, isRunning)`
21
+ * snapshot, returns the next state plus any `MessageTiming` finalized this
22
+ * update (empty when streaming is still in flight). The caller owns the
23
+ * state (e.g. a ref) and merges finalized timings into its store.
24
+ *
25
+ * At finalize the text length is recomputed from the final messages, and any
26
+ * final growth delta that landed in the same update as the
27
+ * `isRunning -> false` transition is reconciled, so the token estimate and
28
+ * `totalChunks` count the same content (the per-adapter hooks this replaces
29
+ * reuse a stale growth delta and miss that last chunk).
30
+ */
31
+ const stepStreamingTiming = (state, messages, isRunning, accessors, options, now = Date.now) => {
32
+ const lastId = accessors.getAssistantMessageId(messages);
33
+ const estimateTokens = options?.estimateTokens ?? defaultEstimateTokens;
34
+ if (isRunning && lastId !== void 0) {
35
+ let next;
36
+ if (state === null || state.messageId !== lastId) next = {
37
+ messageId: lastId,
38
+ startTime: now(),
39
+ lastContentLength: 0,
40
+ totalChunks: 0
41
+ };
42
+ else next = state;
43
+ return {
44
+ state: applyGrowth(next, accessors.getTextLength(messages, next.messageId), now),
45
+ timings: {}
46
+ };
47
+ }
48
+ if (!isRunning && state !== null) {
49
+ const nowMs = now();
50
+ const reconciled = applyGrowth(state, accessors.getTextLength(messages, state.messageId), () => nowMs);
51
+ const totalStreamTime = nowMs - reconciled.startTime;
52
+ const tokenCount = estimateTokens(reconciled.lastContentLength);
53
+ const toolCallCount = accessors.getToolCallCount(messages, reconciled.messageId);
54
+ const timing = {
55
+ streamStartTime: reconciled.startTime,
56
+ totalStreamTime,
57
+ totalChunks: reconciled.totalChunks,
58
+ toolCallCount,
59
+ ...reconciled.firstTokenTime !== void 0 && { firstTokenTime: reconciled.firstTokenTime },
60
+ ...tokenCount > 0 && { tokenCount },
61
+ ...totalStreamTime > 0 && tokenCount > 0 && { tokensPerSecond: tokenCount / (totalStreamTime / 1e3) }
62
+ };
63
+ return {
64
+ state: null,
65
+ timings: { [reconciled.messageId]: timing }
66
+ };
67
+ }
68
+ return {
69
+ state,
70
+ timings: {}
71
+ };
72
+ };
73
+ //#endregion
74
+ export { stepStreamingTiming };
75
+
76
+ //# sourceMappingURL=streaming-timing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streaming-timing.js","names":["MessageTiming","StreamingTimingAccessors","getAssistantMessageId","messages","TMessage","getTextLength","messageId","getToolCallCount","StreamingTimingOptions","estimateTokens","textLength","StreamingTimingState","startTime","firstTokenTime","lastContentLength","totalChunks","defaultEstimateTokens","Math","ceil","applyGrowth","state","len","now","stepStreamingTiming","isRunning","accessors","options","Date","timings","Record","lastId","undefined","next","nowMs","reconciled","totalStreamTime","tokenCount","toolCallCount","timing","streamStartTime","tokensPerSecond"],"sources":["../../../src/runtime/utils/streaming-timing.ts"],"sourcesContent":["import type { MessageTiming } from \"../../types/message\";\n\n/**\n * Shape-specific accessors that adapt {@link stepStreamingTiming} /\n * {@link useStreamingTiming} to a runtime's message list. Each adapter\n * provides three pure functions over its own message type; the timing state\n * machine itself lives once in core.\n */\nexport type StreamingTimingAccessors<TMessage> = {\n /** Resolve the id of the last assistant message, or `undefined` if none. */\n readonly getAssistantMessageId: (\n messages: readonly TMessage[],\n ) => string | undefined;\n /** Total text length (incl. reasoning/thinking) of the assistant message. */\n readonly getTextLength: (\n messages: readonly TMessage[],\n messageId: string,\n ) => number;\n /** Number of tool calls recorded on the assistant message. */\n readonly getToolCallCount: (\n messages: readonly TMessage[],\n messageId: string,\n ) => number;\n};\n\nexport type StreamingTimingOptions = {\n /**\n * Estimate token count from a text length. Defaults to `Math.ceil(n / 4)`.\n * Adapters with real usage data can override this.\n */\n readonly estimateTokens?: (textLength: number) => number;\n};\n\n/**\n * Mutable tracking state for a single in-flight assistant message. Kept\n * between updates by the caller (a ref in the React hook); `null` means no\n * message is being tracked.\n *\n * `totalChunks` counts content growth deltas observed while streaming. The\n * final delta that lands in the same update as the `isRunning -> false`\n * transition is reconciled at finalize, so `totalChunks` and the token\n * estimate (derived from final length) count the same content.\n */\nexport type StreamingTimingState = {\n readonly messageId: string;\n readonly startTime: number;\n readonly firstTokenTime?: number;\n readonly lastContentLength: number;\n readonly totalChunks: number;\n};\n\nconst defaultEstimateTokens = (textLength: number): number =>\n Math.ceil(textLength / 4);\n\n/**\n * Apply one content growth delta to `state`, recording the first-token time\n * on the first growth and bumping the chunk count. Returns `state` unchanged\n * when `len` has not grown.\n */\nconst applyGrowth = (\n state: StreamingTimingState,\n len: number,\n now: () => number,\n): StreamingTimingState => {\n if (len <= state.lastContentLength) return state;\n return {\n ...state,\n firstTokenTime: state.firstTokenTime ?? now() - state.startTime,\n lastContentLength: len,\n totalChunks: state.totalChunks + 1,\n };\n};\n\n/**\n * Advance the client-side streaming timing tracker by one update.\n *\n * Pure: given the previous state and the current `(messages, isRunning)`\n * snapshot, returns the next state plus any `MessageTiming` finalized this\n * update (empty when streaming is still in flight). The caller owns the\n * state (e.g. a ref) and merges finalized timings into its store.\n *\n * At finalize the text length is recomputed from the final messages, and any\n * final growth delta that landed in the same update as the\n * `isRunning -> false` transition is reconciled, so the token estimate and\n * `totalChunks` count the same content (the per-adapter hooks this replaces\n * reuse a stale growth delta and miss that last chunk).\n */\nexport const stepStreamingTiming = <TMessage>(\n state: StreamingTimingState | null,\n messages: readonly TMessage[],\n isRunning: boolean,\n accessors: StreamingTimingAccessors<TMessage>,\n options: StreamingTimingOptions | undefined,\n now: () => number = Date.now,\n): {\n readonly state: StreamingTimingState | null;\n readonly timings: Record<string, MessageTiming>;\n} => {\n const lastId = accessors.getAssistantMessageId(messages);\n const estimateTokens = options?.estimateTokens ?? defaultEstimateTokens;\n\n if (isRunning && lastId !== undefined) {\n let next: StreamingTimingState;\n if (state === null || state.messageId !== lastId) {\n next = {\n messageId: lastId,\n startTime: now(),\n lastContentLength: 0,\n totalChunks: 0,\n };\n } else {\n next = state;\n }\n\n return {\n state: applyGrowth(\n next,\n accessors.getTextLength(messages, next.messageId),\n now,\n ),\n timings: {},\n };\n }\n\n if (!isRunning && state !== null) {\n const nowMs = now();\n // Reconcile any final growth that landed with the stop signal so the\n // token estimate (derived from final length) and totalChunks agree.\n const reconciled = applyGrowth(\n state,\n accessors.getTextLength(messages, state.messageId),\n () => nowMs,\n );\n\n const totalStreamTime = nowMs - reconciled.startTime;\n const tokenCount = estimateTokens(reconciled.lastContentLength);\n const toolCallCount = accessors.getToolCallCount(\n messages,\n reconciled.messageId,\n );\n\n const timing: MessageTiming = {\n streamStartTime: reconciled.startTime,\n totalStreamTime,\n totalChunks: reconciled.totalChunks,\n toolCallCount,\n ...(reconciled.firstTokenTime !== undefined && {\n firstTokenTime: reconciled.firstTokenTime,\n }),\n ...(tokenCount > 0 && { tokenCount }),\n ...(totalStreamTime > 0 &&\n tokenCount > 0 && {\n tokensPerSecond: tokenCount / (totalStreamTime / 1000),\n }),\n };\n\n return { state: null, timings: { [reconciled.messageId]: timing } };\n }\n\n return { state, timings: {} };\n};\n"],"mappings":";AAmDA,MAAMgB,yBAAyBN,eAC7BO,KAAKC,KAAKR,aAAa,CAAC;;;;;;AAO1B,MAAMS,eACJC,OACAC,KACAC,QACyB;CACzB,IAAID,OAAOD,MAAMN,mBAAmB,OAAOM;CAC3C,OAAO;EACL,GAAGA;EACHP,gBAAgBO,MAAMP,kBAAkBS,IAAI,IAAIF,MAAMR;EACtDE,mBAAmBO;EACnBN,aAAaK,MAAML,cAAc;CACnC;AACF;;;;;;;;;;;;;;;AAgBA,MAAaQ,uBACXH,OACAjB,UACAqB,WACAC,WACAC,SACAJ,MAAoBK,KAAKL,QAItB;CACH,MAAMQ,SAASL,UAAUvB,sBAAsBC,QAAQ;CACvD,MAAMM,iBAAiBiB,SAASjB,kBAAkBO;CAElD,IAAIQ,aAAaM,WAAWC,KAAAA,GAAW;EACrC,IAAIC;EACJ,IAAIZ,UAAU,QAAQA,MAAMd,cAAcwB,QACxCE,OAAO;GACL1B,WAAWwB;GACXlB,WAAWU,IAAI;GACfR,mBAAmB;GACnBC,aAAa;EACf;OAEAiB,OAAOZ;EAGT,OAAO;GACLA,OAAOD,YACLa,MACAP,UAAUpB,cAAcF,UAAU6B,KAAK1B,SAAS,GAChDgB,GACF;GACAM,SAAS,CAAC;EACZ;CACF;CAEA,IAAI,CAACJ,aAAaJ,UAAU,MAAM;EAChC,MAAMa,QAAQX,IAAI;EAGlB,MAAMY,aAAaf,YACjBC,OACAK,UAAUpB,cAAcF,UAAUiB,MAAMd,SAAS,SAC3C2B,KACR;EAEA,MAAME,kBAAkBF,QAAQC,WAAWtB;EAC3C,MAAMwB,aAAa3B,eAAeyB,WAAWpB,iBAAiB;EAC9D,MAAMuB,gBAAgBZ,UAAUlB,iBAC9BJ,UACA+B,WAAW5B,SACb;EAEA,MAAMgC,SAAwB;GAC5BC,iBAAiBL,WAAWtB;GAC5BuB;GACApB,aAAamB,WAAWnB;GACxBsB;GACA,GAAIH,WAAWrB,mBAAmBkB,KAAAA,KAAa,EAC7ClB,gBAAgBqB,WAAWrB,eAC7B;GACA,GAAIuB,aAAa,KAAK,EAAEA,WAAW;GACnC,GAAID,kBAAkB,KACpBC,aAAa,KAAK,EAChBI,iBAAiBJ,cAAcD,kBAAkB,KACnD;EACJ;EAEA,OAAO;GAAEf,OAAO;GAAMQ,SAAS,GAAGM,WAAW5B,YAAYgC,OAAO;EAAE;CACpE;CAEA,OAAO;EAAElB;EAAOQ,SAAS,CAAC;CAAE;AAC9B"}
@@ -23,6 +23,7 @@ const fromThreadMessageLike = (like, fallbackId, fallbackStatus) => {
23
23
  text: like.content
24
24
  }] : like.content;
25
25
  const sanitizeImageContent = ({ image, ...rest }) => {
26
+ if (typeof image !== "string") return null;
26
27
  if (image.match(/^data:image\/(png|jpeg|jpg|gif|webp|svg\+xml);base64,(.*)$/)) return {
27
28
  ...rest,
28
29
  image
@@ -46,7 +47,7 @@ const fromThreadMessageLike = (like, fallbackId, fallbackStatus) => {
46
47
  switch (type) {
47
48
  case "text":
48
49
  case "reasoning":
49
- if (part.text.trim().length === 0) return null;
50
+ if (!part.text?.trim()) return null;
50
51
  return part;
51
52
  case "file":
52
53
  case "source": return part;
@@ -1 +1 @@
1
- {"version":3,"file":"thread-message-like.js","names":["parsePartialJsonObject","generateId","ReasoningMessagePart","SourceMessagePart","ThreadStep","MessageStatus","ImageMessagePart","ThreadMessage","ThreadAssistantMessagePart","ThreadAssistantMessage","ThreadUserMessagePart","ThreadUserMessage","ThreadSystemMessage","FileMessagePart","DataMessagePart","GenerativeUIMessagePart","Unstable_AudioMessagePart","CompleteAttachment","MessageTiming","TextMessagePart","ToolApprovalOption","ToolCallTiming","ReadonlyJSONObject","ReadonlyJSONValue","DataPrefixedPart","type","data","ThreadMessageLike","role","content","toolCallId","toolName","args","argsText","artifact","result","isError","parentId","messages","interrupt","payload","timing","approval","id","approved","reason","isAutomatic","options","optionId","resolution","createdAt","Date","status","attachments","Omit","metadata","unstable_state","unstable_annotations","unstable_data","steps","submittedFeedback","isOptimistic","custom","Record","convertDataPrefixedPart","startsWith","undefined","name","substring","fromThreadMessageLike","like","fallbackId","fallbackStatus","common","const","text","sanitizeImageContent","image","rest","dataUri","match","test","console","warn","length","Error","map","part","trim","basePart","commonProps","JSON","stringify","converted","filter","c","att","unsupportedRole"],"sources":["../../../src/runtime/utils/thread-message-like.ts"],"sourcesContent":["import { parsePartialJsonObject } from \"assistant-stream/utils\";\nimport { generateId } from \"../../utils/id\";\nimport type {\n ReasoningMessagePart,\n SourceMessagePart,\n ThreadStep,\n MessageStatus,\n ImageMessagePart,\n ThreadMessage,\n ThreadAssistantMessagePart,\n ThreadAssistantMessage,\n ThreadUserMessagePart,\n ThreadUserMessage,\n ThreadSystemMessage,\n FileMessagePart,\n DataMessagePart,\n GenerativeUIMessagePart,\n Unstable_AudioMessagePart,\n} from \"../../types/message\";\nimport type { CompleteAttachment } from \"../../types/attachment\";\nimport type {\n MessageTiming,\n TextMessagePart,\n ToolApprovalOption,\n ToolCallTiming,\n} from \"../../types/message\";\nimport type {\n ReadonlyJSONObject,\n ReadonlyJSONValue,\n} from \"assistant-stream/utils\";\n\ntype DataPrefixedPart = {\n readonly type: `data-${string}`;\n readonly data: any;\n};\n\nexport type ThreadMessageLike = {\n readonly role: \"assistant\" | \"user\" | \"system\";\n readonly content:\n | string\n | readonly (\n | TextMessagePart\n | ReasoningMessagePart\n | SourceMessagePart\n | ImageMessagePart\n | FileMessagePart\n | DataMessagePart\n | GenerativeUIMessagePart\n | Unstable_AudioMessagePart\n | DataPrefixedPart\n | {\n readonly type: \"tool-call\";\n readonly toolCallId?: string;\n readonly toolName: string;\n readonly args?: ReadonlyJSONObject;\n readonly argsText?: string;\n readonly artifact?: any;\n readonly result?: any | undefined;\n readonly isError?: boolean | undefined;\n readonly parentId?: string | undefined;\n readonly messages?: readonly ThreadMessage[] | undefined;\n readonly interrupt?: { type: \"human\"; payload: unknown };\n readonly timing?: ToolCallTiming;\n readonly approval?: {\n readonly id: string;\n readonly approved?: boolean;\n readonly reason?: string;\n readonly isAutomatic?: boolean;\n readonly options?: readonly ToolApprovalOption[];\n readonly optionId?: string;\n readonly resolution?: \"cancelled\" | \"expired\";\n };\n }\n )[];\n readonly id?: string | undefined;\n readonly createdAt?: Date | undefined;\n readonly status?: MessageStatus | undefined;\n readonly attachments?:\n | readonly (Omit<CompleteAttachment, \"content\"> & {\n readonly content: readonly (ThreadUserMessagePart | DataPrefixedPart)[];\n })[]\n | undefined;\n readonly metadata?:\n | {\n readonly unstable_state?: ReadonlyJSONValue;\n readonly unstable_annotations?:\n | readonly ReadonlyJSONValue[]\n | undefined;\n readonly unstable_data?: readonly ReadonlyJSONValue[] | undefined;\n readonly steps?: readonly ThreadStep[] | undefined;\n readonly timing?: MessageTiming | undefined;\n readonly submittedFeedback?: { readonly type: \"positive\" | \"negative\" };\n readonly isOptimistic?: boolean | undefined;\n readonly custom?: Record<string, unknown> | undefined;\n }\n | undefined;\n};\n\nconst convertDataPrefixedPart = (\n type: string,\n data: unknown,\n): DataMessagePart | undefined => {\n if (!type.startsWith(\"data-\")) return undefined;\n return { type: \"data\", name: type.substring(5), data };\n};\n\n/**\n * @deprecated This API is experimental and may change without notice.\n */\nexport const fromThreadMessageLike = (\n like: ThreadMessageLike,\n fallbackId: string,\n fallbackStatus: MessageStatus,\n): ThreadMessage => {\n const { role, id, createdAt, attachments, status, metadata } = like;\n const common = {\n id: id ?? fallbackId,\n createdAt: createdAt ?? new Date(),\n };\n\n const content =\n typeof like.content === \"string\"\n ? [{ type: \"text\" as const, text: like.content }]\n : like.content;\n\n const sanitizeImageContent = ({\n image,\n ...rest\n }: ImageMessagePart): ImageMessagePart | null => {\n const dataUri = image.match(\n /^data:image\\/(png|jpeg|jpg|gif|webp|svg\\+xml);base64,(.*)$/,\n );\n if (dataUri) {\n return { ...rest, image };\n }\n if (/^(https:\\/\\/|blob:)/.test(image)) {\n return { ...rest, image };\n }\n console.warn(`Invalid image data format detected`);\n return null;\n };\n\n if (role !== \"user\" && attachments?.length)\n throw new Error(\"attachments are only supported for user messages\");\n\n if (role !== \"assistant\" && status)\n throw new Error(\"status is only supported for assistant messages\");\n\n if (role !== \"assistant\" && metadata?.steps)\n throw new Error(\"metadata.steps is only supported for assistant messages\");\n\n switch (role) {\n case \"assistant\":\n return {\n ...common,\n role,\n content: content\n .map((part): ThreadAssistantMessagePart | null => {\n const type = part.type;\n switch (type) {\n case \"text\":\n case \"reasoning\":\n if (part.text.trim().length === 0) return null;\n return part;\n\n case \"file\":\n case \"source\":\n return part;\n\n case \"image\":\n return sanitizeImageContent(part);\n\n case \"data\":\n return part;\n\n case \"generative-ui\":\n return part;\n\n case \"tool-call\": {\n const { parentId, messages, ...basePart } = part;\n const commonProps = {\n ...basePart,\n toolCallId: part.toolCallId ?? `tool-${generateId()}`,\n ...(parentId !== undefined && { parentId }),\n ...(messages !== undefined && { messages }),\n };\n\n if (part.args) {\n return {\n ...commonProps,\n args: part.args,\n argsText: part.argsText ?? JSON.stringify(part.args),\n };\n }\n return {\n ...commonProps,\n args: parsePartialJsonObject(part.argsText ?? \"\") ?? {},\n argsText: part.argsText ?? \"\",\n };\n }\n\n default: {\n const converted = convertDataPrefixedPart(\n type,\n (part as DataPrefixedPart).data,\n );\n if (converted) return converted;\n throw new Error(\n `Unsupported assistant message part type: ${type}`,\n );\n }\n }\n })\n .filter((c) => !!c),\n status: status ?? fallbackStatus,\n metadata: {\n unstable_state: metadata?.unstable_state ?? null,\n unstable_annotations: metadata?.unstable_annotations ?? [],\n unstable_data: metadata?.unstable_data ?? [],\n custom: metadata?.custom ?? {},\n steps: metadata?.steps ?? [],\n ...(metadata?.timing && { timing: metadata.timing }),\n ...(metadata?.submittedFeedback && {\n submittedFeedback: metadata.submittedFeedback,\n }),\n ...(metadata?.isOptimistic && { isOptimistic: true }),\n },\n } satisfies ThreadAssistantMessage;\n\n case \"user\":\n return {\n ...common,\n role,\n content: content.map((part): ThreadUserMessagePart => {\n const type = part.type;\n switch (type) {\n case \"text\":\n case \"image\":\n case \"audio\":\n case \"file\":\n case \"data\":\n return part;\n\n default: {\n const converted = convertDataPrefixedPart(\n type,\n (part as DataPrefixedPart).data,\n );\n if (converted) return converted;\n throw new Error(`Unsupported user message part type: ${type}`);\n }\n }\n }),\n attachments: (attachments ?? []).map((att) => ({\n ...att,\n content: att.content.map((part): ThreadUserMessagePart => {\n const converted = convertDataPrefixedPart(\n part.type,\n (part as DataPrefixedPart).data,\n );\n return converted ?? (part as ThreadUserMessagePart);\n }),\n })),\n metadata: {\n custom: metadata?.custom ?? {},\n },\n } satisfies ThreadUserMessage;\n\n case \"system\":\n if (content.length !== 1 || content[0]!.type !== \"text\")\n throw new Error(\n \"System messages must have exactly one text message part.\",\n );\n\n return {\n ...common,\n role,\n content: content as [TextMessagePart],\n metadata: {\n custom: metadata?.custom ?? {},\n },\n } satisfies ThreadSystemMessage;\n\n default: {\n const unsupportedRole: never = role;\n throw new Error(`Unknown message role: ${unsupportedRole}`);\n }\n }\n};\n"],"mappings":";;;AAkGA,MAAMgE,2BACJvC,MACAC,SACgC;CAChC,IAAI,CAACD,KAAKwC,WAAW,OAAO,GAAG,OAAOC,KAAAA;CACtC,OAAO;EAAEzC,MAAM;EAAQ0C,MAAM1C,KAAK2C,UAAU,CAAC;EAAG1C;CAAK;AACvD;;;;AAKA,MAAa2C,yBACXC,MACAC,YACAC,mBACkB;CAClB,MAAM,EAAE5C,MAAMe,IAAIO,WAAWG,aAAaD,QAAQG,aAAae;CAC/D,MAAMG,SAAS;EACb9B,IAAIA,MAAM4B;EACVrB,WAAWA,6BAAa,IAAIC,KAAK;CACnC;CAEA,MAAMtB,UACJ,OAAOyC,KAAKzC,YAAY,WACpB,CAAC;EAAEJ,MAAM;EAAiBkD,MAAML,KAAKzC;CAAQ,CAAC,IAC9CyC,KAAKzC;CAEX,MAAM+C,wBAAwB,EAC5BC,OACA,GAAGC,WAC4C;EAI/C,IAHgBD,MAAMG,MACpB,4DAEED,GACF,OAAO;GAAE,GAAGD;GAAMD;EAAM;EAE1B,IAAI,sBAAsBI,KAAKJ,KAAK,GAClC,OAAO;GAAE,GAAGC;GAAMD;EAAM;EAE1BK,QAAQC,KAAK,oCAAoC;EACjD,OAAO;CACT;CAEA,IAAIvD,SAAS,UAAUyB,aAAa+B,QAClC,MAAM,IAAIC,MAAM,kDAAkD;CAEpE,IAAIzD,SAAS,eAAewB,QAC1B,MAAM,IAAIiC,MAAM,iDAAiD;CAEnE,IAAIzD,SAAS,eAAe2B,UAAUI,OACpC,MAAM,IAAI0B,MAAM,yDAAyD;CAE3E,QAAQzD,MAAR;EACE,KAAK,aACH,OAAO;GACL,GAAG6C;GACH7C;GACAC,SAASA,QACNyD,KAAKC,SAA4C;IAChD,MAAM9D,OAAO8D,KAAK9D;IAClB,QAAQA,MAAR;KACE,KAAK;KACL,KAAK;MACH,IAAI8D,KAAKZ,KAAKa,KAAK,CAAC,CAACJ,WAAW,GAAG,OAAO;MAC1C,OAAOG;KAET,KAAK;KACL,KAAK,UACH,OAAOA;KAET,KAAK,SACH,OAAOX,qBAAqBW,IAAI;KAElC,KAAK,QACH,OAAOA;KAET,KAAK,iBACH,OAAOA;KAET,KAAK,aAAa;MAChB,MAAM,EAAElD,UAAUC,UAAU,GAAGmD,aAAaF;MAC5C,MAAMG,cAAc;OAClB,GAAGD;OACH3D,YAAYyD,KAAKzD,cAAc,QAAQ7B,WAAW;OAClD,GAAIoC,aAAa6B,KAAAA,KAAa,EAAE7B,SAAS;OACzC,GAAIC,aAAa4B,KAAAA,KAAa,EAAE5B,SAAS;MAC3C;MAEA,IAAIiD,KAAKvD,MACP,OAAO;OACL,GAAG0D;OACH1D,MAAMuD,KAAKvD;OACXC,UAAUsD,KAAKtD,YAAY0D,KAAKC,UAAUL,KAAKvD,IAAI;MACrD;MAEF,OAAO;OACL,GAAG0D;OACH1D,MAAMhC,uBAAuBuF,KAAKtD,YAAY,EAAE,KAAK,CAAC;OACtDA,UAAUsD,KAAKtD,YAAY;MAC7B;KACF;KAEA,SAAS;MACP,MAAM4D,YAAY7B,wBAChBvC,MACC8D,KAA0B7D,IAC7B;MACA,IAAImE,WAAW,OAAOA;MACtB,MAAM,IAAIR,MACR,4CAA4C5D,MAC9C;KACF;IACF;GACF,CAAC,CAAC,CACDqE,QAAQC,MAAM,CAAC,CAACA,CAAC;GACpB3C,QAAQA,UAAUoB;GAClBjB,UAAU;IACRC,gBAAgBD,UAAUC,kBAAkB;IAC5CC,sBAAsBF,UAAUE,wBAAwB,CAAA;IACxDC,eAAeH,UAAUG,iBAAiB,CAAA;IAC1CI,QAAQP,UAAUO,UAAU,CAAC;IAC7BH,OAAOJ,UAAUI,SAAS,CAAA;IAC1B,GAAIJ,UAAUd,UAAU,EAAEA,QAAQc,SAASd,OAAO;IAClD,GAAIc,UAAUK,qBAAqB,EACjCA,mBAAmBL,SAASK,kBAC9B;IACA,GAAIL,UAAUM,gBAAgB,EAAEA,cAAc,KAAK;GACrD;EACF;EAEF,KAAK,QACH,OAAO;GACL,GAAGY;GACH7C;GACAC,SAASA,QAAQyD,KAAKC,SAAgC;IACpD,MAAM9D,OAAO8D,KAAK9D;IAClB,QAAQA,MAAR;KACE,KAAK;KACL,KAAK;KACL,KAAK;KACL,KAAK;KACL,KAAK,QACH,OAAO8D;KAET,SAAS;MACP,MAAMM,YAAY7B,wBAChBvC,MACC8D,KAA0B7D,IAC7B;MACA,IAAImE,WAAW,OAAOA;MACtB,MAAM,IAAIR,MAAM,uCAAuC5D,MAAM;KAC/D;IACF;GACF,CAAC;GACD4B,cAAcA,eAAe,CAAA,EAAA,CAAIiC,KAAKU,SAAS;IAC7C,GAAGA;IACHnE,SAASmE,IAAInE,QAAQyD,KAAKC,SAAgC;KAKxD,OAJkBvB,wBAChBuB,KAAK9D,MACJ8D,KAA0B7D,IAEtBmE,KAAcN;IACvB,CAAC;GACH,EAAE;GACFhC,UAAU,EACRO,QAAQP,UAAUO,UAAU,CAAC,EAC/B;EACF;EAEF,KAAK;GACH,IAAIjC,QAAQuD,WAAW,KAAKvD,QAAQ,EAAE,CAAEJ,SAAS,QAC/C,MAAM,IAAI4D,MACR,0DACF;GAEF,OAAO;IACL,GAAGZ;IACH7C;IACSC;IACT0B,UAAU,EACRO,QAAQP,UAAUO,UAAU,CAAC,EAC/B;GACF;EAEF,SAEE,MAAM,IAAIuB,MAAM,yBAAyBY,MAAiB;CAE9D;AACF"}
1
+ {"version":3,"file":"thread-message-like.js","names":["parsePartialJsonObject","generateId","ReasoningMessagePart","SourceMessagePart","ThreadStep","MessageStatus","ImageMessagePart","ThreadMessage","ThreadAssistantMessagePart","ThreadAssistantMessage","ThreadUserMessagePart","ThreadUserMessage","ThreadSystemMessage","FileMessagePart","DataMessagePart","GenerativeUIMessagePart","Unstable_AudioMessagePart","CompleteAttachment","MessageTiming","TextMessagePart","ToolApprovalOption","ToolCallTiming","ReadonlyJSONObject","ReadonlyJSONValue","DataPrefixedPart","type","data","ThreadMessageLike","role","content","toolCallId","toolName","args","argsText","artifact","result","isError","parentId","messages","interrupt","payload","timing","approval","id","approved","reason","isAutomatic","options","optionId","resolution","createdAt","Date","status","attachments","Omit","metadata","unstable_state","unstable_annotations","unstable_data","steps","submittedFeedback","isOptimistic","custom","Record","convertDataPrefixedPart","startsWith","undefined","name","substring","fromThreadMessageLike","like","fallbackId","fallbackStatus","common","const","text","sanitizeImageContent","image","rest","dataUri","match","test","console","warn","length","Error","map","part","trim","basePart","commonProps","JSON","stringify","converted","filter","c","att","unsupportedRole"],"sources":["../../../src/runtime/utils/thread-message-like.ts"],"sourcesContent":["import { parsePartialJsonObject } from \"assistant-stream/utils\";\nimport { generateId } from \"../../utils/id\";\nimport type {\n ReasoningMessagePart,\n SourceMessagePart,\n ThreadStep,\n MessageStatus,\n ImageMessagePart,\n ThreadMessage,\n ThreadAssistantMessagePart,\n ThreadAssistantMessage,\n ThreadUserMessagePart,\n ThreadUserMessage,\n ThreadSystemMessage,\n FileMessagePart,\n DataMessagePart,\n GenerativeUIMessagePart,\n Unstable_AudioMessagePart,\n} from \"../../types/message\";\nimport type { CompleteAttachment } from \"../../types/attachment\";\nimport type {\n MessageTiming,\n TextMessagePart,\n ToolApprovalOption,\n ToolCallTiming,\n} from \"../../types/message\";\nimport type {\n ReadonlyJSONObject,\n ReadonlyJSONValue,\n} from \"assistant-stream/utils\";\n\ntype DataPrefixedPart = {\n readonly type: `data-${string}`;\n readonly data: any;\n};\n\nexport type ThreadMessageLike = {\n readonly role: \"assistant\" | \"user\" | \"system\";\n readonly content:\n | string\n | readonly (\n | TextMessagePart\n | ReasoningMessagePart\n | SourceMessagePart\n | ImageMessagePart\n | FileMessagePart\n | DataMessagePart\n | GenerativeUIMessagePart\n | Unstable_AudioMessagePart\n | DataPrefixedPart\n | {\n readonly type: \"tool-call\";\n readonly toolCallId?: string;\n readonly toolName: string;\n readonly args?: ReadonlyJSONObject;\n readonly argsText?: string;\n readonly artifact?: any;\n readonly result?: any | undefined;\n readonly isError?: boolean | undefined;\n readonly parentId?: string | undefined;\n readonly messages?: readonly ThreadMessage[] | undefined;\n readonly interrupt?: { type: \"human\"; payload: unknown };\n readonly timing?: ToolCallTiming;\n readonly approval?: {\n readonly id: string;\n readonly approved?: boolean;\n readonly reason?: string;\n readonly isAutomatic?: boolean;\n readonly options?: readonly ToolApprovalOption[];\n readonly optionId?: string;\n readonly resolution?: \"cancelled\" | \"expired\";\n };\n }\n )[];\n readonly id?: string | undefined;\n readonly createdAt?: Date | undefined;\n readonly status?: MessageStatus | undefined;\n readonly attachments?:\n | readonly (Omit<CompleteAttachment, \"content\"> & {\n readonly content: readonly (ThreadUserMessagePart | DataPrefixedPart)[];\n })[]\n | undefined;\n readonly metadata?:\n | {\n readonly unstable_state?: ReadonlyJSONValue;\n readonly unstable_annotations?:\n | readonly ReadonlyJSONValue[]\n | undefined;\n readonly unstable_data?: readonly ReadonlyJSONValue[] | undefined;\n readonly steps?: readonly ThreadStep[] | undefined;\n readonly timing?: MessageTiming | undefined;\n readonly submittedFeedback?: { readonly type: \"positive\" | \"negative\" };\n readonly isOptimistic?: boolean | undefined;\n readonly custom?: Record<string, unknown> | undefined;\n }\n | undefined;\n};\n\nconst convertDataPrefixedPart = (\n type: string,\n data: unknown,\n): DataMessagePart | undefined => {\n if (!type.startsWith(\"data-\")) return undefined;\n return { type: \"data\", name: type.substring(5), data };\n};\n\n/**\n * @deprecated This API is experimental and may change without notice.\n */\nexport const fromThreadMessageLike = (\n like: ThreadMessageLike,\n fallbackId: string,\n fallbackStatus: MessageStatus,\n): ThreadMessage => {\n const { role, id, createdAt, attachments, status, metadata } = like;\n const common = {\n id: id ?? fallbackId,\n createdAt: createdAt ?? new Date(),\n };\n\n const content =\n typeof like.content === \"string\"\n ? [{ type: \"text\" as const, text: like.content }]\n : like.content;\n\n const sanitizeImageContent = ({\n image,\n ...rest\n }: ImageMessagePart): ImageMessagePart | null => {\n if (typeof image !== \"string\") return null;\n const dataUri = image.match(\n /^data:image\\/(png|jpeg|jpg|gif|webp|svg\\+xml);base64,(.*)$/,\n );\n if (dataUri) {\n return { ...rest, image };\n }\n if (/^(https:\\/\\/|blob:)/.test(image)) {\n return { ...rest, image };\n }\n console.warn(`Invalid image data format detected`);\n return null;\n };\n\n if (role !== \"user\" && attachments?.length)\n throw new Error(\"attachments are only supported for user messages\");\n\n if (role !== \"assistant\" && status)\n throw new Error(\"status is only supported for assistant messages\");\n\n if (role !== \"assistant\" && metadata?.steps)\n throw new Error(\"metadata.steps is only supported for assistant messages\");\n\n switch (role) {\n case \"assistant\":\n return {\n ...common,\n role,\n content: content\n .map((part): ThreadAssistantMessagePart | null => {\n const type = part.type;\n switch (type) {\n case \"text\":\n case \"reasoning\":\n if (!part.text?.trim()) return null;\n return part;\n\n case \"file\":\n case \"source\":\n return part;\n\n case \"image\":\n return sanitizeImageContent(part);\n\n case \"data\":\n return part;\n\n case \"generative-ui\":\n return part;\n\n case \"tool-call\": {\n const { parentId, messages, ...basePart } = part;\n const commonProps = {\n ...basePart,\n toolCallId: part.toolCallId ?? `tool-${generateId()}`,\n ...(parentId !== undefined && { parentId }),\n ...(messages !== undefined && { messages }),\n };\n\n if (part.args) {\n return {\n ...commonProps,\n args: part.args,\n argsText: part.argsText ?? JSON.stringify(part.args),\n };\n }\n return {\n ...commonProps,\n args: parsePartialJsonObject(part.argsText ?? \"\") ?? {},\n argsText: part.argsText ?? \"\",\n };\n }\n\n default: {\n const converted = convertDataPrefixedPart(\n type,\n (part as DataPrefixedPart).data,\n );\n if (converted) return converted;\n throw new Error(\n `Unsupported assistant message part type: ${type}`,\n );\n }\n }\n })\n .filter((c) => !!c),\n status: status ?? fallbackStatus,\n metadata: {\n unstable_state: metadata?.unstable_state ?? null,\n unstable_annotations: metadata?.unstable_annotations ?? [],\n unstable_data: metadata?.unstable_data ?? [],\n custom: metadata?.custom ?? {},\n steps: metadata?.steps ?? [],\n ...(metadata?.timing && { timing: metadata.timing }),\n ...(metadata?.submittedFeedback && {\n submittedFeedback: metadata.submittedFeedback,\n }),\n ...(metadata?.isOptimistic && { isOptimistic: true }),\n },\n } satisfies ThreadAssistantMessage;\n\n case \"user\":\n return {\n ...common,\n role,\n content: content.map((part): ThreadUserMessagePart => {\n const type = part.type;\n switch (type) {\n case \"text\":\n case \"image\":\n case \"audio\":\n case \"file\":\n case \"data\":\n return part;\n\n default: {\n const converted = convertDataPrefixedPart(\n type,\n (part as DataPrefixedPart).data,\n );\n if (converted) return converted;\n throw new Error(`Unsupported user message part type: ${type}`);\n }\n }\n }),\n attachments: (attachments ?? []).map((att) => ({\n ...att,\n content: att.content.map((part): ThreadUserMessagePart => {\n const converted = convertDataPrefixedPart(\n part.type,\n (part as DataPrefixedPart).data,\n );\n return converted ?? (part as ThreadUserMessagePart);\n }),\n })),\n metadata: {\n custom: metadata?.custom ?? {},\n },\n } satisfies ThreadUserMessage;\n\n case \"system\":\n if (content.length !== 1 || content[0]!.type !== \"text\")\n throw new Error(\n \"System messages must have exactly one text message part.\",\n );\n\n return {\n ...common,\n role,\n content: content as [TextMessagePart],\n metadata: {\n custom: metadata?.custom ?? {},\n },\n } satisfies ThreadSystemMessage;\n\n default: {\n const unsupportedRole: never = role;\n throw new Error(`Unknown message role: ${unsupportedRole}`);\n }\n }\n};\n"],"mappings":"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"}
@@ -42,6 +42,13 @@ type ExternalStoreThreadListAdapter = {
42
42
  onDelete?: ((threadId: string) => Promise<void> | void) | undefined;
43
43
  };
44
44
  type ExternalStoreMessageConverter<T> = (message: T, idx: number) => ThreadMessageLike;
45
+ /**
46
+ * @deprecated This API is still under active development and might change without notice.
47
+ */
48
+ type ExternalStoreBranchChange = {
49
+ headId: string | null;
50
+ visibleMessageIds: readonly string[];
51
+ };
45
52
  type ExternalStoreMessageConverterAdapter<T> = {
46
53
  convertMessage: ExternalStoreMessageConverter<T>;
47
54
  };
@@ -78,6 +85,24 @@ type ExternalStoreAdapterBase<T> = {
78
85
  state?: ReadonlyJSONValue | undefined;
79
86
  extras?: unknown;
80
87
  setMessages?: ((messages: readonly T[]) => void) | undefined;
88
+ /**
89
+ * Fires when the user explicitly switches branches via the runtime's
90
+ * `switchToBranch` action (e.g. a BranchPicker click). It does not fire on
91
+ * adapter resync, `append`, edit/regenerate, content-only updates, or while
92
+ * the thread is running. Consecutive switches that resolve to the same
93
+ * canonical head are de-duped.
94
+ *
95
+ * `headId` is the canonical (persisted) head of the now-visible branch —
96
+ * optimistic/transient ids are never surfaced. `visibleMessageIds` lists the
97
+ * visible path in order.
98
+ *
99
+ * This complements `setMessages` rather than replacing it: switching still
100
+ * requires `setMessages`, and this callback does not on its own enable branch
101
+ * switching.
102
+ *
103
+ * @deprecated This API is still under active development and might change without notice.
104
+ */
105
+ unstable_onBranchChange?: ((event: ExternalStoreBranchChange) => void) | undefined;
81
106
  onImport?: ((messages: readonly ThreadMessage[]) => void) | undefined;
82
107
  onExportExternalState?: (() => any) | undefined;
83
108
  onLoadExternalState?: ((state: any) => void) | undefined;
@@ -142,5 +167,5 @@ type ExternalStoreAdapterBase<T> = {
142
167
  };
143
168
  type ExternalStoreAdapter<T = ThreadMessage> = ExternalStoreAdapterBase<T> & (T extends ThreadMessage ? object : ExternalStoreMessageConverterAdapter<T>);
144
169
  //#endregion
145
- export { ExternalStoreAdapter, ExternalStoreMessageConverter, ExternalStoreThreadData, ExternalStoreThreadListAdapter };
170
+ export { ExternalStoreAdapter, ExternalStoreBranchChange, ExternalStoreMessageConverter, ExternalStoreThreadData, ExternalStoreThreadListAdapter };
146
171
  //# sourceMappingURL=external-store-adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"external-store-adapter.d.ts","names":[],"sources":["../../../src/runtimes/external-store/external-store-adapter.ts"],"mappings":";;;;;;;;;;;;;KAqBY,uBAAA;EACV,MAAA,EAAQ,MAAA;EACR,EAAA;EACA,QAAA;EACA,UAAA;EACA,KAAA;EACA,MAAA,GAAS,MAAM;AAAA;AAAA,KAGL,8BAAA;EAPV;;;EAWA,QAAA;EACA,SAAA;EACA,OAAA,YAAmB,uBAAA;EACnB,eAAA,YAA2B,uBAAA;EAVZ;AAGjB;;EAWE,mBAAA,UAA6B,OAAA;EALV;;;EASnB,gBAAA,KAAqB,QAAA,aAAqB,OAAA;EAC1C,QAAA,IACE,QAAA,UACA,QAAA,cACI,OAAA;EACN,cAAA,KAEM,QAAA,UACA,MAAA,EAAQ,MAAA,kCACL,OAAA;EAET,SAAA,KAAc,QAAA,aAAqB,OAAA;EACnC,WAAA,KAAgB,QAAA,aAAqB,OAAA;EACrC,QAAA,KAAa,QAAA,aAAqB,OAAA;AAAA;AAAA,KAGxB,6BAAA,OACV,OAAA,EAAS,CAAA,EACT,GAAA,aACG,iBAAiB;AAAA,KAEjB,oCAAA;EACH,cAAA,EAAgB,6BAA6B,CAAC,CAAA;AAAA;AAAA,KAG3C,wBAAA;EAlCgB;;;;;;EAyCnB,UAAA;EAhC0C;;;;;;;;EAyC1C,cAAA;EAhCS;;;;;;;;EAyCT,SAAA;EACA,SAAA;EACA,QAAA,YAAoB,CAAA;EACpB,iBAAA,GAAoB,yBAAA;EACpB,WAAA,YAAuB,gBAAA;EACvB,KAAA,GAAQ,iBAAA;EACR,MAAA;EAEA,WAAA,KAAgB,QAAA,WAAmB,CAAA;EACnC,QAAA,KAAa,QAAA,WAAmB,aAAA;EAChC,qBAAA;EACA,mBAAA,KAAwB,KAAA;EACxB,KAAA,GAAQ,OAAA,EAAS,aAAA,KAAkB,OAAA,QA3ChC;EA6CH,KAAA,GAAQ,0BAAA;EACR,MAAA,KAAW,OAAA,EAAS,aAAA,KAAkB,OAAA;EACtC,QAAA,KAAa,SAAA,aAAsB,OAAA;EACnC,QAAA,KACM,QAAA,iBAAyB,MAAA,EAAQ,cAAA,KAAmB,OAAA;EAE1D,QAAA,KAAa,MAAA,EAAQ,eAAA,KAAoB,OAAA;EACzC,QAAA,UAAkB,OAAA;EAClB,eAAA,KACM,OAAA,EAAS,oBAAA,KAAyB,OAAA;EAExC,gBAAA,KACM,OAAA;IAAW,UAAA;IAAoB,OAAA;EAAA;EAErC,uBAAA,KACM,OAAA,EAAS,4BAAA,KAAiC,OAAA;EAEhD,cAAA,GAAiB,6BAAA,CAA8B,CAAA;EAC/C,QAAA;IAEM,WAAA,GAAc,iBAAA;IACd,MAAA,GAAS,sBAAA;IACT,SAAA,GAAY,gBAAA;IACZ,KAAA,GAAQ,oBAAA;IACR,QAAA,GAAW,eAAA;IA9BkB;;;IAkC7B,UAAA,GAAa,8BAAA;EAAA;EAGnB,qBAAA;IAEM,IAAA;EAAA;EA7BiC;;;;;;;;;;;;;;;;;;;EAmDvC,8BAAA;EAWsB;;;;;;;;;EADtB,eAAA,KACM,QAAA,EAAU,MAAA,SAAe,mBAAA;AAAA;AAAA,KAIrB,oBAAA,KAAyB,aAAA,IACnC,wBAAA,CAAyB,CAAA,KACtB,CAAA,SAAU,aAAA,YAEP,oCAAA,CAAqC,CAAA"}
1
+ {"version":3,"file":"external-store-adapter.d.ts","names":[],"sources":["../../../src/runtimes/external-store/external-store-adapter.ts"],"mappings":";;;;;;;;;;;;;KAqBY,uBAAA;EACV,MAAA,EAAQ,MAAA;EACR,EAAA;EACA,QAAA;EACA,UAAA;EACA,KAAA;EACA,MAAA,GAAS,MAAM;AAAA;AAAA,KAGL,8BAAA;EAPV;;;EAWA,QAAA;EACA,SAAA;EACA,OAAA,YAAmB,uBAAA;EACnB,eAAA,YAA2B,uBAAA;EAVZ;AAGjB;;EAWE,mBAAA,UAA6B,OAAA;EALV;;;EASnB,gBAAA,KAAqB,QAAA,aAAqB,OAAA;EAC1C,QAAA,IACE,QAAA,UACA,QAAA,cACI,OAAA;EACN,cAAA,KAEM,QAAA,UACA,MAAA,EAAQ,MAAA,kCACL,OAAA;EAET,SAAA,KAAc,QAAA,aAAqB,OAAA;EACnC,WAAA,KAAgB,QAAA,aAAqB,OAAA;EACrC,QAAA,KAAa,QAAA,aAAqB,OAAA;AAAA;AAAA,KAGxB,6BAAA,OACV,OAAA,EAAS,CAAA,EACT,GAAA,aACG,iBAAiB;;;;KAKV,yBAAA;EACV,MAAA;EACA,iBAAiB;AAAA;AAAA,KAGd,oCAAA;EACH,cAAA,EAAgB,6BAA6B,CAAC,CAAA;AAAA;AAAA,KAG3C,wBAAA;EAjCuC;;;;;;EAwC1C,UAAA;EAhCc;;;;;;;;EAyCd,cAAA;EApCA;;;;AAAyC;AAG3C;;;EA0CE,SAAA;EACA,SAAA;EACA,QAAA,YAAoB,CAAA;EACpB,iBAAA,GAAoB,yBAAA;EACpB,WAAA,YAAuB,gBAAA;EACvB,KAAA,GAAQ,iBAAA;EACR,MAAA;EAEA,WAAA,KAAgB,QAAA,WAAmB,CAAA;EA1CzB;;;;AAEO;AACjB;;;;;;;;;AAG+C;AAAA;;EAsD/C,uBAAA,KACM,KAAA,EAAO,yBAAA;EAEb,QAAA,KAAa,QAAA,WAAmB,aAAA;EAChC,qBAAA;EACA,mBAAA,KAAwB,KAAA;EACxB,KAAA,GAAQ,OAAA,EAAS,aAAA,KAAkB,OAAA,QAxBA;EA0BnC,KAAA,GAAQ,0BAAA;EACR,MAAA,KAAW,OAAA,EAAS,aAAA,KAAkB,OAAA;EACtC,QAAA,KAAa,SAAA,aAAsB,OAAA;EACnC,QAAA,KACM,QAAA,iBAAyB,MAAA,EAAQ,cAAA,KAAmB,OAAA;EAE1D,QAAA,KAAa,MAAA,EAAQ,eAAA,KAAoB,OAAA;EACzC,QAAA,UAAkB,OAAA;EAClB,eAAA,KACM,OAAA,EAAS,oBAAA,KAAyB,OAAA;EAExC,gBAAA,KACM,OAAA;IAAW,UAAA;IAAoB,OAAA;EAAA;EAErC,uBAAA,KACM,OAAA,EAAS,4BAAA,KAAiC,OAAA;EAEhD,cAAA,GAAiB,6BAAA,CAA8B,CAAA;EAC/C,QAAA;IAEM,WAAA,GAAc,iBAAA;IACd,MAAA,GAAS,sBAAA;IACT,SAAA,GAAY,gBAAA;IACZ,KAAA,GAAQ,oBAAA;IACR,QAAA,GAAW,eAAA;IAJG;;;IAQd,UAAA,GAAa,8BAAA;EAAA;EAGnB,qBAAA;IAEM,IAAA;EAAA;EAiCgB;;;;;;;;;;;;;;;;;;;EAXtB,8BAAA;EA9Da;;;;;;;;;EAwEb,eAAA,KACM,QAAA,EAAU,MAAA,SAAe,mBAAA;AAAA;AAAA,KAIrB,oBAAA,KAAyB,aAAA,IACnC,wBAAA,CAAyB,CAAA,KACtB,CAAA,SAAU,aAAA,YAEP,oCAAA,CAAqC,CAAA"}
@@ -63,6 +63,15 @@ declare class ExternalStoreThreadRuntimeCore extends BaseThreadRuntimeCore imple
63
63
  */
64
64
  private _findMessageIdForToolCall;
65
65
  switchToBranch(branchId: string): void;
66
+ /**
67
+ * Emit `unstable_onBranchChange` for an explicit branch switch. Reads the
68
+ * canonical head from the repository (which skips optimistic/transient
69
+ * messages) and de-dupes switches that leave the canonical head unchanged.
70
+ * Comparing against the head observed just before the switch — rather than the
71
+ * last emitted head — keeps a switch firing after an adapter resync moved the
72
+ * head elsewhere in the meantime.
73
+ */
74
+ private _notifyBranchChange;
66
75
  append(message: AppendMessage): Promise<void>;
67
76
  deleteMessage(messageId: string): Promise<void>;
68
77
  getQueueItems(): readonly QueueItemState[];
@@ -1 +1 @@
1
- {"version":3,"file":"external-store-thread-runtime-core.d.ts","names":[],"sources":["../../../src/runtimes/external-store/external-store-thread-runtime-core.ts"],"mappings":";;;;;;;;;;;;;cA+Ca,kBAAA,GACX,SAAA,WACA,QAAA,WAAmB,aAAa;AAAA,cAKrB,8BAAA,SACH,qBAAA,YACG,iBAAA;EAAA,QAEH,aAAA;EAAA,IAgBG,YAAA,IAAY,mBAAA;EAAA,QAIf,SAAA;EACD,UAAA;EACA,cAAA;EAAA,IACI,SAAA;EAAA,IAIA,SAAA;EAAA,UAIQ,gBAAA,aAA6B,aAAA;EAAA,IAI5B,KAAA,iEAAK,kBAAA,oCAAA,iBAAA;EAAA,IAId,QAAA;kBARkD,iBAAA;;;;;;;EAYtD,WAAA,WAAsB,gBAAA;EACtB,MAAA;EAAA,QAEC,UAAA;EAAA,QAEA,MAAA;;;;;UAMA,gBAAA;EAEQ,SAAA,CAAU,SAAA;cAQxB,eAAA,EAAiB,oBAAA,EACjB,KAAA,EAAO,oBAAA;EAMF,qBAAA,CAAsB,KAAA,EAAO,oBAAA;EAAA;;;;;;EAAA,QA8K5B,qBAAA;EA0LuB;;;;EAAA,QAvHvB,oBAAA;EAAA,QACA,yBAAA;EAuOqB;;;;;;EAAA,QA/NrB,yBAAA;EAmBQ,cAAA,CAAe,QAAA;EAalB,MAAA,CAAO,OAAA,EAAS,aAAA,GAAgB,OAAA;EA8BhC,aAAA,CAAc,SAAA,WAAoB,OAAA;EAoBxC,aAAA,aApB+C,cAAA;EA0B/C,cAAA,CAAe,WAAA;EAIf,eAAA,CAAgB,WAAA;EAIV,QAAA,CAAS,MAAA,EAAQ,cAAA,GAAiB,OAAA;EAclC,SAAA,CAAU,MAAA,EAAQ,eAAA,GAAkB,OAAA;EAO1C,mBAAA;EAOA,mBAAA,CAAoB,KAAA;EAmBpB,SAAA;EA0CA,aAAA,CAAc,OAAA,EAAS,oBAAA;EAMvB,cAAA,CAAe,OAAA,EAAS,qBAAA;EAoBxB,qBAAA,CAAsB,OAAA,EAAS,4BAAA;EAMtB,KAAA,CAAM,eAAA,YAA2B,iBAAA;EAMjC,MAAA,CAAO,IAAA,EAAM,yBAAA;EAAA,QAQrB,cAAA;AAAA"}
1
+ {"version":3,"file":"external-store-thread-runtime-core.d.ts","names":[],"sources":["../../../src/runtimes/external-store/external-store-thread-runtime-core.ts"],"mappings":";;;;;;;;;;;;;cAkDa,kBAAA,GACX,SAAA,WACA,QAAA,WAAmB,aAAa;AAAA,cAKrB,8BAAA,SACH,qBAAA,YACG,iBAAA;EAAA,QAEH,aAAA;EAAA,IAgBG,YAAA,IAAY,mBAAA;EAAA,QAIf,SAAA;EACD,UAAA;EACA,cAAA;EAAA,IACI,SAAA;EAAA,IAIA,SAAA;EAAA,UAIQ,gBAAA,aAA6B,aAAA;EAAA,IAI5B,KAAA,iEAAK,kBAAA,oCAAA,iBAAA;EAAA,IAId,QAAA;kBARkD,iBAAA;;;;;;;EAYtD,WAAA,WAAsB,gBAAA;EACtB,MAAA;EAAA,QAEC,UAAA;EAAA,QAEA,MAAA;;;;;UAMA,gBAAA;EAEQ,SAAA,CAAU,SAAA;cAQxB,eAAA,EAAiB,oBAAA,EACjB,KAAA,EAAO,oBAAA;EAMF,qBAAA,CAAsB,KAAA,EAAO,oBAAA;EAAA;;;;;;EAAA,QA8K5B,qBAAA;EAuNuB;;;;EAAA,QApJvB,oBAAA;EAAA,QACA,yBAAA;EAoQqB;;;;;;EAAA,QA5PrB,yBAAA;EAmBQ,cAAA,CAAe,QAAA;EApUpB;;;;;;;;EAAA,QAiWH,mBAAA;EAaK,MAAA,CAAO,OAAA,EAAS,aAAA,GAAgB,OAAA;EA8BhC,aAAA,CAAc,SAAA,WAAoB,OAAA;EAoBxC,aAAA,aApB+C,cAAA;EA0B/C,cAAA,CAAe,WAAA;EAIf,eAAA,CAAgB,WAAA;EAIV,QAAA,CAAS,MAAA,EAAQ,cAAA,GAAiB,OAAA;EAclC,SAAA,CAAU,MAAA,EAAQ,eAAA,GAAkB,OAAA;EAO1C,mBAAA;EAOA,mBAAA,CAAoB,KAAA;EAmBpB,SAAA;EA0CA,aAAA,CAAc,OAAA,EAAS,oBAAA;EAMvB,cAAA,CAAe,OAAA,EAAS,qBAAA;EAoBxB,qBAAA,CAAsB,OAAA,EAAS,4BAAA;EAMtB,KAAA,CAAM,eAAA,YAA2B,iBAAA;EAMjC,MAAA,CAAO,IAAA,EAAM,yBAAA;EAAA,QAQrB,cAAA;AAAA"}
@@ -232,8 +232,27 @@ var ExternalStoreThreadRuntimeCore = class extends BaseThreadRuntimeCore {
232
232
  switchToBranch(branchId) {
233
233
  if (!this._store.setMessages) throw new Error("Runtime does not support switching branches.");
234
234
  if (this._store.isRunning) return;
235
+ const onBranchChange = this._store.unstable_onBranchChange;
236
+ const previousHeadId = onBranchChange ? this.repository.canonicalHeadId : null;
235
237
  this.repository.switchToBranch(branchId);
236
238
  this.updateMessages(this.repository.getMessages());
239
+ if (onBranchChange) this._notifyBranchChange(previousHeadId, onBranchChange);
240
+ }
241
+ /**
242
+ * Emit `unstable_onBranchChange` for an explicit branch switch. Reads the
243
+ * canonical head from the repository (which skips optimistic/transient
244
+ * messages) and de-dupes switches that leave the canonical head unchanged.
245
+ * Comparing against the head observed just before the switch — rather than the
246
+ * last emitted head — keeps a switch firing after an adapter resync moved the
247
+ * head elsewhere in the meantime.
248
+ */
249
+ _notifyBranchChange(previousHeadId, onBranchChange) {
250
+ const headId = this.repository.canonicalHeadId;
251
+ if (headId === previousHeadId) return;
252
+ onBranchChange({
253
+ headId,
254
+ visibleMessageIds: this.repository.getMessages().map((m) => m.id)
255
+ });
237
256
  }
238
257
  async append(message) {
239
258
  const isEdit = message.parentId !== (this.messages.at(-1)?.id ?? null);