@elabs-ai/components-ai 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (527) hide show
  1. package/README.md +3 -3
  2. package/dist/{_audio-player-media-chrome-T3XVXWRZ.js → _audio-player-media-chrome.js} +1 -1
  3. package/dist/_code-block-theme.js +12 -0
  4. package/dist/_code-block-theme.js.map +1 -0
  5. package/dist/{_flow-boundary-SHNWLQG5.js → _flow-boundary.js} +1 -1
  6. package/dist/_flow-boundary.js.map +1 -0
  7. package/dist/_flow-lazy.js +8 -0
  8. package/dist/_flow-lazy.js.map +1 -0
  9. package/dist/_lazy-boundary-conformance.js +2 -0
  10. package/dist/_lazy-boundary-conformance.js.map +1 -0
  11. package/dist/_lazy-cjk.js +13 -0
  12. package/dist/_lazy-cjk.js.map +1 -0
  13. package/dist/_lazy-engine-boundary.js +9 -0
  14. package/dist/_lazy-engine-boundary.js.map +1 -0
  15. package/dist/_lazy-math.js +13 -0
  16. package/dist/_lazy-math.js.map +1 -0
  17. package/dist/_lazy-mermaid.js +12 -0
  18. package/dist/_lazy-mermaid.js.map +1 -0
  19. package/dist/_mermaid-error-panel.js +9 -0
  20. package/dist/_mermaid-error-panel.js.map +1 -0
  21. package/dist/{_persona-rive-JEG44YHX.js → _persona-rive.js} +19 -24
  22. package/dist/_persona-rive.js.map +1 -0
  23. package/dist/_streamdown-i18n.js +21 -0
  24. package/dist/_streamdown-i18n.js.map +1 -0
  25. package/dist/_streamdown-safety.js +10 -0
  26. package/dist/_streamdown-safety.js.map +1 -0
  27. package/dist/_theme-scope-store.js +13 -0
  28. package/dist/_theme-scope-store.js.map +1 -0
  29. package/dist/a2ui/a2ui-surface.js +17 -0
  30. package/dist/a2ui/a2ui-surface.js.map +1 -0
  31. package/dist/a2ui/core/catalog.generated.js +10 -0
  32. package/dist/a2ui/core/catalog.generated.js.map +1 -0
  33. package/dist/a2ui/core/complete-json.js +10 -0
  34. package/dist/a2ui/core/complete-json.js.map +1 -0
  35. package/dist/a2ui/core/index.js +37 -0
  36. package/dist/a2ui/core/index.js.map +1 -0
  37. package/dist/a2ui/core/schema.js +11 -0
  38. package/dist/a2ui/core/schema.js.map +1 -0
  39. package/dist/a2ui/core/spec.js +10 -0
  40. package/dist/a2ui/core/spec.js.map +1 -0
  41. package/dist/a2ui/core/validate.js +16 -0
  42. package/dist/a2ui/core/validate.js.map +1 -0
  43. package/dist/a2ui/index.js +54 -0
  44. package/dist/a2ui/index.js.map +1 -0
  45. package/dist/a2ui/ui-catalog.js +18 -0
  46. package/dist/a2ui/ui-catalog.js.map +1 -0
  47. package/dist/agent-event.js +10 -0
  48. package/dist/agent-event.js.map +1 -0
  49. package/dist/agent-timeline.js +11 -0
  50. package/dist/agent-timeline.js.map +1 -0
  51. package/dist/agent.js +25 -0
  52. package/dist/agent.js.map +1 -0
  53. package/dist/artifact.js +23 -0
  54. package/dist/artifact.js.map +1 -0
  55. package/dist/asset-preview.js +22 -0
  56. package/dist/asset-preview.js.map +1 -0
  57. package/dist/attachments.js +33 -0
  58. package/dist/attachments.js.map +1 -0
  59. package/dist/audio-player.js +30 -0
  60. package/dist/audio-player.js.map +1 -0
  61. package/dist/audio-visualizer.js +9 -0
  62. package/dist/audio-visualizer.js.map +1 -0
  63. package/dist/canvas.js +10 -0
  64. package/dist/canvas.js.map +1 -0
  65. package/dist/chain-of-thought.js +22 -0
  66. package/dist/chain-of-thought.js.map +1 -0
  67. package/dist/chat-greeting.js +8 -0
  68. package/dist/chat-greeting.js.map +1 -0
  69. package/dist/chat-shell.js +9 -0
  70. package/dist/chat-shell.js.map +1 -0
  71. package/dist/checkpoint.js +13 -0
  72. package/dist/checkpoint.js.map +1 -0
  73. package/dist/chunk-2XOIG5TG.js +72 -0
  74. package/dist/chunk-2XOIG5TG.js.map +1 -0
  75. package/dist/chunk-34CQ2VHM.js +172 -0
  76. package/dist/chunk-34CQ2VHM.js.map +1 -0
  77. package/dist/chunk-3TDHPXZZ.js +60 -0
  78. package/dist/chunk-3TDHPXZZ.js.map +1 -0
  79. package/dist/chunk-3XNPMDZY.js +478 -0
  80. package/dist/chunk-3XNPMDZY.js.map +1 -0
  81. package/dist/chunk-3YPT3V7L.js +56 -0
  82. package/dist/chunk-3YPT3V7L.js.map +1 -0
  83. package/dist/chunk-475UEU2Q.js +87 -0
  84. package/dist/chunk-475UEU2Q.js.map +1 -0
  85. package/dist/chunk-4CBTII2I.js +95 -0
  86. package/dist/chunk-4CBTII2I.js.map +1 -0
  87. package/dist/chunk-4XRNIZMG.js +22 -0
  88. package/dist/chunk-4XRNIZMG.js.map +1 -0
  89. package/dist/chunk-5NNWUKK5.js +2 -0
  90. package/dist/chunk-5NNWUKK5.js.map +1 -0
  91. package/dist/chunk-5UOMU27Z.js +164 -0
  92. package/dist/chunk-5UOMU27Z.js.map +1 -0
  93. package/dist/chunk-5VGYZ62I.js +264 -0
  94. package/dist/chunk-5VGYZ62I.js.map +1 -0
  95. package/dist/chunk-6IQPH2OS.js +21 -0
  96. package/dist/chunk-6IQPH2OS.js.map +1 -0
  97. package/dist/chunk-6PB2MXCI.js +482 -0
  98. package/dist/chunk-6PB2MXCI.js.map +1 -0
  99. package/dist/chunk-7FD5CZEU.js +215 -0
  100. package/dist/chunk-7FD5CZEU.js.map +1 -0
  101. package/dist/chunk-7RL6KZDF.js +266 -0
  102. package/dist/chunk-7RL6KZDF.js.map +1 -0
  103. package/dist/chunk-ADOYAGGK.js +482 -0
  104. package/dist/chunk-ADOYAGGK.js.map +1 -0
  105. package/dist/chunk-AXMNJMMH.js +984 -0
  106. package/dist/chunk-AXMNJMMH.js.map +1 -0
  107. package/dist/chunk-BA3AEMOU.js +21 -0
  108. package/dist/chunk-BA3AEMOU.js.map +1 -0
  109. package/dist/chunk-BBHFOYML.js +336 -0
  110. package/dist/chunk-BBHFOYML.js.map +1 -0
  111. package/dist/chunk-BHUVISIY.js +221 -0
  112. package/dist/chunk-BHUVISIY.js.map +1 -0
  113. package/dist/chunk-BM2LWERA.js +100 -0
  114. package/dist/chunk-BM2LWERA.js.map +1 -0
  115. package/dist/chunk-CMOYXNPJ.js +211 -0
  116. package/dist/chunk-CMOYXNPJ.js.map +1 -0
  117. package/dist/chunk-CZP3RHC4.js +74 -0
  118. package/dist/chunk-CZP3RHC4.js.map +1 -0
  119. package/dist/chunk-DDG5JHQT.js +112 -0
  120. package/dist/chunk-DDG5JHQT.js.map +1 -0
  121. package/dist/chunk-DJODXO7I.js +38 -0
  122. package/dist/chunk-DJODXO7I.js.map +1 -0
  123. package/dist/chunk-DWJDJLPU.js +192 -0
  124. package/dist/chunk-DWJDJLPU.js.map +1 -0
  125. package/dist/chunk-DZW6LM22.js +45 -0
  126. package/dist/chunk-DZW6LM22.js.map +1 -0
  127. package/dist/chunk-ELGCZLZF.js +305 -0
  128. package/dist/chunk-ELGCZLZF.js.map +1 -0
  129. package/dist/chunk-EV3U6ZZB.js +209 -0
  130. package/dist/chunk-EV3U6ZZB.js.map +1 -0
  131. package/dist/chunk-EY7LVUKQ.js +87 -0
  132. package/dist/chunk-EY7LVUKQ.js.map +1 -0
  133. package/dist/chunk-F4C74F7M.js +230 -0
  134. package/dist/chunk-F4C74F7M.js.map +1 -0
  135. package/dist/chunk-FERYDFZA.js +108 -0
  136. package/dist/chunk-FERYDFZA.js.map +1 -0
  137. package/dist/chunk-FUIYG2DS.js +166 -0
  138. package/dist/chunk-FUIYG2DS.js.map +1 -0
  139. package/dist/chunk-GKYZT3LA.js +136 -0
  140. package/dist/chunk-GKYZT3LA.js.map +1 -0
  141. package/dist/chunk-H42LRUQ4.js +102 -0
  142. package/dist/chunk-H42LRUQ4.js.map +1 -0
  143. package/dist/chunk-HVXM2G5F.js +222 -0
  144. package/dist/chunk-HVXM2G5F.js.map +1 -0
  145. package/dist/chunk-I4RDAGVY.js +53 -0
  146. package/dist/chunk-I4RDAGVY.js.map +1 -0
  147. package/dist/chunk-IQJG4IDX.js +208 -0
  148. package/dist/chunk-IQJG4IDX.js.map +1 -0
  149. package/dist/chunk-IX2PXG4Y.js +144 -0
  150. package/dist/chunk-IX2PXG4Y.js.map +1 -0
  151. package/dist/chunk-IXICSOOP.js +287 -0
  152. package/dist/chunk-IXICSOOP.js.map +1 -0
  153. package/dist/chunk-JY5HSN4D.js +84 -0
  154. package/dist/chunk-JY5HSN4D.js.map +1 -0
  155. package/dist/chunk-LMNSBOBG.js +19 -0
  156. package/dist/chunk-LMNSBOBG.js.map +1 -0
  157. package/dist/chunk-MBI7D6IL.js +42 -0
  158. package/dist/chunk-MBI7D6IL.js.map +1 -0
  159. package/dist/chunk-MLFMYCPH.js +192 -0
  160. package/dist/chunk-MLFMYCPH.js.map +1 -0
  161. package/dist/chunk-MOK66734.js +33 -0
  162. package/dist/chunk-MOK66734.js.map +1 -0
  163. package/dist/chunk-MXTAWPYM.js +196 -0
  164. package/dist/chunk-MXTAWPYM.js.map +1 -0
  165. package/dist/chunk-NCL2UZKI.js +96 -0
  166. package/dist/chunk-NCL2UZKI.js.map +1 -0
  167. package/dist/chunk-NET4YLRY.js +107 -0
  168. package/dist/chunk-NET4YLRY.js.map +1 -0
  169. package/dist/chunk-NIG67CFM.js +329 -0
  170. package/dist/chunk-NIG67CFM.js.map +1 -0
  171. package/dist/chunk-NMVNNO3L.js +10 -0
  172. package/dist/chunk-NMVNNO3L.js.map +1 -0
  173. package/dist/chunk-NP7VID4K.js +2 -0
  174. package/dist/chunk-NP7VID4K.js.map +1 -0
  175. package/dist/chunk-NPUCMAG2.js +382 -0
  176. package/dist/chunk-NPUCMAG2.js.map +1 -0
  177. package/dist/chunk-NSCLRHES.js +102 -0
  178. package/dist/chunk-NSCLRHES.js.map +1 -0
  179. package/dist/chunk-OEUXHVFW.js +86 -0
  180. package/dist/chunk-OEUXHVFW.js.map +1 -0
  181. package/dist/chunk-ONBHFIMZ.js +137 -0
  182. package/dist/chunk-ONBHFIMZ.js.map +1 -0
  183. package/dist/chunk-OPH2HHL2.js +228 -0
  184. package/dist/chunk-OPH2HHL2.js.map +1 -0
  185. package/dist/chunk-OQERHIQP.js +287 -0
  186. package/dist/chunk-OQERHIQP.js.map +1 -0
  187. package/dist/chunk-P6YJAZFG.js +103 -0
  188. package/dist/chunk-P6YJAZFG.js.map +1 -0
  189. package/dist/chunk-PG7ERDOD.js +65 -0
  190. package/dist/chunk-PG7ERDOD.js.map +1 -0
  191. package/dist/chunk-PIB5JBMA.js +90 -0
  192. package/dist/chunk-PIB5JBMA.js.map +1 -0
  193. package/dist/chunk-PQ5BPIWQ.js +22 -0
  194. package/dist/chunk-PQ5BPIWQ.js.map +1 -0
  195. package/dist/chunk-PQBF6ZIQ.js +55 -0
  196. package/dist/chunk-PQBF6ZIQ.js.map +1 -0
  197. package/dist/chunk-PTWCTSZL.js +173 -0
  198. package/dist/chunk-PTWCTSZL.js.map +1 -0
  199. package/dist/chunk-QJUNPVOS.js +101 -0
  200. package/dist/chunk-QJUNPVOS.js.map +1 -0
  201. package/dist/chunk-QUDZOTRV.js +43 -0
  202. package/dist/chunk-QUDZOTRV.js.map +1 -0
  203. package/dist/chunk-RB7G65JC.js +213 -0
  204. package/dist/chunk-RB7G65JC.js.map +1 -0
  205. package/dist/chunk-RPHAOR7P.js +45 -0
  206. package/dist/chunk-RPHAOR7P.js.map +1 -0
  207. package/dist/chunk-RTCSH2AN.js +331 -0
  208. package/dist/chunk-RTCSH2AN.js.map +1 -0
  209. package/dist/chunk-S3ISPW3A.js +28 -0
  210. package/dist/chunk-S3ISPW3A.js.map +1 -0
  211. package/dist/chunk-SEVN656N.js +36 -0
  212. package/dist/chunk-SEVN656N.js.map +1 -0
  213. package/dist/chunk-SKVG672B.js +64 -0
  214. package/dist/chunk-SKVG672B.js.map +1 -0
  215. package/dist/chunk-SPQIU7GA.js +236 -0
  216. package/dist/chunk-SPQIU7GA.js.map +1 -0
  217. package/dist/chunk-SSGJ6MEM.js +1076 -0
  218. package/dist/chunk-SSGJ6MEM.js.map +1 -0
  219. package/dist/chunk-SSKZ2AED.js +118 -0
  220. package/dist/chunk-SSKZ2AED.js.map +1 -0
  221. package/dist/chunk-T56JFGXI.js +561 -0
  222. package/dist/chunk-T56JFGXI.js.map +1 -0
  223. package/dist/chunk-TCB6O74A.js +137 -0
  224. package/dist/chunk-TCB6O74A.js.map +1 -0
  225. package/dist/chunk-TTEYPO6X.js +57 -0
  226. package/dist/chunk-TTEYPO6X.js.map +1 -0
  227. package/dist/chunk-TWPHAJNM.js +462 -0
  228. package/dist/chunk-TWPHAJNM.js.map +1 -0
  229. package/dist/chunk-TXJJLP4O.js +336 -0
  230. package/dist/chunk-TXJJLP4O.js.map +1 -0
  231. package/dist/chunk-TZ6TB7VM.js +245 -0
  232. package/dist/chunk-TZ6TB7VM.js.map +1 -0
  233. package/dist/chunk-UBLKJJ7K.js +278 -0
  234. package/dist/chunk-UBLKJJ7K.js.map +1 -0
  235. package/dist/chunk-UCJ6IUFS.js +52 -0
  236. package/dist/chunk-UCJ6IUFS.js.map +1 -0
  237. package/dist/chunk-UCKFJ3QF.js +197 -0
  238. package/dist/chunk-UCKFJ3QF.js.map +1 -0
  239. package/dist/chunk-UPXSLILN.js +20 -0
  240. package/dist/chunk-UPXSLILN.js.map +1 -0
  241. package/dist/chunk-V4DORGWJ.js +92 -0
  242. package/dist/chunk-V4DORGWJ.js.map +1 -0
  243. package/dist/chunk-V6E5DYZD.js +80 -0
  244. package/dist/chunk-V6E5DYZD.js.map +1 -0
  245. package/dist/chunk-VLLYKF3P.js +96 -0
  246. package/dist/chunk-VLLYKF3P.js.map +1 -0
  247. package/dist/chunk-VLNBTOH6.js +43 -0
  248. package/dist/chunk-VLNBTOH6.js.map +1 -0
  249. package/dist/chunk-WMDSMSPL.js +303 -0
  250. package/dist/chunk-WMDSMSPL.js.map +1 -0
  251. package/dist/chunk-WU5BLGTF.js +149 -0
  252. package/dist/chunk-WU5BLGTF.js.map +1 -0
  253. package/dist/chunk-WYTZVSGK.js +245 -0
  254. package/dist/chunk-WYTZVSGK.js.map +1 -0
  255. package/dist/chunk-X47QMM6K.js +187 -0
  256. package/dist/chunk-X47QMM6K.js.map +1 -0
  257. package/dist/chunk-XNBKO32Y.js +174 -0
  258. package/dist/chunk-XNBKO32Y.js.map +1 -0
  259. package/dist/chunk-YHUSW7D2.js +40 -0
  260. package/dist/chunk-YHUSW7D2.js.map +1 -0
  261. package/dist/chunk-YSKD7UXX.js +126 -0
  262. package/dist/chunk-YSKD7UXX.js.map +1 -0
  263. package/dist/chunk-YYRDGWOU.js +72 -0
  264. package/dist/chunk-YYRDGWOU.js.map +1 -0
  265. package/dist/chunk-ZIGZMWM7.js +19 -0
  266. package/dist/chunk-ZIGZMWM7.js.map +1 -0
  267. package/dist/chunk-ZKRTUKNP.js +40 -0
  268. package/dist/chunk-ZKRTUKNP.js.map +1 -0
  269. package/dist/code-block.js +38 -0
  270. package/dist/code-block.js.map +1 -0
  271. package/dist/commit.js +51 -0
  272. package/dist/commit.js.map +1 -0
  273. package/dist/composer.js +15 -0
  274. package/dist/composer.js.map +1 -0
  275. package/dist/confirmation.js +53 -0
  276. package/dist/confirmation.js.map +1 -0
  277. package/dist/connection.js +8 -0
  278. package/dist/connection.js.map +1 -0
  279. package/dist/context-panel.js +25 -0
  280. package/dist/context-panel.js.map +1 -0
  281. package/dist/controls.js +10 -0
  282. package/dist/controls.js.map +1 -0
  283. package/dist/conversation.js +19 -0
  284. package/dist/conversation.js.map +1 -0
  285. package/dist/diff-view.js +15 -0
  286. package/dist/diff-view.js.map +1 -0
  287. package/dist/edge.js +10 -0
  288. package/dist/edge.js.map +1 -0
  289. package/dist/environment-variables.js +29 -0
  290. package/dist/environment-variables.js.map +1 -0
  291. package/dist/file-tree.js +23 -0
  292. package/dist/file-tree.js.map +1 -0
  293. package/dist/gallery.js +9 -0
  294. package/dist/gallery.js.map +1 -0
  295. package/dist/grouped-parts.js +21 -0
  296. package/dist/grouped-parts.js.map +1 -0
  297. package/dist/image.js +9 -0
  298. package/dist/image.js.map +1 -0
  299. package/dist/index.d.ts +403 -108
  300. package/dist/index.js +671 -13205
  301. package/dist/index.js.map +1 -1
  302. package/dist/inline-citation.js +39 -0
  303. package/dist/inline-citation.js.map +1 -0
  304. package/dist/jsx-preview.js +17 -0
  305. package/dist/jsx-preview.js.map +1 -0
  306. package/dist/markdown-view.js +17 -0
  307. package/dist/markdown-view.js.map +1 -0
  308. package/dist/message-compare.js +12 -0
  309. package/dist/message-compare.js.map +1 -0
  310. package/dist/message-edit.js +19 -0
  311. package/dist/message-edit.js.map +1 -0
  312. package/dist/message-feedback.js +11 -0
  313. package/dist/message-feedback.js.map +1 -0
  314. package/dist/message-form-spec.js +38 -0
  315. package/dist/message-form-spec.js.map +1 -0
  316. package/dist/message-form.js +26 -0
  317. package/dist/message-form.js.map +1 -0
  318. package/dist/message-table-spec.js +26 -0
  319. package/dist/message-table-spec.js.map +1 -0
  320. package/dist/message-table.js +12 -0
  321. package/dist/message-table.js.map +1 -0
  322. package/dist/message.js +51 -0
  323. package/dist/message.js.map +1 -0
  324. package/dist/mic-selector.js +27 -0
  325. package/dist/mic-selector.js.map +1 -0
  326. package/dist/model-provider-logo.js +13 -0
  327. package/dist/model-provider-logo.js.map +1 -0
  328. package/dist/motion-config.js +9 -0
  329. package/dist/motion-config.js.map +1 -0
  330. package/dist/node.js +22 -0
  331. package/dist/node.js.map +1 -0
  332. package/dist/open-in-chat.js +31 -0
  333. package/dist/open-in-chat.js.map +1 -0
  334. package/dist/package-info.js +25 -0
  335. package/dist/package-info.js.map +1 -0
  336. package/dist/panel.js +10 -0
  337. package/dist/panel.js.map +1 -0
  338. package/dist/part-groups.js +18 -0
  339. package/dist/part-groups.js.map +1 -0
  340. package/dist/permission-mode-select.js +9 -0
  341. package/dist/permission-mode-select.js.map +1 -0
  342. package/dist/persona-sources.js +8 -0
  343. package/dist/persona-sources.js.map +1 -0
  344. package/dist/persona.js +14 -0
  345. package/dist/persona.js.map +1 -0
  346. package/dist/plan.js +32 -0
  347. package/dist/plan.js.map +1 -0
  348. package/dist/prompt-input-effort.js +9 -0
  349. package/dist/prompt-input-effort.js.map +1 -0
  350. package/dist/prompt-input-mode.js +10 -0
  351. package/dist/prompt-input-mode.js.map +1 -0
  352. package/dist/prompt-input-slash.js +12 -0
  353. package/dist/prompt-input-slash.js.map +1 -0
  354. package/dist/prompt-input.js +89 -0
  355. package/dist/prompt-input.js.map +1 -0
  356. package/dist/queue.js +37 -0
  357. package/dist/queue.js.map +1 -0
  358. package/dist/reasoning.js +23 -0
  359. package/dist/reasoning.js.map +1 -0
  360. package/dist/sandbox.js +28 -0
  361. package/dist/sandbox.js.map +1 -0
  362. package/dist/schema-display.js +33 -0
  363. package/dist/schema-display.js.map +1 -0
  364. package/dist/selection-toolbar.js +11 -0
  365. package/dist/selection-toolbar.js.map +1 -0
  366. package/dist/session-header.js +9 -0
  367. package/dist/session-header.js.map +1 -0
  368. package/dist/session-status-bar.js +9 -0
  369. package/dist/session-status-bar.js.map +1 -0
  370. package/dist/shimmer.js +9 -0
  371. package/dist/shimmer.js.map +1 -0
  372. package/dist/snippet.js +17 -0
  373. package/dist/snippet.js.map +1 -0
  374. package/dist/sources.js +17 -0
  375. package/dist/sources.js.map +1 -0
  376. package/dist/speech-input.js +9 -0
  377. package/dist/speech-input.js.map +1 -0
  378. package/dist/stack-trace.js +27 -0
  379. package/dist/stack-trace.js.map +1 -0
  380. package/dist/suggestion.js +16 -0
  381. package/dist/suggestion.js.map +1 -0
  382. package/dist/task.js +18 -0
  383. package/dist/task.js.map +1 -0
  384. package/dist/test-results.js +43 -0
  385. package/dist/test-results.js.map +1 -0
  386. package/dist/token-usage.js +27 -0
  387. package/dist/token-usage.js.map +1 -0
  388. package/dist/tool-result-card.js +9 -0
  389. package/dist/tool-result-card.js.map +1 -0
  390. package/dist/tool.js +27 -0
  391. package/dist/tool.js.map +1 -0
  392. package/dist/toolbar.js +10 -0
  393. package/dist/toolbar.js.map +1 -0
  394. package/dist/transcription.js +11 -0
  395. package/dist/transcription.js.map +1 -0
  396. package/dist/turn-status.js +9 -0
  397. package/dist/turn-status.js.map +1 -0
  398. package/dist/use-audio-level.js +9 -0
  399. package/dist/use-audio-level.js.map +1 -0
  400. package/dist/voice-selector.js +47 -0
  401. package/dist/voice-selector.js.map +1 -0
  402. package/dist/web-preview.js +19 -0
  403. package/dist/web-preview.js.map +1 -0
  404. package/package.json +12 -10
  405. package/schemas/a2ui-surface.v1.schema.json +5425 -0
  406. package/src/__contract__/audio-visualizer.contract.test.tsx +49 -0
  407. package/src/__contract__/chat-shell.contract.test.tsx +49 -0
  408. package/src/__contract__/grouped-parts.contract.test.tsx +49 -0
  409. package/src/__contract__/image.contract.test.tsx +49 -0
  410. package/src/__contract__/markdown-view.contract.test.tsx +49 -0
  411. package/src/__contract__/message-feedback.contract.test.tsx +49 -0
  412. package/src/__contract__/message-form.contract.test.tsx +49 -0
  413. package/src/__contract__/message-table.contract.test.tsx +49 -0
  414. package/src/__contract__/model-provider-logo.contract.test.tsx +49 -0
  415. package/src/__contract__/persona.contract.test.tsx +49 -0
  416. package/src/__contract__/prompt-input-effort.contract.test.tsx +49 -0
  417. package/src/__contract__/prompt-input-mode.contract.test.tsx +49 -0
  418. package/src/_chat-shell-rail.tsx +4 -2
  419. package/src/_flow-boundary.tsx +2 -2
  420. package/src/_lazy-cjk.test.ts +43 -0
  421. package/src/_lazy-cjk.ts +73 -0
  422. package/src/_lazy-math.test.ts +63 -0
  423. package/src/_lazy-math.ts +90 -0
  424. package/src/_persona-rive.tsx +27 -41
  425. package/src/_streamdown-i18n.ts +73 -21
  426. package/src/_streamdown-safety.ts +2 -2
  427. package/src/_theme-scope-store.test.ts +83 -0
  428. package/src/_theme-scope-store.ts +103 -0
  429. package/src/a2ui/a2ui-surface.stories.tsx +662 -0
  430. package/src/a2ui/a2ui-surface.test.tsx +201 -0
  431. package/src/a2ui/a2ui-surface.tsx +354 -0
  432. package/src/a2ui/catalog.source.json +772 -0
  433. package/src/a2ui/core/a2ui-core.test.ts +178 -0
  434. package/src/a2ui/core/catalog.generated.ts +1006 -0
  435. package/src/a2ui/core/complete-json.ts +81 -0
  436. package/src/a2ui/core/index.ts +10 -0
  437. package/src/a2ui/core/schema.ts +111 -0
  438. package/src/a2ui/core/spec.ts +148 -0
  439. package/src/a2ui/core/validate.ts +266 -0
  440. package/src/a2ui/index.ts +7 -0
  441. package/src/a2ui/ui-catalog.tsx +225 -0
  442. package/src/a2ui/wave1-chart-spec.test.ts +87 -0
  443. package/src/a2ui/wave2-chart-spec.test.ts +108 -0
  444. package/src/agent-event.tsx +1 -1
  445. package/src/agent.tsx +18 -13
  446. package/src/agentic-workspace.stories.tsx +6 -45
  447. package/src/artifact.tsx +2 -2
  448. package/src/asset-preview.test.tsx +40 -0
  449. package/src/asset-preview.tsx +83 -12
  450. package/src/attachments.tsx +7 -4
  451. package/src/chat-shell.test.tsx +40 -10
  452. package/src/chat-shell.tsx +90 -44
  453. package/src/chat.stories.tsx +47 -42
  454. package/src/code-block.test.tsx +100 -1
  455. package/src/code-block.tsx +166 -103
  456. package/src/commit.tsx +30 -41
  457. package/src/composer.stories.tsx +4 -5
  458. package/src/composer.test.tsx +45 -0
  459. package/src/composer.tsx +104 -21
  460. package/src/confirmation.tsx +1 -1
  461. package/src/context-panel.stories.tsx +1 -1
  462. package/src/context-panel.tsx +2 -2
  463. package/src/conversation.stories.tsx +24 -0
  464. package/src/conversation.test.tsx +69 -0
  465. package/src/conversation.tsx +47 -8
  466. package/src/diff-view.stories.tsx +79 -0
  467. package/src/diff-view.test.tsx +52 -2
  468. package/src/diff-view.tsx +92 -36
  469. package/src/environment-variables.tsx +20 -37
  470. package/src/file-tree.test.tsx +21 -0
  471. package/src/file-tree.tsx +33 -16
  472. package/src/gallery.stories.tsx +42 -0
  473. package/src/gallery.tsx +2 -2
  474. package/src/grouped-parts.stories.tsx +23 -0
  475. package/src/grouped-parts.tsx +1 -1
  476. package/src/index.ts +1 -0
  477. package/src/inline-citation.tsx +5 -5
  478. package/src/jsx-preview.tsx +151 -43
  479. package/src/markdown-view.test.tsx +7 -3
  480. package/src/markdown-view.tsx +8 -1
  481. package/src/message-compare.stories.tsx +40 -0
  482. package/src/message-compare.tsx +2 -1
  483. package/src/message-form.stories.tsx +36 -3
  484. package/src/message-form.test.tsx +8 -2
  485. package/src/message-form.tsx +16 -8
  486. package/src/message-table.stories.tsx +26 -2
  487. package/src/message-table.test.tsx +7 -0
  488. package/src/message-table.tsx +9 -5
  489. package/src/message.test.tsx +73 -3
  490. package/src/message.tsx +36 -12
  491. package/src/model-provider-logo.test.tsx +57 -3
  492. package/src/model-provider-logo.tsx +45 -11
  493. package/src/open-in-chat.tsx +50 -29
  494. package/src/package-info.tsx +23 -20
  495. package/src/prompt-input-slash.stories.tsx +1 -1
  496. package/src/prompt-input.test.tsx +67 -1
  497. package/src/prompt-input.tsx +42 -4
  498. package/src/queue.tsx +4 -4
  499. package/src/reasoning.stories.tsx +27 -0
  500. package/src/reasoning.tsx +17 -6
  501. package/src/sandbox.tsx +3 -3
  502. package/src/schema-display.test.tsx +56 -0
  503. package/src/schema-display.tsx +68 -37
  504. package/src/session-header.stories.tsx +35 -1
  505. package/src/session-header.tsx +1 -1
  506. package/src/snippet.test.tsx +6 -2
  507. package/src/snippet.tsx +14 -34
  508. package/src/sources.stories.tsx +30 -0
  509. package/src/sources.tsx +4 -1
  510. package/src/speech-input.test.tsx +109 -0
  511. package/src/speech-input.tsx +31 -3
  512. package/src/stack-trace.tsx +20 -36
  513. package/src/streamdown-i18n.test.tsx +23 -0
  514. package/src/templates-ai-assistant.stories.tsx +151 -87
  515. package/src/test-results.tsx +47 -27
  516. package/src/token-usage.tsx +6 -6
  517. package/src/tool.stories.tsx +39 -0
  518. package/src/tool.test.tsx +65 -0
  519. package/src/tool.tsx +61 -25
  520. package/src/toolbar.tsx +0 -10
  521. package/src/transcription.tsx +1 -1
  522. package/src/voice-selector.tsx +5 -5
  523. package/src/web-preview.test.tsx +51 -1
  524. package/src/web-preview.tsx +39 -8
  525. package/dist/_flow-boundary-SHNWLQG5.js.map +0 -1
  526. package/dist/_persona-rive-JEG44YHX.js.map +0 -1
  527. /package/dist/{_audio-player-media-chrome-T3XVXWRZ.js.map → _audio-player-media-chrome.js.map} +0 -0
