@tambo-ai/react 0.68.0 → 0.69.1

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 (326) hide show
  1. package/README.md +1 -1
  2. package/dist/context-helpers/context-helpers.test.js +16 -4
  3. package/dist/context-helpers/context-helpers.test.js.map +1 -1
  4. package/dist/context-helpers/current-interactables-context-helper.d.ts +2 -2
  5. package/dist/context-helpers/current-interactables-context-helper.d.ts.map +1 -1
  6. package/dist/context-helpers/current-interactables-context-helper.js +31 -15
  7. package/dist/context-helpers/current-interactables-context-helper.js.map +1 -1
  8. package/dist/context-helpers/registry.d.ts +2 -2
  9. package/dist/context-helpers/registry.d.ts.map +1 -1
  10. package/dist/context-helpers/registry.js.map +1 -1
  11. package/dist/context-helpers/types.d.ts +2 -2
  12. package/dist/context-helpers/types.d.ts.map +1 -1
  13. package/dist/context-helpers/types.js.map +1 -1
  14. package/dist/hooks/use-message-images.test.js +174 -37
  15. package/dist/hooks/use-message-images.test.js.map +1 -1
  16. package/dist/hooks/use-tambo-voice.d.ts +1 -1
  17. package/dist/hooks/use-tambo-voice.js +1 -1
  18. package/dist/hooks/use-tambo-voice.js.map +1 -1
  19. package/dist/hooks/use-tambo-voice.test.d.ts +2 -0
  20. package/dist/hooks/use-tambo-voice.test.d.ts.map +1 -0
  21. package/dist/hooks/use-tambo-voice.test.js +239 -0
  22. package/dist/hooks/use-tambo-voice.test.js.map +1 -0
  23. package/dist/index.d.ts +2 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/mcp/elicitation.d.ts.map +1 -1
  27. package/dist/mcp/elicitation.js +12 -0
  28. package/dist/mcp/elicitation.js.map +1 -1
  29. package/dist/mcp/elicitation.test.js +8 -1
  30. package/dist/mcp/elicitation.test.js.map +1 -1
  31. package/dist/mcp/mcp-client.d.ts +6 -10
  32. package/dist/mcp/mcp-client.d.ts.map +1 -1
  33. package/dist/mcp/mcp-client.js.map +1 -1
  34. package/dist/mcp/mcp-hooks.d.ts +12 -60
  35. package/dist/mcp/mcp-hooks.d.ts.map +1 -1
  36. package/dist/mcp/mcp-hooks.js +90 -10
  37. package/dist/mcp/mcp-hooks.js.map +1 -1
  38. package/dist/mcp/mcp-hooks.test.js +423 -0
  39. package/dist/mcp/mcp-hooks.test.js.map +1 -1
  40. package/dist/mcp/tambo-mcp-provider.d.ts.map +1 -1
  41. package/dist/mcp/tambo-mcp-provider.js +3 -0
  42. package/dist/mcp/tambo-mcp-provider.js.map +1 -1
  43. package/dist/mcp/tambo-mcp-provider.test.js +37 -0
  44. package/dist/mcp/tambo-mcp-provider.test.js.map +1 -1
  45. package/dist/model/component-metadata.d.ts +53 -20
  46. package/dist/model/component-metadata.d.ts.map +1 -1
  47. package/dist/model/component-metadata.js.map +1 -1
  48. package/dist/model/tambo-interactable.d.ts +6 -0
  49. package/dist/model/tambo-interactable.d.ts.map +1 -1
  50. package/dist/model/tambo-interactable.js.map +1 -1
  51. package/dist/providers/index.d.ts +1 -1
  52. package/dist/providers/index.d.ts.map +1 -1
  53. package/dist/providers/index.js.map +1 -1
  54. package/dist/providers/tambo-client-provider.d.ts +8 -0
  55. package/dist/providers/tambo-client-provider.d.ts.map +1 -1
  56. package/dist/providers/tambo-client-provider.js +10 -11
  57. package/dist/providers/tambo-client-provider.js.map +1 -1
  58. package/dist/providers/tambo-client-provider.test.d.ts +2 -0
  59. package/dist/providers/tambo-client-provider.test.d.ts.map +1 -0
  60. package/dist/providers/tambo-client-provider.test.js +208 -0
  61. package/dist/providers/tambo-client-provider.test.js.map +1 -0
  62. package/dist/providers/tambo-context-attachment-provider.d.ts +34 -92
  63. package/dist/providers/tambo-context-attachment-provider.d.ts.map +1 -1
  64. package/dist/providers/tambo-context-attachment-provider.js +62 -105
  65. package/dist/providers/tambo-context-attachment-provider.js.map +1 -1
  66. package/dist/providers/tambo-context-attachment-provider.test.js +229 -463
  67. package/dist/providers/tambo-context-attachment-provider.test.js.map +1 -1
  68. package/dist/providers/tambo-interactable-provider.d.ts +2 -0
  69. package/dist/providers/tambo-interactable-provider.d.ts.map +1 -1
  70. package/dist/providers/tambo-interactable-provider.js +29 -4
  71. package/dist/providers/tambo-interactable-provider.js.map +1 -1
  72. package/dist/providers/tambo-interactable-provider.test.js +1 -1
  73. package/dist/providers/tambo-interactable-provider.test.js.map +1 -1
  74. package/dist/providers/tambo-interactables-additional-context.test.js +2 -5
  75. package/dist/providers/tambo-interactables-additional-context.test.js.map +1 -1
  76. package/dist/providers/tambo-provider.d.ts +2 -3
  77. package/dist/providers/tambo-provider.d.ts.map +1 -1
  78. package/dist/providers/tambo-provider.js +5 -6
  79. package/dist/providers/tambo-provider.js.map +1 -1
  80. package/dist/providers/tambo-registry-provider.test.js +16 -0
  81. package/dist/providers/tambo-registry-provider.test.js.map +1 -1
  82. package/dist/providers/tambo-registry-schema-compat.test.js +31 -0
  83. package/dist/providers/tambo-registry-schema-compat.test.js.map +1 -1
  84. package/dist/providers/tambo-thread-input-provider.d.ts +1 -1
  85. package/dist/providers/tambo-thread-input-provider.d.ts.map +1 -1
  86. package/dist/providers/tambo-thread-input-provider.js +5 -1
  87. package/dist/providers/tambo-thread-input-provider.js.map +1 -1
  88. package/dist/providers/tambo-thread-provider-initial-messages.test.js +84 -2
  89. package/dist/providers/tambo-thread-provider-initial-messages.test.js.map +1 -1
  90. package/dist/providers/tambo-thread-provider.d.ts.map +1 -1
  91. package/dist/providers/tambo-thread-provider.js +56 -43
  92. package/dist/providers/tambo-thread-provider.js.map +1 -1
  93. package/dist/providers/tambo-thread-provider.test.js +456 -262
  94. package/dist/providers/tambo-thread-provider.test.js.map +1 -1
  95. package/dist/schema/json-schema.js +29 -29
  96. package/dist/schema/json-schema.js.map +1 -1
  97. package/dist/schema/schema.test.js +237 -0
  98. package/dist/schema/schema.test.js.map +1 -1
  99. package/dist/schema/standard-schema.d.ts +1 -0
  100. package/dist/schema/standard-schema.d.ts.map +1 -1
  101. package/dist/schema/standard-schema.js +18 -13
  102. package/dist/schema/standard-schema.js.map +1 -1
  103. package/dist/schema/standard-schema.test.d.ts +2 -0
  104. package/dist/schema/standard-schema.test.d.ts.map +1 -0
  105. package/dist/schema/standard-schema.test.js +165 -0
  106. package/dist/schema/standard-schema.test.js.map +1 -0
  107. package/dist/schema/validate.test.js +149 -0
  108. package/dist/schema/validate.test.js.map +1 -1
  109. package/dist/schema/zod.d.ts +7 -4
  110. package/dist/schema/zod.d.ts.map +1 -1
  111. package/dist/schema/zod.js +65 -22
  112. package/dist/schema/zod.js.map +1 -1
  113. package/dist/schema/zod.test.js +112 -0
  114. package/dist/schema/zod.test.js.map +1 -1
  115. package/dist/testing/tools.d.ts +4 -1
  116. package/dist/testing/tools.d.ts.map +1 -1
  117. package/dist/testing/tools.js +6 -1
  118. package/dist/testing/tools.js.map +1 -1
  119. package/dist/util/generate-component.d.ts.map +1 -1
  120. package/dist/util/generate-component.js +18 -3
  121. package/dist/util/generate-component.js.map +1 -1
  122. package/dist/util/generate-component.test.d.ts +2 -0
  123. package/dist/util/generate-component.test.d.ts.map +1 -0
  124. package/dist/util/generate-component.test.js +340 -0
  125. package/dist/util/generate-component.test.js.map +1 -0
  126. package/dist/util/is-promise.d.ts +9 -0
  127. package/dist/util/is-promise.d.ts.map +1 -0
  128. package/dist/util/is-promise.js +20 -0
  129. package/dist/util/is-promise.js.map +1 -0
  130. package/dist/util/is-promise.test.d.ts +2 -0
  131. package/dist/util/is-promise.test.d.ts.map +1 -0
  132. package/dist/util/is-promise.test.js +48 -0
  133. package/dist/util/is-promise.test.js.map +1 -0
  134. package/dist/util/query-utils.test.d.ts +2 -0
  135. package/dist/util/query-utils.test.d.ts.map +1 -0
  136. package/dist/util/query-utils.test.js +382 -0
  137. package/dist/util/query-utils.test.js.map +1 -0
  138. package/dist/util/registry-validators.d.ts.map +1 -1
  139. package/dist/util/registry-validators.js +7 -0
  140. package/dist/util/registry-validators.js.map +1 -1
  141. package/dist/util/registry-validators.test.js +57 -0
  142. package/dist/util/registry-validators.test.js.map +1 -1
  143. package/dist/util/registry.d.ts.map +1 -1
  144. package/dist/util/registry.js +9 -0
  145. package/dist/util/registry.js.map +1 -1
  146. package/dist/util/registry.test.js +323 -1
  147. package/dist/util/registry.test.js.map +1 -1
  148. package/dist/util/resource-validators.test.d.ts +2 -0
  149. package/dist/util/resource-validators.test.d.ts.map +1 -0
  150. package/dist/util/resource-validators.test.js +90 -0
  151. package/dist/util/resource-validators.test.js.map +1 -0
  152. package/dist/util/tool-caller.d.ts +2 -2
  153. package/dist/util/tool-caller.d.ts.map +1 -1
  154. package/dist/util/tool-caller.js +8 -8
  155. package/dist/util/tool-caller.js.map +1 -1
  156. package/dist/util/validate-component-name.test.d.ts +2 -0
  157. package/dist/util/validate-component-name.test.d.ts.map +1 -0
  158. package/dist/util/validate-component-name.test.js +35 -0
  159. package/dist/util/validate-component-name.test.js.map +1 -0
  160. package/esm/context-helpers/context-helpers.test.js +16 -4
  161. package/esm/context-helpers/context-helpers.test.js.map +1 -1
  162. package/esm/context-helpers/current-interactables-context-helper.d.ts +2 -2
  163. package/esm/context-helpers/current-interactables-context-helper.d.ts.map +1 -1
  164. package/esm/context-helpers/current-interactables-context-helper.js +31 -15
  165. package/esm/context-helpers/current-interactables-context-helper.js.map +1 -1
  166. package/esm/context-helpers/registry.d.ts +2 -2
  167. package/esm/context-helpers/registry.d.ts.map +1 -1
  168. package/esm/context-helpers/registry.js.map +1 -1
  169. package/esm/context-helpers/types.d.ts +2 -2
  170. package/esm/context-helpers/types.d.ts.map +1 -1
  171. package/esm/context-helpers/types.js.map +1 -1
  172. package/esm/hooks/use-message-images.test.js +174 -37
  173. package/esm/hooks/use-message-images.test.js.map +1 -1
  174. package/esm/hooks/use-tambo-voice.d.ts +1 -1
  175. package/esm/hooks/use-tambo-voice.js +1 -1
  176. package/esm/hooks/use-tambo-voice.js.map +1 -1
  177. package/esm/hooks/use-tambo-voice.test.d.ts +2 -0
  178. package/esm/hooks/use-tambo-voice.test.d.ts.map +1 -0
  179. package/esm/hooks/use-tambo-voice.test.js +234 -0
  180. package/esm/hooks/use-tambo-voice.test.js.map +1 -0
  181. package/esm/index.d.ts +2 -2
  182. package/esm/index.d.ts.map +1 -1
  183. package/esm/index.js.map +1 -1
  184. package/esm/mcp/elicitation.d.ts.map +1 -1
  185. package/esm/mcp/elicitation.js +12 -0
  186. package/esm/mcp/elicitation.js.map +1 -1
  187. package/esm/mcp/elicitation.test.js +8 -1
  188. package/esm/mcp/elicitation.test.js.map +1 -1
  189. package/esm/mcp/mcp-client.d.ts +6 -10
  190. package/esm/mcp/mcp-client.d.ts.map +1 -1
  191. package/esm/mcp/mcp-client.js.map +1 -1
  192. package/esm/mcp/mcp-hooks.d.ts +12 -60
  193. package/esm/mcp/mcp-hooks.d.ts.map +1 -1
  194. package/esm/mcp/mcp-hooks.js +57 -10
  195. package/esm/mcp/mcp-hooks.js.map +1 -1
  196. package/esm/mcp/mcp-hooks.test.js +423 -0
  197. package/esm/mcp/mcp-hooks.test.js.map +1 -1
  198. package/esm/mcp/tambo-mcp-provider.d.ts.map +1 -1
  199. package/esm/mcp/tambo-mcp-provider.js +3 -0
  200. package/esm/mcp/tambo-mcp-provider.js.map +1 -1
  201. package/esm/mcp/tambo-mcp-provider.test.js +37 -0
  202. package/esm/mcp/tambo-mcp-provider.test.js.map +1 -1
  203. package/esm/model/component-metadata.d.ts +53 -20
  204. package/esm/model/component-metadata.d.ts.map +1 -1
  205. package/esm/model/component-metadata.js.map +1 -1
  206. package/esm/model/tambo-interactable.d.ts +6 -0
  207. package/esm/model/tambo-interactable.d.ts.map +1 -1
  208. package/esm/model/tambo-interactable.js.map +1 -1
  209. package/esm/providers/index.d.ts +1 -1
  210. package/esm/providers/index.d.ts.map +1 -1
  211. package/esm/providers/index.js.map +1 -1
  212. package/esm/providers/tambo-client-provider.d.ts +8 -0
  213. package/esm/providers/tambo-client-provider.d.ts.map +1 -1
  214. package/esm/providers/tambo-client-provider.js +11 -12
  215. package/esm/providers/tambo-client-provider.js.map +1 -1
  216. package/esm/providers/tambo-client-provider.test.d.ts +2 -0
  217. package/esm/providers/tambo-client-provider.test.d.ts.map +1 -0
  218. package/esm/providers/tambo-client-provider.test.js +203 -0
  219. package/esm/providers/tambo-client-provider.test.js.map +1 -0
  220. package/esm/providers/tambo-context-attachment-provider.d.ts +34 -92
  221. package/esm/providers/tambo-context-attachment-provider.d.ts.map +1 -1
  222. package/esm/providers/tambo-context-attachment-provider.js +63 -106
  223. package/esm/providers/tambo-context-attachment-provider.js.map +1 -1
  224. package/esm/providers/tambo-context-attachment-provider.test.js +230 -464
  225. package/esm/providers/tambo-context-attachment-provider.test.js.map +1 -1
  226. package/esm/providers/tambo-interactable-provider.d.ts +2 -0
  227. package/esm/providers/tambo-interactable-provider.d.ts.map +1 -1
  228. package/esm/providers/tambo-interactable-provider.js +29 -4
  229. package/esm/providers/tambo-interactable-provider.js.map +1 -1
  230. package/esm/providers/tambo-interactable-provider.test.js +1 -1
  231. package/esm/providers/tambo-interactable-provider.test.js.map +1 -1
  232. package/esm/providers/tambo-interactables-additional-context.test.js +2 -5
  233. package/esm/providers/tambo-interactables-additional-context.test.js.map +1 -1
  234. package/esm/providers/tambo-provider.d.ts +2 -3
  235. package/esm/providers/tambo-provider.d.ts.map +1 -1
  236. package/esm/providers/tambo-provider.js +5 -6
  237. package/esm/providers/tambo-provider.js.map +1 -1
  238. package/esm/providers/tambo-registry-provider.test.js +16 -0
  239. package/esm/providers/tambo-registry-provider.test.js.map +1 -1
  240. package/esm/providers/tambo-registry-schema-compat.test.js +31 -0
  241. package/esm/providers/tambo-registry-schema-compat.test.js.map +1 -1
  242. package/esm/providers/tambo-thread-input-provider.d.ts +1 -1
  243. package/esm/providers/tambo-thread-input-provider.d.ts.map +1 -1
  244. package/esm/providers/tambo-thread-input-provider.js +5 -1
  245. package/esm/providers/tambo-thread-input-provider.js.map +1 -1
  246. package/esm/providers/tambo-thread-provider-initial-messages.test.js +84 -2
  247. package/esm/providers/tambo-thread-provider-initial-messages.test.js.map +1 -1
  248. package/esm/providers/tambo-thread-provider.d.ts.map +1 -1
  249. package/esm/providers/tambo-thread-provider.js +56 -43
  250. package/esm/providers/tambo-thread-provider.js.map +1 -1
  251. package/esm/providers/tambo-thread-provider.test.js +456 -262
  252. package/esm/providers/tambo-thread-provider.test.js.map +1 -1
  253. package/esm/schema/json-schema.js +1 -1
  254. package/esm/schema/json-schema.js.map +1 -1
  255. package/esm/schema/schema.test.js +238 -1
  256. package/esm/schema/schema.test.js.map +1 -1
  257. package/esm/schema/standard-schema.d.ts +1 -0
  258. package/esm/schema/standard-schema.d.ts.map +1 -1
  259. package/esm/schema/standard-schema.js +18 -13
  260. package/esm/schema/standard-schema.js.map +1 -1
  261. package/esm/schema/standard-schema.test.d.ts +2 -0
  262. package/esm/schema/standard-schema.test.d.ts.map +1 -0
  263. package/esm/schema/standard-schema.test.js +130 -0
  264. package/esm/schema/standard-schema.test.js.map +1 -0
  265. package/esm/schema/validate.test.js +149 -0
  266. package/esm/schema/validate.test.js.map +1 -1
  267. package/esm/schema/zod.d.ts +7 -4
  268. package/esm/schema/zod.d.ts.map +1 -1
  269. package/esm/schema/zod.js +65 -22
  270. package/esm/schema/zod.js.map +1 -1
  271. package/esm/schema/zod.test.js +113 -1
  272. package/esm/schema/zod.test.js.map +1 -1
  273. package/esm/testing/tools.d.ts +4 -1
  274. package/esm/testing/tools.d.ts.map +1 -1
  275. package/esm/testing/tools.js +6 -1
  276. package/esm/testing/tools.js.map +1 -1
  277. package/esm/util/generate-component.d.ts.map +1 -1
  278. package/esm/util/generate-component.js +18 -3
  279. package/esm/util/generate-component.js.map +1 -1
  280. package/esm/util/generate-component.test.d.ts +2 -0
  281. package/esm/util/generate-component.test.d.ts.map +1 -0
  282. package/esm/util/generate-component.test.js +302 -0
  283. package/esm/util/generate-component.test.js.map +1 -0
  284. package/esm/util/is-promise.d.ts +9 -0
  285. package/esm/util/is-promise.d.ts.map +1 -0
  286. package/esm/util/is-promise.js +17 -0
  287. package/esm/util/is-promise.js.map +1 -0
  288. package/esm/util/is-promise.test.d.ts +2 -0
  289. package/esm/util/is-promise.test.d.ts.map +1 -0
  290. package/esm/util/is-promise.test.js +46 -0
  291. package/esm/util/is-promise.test.js.map +1 -0
  292. package/esm/util/query-utils.test.d.ts +2 -0
  293. package/esm/util/query-utils.test.d.ts.map +1 -0
  294. package/esm/util/query-utils.test.js +380 -0
  295. package/esm/util/query-utils.test.js.map +1 -0
  296. package/esm/util/registry-validators.d.ts.map +1 -1
  297. package/esm/util/registry-validators.js +7 -0
  298. package/esm/util/registry-validators.js.map +1 -1
  299. package/esm/util/registry-validators.test.js +57 -0
  300. package/esm/util/registry-validators.test.js.map +1 -1
  301. package/esm/util/registry.d.ts.map +1 -1
  302. package/esm/util/registry.js +9 -0
  303. package/esm/util/registry.js.map +1 -1
  304. package/esm/util/registry.test.js +324 -2
  305. package/esm/util/registry.test.js.map +1 -1
  306. package/esm/util/resource-validators.test.d.ts +2 -0
  307. package/esm/util/resource-validators.test.d.ts.map +1 -0
  308. package/esm/util/resource-validators.test.js +88 -0
  309. package/esm/util/resource-validators.test.js.map +1 -0
  310. package/esm/util/tool-caller.d.ts +2 -2
  311. package/esm/util/tool-caller.d.ts.map +1 -1
  312. package/esm/util/tool-caller.js +8 -8
  313. package/esm/util/tool-caller.js.map +1 -1
  314. package/esm/util/validate-component-name.test.d.ts +2 -0
  315. package/esm/util/validate-component-name.test.d.ts.map +1 -0
  316. package/esm/util/validate-component-name.test.js +33 -0
  317. package/esm/util/validate-component-name.test.js.map +1 -0
  318. package/package.json +15 -23
  319. package/dist/schema/alias.d.ts +0 -3
  320. package/dist/schema/alias.d.ts.map +0 -1
  321. package/dist/schema/alias.js +0 -6
  322. package/dist/schema/alias.js.map +0 -1
  323. package/esm/schema/alias.d.ts +0 -3
  324. package/esm/schema/alias.d.ts.map +0 -1
  325. package/esm/schema/alias.js +0 -13
  326. package/esm/schema/alias.js.map +0 -1
