@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
@@ -531,3 +531,48 @@ describe("Composer — shortcut hints change with a busy state (#107)", () => {
531
531
  expect(shortcutRow).not.toContainElement(stopButton);
532
532
  });
533
533
  });
534
+
535
+ describe("Composer — mentions slot", () => {
536
+ const ROSTER = [
537
+ { id: "u-ada", label: "Ada Lovelace" },
538
+ { id: "u-grace", label: "Grace Hopper" },
539
+ ];
540
+
541
+ it("opens the roster on @, inserts on Enter without submitting, then submits and resets", async () => {
542
+ const user = userEvent.setup();
543
+ const onSubmit = vi.fn();
544
+ const onValueChange = vi.fn();
545
+ render(
546
+ <Composer
547
+ onSubmit={onSubmit}
548
+ showAttach={false}
549
+ showVoice={false}
550
+ mentions={{ options: ROSTER, onValueChange }}
551
+ />,
552
+ );
553
+ const field = screen.getByRole("textbox") as HTMLTextAreaElement;
554
+ // The composer's own field — what PromptInput's submit reads.
555
+ expect(field).toHaveAttribute("name", "message");
556
+
557
+ await user.click(field);
558
+ await user.keyboard("hi @ada");
559
+ expect(field).toHaveAttribute("data-state", "open");
560
+ await user.keyboard("{Enter}");
561
+ expect(field.value).toBe("hi @Ada Lovelace ");
562
+ expect(onSubmit).not.toHaveBeenCalled();
563
+ expect(onValueChange).toHaveBeenLastCalledWith(
564
+ expect.objectContaining({ mentions: [expect.objectContaining({ id: "u-ada" })] }),
565
+ );
566
+
567
+ await user.keyboard("{Enter}");
568
+ expect(onSubmit).toHaveBeenCalledTimes(1);
569
+ expect(onSubmit.mock.calls[0]![0].text.trim()).toBe("hi @Ada Lovelace");
570
+ expect(field.value).toBe("");
571
+ expect(onValueChange).toHaveBeenLastCalledWith({ text: "", mentions: [] });
572
+ });
573
+
574
+ it("refuses slashCommands and mentions together at the type level", () => {
575
+ // @ts-expect-error — the field takes a slash palette OR a mention roster.
576
+ render(<Composer slashCommands={[]} mentions={{ options: ROSTER }} />);
577
+ });
578
+ });
package/src/composer.tsx CHANGED
@@ -2,7 +2,20 @@
2
2
 
3
3
  import { useCallback, useRef, useState, type ComponentProps, type ReactNode } from "react";
4
4
  import { ArrowUp, Mic, Paperclip, Sparkles } from "lucide-react";
5
- import { cn, Kbd, useLocale } from "@elabs-ai/components-ui";
5
+ import { useControllableState } from "@radix-ui/react-use-controllable-state";
6
+ import {
7
+ cn,
8
+ Kbd,
9
+ MentionInput,
10
+ MentionInputContent,
11
+ MentionInputEmpty,
12
+ MentionInputItem,
13
+ MentionInputList,
14
+ MentionInputTextarea,
15
+ useLocale,
16
+ type MentionOption,
17
+ type MentionValue,
18
+ } from "@elabs-ai/components-ui";
6
19
 