@@ -0,0 +1,43 @@
1
+ import { renderHook, waitFor } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import { needsCjkPlugin, preloadCjk, useLazyCjkPlugin } from "./_lazy-cjk";
5
+
6
+ const fakeCjkPlugin = {
7
+ name: "cjk",
8
+ remarkPlugins: [],
9
+ remarkPluginsAfter: [],
10
+ remarkPluginsBefore: [],
11
+ type: "cjk",
12
+ };
13
+
14
+ vi.mock("@streamdown/cjk", () => ({ cjk: fakeCjkPlugin }));
15
+
16
+ describe("needsCjkPlugin", () => {
17
+ it("returns false for plain ASCII text", () => {
18
+ expect(needsCjkPlugin("plain text")).toBe(false);
19
+ });
20
+
21
+ it("matches CJK Unified Ideographs, Hiragana, Katakana and Hangul", () => {
22
+ expect(needsCjkPlugin("你好")).toBe(true);
23
+ expect(needsCjkPlugin("こんにちは")).toBe(true);
24
+ expect(needsCjkPlugin("コンニチハ")).toBe(true);
25
+ expect(needsCjkPlugin("안녕하세요")).toBe(true);
26
+ });
27
+ });
28
+
29
+ describe("useLazyCjkPlugin", () => {
30
+ it("returns undefined when the text has no CJK codepoints", () => {
31
+ const { result } = renderHook(() => useLazyCjkPlugin("plain text"));
32
+ expect(result.current).toBeUndefined();
33
+ });
34
+
35
+ it("loads and returns the plugin once the text needs CJK handling", async () => {
36
+ const { result } = renderHook(() => useLazyCjkPlugin("你好,世界"));
37
+ await waitFor(() => expect(result.current).toBe(fakeCjkPlugin));
38
+ });
39
+
40
+ it("preloadCjk resolves without throwing", () => {
41
+ expect(() => preloadCjk()).not.toThrow();
42
+ });
43
+ });
@@ -0,0 +1,73 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Lazy-loaded replacement for `@streamdown/cjk`'s eager `cjk` export.
5
+ *
6
+ * Same rationale and mechanism as `_lazy-math.ts` — `remark-cjk-friendly` +
7
+ * `remark-cjk-friendly-gfm-strikethrough` load only once `needsCjkPlugin(text)`
8
+ * sees a CJK codepoint, instead of shipping in every consumer's entry chunk.
9
+ * See `_lazy-math.ts` for why a remark `Pluggable` cannot be deferred the way
10
+ * `DiagramPlugin` defers Mermaid's render, and for the one-paint literal-text
11
+ * cost this trades for it.
12
+ */
13
+ import type { CjkPlugin } from "@streamdown/cjk";
14
+ import { useEffect, useState } from "react";
15
+
16
+ /**
17
+ * Hiragana, Katakana, CJK Unified Ideographs (+ Extension A), Hangul Syllables,
18
+ * CJK punctuation and halfwidth Katakana — the ranges `remark-cjk-friendly`
19
+ * exists to handle (emphasis/strikethrough boundary rules around CJK text).
20
+ */
21
+ // The range's first codepoint is the ideographic full-width space, not
22
+ // accidental whitespace — a deliberate boundary character, not noise.
23
+ // eslint-disable-next-line no-irregular-whitespace -- see comment above
24
+ const CJK_RANGE_RE = /[ -〿぀-ヿ㐀-䶿一-鿿가-힣・-゚]/u;
25
+
26
+ /** Does `text` contain a CJK codepoint? */
27
+ export function needsCjkPlugin(text: string): boolean {
28
+ return CJK_RANGE_RE.test(text);
29
+ }
30
+
31
+ /** Module-level singleton: the plugin loads at most once. */
32
+ let cachedPlugin: CjkPlugin | undefined;
33
+ let loadPromise: Promise<CjkPlugin> | undefined;
34
+
35
+ function loadCjkPlugin(): Promise<CjkPlugin> {
36
+ loadPromise ??= import("@streamdown/cjk").then((mod) => {
37
+ cachedPlugin = mod.cjk;
38
+ return cachedPlugin;
39
+ });
40
+ return loadPromise;
41
+ }
42
+
43
+ /**
44
+ * Start fetching the CJK plugin ahead of time (e.g. once a conversation's
45
+ * locale is known to be CJK). Optional; rendering CJK text loads it either way.
46
+ */
47
+ export const preloadCjk = (): void => {
48
+ void loadCjkPlugin();
49
+ };
50
+
51
+ /**
52
+ * Returns the loaded `CjkPlugin` once `text` needs one, else `undefined`
53
+ * (Streamdown treats an absent `plugins.cjk` slot as "no CJK handling" — plain
54
+ * remark-gfm boundary rules apply until the plugin arrives).
55
+ */
56
+ export function useLazyCjkPlugin(text: string): CjkPlugin | undefined {
57
+ const needed = needsCjkPlugin(text);
58
+ const [plugin, setPlugin] = useState<CjkPlugin | undefined>(cachedPlugin);
59
+
60
+ useEffect(() => {
61
+ if (!needed || cachedPlugin) return;
62
+ let cancelled = false;
63
+ loadCjkPlugin().then((loaded) => {
64
+ if (!cancelled) setPlugin(loaded);
65
+ });
66
+ return () => {
67
+ cancelled = true;
68
+ };
69
+ }, [needed]);
70
+
71
+ if (!needed) return undefined;
72
+ return plugin ?? cachedPlugin;
73
+ }
@@ -0,0 +1,63 @@
1
+ import { renderHook, waitFor } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+
4
+ import { needsMathPlugin, preloadMath, useLazyMathPlugin } from "./_lazy-math";
5
+
6
+ /**
7
+ * The KaTeX-carrying `@streamdown/math` module is mocked so this file tests
8
+ * the lazy wrapper's *behaviour*. That the real dependency is genuinely
9
+ * absent from the entry chunk is a property of the BUILD, not of jsdom — see
10
+ * `pnpm check --rule eager-heavy-deps`, which
11
+ * asserts no `packages/ai/src` module statically imports `@streamdown/math`
12
+ * or `katex`.
13
+ */
14
+ const fakeMathPlugin = {
15
+ name: "katex",
16
+ rehypePlugin: vi.fn(),
17
+ remarkPlugin: vi.fn(),
18
+ type: "math",
19
+ };
20
+
21
+ vi.mock("@streamdown/math", () => ({ math: fakeMathPlugin }));
22
+
23
+ describe("needsMathPlugin", () => {
24
+ it("matches $$block$$ and $inline$ delimiters", () => {
25
+ expect(needsMathPlugin("plain text")).toBe(false);
26
+ expect(needsMathPlugin("$$x^2$$")).toBe(true);
27
+ expect(needsMathPlugin("the answer is $x + 1$ today")).toBe(true);
28
+ });
29
+
30
+ it("matches \\( \\) and \\[ \\] delimiters", () => {
31
+ expect(needsMathPlugin("\\(x^2\\)")).toBe(true);
32
+ expect(needsMathPlugin("\\[x^2\\]")).toBe(true);
33
+ });
34
+
35
+ it("does not false-positive on a bare dollar amount", () => {
36
+ // A single, unpaired `$` (no closing delimiter on the same line) is not math.
37
+ expect(needsMathPlugin("that costs $5")).toBe(false);
38
+ });
39
+ });
40
+
41
+ describe("useLazyMathPlugin", () => {
42
+ it("returns undefined when the text has no math delimiters", () => {
43
+ const { result } = renderHook(() => useLazyMathPlugin("plain text"));
44
+ expect(result.current).toBeUndefined();
45
+ });
46
+
47
+ it("loads and returns the plugin once the text needs math", async () => {
48
+ const { result } = renderHook(() => useLazyMathPlugin("$$x^2$$"));
49
+ await waitFor(() => expect(result.current).toBe(fakeMathPlugin));
50
+ });
51
+
52
+ it("preloadMath resolves without throwing", () => {
53
+ expect(() => preloadMath()).not.toThrow();
54
+ });
55
+
56
+ it("serves a second, simultaneous consumer off the same cached load", async () => {
57
+ const first = renderHook(() => useLazyMathPlugin("$$x^2$$"));
58
+ const second = renderHook(() => useLazyMathPlugin("$y^2$"));
59
+
60
+ await waitFor(() => expect(first.result.current).toBe(fakeMathPlugin));
61
+ await waitFor(() => expect(second.result.current).toBe(fakeMathPlugin));
62
+ });
63
+ });
@@ -0,0 +1,90 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Lazy-loaded replacement for `@streamdown/math`'s eager `math` export.
5
+ *
6
+ * Why this exists
7
+ * ----------------
8
+ * `@streamdown/math` bundles KaTeX (~600 KB unminified) plus `remark-math` and
9
+ * `rehype-katex`. `useStreamdownPlugins()` (`_streamdown-i18n.ts`) used to
10
+ * import it statically, so every consumer of `MessageResponse`/`MarkdownView`/
11
+ * `ReasoningContent` paid for KaTeX in their entry chunk whether or not a
12
+ * single conversation ever contains `$…$`.
13
+ *
14
+ * How it stays correct
15
+ * ---------------------
16
+ * Unlike Mermaid (`_lazy-mermaid.ts`), a remark/rehype `Pluggable` cannot be
17
+ * wrapped in a lazy accessor: `remark-math`'s micromark extension has to be
18
+ * registered on the unified processor BEFORE it parses, so there is no way to
19
+ * hand Streamdown a present-but-deferred math plugin the way `DiagramPlugin`
20
+ * defers its render. Instead:
21
+ *
22
+ * 1. `needsMathPlugin(text)` cheaply sniffs the RAW markdown source for `$…$`
23
+ * / `\(…\)` / `\[…\]` delimiters — no parsing, just a regex.
24
+ * 2. `useLazyMathPlugin(text)` returns `undefined` until that sniff is true,
25
+ * at which point it kicks off `import("@streamdown/math")` and returns the
26
+ * loaded plugin once it resolves, triggering a re-render.
27
+ *
28
+ * The one user-visible cost: a message whose FIRST rendered chunk already
29
+ * contains math delimiters shows literal `$x^2$` for one paint before the
30
+ * import resolves (typically well under a frame from a warm cache, since the
31
+ * chunk is tiny next to KaTeX's own parse/layout work). Every subsequent
32
+ * message reuses the module-level cache and never re-pays it.
33
+ */
34
+ import type { MathPlugin } from "@streamdown/math";
35
+ import { useEffect, useState } from "react";
36
+
37
+ /** Cheap, over-inclusive sniff — a false positive just means an unneeded fetch. */
38
+ const MATH_DELIMITER_RE = /\$\$[\s\S]*?\$\$|\$[^\s$][^$\n]*\$|\\\(|\\\)|\\\[|\\\]/;
39
+
40
+ /** Does `text` look like it contains LaTeX math delimiters? */
41
+ export function needsMathPlugin(text: string): boolean {
42
+ return MATH_DELIMITER_RE.test(text);
43
+ }
44
+
45
+ /** Module-level singleton: the plugin (and its KaTeX bytes) load at most once. */
46
+ let cachedPlugin: MathPlugin | undefined;
47
+ let loadPromise: Promise<MathPlugin> | undefined;
48
+
49
+ function loadMathPlugin(): Promise<MathPlugin> {
50
+ loadPromise ??= import("@streamdown/math").then((mod) => {
51
+ cachedPlugin = mod.math;
52
+ return cachedPlugin;
53
+ });
54
+ return loadPromise;
55
+ }
56
+
57
+ /**
58
+ * Start fetching the math plugin ahead of time (e.g. once a conversation is
59
+ * known to contain math, or on route entry). Optional; rendering math loads
60
+ * it either way.
61
+ */
62
+ export const preloadMath = (): void => {
63
+ void loadMathPlugin();
64
+ };
65
+
66
+ /**
67
+ * Returns the loaded `MathPlugin` once `text` needs one, else `undefined`
68
+ * (Streamdown treats an absent `plugins.math` slot as "no math support" — the
69
+ * delimiters render as literal text until the plugin arrives).
70
+ */
71
+ export function useLazyMathPlugin(text: string): MathPlugin | undefined {
72
+ const needed = needsMathPlugin(text);
73
+ const [plugin, setPlugin] = useState<MathPlugin | undefined>(cachedPlugin);
74
+
75
+ useEffect(() => {
76
+ if (!needed || cachedPlugin) return;
77
+ let cancelled = false;
78
+ loadMathPlugin().then((loaded) => {
79
+ if (!cancelled) setPlugin(loaded);
80
+ });
81
+ return () => {
82
+ cancelled = true;
83
+ };
84
+ }, [needed]);
85
+
86
+ if (!needed) return undefined;
87
+ // `cachedPlugin` covers the case where another instance finished loading
88
+ // between this component's mount and this render (state update pending).
89
+ return plugin ?? cachedPlugin;
90
+ }
@@ -40,8 +40,7 @@ import type { RiveParameters } from "@rive-app/react-webgl2";
40
40
  // below to `undefined`, and the guard turns that into the render-phase throw
41
41
  // `LazyEngineBoundary` (see `persona.tsx`) already catches.
42
42
  import * as RiveModule from "@rive-app/react-webgl2";
43
- import type { ReactNode } from "react";
44
- import { memo, useEffect, useMemo, useRef, useState } from "react";
43
+ import { useEffect, useMemo, useRef, useState } from "react";
45
44
 
46
45
  import type { AssertAssignable } from "./_lazy-boundary-conformance";
47
46
  import type { PersonaRiveEventCallback } from "./persona";
@@ -132,39 +131,6 @@ const useTheme = (enabled: boolean) => {
132
131
  return theme;
133
132
  };
134
133
 
135
- interface PersonaWithModelProps {
136
- rive: ReturnType<typeof useRive>["rive"];
137
- source: PersonaSource;
138
- children: ReactNode;
139
- }
140
-
141
- const PersonaWithModel = memo(({ rive, source, children }: PersonaWithModelProps) => {
142
- const theme = useTheme(source.dynamicColor);
143
- const viewModel = useViewModel(rive, { useDefault: true });
144
- const viewModelInstance = useViewModelInstance(viewModel, {
145
- rive,
146
- useDefault: true,
147
- });
148
- const viewModelInstanceColor = useViewModelInstanceColor("color", viewModelInstance);
149
-
150
- useEffect(() => {
151
- if (!(viewModelInstanceColor && source.dynamicColor)) {
152
- return;
153
- }
154
-
155
- const [r, g, b] = theme === "dark" ? [255, 255, 255] : [0, 0, 0];
156
- viewModelInstanceColor.setRgb(r, g, b);
157
- }, [viewModelInstanceColor, theme, source.dynamicColor]);
158
-
159
- return children;
160
- });
161
-
162
- PersonaWithModel.displayName = "PersonaWithModel";
163
-
164
- const PersonaWithoutModel = memo(({ children }: { children: ReactNode }) => children);
165
-
166
- PersonaWithoutModel.displayName = "PersonaWithoutModel";
167
-
168
134
  export interface PersonaRiveProps {
169
135
  className?: string;
170
136
  onLoad?: PersonaRiveEventCallback;
@@ -268,13 +234,33 @@ const PersonaRive = ({
268
234
  }
269
235
  }, [state, listeningInput, thinkingInput, speakingInput, asleepInput]);
270
236
 
271
- const Wrapper = source.hasModel ? PersonaWithModel : PersonaWithoutModel;
237
+ // The view-model hooks run here rather than in a child that takes `rive` as
238
+ // a prop. React 19.2's development build logs each re-render's changed props
239
+ // to the browser's performance track by walking every object prop a few
240
+ // levels deep. That walk never finished on a loaded Rive instance: handing
241
+ // one to a component as a prop froze the tab until it ran out of memory, in
242
+ // development builds only. Keep the instance out of props. Each hook accepts
243
+ // `null`, which is how a source without a view model opts out.
244
+ const modelRive = source.hasModel ? rive : null;
245
+ const dynamicColor = source.hasModel && source.dynamicColor;
246
+ const theme = useTheme(dynamicColor);
247
+ const viewModel = useViewModel(modelRive, { useDefault: true });
248
+ const viewModelInstance = useViewModelInstance(viewModel, {
249
+ rive: modelRive,
250
+ useDefault: true,
251
+ });
252
+ const viewModelInstanceColor = useViewModelInstanceColor("color", viewModelInstance);
272
253
 
273
- return (
274
- <Wrapper rive={rive} source={source}>
275
- <RiveComponent className={cn("size-16 shrink-0", className)} />
276
- </Wrapper>
277
- );
254
+ useEffect(() => {
255
+ if (!(viewModelInstanceColor && dynamicColor)) {
256
+ return;
257
+ }
258
+
259
+ const [r, g, b] = theme === "dark" ? [255, 255, 255] : [0, 0, 0];
260
+ viewModelInstanceColor.setRgb(r, g, b);
261
+ }, [viewModelInstanceColor, theme, dynamicColor]);
262
+
263
+ return <RiveComponent className={cn("size-16 shrink-0", className)} />;
278
264
  };