@@ -110,8 +110,18 @@ describe("TamboThreadProvider", () => {
110
110
  ],
111
111
  },
112
112
  ];
113
- // Use helpers that explicitly return null so they don't appear in additionalContext
114
- const Wrapper = ({ children }) => {
113
+ /**
114
+ * Creates a test wrapper component with configurable options.
115
+ * Reduces duplication across tests by centralizing provider setup.
116
+ * @param options - Configuration options for the wrapper
117
+ * @param options.components - The Tambo components to register
118
+ * @param options.streaming - Whether to enable streaming responses
119
+ * @param options.onCallUnregisteredTool - Handler for unregistered tool calls
120
+ * @param options.autoGenerateThreadName - Whether to auto-generate thread names
121
+ * @param options.autoGenerateNameThreshold - Token threshold for auto-generating names
122
+ * @returns A React component that wraps children with the necessary providers
123
+ */
124
+ const createWrapper = ({ components = mockRegistry, streaming = false, onCallUnregisteredTool, autoGenerateThreadName, autoGenerateNameThreshold, } = {}) => function TestWrapper({ children }) {
115
125
  const client = useTamboClient();
116
126
  const queryClient = useTamboQueryClient();
117
127
  return (React.createElement(TamboClientContext.Provider, { value: {
@@ -119,14 +129,16 @@ describe("TamboThreadProvider", () => {
119
129
  queryClient,
120
130
  isUpdatingToken: false,
121
131
  } },
122
- React.createElement(TamboRegistryProvider, { components: mockRegistry },
132
+ React.createElement(TamboRegistryProvider, { components: components, onCallUnregisteredTool: onCallUnregisteredTool },
123
133
  React.createElement(TamboContextHelpersProvider, { contextHelpers: {
124
134
  currentTimeContextHelper: () => null,
125
135
  currentPageContextHelper: () => null,
126
136
  } },
127
137
  React.createElement(TamboMcpTokenProvider, null,
128
- React.createElement(TamboThreadProvider, { streaming: false }, children))))));
138
+ React.createElement(TamboThreadProvider, { streaming: streaming, autoGenerateThreadName: autoGenerateThreadName, autoGenerateNameThreshold: autoGenerateNameThreshold }, children))))));
129
139
  };
