@copilotkit/react-core 1.57.3 → 1.58.0-canary.thread-id-propagation

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 (292) hide show
  1. package/LICENSE +21 -0
  2. package/dist/{copilotkit-CtXcs1ea.cjs → copilotkit-B4ouY7qC.cjs} +14 -3
  3. package/dist/copilotkit-B4ouY7qC.cjs.map +1 -0
  4. package/dist/copilotkit-BK9CVq9A.d.cts.map +1 -1
  5. package/dist/{copilotkit-CC8DjOiC.mjs → copilotkit-L4mM_JqG.mjs} +14 -3
  6. package/dist/copilotkit-L4mM_JqG.mjs.map +1 -0
  7. package/dist/copilotkit-WlmeVijs.d.mts.map +1 -1
  8. package/dist/index.cjs +3 -77
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +2 -2
  11. package/dist/index.d.mts +2 -2
  12. package/dist/index.mjs +3 -77
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/index.umd.js +15 -78
  15. package/dist/index.umd.js.map +1 -1
  16. package/dist/v2/headless.cjs +11 -0
  17. package/dist/v2/headless.cjs.map +1 -1
  18. package/dist/v2/headless.d.cts.map +1 -1
  19. package/dist/v2/headless.d.mts.map +1 -1
  20. package/dist/v2/headless.mjs +11 -0
  21. package/dist/v2/headless.mjs.map +1 -1
  22. package/dist/v2/index.cjs +1 -1
  23. package/dist/v2/index.mjs +1 -1
  24. package/dist/v2/index.umd.js +13 -2
  25. package/dist/v2/index.umd.js.map +1 -1
  26. package/package.json +12 -13
  27. package/skills/react-core/SKILL.md +108 -0
  28. package/skills/react-core/references/agent-access.md +288 -0
  29. package/skills/react-core/references/attachments.md +291 -0
  30. package/skills/react-core/references/capabilities.md +138 -0
  31. package/skills/react-core/references/chat-components.md +221 -0
  32. package/skills/react-core/references/client-side-tools.md +358 -0
  33. package/skills/react-core/references/custom-message-renderers.md +226 -0
  34. package/skills/react-core/references/debug-mode.md +153 -0
  35. package/skills/react-core/references/human-in-the-loop.md +312 -0
  36. package/skills/react-core/references/provider-setup.md +326 -0
  37. package/skills/react-core/references/rendering-activity-messages.md +207 -0
  38. package/skills/react-core/references/rendering-tool-calls.md +319 -0
  39. package/skills/react-core/references/suggestions.md +211 -0
  40. package/skills/react-core/references/switching-agents-recipes.md +160 -0
  41. package/skills/react-core/references/switching-agents.md +231 -0
  42. package/skills/react-core/references/threads.md +226 -0
  43. package/.attw.json +0 -3
  44. package/CHANGELOG.md +0 -5043
  45. package/dist/copilotkit-CC8DjOiC.mjs.map +0 -1
  46. package/dist/copilotkit-CtXcs1ea.cjs.map +0 -1
  47. package/scripts/scope-preflight.mjs +0 -100
  48. package/src/components/CopilotListeners.tsx +0 -137
  49. package/src/components/__tests__/CopilotListeners.test.tsx +0 -38
  50. package/src/components/copilot-provider/__tests__/copilot-messages-key.test.tsx +0 -92
  51. package/src/components/copilot-provider/__tests__/copilotkit-error.test.tsx +0 -77
  52. package/src/components/copilot-provider/__tests__/error-visibility-prod.test.tsx +0 -70
  53. package/src/components/copilot-provider/__tests__/v1-explicit-threadid-bridge.test.tsx +0 -107
  54. package/src/components/copilot-provider/copilot-messages.tsx +0 -314
  55. package/src/components/copilot-provider/copilotkit-props.tsx +0 -214
  56. package/src/components/copilot-provider/copilotkit.tsx +0 -853
  57. package/src/components/copilot-provider/index.ts +0 -3
  58. package/src/components/dev-console/console-trigger.tsx +0 -283
  59. package/src/components/dev-console/developer-console-modal.tsx +0 -1016
  60. package/src/components/dev-console/icons.tsx +0 -106
  61. package/src/components/error-boundary/error-boundary.tsx +0 -99
  62. package/src/components/error-boundary/error-utils.tsx +0 -105
  63. package/src/components/index.ts +0 -1
  64. package/src/components/toast/exclamation-mark-icon.tsx +0 -27
  65. package/src/components/toast/toast-provider.tsx +0 -448
  66. package/src/components/usage-banner.tsx +0 -266
  67. package/src/context/__tests__/threads-context.test.tsx +0 -141
  68. package/src/context/coagent-state-renders-context.tsx +0 -89
  69. package/src/context/copilot-context.tsx +0 -365
  70. package/src/context/copilot-messages-context.tsx +0 -35
  71. package/src/context/index.ts +0 -22
  72. package/src/context/threads-context.tsx +0 -69
  73. package/src/hooks/__tests__/use-coagent-config.test.ts +0 -352
  74. package/src/hooks/__tests__/use-coagent-state-render-bridge.helpers.test.ts +0 -107
  75. package/src/hooks/__tests__/use-coagent-state-render.e2e.test.tsx +0 -1209
  76. package/src/hooks/__tests__/use-coagent-state-render.test.tsx +0 -356
  77. package/src/hooks/__tests__/use-copilot-chat-internal-connect.test.tsx +0 -241
  78. package/src/hooks/__tests__/use-frontend-tool-available.test.tsx +0 -72
  79. package/src/hooks/__tests__/use-frontend-tool-remount.e2e.test.tsx +0 -102
  80. package/src/hooks/index.ts +0 -33
  81. package/src/hooks/use-agent-nodename.ts +0 -33
  82. package/src/hooks/use-coagent-state-render-bridge.helpers.ts +0 -345
  83. package/src/hooks/use-coagent-state-render-bridge.tsx +0 -222
  84. package/src/hooks/use-coagent-state-render-registry.ts +0 -230
  85. package/src/hooks/use-coagent-state-render.ts +0 -163
  86. package/src/hooks/use-coagent.ts +0 -377
  87. package/src/hooks/use-configure-chat-suggestions.tsx +0 -96
  88. package/src/hooks/use-copilot-action.ts +0 -245
  89. package/src/hooks/use-copilot-additional-instructions.ts +0 -98
  90. package/src/hooks/use-copilot-authenticated-action.ts +0 -73
  91. package/src/hooks/use-copilot-chat-headless_c.ts +0 -264
  92. package/src/hooks/use-copilot-chat-suggestions.tsx +0 -134
  93. package/src/hooks/use-copilot-chat.ts +0 -132
  94. package/src/hooks/use-copilot-chat_internal.ts +0 -875
  95. package/src/hooks/use-copilot-readable.ts +0 -135
  96. package/src/hooks/use-copilot-runtime-client.ts +0 -178
  97. package/src/hooks/use-default-tool.ts +0 -13
  98. package/src/hooks/use-flat-category-store.ts +0 -109
  99. package/src/hooks/use-frontend-tool.ts +0 -113
  100. package/src/hooks/use-human-in-the-loop.ts +0 -138
  101. package/src/hooks/use-langgraph-interrupt.ts +0 -103
  102. package/src/hooks/use-lazy-tool-renderer.tsx +0 -30
  103. package/src/hooks/use-make-copilot-document-readable.ts +0 -30
  104. package/src/hooks/use-render-tool-call.ts +0 -89
  105. package/src/hooks/use-tree.ts +0 -222
  106. package/src/index.tsx +0 -7
  107. package/src/lib/copilot-task.ts +0 -215
  108. package/src/lib/index.ts +0 -1
  109. package/src/lib/status-checker.ts +0 -67
  110. package/src/setupTests.ts +0 -37
  111. package/src/test-helpers/copilot-context.ts +0 -91
  112. package/src/types/chat-suggestion-configuration.ts +0 -23
  113. package/src/types/coagent-action.ts +0 -35
  114. package/src/types/coagent-state.ts +0 -13
  115. package/src/types/crew.ts +0 -89
  116. package/src/types/document-pointer.ts +0 -7
  117. package/src/types/frontend-action.ts +0 -213
  118. package/src/types/index.ts +0 -17
  119. package/src/types/interrupt-action.ts +0 -58
  120. package/src/types/system-message.ts +0 -4
  121. package/src/utils/dev-console.ts +0 -19
  122. package/src/utils/index.ts +0 -2
  123. package/src/utils/suggestions-constants.ts +0 -8
  124. package/src/utils/utils.test.ts +0 -7
  125. package/src/utils/utils.ts +0 -6
  126. package/src/v2/__tests__/A2UIMessageRenderer.test.tsx +0 -240
  127. package/src/v2/__tests__/globalSetup.ts +0 -14
  128. package/src/v2/__tests__/setup.ts +0 -93
  129. package/src/v2/__tests__/utils/test-helpers.tsx +0 -570
  130. package/src/v2/a2ui/A2UICatalogContext.tsx +0 -79
  131. package/src/v2/a2ui/A2UIMessageRenderer.tsx +0 -294
  132. package/src/v2/a2ui/A2UIToolCallRenderer.tsx +0 -290
  133. package/src/v2/components/CopilotKitInspector.tsx +0 -52
  134. package/src/v2/components/MCPAppsActivityRenderer.tsx +0 -815
  135. package/src/v2/components/OpenGenerativeUIRenderer.tsx +0 -598
  136. package/src/v2/components/WildcardToolCallRender.tsx +0 -86
  137. package/src/v2/components/__tests__/OpenGenerativeUIRenderer.test.tsx +0 -665
  138. package/src/v2/components/chat/CopilotChat.tsx +0 -664
  139. package/src/v2/components/chat/CopilotChatAssistantMessage.tsx +0 -393
  140. package/src/v2/components/chat/CopilotChatAttachmentQueue.tsx +0 -374
  141. package/src/v2/components/chat/CopilotChatAttachmentRenderer.tsx +0 -159
  142. package/src/v2/components/chat/CopilotChatAudioRecorder.tsx +0 -350
  143. package/src/v2/components/chat/CopilotChatInput.tsx +0 -1412
  144. package/src/v2/components/chat/CopilotChatMessageView.tsx +0 -716
  145. package/src/v2/components/chat/CopilotChatReasoningMessage.tsx +0 -265
  146. package/src/v2/components/chat/CopilotChatSuggestionPill.tsx +0 -59
  147. package/src/v2/components/chat/CopilotChatSuggestionView.tsx +0 -134
  148. package/src/v2/components/chat/CopilotChatToggleButton.tsx +0 -171
  149. package/src/v2/components/chat/CopilotChatToolCallsView.tsx +0 -40
  150. package/src/v2/components/chat/CopilotChatUserMessage.tsx +0 -445
  151. package/src/v2/components/chat/CopilotChatView.tsx +0 -890
  152. package/src/v2/components/chat/CopilotModalHeader.tsx +0 -129
  153. package/src/v2/components/chat/CopilotPopup.tsx +0 -81
  154. package/src/v2/components/chat/CopilotPopupView.tsx +0 -317
  155. package/src/v2/components/chat/CopilotSidebar.tsx +0 -80
  156. package/src/v2/components/chat/CopilotSidebarView.tsx +0 -269
  157. package/src/v2/components/chat/Lightbox.tsx +0 -103
  158. package/src/v2/components/chat/__tests__/CopilotChat.absentThreadConnect.test.tsx +0 -66
  159. package/src/v2/components/chat/__tests__/CopilotChat.attachments.test.tsx +0 -168
  160. package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.tsx +0 -1239
  161. package/src/v2/components/chat/__tests__/CopilotChat.onError.test.tsx +0 -73
  162. package/src/v2/components/chat/__tests__/CopilotChat.slots.e2e.test.tsx +0 -432
  163. package/src/v2/components/chat/__tests__/CopilotChat.suggestionsAlways.test.tsx +0 -183
  164. package/src/v2/components/chat/__tests__/CopilotChat.welcomeGate.test.tsx +0 -184
  165. package/src/v2/components/chat/__tests__/CopilotChatActivityRendering.e2e.test.tsx +0 -649
  166. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.slots.e2e.test.tsx +0 -624
  167. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.test.tsx +0 -702
  168. package/src/v2/components/chat/__tests__/CopilotChatAssistantMessage.thumbs.test.tsx +0 -72
  169. package/src/v2/components/chat/__tests__/CopilotChatCopyButton.clipboard.test.tsx +0 -241
  170. package/src/v2/components/chat/__tests__/CopilotChatCssClasses.test.tsx +0 -107
  171. package/src/v2/components/chat/__tests__/CopilotChatInput.slots.e2e.test.tsx +0 -929
  172. package/src/v2/components/chat/__tests__/CopilotChatInput.test.tsx +0 -1567
  173. package/src/v2/components/chat/__tests__/CopilotChatMessageView.slots.e2e.test.tsx +0 -1004
  174. package/src/v2/components/chat/__tests__/CopilotChatMessageView.test.tsx +0 -279
  175. package/src/v2/components/chat/__tests__/CopilotChatPerf.e2e.test.tsx +0 -336
  176. package/src/v2/components/chat/__tests__/CopilotChatPropsRerender.e2e.test.tsx +0 -249
  177. package/src/v2/components/chat/__tests__/CopilotChatSuggestionView.slots.e2e.test.tsx +0 -530
  178. package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.tsx +0 -785
  179. package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.tsx +0 -2416
  180. package/src/v2/components/chat/__tests__/CopilotChatUserMessage.slots.e2e.test.tsx +0 -621
  181. package/src/v2/components/chat/__tests__/CopilotChatView.connectingGate.test.tsx +0 -56
  182. package/src/v2/components/chat/__tests__/CopilotChatView.inputOverlay.test.tsx +0 -264
  183. package/src/v2/components/chat/__tests__/CopilotChatView.onClick.e2e.test.tsx +0 -853
  184. package/src/v2/components/chat/__tests__/CopilotChatView.pinToSend.test.tsx +0 -94
  185. package/src/v2/components/chat/__tests__/CopilotChatView.slots.e2e.test.tsx +0 -1050
  186. package/src/v2/components/chat/__tests__/CopilotModalHeader.slots.e2e.test.tsx +0 -484
  187. package/src/v2/components/chat/__tests__/CopilotPopupView.slots.e2e.test.tsx +0 -612
  188. package/src/v2/components/chat/__tests__/CopilotSidebarView.position.test.tsx +0 -159
  189. package/src/v2/components/chat/__tests__/CopilotSidebarView.slots.e2e.test.tsx +0 -502
  190. package/src/v2/components/chat/__tests__/MCPAppsActivityRenderer.e2e.test.tsx +0 -1068
  191. package/src/v2/components/chat/__tests__/MCPAppsProxy.e2e.test.tsx +0 -589
  192. package/src/v2/components/chat/__tests__/MCPAppsUiMessage.e2e.test.tsx +0 -403
  193. package/src/v2/components/chat/__tests__/copilot-chat-throttle.test.tsx +0 -137
  194. package/src/v2/components/chat/__tests__/normalize-auto-scroll.test.ts +0 -37
  195. package/src/v2/components/chat/__tests__/setup.ts +0 -1
  196. package/src/v2/components/chat/index.ts +0 -90
  197. package/src/v2/components/chat/last-user-message-context.ts +0 -21
  198. package/src/v2/components/chat/normalize-auto-scroll.ts +0 -17
  199. package/src/v2/components/chat/scroll-element-context.ts +0 -13
  200. package/src/v2/components/index.ts +0 -8
  201. package/src/v2/components/intelligence-indicator/IntelligenceIndicator.tsx +0 -286
  202. package/src/v2/components/intelligence-indicator/__tests__/IntelligenceIndicator.e2e.test.tsx +0 -464
  203. package/src/v2/components/intelligence-indicator/index.ts +0 -2
  204. package/src/v2/components/license-warning-banner.tsx +0 -217
  205. package/src/v2/components/ui/button.tsx +0 -124
  206. package/src/v2/components/ui/dropdown-menu.tsx +0 -258
  207. package/src/v2/components/ui/tooltip.tsx +0 -60
  208. package/src/v2/context.ts +0 -62
  209. package/src/v2/headless.ts +0 -64
  210. package/src/v2/hooks/__tests__/standard-schema-types.test.tsx +0 -152
  211. package/src/v2/hooks/__tests__/standard-schema.test.tsx +0 -282
  212. package/src/v2/hooks/__tests__/use-agent-context-timing.e2e.test.tsx +0 -140
  213. package/src/v2/hooks/__tests__/use-agent-context.test.tsx +0 -401
  214. package/src/v2/hooks/__tests__/use-agent-error-state.test.tsx +0 -44
  215. package/src/v2/hooks/__tests__/use-agent-stability.test.tsx +0 -211
  216. package/src/v2/hooks/__tests__/use-agent-throttle.test.tsx +0 -1029
  217. package/src/v2/hooks/__tests__/use-agent.e2e.test.tsx +0 -159
  218. package/src/v2/hooks/__tests__/use-attachments.test.tsx +0 -169
  219. package/src/v2/hooks/__tests__/use-capabilities.test.tsx +0 -76
  220. package/src/v2/hooks/__tests__/use-component.test.tsx +0 -126
  221. package/src/v2/hooks/__tests__/use-configure-suggestions.e2e.test.tsx +0 -696
  222. package/src/v2/hooks/__tests__/use-default-render-tool.test.tsx +0 -153
  223. package/src/v2/hooks/__tests__/use-frontend-tool-available.test.tsx +0 -167
  224. package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.tsx +0 -2148
  225. package/src/v2/hooks/__tests__/use-human-in-the-loop.e2e.test.tsx +0 -1261
  226. package/src/v2/hooks/__tests__/use-interrupt.test.tsx +0 -397
  227. package/src/v2/hooks/__tests__/use-katex-styles.test.tsx +0 -56
  228. package/src/v2/hooks/__tests__/use-keyboard-height.test.tsx +0 -192
  229. package/src/v2/hooks/__tests__/use-pin-to-send.test.tsx +0 -219
  230. package/src/v2/hooks/__tests__/use-render-custom-messages.test.tsx +0 -55
  231. package/src/v2/hooks/__tests__/use-render-tool.test.tsx +0 -259
  232. package/src/v2/hooks/__tests__/use-suggestions.e2e.test.tsx +0 -524
  233. package/src/v2/hooks/__tests__/use-threads.test.tsx +0 -757
  234. package/src/v2/hooks/__tests__/zod-regression.test.tsx +0 -311
  235. package/src/v2/hooks/index.ts +0 -24
  236. package/src/v2/hooks/use-agent-context.tsx +0 -45
  237. package/src/v2/hooks/use-agent.tsx +0 -227
  238. package/src/v2/hooks/use-attachments.tsx +0 -269
  239. package/src/v2/hooks/use-capabilities.tsx +0 -25
  240. package/src/v2/hooks/use-component.tsx +0 -91
  241. package/src/v2/hooks/use-configure-suggestions.tsx +0 -236
  242. package/src/v2/hooks/use-default-render-tool.tsx +0 -271
  243. package/src/v2/hooks/use-frontend-tool.tsx +0 -46
  244. package/src/v2/hooks/use-human-in-the-loop.tsx +0 -81
  245. package/src/v2/hooks/use-interrupt.tsx +0 -305
  246. package/src/v2/hooks/use-keyboard-height.tsx +0 -67
  247. package/src/v2/hooks/use-pin-to-send.ts +0 -94
  248. package/src/v2/hooks/use-render-activity-message.tsx +0 -72
  249. package/src/v2/hooks/use-render-custom-messages.tsx +0 -93
  250. package/src/v2/hooks/use-render-tool-call.tsx +0 -208
  251. package/src/v2/hooks/use-render-tool.tsx +0 -184
  252. package/src/v2/hooks/use-suggestions.tsx +0 -91
  253. package/src/v2/hooks/use-threads.tsx +0 -325
  254. package/src/v2/hooks/useKatexStyles.ts +0 -27
  255. package/src/v2/index.css +0 -1
  256. package/src/v2/index.ts +0 -27
  257. package/src/v2/lib/__tests__/completePartialMarkdown.test.ts +0 -495
  258. package/src/v2/lib/__tests__/processPartialHtml.test.ts +0 -112
  259. package/src/v2/lib/__tests__/renderSlot.test.tsx +0 -588
  260. package/src/v2/lib/__tests__/slots.test.ts +0 -56
  261. package/src/v2/lib/processPartialHtml.ts +0 -45
  262. package/src/v2/lib/react-core.ts +0 -156
  263. package/src/v2/lib/slots.tsx +0 -184
  264. package/src/v2/lib/transcription-client.ts +0 -184
  265. package/src/v2/lib/utils.ts +0 -8
  266. package/src/v2/providers/CopilotChatConfigurationProvider.tsx +0 -196
  267. package/src/v2/providers/CopilotKitProvider.tsx +0 -800
  268. package/src/v2/providers/SandboxFunctionsContext.ts +0 -10
  269. package/src/v2/providers/__tests__/CopilotChatConfigurationProvider.test.tsx +0 -652
  270. package/src/v2/providers/__tests__/CopilotKitProvider.license.test.tsx +0 -101
  271. package/src/v2/providers/__tests__/CopilotKitProvider.onError.test.tsx +0 -69
  272. package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.tsx +0 -881
  273. package/src/v2/providers/__tests__/CopilotKitProvider.sandboxFunctions.test.tsx +0 -198
  274. package/src/v2/providers/__tests__/CopilotKitProvider.stability.test.tsx +0 -740
  275. package/src/v2/providers/__tests__/CopilotKitProvider.test.tsx +0 -713
  276. package/src/v2/providers/__tests__/CopilotKitProvider.wildcard.test.tsx +0 -294
  277. package/src/v2/providers/index.ts +0 -21
  278. package/src/v2/styles/globals.css +0 -349
  279. package/src/v2/types/__tests__/defineToolCallRenderer.test.tsx +0 -525
  280. package/src/v2/types/defineToolCallRenderer.ts +0 -68
  281. package/src/v2/types/frontend-tool.ts +0 -8
  282. package/src/v2/types/human-in-the-loop.ts +0 -33
  283. package/src/v2/types/index.ts +0 -8
  284. package/src/v2/types/interrupt.ts +0 -15
  285. package/src/v2/types/react-activity-message-renderer.ts +0 -27
  286. package/src/v2/types/react-custom-message-renderer.ts +0 -17
  287. package/src/v2/types/react-tool-call-renderer.ts +0 -35
  288. package/src/v2/types/sandbox-function.ts +0 -11
  289. package/tsconfig.json +0 -8
  290. package/tsdown.config.ts +0 -193
  291. package/typedoc.json +0 -4
  292. package/vitest.config.mjs +0 -31