279
265
 
280
266
  export default PersonaRive;
@@ -29,14 +29,15 @@ import {
29
29
  useStreamdownTranslations,
30
30
  type StreamdownTranslationKey,
31
31
  } from "@elabs-ai/components-ui";
32
- import { cjk } from "@streamdown/cjk";
33
32
  import { createCodePlugin } from "@streamdown/code";
34
- import { math } from "@streamdown/math";
35
- import { useEffect, useMemo, useState } from "react";
33
+ import { useMemo } from "react";
36
34
  import type { StreamdownTranslations } from "streamdown";
37
35
  import { buildCodeBlockTheme } from "./_code-block-theme";
36
+ import { useLazyCjkPlugin } from "./_lazy-cjk";
37
+ import { useLazyMathPlugin } from "./_lazy-math";
38
38
  import { lazyMermaid } from "./_lazy-mermaid";
39
39
  import { MermaidErrorPanel } from "./_mermaid-error-panel";
40
+ import { useThemeScopeRevision } from "./_theme-scope-store";
40
41
 
41
42
  /**
42
43
  * Reactive replacement for `@streamdown/code`'s pre-configured `code` export
@@ -55,24 +56,25 @@ import { MermaidErrorPanel } from "./_mermaid-error-panel";
55
56
  * theme the active `data-theme` resolves to — so whichever slot the `.dark`
56
57
  * selector picks, it's the CORRECT theme for whatever is actually active.
57
58
  *
58
- * Re-derives on every `data-theme` mutation (a `MutationObserver` on
59
- * `<html>`, mirroring `code-block.tsx`'s own) and returns a NEW plugin object
59
+ * Re-derives on every `data-theme` mutation and returns a NEW plugin object
60
60
  * each time, because Streamdown only re-reads `plugins.code.getThemes()` when
61
61
  * the `plugins.code` object's REFERENCE changes (see streamdown's internal
62
62
  * `shikiTheme` memo) — a mutated-in-place plugin would never be picked up.
63
+ *
64
+ * The mutation watch itself comes from the SHARED, ref-counted store in
65
+ * `./_theme-scope-store` (perf review §3.3) rather than a `MutationObserver`
66
+ * instantiated per hook call — every simultaneously-mounted
67
+ * `MessageResponse`/`MarkdownView` (and every `CodeBlock`) in a message-dense
68
+ * conversation now shares exactly one observer per distinct scope element,
69
+ * instead of one each. This hook has no element of its own to scope to (it
70
+ * runs before Streamdown renders anything), so it resolves to the document's
71
+ * theme scope (`getThemeScope(null)` → closest `[data-theme]` on `<html>`,
72
+ * same as its prior always-`<html>` behaviour) — a genuinely region-scoped
73
+ * derivation would need a ref to the rendered surface, which no current call
74
+ * site (`message.tsx`, `markdown-view.tsx`, `reasoning.tsx`) has available.
63
75
  */