140
+ // Default wrapper for most tests
141
+ const Wrapper = createWrapper();
130
142
  beforeEach(() => {
131
143
  jest.clearAllMocks();
132
144
  // Setup mock query client
@@ -344,22 +356,6 @@ describe("TamboThreadProvider", () => {
344
356
  const mockOnCallUnregisteredTool = jest
345
357
  .fn()
346
358
  .mockResolvedValue("unregistered-tool-result");
347
- const WrapperWithUnregisteredTool = ({ children, }) => {
348
- const client = useTamboClient();
349
- const queryClient = useTamboQueryClient();
350
- return (React.createElement(TamboClientContext.Provider, { value: {
351
- client,
352
- queryClient,
353
- isUpdatingToken: false,
354
- } },
355
- React.createElement(TamboRegistryProvider, { components: mockRegistry, onCallUnregisteredTool: mockOnCallUnregisteredTool },
356
- React.createElement(TamboContextHelpersProvider, { contextHelpers: {
357
- currentTimeContextHelper: () => null,
358
- currentPageContextHelper: () => null,
359
- } },
360
- React.createElement(TamboMcpTokenProvider, null,
361
- React.createElement(TamboThreadProvider, { streaming: false }, children))))));
362
- };
363
359
  const mockUnregisteredToolCallResponse = {
364
360
  responseMessageDto: {
365
361
  id: "unregistered-tool-call-1",
@@ -394,7 +390,9 @@ describe("TamboThreadProvider", () => {
394
390
  mcpAccessToken: "test-mcp-access-token",
395
391
  });
396
392
  const { result } = renderHook(() => useTamboThread(), {
397
- wrapper: WrapperWithUnregisteredTool,
393
+ wrapper: createWrapper({
394
+ onCallUnregisteredTool: mockOnCallUnregisteredTool,
395
+ }),
398
396
  });
399
397
  await act(async () => {
400
398
  await result.current.sendThreadMessage("Use unregistered tool", {
@@ -451,23 +449,6 @@ describe("TamboThreadProvider", () => {
451
449
  });
452
450
  describe("streaming behavior", () => {
453
451
  it("should call advanceStream when streamResponse=true", async () => {
454
- // Use wrapper with streaming=true to show that explicit streamResponse=true works
455
- const WrapperWithStreaming = ({ children, }) => {
456
- const client = useTamboClient();
457
- const queryClient = useTamboQueryClient();
458
- return (React.createElement(TamboClientContext.Provider, { value: {
459
- client,
460
- queryClient,
461
- isUpdatingToken: false,
462
- } },
463
- React.createElement(TamboRegistryProvider, { components: mockRegistry },
464
- React.createElement(TamboContextHelpersProvider, { contextHelpers: {
465
- currentTimeContextHelper: () => null,
466
- currentPageContextHelper: () => null,
467
- } },
468
- React.createElement(TamboMcpTokenProvider, null,
469
- React.createElement(TamboThreadProvider, { streaming: true }, children))))));
470
- };
471
452
  const mockStreamResponse = {
472
453
  responseMessageDto: {
473
454
  id: "stream-response",
@@ -488,7 +469,7 @@ describe("TamboThreadProvider", () => {
488
469
  };
489
470
  jest.mocked(advanceStream).mockResolvedValue(mockAsyncIterator);
490
471
  const { result } = renderHook(() => useTamboThread(), {
491
- wrapper: WrapperWithStreaming,
472
+ wrapper: createWrapper({ streaming: true }),
492
473
  });
493
474
  await act(async () => {
494
475
  await result.current.sendThreadMessage("Hello streaming", {
@@ -523,24 +504,8 @@ describe("TamboThreadProvider", () => {
523
504
  });
524
505
  it("should call advanceById when streamResponse=false for existing thread", async () => {
525
506
  // Use wrapper with streaming=true to show that explicit streamResponse=false overrides provider setting
526
- const WrapperWithStreaming = ({ children, }) => {
527
- const client = useTamboClient();
528
- const queryClient = useTamboQueryClient();
529
- return (React.createElement(TamboClientContext.Provider, { value: {
530
- client,
531
- queryClient,
532
- isUpdatingToken: false,
533
- } },
534
- React.createElement(TamboRegistryProvider, { components: mockRegistry },
535
- React.createElement(TamboContextHelpersProvider, { contextHelpers: {
536
- currentTimeContextHelper: () => null,
537
- currentPageContextHelper: () => null,
538
- } },
539
- React.createElement(TamboMcpTokenProvider, null,
540
- React.createElement(TamboThreadProvider, { streaming: true }, children))))));
541
- };
542
507
  const { result } = renderHook(() => useTamboThread(), {
543
- wrapper: WrapperWithStreaming,
508
+ wrapper: createWrapper({ streaming: true }),
544
509
  });
545
510
  await act(async () => {
546
511
  await result.current.sendThreadMessage("Hello non-streaming", {
@@ -575,24 +540,8 @@ describe("TamboThreadProvider", () => {
575
540
  });
576
541
  it("should call advanceById when streamResponse is undefined and provider streaming=false", async () => {
577
542
  // Use wrapper with streaming=false to test that undefined streamResponse respects provider setting
578
- const WrapperWithoutStreaming = ({ children, }) => {
579
- const client = useTamboClient();
580
- const queryClient = useTamboQueryClient();
581
- return (React.createElement(TamboClientContext.Provider, { value: {
582
- client,
583
- queryClient,
584
- isUpdatingToken: false,
585
- } },
586
- React.createElement(TamboRegistryProvider, { components: mockRegistry },
587
- React.createElement(TamboContextHelpersProvider, { contextHelpers: {
588
- currentTimeContextHelper: () => null,
589
- currentPageContextHelper: () => null,
590
- } },
591
- React.createElement(TamboMcpTokenProvider, null,
592
- React.createElement(TamboThreadProvider, { streaming: false }, children))))));
593
- };
594
543
  const { result } = renderHook(() => useTamboThread(), {
595
- wrapper: WrapperWithoutStreaming,
544
+ wrapper: createWrapper({ streaming: false }),
596
545
  });
597
546
  await act(async () => {
598
547
  await result.current.sendThreadMessage("Hello default", {
@@ -626,23 +575,6 @@ describe("TamboThreadProvider", () => {
626
575
  expect(advanceStream).not.toHaveBeenCalled();
627
576
  });
628
577
  it("should call advanceStream when streamResponse is undefined and provider streaming=true (default)", async () => {
629
- // Use wrapper with streaming=true (default) to test that undefined streamResponse respects provider setting
630
- const WrapperWithDefaultStreaming = ({ children, }) => {
631
- const client = useTamboClient();
632
- const queryClient = useTamboQueryClient();
633
- return (React.createElement(TamboClientContext.Provider, { value: {
634
- client,
635
- queryClient,
636
- isUpdatingToken: false,
637
- } },
638
- React.createElement(TamboRegistryProvider, { components: mockRegistry },
639
- React.createElement(TamboContextHelpersProvider, { contextHelpers: {
640
- currentTimeContextHelper: () => null,
641
- currentPageContextHelper: () => null,
642
- } },
643
- React.createElement(TamboMcpTokenProvider, null,
644
- React.createElement(TamboThreadProvider, null, children))))));
645
- };
646
578
  const mockStreamResponse = {
647
579
  responseMessageDto: {
648
580
  id: "stream-response",
@@ -663,7 +595,7 @@ describe("TamboThreadProvider", () => {
663
595
  };
664
596
  jest.mocked(advanceStream).mockResolvedValue(mockAsyncIterator);
665
597
  const { result } = renderHook(() => useTamboThread(), {
666
- wrapper: WrapperWithDefaultStreaming,
598
+ wrapper: createWrapper({ streaming: true }),
667
599
  });
668
600
  await act(async () => {
669
601
  await result.current.sendThreadMessage("Hello default streaming", {
@@ -698,24 +630,8 @@ describe("TamboThreadProvider", () => {
698
630
  });
699
631
  it("should call advance when streamResponse=false for placeholder thread", async () => {
700
632
  // Use wrapper with streaming=true to show that explicit streamResponse=false overrides provider setting
701
- const WrapperWithStreaming = ({ children, }) => {
702
- const client = useTamboClient();
703
- const queryClient = useTamboQueryClient();
704
- return (React.createElement(TamboClientContext.Provider, { value: {
705
- client,
706
- queryClient,
707
- isUpdatingToken: false,
708
- } },
709
- React.createElement(TamboRegistryProvider, { components: mockRegistry },
710
- React.createElement(TamboContextHelpersProvider, { contextHelpers: {
711
- currentTimeContextHelper: () => null,
712
- currentPageContextHelper: () => null,
713
- } },
714
- React.createElement(TamboMcpTokenProvider, null,
715
- React.createElement(TamboThreadProvider, { streaming: true }, children))))));
716
- };
717
633
  const { result } = renderHook(() => useTamboThread(), {
718
- wrapper: WrapperWithStreaming,
634
+ wrapper: createWrapper({ streaming: true }),
719
635
  });
720
636
  // Start with placeholder thread (which is the default state)
721
637
  expect(result.current.thread.id).toBe("placeholder");
@@ -751,23 +667,6 @@ describe("TamboThreadProvider", () => {
751
667
  expect(advanceStream).not.toHaveBeenCalled();
752
668
  });
753
669
  it("should call advanceStream when streamResponse=true for placeholder thread", async () => {
754
- // Use wrapper with streaming=false to show that explicit streamResponse=true overrides provider setting
755
- const WrapperWithoutStreaming = ({ children, }) => {
756
- const client = useTamboClient();
757
- const queryClient = useTamboQueryClient();
758
- return (React.createElement(TamboClientContext.Provider, { value: {
759
- client,
760
- queryClient,
761
- isUpdatingToken: false,
762
- } },
763
- React.createElement(TamboRegistryProvider, { components: mockRegistry },
764
- React.createElement(TamboContextHelpersProvider, { contextHelpers: {
765
- currentTimeContextHelper: () => null,
766
- currentPageContextHelper: () => null,
767
- } },
768
- React.createElement(TamboMcpTokenProvider, null,
769
- React.createElement(TamboThreadProvider, { streaming: false }, children))))));
770
- };
771
670
  const mockStreamResponse = {
772
671
  responseMessageDto: {
773
672
  id: "stream-response",
@@ -788,7 +687,7 @@ describe("TamboThreadProvider", () => {
788
687
  };
789
688
  jest.mocked(advanceStream).mockResolvedValue(mockAsyncIterator);
790
689
  const { result } = renderHook(() => useTamboThread(), {
791
- wrapper: WrapperWithoutStreaming,
690
+ wrapper: createWrapper({ streaming: false }),
792
691
  });
793
692
  // Start with placeholder thread (which is the default state)
794
693
  expect(result.current.thread.id).toBe("placeholder");
@@ -823,6 +722,94 @@ describe("TamboThreadProvider", () => {
823
722
  expect(mockThreadsApi.advance).not.toHaveBeenCalled();
824
723
  expect(mockThreadsApi.advanceByID).not.toHaveBeenCalled();
825
724
  });
725
+ it("should handle multiple sequential messages during streaming (server tool scenario)", async () => {
726
+ // This test verifies the fix for the bug where the second message doesn't render
727
+ // during server tool response streaming. The scenario:
728
+ // 1. First message: "I will call the tool..." with statusMessage
729
+ // 2. Second message: The tool result response streaming in
730
+ // First message - tool announcement (server tools don't have componentName set during streaming)
731
+ const mockFirstMessage = {
732
+ responseMessageDto: {
733
+ id: "msg-first",
734
+ content: [{ type: "text", text: "I will search the docs..." }],
735
+ role: "assistant",
736
+ threadId: "test-thread-1",
737
+ component: {
738
+ componentName: "",
739
+ componentState: {},
740
+ message: "",
741
+ props: {},
742
+ statusMessage: "searching the Tambo docs...",
743
+ },
744
+ componentState: {},
745
+ createdAt: new Date().toISOString(),
746
+ },
747
+ generationStage: GenerationStage.STREAMING_RESPONSE,
748
+ mcpAccessToken: "test-mcp-access-token",
749
+ };
750
+ // Second message - tool result (different ID!)
751
+ const mockSecondMessageChunk1 = {
752
+ responseMessageDto: {
753
+ id: "msg-second",
754
+ content: [{ type: "text", text: "Here's what I found..." }],
755
+ role: "assistant",
756
+ threadId: "test-thread-1",
757
+ componentState: {},
758
+ createdAt: new Date().toISOString(),
759
+ },
760
+ generationStage: GenerationStage.STREAMING_RESPONSE,
761
+ mcpAccessToken: "test-mcp-access-token",
762
+ };
763
+ const mockSecondMessageChunk2 = {
764
+ responseMessageDto: {
765
+ id: "msg-second",
766
+ content: [
767
+ {
768
+ type: "text",
769
+ text: "Here's what I found in the documentation about that topic.",
770
+ },
771
+ ],
772
+ role: "assistant",
773
+ threadId: "test-thread-1",
774
+ componentState: {},
775
+ createdAt: new Date().toISOString(),
776
+ },
777
+ generationStage: GenerationStage.COMPLETE,
778
+ mcpAccessToken: "test-mcp-access-token",
779
+ };
780
+ const mockAsyncIterator = {
781
+ [Symbol.asyncIterator]: async function* () {
782
+ yield mockFirstMessage;
783
+ yield mockSecondMessageChunk1;
784
+ yield mockSecondMessageChunk2;
785
+ },
786
+ };
787
+ jest.mocked(advanceStream).mockResolvedValue(mockAsyncIterator);
788
+ const { result } = renderHook(() => useTamboThread(), {
789
+ wrapper: createWrapper({ streaming: true }),
790
+ });
791
+ await act(async () => {
792
+ await result.current.sendThreadMessage("Search the docs", {
793
+ threadId: "test-thread-1",
794
+ streamResponse: true,
795
+ });
796
+ });
797
+ // Thread should have 3 messages: user message + 2 assistant messages
798
+ expect(result.current.thread.messages).toHaveLength(3);
799
+ // Filter to assistant messages only
800
+ const assistantMessages = result.current.thread.messages.filter((m) => m.role === "assistant");
801
+ expect(assistantMessages).toHaveLength(2);
802
+ // First assistant message should have the tool status
803
+ const firstMsg = result.current.thread.messages.find((m) => m.id === "msg-first");
804
+ expect(firstMsg).toBeDefined();
805
+ expect(firstMsg?.content[0]?.text).toContain("search the docs");
806
+ // Second assistant message should have the final content
807
+ const secondMsg = result.current.thread.messages.find((m) => m.id === "msg-second");
808
+ expect(secondMsg).toBeDefined();
809
+ expect(secondMsg?.content[0]?.text).toContain("what I found in the documentation");
810
+ // Generation should be complete
811
+ expect(result.current.generationStage).toBe(GenerationStage.COMPLETE);
812
+ });
826
813
  });
827
814
  describe("error handling", () => {
828
815
  it("should set generation stage to ERROR when non-streaming sendThreadMessage fails", async () => {
@@ -973,22 +960,6 @@ describe("TamboThreadProvider", () => {
973
960
  ],
974
961
  },
975
962
  ];
976
- const WrapperWithCustomTool = ({ children, }) => {
977
- const client = useTamboClient();
978
- const queryClient = useTamboQueryClient();
979
- return (React.createElement(TamboClientContext.Provider, { value: {
980
- client,
981
- queryClient,
982
- isUpdatingToken: false,
983
- } },
984
- React.createElement(TamboRegistryProvider, { components: customToolRegistry },
985
- React.createElement(TamboContextHelpersProvider, { contextHelpers: {
986
- currentTimeContextHelper: () => null,
987
- currentPageContextHelper: () => null,
988
- } },
989
- React.createElement(TamboMcpTokenProvider, null,
990
- React.createElement(TamboThreadProvider, { streaming: false }, children))))));
991
- };
992
963
  const mockToolCallResponse = {
993
964
  responseMessageDto: {
994
965
  id: "tool-call-1",
@@ -1021,7 +992,7 @@ describe("TamboThreadProvider", () => {
1021
992
  mcpAccessToken: "test-mcp-access-token",
1022
993
  });
1023
994
  const { result } = renderHook(() => useTamboThread(), {
1024
- wrapper: WrapperWithCustomTool,
995
+ wrapper: createWrapper({ components: customToolRegistry }),
1025
996
  });
1026
997
  await act(async () => {
1027
998
  await result.current.sendThreadMessage("Use custom tool", {
@@ -1074,22 +1045,6 @@ describe("TamboThreadProvider", () => {
1074
1045
  ],
1075
1046
  },
1076
1047
  ];
1077
- const WrapperWithAsyncTool = ({ children, }) => {
1078
- const client = useTamboClient();
1079
- const queryClient = useTamboQueryClient();
1080
- return (React.createElement(TamboClientContext.Provider, { value: {
1081
- client,
1082
- queryClient,
1083
- isUpdatingToken: false,
1084
- } },
1085
- React.createElement(TamboRegistryProvider, { components: customToolRegistry },
1086
- React.createElement(TamboContextHelpersProvider, { contextHelpers: {
1087
- currentTimeContextHelper: () => null,
1088
- currentPageContextHelper: () => null,
1089
- } },
1090
- React.createElement(TamboMcpTokenProvider, null,
1091
- React.createElement(TamboThreadProvider, { streaming: true }, children))))));
1092
- };
1093
1048
  const mockToolCallChunk = {
1094
1049
  responseMessageDto: {
1095
1050
  id: "tool-call-chunk",
@@ -1135,7 +1090,10 @@ describe("TamboThreadProvider", () => {
1135
1090
  },
1136
1091
  });
1137
1092
  const { result } = renderHook(() => useTamboThread(), {
1138
- wrapper: WrapperWithAsyncTool,
1093
+ wrapper: createWrapper({
1094
+ components: customToolRegistry,
1095
+ streaming: true,
1096
+ }),
1139
1097
  });
1140
1098
  await act(async () => {
1141
1099
  await result.current.sendThreadMessage("Use async tool", {
@@ -1183,22 +1141,6 @@ describe("TamboThreadProvider", () => {
1183
1141
  ],
1184
1142
  },
1185
1143
  ];
1186
- const WrapperWithoutTransform = ({ children, }) => {
1187
- const client = useTamboClient();
1188
- const queryClient = useTamboQueryClient();
1189
- return (React.createElement(TamboClientContext.Provider, { value: {
1190
- client,
1191
- queryClient,
1192
- isUpdatingToken: false,
1193
- } },
1194
- React.createElement(TamboRegistryProvider, { components: toolWithoutTransform },
1195
- React.createElement(TamboContextHelpersProvider, { contextHelpers: {
1196
- currentTimeContextHelper: () => null,
1197
- currentPageContextHelper: () => null,
1198
- } },
1199
- React.createElement(TamboMcpTokenProvider, null,
1200
- React.createElement(TamboThreadProvider, { streaming: false }, children))))));
1201
- };
1202
1144
  const mockToolCallResponse = {
1203
1145
  responseMessageDto: {
1204
1146
  id: "tool-call-1",
@@ -1231,7 +1173,7 @@ describe("TamboThreadProvider", () => {
1231
1173
  mcpAccessToken: "test-mcp-access-token",
1232
1174
  });
1233
1175
  const { result } = renderHook(() => useTamboThread(), {
1234
- wrapper: WrapperWithoutTransform,
1176
+ wrapper: createWrapper({ components: toolWithoutTransform }),
1235
1177
  });
1236
1178
  await act(async () => {
1237
1179
  await result.current.sendThreadMessage("Use tool without transform", {
@@ -1281,22 +1223,6 @@ describe("TamboThreadProvider", () => {
1281
1223
  ],
1282
1224
  },
1283
1225
  ];
1284
- const WrapperWithErrorTool = ({ children, }) => {
1285
- const client = useTamboClient();
1286
- const queryClient = useTamboQueryClient();
1287
- return (React.createElement(TamboClientContext.Provider, { value: {
1288
- client,
1289
- queryClient,
1290
- isUpdatingToken: false,
1291
- } },
1292
- React.createElement(TamboRegistryProvider, { components: toolWithTransform },
1293
- React.createElement(TamboContextHelpersProvider, { contextHelpers: {
1294
- currentTimeContextHelper: () => null,
1295
- currentPageContextHelper: () => null,
1296
- } },
1297
- React.createElement(TamboMcpTokenProvider, null,
1298
- React.createElement(TamboThreadProvider, { streaming: false }, children))))));
1299
- };
1300
1226
  const mockToolCallResponse = {
1301
1227
  responseMessageDto: {
1302
1228
  id: "tool-call-1",
@@ -1329,7 +1255,7 @@ describe("TamboThreadProvider", () => {
1329
1255
  mcpAccessToken: "test-mcp-access-token",
1330
1256
  });
1331
1257
  const { result } = renderHook(() => useTamboThread(), {
1332
- wrapper: WrapperWithErrorTool,
1258
+ wrapper: createWrapper({ components: toolWithTransform }),
1333
1259
  });
1334
1260
  await act(async () => {
1335
1261
  await result.current.sendThreadMessage("Use error tool", {
@@ -1355,26 +1281,339 @@ describe("TamboThreadProvider", () => {
1355
1281
  }));
1356
1282
  });
1357
1283
  });
1284
+ describe("tamboStreamableHint streaming behavior", () => {
1285
+ it("should call streamable tool during streaming when tamboStreamableHint is true", async () => {
1286
+ const streamableToolFn = jest
1287
+ .fn()
1288
+ .mockResolvedValue({ data: "streamed" });
1289
+ const customToolRegistry = [
1290
+ {
1291
+ name: "TestComponent",
1292
+ component: () => React.createElement("div", null, "Test"),
1293
+ description: "Test",
1294
+ propsSchema: z.object({ test: z.string() }),
1295
+ associatedTools: [
1296
+ {
1297
+ name: "streamable-tool",
1298
+ tool: streamableToolFn,
1299
+ description: "Tool safe for streaming",
1300
+ inputSchema: z.object({ input: z.string() }),
1301
+ outputSchema: z.object({ data: z.string() }),
1302
+ annotations: { tamboStreamableHint: true },
1303
+ },
1304
+ ],
1305
+ },
1306
+ ];
1307
+ // First chunk initializes finalMessage
1308
+ const mockInitialChunk = {
1309
+ responseMessageDto: {
1310
+ id: "initial-chunk",
1311
+ content: [{ type: "text", text: "Starting..." }],
1312
+ role: "assistant",
1313
+ threadId: "test-thread-1",
1314
+ componentState: {},
1315
+ createdAt: new Date().toISOString(),
1316
+ },
1317
+ generationStage: GenerationStage.STREAMING_RESPONSE,
1318
+ mcpAccessToken: "test-mcp-access-token",
1319
+ };
1320
+ // Second chunk has the tool call - this triggers streaming tool handling
1321
+ const mockToolCallChunk = {
1322
+ responseMessageDto: {
1323
+ id: "initial-chunk", // Same ID as initial - it's an update
1324
+ content: [{ type: "text", text: "Streaming..." }],
1325
+ role: "assistant",
1326
+ threadId: "test-thread-1",
1327
+ component: {
1328
+ componentName: "",
1329
+ componentState: {},
1330
+ message: "",
1331
+ props: {},
1332
+ toolCallRequest: {
1333
+ toolName: "streamable-tool",
1334
+ parameters: [
1335
+ { parameterName: "input", parameterValue: "stream-test" },
1336
+ ],
1337
+ },
1338
+ },
1339
+ componentState: {},
1340
+ createdAt: new Date().toISOString(),
1341
+ },
1342
+ generationStage: GenerationStage.STREAMING_RESPONSE,
1343
+ mcpAccessToken: "test-mcp-access-token",
1344
+ };
1345
+ const mockFinalChunk = {
1346
+ responseMessageDto: {
1347
+ id: "initial-chunk",
1348
+ content: [{ type: "text", text: "Complete" }],
1349
+ role: "assistant",
1350
+ threadId: "test-thread-1",
1351
+ componentState: {},
1352
+ createdAt: new Date().toISOString(),
1353
+ },
1354
+ generationStage: GenerationStage.COMPLETE,
1355
+ mcpAccessToken: "test-mcp-access-token",
1356
+ };
1357
+ const mockAsyncIterator = {
1358
+ [Symbol.asyncIterator]: async function* () {
1359
+ yield mockInitialChunk;
1360
+ yield mockToolCallChunk;
1361
+ yield mockFinalChunk;
1362
+ },
1363
+ };
1364
+ jest.mocked(advanceStream).mockResolvedValueOnce(mockAsyncIterator);
1365
+ const { result } = renderHook(() => useTamboThread(), {
1366
+ wrapper: createWrapper({
1367
+ components: customToolRegistry,
1368
+ streaming: true,
1369
+ }),
1370
+ });
1371
+ await act(async () => {
1372
+ await result.current.sendThreadMessage("Test streamable tool", {
1373
+ threadId: "test-thread-1",
1374
+ streamResponse: true,
1375
+ });
1376
+ });
1377
+ // Streamable tool should be called during streaming
1378
+ expect(streamableToolFn).toHaveBeenCalledWith({ input: "stream-test" });
1379
+ });
1380
+ it("should NOT call non-streamable tool during streaming", async () => {
1381
+ const nonStreamableToolFn = jest
1382
+ .fn()
1383
+ .mockResolvedValue({ data: "result" });
1384
+ const customToolRegistry = [
1385
+ {
1386
+ name: "TestComponent",
1387
+ component: () => React.createElement("div", null, "Test"),
1388
+ description: "Test",
1389
+ propsSchema: z.object({ test: z.string() }),
1390
+ associatedTools: [
1391
+ {
1392
+ name: "non-streamable-tool",
1393
+ tool: nonStreamableToolFn,
1394
+ description: "Tool not safe for streaming",
1395
+ inputSchema: z.object({ input: z.string() }),
1396
+ outputSchema: z.object({ data: z.string() }),
1397
+ // No tamboStreamableHint - defaults to false
1398
+ },
1399
+ ],
1400
+ },
1401
+ ];
1402
+ // First chunk initializes finalMessage
1403
+ const mockInitialChunk = {
1404
+ responseMessageDto: {
1405
+ id: "streaming-chunk",
1406
+ content: [{ type: "text", text: "Starting..." }],
1407
+ role: "assistant",
1408
+ threadId: "test-thread-1",
1409
+ componentState: {},
1410
+ createdAt: new Date().toISOString(),
1411
+ },
1412
+ generationStage: GenerationStage.STREAMING_RESPONSE,
1413
+ mcpAccessToken: "test-mcp-access-token",
1414
+ };
1415
+ // Second chunk has the tool call - but tool is NOT streamable
1416
+ const mockToolCallChunk = {
1417
+ responseMessageDto: {
1418
+ id: "streaming-chunk",
1419
+ content: [{ type: "text", text: "Streaming..." }],
1420
+ role: "assistant",
1421
+ threadId: "test-thread-1",
1422
+ component: {
1423
+ componentName: "",
1424
+ componentState: {},
1425
+ message: "",
1426
+ props: {},
1427
+ toolCallRequest: {
1428
+ toolName: "non-streamable-tool",
1429
+ parameters: [{ parameterName: "input", parameterValue: "test" }],
1430
+ },
1431
+ },
1432
+ componentState: {},
1433
+ createdAt: new Date().toISOString(),
1434
+ },
1435
+ generationStage: GenerationStage.STREAMING_RESPONSE,
1436
+ mcpAccessToken: "test-mcp-access-token",
1437
+ };
1438
+ const mockFinalChunk = {
1439
+ responseMessageDto: {
1440
+ id: "streaming-chunk",
1441
+ content: [{ type: "text", text: "Complete" }],
1442
+ role: "assistant",
1443
+ threadId: "test-thread-1",
1444
+ componentState: {},
1445
+ createdAt: new Date().toISOString(),
1446
+ },
1447
+ generationStage: GenerationStage.COMPLETE,
1448
+ mcpAccessToken: "test-mcp-access-token",
1449
+ };
1450
+ const mockAsyncIterator = {
1451
+ [Symbol.asyncIterator]: async function* () {
1452
+ yield mockInitialChunk;
1453
+ yield mockToolCallChunk;
1454
+ yield mockFinalChunk;
1455
+ },
1456
+ };
1457
+ jest.mocked(advanceStream).mockResolvedValueOnce(mockAsyncIterator);
1458
+ const { result } = renderHook(() => useTamboThread(), {
1459
+ wrapper: createWrapper({
1460
+ components: customToolRegistry,
1461
+ streaming: true,
1462
+ }),
1463
+ });
1464
+ await act(async () => {
1465
+ await result.current.sendThreadMessage("Test non-streamable tool", {
1466
+ threadId: "test-thread-1",
1467
+ streamResponse: true,
1468
+ });
1469
+ });
1470
+ // Non-streamable tool should NOT be called during the streaming chunk phase
1471
+ // (it would only be called when generationStage is COMPLETE with a toolCallRequest)
1472
+ expect(nonStreamableToolFn).not.toHaveBeenCalled();
1473
+ });
1474
+ it("should only call streamable tools during streaming when mixed", async () => {
1475
+ const streamableToolFn = jest
1476
+ .fn()
1477
+ .mockResolvedValue({ data: "streamed" });
1478
+ const nonStreamableToolFn = jest
1479
+ .fn()
1480
+ .mockResolvedValue({ data: "not-streamed" });
1481
+ const customToolRegistry = [
1482
+ {
1483
+ name: "TestComponent",
1484
+ component: () => React.createElement("div", null, "Test"),
1485
+ description: "Test",
1486
+ propsSchema: z.object({ test: z.string() }),
1487
+ associatedTools: [
1488
+ {
1489
+ name: "streamable-tool",
1490
+ tool: streamableToolFn,
1491
+ description: "Tool safe for streaming",
1492
+ inputSchema: z.object({ input: z.string() }),
1493
+ outputSchema: z.object({ data: z.string() }),
1494
+ annotations: { tamboStreamableHint: true },
1495
+ },
1496
+ {
1497
+ name: "non-streamable-tool",
1498
+ tool: nonStreamableToolFn,
1499
+ description: "Tool not safe for streaming",
1500
+ inputSchema: z.object({ input: z.string() }),
1501
+ outputSchema: z.object({ data: z.string() }),
1502
+ annotations: { tamboStreamableHint: false },
1503
+ },
1504
+ ],
1505
+ },
1506
+ ];
1507
+ // First chunk initializes finalMessage
1508
+ const mockInitialChunk = {
1509
+ responseMessageDto: {
1510
+ id: "streaming-chunk",
1511
+ content: [{ type: "text", text: "Starting..." }],
1512
+ role: "assistant",
1513
+ threadId: "test-thread-1",
1514
+ componentState: {},
1515
+ createdAt: new Date().toISOString(),
1516
+ },
1517
+ generationStage: GenerationStage.STREAMING_RESPONSE,
1518
+ mcpAccessToken: "test-mcp-access-token",
1519
+ };
1520
+ // Second chunk calls the streamable tool
1521
+ const mockStreamableToolChunk = {
1522
+ responseMessageDto: {
1523
+ id: "streaming-chunk",
1524
+ content: [{ type: "text", text: "Calling streamable..." }],
1525
+ role: "assistant",
1526
+ threadId: "test-thread-1",
1527
+ component: {
1528
+ componentName: "",
1529
+ componentState: {},
1530
+ message: "",
1531
+ props: {},
1532
+ toolCallRequest: {
1533
+ toolName: "streamable-tool",
1534
+ parameters: [
1535
+ { parameterName: "input", parameterValue: "streamed-input" },
1536
+ ],
1537
+ },
1538
+ },
1539
+ componentState: {},
1540
+ createdAt: new Date().toISOString(),
1541
+ },
1542
+ generationStage: GenerationStage.STREAMING_RESPONSE,
1543
+ mcpAccessToken: "test-mcp-access-token",
1544
+ };
1545
+ // Third chunk calls the non-streamable tool
1546
+ const mockNonStreamableToolChunk = {
1547
+ responseMessageDto: {
1548
+ id: "streaming-chunk",
1549
+ content: [{ type: "text", text: "Calling non-streamable..." }],
1550
+ role: "assistant",
1551
+ threadId: "test-thread-1",
1552
+ component: {
1553
+ componentName: "",
1554
+ componentState: {},
1555
+ message: "",
1556
+ props: {},
1557
+ toolCallRequest: {
1558
+ toolName: "non-streamable-tool",
1559
+ parameters: [
1560
+ {
1561
+ parameterName: "input",
1562
+ parameterValue: "non-streamed-input",
1563
+ },
1564
+ ],
1565
+ },
1566
+ },
1567
+ componentState: {},
1568
+ createdAt: new Date().toISOString(),
1569
+ },
1570
+ generationStage: GenerationStage.STREAMING_RESPONSE,
1571
+ mcpAccessToken: "test-mcp-access-token",
1572
+ };
1573
+ const mockFinalChunk = {
1574
+ responseMessageDto: {
1575
+ id: "streaming-chunk",
1576
+ content: [{ type: "text", text: "Complete" }],
1577
+ role: "assistant",
1578
+ threadId: "test-thread-1",
1579
+ componentState: {},
1580
+ createdAt: new Date().toISOString(),
1581
+ },
1582
+ generationStage: GenerationStage.COMPLETE,
1583
+ mcpAccessToken: "test-mcp-access-token",
1584
+ };
1585
+ const mockAsyncIterator = {
1586
+ [Symbol.asyncIterator]: async function* () {
1587
+ yield mockInitialChunk;
1588
+ yield mockStreamableToolChunk;
1589
+ yield mockNonStreamableToolChunk;
1590
+ yield mockFinalChunk;
1591
+ },
1592
+ };
1593
+ jest.mocked(advanceStream).mockResolvedValueOnce(mockAsyncIterator);
1594
+ const { result } = renderHook(() => useTamboThread(), {
1595
+ wrapper: createWrapper({
1596
+ components: customToolRegistry,
1597
+ streaming: true,
1598
+ }),
1599
+ });
1600
+ await act(async () => {
1601
+ await result.current.sendThreadMessage("Test mixed tools", {
1602
+ threadId: "test-thread-1",
1603
+ streamResponse: true,
1604
+ });
1605
+ });
1606
+ // Only the streamable tool should be called during streaming
1607
+ expect(streamableToolFn).toHaveBeenCalledWith({
1608
+ input: "streamed-input",
1609
+ });
1610
+ expect(nonStreamableToolFn).not.toHaveBeenCalled();
1611
+ });
1612
+ });
1358
1613
  describe("auto-generate thread name", () => {
1359
1614
  it("should auto-generate thread name after reaching threshold", async () => {
1360
- const WrapperWithAutoGenerate = ({ children, }) => {
1361
- const client = useTamboClient();
1362
- const queryClient = useTamboQueryClient();
1363
- return (React.createElement(TamboClientContext.Provider, { value: {
1364
- client,
1365
- queryClient,
1366
- isUpdatingToken: false,
1367
- } },
1368
- React.createElement(TamboRegistryProvider, { components: mockRegistry },
1369
- React.createElement(TamboContextHelpersProvider, { contextHelpers: {
1370
- currentTimeContextHelper: () => null,
1371
- currentPageContextHelper: () => null,
1372
- } },
1373
- React.createElement(TamboMcpTokenProvider, null,
1374
- React.createElement(TamboThreadProvider, { streaming: false, autoGenerateNameThreshold: 2 }, children))))));
1375
- };
1376
1615
  const { result } = renderHook(() => useTamboThread(), {
1377
- wrapper: WrapperWithAutoGenerate,
1616
+ wrapper: createWrapper({ autoGenerateNameThreshold: 2 }),
1378
1617
  });
1379
1618
  const existingThread = createMockThread({
1380
1619
  id: "test-thread-1",
@@ -1411,24 +1650,11 @@ describe("TamboThreadProvider", () => {
1411
1650
  expect(mockQueryClient.setQueryData).toHaveBeenCalledWith(["threads", "test-project-id", undefined], expect.any(Function));
1412
1651
  });
1413
1652
  it("should NOT auto-generate when autoGenerateThreadName is false", async () => {
1414
- const WrapperWithDisabled = ({ children, }) => {
1415
- const client = useTamboClient();
1416
- const queryClient = useTamboQueryClient();
1417
- return (React.createElement(TamboClientContext.Provider, { value: {
1418
- client,
1419
- queryClient,
1420
- isUpdatingToken: false,
1421
- } },
1422
- React.createElement(TamboRegistryProvider, { components: mockRegistry },
1423
- React.createElement(TamboContextHelpersProvider, { contextHelpers: {
1424
- currentTimeContextHelper: () => null,
1425
- currentPageContextHelper: () => null,
1426
- } },
1427
- React.createElement(TamboMcpTokenProvider, null,
1428
- React.createElement(TamboThreadProvider, { streaming: false, autoGenerateThreadName: false, autoGenerateNameThreshold: 2 }, children))))));
1429
- };
1430
1653
  const { result } = renderHook(() => useTamboThread(), {
1431
- wrapper: WrapperWithDisabled,
1654
+ wrapper: createWrapper({
1655
+ autoGenerateThreadName: false,
1656
+ autoGenerateNameThreshold: 2,
1657
+ }),
1432
1658
  });
1433
1659
  const existingThread = createMockThread({
1434
1660
  id: "test-thread-1",
@@ -1461,24 +1687,8 @@ describe("TamboThreadProvider", () => {
1461
1687
  expect(mockThreadsApi.generateName).not.toHaveBeenCalled();
1462
1688
  });
1463
1689
  it("should NOT auto-generate when thread already has a name", async () => {
1464
- const WrapperWithAutoGenerate = ({ children, }) => {
1465
- const client = useTamboClient();
1466
- const queryClient = useTamboQueryClient();
1467
- return (React.createElement(TamboClientContext.Provider, { value: {
1468
- client,
1469
- queryClient,
1470
- isUpdatingToken: false,
1471
- } },
1472
- React.createElement(TamboRegistryProvider, { components: mockRegistry },
1473
- React.createElement(TamboContextHelpersProvider, { contextHelpers: {
1474
- currentTimeContextHelper: () => null,
1475
- currentPageContextHelper: () => null,
1476
- } },
1477
- React.createElement(TamboMcpTokenProvider, null,
1478
- React.createElement(TamboThreadProvider, { streaming: false, autoGenerateNameThreshold: 2 }, children))))));
1479
- };
1480
1690
  const { result } = renderHook(() => useTamboThread(), {
1481
- wrapper: WrapperWithAutoGenerate,
1691
+ wrapper: createWrapper({ autoGenerateNameThreshold: 2 }),
1482
1692
  });
1483
1693
  const threadWithName = createMockThread({
1484
1694
  id: "test-thread-1",
@@ -1516,24 +1726,8 @@ describe("TamboThreadProvider", () => {
1516
1726
  expect(mockThreadsApi.generateName).not.toHaveBeenCalled();
1517
1727
  });
1518
1728
  it("should NOT auto-generate for placeholder thread", async () => {
1519
- const WrapperWithAutoGenerate = ({ children, }) => {
1520
- const client = useTamboClient();
1521
- const queryClient = useTamboQueryClient();
1522
- return (React.createElement(TamboClientContext.Provider, { value: {
1523
- client,
1524
- queryClient,
1525
- isUpdatingToken: false,
1526
- } },
1527
- React.createElement(TamboRegistryProvider, { components: mockRegistry },
1528
- React.createElement(TamboContextHelpersProvider, { contextHelpers: {
1529
- currentTimeContextHelper: () => null,
1530
- currentPageContextHelper: () => null,
1531
- } },
1532
- React.createElement(TamboMcpTokenProvider, null,
1533
- React.createElement(TamboThreadProvider, { streaming: false, autoGenerateNameThreshold: 2 }, children))))));
1534
- };
1535
1729
  const { result } = renderHook(() => useTamboThread(), {
1536
- wrapper: WrapperWithAutoGenerate,
1730
+ wrapper: createWrapper({ autoGenerateNameThreshold: 2 }),
1537
1731
  });
1538
1732
  // Stay on placeholder thread
1539
1733
  expect(result.current.thread.id).toBe("placeholder");