7
20
  import {
8
21
  PromptInput,
@@ -62,7 +75,29 @@ export interface ComposerShortcut {
62
75
  label: string;
63
76
  }
64
77
 
65
- export interface ComposerProps {
78
+ /**
79
+ * An `@`-mention roster for the composer field (`MentionInput` from
80
+ * `@elabs-ai/components-ui`). The field keeps its own submit contract; the
81
+ * mentions ride alongside the text in a `MentionValue`.
82
+ *
83
+ * Read the mentions on submit from `value` (controlled) or from
84
+ * `onValueChange`, and resolve them with `serializeMentions(value)`
85
+ * (`{ text, mentionedIds }`). `Composer` resets the value to empty after a
86
+ * submit it accepts — through `onValueChange`, so a controlled caller's state
87
+ * clears too.
88
+ */
89
+ export interface ComposerMentions {
90
+ /** Who (or what) can be mentioned. */
91
+ options: MentionOption[];
92
+ /** Controlled value. Omit for an uncontrolled roster. */
93
+ value?: MentionValue;
94
+ /** Fires on every edit, pick and post-submit reset. */
95
+ onValueChange?: (value: MentionValue) => void;
96
+ }
97
+
98
+ const EMPTY_MENTION_VALUE: MentionValue = { text: "", mentions: [] };
99
+
100
+ export interface ComposerBaseProps {
66
101
  /** Submit handler — receives the assembled message (text + attachments). */
67
102
  onSubmit?: PromptInputProps["onSubmit"];
68
103
  /** Textarea placeholder. Default `"Ask me anything…"`. */
@@ -96,20 +131,6 @@ export interface ComposerProps {
96
131
  * and is entirely yours; `aria-label` names the scale.
97
132
  */
98
133
  effort?: ComposerEffortProps;
99
- /**
100
- * Slash-command palette. When set (and non-empty) the textarea becomes a
101
- * `PromptInputSlashTextarea` wrapped in a `PromptInputSlash`, so typing `/`
102
- * at the start of a line opens a filtered command list — nothing else about
103
- * the composer changes.
104
- *
105
- * Note this makes the textarea REACT-CONTROLLED (the palette needs to read
106
- * and splice the text), which is why `Composer` clears its own copy of the
107
- * text on submit: `PromptInput`'s `form.reset()` reaches the DOM node, not
108
- * React state.
109
- */
110
- slashCommands?: PromptInputSlashProps["commands"];
111
- /** Fires with the chosen slash command (alongside the text splice). */
112
- onSlashCommand?: PromptInputSlashProps["onSelect"];
113
134
  /**
114
135
  * Override the DEFAULT tool buttons (today: the attach button) — e.g. to add
115
136
  * a web-search or connect-data control. Render `PromptInputButton`s /
@@ -185,6 +206,38 @@ export interface ComposerProps {
185
206
  className?: string;
186
207
  }
187
208
 
209
+ /**
210
+ * The field enhancements that each take over the textarea — a slash palette OR
211
+ * a mention roster, never both. The union makes the impossible combination a
212
+ * type error instead of a silent precedence rule.
213
+ */
214
+ export type ComposerFieldProps =
215
+ | {
216
+ /**
217
+ * Slash-command palette. When set (and non-empty) the textarea becomes a
218
+ * `PromptInputSlashTextarea` wrapped in a `PromptInputSlash`, so typing `/`
219
+ * at the start of a line opens a filtered command list — nothing else about
220
+ * the composer changes.
221
+ *
222
+ * Note this makes the textarea REACT-CONTROLLED (the palette needs to read
223
+ * and splice the text), which is why `Composer` clears its own copy of the
224
+ * text on submit: `PromptInput`'s `form.reset()` reaches the DOM node, not
225
+ * React state.
226
+ */
227
+ slashCommands?: PromptInputSlashProps["commands"];
228
+ /** Fires with the chosen slash command (alongside the text splice). */
229
+ onSlashCommand?: PromptInputSlashProps["onSelect"];
230
+ mentions?: never;
231
+ }
232
+ | {
233
+ /** `@`-mention roster on the field — see `ComposerMentions`. */
234
+ mentions?: ComposerMentions;
235
+ slashCommands?: never;
236
+ onSlashCommand?: never;
237
+ };
238
+
239
+ export type ComposerProps = ComposerBaseProps & ComposerFieldProps;
240
+
188
241
  /**
189
242
  * Composer — the standard brand-ui AI chat input.
190
243
  *
@@ -202,9 +255,10 @@ export interface ComposerProps {
202
255
  *
203
256
  * **Composer is the chat input. Every control the `PromptInput` family ships
204
257
  * is reachable from a `Composer` prop; drop to `PromptInput` only for a
205
- * bespoke shell.** `modelPicker`, `mode`, `effort` and `slashCommands` are the
206
- * four slots that make that true — they render `ModelPicker`,
207
- * `PromptInputMode`, `PromptInputEffort` and `PromptInputSlash` respectively,
258
+ * bespoke shell.** `modelPicker`, `mode`, `effort`, `slashCommands` and
259
+ * `mentions` are the slots that make that true — they render `ModelPicker`,
260
+ * `PromptInputMode`, `PromptInputEffort`, `PromptInputSlash` and
261
+ * `MentionInput` respectively,
208
262
  * in the footer order `attach · modelPicker · mode · effort │ voice · send`
209
263
  * (the same left-to-right arrangement `TerminalComposer` uses, so the chat and
210
264
  * console skins agree).
@@ -218,6 +272,7 @@ export function Composer({
218
272
  effort,
219
273
  slashCommands,
220
274
  onSlashCommand,
275
+ mentions,
221
276
  tools,
222
277
  sendStatus,
223
278
  onStop,
@@ -239,6 +294,12 @@ export function Composer({
239
294
  const textareaRef = useRef<HTMLTextAreaElement>(null);
240
295
  const [text, setText] = useState("");
241
296
  const hasSlash = slashCommands !== undefined && slashCommands.length > 0;
297
+ // Always called (hook order); only read when `mentions` is set.
298
+ const [mentionValue, setMentionValue] = useControllableState<MentionValue>({
299
+ prop: mentions?.value,
300
+ defaultProp: EMPTY_MENTION_VALUE,
301
+ onChange: mentions?.onValueChange,
302
+ });
242
303
 
243
304
  // #107: "Composer shortcut hints change with a busy state" — derived from
244
305
  // the existing canonical `sendStatus`, never a second boolean prop. The
@@ -258,9 +319,12 @@ export function Composer({
258
319
  // keep rendering the sent text straight back. Clearing here is a no-op in
259
320
  // the uncontrolled (non-slash) arrangement.
260
321
  setText("");
261
- return onSubmit?.(message, event);
322
+ const result = onSubmit?.(message, event);
323
+ // `form.reset()` cannot clear a value MentionInput controls either.
324
+ if (mentions) setMentionValue(EMPTY_MENTION_VALUE);
325
+ return result;
262
326
  },
263
- [onSubmit],
327
+ [onSubmit, mentions, setMentionValue],
264
328
  );
265
329
 
266
330
  const resolvedPlaceholder = placeholder ?? t("ai.composer.placeholder");
@@ -305,6 +369,25 @@ export function Composer({
305
369
  placeholder={resolvedPlaceholder}
306
370
  />
307
371
  </PromptInputSlash>
372
+ ) : mentions ? (
373
+ <MentionInput
374
+ options={mentions.options}
375
+ value={mentionValue}
376
+ onValueChange={setMentionValue}
377
+ >
378
+ {/* `asChild` lends the mention behaviour to the composer's own
379
+ textarea, so `name="message"` — what PromptInput's submit
380
+ reads — survives. */}
381
+ <MentionInputTextarea asChild>
382
+ <PromptInputTextarea placeholder={resolvedPlaceholder} />
383
+ </MentionInputTextarea>
384
+ <MentionInputContent>
385
+ <MentionInputList>
386
+ {(option) => <MentionInputItem key={option.id} option={option} />}
387
+ </MentionInputList>
388
+ <MentionInputEmpty />
389
+ </MentionInputContent>
390
+ </MentionInput>
308
391
  ) : (
309
392
  <PromptInputTextarea placeholder={resolvedPlaceholder} />
310
393
  )}
@@ -251,7 +251,7 @@ export const ConfirmationActions = ({ className, ...props }: ConfirmationActions
251
251
  export type ConfirmationActionProps = ComponentProps<typeof Button>;
252
252
 
253
253
  export const ConfirmationAction = ({ className, ...props }: ConfirmationActionProps) => (
254
- <Button className={cn("h-8 px-3 text-sm", className)} type="button" {...props} />
254
+ <Button className={cn("h-8 px-3 text-body", className)} type="button" {...props} />
255
255
  );
256
256
 
257
257
  export type ConfirmationApproveProps = ConfirmationActionProps;
@@ -102,7 +102,7 @@ function WorkspaceDemo(providerProps: Omit<ContextPanelProviderProps, "children"
102
102
  <ContextPanelProvider {...providerProps}>
103
103
  <div className="flex h-[520px] w-full overflow-hidden rounded-xl border bg-background">
104
104
  <div className="flex min-w-0 flex-1 flex-col">
105
- <header className="flex h-12 shrink-0 items-center justify-between border-b px-4">
105
+ <header className="flex h-header shrink-0 items-center justify-between border-b px-4">
106
106
  <span className="text-subtitle">Agentic workspace</span>
107
107
  <ContextPanelTrigger />
108
108
  </header>
@@ -382,7 +382,7 @@ export const ContextPanel = forwardRef<HTMLDivElement, ContextPanelProps>(functi
382
382
  >
383
383
  <SheetHeader className="sr-only">
384
384
  <SheetTitle>{t("ai.contextPanel.title")}</SheetTitle>
385
- <SheetDescription>Displays the chat context panel.</SheetDescription>
385
+ <SheetDescription>{t("ai.contextPanel.description")}</SheetDescription>
386
386
  </SheetHeader>
387
387
  <div className="flex h-full w-full flex-col">{children}</div>
388
388
  </SheetContent>
@@ -469,7 +469,7 @@ export const ContextPanelHeader = forwardRef<HTMLDivElement, ContextPanelHeaderP
469
469
  <div
470
470
  ref={ref}
471
471
  data-slot="context-panel-header"
472
- className={cn("flex h-12 shrink-0 items-center gap-2 border-b px-3", className)}
472
+ className={cn("flex h-header shrink-0 items-center gap-2 border-b px-3", className)}
473
473
  {...props}
474
474
  >
475
475
  {isDetail ? (
@@ -5,6 +5,7 @@ import { Message, MessageContent } from "./message";
5
5
  const meta = {
6
6
  title: "AI/Conversation",
7
7
  component: Conversation,
8
+ tags: ["autodocs"],
8
9
  parameters: {
9
10
  layout: "padded",
10
11
  docs: {
@@ -34,6 +35,29 @@ export const Default: Story = {
34
35
  ),
35
36
  };
36
37
 
38
+ /**
39
+ * `isStreaming` suppresses the live-region chatter while tokens are still
40
+ * arriving (`aria-live="off"` + `aria-busy`) — see the prop's doc on
41
+ * `ConversationProps` for why a `"polite"` region would otherwise announce
42
+ * every incoming token.
43
+ */
44
+ export const Streaming: Story = {
45
+ render: () => (
46
+ <div className="flex h-72 flex-col rounded-lg border">
47
+ <Conversation isStreaming>
48
+ <ConversationContent>
49
+ <Message from="user">
50
+ <MessageContent>Summarize the deploy log.</MessageContent>
51
+ </Message>
52
+ <Message from="assistant">
53
+ <MessageContent>Deploying to production</MessageContent>
54
+ </Message>
55
+ </ConversationContent>
56
+ </Conversation>
57
+ </div>
58
+ ),
59
+ };
60
+
37
61
  /**
38
62
  * **Layout lock (#72).** With zero messages, `ConversationEmptyState` must render
39
63
  * vertically centred in the conversation canvas — not pinned to the top with a
@@ -16,6 +16,7 @@ import type { UIMessage } from "ai";
16
16
  import {
17
17
  Conversation,
18
18
  ConversationContent,
19
+ ConversationDownload,
19
20
  ConversationEmptyState,
20
21
  messagesToMarkdown,
21
22
  } from "./conversation";
@@ -44,6 +45,22 @@ describe("Conversation — transcript region", () => {
44
45
  expect(screen.getByRole("log")).toBeInTheDocument();
45
46
  });
46
47
 
48
+ it("lets the content grow past the viewport so its composer inset stays scrollable", () => {
49
+ render(
50
+ <Conversation>
51
+ <ConversationContent>
52
+ <p>turn</p>
53
+ </ConversationContent>
54
+ </Conversation>,
55
+ );
56
+ const content = screen.getByText("turn").parentElement!;
57
+ // `min-h-0` clamps the content to the viewport height: the turns then
58
+ // overflow it and the bottom padding (the floating-composer inset) is
59
+ // left behind, so the last turn scrolls under the composer.
60
+ expect(content).not.toHaveClass("min-h-0");
61
+ expect(content).toHaveClass("shrink-0", "flex-1");
62
+ });
63
+
47
64
  it("renders its children inside the log, in order", () => {
48
65
  render(
49
66
  <Conversation>
@@ -69,6 +86,46 @@ describe("Conversation — transcript region", () => {
69
86
  expect(screen.getByRole("log")).toHaveClass("extra");
70
87
  expect(screen.getByRole("log")).toHaveClass("flex-1");
71
88
  });
89
+
90
+ it('defaults to aria-live="polite" and no aria-busy (not streaming)', () => {
91
+ render(
92
+ <Conversation>
93
+ <ConversationContent>x</ConversationContent>
94
+ </Conversation>,
95
+ );
96
+ const log = screen.getByRole("log");
97
+ expect(log).toHaveAttribute("aria-live", "polite");
98
+ expect(log).not.toHaveAttribute("aria-busy");
99
+ });
100
+
101
+ it('suppresses aria-live and sets aria-busy while isStreaming (perf review §2 — role="log" must not announce every token)', () => {
102
+ render(
103
+ <Conversation isStreaming>
104
+ <ConversationContent>x</ConversationContent>
105
+ </Conversation>,
106
+ );
107
+ const log = screen.getByRole("log");
108
+ expect(log).toHaveAttribute("aria-live", "off");
109
+ expect(log).toHaveAttribute("aria-busy", "true");
110
+ });
111
+
112
+ it("returns to aria-live=polite once streaming settles, so the final content is announced", () => {
113
+ const { rerender } = render(
114
+ <Conversation isStreaming>
115
+ <ConversationContent>partial</ConversationContent>
116
+ </Conversation>,
117
+ );
118
+ expect(screen.getByRole("log")).toHaveAttribute("aria-live", "off");
119
+
120
+ rerender(
121
+ <Conversation isStreaming={false}>
122
+ <ConversationContent>final</ConversationContent>
123
+ </Conversation>,
124
+ );
125
+ const log = screen.getByRole("log");
126
+ expect(log).toHaveAttribute("aria-live", "polite");
127
+ expect(log).not.toHaveAttribute("aria-busy");
128
+ });
72
129
  });
73
130
 
74
131
  describe("ConversationEmptyState", () => {
@@ -108,6 +165,18 @@ describe("ConversationEmptyState", () => {
108
165
  });
109
166
  });
110
167
 
168
+ describe("ConversationDownload — accessible name (accessibility.md)", () => {
169
+ it("names the icon-only download control", () => {
170
+ render(<ConversationDownload messages={messages} />);
171
+ expect(screen.getByRole("button", { name: "Download conversation" })).toBeInTheDocument();
172
+ });
173
+
174
+ it("lets a caller override the accessible name", () => {
175
+ render(<ConversationDownload aria-label="Export chat" messages={messages} />);
176
+ expect(screen.getByRole("button", { name: "Export chat" })).toBeInTheDocument();
177
+ });
178
+ });
179
+
111
180
  describe("messagesToMarkdown — the download serializer", () => {
112
181
  it("labels each message by role and joins every text part", () => {
113
182
  expect(messagesToMarkdown(messages)).toBe(
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { Button, downloadBlob } from "@elabs-ai/components-ui";
3
+ import { Button, downloadBlob, useLocale } from "@elabs-ai/components-ui";
4
4
  import { cn } from "@elabs-ai/components-ui/lib/cn";
5
5
  import type { UIMessage } from "ai";
6
6
  import { ArrowDownIcon, DownloadIcon } from "lucide-react";
@@ -8,14 +8,27 @@ import type { ComponentProps } from "react";
8
8
  import { useCallback } from "react";
9
9
  import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
10
10
 
11
- export type ConversationProps = ComponentProps<typeof StickToBottom>;
11
+ export type ConversationProps = ComponentProps<typeof StickToBottom> & {
12
+ /**
13
+ * The assistant response is still arriving (loading-states.md
14
+ * `isStreaming`). `role="log"` otherwise announces every incoming token to
15
+ * assistive tech as it streams in — deafening. While `true`, `aria-live`
16
+ * is suppressed (`"off"`) and `aria-busy` is set; once streaming ends,
17
+ * `aria-live` returns to `"polite"` so the settled content is what
18
+ * actually gets announced.
19
+ * @default false
20
+ */
21
+ isStreaming?: boolean;
22
+ };
12
23
 
13
- export const Conversation = ({ className, ...props }: ConversationProps) => (
24
+ export const Conversation = ({ className, isStreaming = false, ...props }: ConversationProps) => (
14
25
  <StickToBottom
15
26
  className={cn("relative flex-1 overflow-y-hidden", className)}
16
27
  initial="smooth"
17
28
  resize="smooth"
18
29
  role="log"
30
+ aria-live={isStreaming ? "off" : "polite"}
31
+ aria-busy={isStreaming || undefined}
19
32
  {...props}
20
33
  />
21
34
  );
@@ -35,7 +48,19 @@ export const ConversationContent = ({
35
48
  // can't resolve against a block-level parent whose own height is
36
49
  // `min-height`-only (the #72 empty-state-pinned-to-the-top bug).
37
50
  scrollClassName={cn("flex flex-col", scrollClassName)}
38
- className={cn("flex flex-1 min-h-0 flex-col gap-8 p-4", className)}
51
+ // Inside a `ChatShell variant="bare"` the composer floats over the
52
+ // transcript: the shell publishes `--chat-composer-inset` (its measured
53
+ // height) so the last turn scrolls clear of it, and `--chat-column` so
54
+ // turns share the composer's reading column. Outside such a shell both
55
+ // variables are unset: the inset falls back to 0 and `max-width` to none.
56
+ // `shrink-0`, never `min-h-0`: the content div must grow with its turns.
57
+ // Clamped to the viewport height, the turns overflow it and its bottom
58
+ // padding stays behind at the viewport's edge, so the last turn ends up
59
+ // under the floating composer instead of above it.
60
+ className={cn(
61
+ "mx-auto flex w-full max-w-(--chat-column) flex-1 shrink-0 flex-col gap-8 p-4 pb-[calc(var(--chat-composer-inset,0px)+--spacing(4))]",
62
+ className,
63
+ )}
39
64
  {...props}
40
65
  />
41
66
  );
@@ -68,8 +93,8 @@ export const ConversationEmptyState = ({
68
93
  <>
69
94
  {icon && <div className="text-muted-foreground">{icon}</div>}
70
95
  <div className="space-y-1">
71
- <h3 className="font-medium text-sm">{title}</h3>
72
- {description && <p className="text-muted-foreground text-sm">{description}</p>}
96
+ <h3 className="font-medium text-body">{title}</h3>
97
+ {description && <p className="text-muted-foreground text-body">{description}</p>}
73
98
  </div>
74
99
  {actions ? <div className="mt-1 flex items-center gap-2">{actions}</div> : null}
75
100
  </>
@@ -84,6 +109,7 @@ export const ConversationScrollButton = ({
84
109
  ...props
85
110
  }: ConversationScrollButtonProps) => {
86
111
  const { isAtBottom, scrollToBottom } = useStickToBottomContext();
112
+ const { t } = useLocale();
87
113
 
88
114
  const handleScrollToBottom = useCallback(() => {
89
115
  scrollToBottom();
@@ -93,9 +119,18 @@ export const ConversationScrollButton = ({
93
119
  !isAtBottom && (
94
120
  <Button
95
121
  className={cn(
96
- "absolute bottom-4 left-[50%] translate-x-[-50%] rounded-full dark:bg-background dark:hover:bg-muted",
122
+ // Was `dark:bg-background dark:hover:bg-muted` — a hardcoded-dark
123
+ // branch invisible to any other registered theme (styling-and-tokens.md).
124
+ // `bg-background` already matches the `outline` variant's own default;
125
+ // the quieter `hover:bg-muted` (in place of `outline`'s default
126
+ // `hover:bg-accent`) now applies in every theme, not only the two
127
+ // shipped ones.
128
+ // `--chat-composer-inset`: lift the button clear of a floating
129
+ // `ChatShell variant="bare"` composer (0 anywhere else).
130
+ "absolute bottom-[calc(var(--chat-composer-inset,0px)+--spacing(4))] left-[50%] z-30 translate-x-[-50%] rounded-full bg-background hover:bg-muted",
97
131
  className,
98
132
  )}
133
+ aria-label={t("ai.turnStatus.scrollToBottom")}
99
134
  onClick={handleScrollToBottom}
100
135
  size="icon"
101
136
  type="button"
@@ -138,6 +173,7 @@ export const ConversationDownload = ({
138
173
  children,
139
174
  ...props
140
175
  }: ConversationDownloadProps) => {
176
+ const { t } = useLocale();
141
177
  const handleDownload = useCallback(() => {
142
178
  const markdown = messagesToMarkdown(messages, formatMessage);
143
179
  downloadBlob(new Blob([markdown], { type: "text/markdown" }), filename);
@@ -146,9 +182,12 @@ export const ConversationDownload = ({
146
182
  return (
147
183
  <Button
148
184
  className={cn(
149
- "absolute top-4 end-4 rounded-full dark:bg-background dark:hover:bg-muted",
185
+ // See `ConversationScrollButton` — was a hardcoded-dark-only branch;
186
+ // `bg-background hover:bg-muted` now applies in every theme.
187
+ "absolute top-4 end-4 rounded-full bg-background hover:bg-muted",
150
188
  className,
151
189
  )}
190
+ aria-label={t("ai.conversation.download")}
152
191
  onClick={handleDownload}
153
192
  size="icon"
154
193
  type="button"
@@ -194,3 +194,82 @@ export const ChangeReviewComposition: Story = {
194
194
  </div>
195
195
  ),
196
196
  };
197
+
198
+ /**
199
+ * Focus indicator on expandable line (#313): Tab to the "show more" button
200
+ * and assert the compound indicator's OUTLINE layer (`outlineStyle: "solid"`,
201
+ * `--ring-contour`) that `focus-ring` adds — not just `boxShadow !== "none"`,
202
+ * which the legacy `ring-2` pattern also produces.
203
+ *
204
+ * The prior version passed a `hiddenLineCount` prop that `DiffViewProps`
205
+ * (`diff-view.tsx:77`) does not define — React spread it onto the DOM as a
206
+ * meaningless `hiddenlinecount` attribute and warned about it, and the "show
207
+ * more" row NEVER rendered (a real diff never mentions "50" anywhere).
208
+ * `getByRole("button", …)` then always threw before the focus check ran.
209
+ * `CollapsedRow` (`diff-view.tsx:289`) only appears when `collapseDiffRows`
210
+ * (`packages/ui/src/lib/diff-rows.ts`) actually collapses a context run
211
+ * longer than `contextLines` — so this story now supplies a real 10-line
212
+ * context run with `contextLines={2}`, which collapses to
213
+ * `top(1) + collapsed(8) + bottom(1)`, i.e. a real "Show 8 more lines"
214
+ * button. The button's accessible name is
215
+ * `t("ai.diffView.showMore", { count })`, not the literal "Show more" —
216
+ * matched by a count-tolerant regex so it doesn't drift with the wording.
217
+ */
218
+ export const FocusIndicatorExpand: Story = {
219
+ name: "Keyboard focus — expand",
220
+ render: () => (
221
+ <div className="max-w-2xl">
222
+ <DiffView
223
+ lines={Array.from({ length: 10 }, (_, i) => ({
224
+ type: "context" as const,
225
+ oldNumber: i + 1,
226
+ newNumber: i + 1,
227
+ text: `line ${i + 1}`,
228
+ }))}
229
+ contextLines={2}
230
+ />
231
+ </div>
232
+ ),
233
+ play: async ({ canvas, userEvent }) => {
234
+ const button = canvas.getByRole("button", { name: /show \d+ more lines?/i });
235
+ await userEvent.tab();
236
+ await expect(button).toHaveFocus();
237
+ const focused = getComputedStyle(button);
238
+ await expect(focused.boxShadow).not.toBe("none");
239
+ await expect(focused.outlineStyle).toBe("solid");
240
+ await expect(parseFloat(focused.outlineWidth)).toBeGreaterThan(0);
241
+ },
242
+ };
243
+
244
+ /**
245
+ * Focus indicator on scrollable pager (#313): Tab to the diff body and
246
+ * assert the compound indicator's OUTLINE layer on the actual scroll
247
+ * container. The prior version located it via `getByText(/^export/)`, which
248
+ * throws "multiple elements" under `variant="split"` (the same line renders
249
+ * once per pane) — and even resolved, `scrollDiv.parentElement` was never
250
+ * the scroll container itself (the text sits several nodes deep inside it).
251
+ * Query the tabbable scroll container directly — it carries `tabIndex={0}`
252
+ * and `focus-ring-inset` (`diff-view.tsx`'s `DiffViewPager`) — instead of
253
+ * inferring it from rendered content.
254
+ */
255
+ export const FocusIndicatorPager: Story = {
256
+ name: "Keyboard focus — pager",
257
+ render: () => (
258
+ <div className="max-w-2xl">
259
+ <DiffView lines={SAMPLE_LINES} pager variant="split" />
260
+ </div>
261
+ ),
262
+ play: async ({ canvas, userEvent }) => {
263
+ const pager = canvas.getByRole("region");
264
+ const scrollDiv = pager.querySelector<HTMLElement>('[tabindex="0"]');
265
+ if (scrollDiv == null) {
266
+ throw new Error("expected a tabbable scroll container inside the pager region");
267
+ }
268
+ await userEvent.tab();
269
+ await expect(scrollDiv).toHaveFocus();
270
+ const focused = getComputedStyle(scrollDiv);
271
+ await expect(focused.boxShadow).not.toBe("none");
272
+ await expect(focused.outlineStyle).toBe("solid");
273
+ await expect(parseFloat(focused.outlineWidth)).toBeGreaterThan(0);
274
+ },
275
+ };
@@ -1,5 +1,5 @@
1
- import { describe, expect, it } from "vitest";
2
- import { render, screen, within } from "@testing-library/react";
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { render, screen, waitFor, within } from "@testing-library/react";
3
3
  import userEvent from "@testing-library/user-event";
4
4
  import { type DiffLine } from "@elabs-ai/components-ui";
5
5
  import { DiffView } from "./diff-view";
@@ -162,6 +162,56 @@ describe("DiffView — pager (absorbs CodexDiff)", () => {
162
162
  });
163
163
  });
164
164
 
165
+ describe("DiffView — old/new sides tokenize SEPARATELY (perf review §2 — a deleted comment opener must not swallow the added side)", () => {
166
+ afterEach(() => {
167
+ document.documentElement.removeAttribute("data-theme");
168
+ for (const token of [
169
+ "--code-foreground",
170
+ "--code-comment",
171
+ "--code-keyword",
172
+ "--code-number",
173
+ ]) {
174
+ document.documentElement.style.removeProperty(token);
175
+ }
176
+ });
177
+
178
+ it("does not let an unterminated block comment on the del side swallow the add side's tokens", async () => {
179
+ // Distinct resolved colors per scope (comment vs. keyword vs. number) so
180
+ // a merged/degenerate single-token line is distinguishable from a real,
181
+ // multi-token breakdown — see the module doc comment on `useDiffTokens`.
182
+ document.documentElement.setAttribute("data-theme", "light");
183
+ document.documentElement.style.setProperty("--code-foreground", "oklch(0.1 0 0)");
184
+ document.documentElement.style.setProperty("--code-comment", "oklch(0.5 0 0)");
185
+ document.documentElement.style.setProperty("--code-keyword", "oklch(0.3 0.2 260)");
186
+ document.documentElement.style.setProperty("--code-number", "oklch(0.4 0.2 140)");
187
+
188
+ const lines: DiffLine[] = [
189
+ { type: "del", oldNumber: 1, text: "/* unterminated" },
190
+ { type: "add", newNumber: 1, text: "const value = 1;" },
191
+ ];
192
+
193
+ const { container } = renderDiffView(<DiffView language="tsx" lines={lines} />);
194
+
195
+ // Combining old+new into ONE document (the bug) makes Shiki carry the
196
+ // del line's still-open `/*` across the line boundary, so the ENTIRE add
197
+ // line becomes one comment-colored token. Tokenized as its own document,
198
+ // `const value = 1;` is real code and splits into several distinctly
199
+ // colored spans.
200
+ await waitFor(() => {
201
+ const addLineText = container.querySelector(
202
+ '[data-diff-type="add"] [data-slot="diff-view-line-text"]',
203
+ );
204
+ const spans = addLineText?.querySelectorAll("span[style]") ?? [];
205
+ expect(spans.length).toBeGreaterThan(1);
206
+ });
207
+
208
+ const addLineText = container.querySelector(
209
+ '[data-diff-type="add"] [data-slot="diff-view-line-text"]',
210
+ );
211
+ expect(addLineText?.textContent).toContain("const value = 1;");
212
+ });
213
+ });
214
+
165
215
  describe("DiffView — contextLines collapsing", () => {
166
216
  const longContext: DiffLine[] = Array.from({ length: 10 }, (_, i) => ({
167
217
  type: "context" as const,