@@ -1,1029 +0,0 @@
1
- import React from "react";
2
- import { render, act, screen } from "@testing-library/react";
3
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
4
- import { useAgent, UseAgentUpdate } from "../use-agent";
5
- import { useCopilotKit } from "../../context";
6
- import { MockStepwiseAgent } from "../../__tests__/utils/test-helpers";
7
- import {
8
- CopilotKitCore,
9
- CopilotKitCoreRuntimeConnectionStatus,
10
- } from "@copilotkit/core";
11
- import type { Message } from "@ag-ui/core";
12
- import type { RunAgentInput } from "@ag-ui/client";
13
-
14
- vi.mock("../../context", () => ({
15
- useCopilotKit: vi.fn(),
16
- }));
17
-
18
- vi.mock("../../providers/CopilotChatConfigurationProvider", () => ({
19
- useCopilotChatConfiguration: vi.fn(() => undefined),
20
- }));
21
-
22
- const mockUseCopilotKit = useCopilotKit as ReturnType<typeof vi.fn>;
23
-
24
- // ---------------------------------------------------------------------------
25
- // Message factories — eliminates `as any` on every message literal
26
- // ---------------------------------------------------------------------------
27
-
28
- function userMsg(id: string, content = `msg-${id}`): Message {
29
- return { id, role: "user" as const, content };
30
- }
31
-
32
- function assistantMsg(id: string, content = `msg-${id}`): Message {
33
- return { id, role: "assistant" as const, content };
34
- }
35
-
36
- /** Create N alternating user/assistant messages (ids "1" … "N") */
37
- function createMessages(count: number): Message[] {
38
- return Array.from({ length: count }, (_, i) =>
39
- i % 2 === 0
40
- ? userMsg(String(i + 1), `tok${i + 1}`)
41
- : assistantMsg(String(i + 1), `tok${i + 1}`),
42
- );
43
- }
44
-
45
- // ---------------------------------------------------------------------------
46
- // Subscriber notification helpers
47
- // ---------------------------------------------------------------------------
48
-
49
- /** Helper: fire onMessagesChanged on all agent subscribers */
50
- function notifyMessagesChanged(agent: MockStepwiseAgent) {
51
- agent.subscribers.forEach((s) =>
52
- s.onMessagesChanged?.({
53
- messages: agent.messages,
54
- state: agent.state,
55
- agent,
56
- }),
57
- );
58
- }
59
-
60
- /** Helper: fire onStateChanged on all agent subscribers */
61
- function notifyStateChanged(agent: MockStepwiseAgent) {
62
- agent.subscribers.forEach((s) =>
63
- s.onStateChanged?.({
64
- state: agent.state,
65
- messages: agent.messages,
66
- agent,
67
- }),
68
- );
69
- }
70
-
71
- function createMockRunAgentInput(
72
- overrides?: Partial<RunAgentInput>,
73
- ): RunAgentInput {
74
- return {
75
- threadId: "t-1",
76
- runId: "r-1",
77
- state: {},
78
- messages: [],
79
- tools: [],
80
- context: [],
81
- forwardedProps: {},
82
- ...overrides,
83
- };
84
- }
85
-
86
- /** Helper: fire onRunInitialized on all agent subscribers */
87
- function notifyRunInitialized(agent: MockStepwiseAgent) {
88
- agent.subscribers.forEach((s) =>
89
- s.onRunInitialized?.({
90
- messages: agent.messages,
91
- state: agent.state,
92
- agent,
93
- input: createMockRunAgentInput(),
94
- }),
95
- );
96
- }
97
-
98
- // ---------------------------------------------------------------------------
99
- // Test component factory
100
- // ---------------------------------------------------------------------------
101
-
102
- /** Helper: create a test component that tracks render count */
103
- function createTestComponent(
104
- options: {
105
- updates?: UseAgentUpdate[];
106
- throttleMs?: number;
107
- renderCount?: { current: number };
108
- } = {},
109
- ) {
110
- const {
111
- updates = [UseAgentUpdate.OnMessagesChanged],
112
- throttleMs,
113
- renderCount,
114
- } = options;
115
-
116
- return function TestComponent() {
117
- if (renderCount) renderCount.current++;
118
- const { agent } = useAgent({
119
- agentId: "test-agent",
120
- updates,
121
- throttleMs,
122
- });
123
- return (
124
- <>
125
- <div data-testid="count">{agent.messages.length}</div>
126
- <div data-testid="state">{JSON.stringify(agent.state)}</div>
127
- </>
128
- );
129
- };
130
- }
131
-
132
- /** Factory for the mock return value of useCopilotKit.
133
- * Uses a real CopilotKitCore instance so subscribeToAgentWithOptions (with its throttle
134
- * logic) is exercised end-to-end rather than mocked. */
135
- function createMockContext(
136
- agent: MockStepwiseAgent,
137
- overrides: { defaultThrottleMs?: number } = {},
138
- ) {
139
- const core = new CopilotKitCore({
140
- runtimeUrl: "http://localhost:3000/api/copilot",
141
- });
142
- if (overrides.defaultThrottleMs !== undefined) {
143
- core.setDefaultThrottleMs(overrides.defaultThrottleMs);
144
- }
145
- return {
146
- copilotkit: {
147
- getAgent: () => agent,
148
- runtimeUrl: "http://localhost:3000/api/copilot",
149
- runtimeConnectionStatus: CopilotKitCoreRuntimeConnectionStatus.Connected,
150
- runtimeTransport: "rest",
151
- headers: {},
152
- agents: { [String(agent.agentId)]: agent },
153
- defaultThrottleMs: core.defaultThrottleMs,
154
- subscribeToAgentWithOptions: core.subscribeToAgentWithOptions.bind(core),
155
- },
156
- executingToolCallIds: new Set(),
157
- };
158
- }
159
-
160
- describe("useAgent throttleMs", () => {
161
- let mockAgent: MockStepwiseAgent;
162
-
163
- beforeEach(() => {
164
- vi.useFakeTimers();
165
- mockAgent = new MockStepwiseAgent();
166
- mockAgent.agentId = "test-agent";
167
-
168
- mockUseCopilotKit.mockReturnValue(createMockContext(mockAgent));
169
- });
170
-
171
- afterEach(() => {
172
- vi.useRealTimers();
173
- vi.restoreAllMocks();
174
- });
175
-
176
- it("without throttleMs, component reflects latest messages after notification", async () => {
177
- const TestComponent = createTestComponent();
178
-
179
- render(<TestComponent />);
180
- expect(screen.getByTestId("count").textContent).toBe("0");
181
-
182
- await act(async () => {
183
- mockAgent.messages = [userMsg("1", "hello")];
184
- notifyMessagesChanged(mockAgent);
185
- });
186
-
187
- expect(screen.getByTestId("count").textContent).toBe("1");
188
- });
189
-
190
- it("with throttleMs: 0 (explicit), behaves identically to omitting throttleMs", async () => {
191
- const TestComponent = createTestComponent({ throttleMs: 0 });
192
-
193
- render(<TestComponent />);
194
- expect(screen.getByTestId("count").textContent).toBe("0");
195
-
196
- await act(async () => {
197
- mockAgent.messages = [userMsg("1", "hello")];
198
- notifyMessagesChanged(mockAgent);
199
- });
200
-
201
- expect(screen.getByTestId("count").textContent).toBe("1");
202
-
203
- // Second notification also fires immediately (no throttle)
204
- await act(async () => {
205
- mockAgent.messages = [userMsg("1", "hello"), assistantMsg("2", "world")];
206
- notifyMessagesChanged(mockAgent);
207
- });
208
-
209
- expect(screen.getByTestId("count").textContent).toBe("2");
210
- });
211
-
212
- it("with throttleMs, first notification fires immediately (leading edge)", async () => {
213
- const TestComponent = createTestComponent({ throttleMs: 100 });
214
-
215
- render(<TestComponent />);
216
- expect(screen.getByTestId("count").textContent).toBe("0");
217
-
218
- await act(async () => {
219
- mockAgent.messages = [userMsg("1", "hello")];
220
- notifyMessagesChanged(mockAgent);
221
- });
222
-
223
- expect(screen.getByTestId("count").textContent).toBe("1");
224
- });
225
-
226
- it("with throttleMs, second notification within window is deferred until trailing edge", async () => {
227
- const TestComponent = createTestComponent({ throttleMs: 100 });
228
-
229
- render(<TestComponent />);
230
-
231
- // First notification — leading edge, fires immediately
232
- await act(async () => {
233
- mockAgent.messages = [userMsg("1", "a")];
234
- notifyMessagesChanged(mockAgent);
235
- });
236
- expect(screen.getByTestId("count").textContent).toBe("1");
237
-
238
- // Second notification 10ms later — within throttle window
239
- await act(async () => {
240
- vi.advanceTimersByTime(10);
241
- mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
242
- notifyMessagesChanged(mockAgent);
243
- });
244
-
245
- // The throttle should have deferred this — component still shows 1
246
- expect(screen.getByTestId("count").textContent).toBe("1");
247
-
248
- // Advance past the throttle window — trailing edge fires
249
- await act(async () => {
250
- vi.advanceTimersByTime(100);
251
- });
252
- expect(screen.getByTestId("count").textContent).toBe("2");
253
- });
254
-
255
- it("with throttleMs, rapid burst of many notifications results in exactly 2 renders (leading + trailing)", async () => {
256
- const renderCount = { current: 0 };
257
- const TestComponent = createTestComponent({ throttleMs: 100, renderCount });
258
-
259
- render(<TestComponent />);
260
- const rendersAfterMount = renderCount.current;
261
-
262
- // Leading edge — fires immediately
263
- await act(async () => {
264
- mockAgent.messages = [userMsg("1", "tok1")];
265
- notifyMessagesChanged(mockAgent);
266
- });
267
- expect(renderCount.current).toBe(rendersAfterMount + 1);
268
-
269
- // Fire 10 rapid notifications within the throttle window (1ms apart)
270
- for (let i = 2; i <= 11; i++) {
271
- await act(async () => {
272
- vi.advanceTimersByTime(1);
273
- mockAgent.messages = createMessages(i);
274
- notifyMessagesChanged(mockAgent);
275
- });
276
- }
277
-
278
- // Should still be at leading-edge render count (burst was coalesced)
279
- expect(renderCount.current).toBe(rendersAfterMount + 1);
280
- expect(screen.getByTestId("count").textContent).toBe("1");
281
-
282
- // Advance past the throttle window — trailing edge fires once
283
- await act(async () => {
284
- vi.advanceTimersByTime(100);
285
- });
286
- expect(renderCount.current).toBe(rendersAfterMount + 2);
287
- expect(screen.getByTestId("count").textContent).toBe("11");
288
- });
289
-
290
- it("with throttleMs, new notification after trailing edge fires immediately (new cycle)", async () => {
291
- const TestComponent = createTestComponent({ throttleMs: 100 });
292
-
293
- render(<TestComponent />);
294
-
295
- // Leading edge
296
- await act(async () => {
297
- mockAgent.messages = [userMsg("1", "a")];
298
- notifyMessagesChanged(mockAgent);
299
- });
300
- expect(screen.getByTestId("count").textContent).toBe("1");
301
-
302
- // Second notification — deferred
303
- await act(async () => {
304
- vi.advanceTimersByTime(10);
305
- mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
306
- notifyMessagesChanged(mockAgent);
307
- });
308
-
309
- // Trailing edge fires
310
- await act(async () => {
311
- vi.advanceTimersByTime(100);
312
- });
313
- expect(screen.getByTestId("count").textContent).toBe("2");
314
-
315
- // New notification well after the window — should fire immediately as a new leading edge
316
- await act(async () => {
317
- vi.advanceTimersByTime(200);
318
- mockAgent.messages = [
319
- userMsg("1", "a"),
320
- assistantMsg("2", "b"),
321
- userMsg("3", "c"),
322
- ];
323
- notifyMessagesChanged(mockAgent);
324
- });
325
- expect(screen.getByTestId("count").textContent).toBe("3");
326
- });
327
-
328
- it("with throttleMs, onStateChanged is also throttled (shared window)", async () => {
329
- const TestComponent = createTestComponent({
330
- updates: [
331
- UseAgentUpdate.OnMessagesChanged,
332
- UseAgentUpdate.OnStateChanged,
333
- ],
334
- throttleMs: 100,
335
- });
336
-
337
- render(<TestComponent />);
338
-
339
- // Fire onMessagesChanged to start the throttle window (leading edge)
340
- await act(async () => {
341
- mockAgent.messages = [userMsg("1", "a")];
342
- notifyMessagesChanged(mockAgent);
343
- });
344
- expect(screen.getByTestId("count").textContent).toBe("1");
345
-
346
- // Fire onStateChanged 10ms later — should be deferred (within throttle window)
347
- await act(async () => {
348
- vi.advanceTimersByTime(10);
349
- mockAgent.state = { count: 42 };
350
- notifyStateChanged(mockAgent);
351
- });
352
-
353
- // State update is pending, not yet rendered
354
- expect(screen.getByTestId("state").textContent).toBe("{}");
355
-
356
- // Trailing edge fires after the window — await so microtask from
357
- // batchedForceUpdate flushes and triggers the React re-render.
358
- await act(async () => {
359
- vi.advanceTimersByTime(100);
360
- });
361
-
362
- expect(screen.getByTestId("state").textContent).toBe('{"count":42}');
363
- });
364
-
365
- it("with throttleMs, pending trailing timer does not fire after unmount", async () => {
366
- const renderCount = { current: 0 };
367
- const TestComponent = createTestComponent({ throttleMs: 100, renderCount });
368
-
369
- const { unmount } = render(<TestComponent />);
370
-
371
- // Leading edge — fires immediately
372
- await act(async () => {
373
- mockAgent.messages = [userMsg("1", "a")];
374
- notifyMessagesChanged(mockAgent);
375
- });
376
-
377
- // Second notification — schedules trailing timer
378
- await act(async () => {
379
- vi.advanceTimersByTime(10);
380
- mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
381
- notifyMessagesChanged(mockAgent);
382
- });
383
-
384
- const countBeforeUnmount = renderCount.current;
385
-
386
- // Unmount before trailing fires
387
- unmount();
388
-
389
- // Advancing past the window should NOT cause additional renders
390
- await act(async () => {
391
- vi.advanceTimersByTime(100);
392
- });
393
-
394
- expect(renderCount.current).toBe(countBeforeUnmount);
395
- });
396
-
397
- it("with throttleMs and only OnStateChanged subscribed, first state fires on leading edge", async () => {
398
- const TestComponent = createTestComponent({
399
- updates: [UseAgentUpdate.OnStateChanged],
400
- throttleMs: 100,
401
- });
402
-
403
- render(<TestComponent />);
404
-
405
- // First onStateChanged fires immediately (leading edge) — await so
406
- // microtask from batchedForceUpdate flushes.
407
- await act(async () => {
408
- mockAgent.state = { value: "test" };
409
- notifyStateChanged(mockAgent);
410
- });
411
-
412
- expect(screen.getByTestId("state").textContent).toBe('{"value":"test"}');
413
-
414
- // No onMessagesChanged subscription should exist — messages notification
415
- // does nothing because the handler was never registered.
416
- act(() => {
417
- mockAgent.messages = [userMsg("1", "a")];
418
- notifyMessagesChanged(mockAgent);
419
- });
420
-
421
- expect(screen.getByTestId("state").textContent).toBe('{"value":"test"}');
422
- });
423
-
424
- it.each([
425
- { label: "NaN", value: NaN },
426
- { label: "Infinity", value: Infinity },
427
- { label: "-1", value: -1 },
428
- { label: "-Infinity", value: -Infinity },
429
- ])(
430
- "with invalid throttleMs ($label), falls back to unthrottled and warns",
431
- async ({ value }) => {
432
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
433
- const TestComponent = createTestComponent({ throttleMs: value });
434
-
435
- render(<TestComponent />);
436
-
437
- // Should warn about the invalid value
438
- expect(errorSpy).toHaveBeenCalledWith(
439
- expect.stringContaining(
440
- "throttleMs must be a non-negative finite number",
441
- ),
442
- expect.any(Error),
443
- );
444
-
445
- // Should behave as unthrottled — every notification fires immediately
446
- await act(async () => {
447
- mockAgent.messages = [userMsg("1", "a")];
448
- notifyMessagesChanged(mockAgent);
449
- });
450
- expect(screen.getByTestId("count").textContent).toBe("1");
451
-
452
- await act(async () => {
453
- mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
454
- notifyMessagesChanged(mockAgent);
455
- });
456
- expect(screen.getByTestId("count").textContent).toBe("2");
457
- },
458
- );
459
-
460
- it("trailing-edge render reflects the latest messages, not stale data", async () => {
461
- const TestComponent = createTestComponent({ throttleMs: 100 });
462
- render(<TestComponent />);
463
-
464
- // Leading edge
465
- await act(async () => {
466
- mockAgent.messages = [userMsg("1", "A")];
467
- notifyMessagesChanged(mockAgent);
468
- });
469
- expect(screen.getByTestId("count").textContent).toBe("1");
470
-
471
- // Multiple deferred notifications with increasing messages
472
- await act(async () => {
473
- vi.advanceTimersByTime(20);
474
- mockAgent.messages = [userMsg("1", "A"), assistantMsg("2", "B")];
475
- notifyMessagesChanged(mockAgent);
476
- });
477
-
478
- await act(async () => {
479
- vi.advanceTimersByTime(20);
480
- mockAgent.messages = [
481
- userMsg("1", "A"),
482
- assistantMsg("2", "B"),
483
- assistantMsg("3", "C"),
484
- ];
485
- notifyMessagesChanged(mockAgent);
486
- });
487
-
488
- // Still deferred
489
- expect(screen.getByTestId("count").textContent).toBe("1");
490
-
491
- // Trailing edge fires — must show all 3 messages (latest state)
492
- await act(async () => {
493
- vi.advanceTimersByTime(100);
494
- });
495
- expect(screen.getByTestId("count").textContent).toBe("3");
496
- });
497
-
498
- it("trailing edge fires at exactly throttleMs after the leading edge", async () => {
499
- const TestComponent = createTestComponent({ throttleMs: 100 });
500
- render(<TestComponent />);
501
-
502
- // Leading edge at T=0
503
- await act(async () => {
504
- mockAgent.messages = [userMsg("1", "a")];
505
- notifyMessagesChanged(mockAgent);
506
- });
507
- expect(screen.getByTestId("count").textContent).toBe("1");
508
-
509
- // Deferred notification at T=40
510
- await act(async () => {
511
- vi.advanceTimersByTime(40);
512
- mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
513
- notifyMessagesChanged(mockAgent);
514
- });
515
- expect(screen.getByTestId("count").textContent).toBe("1");
516
-
517
- // At T=99, trailing has NOT fired yet
518
- await act(async () => {
519
- vi.advanceTimersByTime(59);
520
- });
521
- expect(screen.getByTestId("count").textContent).toBe("1");
522
-
523
- // At T=100, trailing fires
524
- await act(async () => {
525
- vi.advanceTimersByTime(1);
526
- });
527
- expect(screen.getByTestId("count").textContent).toBe("2");
528
- });
529
-
530
- it("changing throttleMs cleans up pending timers from the previous configuration", async () => {
531
- function DynamicThrottleComponent({ throttleMs }: { throttleMs: number }) {
532
- const { agent } = useAgent({
533
- agentId: "test-agent",
534
- updates: [UseAgentUpdate.OnMessagesChanged],
535
- throttleMs,
536
- });
537
- return <div data-testid="count">{agent.messages.length}</div>;
538
- }
539
-
540
- const { rerender } = render(<DynamicThrottleComponent throttleMs={200} />);
541
-
542
- // Leading edge
543
- await act(async () => {
544
- mockAgent.messages = [userMsg("1", "a")];
545
- notifyMessagesChanged(mockAgent);
546
- });
547
- expect(screen.getByTestId("count").textContent).toBe("1");
548
-
549
- // Deferred notification — pending timer set for 200ms
550
- await act(async () => {
551
- vi.advanceTimersByTime(50);
552
- mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
553
- notifyMessagesChanged(mockAgent);
554
- });
555
- expect(screen.getByTestId("count").textContent).toBe("1");
556
-
557
- // Change throttleMs — effect re-runs, old 200ms timer should be cleaned up
558
- rerender(<DynamicThrottleComponent throttleMs={50} />);
559
-
560
- // New notification fires as leading edge under the new 50ms throttle
561
- await act(async () => {
562
- mockAgent.messages = [
563
- userMsg("1", "a"),
564
- assistantMsg("2", "b"),
565
- userMsg("3", "c"),
566
- ];
567
- notifyMessagesChanged(mockAgent);
568
- });
569
- expect(screen.getByTestId("count").textContent).toBe("3");
570
-
571
- // Advance past what would have been the old 200ms trailing edge —
572
- // no ghost render should occur from the old timer
573
- await act(async () => {
574
- vi.advanceTimersByTime(200);
575
- });
576
- expect(screen.getByTestId("count").textContent).toBe("3");
577
- });
578
-
579
- it("notification immediately after trailing edge is throttled (trailing restarts the window)", async () => {
580
- const renderCount = { current: 0 };
581
- const TestComponent = createTestComponent({ throttleMs: 100, renderCount });
582
-
583
- render(<TestComponent />);
584
- const rendersAfterMount = renderCount.current;
585
-
586
- // T=0: Leading edge fires immediately
587
- await act(async () => {
588
- mockAgent.messages = [userMsg("1", "a")];
589
- notifyMessagesChanged(mockAgent);
590
- });
591
- expect(renderCount.current).toBe(rendersAfterMount + 1);
592
- expect(screen.getByTestId("count").textContent).toBe("1");
593
-
594
- // T=10: Deferred notification — schedules trailing
595
- await act(async () => {
596
- vi.advanceTimersByTime(10);
597
- mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
598
- notifyMessagesChanged(mockAgent);
599
- });
600
-
601
- // T=100: Trailing fires (render #2) and restarts window
602
- await act(async () => {
603
- vi.advanceTimersByTime(90);
604
- });
605
- expect(renderCount.current).toBe(rendersAfterMount + 2);
606
- expect(screen.getByTestId("count").textContent).toBe("2");
607
-
608
- // T=101: Notification 1ms after trailing — should be DEFERRED (within new window), not immediate
609
- await act(async () => {
610
- vi.advanceTimersByTime(1);
611
- mockAgent.messages = [
612
- userMsg("1", "a"),
613
- assistantMsg("2", "b"),
614
- userMsg("3", "c"),
615
- ];
616
- notifyMessagesChanged(mockAgent);
617
- });
618
- // Still 2 — the notification was deferred, not a new leading edge
619
- expect(renderCount.current).toBe(rendersAfterMount + 2);
620
- expect(screen.getByTestId("count").textContent).toBe("2");
621
-
622
- // T=200: New trailing fires (render #3)
623
- await act(async () => {
624
- vi.advanceTimersByTime(99);
625
- });
626
- expect(renderCount.current).toBe(rendersAfterMount + 3);
627
- expect(screen.getByTestId("count").textContent).toBe("3");
628
- });
629
-
630
- it("cleans up all subscriptions after unmount", () => {
631
- const TestComponent = createTestComponent({
632
- updates: [
633
- UseAgentUpdate.OnMessagesChanged,
634
- UseAgentUpdate.OnStateChanged,
635
- ],
636
- throttleMs: 100,
637
- });
638
-
639
- const subscriberCountBefore = mockAgent.subscribers.length;
640
- const { unmount } = render(<TestComponent />);
641
-
642
- // Should have added subscriber(s)
643
- expect(mockAgent.subscribers.length).toBeGreaterThan(subscriberCountBefore);
644
-
645
- unmount();
646
-
647
- // All subscriptions should be cleaned up
648
- expect(mockAgent.subscribers.length).toBe(subscriberCountBefore);
649
- });
650
-
651
- it("single notification within window does not trigger a trailing re-render", async () => {
652
- const renderCount = { current: 0 };
653
- const TestComponent = createTestComponent({ throttleMs: 100, renderCount });
654
-
655
- render(<TestComponent />);
656
- const rendersAfterMount = renderCount.current;
657
-
658
- // Leading edge — fires immediately
659
- await act(async () => {
660
- mockAgent.messages = [userMsg("1", "a")];
661
- notifyMessagesChanged(mockAgent);
662
- });
663
- expect(renderCount.current).toBe(rendersAfterMount + 1);
664
-
665
- // Advance well past the throttle window — no trailing should fire
666
- await act(async () => {
667
- vi.advanceTimersByTime(200);
668
- });
669
-
670
- // No additional render since there was no second notification
671
- expect(renderCount.current).toBe(rendersAfterMount + 1);
672
- });
673
-
674
- it("with throttleMs, onRunInitialized still fires immediately during throttle window", async () => {
675
- const renderCount = { current: 0 };
676
- const TestComponent = createTestComponent({
677
- updates: [
678
- UseAgentUpdate.OnMessagesChanged,
679
- UseAgentUpdate.OnRunStatusChanged,
680
- ],
681
- throttleMs: 100,
682
- renderCount,
683
- });
684
-
685
- render(<TestComponent />);
686
- const rendersAfterMount = renderCount.current;
687
-
688
- // Fire onMessagesChanged to start the throttle window
689
- await act(async () => {
690
- mockAgent.messages = [userMsg("1", "a")];
691
- notifyMessagesChanged(mockAgent);
692
- });
693
- expect(renderCount.current).toBe(rendersAfterMount + 1);
694
-
695
- // Fire onRunInitialized 10ms later — fires via microtask batch
696
- await act(async () => {
697
- vi.advanceTimersByTime(10);
698
- notifyRunInitialized(mockAgent);
699
- });
700
-
701
- // Run status notification fires via microtask batch
702
- expect(renderCount.current).toBe(rendersAfterMount + 2);
703
- });
704
-
705
- it("changing throttleMs from positive to 0 disables throttling immediately", async () => {
706
- function DynamicThrottleComponent({ throttleMs }: { throttleMs: number }) {
707
- const { agent } = useAgent({
708
- agentId: "test-agent",
709
- updates: [UseAgentUpdate.OnMessagesChanged],
710
- throttleMs,
711
- });
712
- return <div data-testid="count">{agent.messages.length}</div>;
713
- }
714
-
715
- const { rerender } = render(<DynamicThrottleComponent throttleMs={200} />);
716
-
717
- // Leading edge with throttle active
718
- await act(async () => {
719
- mockAgent.messages = [userMsg("1", "a")];
720
- notifyMessagesChanged(mockAgent);
721
- });
722
- expect(screen.getByTestId("count").textContent).toBe("1");
723
-
724
- // Deferred notification — within throttle window
725
- await act(async () => {
726
- vi.advanceTimersByTime(50);
727
- mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
728
- notifyMessagesChanged(mockAgent);
729
- });
730
- expect(screen.getByTestId("count").textContent).toBe("1");
731
-
732
- // Switch to unthrottled
733
- rerender(<DynamicThrottleComponent throttleMs={0} />);
734
-
735
- // Both notifications should fire immediately now
736
- await act(async () => {
737
- mockAgent.messages = [
738
- userMsg("1", "a"),
739
- assistantMsg("2", "b"),
740
- userMsg("3", "c"),
741
- ];
742
- notifyMessagesChanged(mockAgent);
743
- });
744
- expect(screen.getByTestId("count").textContent).toBe("3");
745
-
746
- // Second immediate notification also fires (no coalescing)
747
- await act(async () => {
748
- mockAgent.messages = [
749
- userMsg("1", "a"),
750
- assistantMsg("2", "b"),
751
- userMsg("3", "c"),
752
- assistantMsg("4", "d"),
753
- ];
754
- notifyMessagesChanged(mockAgent);
755
- });
756
- expect(screen.getByTestId("count").textContent).toBe("4");
757
- });
758
- });
759
-
760
- describe("useAgent defaultThrottleMs from provider", () => {
761
- let mockAgent: MockStepwiseAgent;
762
-
763
- beforeEach(() => {
764
- vi.useFakeTimers();
765
- mockAgent = new MockStepwiseAgent();
766
- mockAgent.agentId = "test-agent";
767
- });
768
-
769
- afterEach(() => {
770
- vi.useRealTimers();
771
- vi.restoreAllMocks();
772
- });
773
-
774
- it("uses provider defaultThrottleMs when no explicit throttleMs is passed", async () => {
775
- mockUseCopilotKit.mockReturnValue(
776
- createMockContext(mockAgent, { defaultThrottleMs: 100 }),
777
- );
778
-
779
- const TestComponent = createTestComponent({ throttleMs: undefined });
780
-
781
- render(<TestComponent />);
782
-
783
- // Leading edge — fires immediately
784
- await act(async () => {
785
- mockAgent.messages = [userMsg("1", "hello")];
786
- notifyMessagesChanged(mockAgent);
787
- });
788
- expect(screen.getByTestId("count").textContent).toBe("1");
789
-
790
- // Second notification within 100ms window — should be deferred (throttled)
791
- await act(async () => {
792
- vi.advanceTimersByTime(10);
793
- mockAgent.messages = [userMsg("1", "hello"), assistantMsg("2", "world")];
794
- notifyMessagesChanged(mockAgent);
795
- });
796
- expect(screen.getByTestId("count").textContent).toBe("1");
797
-
798
- // Trailing edge fires after 100ms
799
- await act(async () => {
800
- vi.advanceTimersByTime(100);
801
- });
802
- expect(screen.getByTestId("count").textContent).toBe("2");
803
- });
804
-
805
- it("explicit throttleMs overrides provider defaultThrottleMs", async () => {
806
- mockUseCopilotKit.mockReturnValue(
807
- createMockContext(mockAgent, { defaultThrottleMs: 5000 }),
808
- );
809
-
810
- // Explicit throttleMs=100 should override provider's 5000
811
- const TestComponent = createTestComponent({ throttleMs: 100 });
812
-
813
- render(<TestComponent />);
814
-
815
- // Leading edge
816
- await act(async () => {
817
- mockAgent.messages = [userMsg("1", "hello")];
818
- notifyMessagesChanged(mockAgent);
819
- });
820
- expect(screen.getByTestId("count").textContent).toBe("1");
821
-
822
- // Deferred within 100ms window
823
- await act(async () => {
824
- vi.advanceTimersByTime(10);
825
- mockAgent.messages = [userMsg("1", "hello"), assistantMsg("2", "world")];
826
- notifyMessagesChanged(mockAgent);
827
- });
828
- expect(screen.getByTestId("count").textContent).toBe("1");
829
-
830
- // At 100ms trailing fires (not waiting for provider's 5000ms)
831
- await act(async () => {
832
- vi.advanceTimersByTime(100);
833
- });
834
- expect(screen.getByTestId("count").textContent).toBe("2");
835
- });
836
-
837
- it("without provider defaultThrottleMs or explicit throttleMs, behaves unthrottled", async () => {
838
- mockUseCopilotKit.mockReturnValue(createMockContext(mockAgent));
839
-
840
- const TestComponent = createTestComponent({});
841
-
842
- render(<TestComponent />);
843
-
844
- await act(async () => {
845
- mockAgent.messages = [userMsg("1", "hello")];
846
- notifyMessagesChanged(mockAgent);
847
- });
848
- expect(screen.getByTestId("count").textContent).toBe("1");
849
-
850
- // Immediately fires — no throttle
851
- await act(async () => {
852
- mockAgent.messages = [userMsg("1", "hello"), assistantMsg("2", "world")];
853
- notifyMessagesChanged(mockAgent);
854
- });
855
- expect(screen.getByTestId("count").textContent).toBe("2");
856
- });
857
-
858
- it("explicit throttleMs: 0 overrides non-zero provider defaultThrottleMs (opt-out)", async () => {
859
- mockUseCopilotKit.mockReturnValue(
860
- createMockContext(mockAgent, { defaultThrottleMs: 500 }),
861
- );
862
-
863
- const TestComponent = createTestComponent({ throttleMs: 0 });
864
-
865
- render(<TestComponent />);
866
-
867
- // Both notifications fire immediately — throttleMs: 0 means no throttle
868
- await act(async () => {
869
- mockAgent.messages = [userMsg("1", "hello")];
870
- notifyMessagesChanged(mockAgent);
871
- });
872
- expect(screen.getByTestId("count").textContent).toBe("1");
873
-
874
- await act(async () => {
875
- mockAgent.messages = [userMsg("1", "hello"), assistantMsg("2", "world")];
876
- notifyMessagesChanged(mockAgent);
877
- });
878
- expect(screen.getByTestId("count").textContent).toBe("2");
879
- });
880
-
881
- it.each([
882
- { label: "NaN", value: NaN },
883
- { label: "Infinity", value: Infinity },
884
- { label: "-1", value: -1 },
885
- { label: "-Infinity", value: -Infinity },
886
- ])(
887
- "with invalid provider defaultThrottleMs ($label), falls back to unthrottled and warns",
888
- async ({ value }) => {
889
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
890
-
891
- mockUseCopilotKit.mockReturnValue(
892
- createMockContext(mockAgent, { defaultThrottleMs: value }),
893
- );
894
-
895
- const TestComponent = createTestComponent({ throttleMs: undefined });
896
-
897
- render(<TestComponent />);
898
-
899
- // The core setter rejects invalid values and logs an error
900
- expect(errorSpy).toHaveBeenCalledWith(
901
- expect.stringContaining("must be a non-negative finite number"),
902
- expect.any(Error),
903
- );
904
-
905
- // Should behave as unthrottled (setter rejected the value)
906
- await act(async () => {
907
- mockAgent.messages = [userMsg("1", "a")];
908
- notifyMessagesChanged(mockAgent);
909
- });
910
- expect(screen.getByTestId("count").textContent).toBe("1");
911
-
912
- await act(async () => {
913
- mockAgent.messages = [userMsg("1", "a"), assistantMsg("2", "b")];
914
- notifyMessagesChanged(mockAgent);
915
- });
916
- expect(screen.getByTestId("count").textContent).toBe("2");
917
- },
918
- );
919
-
920
- it("dynamically changing provider defaultThrottleMs updates throttle behavior", async () => {
921
- // Start with 200ms throttle from provider
922
- mockUseCopilotKit.mockReturnValue(
923
- createMockContext(mockAgent, { defaultThrottleMs: 200 }),
924
- );
925
-
926
- const TestComponent = createTestComponent({ throttleMs: undefined });
927
- const { rerender } = render(<TestComponent />);
928
-
929
- // Leading edge fires immediately
930
- await act(async () => {
931
- mockAgent.messages = [userMsg("1", "hello")];
932
- notifyMessagesChanged(mockAgent);
933
- });
934
- expect(screen.getByTestId("count").textContent).toBe("1");
935
-
936
- // Deferred within 200ms window
937
- await act(async () => {
938
- vi.advanceTimersByTime(10);
939
- mockAgent.messages = [userMsg("1", "hello"), assistantMsg("2", "world")];
940
- notifyMessagesChanged(mockAgent);
941
- });
942
- expect(screen.getByTestId("count").textContent).toBe("1");
943
-
944
- // Flush trailing edge
945
- await act(async () => {
946
- vi.advanceTimersByTime(200);
947
- });
948
- expect(screen.getByTestId("count").textContent).toBe("2");
949
-
950
- // Change provider default to 50ms
951
- mockUseCopilotKit.mockReturnValue(
952
- createMockContext(mockAgent, { defaultThrottleMs: 50 }),
953
- );
954
- rerender(<TestComponent />);
955
-
956
- // Leading edge fires immediately
957
- await act(async () => {
958
- mockAgent.messages = [
959
- userMsg("1", "hello"),
960
- assistantMsg("2", "world"),
961
- userMsg("3", "new"),
962
- ];
963
- notifyMessagesChanged(mockAgent);
964
- });
965
- expect(screen.getByTestId("count").textContent).toBe("3");
966
-
967
- // Deferred within 50ms window
968
- await act(async () => {
969
- vi.advanceTimersByTime(10);
970
- mockAgent.messages = [
971
- userMsg("1", "hello"),
972
- assistantMsg("2", "world"),
973
- userMsg("3", "new"),
974
- assistantMsg("4", "reply"),
975
- ];
976
- notifyMessagesChanged(mockAgent);
977
- });
978
- expect(screen.getByTestId("count").textContent).toBe("3");
979
-
980
- // Trailing fires after only 50ms (not 200ms)
981
- await act(async () => {
982
- vi.advanceTimersByTime(50);
983
- });
984
- expect(screen.getByTestId("count").textContent).toBe("4");
985
- });
986
- });
987
-
988
- describe("CopilotKitCore.setDefaultThrottleMs", () => {
989
- it("stores valid values", () => {
990
- const core = new CopilotKitCore({});
991
- core.setDefaultThrottleMs(100);
992
- expect(core.defaultThrottleMs).toBe(100);
993
- });
994
-
995
- it("stores 0", () => {
996
- const core = new CopilotKitCore({});
997
- core.setDefaultThrottleMs(100);
998
- core.setDefaultThrottleMs(0);
999
- expect(core.defaultThrottleMs).toBe(0);
1000
- });
1001
-
1002
- it("stores undefined", () => {
1003
- const core = new CopilotKitCore({});
1004
- core.setDefaultThrottleMs(100);
1005
- core.setDefaultThrottleMs(undefined);
1006
- expect(core.defaultThrottleMs).toBeUndefined();
1007
- });
1008
-
1009
- it.each([
1010
- { label: "NaN", value: NaN },
1011
- { label: "Infinity", value: Infinity },
1012
- { label: "-1", value: -1 },
1013
- { label: "-Infinity", value: -Infinity },
1014
- ])(
1015
- "rejects invalid value ($label) and preserves previous value",
1016
- ({ value }) => {
1017
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
1018
- const core = new CopilotKitCore({});
1019
- core.setDefaultThrottleMs(200);
1020
- core.setDefaultThrottleMs(value);
1021
- expect(core.defaultThrottleMs).toBe(200);
1022
- expect(errorSpy).toHaveBeenCalledWith(
1023
- expect.stringContaining("must be a non-negative finite number"),
1024
- expect.any(Error),
1025
- );
1026
- errorSpy.mockRestore();
1027
- },
1028
- );
1029
- });