64
76
  function useReactiveCodePlugin() {
65
- const [revision, setRevision] = useState(0);
66
-
67
- useEffect(() => {
68
- if (typeof document === "undefined") return;
69
- const observer = new MutationObserver(() => setRevision((r) => r + 1));
70
- observer.observe(document.documentElement, {
71
- attributes: true,
72
- attributeFilter: ["data-theme"],
73
- });
74
- return () => observer.disconnect();
75
- }, []);
77
+ const revision = useThemeScopeRevision(null);
76
78
 
77
79
  return useMemo(() => {
78
80
  const theme = buildCodeBlockTheme();
@@ -85,15 +87,65 @@ function useReactiveCodePlugin() {
85
87
  * The plugin set every `@elabs-ai/components-ai` markdown surface renders with.
86
88
  *
87
89
  * `mermaid` is the LAZY plugin (`./_lazy-mermaid`): the engine + d3 + DOMPurify
88
- * load on first diagram render, not in the entry chunk of every consumer. `code`
89
- * is the reactive, brand-token-derived plugin above (#315) — never the
90
+ * load on first diagram render, not in the entry chunk of every consumer.
91
+ * `math`/`cjk` are ALSO lazy (`./_lazy-math`, `./_lazy-cjk`, #perf-5): KaTeX
92
+ * (~600 KB) and the CJK remark plugins load only once `text` — the raw
93
+ * markdown source about to render — actually needs them, sniffed by a cheap
94
+ * regex rather than shipped unconditionally in every consumer's entry chunk.
95
+ * Both resolve to `undefined` (the slot is simply absent) until their content
96
+ * cue fires and the dynamic import resolves; see the two modules' docs for the
97
+ * one-paint tradeoff this makes and why it can't be avoided the way Mermaid's
98
+ * render-time laziness is.
99
+ *
100
+ * `code` is the reactive, brand-token-derived plugin above (#315) — never the
90
101
  * `@streamdown/code` package's static `github-light`/`github-dark` default.
91
102
  * Memoized so Streamdown sees a referentially stable `plugins` prop except when
92
- * the active theme actually changes.
103
+ * the active theme (or a lazy slot) actually changes.
104
+ *
105
+ * @param text The raw markdown source about to render — used only to decide
106
+ * whether the math/CJK plugins are needed. Pass `""` to never load them
107
+ * speculatively.
93
108
  */
94
- export function useStreamdownPlugins() {
109
+ export function useStreamdownPlugins(text = "") {
95
110
  const code = useReactiveCodePlugin();
96
- return useMemo(() => ({ cjk, code, math, mermaid: lazyMermaid }), [code]);
111
+ const math = useLazyMathPlugin(text);
112
+ const cjk = useLazyCjkPlugin(text);
113
+ return useMemo(() => ({ cjk, code, math, mermaid: lazyMermaid }), [cjk, code, math]);
114
+ }
115
+
116
+ /**
117
+ * A React `key` for the `<Streamdown>` element that renders with
118
+ * {@link useStreamdownPlugins}'s result — pass it as `key={getStreamdownPluginsKey(plugins)}`
119
+ * on the `<Streamdown>` JSX, never inside the `plugins` object itself.
120
+ *
121
+ * Why this exists (#perf-5 follow-up): Streamdown v2.5.0's `rehypePlugins`/
122
+ * `remarkPlugins` are `useMemo`'d with `plugins.math`/`plugins.cjk` in their
123
+ * dependency arrays, and its `Block`/`Streamdown` components' custom `memo`
124
+ * comparators do check plugin reference equality — on paper, a fresh
125
+ * `plugins.math` object arriving after the lazy `import()` resolves should be
126
+ * enough to make Streamdown rebuild its processing pipeline with the math
127
+ * plugin included. Empirically (verified directly against the real
128
+ * `streamdown` package, not a mock) it does not: once a block has rendered
129
+ * once without `plugins.math`/`plugins.cjk`, Streamdown's internal processor
130
+ * cache keeps serving the plugin-less result even after the reference
131
+ * changes, and the literal `$$…$$`/CJK-remark-less markdown source stays
132
+ * un-rendered indefinitely.
133
+ *
134
+ * Forcing React to unmount and remount the `<Streamdown>` element — by
135
+ * changing its `key` — sidesteps that internal cache entirely (a fresh
136
+ * instance has no stale cache to serve) and reliably picks up the lazily
137
+ * loaded plugin. The cost is a one-time full remount of that Streamdown
138
+ * instance the first time its lazy math/CJK slot resolves — at most once per
139
+ * mounted instance, since after that the key is stable again. Filed for
140
+ * upstream investigation; if a future `streamdown` release fixes the
141
+ * underlying cache, this key can be dropped without changing any call site's
142
+ * behavior (an unchanging key is a no-op).
143
+ */
144
+ export function getStreamdownPluginsKey(plugins: {
145
+ readonly cjk?: unknown;
146
+ readonly math?: unknown;
147
+ }): string {
148
+ return `${plugins.math ? "math" : "no-math"}:${plugins.cjk ? "cjk" : "no-cjk"}`;
97
149
  }
98
150
 
99
151
  /**
@@ -36,7 +36,7 @@
36
36
  * schema instead of replacing the pipeline outright.
37
37
  *
38
38
  * THIS LIST HAS A PARITY COUNTERPART, AND EDITING ONE SIDE FAILS CI (#75).
39
- * `scripts/check-sanitizer-passthrough.mjs` declares the same key set as
39
+ * `scripts/check/rules/sanitizer-passthrough.mjs` declares the same key set as
40
40
  * `SAFE_RENDERERS[streamdown].dangerousProps` and reads THIS array literal back
41
41
  * out of this file, asserting set-equality in BOTH directions — so removing a
42
42
  * key here (the runtime stops stripping it) and removing a key there (the gate
@@ -153,7 +153,7 @@ export function warnOnTrustedPluginSlots(overrides: object | undefined, internal
153
153
 
154
154
  if (isPluginObject(consumer.mermaid) && consumer.mermaid !== defaults.mermaid) {
155
155
  // The message describes React's `dangerouslySetInnerHTML` in prose rather than
156
- // naming the identifier: `pnpm csp-sinks:check` preserves string literals on
156
+ // naming the identifier: the `trusted-types-sinks` check preserves string literals on
157
157
  // purpose (a sink hidden in a string is still a sink), so spelling the prop out
158
158
  // inside this warning makes the gate read THIS file as a new HTML-assigning
159
159
  // module. Comments like this one are stripped by that gate, so the name is safe
@@ -0,0 +1,83 @@
1
+ import { renderHook, waitFor } from "@testing-library/react";
2
+ import { afterEach, describe, expect, it } from "vitest";
3
+ import {
4
+ _debugActiveThemeScopeCount,
5
+ getThemeScope,
6
+ useThemeScopeRevision,
7
+ } from "./_theme-scope-store";
8
+
9
+ afterEach(() => {
10
+ document.documentElement.removeAttribute("data-theme");
11
+ document.body.innerHTML = "";
12
+ });
13
+
14
+ describe("getThemeScope", () => {
15
+ it("resolves the nearest data-theme ancestor, falling back to <html>", () => {
16
+ expect(getThemeScope(document.body)).toBe(document.documentElement);
17
+
18
+ const scoped = document.createElement("div");
19
+ scoped.setAttribute("data-theme", "dark");
20
+ document.body.append(scoped);
21
+ expect(getThemeScope(scoped)).toBe(scoped);
22
+ });
23
+ });
24
+
25
+ describe("useThemeScopeRevision (perf review §3.3)", () => {
26
+ it("bumps the revision when the scoped element's data-theme mutates", async () => {
27
+ const { result } = renderHook(() => useThemeScopeRevision(document.documentElement));
28
+ const before = result.current;
29
+
30
+ document.documentElement.setAttribute("data-theme", "dark");
31
+
32
+ await waitFor(() => expect(result.current).toBe(before + 1));
33
+ });
34
+
35
+ it("shares ONE MutationObserver across every subscriber of the same scope", async () => {
36
+ const before = _debugActiveThemeScopeCount();
37
+ const hookA = renderHook(() => useThemeScopeRevision(document.documentElement));
38
+ const hookB = renderHook(() => useThemeScopeRevision(document.documentElement));
39
+ const hookC = renderHook(() => useThemeScopeRevision(document.documentElement));
40
+
41
+ // Three subscribers to the SAME scope element register as exactly one
42
+ // tracked scope, not three — this is the fix: previously each caller
43
+ // (CodeBlockContent, useReactiveCodePlugin) instantiated its own
44
+ // MutationObserver on the same ancestor.
45
+ expect(_debugActiveThemeScopeCount()).toBe(before + 1);
46
+
47
+ document.documentElement.setAttribute("data-theme", "dark");
48
+
49
+ // Every subscriber still gets notified through the single observer.
50
+ await waitFor(() => expect(hookA.result.current).toBe(1));
51
+ expect(hookB.result.current).toBe(1);
52
+ expect(hookC.result.current).toBe(1);
53
+
54
+ hookA.unmount();
55
+ hookB.unmount();
56
+ expect(_debugActiveThemeScopeCount()).toBe(before + 1); // still one listener left (hookC)
57
+ hookC.unmount();
58
+ expect(_debugActiveThemeScopeCount()).toBe(before); // the observer is disconnected once unused
59
+ });
60
+
61
+ it("scopes to the closest [data-theme] ancestor, not always <html>", async () => {
62
+ const outer = document.createElement("div");
63
+ outer.setAttribute("data-theme", "light");
64
+ const inner = document.createElement("div");
65
+ inner.setAttribute("data-theme", "dark");
66
+ outer.append(inner);
67
+ document.body.append(outer);
68
+
69
+ const { result } = renderHook(() => useThemeScopeRevision(inner));
70
+ const before = result.current;
71
+
72
+ // A mutation on the OUTER (non-closest) ancestor must not affect a
73
+ // consumer scoped to the inner region. There is nothing to `waitFor`
74
+ // here (a NEGATIVE assertion), so flush the microtask queue directly.
75
+ outer.setAttribute("data-theme", "light-updated");
76
+ await Promise.resolve();
77
+ expect(result.current).toBe(before);
78
+
79
+ // A mutation on the actually-closest scope element does.
80
+ inner.setAttribute("data-theme", "light");
81
+ await waitFor(() => expect(result.current).toBe(before + 1));
82
+ });
83
+ });