@keboola/design 13.1.3 → 14.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 (347) hide show
  1. package/AGENTS.md +68 -6
  2. package/CHANGELOG.md +196 -0
  3. package/COMPONENT-API.md +60 -50
  4. package/dist/{AIKit-BVCWP9Ks.js → AIKit-BRSZ3nY7.js} +2227 -313
  5. package/dist/AIKit-BRSZ3nY7.js.map +1 -0
  6. package/dist/{Alert-UI-CIs-1.js → Alert-CZ0dDsuk.js} +7 -7
  7. package/dist/{Alert-UI-CIs-1.js.map → Alert-CZ0dDsuk.js.map} +1 -1
  8. package/dist/{AlertModal-mbSnuuka.js → AlertModal-BDle9ZVQ.js} +6 -6
  9. package/dist/AlertModal-BDle9ZVQ.js.map +1 -0
  10. package/dist/{AppShell-WSQabK8r.js → AppShell-B03fj1j7.js} +2 -2
  11. package/dist/{AppShell-WSQabK8r.js.map → AppShell-B03fj1j7.js.map} +1 -1
  12. package/dist/{AutoSaveTextarea-C5iIr2Ts.js → AutoSaveTextarea-jiqNeeE4.js} +3 -3
  13. package/dist/{AutoSaveTextarea-C5iIr2Ts.js.map → AutoSaveTextarea-jiqNeeE4.js.map} +1 -1
  14. package/dist/{Badge-Dz0cj6rj.js → Badge-Ba3epgcI.js} +2 -2
  15. package/dist/{Badge-Dz0cj6rj.js.map → Badge-Ba3epgcI.js.map} +1 -1
  16. package/dist/{BrandEditor-P8OZX2hi.js → BrandEditor-CN2HJ_12.js} +155 -17
  17. package/dist/BrandEditor-CN2HJ_12.js.map +1 -0
  18. package/dist/{BrandLogo-CKpVSZb6.js → BrandLogo-BfITD0im.js} +2 -2
  19. package/dist/{BrandLogo-CKpVSZb6.js.map → BrandLogo-BfITD0im.js.map} +1 -1
  20. package/dist/{Breadcrumb-BUoAZyhO.js → Breadcrumb-BnIlsoIH.js} +62 -20
  21. package/dist/Breadcrumb-BnIlsoIH.js.map +1 -0
  22. package/dist/{Button-B21oVcPU.js → Button-D2JqBAq7.js} +199 -7
  23. package/dist/Button-D2JqBAq7.js.map +1 -0
  24. package/dist/{Calendar-DFtmZzbF.js → Calendar-BnKi1kPu.js} +4 -4
  25. package/dist/{Calendar-DFtmZzbF.js.map → Calendar-BnKi1kPu.js.map} +1 -1
  26. package/dist/{Card-BQmBWHXl.js → Card-Bpc_ZlL4.js} +3 -3
  27. package/dist/Card-Bpc_ZlL4.js.map +1 -0
  28. package/dist/{Checkbox-8yIIgY7g.js → Checkbox-uFE8XLT_.js} +6 -6
  29. package/dist/Checkbox-uFE8XLT_.js.map +1 -0
  30. package/dist/{Clipboard-_xf6hMDD.js → Clipboard-D-9XOqy4.js} +3 -3
  31. package/dist/{Clipboard-_xf6hMDD.js.map → Clipboard-D-9XOqy4.js.map} +1 -1
  32. package/dist/{ClipboardButton-srUgWgwH.js → ClipboardButton-DxvZG_H0.js} +4 -4
  33. package/dist/{ClipboardButton-srUgWgwH.js.map → ClipboardButton-DxvZG_H0.js.map} +1 -1
  34. package/dist/{CodeEditor-Df7VVx08.js → CodeEditor-BfRSCKEs.js} +6 -6
  35. package/dist/{CodeEditor-Df7VVx08.js.map → CodeEditor-BfRSCKEs.js.map} +1 -1
  36. package/dist/{Collapsible-BQXWEOBC.js → Collapsible-DD4xTL75.js} +2 -2
  37. package/dist/{Collapsible-BQXWEOBC.js.map → Collapsible-DD4xTL75.js.map} +1 -1
  38. package/dist/{ColorInput-B3gBZ8Ya.js → ColorInput-CDKH8q_f.js} +5 -5
  39. package/dist/{ColorInput-B3gBZ8Ya.js.map → ColorInput-CDKH8q_f.js.map} +1 -1
  40. package/dist/{ColorModeSwitch-D8Cki137.js → ColorModeSwitch-DR6J2U6M.js} +4 -4
  41. package/dist/{ColorModeSwitch-D8Cki137.js.map → ColorModeSwitch-DR6J2U6M.js.map} +1 -1
  42. package/dist/{Combobox-BNhW0TDB.js → Combobox-CEesMe2M.js} +246 -44
  43. package/dist/Combobox-CEesMe2M.js.map +1 -0
  44. package/dist/{Command-BQPiZCNz.js → Command-T3MLRaen.js} +10 -6
  45. package/dist/Command-T3MLRaen.js.map +1 -0
  46. package/dist/{CopyField-Ba7h8MqZ.js → CopyField-CNbCHw9a.js} +4 -4
  47. package/dist/{CopyField-Ba7h8MqZ.js.map → CopyField-CNbCHw9a.js.map} +1 -1
  48. package/dist/{DateInput-D0nWUasV.js → DateInput-ClK1lCxz.js} +5 -5
  49. package/dist/{DateInput-D0nWUasV.js.map → DateInput-ClK1lCxz.js.map} +1 -1
  50. package/dist/{DatePicker-CtSa9b3V.js → DatePicker-CgHAu9KX.js} +3 -3
  51. package/dist/{DatePicker-CtSa9b3V.js.map → DatePicker-CgHAu9KX.js.map} +1 -1
  52. package/dist/{DateRangeInput-i6wE45-V.js → DateRangeInput-DtlAffS1.js} +5 -5
  53. package/dist/{DateRangeInput-i6wE45-V.js.map → DateRangeInput-DtlAffS1.js.map} +1 -1
  54. package/dist/{DateRangePicker-BAnuz6vO.js → DateRangePicker-CXpXPMoh.js} +8 -8
  55. package/dist/{DateRangePicker-BAnuz6vO.js.map → DateRangePicker-CXpXPMoh.js.map} +1 -1
  56. package/dist/{DiffView-ppTIzh2h.js → DiffView-Dy4z-n-1.js} +3 -3
  57. package/dist/{DiffView-ppTIzh2h.js.map → DiffView-Dy4z-n-1.js.map} +1 -1
  58. package/dist/{DropdownMenu-BJGLjpVc.js → DropdownMenu-B1gGvZoh.js} +9 -9
  59. package/dist/DropdownMenu-B1gGvZoh.js.map +1 -0
  60. package/dist/{FieldBox-D7DYtX_m.js → FieldBox-BrxLkY9C.js} +8 -6
  61. package/dist/FieldBox-BrxLkY9C.js.map +1 -0
  62. package/dist/{FieldItem-BHViBJS3.js → FieldItem-CYvJyjiI.js} +4 -4
  63. package/dist/{FieldItem-BHViBJS3.js.map → FieldItem-CYvJyjiI.js.map} +1 -1
  64. package/dist/{FileUpload-Ch2hrIWq.js → FileUpload-D6vPG3WB.js} +9 -9
  65. package/dist/FileUpload-D6vPG3WB.js.map +1 -0
  66. package/dist/{Form-VkzfVPyT.js → Form-CD55zJiy.js} +10 -10
  67. package/dist/{Form-VkzfVPyT.js.map → Form-CD55zJiy.js.map} +1 -1
  68. package/dist/{FormGroup-CmCppxOn.js → FormGroup-DiYFdEhH.js} +4 -4
  69. package/dist/{FormGroup-CmCppxOn.js.map → FormGroup-DiYFdEhH.js.map} +1 -1
  70. package/dist/{FullScreenModal-Cwnm8WdM.js → FullScreenModal-CUuAakh3.js} +20 -6
  71. package/dist/{FullScreenModal-Cwnm8WdM.js.map → FullScreenModal-CUuAakh3.js.map} +1 -1
  72. package/dist/HoverCard-BBdPYqEf.js +31 -0
  73. package/dist/HoverCard-BBdPYqEf.js.map +1 -0
  74. package/dist/InlineEdit-D5RdunZo.js +370 -0
  75. package/dist/InlineEdit-D5RdunZo.js.map +1 -0
  76. package/dist/{InputGroup-CrlMRz4X.js → InputGroup-Dm014ruw.js} +4 -4
  77. package/dist/{InputGroup-CrlMRz4X.js.map → InputGroup-Dm014ruw.js.map} +1 -1
  78. package/dist/{Kbd-J4n1J6i4.js → Kbd-3YM7NQO4.js} +3 -3
  79. package/dist/{Kbd-J4n1J6i4.js.map → Kbd-3YM7NQO4.js.map} +1 -1
  80. package/dist/MermaidDiagram-CeiOkI60.js +199 -0
  81. package/dist/MermaidDiagram-CeiOkI60.js.map +1 -0
  82. package/dist/{Modal-BkSuS9Yz.js → Modal-DMZRswk4.js} +8 -7
  83. package/dist/Modal-DMZRswk4.js.map +1 -0
  84. package/dist/{ModalTitle-BxopkIoQ.js → ModalTitle-CTECVgL8.js} +3 -3
  85. package/dist/ModalTitle-CTECVgL8.js.map +1 -0
  86. package/dist/{MultiActionButtonGroup-AWLj1Tg7.js → MultiActionButtonGroup-CEv_txvz.js} +5 -5
  87. package/dist/{MultiActionButtonGroup-AWLj1Tg7.js.map → MultiActionButtonGroup-CEv_txvz.js.map} +1 -1
  88. package/dist/{Navbar-B_DNpv5s.js → Navbar-CtVYcRFn.js} +18 -14
  89. package/dist/Navbar-CtVYcRFn.js.map +1 -0
  90. package/dist/{OtpInput-I8B8Vjdl.js → OtpInput-CY8yCaJk.js} +4 -4
  91. package/dist/{OtpInput-I8B8Vjdl.js.map → OtpInput-CY8yCaJk.js.map} +1 -1
  92. package/dist/{PageHeader-CgfyXpf1.js → PageHeader-CL1A6jFM.js} +21 -14
  93. package/dist/PageHeader-CL1A6jFM.js.map +1 -0
  94. package/dist/{PageLayout-DiAAILuS.js → PageLayout-C9aXTpqV.js} +2 -2
  95. package/dist/{PageLayout-DiAAILuS.js.map → PageLayout-C9aXTpqV.js.map} +1 -1
  96. package/dist/{PageToolbar-C06RKIif.js → PageToolbar-B_e7HGva.js} +6 -6
  97. package/dist/{PageToolbar-C06RKIif.js.map → PageToolbar-B_e7HGva.js.map} +1 -1
  98. package/dist/{Pagination-DH3f6LN0.js → Pagination-DDp6PRqf.js} +5 -5
  99. package/dist/Pagination-DDp6PRqf.js.map +1 -0
  100. package/dist/{Popconfirm-Dhzn7sy6.js → Popconfirm-BgV6a0sl.js} +5 -5
  101. package/dist/{Popconfirm-Dhzn7sy6.js.map → Popconfirm-BgV6a0sl.js.map} +1 -1
  102. package/dist/{Popover-CEk915I1.js → Popover-BU7nGOv-.js} +3 -3
  103. package/dist/Popover-BU7nGOv-.js.map +1 -0
  104. package/dist/{RadioGroup-CYEp8Wnx.js → RadioGroup-C8EC3fqO.js} +6 -6
  105. package/dist/{RadioGroup-CYEp8Wnx.js.map → RadioGroup-C8EC3fqO.js.map} +1 -1
  106. package/dist/{Resizable-D3vYToZH.js → Resizable-D8zs0kcq.js} +2 -2
  107. package/dist/{Resizable-D3vYToZH.js.map → Resizable-D8zs0kcq.js.map} +1 -1
  108. package/dist/{Search-DmkQo716.js → Search-aaUMI4ix.js} +4 -4
  109. package/dist/{Search-DmkQo716.js.map → Search-aaUMI4ix.js.map} +1 -1
  110. package/dist/{SearchInput-D3qMkHaW.js → SearchInput-Dxi2GK4n.js} +4 -4
  111. package/dist/{SearchInput-D3qMkHaW.js.map → SearchInput-Dxi2GK4n.js.map} +1 -1
  112. package/dist/{SectionNav-I2p2uXTd.js → SectionNav-BnHOMLtE.js} +11 -11
  113. package/dist/{SectionNav-I2p2uXTd.js.map → SectionNav-BnHOMLtE.js.map} +1 -1
  114. package/dist/{Select-B4uK0QVm.js → Select-CjQgU_6K.js} +6 -6
  115. package/dist/Select-CjQgU_6K.js.map +1 -0
  116. package/dist/{Sheet-D0fAE14T.js → Sheet-utw4_rUa.js} +9 -9
  117. package/dist/Sheet-utw4_rUa.js.map +1 -0
  118. package/dist/{Sidebar-D9ytIs8V.js → Sidebar-BAmux2Uj.js} +21 -18
  119. package/dist/Sidebar-BAmux2Uj.js.map +1 -0
  120. package/dist/{Slider-B0ou9r0B.js → Slider-DbzzSDRV.js} +5 -5
  121. package/dist/{Slider-B0ou9r0B.js.map → Slider-DbzzSDRV.js.map} +1 -1
  122. package/dist/{Stepper-BLkLhfnd.js → Stepper-BBPembeK.js} +3 -3
  123. package/dist/{Stepper-BLkLhfnd.js.map → Stepper-BBPembeK.js.map} +1 -1
  124. package/dist/{Switch-DZvNePOm.js → Switch-Dw4tvQls.js} +3 -3
  125. package/dist/Switch-Dw4tvQls.js.map +1 -0
  126. package/dist/{Table-BaVT5Tkn.js → Table-CilEvpMq.js} +65 -6
  127. package/dist/Table-CilEvpMq.js.map +1 -0
  128. package/dist/{Tabs-CTDmye1N.js → Tabs-C0uvkPHc.js} +5 -5
  129. package/dist/Tabs-C0uvkPHc.js.map +1 -0
  130. package/dist/{TextInput-BWYXbfs4.js → TextInput-K2DSDs5O.js} +5 -5
  131. package/dist/{TextInput-BWYXbfs4.js.map → TextInput-K2DSDs5O.js.map} +1 -1
  132. package/dist/{ThemeProvider-VF38VfUa.js → ThemeProvider-C8yaKlM1.js} +22 -5
  133. package/dist/ThemeProvider-C8yaKlM1.js.map +1 -0
  134. package/dist/{TimePicker-CDGYrRxi.js → TimePicker-DyyV3nw8.js} +2 -2
  135. package/dist/{TimePicker-CDGYrRxi.js.map → TimePicker-DyyV3nw8.js.map} +1 -1
  136. package/dist/{Timeline-B-I6sb0M.js → Timeline-DUXgByUE.js} +3 -3
  137. package/dist/{Timeline-B-I6sb0M.js.map → Timeline-DUXgByUE.js.map} +1 -1
  138. package/dist/{Toast-NrOdDDru.js → Toast-BGnUL4jV.js} +2 -2
  139. package/dist/{Toast-NrOdDDru.js.map → Toast-BGnUL4jV.js.map} +1 -1
  140. package/dist/ToggleAllTooltip-Dlmq-yqS.js +37 -0
  141. package/dist/ToggleAllTooltip-Dlmq-yqS.js.map +1 -0
  142. package/dist/{ToggleGroup-Cy_10Mvu.js → ToggleGroup-BOz1yyz6.js} +3 -3
  143. package/dist/ToggleGroup-BOz1yyz6.js.map +1 -0
  144. package/dist/{Workbench-DmZzpkj7.js → Workbench-CxgNCs3I.js} +4 -4
  145. package/dist/{Workbench-DmZzpkj7.js.map → Workbench-CxgNCs3I.js.map} +1 -1
  146. package/dist/{ZoomableSvg-C65nta_r.js → ZoomableSvg-Dh1OstPN.js} +87 -83
  147. package/dist/ZoomableSvg-Dh1OstPN.js.map +1 -0
  148. package/dist/charts.js +1 -1
  149. package/dist/classNames-DcGnTh2N.js +21 -0
  150. package/dist/classNames-DcGnTh2N.js.map +1 -0
  151. package/dist/editors.js +6 -6
  152. package/dist/flint.js +10 -45
  153. package/dist/flint.js.map +1 -1
  154. package/dist/highlightCode-D354tCXO.js +81 -0
  155. package/dist/highlightCode-D354tCXO.js.map +1 -0
  156. package/dist/{icons-wcA70iQx.js → icons-ZIeZXXS7.js} +37 -4
  157. package/dist/icons-ZIeZXXS7.js.map +1 -0
  158. package/dist/icons.d.ts +2 -2
  159. package/dist/icons.js +2 -2
  160. package/dist/{index-B7k-09i1.d.ts → index-D-itZ3VP.d.ts} +14 -3
  161. package/dist/index.d.ts +942 -424
  162. package/dist/index.js +72 -70
  163. package/dist/internal/blocks/AIKit.js +2 -2
  164. package/dist/internal/blocks/AppShell.js +1 -1
  165. package/dist/internal/blocks/AuthLayout.js +2 -2
  166. package/dist/internal/blocks/AutoSaveTextarea.js +1 -1
  167. package/dist/internal/blocks/BrandEditor.js +1 -1
  168. package/dist/internal/blocks/ChartPanel.js +2 -2
  169. package/dist/internal/blocks/ChipEditor.js +4 -4
  170. package/dist/internal/blocks/ClipboardButton.js +1 -1
  171. package/dist/internal/blocks/ColorInput.js +1 -1
  172. package/dist/internal/blocks/ColorModeSwitch.js +1 -1
  173. package/dist/internal/blocks/Command.js +1 -1
  174. package/dist/internal/blocks/CopyField.js +1 -1
  175. package/dist/internal/blocks/DashboardHero.js +2 -2
  176. package/dist/internal/blocks/DashboardHero.js.map +1 -1
  177. package/dist/internal/blocks/DateInput.js +1 -1
  178. package/dist/internal/blocks/DateRangeInput.js +1 -1
  179. package/dist/internal/blocks/DiagramShared/DiagramSkeleton.js.map +1 -1
  180. package/dist/internal/blocks/EmptyConfigurationActionCard.js +1 -1
  181. package/dist/internal/blocks/ExportMenu.js +4 -4
  182. package/dist/internal/blocks/FacebookButton.js +66 -0
  183. package/dist/internal/blocks/FacebookButton.js.map +1 -0
  184. package/dist/internal/blocks/Form/connectors/ComboboxField.js +2 -2
  185. package/dist/internal/blocks/Form/connectors/MultiComboboxField.js +1 -1
  186. package/dist/internal/blocks/Form.js +1 -1
  187. package/dist/internal/blocks/FormGroup.js +1 -1
  188. package/dist/internal/blocks/GoogleButton.js +2 -2
  189. package/dist/internal/blocks/InlineEdit.js +2 -0
  190. package/dist/internal/blocks/MicrosoftButton.js +2 -2
  191. package/dist/internal/blocks/MultiActionButtonGroup.js +1 -1
  192. package/dist/internal/blocks/MultiCombobox.js +204 -68
  193. package/dist/internal/blocks/MultiCombobox.js.map +1 -1
  194. package/dist/internal/blocks/NumberInput.js +1 -1
  195. package/dist/internal/blocks/OtpInput.js +1 -1
  196. package/dist/internal/blocks/PageHeader.js +1 -1
  197. package/dist/internal/blocks/PageLayout.js +1 -1
  198. package/dist/internal/blocks/PageToolbar.js +1 -1
  199. package/dist/internal/blocks/PasswordInput.js +3 -3
  200. package/dist/internal/blocks/SearchInput.js +1 -1
  201. package/dist/internal/blocks/SearchInputWithNavigation.js +5 -5
  202. package/dist/internal/blocks/SectionNav.js +1 -1
  203. package/dist/internal/blocks/SplitButton.js +6 -5
  204. package/dist/internal/blocks/SplitButton.js.map +1 -1
  205. package/dist/internal/blocks/StatCard.js +2 -2
  206. package/dist/internal/blocks/TextInput.js +1 -1
  207. package/dist/internal/blocks/Textarea.js +1 -1
  208. package/dist/internal/blocks/TimeInput.js +4 -4
  209. package/dist/internal/components/Alert.js +1 -1
  210. package/dist/internal/components/AlertModal.js +1 -1
  211. package/dist/internal/components/Avatar.js +2 -2
  212. package/dist/internal/components/Badge.js +1 -1
  213. package/dist/internal/components/BrandLogo.js +1 -1
  214. package/dist/internal/components/Breadcrumb.js +1 -1
  215. package/dist/internal/components/Button.js +1 -1
  216. package/dist/internal/components/ButtonGroup.js +1 -1
  217. package/dist/internal/components/Calendar.js +1 -1
  218. package/dist/internal/components/Card.js +1 -1
  219. package/dist/internal/components/Checkbox.js +1 -1
  220. package/dist/internal/components/Clipboard.js +1 -1
  221. package/dist/internal/components/Collapsible.js +1 -1
  222. package/dist/internal/components/CollapsiblePanel.js +5 -5
  223. package/dist/internal/components/CollapsiblePanel.js.map +1 -1
  224. package/dist/internal/components/Combobox.js +1 -1
  225. package/dist/internal/components/DatePicker.js +1 -1
  226. package/dist/internal/components/DateRangePicker.js +1 -1
  227. package/dist/internal/components/DiffView.js +1 -1
  228. package/dist/internal/components/DotsLoader.js +1 -1
  229. package/dist/internal/components/DropdownMenu.js +1 -1
  230. package/dist/internal/components/FileUpload.js +1 -1
  231. package/dist/internal/components/FullScreenModal.js +1 -1
  232. package/dist/internal/components/HoverCard.js +1 -1
  233. package/dist/internal/components/InputGroup.js +1 -1
  234. package/dist/internal/components/Kbd.js +1 -1
  235. package/dist/internal/components/Label.js +2 -2
  236. package/dist/internal/components/Label.js.map +1 -1
  237. package/dist/internal/components/Link.js +1 -1
  238. package/dist/internal/components/MergeEditor/ConflictStatus/ConflictStatusIndicator.js +2 -2
  239. package/dist/internal/components/Modal.js +1 -1
  240. package/dist/internal/components/Navbar.js +1 -1
  241. package/dist/internal/components/Pagination.js +1 -1
  242. package/dist/internal/components/PanelWithDetails.js +3 -3
  243. package/dist/internal/components/Popconfirm.js +1 -1
  244. package/dist/internal/components/Popover.js +1 -1
  245. package/dist/internal/components/Progress.js +1 -1
  246. package/dist/internal/components/RadioGroup.js +1 -1
  247. package/dist/internal/components/Resizable.js +1 -1
  248. package/dist/internal/components/ScrollArea.js +1 -1
  249. package/dist/internal/components/Search.js +1 -1
  250. package/dist/internal/components/Select.js +1 -1
  251. package/dist/internal/components/Separator.js +1 -1
  252. package/dist/internal/components/Sheet.js +1 -1
  253. package/dist/internal/components/Sidebar.js +1 -1
  254. package/dist/internal/components/Skeleton/Skeleton.js +2 -2
  255. package/dist/internal/components/Skeleton/Skeleton.js.map +1 -1
  256. package/dist/internal/components/Skeleton/SkeletonList.js +1 -1
  257. package/dist/internal/components/Slider.js +1 -1
  258. package/dist/internal/components/SpotlightBackdrop.js +1 -1
  259. package/dist/internal/components/Stepper.js +1 -1
  260. package/dist/internal/components/Switch.js +1 -1
  261. package/dist/internal/components/Table.js +1 -1
  262. package/dist/internal/components/Tabs.js +1 -1
  263. package/dist/internal/components/ThemeProvider.js +1 -1
  264. package/dist/internal/components/TimePicker.js +1 -1
  265. package/dist/internal/components/Timeline.js +1 -1
  266. package/dist/internal/components/Toast.js +2 -2
  267. package/dist/internal/components/Toggle.js +5 -5
  268. package/dist/internal/components/Toggle.js.map +1 -1
  269. package/dist/internal/components/ToggleGroup.js +1 -1
  270. package/dist/internal/components/Tooltip.js +1 -1
  271. package/dist/internal/components/Truncated.js +2 -2
  272. package/dist/internal/components/Workbench.js +1 -1
  273. package/dist/internal/components/ZoomableSvg.js +1 -1
  274. package/dist/internal/utils.js +2 -2
  275. package/dist/markdown-editor.js +14 -14
  276. package/dist/markdown-editor.js.map +1 -1
  277. package/dist/mermaid.d.ts +1 -2
  278. package/dist/mermaid.js +1 -1
  279. package/dist/rjsf.js +101 -14
  280. package/dist/rjsf.js.map +1 -1
  281. package/dist/{styles-DBGXH5-_.js → styles-BDA5rQz3.js} +6 -6
  282. package/dist/styles-BDA5rQz3.js.map +1 -0
  283. package/dist/{styles-gzcjOyKv.js → styles-YQdM7G8c.js} +3 -3
  284. package/dist/{styles-gzcjOyKv.js.map → styles-YQdM7G8c.js.map} +1 -1
  285. package/dist/styles.css +1 -1
  286. package/dist/svgCache-Dhmbe-7i.js +26 -0
  287. package/dist/svgCache-Dhmbe-7i.js.map +1 -0
  288. package/dist/svgTransforms-Hnvev3Vs.js +38 -0
  289. package/dist/svgTransforms-Hnvev3Vs.js.map +1 -0
  290. package/dist/testing.js +1 -1
  291. package/dist/{themeTokens-7DEab0kp.js → themeTokens-l2Fsfcs7.js} +3 -12
  292. package/dist/themeTokens-l2Fsfcs7.js.map +1 -0
  293. package/dist/{toast-CyXEdaCZ.js → toast-D6Z6MOB5.js} +5 -5
  294. package/dist/toast-D6Z6MOB5.js.map +1 -0
  295. package/dist/{utils-Bd28bWgm.js → utils-LQ4jJA-9.js} +21 -5
  296. package/dist/utils-LQ4jJA-9.js.map +1 -0
  297. package/dist/vega.js +1 -1
  298. package/dist/vegaTheme-BrmVoPEz.js +49 -0
  299. package/dist/vegaTheme-BrmVoPEz.js.map +1 -0
  300. package/dist/{vegaTheme-VQMWTlBw.js → vegaTokens-Ce07C0DH.js} +36 -44
  301. package/dist/vegaTokens-Ce07C0DH.js.map +1 -0
  302. package/package.json +4 -4
  303. package/src/styles.css +183 -4
  304. package/src/theme.css +47 -3
  305. package/dist/AIKit-BVCWP9Ks.js.map +0 -1
  306. package/dist/AlertModal-mbSnuuka.js.map +0 -1
  307. package/dist/BrandEditor-P8OZX2hi.js.map +0 -1
  308. package/dist/Breadcrumb-BUoAZyhO.js.map +0 -1
  309. package/dist/Button-B21oVcPU.js.map +0 -1
  310. package/dist/Card-BQmBWHXl.js.map +0 -1
  311. package/dist/Checkbox-8yIIgY7g.js.map +0 -1
  312. package/dist/Combobox-BNhW0TDB.js.map +0 -1
  313. package/dist/Command-BQPiZCNz.js.map +0 -1
  314. package/dist/DiagramSource-C5_d8Gdj.js +0 -11
  315. package/dist/DiagramSource-C5_d8Gdj.js.map +0 -1
  316. package/dist/DropdownMenu-BJGLjpVc.js.map +0 -1
  317. package/dist/FieldBox-D7DYtX_m.js.map +0 -1
  318. package/dist/FileUpload-Ch2hrIWq.js.map +0 -1
  319. package/dist/HoverCard-B8OKu9XZ.js +0 -31
  320. package/dist/HoverCard-B8OKu9XZ.js.map +0 -1
  321. package/dist/MermaidDiagram-B30x2yV7.js +0 -302
  322. package/dist/MermaidDiagram-B30x2yV7.js.map +0 -1
  323. package/dist/Modal-BkSuS9Yz.js.map +0 -1
  324. package/dist/ModalTitle-BxopkIoQ.js.map +0 -1
  325. package/dist/Navbar-B_DNpv5s.js.map +0 -1
  326. package/dist/PageHeader-CgfyXpf1.js.map +0 -1
  327. package/dist/Pagination-DH3f6LN0.js.map +0 -1
  328. package/dist/Popover-CEk915I1.js.map +0 -1
  329. package/dist/Select-B4uK0QVm.js.map +0 -1
  330. package/dist/Sheet-D0fAE14T.js.map +0 -1
  331. package/dist/Sidebar-D9ytIs8V.js.map +0 -1
  332. package/dist/Switch-DZvNePOm.js.map +0 -1
  333. package/dist/Table-BaVT5Tkn.js.map +0 -1
  334. package/dist/Tabs-CTDmye1N.js.map +0 -1
  335. package/dist/ThemeProvider-VF38VfUa.js.map +0 -1
  336. package/dist/ToggleGroup-Cy_10Mvu.js.map +0 -1
  337. package/dist/ZoomableSvg-C65nta_r.js.map +0 -1
  338. package/dist/classNames-BzrkDnOD.js +0 -9
  339. package/dist/classNames-BzrkDnOD.js.map +0 -1
  340. package/dist/icons-wcA70iQx.js.map +0 -1
  341. package/dist/styles-DBGXH5-_.js.map +0 -1
  342. package/dist/svgTransforms-Dw2hRA-i.js +0 -51
  343. package/dist/svgTransforms-Dw2hRA-i.js.map +0 -1
  344. package/dist/themeTokens-7DEab0kp.js.map +0 -1
  345. package/dist/toast-CyXEdaCZ.js.map +0 -1
  346. package/dist/utils-Bd28bWgm.js.map +0 -1
  347. package/dist/vegaTheme-VQMWTlBw.js.map +0 -1
@@ -1,29 +1,69 @@
1
- import { cn } from "./classNames-BzrkDnOD.js";
2
- import { Tooltip, TooltipProvider } from "./utils-Bd28bWgm.js";
3
- import { AlertCircle, ArrowDown, ArrowLeft, ArrowRight, ArrowUp, Ban, BarChart3, Bell, Bug, Calendar, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Circle, CircleAlert, CircleCheck, CircleHelp, CirclePause, CirclePlay, CirclePlus, CircleStop, CircleX, Clock, Cog, Copy, Database, Download, Ellipsis, ExternalLink, Eye, File, FilePen, FileText, Flag, Folder, Gauge, GitBranch, GitMerge, GitPullRequest, Globe, Hand, History, Hourglass, Info, Key, Layers, LineChart, Link, List, ListChecks, Loader2, Lock, Mail, Maximize2, Minimize2, Minus, Pencil, Play, Plus, RefreshCw, Rocket, RotateCcw, RotateCw, Save, Search, Send, Settings, Sparkles, Star, Table, ThumbsDown, ThumbsUp, Trash2, TriangleAlert, Upload, User, Users, WandSparkles, Warehouse, Workflow, X, XCircle, Zap } from "./icons-wcA70iQx.js";
4
- import { Button } from "./Button-B21oVcPU.js";
1
+ import { cn } from "./classNames-DcGnTh2N.js";
2
+ import { Tooltip, TooltipProvider, isMac } from "./utils-LQ4jJA-9.js";
3
+ import { AlertCircle, ArrowDown, ArrowLeft, ArrowRight, ArrowUp, Ban, BarChart3, Bell, BookOpen, Bookmark, Box, Bug, Calendar, ChartBar, ChartPie, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Circle, CircleAlert, CircleCheck, CircleHelp, CirclePause, CirclePlay, CirclePlus, CircleStop, CircleX, Clock, Cloud, Cog, Copy, Database, Download, Ellipsis, ExternalLink, Eye, File, FilePen, FileText, Filter, Flag, Folder, Gauge, GitBranch, GitMerge, GitPullRequest, Globe, Hand, HardDrive, History, Hourglass, Info, Key, Layers, Lightbulb, LineChart, Link, List, ListChecks, Loader2, LoaderCircle, Lock, Mail, Maximize2, Minimize2, Minus, Package, Pause, Pencil, Pin, Play, Plus, Quote, RefreshCw, Rocket, RotateCcw, RotateCw, Save, Search, Send, Server, Settings, Shield, Sparkles, Star, Table, ThumbsDown, ThumbsUp, Trash2, TrendingDown, TrendingUp, TriangleAlert, Upload, User, Users, WandSparkles, Warehouse, Workflow, Wrench, X, XCircle, Zap } from "./icons-ZIeZXXS7.js";
4
+ import { Button } from "./Button-D2JqBAq7.js";
5
5
  import { ButtonGroup } from "./internal/components/ButtonGroup.js";
6
- import { ClipboardButton } from "./ClipboardButton-srUgWgwH.js";
7
- import { FullScreenModal, FullScreenModalBody, FullScreenModalHeader } from "./FullScreenModal-Cwnm8WdM.js";
6
+ import { ClipboardButton } from "./ClipboardButton-DxvZG_H0.js";
7
+ import { FullScreenModal, FullScreenModalBody, FullScreenModalHeader } from "./FullScreenModal-CUuAakh3.js";
8
8
  import { Truncated } from "./internal/components/Truncated.js";
9
- import { HoverCard } from "./HoverCard-B8OKu9XZ.js";
10
- import { Command } from "./Command-BQPiZCNz.js";
11
- import { TextInput } from "./TextInput-BWYXbfs4.js";
12
- import { Badge } from "./Badge-Dz0cj6rj.js";
9
+ import { useStableCallback } from "./internal/hooks/useStableCallback.js";
10
+ import { HoverCard } from "./HoverCard-BBdPYqEf.js";
11
+ import { Command } from "./Command-T3MLRaen.js";
12
+ import { TextInput } from "./TextInput-K2DSDs5O.js";
13
+ import { Badge } from "./Badge-Ba3epgcI.js";
13
14
  import { Textarea } from "./internal/blocks/Textarea.js";
14
- import { Table as Table$1 } from "./Table-BaVT5Tkn.js";
15
+ import { Kbd, keys } from "./Kbd-3YM7NQO4.js";
16
+ import { Collapsible } from "./Collapsible-DD4xTL75.js";
17
+ import { Alert } from "./Alert-CZ0dDsuk.js";
18
+ import { Table as Table$1, tableAriaSort } from "./Table-CilEvpMq.js";
15
19
  import { DiagramSkeleton } from "./internal/blocks/DiagramShared/DiagramSkeleton.js";
16
- import { DiagramSource } from "./DiagramSource-C5_d8Gdj.js";
17
- import { Kbd, keys } from "./Kbd-J4n1J6i4.js";
20
+ import { DiagramSource, makeResponsive } from "./svgTransforms-Hnvev3Vs.js";
21
+ import { ZoomableSvg, toSvgFileContent } from "./ZoomableSvg-Dh1OstPN.js";
18
22
  import { Progress } from "./internal/components/Progress.js";
19
23
  import { digitFromKeyCode, useAltDigitHotkey } from "./internal/hooks/useAltDigitHotkey.js";
24
+ import { createContext, isValidElement, use, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
20
25
  import { Slot } from "@radix-ui/react-slot";
21
- import { createContext, isValidElement, use, useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
22
26
  import { cva } from "class-variance-authority";
23
27
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
24
28
  import TextareaAutosize from "react-textarea-autosize";
25
29
  import ReactMarkdown from "react-markdown";
26
30
  import remarkGfm from "remark-gfm";
31
+ import DOMPurify from "dompurify";
32
+ //#region src/blocks/AIKit/AnswerCommentInput.tsx
33
+ const MAX_COMMENT_LENGTH = 2e3;
34
+ /**
35
+ * One-line follow-up to a thumbs-down.
36
+ *
37
+ * - Submits on Enter or blur, trimmed, and only when the text differs from the last one sent.
38
+ * - Never submits an empty comment.
39
+ */
40
+ const AnswerCommentInput = ({ placeholder, onSubmit }) => {
41
+ const [text, setText] = useState("");
42
+ const lastSubmittedRef = useRef("");
43
+ const submit = () => {
44
+ const comment = text.trim();
45
+ if (!comment || comment === lastSubmittedRef.current) return;
46
+ lastSubmittedRef.current = comment;
47
+ onSubmit(comment);
48
+ };
49
+ const handleKeyDown = (event) => {
50
+ if (event.key !== "Enter") return;
51
+ event.preventDefault();
52
+ submit();
53
+ };
54
+ return /* @__PURE__ */ jsx(TextInput, {
55
+ sizing: "small",
56
+ value: text,
57
+ onValueChange: setText,
58
+ onKeyDown: handleKeyDown,
59
+ onBlur: submit,
60
+ maxLength: MAX_COMMENT_LENGTH,
61
+ placeholder,
62
+ "aria-label": placeholder,
63
+ className: "tw:w-64 tw:max-w-full tw:[&_input]:text-ellipsis"
64
+ });
65
+ };
66
+ //#endregion
27
67
  //#region src/blocks/AIKit/AnswerActions.tsx
28
68
  const VOTE_BUTTONS = [{
29
69
  vote: "up",
@@ -54,7 +94,7 @@ const SIZES = {
54
94
  * - A vote can be changed but not withdrawn; pressing the pressed one re-sends it.
55
95
  * - The caller owns the recorded vote and the write.
56
96
  */
57
- const AnswerActions = ({ copyText, vote, onVote, onCopy, actions, labels = DEFAULT_LABELS, confirmation, size = "small", layout = "inline" }) => {
97
+ const AnswerActions = ({ copyText, vote, onVote, onCopy, actions, labels = DEFAULT_LABELS, confirmation, size = "small", layout = "inline", onDownComment, commentPlaceholder = "What was wrong? (optional)", onRegenerate, regenerateLabel = "Ask again" }) => {
58
98
  const { button, gap } = SIZES[size];
59
99
  const votes = VOTE_BUTTONS.map(({ vote: candidate, icon }) => {
60
100
  const isRecorded = vote === candidate;
@@ -81,32 +121,176 @@ const AnswerActions = ({ copyText, vote, onVote, onCopy, actions, labels = DEFAU
81
121
  variant: "invisible",
82
122
  onClick: onCopy
83
123
  }) : null;
124
+ const regenerate = onRegenerate ? /* @__PURE__ */ jsx(Tooltip, {
125
+ tooltip: regenerateLabel,
126
+ placement: "top",
127
+ triggerAsChild: true,
128
+ children: /* @__PURE__ */ jsx(Button, {
129
+ size: button,
130
+ variant: "invisible",
131
+ "aria-label": regenerateLabel,
132
+ onClick: onRegenerate,
133
+ children: /* @__PURE__ */ jsx(RotateCcw, { className: "tw:size-4" })
134
+ })
135
+ }) : null;
136
+ const comment = onDownComment && vote === "down" ? /* @__PURE__ */ jsx(AnswerCommentInput, {
137
+ placeholder: commentPlaceholder,
138
+ onSubmit: onDownComment
139
+ }) : null;
84
140
  const said = confirmation ? /* @__PURE__ */ jsx("span", {
85
141
  "aria-live": "polite",
86
142
  className: "tw:ml-1 tw:text-xs tw:font-medium tw:text-foreground tw:empty:hidden tw:animate-in tw:fade-in tw:slide-in-from-left-1 tw:duration-300",
87
143
  children: vote ? confirmation[vote] : ""
88
144
  }) : null;
89
145
  if (layout === "split") return /* @__PURE__ */ jsxs("div", {
90
- className: "tw:flex tw:w-full tw:items-center tw:justify-between tw:gap-2",
91
- children: [/* @__PURE__ */ jsxs(ButtonGroup, {
92
- space: gap,
93
- "aria-label": "Answer actions",
94
- children: [votes, said]
95
- }), /* @__PURE__ */ jsxs(ButtonGroup, {
96
- space: gap,
97
- children: [actions, copy]
98
- })]
146
+ className: "tw:flex tw:w-full tw:flex-wrap tw:items-center tw:justify-between tw:gap-2",
147
+ children: [
148
+ /* @__PURE__ */ jsxs(ButtonGroup, {
149
+ space: gap,
150
+ "aria-label": "Answer actions",
151
+ children: [votes, said]
152
+ }),
153
+ comment,
154
+ /* @__PURE__ */ jsxs(ButtonGroup, {
155
+ space: gap,
156
+ children: [
157
+ actions,
158
+ regenerate,
159
+ copy
160
+ ]
161
+ })
162
+ ]
99
163
  });
100
- return /* @__PURE__ */ jsxs(ButtonGroup, {
164
+ const group = /* @__PURE__ */ jsxs(ButtonGroup, {
101
165
  space: gap,
102
166
  "aria-label": "Answer actions",
103
167
  children: [
104
168
  copy,
105
169
  votes,
170
+ regenerate,
106
171
  actions,
107
172
  said
108
173
  ]
109
174
  });
175
+ if (!comment) return group;
176
+ return /* @__PURE__ */ jsxs("div", {
177
+ className: "tw:flex tw:flex-wrap tw:items-center tw:gap-2",
178
+ children: [group, comment]
179
+ });
180
+ };
181
+ //#endregion
182
+ //#region src/blocks/AIKit/SourceChip.tsx
183
+ const STATE_CLASSES = {
184
+ verified: "tw:border-solid tw:border-success/30 tw:bg-success/10 tw:text-foreground",
185
+ unverified: "tw:border-dashed tw:border-warning/60 tw:bg-warning/10 tw:text-foreground",
186
+ neutral: "tw:border-solid tw:border-border tw:bg-muted tw:text-foreground"
187
+ };
188
+ const STATE_WORD = {
189
+ verified: "verified",
190
+ unverified: "unverified"
191
+ };
192
+ const accessibleName = ({ kind, label, state = "neutral" }) => [[kind, label].filter(Boolean).join(" "), STATE_WORD[state]].filter(Boolean).join(", ");
193
+ /**
194
+ * One cited source in an answer's provenance row.
195
+ *
196
+ * - The verdict rides the border and the accessible name, not the visible text.
197
+ * - A link when it has `href`, a button when it only has `onClick`, a plain label otherwise.
198
+ * - A plain left click goes to `onClick`; modified clicks keep the link's own behaviour.
199
+ */
200
+ const SourceChip = (props) => {
201
+ const { label, state = "neutral", icon, href, external, onClick, title, className } = props;
202
+ const classes = cn("tailwindcss-preflight tw:inline-flex tw:h-6 tw:max-w-64 tw:min-w-0 tw:items-center tw:gap-1 tw:rounded-md tw:border tw:px-2 tw:text-xs tw:no-underline", STATE_CLASSES[state], (href || onClick) && "tw:cursor-pointer tw:hover:bg-hover tw:focus-visible:outline-none tw:focus-visible:ring-2 tw:focus-visible:ring-ring", className);
203
+ const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [icon && /* @__PURE__ */ jsx("span", {
204
+ "aria-hidden": true,
205
+ className: "tw:inline-flex tw:shrink-0 tw:[&_svg]:size-3.5",
206
+ children: icon
207
+ }), /* @__PURE__ */ jsx("span", {
208
+ className: "tw:min-w-0 tw:truncate",
209
+ children: label
210
+ })] });
211
+ const common = {
212
+ "aria-label": accessibleName(props),
213
+ title,
214
+ "data-state": state
215
+ };
216
+ if (href) return /* @__PURE__ */ jsx("a", {
217
+ ...common,
218
+ href,
219
+ target: external ? "_blank" : void 0,
220
+ rel: external ? "noopener noreferrer" : void 0,
221
+ className: classes,
222
+ onClick: (event) => {
223
+ const isModified = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
224
+ if (!onClick || isModified || event.button !== 0) return;
225
+ event.preventDefault();
226
+ onClick();
227
+ },
228
+ children: content
229
+ });
230
+ if (onClick) return /* @__PURE__ */ jsx("button", {
231
+ ...common,
232
+ type: "button",
233
+ className: cn(classes, "tw:bg-clip-padding"),
234
+ onClick,
235
+ children: content
236
+ });
237
+ return /* @__PURE__ */ jsx("span", {
238
+ ...common,
239
+ className: classes,
240
+ children: content
241
+ });
242
+ };
243
+ //#endregion
244
+ //#region src/blocks/AIKit/AnswerSources.tsx
245
+ const ANSWER_SOURCES_VISIBLE = 6;
246
+ /**
247
+ * The provenance row under an answer: where its figures came from.
248
+ *
249
+ * - Says "N unverified" once, instead of on every chip, so checked sources can look calm.
250
+ * - Collapses past `visibleCount` behind a "+N more" toggle.
251
+ * - Empty `sources` states that none were declared, which is itself information.
252
+ */
253
+ const AnswerSources = ({ sources, label = "Sources", emptyLabel = "none declared", visibleCount = 6, footnote, className }) => {
254
+ const [isExpanded, setIsExpanded] = useState(false);
255
+ const isCollapsible = sources.length > visibleCount;
256
+ const shown = isCollapsible && !isExpanded ? sources.slice(0, visibleCount) : sources;
257
+ const unverifiedCount = sources.filter((source) => source.state === "unverified").length;
258
+ return /* @__PURE__ */ jsxs("div", {
259
+ className: cn("tailwindcss-preflight tw:flex tw:flex-col tw:gap-1 tw:text-xs", className),
260
+ children: [/* @__PURE__ */ jsxs("div", {
261
+ className: "tw:flex tw:flex-wrap tw:items-center tw:gap-1.5",
262
+ children: [
263
+ /* @__PURE__ */ jsx("span", {
264
+ className: "tw:font-medium tw:text-muted-foreground",
265
+ children: label
266
+ }),
267
+ sources.length === 0 && /* @__PURE__ */ jsx("span", {
268
+ className: "tw:text-muted-foreground",
269
+ children: emptyLabel
270
+ }),
271
+ shown.map(({ id, ...chip }) => /* @__PURE__ */ jsx(SourceChip, { ...chip }, id)),
272
+ isCollapsible && /* @__PURE__ */ jsx("button", {
273
+ type: "button",
274
+ "aria-expanded": isExpanded,
275
+ onClick: () => setIsExpanded((current) => !current),
276
+ className: "tw:cursor-pointer tw:border-0 tw:bg-transparent tw:p-0 tw:text-xs tw:text-muted-foreground tw:underline tw:hover:text-foreground",
277
+ children: isExpanded ? "Show fewer" : `+${sources.length - visibleCount} more`
278
+ }),
279
+ unverifiedCount > 0 && /* @__PURE__ */ jsxs("span", {
280
+ className: "tw:inline-flex tw:items-center tw:gap-1 tw:text-warning",
281
+ title: "No tool call in this turn touched these: the answer named them, nothing ran on them.",
282
+ children: [
283
+ /* @__PURE__ */ jsx(TriangleAlert, {
284
+ "aria-hidden": true,
285
+ className: "tw:size-3.5"
286
+ }),
287
+ unverifiedCount,
288
+ " unverified"
289
+ ]
290
+ })
291
+ ]
292
+ }), footnote]
293
+ });
110
294
  };
111
295
  //#endregion
112
296
  //#region src/blocks/AIKit/CollapsibleBody.tsx
@@ -185,23 +369,55 @@ const renderVariantIcon = (variant) => {
185
369
  if (variant === "dismissed") return /* @__PURE__ */ jsx(XCircle, { className: "tw:size-3.5 tw:text-muted-foreground/50" });
186
370
  return /* @__PURE__ */ jsx(CircleCheck, { className: "tw:size-3.5 tw:text-muted-foreground/50" });
187
371
  };
188
- const InlineStepLabel = ({ variant, children }) => /* @__PURE__ */ jsxs("div", {
189
- className: "tw:flex tw:min-w-0 tw:items-center tw:gap-1",
190
- children: [/* @__PURE__ */ jsx("div", {
191
- className: "tw:relative tw:flex tw:size-5 tw:shrink-0 tw:items-center tw:justify-center tw:[background:var(--inline-step-surface,transparent)]",
192
- children: renderVariantIcon(variant)
193
- }), /* @__PURE__ */ jsx("div", {
194
- className: "tw:flex tw:min-w-0 tw:flex-wrap tw:items-center tw:gap-x-1.5 tw:gap-y-1 tw:text-xs tw:font-medium tw:text-muted-foreground",
195
- children
196
- })]
197
- });
372
+ const InlineStepLabel = ({ variant, children, meta, detail, defaultExpanded = false }) => {
373
+ const [isExpanded, setIsExpanded] = useState(defaultExpanded);
374
+ const row = /* @__PURE__ */ jsxs("div", {
375
+ className: "tw:flex tw:min-w-0 tw:items-center tw:gap-1",
376
+ children: [/* @__PURE__ */ jsx("div", {
377
+ className: "tw:relative tw:flex tw:size-5 tw:shrink-0 tw:items-center tw:justify-center tw:[background:var(--inline-step-surface,transparent)]",
378
+ children: renderVariantIcon(variant)
379
+ }), /* @__PURE__ */ jsxs("div", {
380
+ className: "tw:flex tw:min-w-0 tw:flex-wrap tw:items-center tw:gap-x-1.5 tw:gap-y-1 tw:text-xs tw:font-medium tw:text-muted-foreground",
381
+ children: [
382
+ children,
383
+ meta && /* @__PURE__ */ jsx("span", {
384
+ className: "tw:font-normal tw:text-muted-foreground/70",
385
+ children: meta
386
+ }),
387
+ detail && /* @__PURE__ */ jsx(Button, {
388
+ variant: "inline",
389
+ size: "small",
390
+ className: "tw:px-0",
391
+ "aria-expanded": isExpanded,
392
+ onClick: () => setIsExpanded((value) => !value),
393
+ children: isExpanded ? "Hide details" : "Details"
394
+ })
395
+ ]
396
+ })]
397
+ });
398
+ if (!detail) return row;
399
+ return /* @__PURE__ */ jsxs("div", {
400
+ className: "tw:flex tw:min-w-0 tw:flex-col tw:gap-1",
401
+ children: [row, isExpanded && /* @__PURE__ */ jsx("div", {
402
+ className: "tw:min-w-0 tw:pl-6 tw:text-xs",
403
+ children: detail
404
+ })]
405
+ });
406
+ };
198
407
  //#endregion
199
408
  //#region src/blocks/AIKit/AnswerSummary.tsx
200
409
  const SECTION_LABELS = {
201
410
  success: "Answered questions",
202
- dismissed: "Dismissed questions"
411
+ dismissed: "Dismissed questions",
412
+ expired: "Expired questions"
203
413
  };
204
- const AnswerSummary = ({ items, variant = "success", noAnswerLabel = "No answer", onExpandToggle }) => {
414
+ const STEP_VARIANT = {
415
+ success: "success",
416
+ dismissed: "dismissed",
417
+ expired: "dismissed"
418
+ };
419
+ const DEFAULT_STATUS_LABEL = { expired: "Expired" };
420
+ const AnswerSummary = ({ items, variant = "success", statusLabel = DEFAULT_STATUS_LABEL[variant], statusMessage, noAnswerLabel = "No answer", onExpandToggle }) => {
205
421
  const [isExpanded, setIsExpanded] = useState(false);
206
422
  const [isClosing, setIsClosing] = useState(false);
207
423
  const toggle = () => {
@@ -212,42 +428,144 @@ const AnswerSummary = ({ items, variant = "success", noAnswerLabel = "No answer"
212
428
  };
213
429
  const count = items.length;
214
430
  const [firstItem] = items;
215
- const label = count === 1 && firstItem ? firstItem.question : `${count} questions`;
431
+ const questionLabel = count === 1 && firstItem ? firstItem.question : `${count} questions`;
432
+ const label = statusLabel ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
433
+ questionLabel,
434
+ " ",
435
+ /* @__PURE__ */ jsx("span", {
436
+ className: "tw:font-normal",
437
+ children: statusLabel
438
+ })
439
+ ] }) : questionLabel;
440
+ const stepVariant = STEP_VARIANT[variant];
216
441
  return /* @__PURE__ */ jsxs("section", {
217
442
  className: "tw:mt-4 tw:flex tw:flex-col",
218
443
  "aria-label": SECTION_LABELS[variant],
219
444
  children: [/* @__PURE__ */ jsx(CollapsibleStepHeader, {
220
445
  isExpanded,
221
446
  onToggle: toggle,
222
- variant,
447
+ variant: stepVariant,
223
448
  label
224
449
  }), /* @__PURE__ */ jsx(CollapsibleBody, {
225
450
  isExpanded,
226
451
  isClosing,
227
452
  onClosed: () => setIsClosing(false),
228
- children: /* @__PURE__ */ jsx("div", {
453
+ children: /* @__PURE__ */ jsxs("div", {
229
454
  className: "tw:flex tw:flex-col tw:gap-4 tw:pt-4 tw:pl-5",
230
- children: items.map(({ question, answer }, index) => /* @__PURE__ */ jsxs("div", {
455
+ children: [statusMessage != null && /* @__PURE__ */ jsx("p", {
456
+ className: "tw:m-0 tw:text-xs tw:text-muted-foreground",
457
+ children: statusMessage
458
+ }), items.map(({ question, answer }, index) => /* @__PURE__ */ jsxs("div", {
231
459
  className: "tw:flex tw:flex-col tw:gap-1",
232
460
  children: [/* @__PURE__ */ jsx(InlineStepLabel, {
233
- variant,
461
+ variant: stepVariant,
234
462
  children: question
235
463
  }), /* @__PURE__ */ jsx(InlineStepDetail, { children: answer && answer.length > 0 ? answer : noAnswerLabel })]
236
- }, `${index}-${question}`))
464
+ }, `${index}-${question}`))]
237
465
  })
238
466
  })]
239
467
  });
240
468
  };
241
469
  //#endregion
242
- //#region src/blocks/AIKit/ChatInlineIcon.tsx
470
+ //#region src/blocks/AIKit/AttachmentChip.tsx
471
+ const Lead = ({ status, thumbnailUrl }) => {
472
+ if (status === "uploading") return /* @__PURE__ */ jsx(LoaderCircle, {
473
+ "aria-hidden": true,
474
+ className: "tw:size-4 tw:shrink-0 tw:animate-spin"
475
+ });
476
+ if (status === "error") return /* @__PURE__ */ jsx(CircleAlert, {
477
+ "aria-hidden": true,
478
+ className: "tw:size-4 tw:shrink-0 tw:text-destructive"
479
+ });
480
+ if (thumbnailUrl) return /* @__PURE__ */ jsx("img", {
481
+ src: thumbnailUrl,
482
+ alt: "",
483
+ className: "tw:size-4 tw:shrink-0 tw:rounded-sm tw:object-cover"
484
+ });
485
+ return /* @__PURE__ */ jsx(FileText, {
486
+ "aria-hidden": true,
487
+ className: "tw:size-4 tw:shrink-0"
488
+ });
489
+ };
243
490
  /**
244
- * The Lucide component each `icon:<name>` token draws.
491
+ * A file attached to a message being composed.
245
492
  *
246
- * - Names are FontAwesome-solid ones, published as `CHAT_INLINE_ICON_NAMES`.
247
- * - The `satisfies` is the drift guard: a name the prompt teaches and this map lacks fails to compile.
248
- * - Read through `Object.hasOwn`: `icon:constructor` would otherwise hit `Object.prototype`.
493
+ * - Capped width with a truncating name, so a long filename cannot push the text away.
494
+ * - `error` explains itself in a tooltip; the remove control stays available in every status.
249
495
  */
496
+ const AttachmentChip = ({ name, thumbnailUrl, status, error, onRemove, trailing, className }) => {
497
+ const chip = /* @__PURE__ */ jsxs("span", {
498
+ "data-status": status,
499
+ className: cn("tailwindcss-preflight tw:inline-flex tw:h-6 tw:min-w-0 tw:max-w-48 tw:items-center tw:gap-1 tw:rounded-md tw:bg-muted tw:px-2 tw:text-xs tw:text-muted-foreground", status === "error" && "tw:text-destructive", className),
500
+ children: [
501
+ /* @__PURE__ */ jsx(Lead, {
502
+ status,
503
+ thumbnailUrl
504
+ }),
505
+ /* @__PURE__ */ jsx("span", {
506
+ className: "tw:min-w-0 tw:truncate",
507
+ children: name
508
+ }),
509
+ trailing,
510
+ /* @__PURE__ */ jsx("button", {
511
+ type: "button",
512
+ "aria-label": `Remove ${name}`,
513
+ onClick: onRemove,
514
+ className: "tw:-mr-1 tw:shrink-0 tw:cursor-pointer tw:rounded-sm tw:border-0 tw:bg-transparent tw:p-0 tw:text-muted-foreground tw:hover:text-foreground tw:focus-visible:outline-none tw:focus-visible:ring-2 tw:focus-visible:ring-ring",
515
+ children: /* @__PURE__ */ jsx(X, { className: "tw:size-3.5" })
516
+ })
517
+ ]
518
+ });
519
+ if (status !== "error" || !error) return chip;
520
+ return /* @__PURE__ */ jsx(Tooltip, {
521
+ tooltip: error,
522
+ placement: "top",
523
+ triggerAsChild: true,
524
+ children: chip
525
+ });
526
+ };
527
+ //#endregion
528
+ //#region src/blocks/AIKit/ChatInlineIcon.tsx
529
+ /** Every drawable name; read through `Object.hasOwn`, or `icon:constructor` hits `Object.prototype`. */
250
530
  const ICON_REGISTRY = {
531
+ "book-open": BookOpen,
532
+ bookmark: Bookmark,
533
+ box: Box,
534
+ "chart-bar": ChartBar,
535
+ "chart-pie": ChartPie,
536
+ "circle-alert": CircleAlert,
537
+ "circle-help": CircleHelp,
538
+ "circle-x": CircleX,
539
+ cloud: Cloud,
540
+ "external-link": ExternalLink,
541
+ "file-text": FileText,
542
+ filter: Filter,
543
+ "git-branch": GitBranch,
544
+ "hard-drive": HardDrive,
545
+ history: History,
546
+ hourglass: Hourglass,
547
+ info: Info,
548
+ layers: Layers,
549
+ lightbulb: Lightbulb,
550
+ list: List,
551
+ "list-checks": ListChecks,
552
+ mail: Mail,
553
+ package: Package,
554
+ pause: Pause,
555
+ pencil: Pencil,
556
+ pin: Pin,
557
+ "refresh-cw": RefreshCw,
558
+ server: Server,
559
+ settings: Settings,
560
+ shield: Shield,
561
+ "trash-2": Trash2,
562
+ "trending-down": TrendingDown,
563
+ "trending-up": TrendingUp,
564
+ "triangle-alert": TriangleAlert,
565
+ users: Users,
566
+ workflow: Workflow,
567
+ wrench: Wrench,
568
+ x: X,
251
569
  "alert-circle": AlertCircle,
252
570
  "arrow-down": ArrowDown,
253
571
  "arrow-left": ArrowLeft,
@@ -390,26 +708,153 @@ const ChatInlineIcon = ({ name }) => {
390
708
  });
391
709
  };
392
710
  //#endregion
711
+ //#region src/blocks/AIKit/useCountdown.ts
712
+ const TICK_MS = 1e3;
713
+ /** Whole seconds left of a countdown that starts at `seconds`; `undefined` when there is none. */
714
+ const useCountdown = (seconds) => {
715
+ const [ticks, setTicks] = useState({
716
+ start: seconds,
717
+ elapsed: 0
718
+ });
719
+ useEffect(() => {
720
+ if (seconds === void 0 || seconds <= 0) return void 0;
721
+ const interval = setInterval(() => {
722
+ setTicks((current) => ({
723
+ start: seconds,
724
+ elapsed: (current.start === seconds ? current.elapsed : 0) + 1
725
+ }));
726
+ }, TICK_MS);
727
+ return () => clearInterval(interval);
728
+ }, [seconds]);
729
+ if (seconds === void 0) return void 0;
730
+ const elapsed = ticks.start === seconds ? ticks.elapsed : 0;
731
+ return Math.max(seconds - elapsed, 0);
732
+ };
733
+ //#endregion
734
+ //#region src/blocks/AIKit/ChatNotice.tsx
735
+ const ALERT_VARIANT = {
736
+ error: "error",
737
+ warn: "warning",
738
+ info: "info"
739
+ };
740
+ const countdownLabel = (remaining) => remaining > 0 ? `Try again in ${remaining}s.` : "You can try again now.";
741
+ /**
742
+ * A notice row in a chat transcript.
743
+ *
744
+ * - Thin wrapper over `Alert`; `warn` maps to the warning variant.
745
+ * - `details` stays collapsed so a stack trace never crowds the message.
746
+ * - `retryAfter` appends a live countdown; `action` is always rendered, so the host decides whether to gate it.
747
+ */
748
+ const ChatNotice = ({ tone, children, action, details, retryAfter, className }) => {
749
+ const remaining = useCountdown(retryAfter);
750
+ return /* @__PURE__ */ jsx(Alert, {
751
+ variant: ALERT_VARIANT[tone],
752
+ role: "status",
753
+ className,
754
+ children: /* @__PURE__ */ jsxs("div", {
755
+ className: "tw:flex tw:flex-wrap tw:items-start tw:gap-x-3 tw:gap-y-2",
756
+ children: [/* @__PURE__ */ jsxs("div", {
757
+ className: "tw:min-w-0 tw:flex-1 tw:basis-40",
758
+ children: [
759
+ /* @__PURE__ */ jsx("span", { children }),
760
+ remaining !== void 0 && /* @__PURE__ */ jsx("span", {
761
+ className: "tw:ml-1 tw:text-muted-foreground",
762
+ "data-testid": "chat-notice-countdown",
763
+ children: countdownLabel(remaining)
764
+ }),
765
+ details && /* @__PURE__ */ jsxs(Collapsible, {
766
+ className: "tw:mt-1",
767
+ children: [/* @__PURE__ */ jsxs(Collapsible.Trigger, {
768
+ className: "tailwindcss-preflight tw:group tw:flex tw:cursor-pointer tw:items-center tw:gap-1 tw:border-0 tw:bg-transparent tw:p-0 tw:text-xs tw:text-muted-foreground tw:hover:text-foreground",
769
+ children: ["Technical details", /* @__PURE__ */ jsx(ChevronDown, { className: "tw:size-3 tw:transition-transform tw:group-data-[state=open]:rotate-180" })]
770
+ }), /* @__PURE__ */ jsx(Collapsible.Content, { children: /* @__PURE__ */ jsx("code", {
771
+ className: "tw:mt-1 tw:block tw:whitespace-pre-wrap tw:break-words tw:text-xs",
772
+ children: details
773
+ }) })]
774
+ })
775
+ ]
776
+ }), action && /* @__PURE__ */ jsx("div", {
777
+ className: "tw:ml-auto tw:flex tw:shrink-0 tw:gap-2",
778
+ children: action
779
+ })]
780
+ })
781
+ });
782
+ };
783
+ //#endregion
784
+ //#region src/blocks/AIKit/ContextChip.tsx
785
+ const CONTEXT_CHIP_EXPANDABLE_LENGTH = 120;
786
+ /**
787
+ * A quote or reference attached to a message being composed.
788
+ *
789
+ * - The excerpt is clamped to two lines; "Show all" appears once it is long enough to be cut.
790
+ * - Images sit under the excerpt as small thumbnails.
791
+ */
792
+ const ContextChip = ({ label, excerpt, images = [], onRemove, className }) => {
793
+ const [isExpanded, setIsExpanded] = useState(false);
794
+ const isExpandable = excerpt.length > 120;
795
+ return /* @__PURE__ */ jsxs("div", {
796
+ className: cn("tailwindcss-preflight tw:flex tw:w-full tw:min-w-0 tw:flex-col tw:gap-1 tw:rounded-md tw:border-0 tw:border-l-2 tw:border-solid tw:border-border tw:bg-muted tw:px-3 tw:py-2 tw:text-xs", className),
797
+ children: [
798
+ /* @__PURE__ */ jsxs("div", {
799
+ className: "tw:flex tw:items-center tw:gap-1 tw:text-muted-foreground",
800
+ children: [
801
+ /* @__PURE__ */ jsx(Quote, {
802
+ "aria-hidden": true,
803
+ className: "tw:size-3.5 tw:shrink-0"
804
+ }),
805
+ /* @__PURE__ */ jsx("span", {
806
+ className: "tw:min-w-0 tw:flex-1 tw:truncate tw:font-medium",
807
+ children: label
808
+ }),
809
+ /* @__PURE__ */ jsx("button", {
810
+ type: "button",
811
+ "aria-label": `Remove ${label}`,
812
+ onClick: onRemove,
813
+ className: "tw:shrink-0 tw:cursor-pointer tw:rounded-sm tw:border-0 tw:bg-transparent tw:p-0 tw:text-muted-foreground tw:hover:text-foreground tw:focus-visible:outline-none tw:focus-visible:ring-2 tw:focus-visible:ring-ring",
814
+ children: /* @__PURE__ */ jsx(X, { className: "tw:size-3.5" })
815
+ })
816
+ ]
817
+ }),
818
+ /* @__PURE__ */ jsx("p", {
819
+ className: cn("tw:m-0 tw:whitespace-pre-wrap tw:break-words tw:text-foreground", !isExpanded && "tw:line-clamp-2"),
820
+ children: excerpt
821
+ }),
822
+ isExpandable && /* @__PURE__ */ jsx("button", {
823
+ type: "button",
824
+ "aria-expanded": isExpanded,
825
+ onClick: () => setIsExpanded((current) => !current),
826
+ className: "tw:w-fit tw:cursor-pointer tw:border-0 tw:bg-transparent tw:p-0 tw:text-xs tw:text-muted-foreground tw:underline tw:hover:text-foreground",
827
+ children: isExpanded ? "Show less" : "Show all"
828
+ }),
829
+ images.length > 0 && /* @__PURE__ */ jsx("div", {
830
+ className: "tw:flex tw:flex-wrap tw:gap-1",
831
+ children: images.map((image) => /* @__PURE__ */ jsx("img", {
832
+ src: image.src,
833
+ alt: image.alt,
834
+ className: "tw:size-10 tw:rounded-sm tw:border tw:border-border tw:object-cover"
835
+ }, image.src))
836
+ })
837
+ ]
838
+ });
839
+ };
840
+ //#endregion
393
841
  //#region src/blocks/AIKit/FileDownloadChip.tsx
394
842
  /**
395
843
  * A file the agent wrote, offered inside its own answer.
396
844
  *
397
- * - The whole chip is the control: a small icon beside a truncating filename is hard to hit.
845
+ * - By default the whole chip is the control: a small icon beside a truncating filename is hard to hit.
846
+ * - With `onPreview` or `actions` the chip splits: the name previews (or downloads, without
847
+ * `onPreview`), the download sits beside it, and `actions` follow.
398
848
  * - Presentation only — the host fetches and saves, through its own auth.
399
849
  */
400
- const FileDownloadChip = ({ fileName, filePath, size, isPending = false, onDownload, className }) => /* @__PURE__ */ jsxs(Button, {
401
- variant: "secondary",
402
- size: "small",
403
- isLoading: isPending,
404
- "aria-disabled": isPending,
405
- "aria-label": `Download ${filePath ?? fileName}`,
406
- title: filePath,
407
- onClick: () => {
850
+ const FileDownloadChip = ({ fileName, filePath, size, isPending = false, onDownload, onPreview, actions, className }) => {
851
+ const isSplit = onPreview !== void 0 || actions !== void 0;
852
+ const target = filePath ?? fileName;
853
+ const handleDownload = () => {
408
854
  if (!isPending) onDownload();
409
- },
410
- className: cn("tw:max-w-full tw:justify-start tw:gap-2 tw:font-normal", className),
411
- children: [
412
- !isPending && /* @__PURE__ */ jsx(FileText, { className: "tw:size-4 tw:shrink-0 tw:text-muted-foreground" }),
855
+ };
856
+ const nameAndSize = /* @__PURE__ */ jsxs(Fragment$1, { children: [
857
+ /* @__PURE__ */ jsx(FileText, { className: "tw:size-4 tw:shrink-0 tw:text-muted-foreground" }),
413
858
  /* @__PURE__ */ jsx("span", {
414
859
  className: "tw:truncate",
415
860
  children: fileName
@@ -417,10 +862,148 @@ const FileDownloadChip = ({ fileName, filePath, size, isPending = false, onDownl
417
862
  size !== void 0 && /* @__PURE__ */ jsx("span", {
418
863
  className: "tw:shrink-0 tw:text-xs tw:text-muted-foreground",
419
864
  children: size
420
- }),
421
- !isPending && /* @__PURE__ */ jsx(Download, { className: "tw:size-4 tw:shrink-0" })
422
- ]
423
- });
865
+ })
866
+ ] });
867
+ if (!isSplit) return /* @__PURE__ */ jsxs(Button, {
868
+ variant: "secondary",
869
+ size: "small",
870
+ isLoading: isPending,
871
+ "aria-disabled": isPending,
872
+ "aria-label": `Download ${target}`,
873
+ title: filePath,
874
+ onClick: handleDownload,
875
+ className: cn("tw:max-w-full tw:justify-start tw:gap-2 tw:font-normal", className),
876
+ children: [nameAndSize, !isPending && /* @__PURE__ */ jsx(Download, { className: "tw:size-4 tw:shrink-0" })]
877
+ });
878
+ return /* @__PURE__ */ jsxs(ButtonGroup, {
879
+ space: "compact",
880
+ className: cn("tw:max-w-full", className),
881
+ children: [
882
+ onPreview ? /* @__PURE__ */ jsx(Button, {
883
+ variant: "secondary",
884
+ size: "small",
885
+ "aria-label": `Preview ${target}`,
886
+ title: filePath,
887
+ onClick: onPreview,
888
+ className: "tw:min-w-0 tw:!shrink tw:justify-start tw:gap-2 tw:overflow-hidden tw:font-normal",
889
+ children: nameAndSize
890
+ }) : null,
891
+ /* @__PURE__ */ jsxs(Button, {
892
+ variant: "secondary",
893
+ size: onPreview ? "icon-sm" : "small",
894
+ isLoading: isPending,
895
+ "aria-disabled": isPending,
896
+ "aria-label": `Download ${target}`,
897
+ title: onPreview ? void 0 : filePath,
898
+ onClick: handleDownload,
899
+ className: cn(onPreview ? "tw:shrink-0" : "tw:min-w-0 tw:!shrink tw:justify-start tw:gap-2 tw:overflow-hidden tw:font-normal"),
900
+ children: [onPreview ? !isPending && /* @__PURE__ */ jsx(Download, { className: "tw:size-4" }) : nameAndSize, !onPreview && !isPending && /* @__PURE__ */ jsx(Download, { className: "tw:size-4 tw:shrink-0" })]
901
+ }),
902
+ actions
903
+ ]
904
+ });
905
+ };
906
+ //#endregion
907
+ //#region src/blocks/AIKit/FileGalleryTile.tsx
908
+ const VISIBLE_MARGIN = "200px";
909
+ /**
910
+ * One image in a `FileGallery`.
911
+ *
912
+ * - Reports itself visible once, 200px before it scrolls in; without an `IntersectionObserver`
913
+ * (jsdom) it reports on mount.
914
+ * - A thumbnail that fails to decode counts as failed, and a press retries instead of opening.
915
+ */
916
+ const FileGalleryTile = ({ image, onOpen, onRetry, onVisible }) => {
917
+ const [tile, setTile] = useState(null);
918
+ const [isBroken, setIsBroken] = useState(false);
919
+ const isFailed = image.isFailed === true || isBroken;
920
+ const isLoading = !isFailed && image.src === void 0;
921
+ const hasVisibleHandler = onVisible !== void 0;
922
+ const reportVisible = useStableCallback(() => onVisible?.(image));
923
+ useEffect(() => {
924
+ if (!tile || !hasVisibleHandler) return;
925
+ if (typeof IntersectionObserver === "undefined") {
926
+ reportVisible();
927
+ return;
928
+ }
929
+ const observer = new IntersectionObserver((entries) => {
930
+ if (!entries.some((entry) => entry.isIntersecting)) return;
931
+ observer.disconnect();
932
+ reportVisible();
933
+ }, { rootMargin: VISIBLE_MARGIN });
934
+ observer.observe(tile);
935
+ return () => observer.disconnect();
936
+ }, [
937
+ tile,
938
+ hasVisibleHandler,
939
+ reportVisible
940
+ ]);
941
+ const handleClick = () => {
942
+ if (!isFailed) return onOpen(image);
943
+ setIsBroken(false);
944
+ onRetry?.(image);
945
+ };
946
+ return /* @__PURE__ */ jsxs(Button, {
947
+ ref: setTile,
948
+ variant: "invisible",
949
+ "aria-label": isFailed ? `Retry preview of ${image.name}` : image.name,
950
+ onClick: handleClick,
951
+ className: cn("tw:relative tw:aspect-square tw:!h-auto tw:!max-h-none tw:!min-h-0 tw:!w-full tw:overflow-hidden tw:rounded-md tw:border tw:border-border tw:bg-muted tw:p-0", isFailed && "tw:text-xs tw:text-muted-foreground"),
952
+ children: [
953
+ isLoading && /* @__PURE__ */ jsx("span", {
954
+ "aria-hidden": true,
955
+ className: "tw:absolute tw:inset-0 tw:animate-pulse tw:bg-muted-foreground/10"
956
+ }),
957
+ isFailed && /* @__PURE__ */ jsx("span", {
958
+ className: "tw:px-2 tw:text-center tw:whitespace-normal",
959
+ children: "Preview unavailable — retry"
960
+ }),
961
+ !isFailed && image.src !== void 0 && /* @__PURE__ */ jsx("img", {
962
+ src: image.src,
963
+ alt: image.name,
964
+ loading: "lazy",
965
+ decoding: "async",
966
+ onError: () => setIsBroken(true),
967
+ className: "tw:size-full tw:object-cover"
968
+ })
969
+ ]
970
+ });
971
+ };
972
+ //#endregion
973
+ //#region src/blocks/AIKit/FileGallery.tsx
974
+ const DEFAULT_MAX_TILES = 12;
975
+ /**
976
+ * Image files the agent wrote, as a grid of thumbnails.
977
+ *
978
+ * - Presentation only: the host owns thumbnail URLs, their loading and their auth.
979
+ * - Tiles ask for their thumbnail as they near the viewport, so a long gallery loads on scroll.
980
+ * - Draws `maxTiles`, then a "+N more" button that reveals the rest.
981
+ */
982
+ const FileGallery = ({ images, onOpen, onRetry, onImageVisible, maxTiles = DEFAULT_MAX_TILES, className }) => {
983
+ const [isShowingAll, setIsShowingAll] = useState(false);
984
+ const shown = isShowingAll ? images : images.slice(0, maxTiles);
985
+ const hiddenCount = images.length - shown.length;
986
+ if (images.length === 0) return null;
987
+ return /* @__PURE__ */ jsxs("div", {
988
+ role: "group",
989
+ "aria-label": "Images",
990
+ className: className ?? "tw:grid tw:grid-cols-[repeat(auto-fill,minmax(6rem,1fr))] tw:gap-2",
991
+ children: [shown.map((image) => /* @__PURE__ */ jsx(FileGalleryTile, {
992
+ image,
993
+ onOpen,
994
+ onRetry,
995
+ onVisible: onImageVisible
996
+ }, image.id)), hiddenCount > 0 && /* @__PURE__ */ jsxs(Button, {
997
+ variant: "secondary",
998
+ onClick: () => setIsShowingAll(true),
999
+ children: [
1000
+ "+",
1001
+ hiddenCount,
1002
+ " more"
1003
+ ]
1004
+ })]
1005
+ });
1006
+ };
424
1007
  //#endregion
425
1008
  //#region src/blocks/AIKit/InlineStepGroup.tsx
426
1009
  /** The surface colour, as the custom property the step icons mask against. */
@@ -433,7 +1016,7 @@ const surfaceStyle = (surface) => ({ "--inline-step-surface": surface });
433
1016
  * - Animates `grid-template-rows`; children stay mounted while closed.
434
1017
  * - `role="group"`, not a `region` landmark: dozens per transcript would drown real landmarks.
435
1018
  */
436
- const InlineStepGroup = ({ label, variant = "success", isExpanded, onToggle, surface = "rgb(var(--background))", "aria-label": ariaLabel, "data-testid": testId, className, children }) => /* @__PURE__ */ jsxs("section", {
1019
+ const InlineStepGroup = ({ label, variant = "success", isExpanded, onToggle, surface = "rgb(var(--background))", "aria-label": ariaLabel, "data-testid": testId, className, footer, children }) => /* @__PURE__ */ jsxs("section", {
437
1020
  role: "group",
438
1021
  className: cn("tw:flex tw:flex-col", className),
439
1022
  "aria-label": ariaLabel,
@@ -452,15 +1035,42 @@ const InlineStepGroup = ({ label, variant = "success", isExpanded, onToggle, sur
452
1035
  children: /* @__PURE__ */ jsxs("div", {
453
1036
  style: surfaceStyle(surface),
454
1037
  className: "tw:relative tw:flex tw:flex-col tw:gap-4 tw:pt-4 tw:pl-5",
455
- children: [/* @__PURE__ */ jsx("span", {
456
- "aria-hidden": "true",
457
- className: "tw:pointer-events-none tw:absolute tw:top-4 tw:bottom-0 tw:left-[30px] tw:w-px tw:bg-border"
458
- }), children]
1038
+ children: [
1039
+ /* @__PURE__ */ jsx("span", {
1040
+ "aria-hidden": "true",
1041
+ className: "tw:pointer-events-none tw:absolute tw:top-4 tw:bottom-0 tw:left-[30px] tw:w-px tw:bg-border"
1042
+ }),
1043
+ children,
1044
+ footer && /* @__PURE__ */ jsx("div", {
1045
+ className: "tw:relative tw:pl-6 tw:[background:var(--inline-step-surface,transparent)]",
1046
+ children: footer
1047
+ })
1048
+ ]
459
1049
  })
460
1050
  })
461
1051
  })]
462
1052
  });
463
1053
  //#endregion
1054
+ //#region src/blocks/AIKit/JumpToLatest.tsx
1055
+ /**
1056
+ * Scrolls a transcript back to its newest message.
1057
+ *
1058
+ * - Renders nothing while `visible` is false.
1059
+ * - Positioning is the host's: pass `className` to pin it over the list.
1060
+ */
1061
+ const JumpToLatest = ({ visible, working = false, onClick, label, className }) => {
1062
+ if (!visible) return null;
1063
+ return /* @__PURE__ */ jsxs(Button, {
1064
+ variant: "outline",
1065
+ size: label ? "small" : "icon-sm",
1066
+ "aria-label": label ?? "Scroll to bottom",
1067
+ "data-working": working,
1068
+ onClick,
1069
+ className: cn("tw:rounded-full tw:shadow-md", working && "tw:animate-pulse tw:motion-reduce:animate-none", className),
1070
+ children: [/* @__PURE__ */ jsx(ArrowDown, { className: "tw:size-4" }), label]
1071
+ });
1072
+ };
1073
+ //#endregion
464
1074
  //#region src/blocks/AIKit/DiagramBlock.tsx
465
1075
  /**
466
1076
  * One diagram block: a skeleton while its chunk loads, the fence source plus a retry if it fails.
@@ -470,7 +1080,7 @@ const InlineStepGroup = ({ label, variant = "success", isExpanded, onToggle, sur
470
1080
  * - A new `load` resets the block, or a dialect switch keeps the previous renderer.
471
1081
  * - Functions in state go through the setter form, or React calls them.
472
1082
  */
473
- const DiagramBlock = ({ load, code }) => {
1083
+ const DiagramBlock = ({ load, code, renderActions }) => {
474
1084
  const [Diagram, setDiagram] = useState(null);
475
1085
  const [failed, setFailed] = useState(false);
476
1086
  const [attempt, setAttempt] = useState(0);
@@ -491,7 +1101,10 @@ const DiagramBlock = ({ load, code }) => {
491
1101
  cancelled = true;
492
1102
  };
493
1103
  }, [load, attempt]);
494
- if (Diagram) return /* @__PURE__ */ jsx(Diagram, { code });
1104
+ if (Diagram) return /* @__PURE__ */ jsx(Diagram, {
1105
+ code,
1106
+ renderActions
1107
+ });
495
1108
  if (failed) return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(DiagramSource, { code }), /* @__PURE__ */ jsx(Button, {
496
1109
  variant: "inline",
497
1110
  size: "small",
@@ -526,6 +1139,622 @@ const diagramChunk = (name, load) => {
526
1139
  };
527
1140
  };
528
1141
  //#endregion
1142
+ //#region src/blocks/AIKit/downloadSvgFile.ts
1143
+ /** How long the object URL outlives the click; the download has started by then. */
1144
+ const REVOKE_DELAY_MS = 1e3;
1145
+ /** Saves sanitized SVG markup as a standalone `.svg` file. */
1146
+ const downloadSvgFile = (svg, filename = "diagram.svg") => {
1147
+ const blob = new Blob([toSvgFileContent(svg)], { type: "image/svg+xml;charset=utf-8" });
1148
+ const url = URL.createObjectURL(blob);
1149
+ const anchor = document.createElement("a");
1150
+ anchor.href = url;
1151
+ anchor.download = filename;
1152
+ document.body.append(anchor);
1153
+ anchor.click();
1154
+ anchor.remove();
1155
+ setTimeout(() => URL.revokeObjectURL(url), REVOKE_DELAY_MS);
1156
+ };
1157
+ //#endregion
1158
+ //#region src/blocks/AIKit/DiagramDownloadButton.tsx
1159
+ /** A diagram toolbar action that saves the drawn diagram as an SVG file. */
1160
+ const DiagramDownloadButton = ({ safeSvg }) => /* @__PURE__ */ jsx(Tooltip, {
1161
+ tooltip: "Download SVG",
1162
+ placement: "top",
1163
+ triggerAsChild: true,
1164
+ children: /* @__PURE__ */ jsx(Button, {
1165
+ size: "icon-sm",
1166
+ variant: "outline",
1167
+ "aria-label": "Download SVG",
1168
+ onClick: () => downloadSvgFile(safeSvg),
1169
+ children: /* @__PURE__ */ jsx(Download, { className: "tw:size-4" })
1170
+ })
1171
+ });
1172
+ //#endregion
1173
+ //#region src/blocks/AIKit/highlightChunk.ts
1174
+ let pending = null;
1175
+ /**
1176
+ * The syntax highlighter, loaded on first use and shared between blocks.
1177
+ *
1178
+ * - The parsers are heavy, so they stay out of the main chunk.
1179
+ * - A failed import is logged and resolves `null`; the next block retries.
1180
+ */
1181
+ const loadHighlighter = () => {
1182
+ pending ??= import("./highlightCode-D354tCXO.js").then((module) => module.highlightCode, (error) => {
1183
+ console.error("[AIKit] failed to load the syntax highlighter chunk", error);
1184
+ pending = null;
1185
+ return null;
1186
+ });
1187
+ return pending;
1188
+ };
1189
+ //#endregion
1190
+ //#region src/blocks/AIKit/HighlightedCode.tsx
1191
+ /**
1192
+ * A fenced block's `<code>`, coloured once the highlighter chunk loads.
1193
+ *
1194
+ * - Functions in state go through the setter form, or React calls them.
1195
+ */
1196
+ const HighlightedCode = ({ code, grammar, className, children }) => {
1197
+ const [highlight, setHighlight] = useState(null);
1198
+ useEffect(() => {
1199
+ let cancelled = false;
1200
+ loadHighlighter().then((loaded) => {
1201
+ if (!cancelled && loaded) setHighlight(() => loaded);
1202
+ });
1203
+ return () => {
1204
+ cancelled = true;
1205
+ };
1206
+ }, []);
1207
+ const tokens = useMemo(() => highlight?.(code, grammar), [
1208
+ highlight,
1209
+ code,
1210
+ grammar
1211
+ ]);
1212
+ if (!tokens) return children;
1213
+ return /* @__PURE__ */ jsx("code", {
1214
+ className,
1215
+ children: tokens.map((token, index) => token.className ? /* @__PURE__ */ jsx("span", {
1216
+ className: token.className,
1217
+ children: token.text
1218
+ }, index) : token.text)
1219
+ });
1220
+ };
1221
+ //#endregion
1222
+ //#region src/blocks/AIKit/highlightLanguages.ts
1223
+ /**
1224
+ * Fence language → the grammar that highlights it.
1225
+ *
1226
+ * - Read via `Object.hasOwn`: ```constructor would otherwise hit `Object.prototype`.
1227
+ */
1228
+ const HIGHLIGHT_GRAMMARS = {
1229
+ sql: "sql",
1230
+ snowflake: "sql",
1231
+ sfsql: "sql",
1232
+ python: "python",
1233
+ py: "python",
1234
+ js: "javascript",
1235
+ javascript: "javascript",
1236
+ jsx: "jsx",
1237
+ ts: "typescript",
1238
+ typescript: "typescript",
1239
+ tsx: "tsx",
1240
+ json: "json",
1241
+ sh: "shell",
1242
+ bash: "shell",
1243
+ shell: "shell",
1244
+ zsh: "shell",
1245
+ yaml: "yaml",
1246
+ yml: "yaml"
1247
+ };
1248
+ /** The grammar for a fence language, or `undefined` when the block stays plain. */
1249
+ const getHighlightGrammar = (language) => {
1250
+ const key = language?.toLowerCase();
1251
+ return key !== void 0 && Object.hasOwn(HIGHLIGHT_GRAMMARS, key) ? HIGHLIGHT_GRAMMARS[key] : void 0;
1252
+ };
1253
+ //#endregion
1254
+ //#region src/blocks/AIKit/markdownOptions.ts
1255
+ const MarkdownOptionsContext = createContext({
1256
+ highlight: false,
1257
+ sortable: false,
1258
+ renderDiagramActions: void 0
1259
+ });
1260
+ //#endregion
1261
+ //#region src/blocks/AIKit/tableSort.ts
1262
+ /** Characters a number cell carries for the reader: grouping, currency, percent, sign, spaces. */
1263
+ const NUMBER_NOISE = /[\s,_%$€£¥+]/g;
1264
+ /** A trailing magnitude: `142k`, `1.2M`, `3B`. */
1265
+ const MAGNITUDE = /^(.*?)([kmb])$/i;
1266
+ const MAGNITUDES = {
1267
+ k: 1e3,
1268
+ m: 1e6,
1269
+ b: 1e9
1270
+ };
1271
+ /** A cell's number, or `null` when it is not one: `1,234`, `−5.1%`, `€142` and `1.2M` all read. */
1272
+ const parseSortNumber = (text) => {
1273
+ const cleaned = text.replace(NUMBER_NOISE, "").replace(/^\u2212/, "-");
1274
+ const [, digits = cleaned, suffix = ""] = MAGNITUDE.exec(cleaned) ?? [];
1275
+ if (digits === "" || /[^\d.eE-]/.test(digits)) return null;
1276
+ const value = Number(digits) * (MAGNITUDES[suffix.toLowerCase()] ?? 1);
1277
+ return Number.isFinite(value) ? value : null;
1278
+ };
1279
+ /** Cells a model writes for "no value"; they sort with the empty ones. */
1280
+ const PLACEHOLDER = /^(?:n\/?a|-+|[\u2013\u2014\u2026?]|none|null)$/i;
1281
+ const isBlank = (key) => key === "" || PLACEHOLDER.test(key);
1282
+ const collator = new Intl.Collator(void 0, {
1283
+ numeric: true,
1284
+ sensitivity: "base"
1285
+ });
1286
+ /**
1287
+ * Row indices in the order one column sorts them.
1288
+ *
1289
+ * - Numeric when every non-empty cell parses as a number; otherwise locale-aware text.
1290
+ * - Empty and placeholder cells (`n/a`, `—`) go last in both directions; ties keep source order.
1291
+ */
1292
+ const sortRowOrder = (cells, direction) => {
1293
+ const keys = cells.map((cell) => cell.trim());
1294
+ const numbers = keys.map(parseSortNumber);
1295
+ const filled = keys.flatMap((key, index) => isBlank(key) ? [] : [index]);
1296
+ const empty = keys.flatMap((key, index) => isBlank(key) ? [index] : []);
1297
+ const numeric = filled.every((index) => numbers[index] !== null);
1298
+ const compare = (a, b) => numeric ? (numbers[a] ?? 0) - (numbers[b] ?? 0) : collator.compare(keys[a] ?? "", keys[b] ?? "");
1299
+ const sorted = filled.toSorted(compare);
1300
+ return [...direction === "ascending" ? sorted : sorted.toReversed(), ...empty];
1301
+ };
1302
+ /** The sort after a header press: a new column sorts ascending, the same one flips. */
1303
+ const nextSort = (current, column) => current?.column === column && current.direction === "ascending" ? {
1304
+ column,
1305
+ direction: "descending"
1306
+ } : {
1307
+ column,
1308
+ direction: "ascending"
1309
+ };
1310
+ const getHastText = (node) => {
1311
+ if (node.type === "text") return node.value;
1312
+ if (node.type === "element") return node.children.map(getHastText).join("");
1313
+ return "";
1314
+ };
1315
+ /** A row's cells, read off its hast node; `[]` without one. */
1316
+ const getRowCells = (row) => row?.children.filter((child) => child.type === "element" && /^t[dh]$/.test(child.tagName)) ?? [];
1317
+ /** The text of a row's cell at `column`; `''` when the row is short. */
1318
+ const getRowCellText = (row, column) => {
1319
+ const cell = getRowCells(row)[column];
1320
+ return cell ? getHastText(cell) : "";
1321
+ };
1322
+ //#endregion
1323
+ //#region src/blocks/AIKit/tableSortContext.ts
1324
+ /** The sort of the table being rendered; `null` outside a sortable table. */
1325
+ const TableSortContext = createContext(null);
1326
+ /** The hast cells of the row being rendered; a cell finds its column by identity. */
1327
+ const RowCellsContext = createContext([]);
1328
+ //#endregion
1329
+ //#region src/blocks/AIKit/TableSortProvider.tsx
1330
+ /** Holds one table's sort; the `Markdown` header, row and body renderers read it. */
1331
+ const TableSortProvider = ({ children }) => {
1332
+ const [sort, setSort] = useState(null);
1333
+ const value = useMemo(() => ({
1334
+ sort,
1335
+ toggle: (column) => setSort((current) => nextSort(current, column))
1336
+ }), [sort]);
1337
+ return /* @__PURE__ */ jsx(TableSortContext, {
1338
+ value,
1339
+ children
1340
+ });
1341
+ };
1342
+ //#endregion
1343
+ //#region src/blocks/AIKit/MarkdownGrid.tsx
1344
+ /** The `table` renderer; a sortable block gives each table its own sort. */
1345
+ const MarkdownGrid = (props) => {
1346
+ const { className, node: _node, ...rest } = props;
1347
+ const { sortable } = use(MarkdownOptionsContext);
1348
+ const grid = /* @__PURE__ */ jsx("div", {
1349
+ className: "tw:not-prose tw:my-4",
1350
+ children: /* @__PURE__ */ jsx(Table$1, {
1351
+ bordered: true,
1352
+ className: cn("tw:!mb-0", className),
1353
+ ...rest
1354
+ })
1355
+ });
1356
+ return sortable ? /* @__PURE__ */ jsx(TableSortProvider, { children: grid }) : grid;
1357
+ };
1358
+ //#endregion
1359
+ //#region src/blocks/AIKit/MarkdownTableBody.tsx
1360
+ /**
1361
+ * The body of an answer's table, in the order its sorted column puts the rows.
1362
+ *
1363
+ * - react-markdown hands rows over as a keyed array, each with its hast `node`.
1364
+ * - Rows keep their keys, so a sort moves them rather than remounting them.
1365
+ */
1366
+ const MarkdownTableBody = (props) => {
1367
+ const { children, node: _node, ...rest } = props;
1368
+ const sort = use(TableSortContext)?.sort ?? null;
1369
+ if (sort === null || !Array.isArray(children)) return /* @__PURE__ */ jsx(Table$1.Body, {
1370
+ ...rest,
1371
+ children
1372
+ });
1373
+ const rows = children.filter(isValidElement);
1374
+ const order = sortRowOrder(rows.map((row) => getRowCellText(row.props.node, sort.column)), sort.direction);
1375
+ return /* @__PURE__ */ jsx(Table$1.Body, {
1376
+ ...rest,
1377
+ children: order.map((index) => rows[index])
1378
+ });
1379
+ };
1380
+ //#endregion
1381
+ //#region src/blocks/AIKit/MarkdownTableHead.tsx
1382
+ /**
1383
+ * A header cell in an answer's table; a sort control inside a sortable table.
1384
+ *
1385
+ * - The button sorts on click, Enter and Space; `aria-sort` sits on the cell.
1386
+ */
1387
+ const MarkdownTableHead = (props) => {
1388
+ const { children, node, ...rest } = props;
1389
+ const sorting = use(TableSortContext);
1390
+ const column = node ? use(RowCellsContext).indexOf(node) : -1;
1391
+ if (sorting === null || column === -1) return /* @__PURE__ */ jsx(Table$1.Head, {
1392
+ ...rest,
1393
+ children
1394
+ });
1395
+ const { sort, toggle } = sorting;
1396
+ const sorted = sort?.column !== column ? false : sort.direction === "ascending" ? "asc" : "desc";
1397
+ return /* @__PURE__ */ jsx(Table$1.Head, {
1398
+ "aria-sort": tableAriaSort(sorted),
1399
+ ...rest,
1400
+ children: /* @__PURE__ */ jsx(Table$1.SortHeader, {
1401
+ sorted,
1402
+ onClick: () => toggle(column),
1403
+ children
1404
+ })
1405
+ });
1406
+ };
1407
+ //#endregion
1408
+ //#region src/blocks/AIKit/MarkdownTableRow.tsx
1409
+ /** A row in an answer's table; inside a sortable table its cells can find their column. */
1410
+ const MarkdownTableRow = (props) => {
1411
+ const { node, ...rest } = props;
1412
+ const sorting = use(TableSortContext);
1413
+ const row = /* @__PURE__ */ jsx(Table$1.Row, { ...rest });
1414
+ return sorting === null ? row : /* @__PURE__ */ jsx(RowCellsContext, {
1415
+ value: getRowCells(node),
1416
+ children: row
1417
+ });
1418
+ };
1419
+ //#endregion
1420
+ //#region src/blocks/AIKit/svgFence.ts
1421
+ /** Fence languages whose body may be a single SVG document. */
1422
+ const SVG_FENCE_LANGUAGES = [
1423
+ "svg",
1424
+ "xml",
1425
+ "html"
1426
+ ];
1427
+ /** A root start tag: `<svg` then whitespace, `/` or `>` — not `<svg-chart`. */
1428
+ const SVG_ROOT = /^<svg[\s/>]/i;
1429
+ /** An end tag for `svg` as the HTML parser sees one. */
1430
+ const SVG_END_TAG = /<\/svg[\s/>]/iy;
1431
+ /** The only spelling that closes a chart: `</svg>`, optional whitespace before `>`. */
1432
+ const SVG_CLOSE = /<\/svg\s*>/iy;
1433
+ const COMMENT_END = /--!?>/g;
1434
+ /**
1435
+ * Where the HTML parser ends the comment, CDATA or other `<!…>` / `<?…>` markup opening at `lt`.
1436
+ *
1437
+ * - The index just past it, `-1` while it is still open, `null` when none opens there.
1438
+ */
1439
+ const skipMarkup = (text, lt) => {
1440
+ if (text.startsWith("<!--", lt)) {
1441
+ const body = lt + 4;
1442
+ if (text.startsWith(">", body)) return body + 1;
1443
+ if (text.startsWith("->", body)) return body + 2;
1444
+ COMMENT_END.lastIndex = body;
1445
+ const end = COMMENT_END.exec(text);
1446
+ return end ? end.index + end[0].length : -1;
1447
+ }
1448
+ if (text.startsWith("<![CDATA[", lt)) {
1449
+ const end = text.indexOf("]]>", lt + 9);
1450
+ return end < 0 ? -1 : end + 3;
1451
+ }
1452
+ if (text.startsWith("<!", lt) || text.startsWith("<?", lt)) {
1453
+ const end = text.indexOf(">", lt + 2);
1454
+ return end < 0 ? -1 : end + 1;
1455
+ }
1456
+ return null;
1457
+ };
1458
+ /**
1459
+ * Index just past the first `</svg>` at or after `from` outside skipped markup, or `-1`.
1460
+ *
1461
+ * - Any other spelling of that first end tag (`</svg/>`, junk attributes) also gives `-1`.
1462
+ * - One pass over the `<`s, never a backtracking regex: it runs on every streamed chunk.
1463
+ */
1464
+ const svgEndTagEnd = (text, from) => {
1465
+ let at = from;
1466
+ for (;;) {
1467
+ const lt = text.indexOf("<", at);
1468
+ if (lt < 0) return -1;
1469
+ const skipped = skipMarkup(text, lt);
1470
+ if (skipped === -1) return -1;
1471
+ if (skipped !== null) {
1472
+ at = skipped;
1473
+ continue;
1474
+ }
1475
+ SVG_END_TAG.lastIndex = lt;
1476
+ if (SVG_END_TAG.test(text)) {
1477
+ SVG_CLOSE.lastIndex = lt;
1478
+ return SVG_CLOSE.test(text) ? SVG_CLOSE.lastIndex : -1;
1479
+ }
1480
+ at = lt + 1;
1481
+ }
1482
+ };
1483
+ /**
1484
+ * The SVG document a fenced block holds, or `null` when it holds anything else.
1485
+ *
1486
+ * - Only `svg`, `xml` and `html` fences; an XML prolog, doctype or comments may lead.
1487
+ * - Exactly one document: a second one, prose around it or one still streaming stays code.
1488
+ * - The scan mirrors the HTML parser; whatever it accepts is still sanitized.
1489
+ */
1490
+ const svgFenceDocument = (language, body) => {
1491
+ if (language === void 0 || !SVG_FENCE_LANGUAGES.includes(language.toLowerCase())) return null;
1492
+ const text = body.trim();
1493
+ let at = 0;
1494
+ for (;;) {
1495
+ while (at < text.length && /\s/.test(text.charAt(at))) at++;
1496
+ const skipped = skipMarkup(text, at);
1497
+ if (skipped === null) break;
1498
+ if (skipped === -1) return null;
1499
+ at = skipped;
1500
+ }
1501
+ if (!SVG_ROOT.test(text.slice(at, at + 5))) return null;
1502
+ return svgEndTagEnd(text, at) === text.length ? text.slice(at) : null;
1503
+ };
1504
+ //#endregion
1505
+ //#region src/blocks/AIKit/scopeSvgReferences.ts
1506
+ /** More cloned nodes than any honest chart needs; matplotlib's glyphs stay well under it. */
1507
+ const MAX_CLONED_NODES = 2e4;
1508
+ const LINK_ATTRIBUTES = ["href", "xlink:href"];
1509
+ /**
1510
+ * The only id shape a reference may name.
1511
+ *
1512
+ * - No `%`, `\\` or quotes: the browser decodes those where this check would not.
1513
+ */
1514
+ const STRICT_ID = /^[A-Za-z_][\w.-]*$/;
1515
+ /** A `url(#id)` paint, clip, mask, filter or marker reference, in its one accepted spelling. */
1516
+ const URL_REFERENCE = /url\(\s*(['"]?)#([A-Za-z_][\w.-]*)\1\s*\)/gi;
1517
+ /** Anything the browser could read as a `url(` or a CSS escape. */
1518
+ const ANY_URL = /url\s*\(|\\/gi;
1519
+ /**
1520
+ * The ids an attribute value points at; `null` when it cannot be read safely.
1521
+ *
1522
+ * - Fails closed: a `url(` or escape the strict spelling does not account for is unreadable.
1523
+ */
1524
+ const referencesIn = (name, value) => {
1525
+ if (LINK_ATTRIBUTES.includes(name)) {
1526
+ const id = value.trim().replace(/^#/, "");
1527
+ return value.trim().startsWith("#") && STRICT_ID.test(id) ? [id] : null;
1528
+ }
1529
+ const strict = [...value.matchAll(URL_REFERENCE)].map((match) => match[2] ?? "");
1530
+ return (value.match(ANY_URL)?.length ?? 0) === strict.length ? strict : null;
1531
+ };
1532
+ /** The ids an element's own attributes point at, by attribute name; `null` marks an unreadable one. */
1533
+ const ownReferences = (element) => {
1534
+ const references = /* @__PURE__ */ new Map();
1535
+ for (const { name, value } of element.attributes) {
1536
+ const ids = referencesIn(name, value);
1537
+ if (ids === null || ids.length > 0) references.set(name, ids);
1538
+ }
1539
+ return references;
1540
+ };
1541
+ /** Drops a reference that failed the check: a `<use>` goes whole, anything else loses the attribute. */
1542
+ const dropReference = (element, attribute) => {
1543
+ if (element.localName === "use" && LINK_ATTRIBUTES.includes(attribute)) element.remove();
1544
+ else element.removeAttribute(attribute);
1545
+ };
1546
+ let fenceCount = 0;
1547
+ const nextPrefix = () => {
1548
+ fenceCount += 1;
1549
+ return `svgf${fenceCount.toString(36)}${Math.random().toString(36).slice(2, 8)}-`;
1550
+ };
1551
+ /**
1552
+ * Confines a sanitized fence's internal references to the fence itself, at linear cost.
1553
+ *
1554
+ * 1. An id outside `STRICT_ID` is removed; a repeated one keeps its first element, as the browser does.
1555
+ * 2. A reference survives only if readable and aimed at an in-fence, reference-free subtree.
1556
+ * 3. Each `<use>` costs its target's node count; past `MAX_CLONED_NODES` the rest go.
1557
+ * 4. Every id and reference gets a per-fence prefix, so nothing resolves into the host page.
1558
+ */
1559
+ const scopeSvgReferences = (root) => {
1560
+ const elements = [...root.querySelectorAll("*")];
1561
+ const byId = /* @__PURE__ */ new Map();
1562
+ for (const element of elements) {
1563
+ if (!element.hasAttribute("id")) continue;
1564
+ if (!STRICT_ID.test(element.id) || byId.has(element.id)) element.removeAttribute("id");
1565
+ else byId.set(element.id, element);
1566
+ }
1567
+ const references = new Map(elements.map((element) => [element, ownReferences(element)]));
1568
+ const referencing = /* @__PURE__ */ new Set();
1569
+ for (const [element, refs] of references) {
1570
+ if (refs.size === 0) continue;
1571
+ for (let node = element; node; node = node.parentElement) referencing.add(node);
1572
+ }
1573
+ for (const [element, refs] of references) for (const [attribute, ids] of refs) if (ids === null || ids.some((id) => {
1574
+ const target = byId.get(id);
1575
+ return !target || referencing.has(target);
1576
+ })) dropReference(element, attribute);
1577
+ const targetSize = /* @__PURE__ */ new Map();
1578
+ let cloned = 0;
1579
+ for (const use of root.querySelectorAll("use")) {
1580
+ const href = use.getAttribute("href") ?? use.getAttribute("xlink:href") ?? "";
1581
+ const target = byId.get(href.trim().slice(1));
1582
+ if (!target) continue;
1583
+ let size = targetSize.get(target);
1584
+ if (size === void 0) {
1585
+ size = target.getElementsByTagName("*").length + 1;
1586
+ targetSize.set(target, size);
1587
+ }
1588
+ cloned += size;
1589
+ if (cloned > MAX_CLONED_NODES) use.remove();
1590
+ }
1591
+ const prefix = nextPrefix();
1592
+ for (const element of root.querySelectorAll("*")) {
1593
+ if (element.id) element.id = `${prefix}${element.id}`;
1594
+ for (const { name, value } of [...element.attributes]) {
1595
+ const scoped = LINK_ATTRIBUTES.includes(name) ? value.trim().replace(/^#/, `#${prefix}`) : value.replace(URL_REFERENCE, (_match, quote, id) => {
1596
+ return `url(${quote}#${prefix}${id}${quote})`;
1597
+ });
1598
+ if (scoped !== value) element.setAttribute(name, scoped);
1599
+ }
1600
+ }
1601
+ };
1602
+ //#endregion
1603
+ //#region src/blocks/AIKit/filterSvgStyle.ts
1604
+ /** The CSS properties a fenced chart may set inline: nothing that loads a URL or leaves the box. */
1605
+ const SAFE_PROPERTIES = /* @__PURE__ */ new Set([
1606
+ "alignment-baseline",
1607
+ "baseline-shift",
1608
+ "clip-path",
1609
+ "clip-rule",
1610
+ "color",
1611
+ "display",
1612
+ "dominant-baseline",
1613
+ "fill",
1614
+ "fill-opacity",
1615
+ "fill-rule",
1616
+ "filter",
1617
+ "flood-color",
1618
+ "flood-opacity",
1619
+ "font",
1620
+ "font-family",
1621
+ "font-size",
1622
+ "font-style",
1623
+ "font-weight",
1624
+ "letter-spacing",
1625
+ "lighting-color",
1626
+ "marker",
1627
+ "marker-end",
1628
+ "marker-mid",
1629
+ "marker-start",
1630
+ "mask",
1631
+ "opacity",
1632
+ "paint-order",
1633
+ "shape-rendering",
1634
+ "stop-color",
1635
+ "stop-opacity",
1636
+ "stroke",
1637
+ "stroke-dasharray",
1638
+ "stroke-dashoffset",
1639
+ "stroke-linecap",
1640
+ "stroke-linejoin",
1641
+ "stroke-miterlimit",
1642
+ "stroke-opacity",
1643
+ "stroke-width",
1644
+ "text-anchor",
1645
+ "text-decoration",
1646
+ "text-rendering",
1647
+ "vector-effect",
1648
+ "visibility",
1649
+ "word-spacing"
1650
+ ]);
1651
+ /**
1652
+ * The only functions a value may call: colours, and a same-document reference.
1653
+ *
1654
+ * - Colour arguments take words, numbers and `/`, never a nested `(`.
1655
+ */
1656
+ const SAFE_FUNCTION = new RegExp(String.raw`(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch)\([\w\s.,%+/-]*\)|${URL_REFERENCE.source}`, "gi");
1657
+ /** What is left of a value once its safe functions are gone: words, numbers, colours, quotes. */
1658
+ const SAFE_REMAINDER = /^[\w\s#.,%'"+/-]*$/;
1659
+ const isSafeDeclaration = (property, value) => SAFE_PROPERTIES.has(property) && SAFE_REMAINDER.test(value.replace(SAFE_FUNCTION, ""));
1660
+ /**
1661
+ * A `style` attribute reduced to the declarations a static chart needs.
1662
+ *
1663
+ * - Allowlist, not denylist: `image-set()`, escapes and positioning never pass.
1664
+ * - A declaration goes whole; `''` when none survive.
1665
+ */
1666
+ const filterSvgStyle = (style) => style.split(";").flatMap((declaration) => {
1667
+ const colon = declaration.indexOf(":");
1668
+ if (colon < 0) return [];
1669
+ const property = declaration.slice(0, colon).trim().toLowerCase();
1670
+ const value = declaration.slice(colon + 1).trim();
1671
+ return isSafeDeclaration(property, value) ? [`${property}: ${value}`] : [];
1672
+ }).join("; ");
1673
+ //#endregion
1674
+ //#region src/blocks/AIKit/sanitizeSvgFence.ts
1675
+ /**
1676
+ * Elements a fenced chart never keeps.
1677
+ *
1678
+ * - SMIL (`animate*`, `set`) sets attributes at runtime, past every attribute check.
1679
+ * - `foreignObject` carries HTML; a `style` element would restyle the whole page.
1680
+ */
1681
+ const FORBIDDEN_TAGS = [
1682
+ "script",
1683
+ "foreignObject",
1684
+ "animate",
1685
+ "animateMotion",
1686
+ "animateTransform",
1687
+ "set",
1688
+ "style"
1689
+ ];
1690
+ const URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "xlink:href"]);
1691
+ let purifier = null;
1692
+ /** A DOMPurify instance of its own, so these hooks never reach `ZoomableSvg` or other callers. */
1693
+ const getPurifier = () => {
1694
+ if (purifier) return purifier;
1695
+ const instance = DOMPurify(window);
1696
+ instance.addHook("uponSanitizeAttribute", (_node, data) => {
1697
+ const name = data.attrName.toLowerCase();
1698
+ if (URL_ATTRIBUTES.has(name) && !data.attrValue.trim().startsWith("#")) data.keepAttr = false;
1699
+ if (name === "style") {
1700
+ data.attrValue = filterSvgStyle(data.attrValue);
1701
+ if (!data.attrValue) data.keepAttr = false;
1702
+ }
1703
+ });
1704
+ purifier = instance;
1705
+ return instance;
1706
+ };
1707
+ /**
1708
+ * A model-written SVG document reduced to static, self-contained drawing.
1709
+ *
1710
+ * - SVG profile only: scripts, event handlers and HTML go.
1711
+ * - `href`s survive only as same-document `#` references, so nothing loads from elsewhere.
1712
+ * - `use` is allowed back in for that reason (matplotlib draws every glyph with it).
1713
+ * - References are then confined to the fence; see `scopeSvgReferences`.
1714
+ */
1715
+ const sanitizeSvgFence = (svg) => {
1716
+ const template = document.createElement("template");
1717
+ template.innerHTML = getPurifier().sanitize(svg, {
1718
+ USE_PROFILES: {
1719
+ svg: true,
1720
+ svgFilters: true
1721
+ },
1722
+ ADD_TAGS: ["use"],
1723
+ FORBID_TAGS: FORBIDDEN_TAGS
1724
+ });
1725
+ scopeSvgReferences(template.content);
1726
+ return template.innerHTML;
1727
+ };
1728
+ //#endregion
1729
+ //#region src/blocks/AIKit/SvgFencesContext.ts
1730
+ /** Whether the `Markdown.Block` being rendered draws ```svg fences; off by default. */
1731
+ const SvgFencesContext = createContext(false);
1732
+ //#endregion
1733
+ //#region src/blocks/AIKit/SvgFenceChart.tsx
1734
+ /** A fenced SVG chart, sanitized and drawn responsive, in a block that opted in. */
1735
+ const SvgFenceChart = ({ svg, source, children }) => {
1736
+ const allowed = use(SvgFencesContext);
1737
+ const html = useMemo(() => allowed ? makeResponsive(sanitizeSvgFence(svg)) : "", [allowed, svg]);
1738
+ if (!allowed || !html) return children;
1739
+ return /* @__PURE__ */ jsx(ZoomableSvg, {
1740
+ html,
1741
+ copyText: source,
1742
+ className: "tw:my-3",
1743
+ allowUse: true
1744
+ });
1745
+ };
1746
+ /**
1747
+ * The fence languages `Markdown` draws instead of printing.
1748
+ *
1749
+ * - Streaming callers hold an open fence back as plain code, or every keystroke fetches the chunk.
1750
+ * - Frozen: it is shared across every app in the bundle.
1751
+ */
1752
+ const DIAGRAM_LANGUAGES = Object.freeze([...[
1753
+ "mermaid",
1754
+ "flint",
1755
+ "flint-chart"
1756
+ ]]);
1757
+ //#endregion
529
1758
  //#region src/blocks/AIKit/Markdown.tsx
530
1759
  const loadMermaid = diagramChunk("MermaidDiagram", () => import("@keboola/design/mermaid").then((module) => module.MermaidDiagram));
531
1760
  const loadFlint = diagramChunk("FlintChart", () => import("@keboola/design/flint").then((module) => module.FlintChart));
@@ -562,13 +1791,6 @@ const DIAGRAM_CHUNKS = {
562
1791
  "flint-chart": loadFlint
563
1792
  };
564
1793
  /**
565
- * The fence languages this renderer draws instead of printing.
566
- *
567
- * - Streaming callers hold an open fence back as plain code, or every keystroke fetches the chunk.
568
- * - Frozen: it is shared across every app in the bundle.
569
- */
570
- const DIAGRAM_LANGUAGES = Object.freeze(Object.keys(DIAGRAM_CHUNKS));
571
- /**
572
1794
  * Whether the `code` being rendered sits inside a fenced block.
573
1795
  *
574
1796
  * - Set by `pre`: a fence without info string has no `language-…` class, like inline code.
@@ -590,39 +1812,61 @@ const MarkdownCode = ({ children, className, ...props }) => {
590
1812
  children
591
1813
  });
592
1814
  };
593
- const baseComponents = {
594
- pre({ children, ...props }) {
595
- const child = Array.isArray(children) ? children[0] : children;
596
- const code = getText(child).replace(/\n$/, "");
597
- const language = getLanguage(child);
598
- const load = language !== void 0 && Object.hasOwn(DIAGRAM_CHUNKS, language) ? DIAGRAM_CHUNKS[language] : void 0;
599
- if (load) {
600
- if (!code.trim()) return null;
601
- return /* @__PURE__ */ jsx(DiagramBlock, {
602
- load,
603
- code
604
- });
605
- }
606
- const { node: _node, ...rest } = props;
607
- return /* @__PURE__ */ jsxs("div", {
608
- className: "tw:group tw:relative",
609
- children: [/* @__PURE__ */ jsx("span", {
610
- className: "tw:absolute tw:right-2 tw:top-2 tw:opacity-0 tw:transition-opacity tw:group-focus-within:opacity-100 tw:group-hover:opacity-100",
611
- children: /* @__PURE__ */ jsx(ClipboardButton, {
612
- text: code,
613
- tooltip: "Copy",
614
- size: "icon-xs",
615
- variant: "outline"
616
- })
617
- }), /* @__PURE__ */ jsx("pre", {
618
- ...rest,
619
- children: /* @__PURE__ */ jsx(CodeBlockContext, {
620
- value: true,
1815
+ /**
1816
+ * The `pre` renderer: a diagram fence draws, any other fence prints with a copy button.
1817
+ *
1818
+ * - Diagrams replace the whole `<pre>`; a `<div>` inside `<pre><code>` is invalid HTML.
1819
+ * - A `highlight` block colours a fence whose language has a grammar, once the chunk loads.
1820
+ */
1821
+ const MarkdownPre = (props) => {
1822
+ const { children, node: _node, ...rest } = props;
1823
+ const { highlight, renderDiagramActions } = use(MarkdownOptionsContext);
1824
+ const child = Array.isArray(children) ? children[0] : children;
1825
+ const code = getText(child).replace(/\n$/, "");
1826
+ const language = getLanguage(child);
1827
+ const load = language !== void 0 && Object.hasOwn(DIAGRAM_CHUNKS, language) ? DIAGRAM_CHUNKS[language] : void 0;
1828
+ if (load) {
1829
+ if (!code.trim()) return null;
1830
+ return /* @__PURE__ */ jsx(DiagramBlock, {
1831
+ load,
1832
+ code,
1833
+ renderActions: renderDiagramActions
1834
+ });
1835
+ }
1836
+ const grammar = highlight ? getHighlightGrammar(language) : void 0;
1837
+ const block = /* @__PURE__ */ jsxs("div", {
1838
+ className: "tw:group tw:relative",
1839
+ children: [/* @__PURE__ */ jsx("span", {
1840
+ className: "tw:absolute tw:right-2 tw:top-2 tw:opacity-0 tw:transition-opacity tw:group-focus-within:opacity-100 tw:group-hover:opacity-100",
1841
+ children: /* @__PURE__ */ jsx(ClipboardButton, {
1842
+ text: code,
1843
+ tooltip: "Copy",
1844
+ size: "icon-xs",
1845
+ variant: "outline"
1846
+ })
1847
+ }), /* @__PURE__ */ jsx("pre", {
1848
+ ...rest,
1849
+ children: /* @__PURE__ */ jsx(CodeBlockContext, {
1850
+ value: true,
1851
+ children: grammar === void 0 ? children : /* @__PURE__ */ jsx(HighlightedCode, {
1852
+ code,
1853
+ grammar,
1854
+ className: `language-${language}`,
621
1855
  children
622
1856
  })
623
- })]
624
- });
625
- },
1857
+ })
1858
+ })]
1859
+ });
1860
+ const svg = svgFenceDocument(language, code);
1861
+ if (svg === null) return block;
1862
+ return /* @__PURE__ */ jsx(SvgFenceChart, {
1863
+ svg,
1864
+ source: `\`\`\`${language}\n${code}\n\`\`\``,
1865
+ children: block
1866
+ });
1867
+ };
1868
+ const baseComponents = {
1869
+ pre: MarkdownPre,
626
1870
  code: MarkdownCode,
627
1871
  a({ children, href, ...props }) {
628
1872
  const { node: _node, ...rest } = props;
@@ -637,33 +1881,14 @@ const baseComponents = {
637
1881
  children
638
1882
  });
639
1883
  },
640
- table({ className, ...props }) {
641
- const { node: _node, ...rest } = props;
642
- return /* @__PURE__ */ jsx("div", {
643
- className: "tw:not-prose tw:my-4",
644
- children: /* @__PURE__ */ jsx(Table$1, {
645
- bordered: true,
646
- className: cn("tw:!mb-0", className),
647
- ...rest
648
- })
649
- });
650
- },
1884
+ table: MarkdownGrid,
651
1885
  thead(props) {
652
1886
  const { node: _node, ...rest } = props;
653
1887
  return /* @__PURE__ */ jsx(Table$1.Header, { ...rest });
654
1888
  },
655
- tbody(props) {
656
- const { node: _node, ...rest } = props;
657
- return /* @__PURE__ */ jsx(Table$1.Body, { ...rest });
658
- },
659
- tr(props) {
660
- const { node: _node, ...rest } = props;
661
- return /* @__PURE__ */ jsx(Table$1.Row, { ...rest });
662
- },
663
- th(props) {
664
- const { node: _node, ...rest } = props;
665
- return /* @__PURE__ */ jsx(Table$1.Head, { ...rest });
666
- },
1889
+ tbody: MarkdownTableBody,
1890
+ tr: MarkdownTableRow,
1891
+ th: MarkdownTableHead,
667
1892
  td(props) {
668
1893
  const { node: _node, ...rest } = props;
669
1894
  return /* @__PURE__ */ jsx(Table$1.Cell, { ...rest });
@@ -681,15 +1906,38 @@ const MarkdownRoot = ({ children, className, rhythm = "answer" }) => /* @__PURE_
681
1906
  className: cn(PROSE_CLASS[rhythm], className),
682
1907
  children
683
1908
  });
1909
+ /** The diagram toolbar a block asks for: the caller's actions, then the built-in download. */
1910
+ const composeDiagramActions = (diagramActions, diagramDownload) => {
1911
+ if (!diagramDownload) return diagramActions;
1912
+ return (safeSvg) => /* @__PURE__ */ jsxs(Fragment$1, { children: [diagramActions?.(safeSvg), /* @__PURE__ */ jsx(DiagramDownloadButton, { safeSvg })] });
1913
+ };
684
1914
  /** One parsed piece of an answer, with no surface of its own; memoise at the call site. */
685
- const MarkdownBlock = ({ children, remarkPlugins, components: componentOverrides }) => /* @__PURE__ */ jsx(ReactMarkdown, {
686
- remarkPlugins: remarkPlugins === void 0 ? baseRemarkPlugins : [...baseRemarkPlugins, ...remarkPlugins],
687
- components: componentOverrides === void 0 ? baseComponents : {
688
- ...baseComponents,
689
- ...componentOverrides
690
- },
691
- children
692
- });
1915
+ const MarkdownBlock = ({ children, remarkPlugins, components: componentOverrides, highlight = false, sortable = false, diagramActions, diagramDownload = false, allowSvgFences = false }) => {
1916
+ const options = useMemo(() => ({
1917
+ highlight,
1918
+ sortable,
1919
+ renderDiagramActions: composeDiagramActions(diagramActions, diagramDownload)
1920
+ }), [
1921
+ highlight,
1922
+ sortable,
1923
+ diagramActions,
1924
+ diagramDownload
1925
+ ]);
1926
+ return /* @__PURE__ */ jsx(MarkdownOptionsContext, {
1927
+ value: options,
1928
+ children: /* @__PURE__ */ jsx(SvgFencesContext, {
1929
+ value: allowSvgFences,
1930
+ children: /* @__PURE__ */ jsx(ReactMarkdown, {
1931
+ remarkPlugins: remarkPlugins === void 0 ? baseRemarkPlugins : [...baseRemarkPlugins, ...remarkPlugins],
1932
+ components: componentOverrides === void 0 ? baseComponents : {
1933
+ ...baseComponents,
1934
+ ...componentOverrides
1935
+ },
1936
+ children
1937
+ })
1938
+ })
1939
+ });
1940
+ };
693
1941
  /**
694
1942
  * Read-only GFM Markdown renderer with design-token typography.
695
1943
  *
@@ -725,7 +1973,7 @@ const FADE_SURFACE = {
725
1973
  * `baseline-widely-available` build floor lacks, and must sit on `Table`'s own container.
726
1974
  * - Past `EXPAND_RATIO` the expand action shows without hover.
727
1975
  */
728
- const MarkdownTable = ({ children, actions, onExpand, surface = "card", className }) => {
1976
+ const MarkdownTable = ({ children, actions, onExpand, surface = "card", sortable = false, className }) => {
729
1977
  const frameRef = useRef(null);
730
1978
  const [overflow, setOverflow] = useState({
731
1979
  scrollable: false,
@@ -775,15 +2023,104 @@ const MarkdownTable = ({ children, actions, onExpand, surface = "card", classNam
775
2023
  }), /* @__PURE__ */ jsxs("div", {
776
2024
  ref: frameRef,
777
2025
  className: "tw:relative",
778
- children: [children, overflow.scrollable && !overflow.atEnd && /* @__PURE__ */ jsx("span", {
2026
+ children: [sortable ? /* @__PURE__ */ jsx(TableSortProvider, { children }) : children, overflow.scrollable && !overflow.atEnd && /* @__PURE__ */ jsx("span", {
2027
+ "aria-hidden": true,
2028
+ "data-testid": "markdown-table-fade",
2029
+ className: cn("tw:pointer-events-none tw:absolute tw:inset-y-px tw:right-px tw:w-12 tw:rounded-r-[10px] tw:bg-gradient-to-l tw:to-transparent", FADE_SURFACE[surface])
2030
+ })]
2031
+ })]
2032
+ });
2033
+ };
2034
+ //#endregion
2035
+ //#region src/blocks/AIKit/CollapsedMessageBody.tsx
2036
+ /**
2037
+ * Clips tall content at `maxHeight` behind a fade, with a Show more / Show less toggle.
2038
+ *
2039
+ * - The toggle and fade appear only while the content is taller than `maxHeight`.
2040
+ * - Height is measured with a `ResizeObserver`; without one (jsdom) nothing is clipped.
2041
+ */
2042
+ const CollapsedMessageBody = ({ maxHeight, fadeClassName, children }) => {
2043
+ const contentRef = useRef(null);
2044
+ const [isOverflowing, setIsOverflowing] = useState(false);
2045
+ const [isExpanded, setIsExpanded] = useState(false);
2046
+ useLayoutEffect(() => {
2047
+ const node = contentRef.current;
2048
+ if (!node || typeof ResizeObserver === "undefined") return;
2049
+ const observer = new ResizeObserver(() => setIsOverflowing(node.offsetHeight > maxHeight));
2050
+ observer.observe(node);
2051
+ return () => observer.disconnect();
2052
+ }, [maxHeight]);
2053
+ const isClipped = isOverflowing && !isExpanded;
2054
+ return /* @__PURE__ */ jsxs("div", {
2055
+ className: "tw:flex tw:flex-col tw:items-start tw:gap-1",
2056
+ children: [/* @__PURE__ */ jsxs("div", {
2057
+ className: "tw:relative tw:w-full tw:overflow-hidden",
2058
+ style: isClipped ? { maxHeight } : void 0,
2059
+ children: [/* @__PURE__ */ jsx("div", {
2060
+ ref: contentRef,
2061
+ children
2062
+ }), isClipped && /* @__PURE__ */ jsx("div", {
779
2063
  "aria-hidden": true,
780
- "data-testid": "markdown-table-fade",
781
- className: cn("tw:pointer-events-none tw:absolute tw:inset-y-px tw:right-px tw:w-12 tw:rounded-r-[10px] tw:bg-gradient-to-l tw:to-transparent", FADE_SURFACE[surface])
2064
+ className: cn("tw:pointer-events-none tw:absolute tw:inset-x-0 tw:bottom-0 tw:h-12 tw:bg-gradient-to-t tw:to-transparent", fadeClassName)
782
2065
  })]
2066
+ }), isOverflowing && /* @__PURE__ */ jsx(Button, {
2067
+ variant: "inline",
2068
+ size: "small",
2069
+ className: "tw:px-0",
2070
+ "aria-expanded": isExpanded,
2071
+ onClick: () => setIsExpanded((v) => !v),
2072
+ children: isExpanded ? "Show less" : "Show more"
2073
+ })]
2074
+ });
2075
+ };
2076
+ //#endregion
2077
+ //#region src/blocks/AIKit/MessageDeliveryStatus.tsx
2078
+ /**
2079
+ * Where a sent message stands, until it is delivered.
2080
+ *
2081
+ * - `sending` is announced politely; `failed` is an alert with a Retry button when `onRetry` is set.
2082
+ */
2083
+ const MessageDeliveryStatus = ({ status, onRetry }) => {
2084
+ if (status === "sending") return /* @__PURE__ */ jsx("span", {
2085
+ role: "status",
2086
+ className: "tw:text-xs tw:text-muted-foreground",
2087
+ children: "Sending…"
2088
+ });
2089
+ return /* @__PURE__ */ jsxs("span", {
2090
+ role: "alert",
2091
+ className: "tw:inline-flex tw:items-center tw:gap-2 tw:text-xs tw:text-destructive",
2092
+ children: ["Failed to send", onRetry && /* @__PURE__ */ jsx(Button, {
2093
+ variant: "inline",
2094
+ size: "small",
2095
+ className: "tw:px-0",
2096
+ onClick: onRetry,
2097
+ children: "Retry"
783
2098
  })]
784
2099
  });
785
2100
  };
786
2101
  //#endregion
2102
+ //#region src/blocks/AIKit/MessageTimestamp.tsx
2103
+ const TIME_FORMAT = {
2104
+ hour: "2-digit",
2105
+ minute: "2-digit"
2106
+ };
2107
+ /**
2108
+ * The time a message was sent.
2109
+ *
2110
+ * - Shows the clock time, with the full locale date and time as the tooltip.
2111
+ * - Draws nothing for a value that is not a date.
2112
+ */
2113
+ const MessageTimestamp = ({ createdAt }) => {
2114
+ const date = new Date(createdAt);
2115
+ if (Number.isNaN(date.getTime())) return null;
2116
+ return /* @__PURE__ */ jsx("time", {
2117
+ dateTime: date.toISOString(),
2118
+ title: date.toLocaleString(),
2119
+ className: "tw:text-xs tw:text-muted-foreground",
2120
+ children: date.toLocaleTimeString([], TIME_FORMAT)
2121
+ });
2122
+ };
2123
+ //#endregion
787
2124
  //#region src/blocks/AIKit/MessageBubble.tsx
788
2125
  const containerClasses = cva([
789
2126
  "tw:flex",
@@ -838,49 +2175,105 @@ const contentClasses = cva([
838
2175
  } },
839
2176
  defaultVariants: { variant: "assistant" }
840
2177
  });
841
- const MessageBubble = ({ variant, children, actions, createdAt }) => {
842
- return /* @__PURE__ */ jsx("div", {
2178
+ const MessageBubble = ({ variant, children, actions, createdAt, timestamp = "tooltip", quote, status, onRetry, author, collapseAfterPx, avatar }) => {
2179
+ const isTimestampVisible = timestamp === "visible" && createdAt !== void 0;
2180
+ const isSending = variant === "user" && status === "sending";
2181
+ const deliveryStatus = variant === "user" && status ? /* @__PURE__ */ jsx(MessageDeliveryStatus, {
2182
+ status,
2183
+ onRetry
2184
+ }) : null;
2185
+ const body = collapseAfterPx === void 0 ? children : /* @__PURE__ */ jsx(CollapsedMessageBody, {
2186
+ maxHeight: collapseAfterPx,
2187
+ fadeClassName: variant === "user" ? "tw:from-muted" : "tw:from-background",
2188
+ children
2189
+ });
2190
+ const hasActionsRow = isTimestampVisible || deliveryStatus !== null;
2191
+ return /* @__PURE__ */ jsxs("div", {
843
2192
  className: containerClasses({ variant }),
844
2193
  "data-testid": `chat-message-${variant}`,
845
- children: /* @__PURE__ */ jsxs("div", {
846
- className: stackClasses({ variant }),
847
- children: [/* @__PURE__ */ jsx("div", {
848
- className: contentClasses({ variant }),
849
- title: variant === "user" ? createdAt : void 0,
850
- children
851
- }), actions]
2194
+ children: [
2195
+ variant === "assistant" && avatar,
2196
+ /* @__PURE__ */ jsxs("div", {
2197
+ className: stackClasses({ variant }),
2198
+ children: [
2199
+ author && /* @__PURE__ */ jsx("div", {
2200
+ className: "tw:text-xs tw:font-medium tw:text-muted-foreground",
2201
+ children: author
2202
+ }),
2203
+ /* @__PURE__ */ jsxs("div", {
2204
+ className: cn(contentClasses({ variant }), isSending && "tw:opacity-70"),
2205
+ title: variant === "user" && timestamp === "tooltip" ? createdAt : void 0,
2206
+ children: [quote && /* @__PURE__ */ jsx("blockquote", {
2207
+ className: "tw:m-0 tw:border-l-2 tw:border-border tw:pl-2 tw:text-sm tw:text-muted-foreground",
2208
+ children: quote
2209
+ }), body]
2210
+ }),
2211
+ hasActionsRow ? /* @__PURE__ */ jsxs("div", {
2212
+ className: "tw:flex tw:items-center tw:gap-2",
2213
+ children: [
2214
+ isTimestampVisible && /* @__PURE__ */ jsx(MessageTimestamp, { createdAt }),
2215
+ deliveryStatus,
2216
+ actions
2217
+ ]
2218
+ }) : actions
2219
+ ]
2220
+ }),
2221
+ variant === "user" && avatar
2222
+ ]
2223
+ });
2224
+ };
2225
+ //#endregion
2226
+ //#region src/blocks/AIKit/ModeChip.tsx
2227
+ /**
2228
+ * A removable pill naming a mode the composer is in.
2229
+ *
2230
+ * - The whole chip turns the mode off, not just the ×.
2231
+ * - Collapsed via a `0fr` / `1fr` grid column and taken out of the tab order while hidden.
2232
+ */
2233
+ const ModeChip = ({ children, visible = true, tooltip, removeLabel, color = "amber", onRemove, className }) => {
2234
+ const button = /* @__PURE__ */ jsx("button", {
2235
+ type: "button",
2236
+ onClick: onRemove,
2237
+ tabIndex: visible ? void 0 : -1,
2238
+ "aria-label": removeLabel,
2239
+ className: "tailwindcss-preflight tw:cursor-pointer tw:border-0 tw:bg-transparent tw:p-0",
2240
+ children: /* @__PURE__ */ jsx(Badge, {
2241
+ variant: "pill",
2242
+ color,
2243
+ suffix: /* @__PURE__ */ jsx(X, { className: "tw:size-3" }),
2244
+ children
2245
+ })
2246
+ });
2247
+ return /* @__PURE__ */ jsx("div", {
2248
+ "aria-hidden": !visible,
2249
+ className: cn("tw:grid tw:transition-all tw:duration-200 tw:ease-out tw:motion-reduce:transition-none", visible ? "tw:mr-1 tw:grid-cols-[1fr] tw:opacity-100" : "tw:mr-0 tw:grid-cols-[0fr] tw:opacity-0", className),
2250
+ children: /* @__PURE__ */ jsx("div", {
2251
+ className: "tw:overflow-hidden",
2252
+ children: tooltip ? /* @__PURE__ */ jsx(Tooltip, {
2253
+ placement: "top",
2254
+ tooltip,
2255
+ triggerAsChild: true,
2256
+ children: button
2257
+ }) : button
852
2258
  })
853
2259
  });
854
2260
  };
855
2261
  //#endregion
856
2262
  //#region src/blocks/AIKit/PlanApproval.tsx
857
2263
  const defaultStepCountLabel = (count) => `${count} step${count === 1 ? "" : "s"}`;
858
- const StatusPill = ({ status, pendingLabel, approvedLabel, dismissedLabel, changesRequestedLabel }) => {
859
- if (status === "approved") return /* @__PURE__ */ jsx(Badge, {
860
- color: "green",
861
- variant: "pill",
862
- className: "tw:normal-case tw:tracking-normal",
863
- children: approvedLabel
864
- });
865
- if (status === "dismissed") return /* @__PURE__ */ jsx(Badge, {
866
- color: "gray",
867
- variant: "pill",
868
- className: "tw:normal-case tw:tracking-normal",
869
- children: dismissedLabel
870
- });
871
- if (status === "changes-requested") return /* @__PURE__ */ jsx(Badge, {
872
- color: "blue",
873
- variant: "pill",
874
- className: "tw:normal-case tw:tracking-normal",
875
- children: changesRequestedLabel
876
- });
877
- return /* @__PURE__ */ jsx(Badge, {
878
- color: "green",
879
- variant: "pill",
880
- className: "tw:normal-case tw:tracking-normal",
881
- children: pendingLabel
882
- });
2264
+ const STATUS_PILL_COLOR = {
2265
+ pending: "green",
2266
+ approved: "green",
2267
+ dismissed: "gray",
2268
+ "changes-requested": "blue",
2269
+ expired: "orange"
883
2270
  };
2271
+ const StatusPill = ({ status, label }) => /* @__PURE__ */ jsx(Badge, {
2272
+ color: STATUS_PILL_COLOR[status],
2273
+ variant: "pill",
2274
+ className: "tw:normal-case tw:tracking-normal",
2275
+ children: label
2276
+ });
884
2277
  const ExpandToggle = ({ isExpanded, onToggle, showMoreLabel, showLessLabel }) => /* @__PURE__ */ jsxs("button", {
885
2278
  type: "button",
886
2279
  onClick: onToggle,
@@ -958,12 +2351,19 @@ const PlanActions = ({ isRequestingChanges, isSubmitting, onApprove, onRequestCh
958
2351
  children: approveLabel
959
2352
  }))]
960
2353
  });
961
- const PlanApproval = ({ status, title, summary, stepCount, planBody, defaultExpanded = false, isExpanded: isExpandedProp, onExpandToggle, onApprove, onRequestChanges, onClose, isSubmitting = false, approveTooltip, pendingPillLabel = "Ready for approval", approvedPillLabel = "Approved", dismissedPillLabel = "Dismissed", changesRequestedPillLabel = "Changes Requested", approveLabel = "Approve", requestChangesLabel = "Request Changes", cancelLabel = "Cancel", showMoreLabel = "Show more", showLessLabel = "Show less", closeLabel = "Dismiss plan", fullScreenLabel = "View full screen", exitFullScreenLabel = "Exit full screen", changeRequestPlaceholder = "What would you like to change in the plan?", changeRequestSubmitLabel = "Send change request", stepCountLabel = defaultStepCountLabel, className }) => {
2354
+ const PlanApproval = ({ status, statusLabel, statusMessage, title, summary, stepCount, planBody, defaultExpanded = false, isExpanded: isExpandedProp, onExpandToggle, onApprove, onRequestChanges, onClose, isSubmitting = false, approveTooltip, pendingPillLabel = "Ready for approval", approvedPillLabel = "Approved", dismissedPillLabel = "Dismissed", changesRequestedPillLabel = "Changes Requested", approveLabel = "Approve", requestChangesLabel = "Request Changes", cancelLabel = "Cancel", showMoreLabel = "Show more", showLessLabel = "Show less", closeLabel = "Dismiss plan", fullScreenLabel = "View full screen", exitFullScreenLabel = "Exit full screen", changeRequestPlaceholder = "What would you like to change in the plan?", changeRequestSubmitLabel = "Send change request", stepCountLabel = defaultStepCountLabel, className }) => {
962
2355
  const [uncontrolledExpanded, setUncontrolledExpanded] = useState(defaultExpanded);
963
2356
  const isExpanded = isExpandedProp ?? uncontrolledExpanded;
964
2357
  const [isRequestingChanges, setIsRequestingChanges] = useState(false);
965
2358
  const [changeRequest, setChangeRequest] = useState("");
966
2359
  const [isFullScreen, setIsFullScreen] = useState(false);
2360
+ const pillLabel = statusLabel ?? {
2361
+ pending: pendingPillLabel,
2362
+ approved: approvedPillLabel,
2363
+ dismissed: dismissedPillLabel,
2364
+ "changes-requested": changesRequestedPillLabel,
2365
+ expired: "Expired"
2366
+ }[status];
967
2367
  const isPending = status === "pending";
968
2368
  const showActions = isPending && (onApprove || onRequestChanges);
969
2369
  const showClose = isPending && Boolean(onClose);
@@ -1030,22 +2430,27 @@ const PlanApproval = ({ status, title, summary, stepCount, planBody, defaultExpa
1030
2430
  className: "tw:flex tw:items-start tw:gap-2 tw:pl-4 tw:pr-2 tw:pt-4",
1031
2431
  children: [/* @__PURE__ */ jsxs("div", {
1032
2432
  className: "tw:flex tw:min-w-0 tw:flex-1 tw:flex-col tw:gap-2 tw:pb-4",
1033
- children: [/* @__PURE__ */ jsxs("div", {
1034
- className: "tw:flex tw:items-center tw:gap-3",
1035
- children: [/* @__PURE__ */ jsx(StatusPill, {
1036
- status,
1037
- pendingLabel: pendingPillLabel,
1038
- approvedLabel: approvedPillLabel,
1039
- dismissedLabel: dismissedPillLabel,
1040
- changesRequestedLabel: changesRequestedPillLabel
1041
- }), stepCount != null && stepCount > 0 && /* @__PURE__ */ jsx("span", {
1042
- className: "tw:text-xs tw:leading-4 tw:text-muted-foreground",
1043
- children: stepCountLabel(stepCount)
1044
- })]
1045
- }), /* @__PURE__ */ jsx("p", {
1046
- className: "tw:m-0 tw:text-base tw:font-semibold tw:leading-6",
1047
- children: title
1048
- })]
2433
+ children: [
2434
+ /* @__PURE__ */ jsxs("div", {
2435
+ className: "tw:flex tw:items-center tw:gap-3",
2436
+ children: [/* @__PURE__ */ jsx(StatusPill, {
2437
+ status,
2438
+ label: pillLabel
2439
+ }), stepCount != null && stepCount > 0 && /* @__PURE__ */ jsx("span", {
2440
+ className: "tw:text-xs tw:leading-4 tw:text-muted-foreground",
2441
+ children: stepCountLabel(stepCount)
2442
+ })]
2443
+ }),
2444
+ /* @__PURE__ */ jsx("p", {
2445
+ className: "tw:m-0 tw:text-base tw:font-semibold tw:leading-6",
2446
+ children: title
2447
+ }),
2448
+ statusMessage != null && /* @__PURE__ */ jsx("p", {
2449
+ className: "tw:m-0 tw:text-sm tw:text-muted-foreground",
2450
+ "data-testid": "plan-status-message",
2451
+ children: statusMessage
2452
+ })
2453
+ ]
1049
2454
  }), /* @__PURE__ */ jsxs("div", {
1050
2455
  className: "tw:flex tw:items-center tw:gap-2",
1051
2456
  children: [canShowPlanBody && !isFullScreen && /* @__PURE__ */ jsx(Tooltip, {
@@ -1105,10 +2510,7 @@ const PlanApproval = ({ status, title, summary, stepCount, planBody, defaultExpa
1105
2510
  className: "tw:flex tw:min-w-0 tw:items-center tw:gap-3",
1106
2511
  children: [/* @__PURE__ */ jsx(StatusPill, {
1107
2512
  status,
1108
- pendingLabel: pendingPillLabel,
1109
- approvedLabel: approvedPillLabel,
1110
- dismissedLabel: dismissedPillLabel,
1111
- changesRequestedLabel: changesRequestedPillLabel
2513
+ label: pillLabel
1112
2514
  }), /* @__PURE__ */ jsx("span", {
1113
2515
  className: "tw:truncate",
1114
2516
  children: title
@@ -1294,7 +2696,7 @@ const clipBand = (node) => {
1294
2696
  bottom
1295
2697
  };
1296
2698
  };
1297
- const PromptInputCommands = ({ items, open, onSelect, filterValue, emptyText = "No commands found", panel, groupAction, groupHeading, children }) => {
2699
+ const PromptInputCommands = ({ items, open, onSelect, filterValue, tabPicks = false, emptyText = "No commands found", panel, groupAction, groupHeading, children }) => {
1298
2700
  const filtered = matchesFilter(items, filterValue);
1299
2701
  const [highlighted, setHighlighted] = useState();
1300
2702
  const [wasOpen, setWasOpen] = useState(open);
@@ -1346,6 +2748,15 @@ const PromptInputCommands = ({ items, open, onSelect, filterValue, emptyText = "
1346
2748
  acc[item.group] = group;
1347
2749
  return acc;
1348
2750
  }, {});
2751
+ const handleKeyDown = (event) => {
2752
+ if (!tabPicks || event.key !== "Tab" || event.shiftKey || !open || panel) return;
2753
+ if (!(event.target instanceof HTMLTextAreaElement)) return;
2754
+ if (event.defaultPrevented || event.nativeEvent.isComposing) return;
2755
+ const picked = filtered.find((item) => item.value === highlightedValue);
2756
+ if (!picked) return;
2757
+ event.preventDefault();
2758
+ onSelect(picked);
2759
+ };
1349
2760
  const menuStyle = { "--prompt-menu-max-h": `${maxHeight}px` };
1350
2761
  return /* @__PURE__ */ jsxs(Command, {
1351
2762
  ref: attachRoot,
@@ -1354,6 +2765,7 @@ const PromptInputCommands = ({ items, open, onSelect, filterValue, emptyText = "
1354
2765
  shouldFilter: false,
1355
2766
  value: highlightedValue ?? "",
1356
2767
  onValueChange: setHighlighted,
2768
+ onKeyDown: handleKeyDown,
1357
2769
  children: [open && /* @__PURE__ */ jsx("div", {
1358
2770
  className: cn("tw:absolute tw:left-0 tw:z-50 tw:w-full tw:max-w-lg", below ? "tw:top-full tw:mt-1" : "tw:bottom-full tw:mb-1", "tw:rounded-[14px] tw:border tw:border-border tw:bg-popover tw:text-popover-foreground tw:shadow-lg", "tw:p-1"),
1359
2771
  style: menuStyle,
@@ -1455,6 +2867,68 @@ const PromptInputSubmit = ({ ref, disabled, status = "ready" }) => {
1455
2867
  })] });
1456
2868
  };
1457
2869
  //#endregion
2870
+ //#region src/blocks/AIKit/usePromptHistory.ts
2871
+ /** The prompts ↑ steps through: blanks dropped, a prompt sent twice in a row kept once. */
2872
+ const toRecallStack = (history) => history.reduce((stack, prompt) => {
2873
+ if (prompt.trim() && stack.at(-1) !== prompt) stack.push(prompt);
2874
+ return stack;
2875
+ }, []);
2876
+ const isPlainArrow = (event) => (event.key === "ArrowUp" || event.key === "ArrowDown") && !event.altKey && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.nativeEvent.isComposing;
2877
+ /** Whether the caret sits on the text's first line (↑) or last line (↓), where a textarea has nowhere to move it. */
2878
+ const isOnEdgeLine = ({ key, currentTarget }) => {
2879
+ const { value, selectionStart, selectionEnd } = currentTarget;
2880
+ return key === "ArrowUp" ? !value.slice(0, selectionStart).includes("\n") : !value.slice(selectionEnd).includes("\n");
2881
+ };
2882
+ /**
2883
+ * Shell-style recall of the prompts already sent, on ↑ and ↓.
2884
+ *
2885
+ * - ↑ starts from the caret at the very start of the text.
2886
+ * - While browsing, ↑ and ↓ step only from the first and last line; inside a multi-line prompt they move the caret.
2887
+ * - ↓ past the newest prompt gives back the draft typed before browsing began.
2888
+ * - Browsing ends on any edit, or once the value is no longer the recalled prompt.
2889
+ */
2890
+ const usePromptHistory = ({ history, value, onRecall }) => {
2891
+ const stack = useMemo(() => toRecallStack(history ?? []), [history]);
2892
+ const browsingRef = useRef(null);
2893
+ const stop = () => {
2894
+ browsingRef.current = null;
2895
+ };
2896
+ const recall = (event, next, text) => {
2897
+ browsingRef.current = next;
2898
+ event.preventDefault();
2899
+ onRecall(text);
2900
+ return true;
2901
+ };
2902
+ const handleKeyDown = (event) => {
2903
+ if (stack.length === 0 || !isPlainArrow(event)) return false;
2904
+ const current = browsingRef.current;
2905
+ const browsing = current && stack[current.position] === value ? current : null;
2906
+ if (event.key === "ArrowUp") {
2907
+ const { selectionStart, selectionEnd } = event.currentTarget;
2908
+ if (browsing ? !isOnEdgeLine(event) : !(selectionStart === 0 && selectionEnd === 0)) return false;
2909
+ const position = (browsing?.position ?? stack.length) - 1;
2910
+ const older = stack[position];
2911
+ if (older === void 0) return false;
2912
+ return recall(event, {
2913
+ position,
2914
+ draft: browsing?.draft ?? value
2915
+ }, older);
2916
+ }
2917
+ if (!browsing || !isOnEdgeLine(event)) return false;
2918
+ const position = browsing.position + 1;
2919
+ const newer = stack[position];
2920
+ if (newer === void 0) return recall(event, null, browsing.draft);
2921
+ return recall(event, {
2922
+ ...browsing,
2923
+ position
2924
+ }, newer);
2925
+ };
2926
+ return {
2927
+ handleKeyDown,
2928
+ stop
2929
+ };
2930
+ };
2931
+ //#endregion
1458
2932
  //#region src/blocks/AIKit/PromptInput.tsx
1459
2933
  const CMDK_HIJACKED_KEYS = /* @__PURE__ */ new Set([
1460
2934
  "ArrowUp",
@@ -1544,9 +3018,11 @@ const wrapperVariants = cva(null, {
1544
3018
  inline: ""
1545
3019
  } }
1546
3020
  });
1547
- const PromptInput = ({ onSend, onAbort, onContinue, prefix, variant, suffix, attachments, className, disabled, disableSubmit, value = "", onValueChange, status = "ready", maxRows = 6, minRows, commands, commandsGroupAction, panel, mentions, onMentionSelect, mentionsEmptyText = "Nothing to attach", mentionsGroupHeading, mentionsMultiple = false, autoFocus, textareaRef: publishedRef, ...textareaProps }) => {
3021
+ const PromptInput = ({ onSend, onAbort, onContinue, prefix, variant, suffix, attachments, className, disabled, disableSubmit, busy = false, blurOnEscape = false, onKeyDown, onPasteFiles, canSubmitEmpty = false, allowSendWhileBusy = false, canAbort = true, value = "", onValueChange, status = "ready", maxRows = 6, minRows, commands, commandsEmptyText = "No commands found", commandsLoading = false, slashTrigger = "value", commandsGroupAction, panel, mentions, onMentionSelect, mentionsEmptyText = "Nothing to attach", mentionsGroupHeading, mentionsMultiple = false, history, autoFocus, textareaRef: publishedRef, ...textareaProps }) => {
1548
3022
  const defaultMinRows = variant === "inline" ? 1 : 2;
1549
- const isDisabled = (disabled || disableSubmit || !value.trim()) && status !== "streaming" && status !== "interrupted";
3023
+ const canSendWhileBusy = allowSendWhileBusy && (status === "streaming" || status === "submitted") && !busy && !disabled && !disableSubmit && value.trim() !== "";
3024
+ const showSeparateStop = allowSendWhileBusy && canAbort && (canSendWhileBusy || status === "submitted");
3025
+ const isDisabled = busy || !canSendWhileBusy && (disabled || disableSubmit || !canSubmitEmpty && !value.trim()) && status !== "streaming" && status !== "interrupted";
1550
3026
  const textareaId = useId();
1551
3027
  const textareaRef = useRef(null);
1552
3028
  const setTextareaNode = useCallback((node) => {
@@ -1565,20 +3041,37 @@ const PromptInput = ({ onSend, onAbort, onContinue, prefix, variant, suffix, att
1565
3041
  useEffect(() => {
1566
3042
  if (autoFocusRef.current) textareaRef.current?.focus({ preventScroll: true });
1567
3043
  }, []);
3044
+ const [typedSlashValue, setTypedSlashValue] = useState(null);
1568
3045
  if (dismissedValue !== null && value !== dismissedValue) setDismissedValue(null);
1569
- const hasCommands = !!commands && commands.length > 0;
3046
+ const hasCommands = commands !== void 0;
1570
3047
  const panelOpen = panel !== void 0;
1571
3048
  const hasMentions = mentions !== void 0;
1572
3049
  const hasMenus = hasCommands || hasMentions;
1573
- const slashToken = hasCommands && !panelOpen && value !== dismissedValue ? findSlashToken(value, Math.min(caret, value.length)) : null;
3050
+ const slashToken = hasCommands && !panelOpen && value !== dismissedValue && (slashTrigger === "value" || typedSlashValue === value) ? findSlashToken(value, Math.min(caret, value.length)) : null;
1574
3051
  const commandsOpen = slashToken !== null;
1575
3052
  const mentionToken = hasMentions && !commandsOpen && !panelOpen && value !== dismissedValue ? findMentionToken(value, Math.min(caret, value.length)) : null;
1576
3053
  const mentionsOpen = mentionToken !== null;
1577
3054
  const menuOpen = commandsOpen || mentionsOpen || panelOpen;
1578
3055
  const menuItems = commandsOpen ? commands ?? [] : mentions ?? [];
1579
3056
  const menuFilter = commandsOpen ? slashToken.query : mentionToken?.query ?? "";
3057
+ const promptHistory = usePromptHistory({
3058
+ history,
3059
+ value,
3060
+ onRecall: (text) => {
3061
+ onValueChange?.(text);
3062
+ moveCaret(text.length, text);
3063
+ setDismissedValue(text);
3064
+ requestAnimationFrame(() => {
3065
+ textareaRef.current?.setSelectionRange(text.length, text.length);
3066
+ });
3067
+ }
3068
+ });
1580
3069
  const handleSubmit = () => {
1581
3070
  if (isDisabled) return;
3071
+ if (canSendWhileBusy) {
3072
+ onSend?.(value);
3073
+ return;
3074
+ }
1582
3075
  if (status === "streaming") onAbort?.();
1583
3076
  if (status === "interrupted") onContinue?.();
1584
3077
  if (status === "ready") onSend?.(value);
@@ -1588,6 +3081,11 @@ const PromptInput = ({ onSend, onAbort, onContinue, prefix, variant, suffix, att
1588
3081
  handleSubmit();
1589
3082
  };
1590
3083
  const handleOnChange = (event) => {
3084
+ promptHistory.stop();
3085
+ const inputType = event.nativeEvent instanceof InputEvent ? event.nativeEvent.inputType : "";
3086
+ const isTyping = inputType === "insertText" || inputType.startsWith("delete");
3087
+ const startsSession = inputType === "insertText" && value === "" && event.target.value === "/";
3088
+ setTypedSlashValue(startsSession || isTyping && typedSlashValue === value ? event.target.value : null);
1591
3089
  moveCaret(event.target.selectionStart ?? event.target.value.length, event.target.value);
1592
3090
  onValueChange?.(event.target.value);
1593
3091
  };
@@ -1641,7 +3139,14 @@ const PromptInput = ({ onSend, onAbort, onContinue, prefix, variant, suffix, att
1641
3139
  }
1642
3140
  setDismissedValue(value);
1643
3141
  };
1644
- const handleOnKeyDown = (event) => {
3142
+ const handleOnPaste = (event) => {
3143
+ if (!onPasteFiles) return;
3144
+ const files = Array.from(event.clipboardData.files);
3145
+ if (files.length === 0) return;
3146
+ onPasteFiles(files);
3147
+ if (!event.clipboardData.getData("text/plain")) event.preventDefault();
3148
+ };
3149
+ const handleOnComposerKeyDown = (event) => {
1645
3150
  if (menuOpen) {
1646
3151
  if (event.key === "Escape") {
1647
3152
  event.preventDefault();
@@ -1653,11 +3158,18 @@ const PromptInput = ({ onSend, onAbort, onContinue, prefix, variant, suffix, att
1653
3158
  if (event.key === "ArrowUp" || event.key === "ArrowDown") return;
1654
3159
  if (event.key === "Enter" && matchesFilter(menuItems, menuFilter).length > 0) return;
1655
3160
  } else if (hasMenus && isCmdkNavigation(event)) event.stopPropagation();
3161
+ if (promptHistory.handleKeyDown(event)) return;
1656
3162
  if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.metaKey) {
1657
3163
  event.preventDefault();
1658
- if (status !== "streaming") handleSubmit();
3164
+ if (status !== "streaming" || canSendWhileBusy) handleSubmit();
1659
3165
  }
1660
3166
  };
3167
+ const handleOnKeyDown = (event) => {
3168
+ handleOnComposerKeyDown(event);
3169
+ if (event.defaultPrevented) return;
3170
+ if (blurOnEscape && event.key === "Escape") event.currentTarget.blur();
3171
+ onKeyDown?.(event);
3172
+ };
1661
3173
  const handleOnFormKeyDown = (event) => {
1662
3174
  if (hasMenus && event.target !== textareaRef.current && isCmdkNavigation(event)) event.stopPropagation();
1663
3175
  };
@@ -1710,15 +3222,37 @@ const PromptInput = ({ onSend, onAbort, onContinue, prefix, variant, suffix, att
1710
3222
  onChange: handleOnChange,
1711
3223
  onSelect: handleOnSelect,
1712
3224
  onKeyDown: handleOnKeyDown,
1713
- className: cn("tw:resize-none", "tw:outline-hidden", "tw:min-w-32 tw:flex-1", variant === "inline" ? "tw:@max-[480px]/composer:pl-2" : "tw:pl-2 tw:pr-0", "tw:bg-transparent", disabled ? "tw:text-muted-foreground" : "tw:text-foreground", "tw:text-sm", "tw:font-sans", "tw:transition-[width]", "tw:delay-75", "tw:ease-linear", "tw:overflow-auto")
3225
+ onPaste: handleOnPaste,
3226
+ readOnly: busy,
3227
+ "aria-busy": busy || void 0,
3228
+ className: cn("tw:resize-none", "tw:outline-hidden", "tw:min-w-32 tw:flex-1", variant === "inline" ? "tw:@max-[480px]/composer:pl-2" : "tw:pl-2 tw:pr-0", "tw:bg-transparent", disabled || busy ? "tw:text-muted-foreground" : "tw:text-foreground", "tw:text-sm", "tw:font-sans", "tw:transition-[width]", "tw:delay-75", "tw:ease-linear", "tw:overflow-auto")
1714
3229
  })
1715
3230
  }),
1716
3231
  /* @__PURE__ */ jsxs("div", {
1717
3232
  className: cn("tw:flex tw:items-center tw:gap-1", "tw:[&>*:last-child]:ml-1", "tw:shrink-0", variant === "inline" && "tw:pr-2.5 tw:@max-[480px]/composer:grow tw:@max-[480px]/composer:pr-0"),
1718
- children: [suffix, /* @__PURE__ */ jsx(PromptInputSubmit, {
1719
- disabled: isDisabled,
1720
- status
1721
- })]
3233
+ children: [
3234
+ suffix,
3235
+ showSeparateStop && /* @__PURE__ */ jsxs(Tooltip.Root, { children: [/* @__PURE__ */ jsx(Tooltip.Trigger, {
3236
+ asChild: true,
3237
+ children: /* @__PURE__ */ jsx(PromptInputAction, { children: /* @__PURE__ */ jsx(Button, {
3238
+ type: "button",
3239
+ onClick: () => onAbort?.(),
3240
+ "aria-label": "Stop",
3241
+ size: "icon-sm",
3242
+ variant: "outline",
3243
+ className: "tw:flex tw:items-center tw:rounded-full",
3244
+ children: /* @__PURE__ */ jsx(CirclePause, { className: "tw:size-4" })
3245
+ }) })
3246
+ }), /* @__PURE__ */ jsx(Tooltip.Content, {
3247
+ placement: "top",
3248
+ variant: "action",
3249
+ children: "Stop"
3250
+ })] }),
3251
+ /* @__PURE__ */ jsx(PromptInputSubmit, {
3252
+ disabled: isDisabled,
3253
+ status: canSendWhileBusy ? "ready" : status
3254
+ })
3255
+ ]
1722
3256
  })
1723
3257
  ]
1724
3258
  })
@@ -1730,7 +3264,8 @@ const PromptInput = ({ onSend, onAbort, onContinue, prefix, variant, suffix, att
1730
3264
  open: menuOpen,
1731
3265
  onSelect: handleMenuSelect,
1732
3266
  filterValue: menuFilter,
1733
- emptyText: mentionsOpen ? mentionsEmptyText : "No commands found",
3267
+ tabPicks: commandsOpen,
3268
+ emptyText: mentionsOpen ? mentionsEmptyText : commandsLoading ? "Loading commands…" : commandsEmptyText,
1734
3269
  panel: panel?.content,
1735
3270
  groupAction: commandsOpen ? commandsGroupAction : void 0,
1736
3271
  groupHeading: mentionsOpen ? mentionsGroupHeading : void 0,
@@ -1741,7 +3276,7 @@ PromptInput.Action = PromptInputAction;
1741
3276
  PromptInput.MenuTrigger = PromptInputMenuTrigger;
1742
3277
  //#endregion
1743
3278
  //#region src/blocks/AIKit/Question.tsx
1744
- const isEditableTarget = (target) => target instanceof HTMLElement && (target.isContentEditable || [
3279
+ const isEditableTarget$1 = (target) => target instanceof HTMLElement && (target.isContentEditable || [
1745
3280
  "INPUT",
1746
3281
  "TEXTAREA",
1747
3282
  "SELECT"
@@ -1752,6 +3287,11 @@ const isFieldWithText = (element) => {
1752
3287
  };
1753
3288
  const hasCommandModifier = (e) => e.altKey || e.ctrlKey || e.metaKey;
1754
3289
  const SEND_KEY_CLASS = "tw:!bg-white/20 tw:!text-primary-foreground";
3290
+ const DEFAULT_SKIP_TOOLTIP = /* @__PURE__ */ jsx(Kbd, {
3291
+ variant: "primary",
3292
+ casing: "as-written",
3293
+ children: "Space"
3294
+ });
1755
3295
  const keyRange = (count) => count > 1 ? `1–${count}` : "1";
1756
3296
  /** An option's key, or a blank slot past the ninth; a picked option's turns into a check. */
1757
3297
  const OptionKey = ({ digit, checked }) => digit === null && !checked ? /* @__PURE__ */ jsx("span", {
@@ -1770,7 +3310,7 @@ const handleKeyDown = (callback) => (e) => {
1770
3310
  callback();
1771
3311
  }
1772
3312
  };
1773
- const Question = ({ question, header, options, multiSelect = true, selectedOptionIds = [], onSelectionChange, customAnswer = "", onCustomAnswerChange, onCustomAnswerConfirm, showCustomAnswer = true, customAnswerPlaceholder = "Type your answer…", showSkip = true, onSkip, onClose, onNext, submitTooltip, onPrevious, currentStep, totalSteps, renderPreview, autoFocus = false, enableShortcuts = false, className }) => {
3313
+ const Question = ({ question, header, options, multiSelect = true, selectedOptionIds = [], onSelectionChange, customAnswer = "", onCustomAnswerChange, onCustomAnswerConfirm, showCustomAnswer = true, customAnswerPlaceholder = "Type your answer…", customAnswerMaxLength, showSkip = true, onSkip, skipTooltip = DEFAULT_SKIP_TOOLTIP, onClose, onNext, submitTooltip, isSubmitDisabled = false, isSubmitting = false, error, onPrevious, currentStep, totalSteps, renderPreview, autoFocus = false, enableShortcuts = false, className }) => {
1774
3314
  const [isCustomExpanded, setIsCustomExpanded] = useState(false);
1775
3315
  const [activeRow, setActiveRow] = useState(0);
1776
3316
  const [pendingId, setPendingId] = useState(null);
@@ -1784,6 +3324,7 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
1784
3324
  const chosenId = multiSelect ? null : pendingId ?? selectedOptionIds[0] ?? null;
1785
3325
  const showSubmitButton = multiSelect || isCustomSelected || chosenId !== null;
1786
3326
  const showFooter = showStepControls || showSkipButton || showSubmitButton;
3327
+ const canSend = !isSubmitDisabled && !isSubmitting;
1787
3328
  const progressValue = hasSteps ? currentStep / totalSteps * 100 : 0;
1788
3329
  const isFirstStep = currentStep === 1;
1789
3330
  const isLastStep = hasSteps && currentStep === totalSteps;
@@ -1834,7 +3375,7 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
1834
3375
  */
1835
3376
  const handleCardKeyDown = (e) => {
1836
3377
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && !e.altKey && !e.repeat) {
1837
- if (!showSubmitButton) return;
3378
+ if (!showSubmitButton || !canSend) return;
1838
3379
  send();
1839
3380
  e.preventDefault();
1840
3381
  e.stopPropagation();
@@ -1845,7 +3386,7 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
1845
3386
  focusRow(options.length);
1846
3387
  return;
1847
3388
  }
1848
- if (e.defaultPrevented || hasCommandModifier(e) || isEditableTarget(e.target)) return;
3389
+ if (e.defaultPrevented || hasCommandModifier(e) || isEditableTarget$1(e.target)) return;
1849
3390
  if ((e.key === "ArrowDown" || e.key === "ArrowUp") && rowCount > 0) {
1850
3391
  e.preventDefault();
1851
3392
  const delta = e.key === "ArrowDown" ? 1 : -1;
@@ -1871,7 +3412,7 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
1871
3412
  if (e.key === " " && e.target === e.currentTarget) {
1872
3413
  e.preventDefault();
1873
3414
  e.stopPropagation();
1874
- if (showSkipButton) onSkip?.();
3415
+ if (showSkipButton && !isSubmitting) onSkip?.();
1875
3416
  return;
1876
3417
  }
1877
3418
  if (showCustomAnswer && digit === null && e.key.length === 1 && e.key !== " ") {
@@ -1912,6 +3453,8 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
1912
3453
  variant: "primary",
1913
3454
  size: "small",
1914
3455
  onClick: send,
3456
+ disabled: !canSend,
3457
+ isLoading: isSubmitting,
1915
3458
  "data-testid": "question-next-button",
1916
3459
  children: [isLastStep ? "Submit" : "Next", /* @__PURE__ */ jsx(Kbd, {
1917
3460
  "aria-hidden": true,
@@ -1920,6 +3463,15 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
1920
3463
  children: `${keys.mod.symbol} Enter`
1921
3464
  })]
1922
3465
  });
3466
+ const skipButton = /* @__PURE__ */ jsx(Button, {
3467
+ variant: "outline",
3468
+ size: "small",
3469
+ onClick: onSkip,
3470
+ disabled: isSubmitting,
3471
+ "aria-keyshortcuts": "Space",
3472
+ "data-testid": "question-skip-button",
3473
+ children: "Skip"
3474
+ });
1923
3475
  return /* @__PURE__ */ jsxs("div", {
1924
3476
  ref: rootRef,
1925
3477
  role: "group",
@@ -1963,7 +3515,7 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
1963
3515
  ref: (row) => {
1964
3516
  rowsRef.current[index] = row;
1965
3517
  },
1966
- className: "tw:-mx-2 tw:-my-1.5 tw:flex tw:cursor-pointer tw:items-start tw:gap-2 tw:rounded-md tw:px-2 tw:py-1.5 tw:outline-none tw:hover:bg-accent tw:focus-visible:ring-2 tw:focus-visible:ring-ring",
3518
+ className: "tw:-mx-2 tw:-my-1.5 tw:flex tw:cursor-pointer tw:items-start tw:gap-2 tw:rounded-md tw:px-2 tw:py-1.5 tw:outline-none tw:hover:bg-hover tw:focus-visible:ring-2 tw:focus-visible:ring-ring",
1967
3519
  onClick: () => handleOptionToggle(option.id),
1968
3520
  onKeyDown: (e) => {
1969
3521
  if (hasCommandModifier(e) || e.repeat) return;
@@ -1986,7 +3538,16 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
1986
3538
  className: "tw:flex tw:min-w-0 tw:flex-1 tw:items-start tw:gap-3",
1987
3539
  children: [/* @__PURE__ */ jsxs("div", {
1988
3540
  className: "tw:flex tw:flex-col tw:gap-1.5",
1989
- children: [/* @__PURE__ */ jsx("span", {
3541
+ children: [option.badge ? /* @__PURE__ */ jsxs("span", {
3542
+ className: "tw:flex tw:flex-wrap tw:items-center tw:gap-x-2 tw:gap-y-1 tw:text-sm tw:font-medium tw:leading-none",
3543
+ children: [option.label, /* @__PURE__ */ jsx(Badge, {
3544
+ variant: "pill",
3545
+ color: "green-inverse",
3546
+ className: "tw:normal-case tw:tracking-normal",
3547
+ "data-testid": "question-option-badge",
3548
+ children: option.badge
3549
+ })]
3550
+ }) : /* @__PURE__ */ jsx("span", {
1990
3551
  className: "tw:text-sm tw:font-medium tw:leading-none",
1991
3552
  children: option.label
1992
3553
  }), option.description && /* @__PURE__ */ jsx("span", {
@@ -2009,7 +3570,7 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
2009
3570
  ref: (row) => {
2010
3571
  rowsRef.current[options.length] = row;
2011
3572
  },
2012
- className: "tw:-mx-2 tw:-my-1.5 tw:flex tw:cursor-pointer tw:items-start tw:gap-2 tw:rounded-md tw:px-2 tw:py-1.5 tw:outline-none tw:hover:bg-accent tw:focus-visible:ring-2 tw:focus-visible:ring-ring",
3573
+ className: "tw:-mx-2 tw:-my-1.5 tw:flex tw:cursor-pointer tw:items-start tw:gap-2 tw:rounded-md tw:px-2 tw:py-1.5 tw:outline-none tw:hover:bg-hover tw:focus-visible:ring-2 tw:focus-visible:ring-ring",
2013
3574
  onClick: handleCustomRowClick,
2014
3575
  onKeyDown: handleKeyDown(handleCustomRowKey),
2015
3576
  onFocus: () => setActiveRow(options.length),
@@ -2036,6 +3597,7 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
2036
3597
  }
2037
3598
  } : void 0,
2038
3599
  placeholder: customAnswerPlaceholder,
3600
+ maxLength: customAnswerMaxLength,
2039
3601
  sizing: "small",
2040
3602
  testId: "question-custom"
2041
3603
  })]
@@ -2067,6 +3629,12 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
2067
3629
  })
2068
3630
  ]
2069
3631
  }),
3632
+ error != null && /* @__PURE__ */ jsx("div", {
3633
+ role: "alert",
3634
+ "data-testid": "question-error",
3635
+ className: "tw:mx-4 tw:-mt-1 tw:rounded-md tw:bg-destructive/10 tw:px-3 tw:py-2 tw:text-sm tw:text-destructive",
3636
+ children: error
3637
+ }),
2070
3638
  /* @__PURE__ */ jsxs("div", {
2071
3639
  className: "tw:flex tw:flex-col",
2072
3640
  children: [showStepControls && /* @__PURE__ */ jsx(Progress, {
@@ -2105,23 +3673,12 @@ const Question = ({ question, header, options, multiSelect = true, selectedOptio
2105
3673
  })
2106
3674
  ]
2107
3675
  }),
2108
- showSkipButton && /* @__PURE__ */ jsx(Tooltip, {
2109
- tooltip: /* @__PURE__ */ jsx(Kbd, {
2110
- variant: "primary",
2111
- casing: "as-written",
2112
- children: "Space"
2113
- }),
3676
+ showSkipButton && (skipTooltip == null ? skipButton : /* @__PURE__ */ jsx(Tooltip, {
3677
+ tooltip: skipTooltip,
2114
3678
  placement: "top",
2115
3679
  triggerAsChild: true,
2116
- children: /* @__PURE__ */ jsx(Button, {
2117
- variant: "outline",
2118
- size: "small",
2119
- onClick: onSkip,
2120
- "aria-keyshortcuts": "Space",
2121
- "data-testid": "question-skip-button",
2122
- children: "Skip"
2123
- })
2124
- }),
3680
+ children: skipButton
3681
+ })),
2125
3682
  showSubmitButton && (submitTooltip ? /* @__PURE__ */ jsx(Tooltip, {
2126
3683
  tooltip: submitTooltip,
2127
3684
  placement: "top",
@@ -2179,22 +3736,188 @@ const QuestionBlock = ({ steps, showSkip = true, onSubmit, onClose, renderPrevie
2179
3736
  }, currentStep);
2180
3737
  };
2181
3738
  //#endregion
3739
+ //#region src/blocks/AIKit/streamingMarkdown.ts
3740
+ /** A fence delimiter line: up to three spaces, then three or more backticks or tildes. */
3741
+ const FENCE_LINE = /^ {0,3}(`{3,}|~{3,})/;
3742
+ /** A list-item marker: a bullet or a `1.`/`1)` ordinal, then a space or the line end. */
3743
+ const LIST_MARKER = /^ {0,3}(?:[-*+]|\d{1,9}[.)])(?:\s|$)/;
3744
+ /** A line at column four continues whatever block sits above a blank line. */
3745
+ const INDENTED = /^(?: {4}| {0,3}\t)/;
3746
+ /** A line that continues an open list: one to three spaces, or another marker. */
3747
+ const CONTINUES_LIST = /^(?: {1,3}|(?:[-*+]|\d{1,9}[.)])(?:\s|$))/;
3748
+ /** A line that can only be a GFM table row: it opens with a pipe. */
3749
+ const TABLE_ROW = /^ {0,3}\|/;
3750
+ /** A delimiter row still streaming: pipes, dashes, colons and spaces only. */
3751
+ const TABLE_DELIMITER_PARTIAL = /^ {0,3}\|?[ :|-]*$/;
3752
+ /** Past this length a lone pipe line with no second pipe is prose, not a header. */
3753
+ const TABLE_HEAD_FIRST_CELL_MAX = 80;
3754
+ /** Whether `line` closes a fence opened with `marker`: same character, at least as long, nothing after. */
3755
+ const closesFence = (line, marker) => {
3756
+ const match = FENCE_LINE.exec(line);
3757
+ return !!match?.[1] && match[1][0] === marker[0] && match[1].length >= marker.length && !line.slice(match[0].length).trim();
3758
+ };
3759
+ /**
3760
+ * Every offset where all blocks before it are closed.
3761
+ *
3762
+ * - A cut is the end of a blank line outside a fence whose next line opens a fresh block.
3763
+ * - The last line is never classified: it is still streaming.
3764
+ * - An indented line, or a list continuation while a list is open, defers the cut.
3765
+ */
3766
+ const sealableBoundaries = (text) => {
3767
+ const cuts = [];
3768
+ let pos = 0;
3769
+ let fence = null;
3770
+ let pendingBlank = -1;
3771
+ let listOpen = false;
3772
+ const cutAtBlank = () => {
3773
+ if (pendingBlank > 0) cuts.push(pendingBlank);
3774
+ listOpen = false;
3775
+ };
3776
+ while (pos < text.length) {
3777
+ const nl = text.indexOf("\n", pos);
3778
+ if (nl === -1) break;
3779
+ const line = text.slice(pos, nl);
3780
+ pos = nl + 1;
3781
+ if (fence !== null) {
3782
+ if (closesFence(line, fence)) fence = null;
3783
+ continue;
3784
+ }
3785
+ const continuesList = listOpen && CONTINUES_LIST.test(line);
3786
+ const opener = FENCE_LINE.exec(line)?.[1];
3787
+ if (opener !== void 0) {
3788
+ fence = opener;
3789
+ if (pendingBlank !== -1 && !continuesList) cutAtBlank();
3790
+ pendingBlank = -1;
3791
+ } else if (!line.trim()) pendingBlank = pos;
3792
+ else {
3793
+ const continues = INDENTED.test(line) || continuesList;
3794
+ if (pendingBlank !== -1 && !continues) cutAtBlank();
3795
+ pendingBlank = -1;
3796
+ if (LIST_MARKER.test(line)) listOpen = true;
3797
+ }
3798
+ }
3799
+ return cuts;
3800
+ };
3801
+ /**
3802
+ * A streaming answer split into blocks that can no longer change and the tail that still can.
3803
+ *
3804
+ * - Render each `sealed` block in its own memoised `Markdown.Block`, keyed by index; only `tail` re-parses.
3805
+ * - A sealed block's text never changes as more text arrives.
3806
+ * - Reference links and footnotes defined in a later block resolve only once the full answer renders.
3807
+ */
3808
+ const splitSealedBlocks = (text) => {
3809
+ const sealed = [];
3810
+ let from = 0;
3811
+ for (const cut of sealableBoundaries(text)) {
3812
+ sealed.push(text.slice(from, cut));
3813
+ from = cut;
3814
+ }
3815
+ return {
3816
+ sealed,
3817
+ tail: text.slice(from)
3818
+ };
3819
+ };
3820
+ /**
3821
+ * The fence still open at the end of `text`, if any.
3822
+ *
3823
+ * - `language` is `null` while the info string is still streaming (no newline yet).
3824
+ */
3825
+ const findOpenFence = (text) => {
3826
+ let open = null;
3827
+ let pos = 0;
3828
+ for (;;) {
3829
+ const nl = text.indexOf("\n", pos);
3830
+ const line = text.slice(pos, nl === -1 ? text.length : nl);
3831
+ if (open !== null) {
3832
+ if (nl !== -1 && closesFence(line, open.marker)) open = null;
3833
+ } else {
3834
+ const match = FENCE_LINE.exec(line);
3835
+ if (match?.[1]) {
3836
+ const info = line.slice(match[0].length).trim().split(/\s+/)[0] ?? "";
3837
+ open = {
3838
+ start: pos,
3839
+ marker: match[1],
3840
+ language: nl === -1 ? null : info.toLowerCase()
3841
+ };
3842
+ }
3843
+ }
3844
+ if (nl === -1) break;
3845
+ pos = nl + 1;
3846
+ }
3847
+ return open && {
3848
+ start: open.start,
3849
+ language: open.language
3850
+ };
3851
+ };
3852
+ /**
3853
+ * Drops a table header still waiting for its delimiter row.
3854
+ *
3855
+ * - Until the delimiter row ends, GFM renders the header as a paragraph of raw pipes.
3856
+ * - Two shapes are withheld at the end: a lone pipe line, or that line plus a partial delimiter.
3857
+ * - A data row is never withheld: it has table rows above it.
3858
+ */
3859
+ const withholdTableHead = (text) => {
3860
+ const lastNl = text.lastIndexOf("\n");
3861
+ const last = text.slice(lastNl + 1);
3862
+ const prevNl = lastNl === -1 ? -1 : text.lastIndexOf("\n", lastNl - 1);
3863
+ const prev = lastNl === -1 ? null : text.slice(prevNl + 1, lastNl);
3864
+ const prevIsRow = prev !== null && TABLE_ROW.test(prev);
3865
+ if (TABLE_ROW.test(last) && !prevIsRow) {
3866
+ const firstPipe = last.indexOf("|");
3867
+ return last.indexOf("|", firstPipe + 1) === -1 && last.length > TABLE_HEAD_FIRST_CELL_MAX ? text : text.slice(0, lastNl + 1);
3868
+ }
3869
+ if (prevIsRow && TABLE_DELIMITER_PARTIAL.test(last)) {
3870
+ const prev2Nl = prevNl === -1 ? -1 : text.lastIndexOf("\n", prevNl - 1);
3871
+ const prev2 = prevNl === -1 ? null : text.slice(prev2Nl + 1, prevNl);
3872
+ if (prev2 === null || !TABLE_ROW.test(prev2)) return text.slice(0, prevNl + 1);
3873
+ }
3874
+ return text;
3875
+ };
3876
+ /**
3877
+ * The part of a streaming answer that is safe to paint now.
3878
+ *
3879
+ * - An open fence whose language is still being typed is always held back.
3880
+ * - An open fence in a `diagram` language is held back, or every chunk redraws the chart.
3881
+ * - Any other open fence streams as a growing code block.
3882
+ */
3883
+ const withholdOpenTail = (text, { diagram = false, tableHead = false } = {}) => {
3884
+ const fence = findOpenFence(text);
3885
+ const diagramLanguages = diagram === true ? DIAGRAM_LANGUAGES : diagram === false ? [] : diagram;
3886
+ const visible = fence === null ? text : fence.language === null || diagramLanguages.includes(fence.language) ? text.slice(0, fence.start) : null;
3887
+ if (visible === null) return text;
3888
+ return tableHead ? withholdTableHead(visible) : visible;
3889
+ };
3890
+ //#endregion
2182
3891
  //#region src/blocks/AIKit/SuggestedPrompt.tsx
2183
- const SuggestedPrompt = ({ label, disabled, onClick, icon, variant = "row", className }) => {
3892
+ const SuggestedPrompt = ({ label, disabled, onClick, icon, description, trailing, "aria-label": ariaLabel, variant = "row", className }) => {
2184
3893
  return /* @__PURE__ */ jsxs("button", {
2185
3894
  type: "button",
2186
3895
  disabled,
3896
+ "aria-label": ariaLabel,
2187
3897
  "data-variant": variant,
2188
- className: cn("tailwindcss-preflight tw:flex tw:items-center tw:justify-start tw:gap-2.5 tw:whitespace-nowrap", "tw:text-left tw:text-sm tw:text-foreground tw:transition-colors", "tw:focus-visible:outline-none tw:focus-visible:ring-2 tw:focus-visible:ring-ring tw:focus-visible:ring-offset-1", "tw:disabled:cursor-not-allowed tw:disabled:opacity-50", variant === "bubble" ? [
3898
+ className: cn("tailwindcss-preflight tw:flex tw:items-center tw:justify-start tw:gap-2.5 tw:whitespace-nowrap", "tw:text-left tw:text-sm tw:text-foreground tw:transition-colors", "tw:focus-visible:outline-none tw:focus-visible:ring-2 tw:focus-visible:ring-ring tw:focus-visible:ring-offset-1", "tw:disabled:cursor-not-allowed tw:disabled:opacity-disabled", variant === "bubble" ? [
2189
3899
  "tw:w-fit tw:max-w-[min(85%,40rem)] tw:rounded-xl tw:px-3 tw:py-2",
2190
3900
  "tw:border tw:border-solid tw:border-border tw:bg-card",
2191
- "tw:not-disabled:hover:bg-muted tw:focus-visible:bg-muted"
2192
- ] : "tw:rounded-lg tw:px-3 tw:py-2 tw:font-medium tw:not-disabled:hover:bg-accent", className),
3901
+ "tw:not-disabled:hover:bg-hover tw:focus-visible:bg-hover"
3902
+ ] : "tw:rounded-lg tw:px-3 tw:py-2 tw:font-medium tw:not-disabled:hover:bg-hover", className),
2193
3903
  onClick,
2194
- children: [icon ?? /* @__PURE__ */ jsx(Sparkles, { className: "tw:size-4 tw:shrink-0 tw:basis-4 tw:[&_*]:stroke-aigradient-600" }), /* @__PURE__ */ jsx("div", {
2195
- className: "tw:whitespace-normal tw:text-left",
2196
- children: label
2197
- })]
3904
+ children: [
3905
+ icon ?? /* @__PURE__ */ jsx(Sparkles, { className: "tw:size-4 tw:shrink-0 tw:basis-4 tw:[&_*]:stroke-aigradient-600" }),
3906
+ description === void 0 ? /* @__PURE__ */ jsx("div", {
3907
+ className: "tw:whitespace-normal tw:text-left",
3908
+ children: label
3909
+ }) : /* @__PURE__ */ jsxs("div", {
3910
+ className: "tw:flex tw:min-w-0 tw:flex-1 tw:flex-col tw:gap-0.5 tw:whitespace-normal tw:text-left",
3911
+ children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx("span", {
3912
+ className: "tw:text-xs tw:font-normal tw:text-muted-foreground",
3913
+ children: description
3914
+ })]
3915
+ }),
3916
+ trailing !== void 0 && /* @__PURE__ */ jsx("span", {
3917
+ className: "tw:ml-auto tw:shrink-0",
3918
+ children: trailing
3919
+ })
3920
+ ]
2198
3921
  });
2199
3922
  };
2200
3923
  //#endregion
@@ -2223,7 +3946,128 @@ const ThinkingIndicator = ({ label = "Thinking..." }) => {
2223
3946
  });
2224
3947
  };
2225
3948
  //#endregion
2226
- //#region src/blocks/AIKit/ToolCall/ToolCallApproval.tsx
3949
+ //#region src/blocks/AIKit/ToolCall/ToolCallApprovalActions.tsx
3950
+ const ShortcutHint = ({ shortcutKey }) => /* @__PURE__ */ jsxs(Kbd.Group, {
3951
+ separator: "+",
3952
+ children: [shortcutKey === "mod-enter" && /* @__PURE__ */ jsx(Kbd, {
3953
+ variant: "primary",
3954
+ children: keys.mod.symbol
3955
+ }), /* @__PURE__ */ jsx(Kbd, {
3956
+ variant: "primary",
3957
+ children: keys.enter.symbol
3958
+ })]
3959
+ });
3960
+ const BUTTON_CLASS = "tw:shrink-0 tw:whitespace-nowrap";
3961
+ const ToolCallApprovalActions = ({ onApprove, onReject, extraAction, approveLabel, rejectLabel, approveVariant, pendingAction, isDisabled, shortcutKey }) => {
3962
+ const isLocked = isDisabled || pendingAction != null;
3963
+ const approveButton = onApprove && /* @__PURE__ */ jsx(Button, {
3964
+ "data-testid": "tool-approve-button",
3965
+ variant: approveVariant,
3966
+ size: "small",
3967
+ className: BUTTON_CLASS,
3968
+ onClick: onApprove,
3969
+ disabled: isLocked,
3970
+ isLoading: pendingAction === "approve",
3971
+ children: approveLabel
3972
+ });
3973
+ return /* @__PURE__ */ jsxs("div", {
3974
+ className: "tw:flex tw:items-center tw:justify-end tw:gap-2 tw:overflow-x-auto",
3975
+ children: [
3976
+ onReject && /* @__PURE__ */ jsx(Button, {
3977
+ "data-testid": "tool-decline-button",
3978
+ variant: "outline",
3979
+ size: "small",
3980
+ className: BUTTON_CLASS,
3981
+ onClick: onReject,
3982
+ disabled: isLocked,
3983
+ isLoading: pendingAction === "reject",
3984
+ children: rejectLabel
3985
+ }),
3986
+ extraAction && /* @__PURE__ */ jsx(Button, {
3987
+ "data-testid": "tool-extra-action-button",
3988
+ variant: "outline",
3989
+ size: "small",
3990
+ className: BUTTON_CLASS,
3991
+ onClick: extraAction.onClick,
3992
+ disabled: isLocked,
3993
+ isLoading: pendingAction === "extra",
3994
+ children: extraAction.label
3995
+ }),
3996
+ approveButton && shortcutKey !== "none" ? /* @__PURE__ */ jsx(Tooltip, {
3997
+ tooltip: /* @__PURE__ */ jsx(ShortcutHint, { shortcutKey }),
3998
+ placement: "top",
3999
+ triggerAsChild: true,
4000
+ children: approveButton
4001
+ }) : approveButton
4002
+ ]
4003
+ });
4004
+ };
4005
+ //#endregion
4006
+ //#region src/blocks/AIKit/ToolCall/ToolCallApprovalNotice.tsx
4007
+ const TONE = {
4008
+ warning: {
4009
+ icon: TriangleAlert,
4010
+ className: "tw:bg-warning/10 tw:text-warning"
4011
+ },
4012
+ destructive: {
4013
+ icon: CircleAlert,
4014
+ className: "tw:bg-destructive/10 tw:text-destructive"
4015
+ },
4016
+ info: {
4017
+ icon: Info,
4018
+ className: "tw:bg-info/10 tw:text-info"
4019
+ }
4020
+ };
4021
+ const ToolCallApprovalNotice = ({ tone, content, icon }) => {
4022
+ const Icon = icon ?? TONE[tone].icon;
4023
+ return /* @__PURE__ */ jsxs("p", {
4024
+ "data-testid": "tool-call-approval-notice",
4025
+ "data-tone": tone,
4026
+ className: cn("tw:m-0 tw:flex tw:items-start tw:gap-2 tw:rounded-md tw:px-2.5 tw:py-1.5 tw:text-sm", TONE[tone].className),
4027
+ children: [/* @__PURE__ */ jsx(Icon, {
4028
+ className: "tw:mt-0.5 tw:size-4 tw:shrink-0",
4029
+ "aria-hidden": true
4030
+ }), /* @__PURE__ */ jsx("span", {
4031
+ className: "tw:min-w-0 tw:text-foreground",
4032
+ children: content
4033
+ })]
4034
+ });
4035
+ };
4036
+ //#endregion
4037
+ //#region src/blocks/AIKit/ToolCall/ToolCallApprovalRecord.tsx
4038
+ const ToolCallApprovalRecord = ({ status, label, subject, resolvedBy, resolvedAt, className }) => {
4039
+ const isApproved = status === "approved";
4040
+ const Icon = isApproved ? CircleCheck : CircleX;
4041
+ return /* @__PURE__ */ jsxs("p", {
4042
+ "data-testid": "tool-call-approval",
4043
+ "data-status": status,
4044
+ "data-density": "record",
4045
+ className: cn("tw:m-0 tw:flex tw:min-w-0 tw:items-center tw:gap-1.5 tw:text-xs tw:text-muted-foreground", className),
4046
+ children: [/* @__PURE__ */ jsx(Icon, {
4047
+ "aria-hidden": true,
4048
+ className: cn("tw:size-3.5 tw:shrink-0", isApproved ? "tw:text-success" : "tw:opacity-70")
4049
+ }), /* @__PURE__ */ jsxs("span", {
4050
+ className: "tw:flex tw:min-w-0 tw:flex-wrap tw:items-baseline tw:gap-x-1.5",
4051
+ children: [
4052
+ /* @__PURE__ */ jsx("span", {
4053
+ className: "tw:font-medium tw:text-foreground",
4054
+ children: label
4055
+ }),
4056
+ subject != null && /* @__PURE__ */ jsx("span", {
4057
+ className: "tw:min-w-0 tw:truncate",
4058
+ children: subject
4059
+ }),
4060
+ resolvedBy != null && /* @__PURE__ */ jsx("span", { children: resolvedBy }),
4061
+ resolvedAt && /* @__PURE__ */ jsx("time", {
4062
+ dateTime: resolvedAt.toISOString(),
4063
+ children: resolvedAt.toLocaleString()
4064
+ })
4065
+ ]
4066
+ })]
4067
+ });
4068
+ };
4069
+ //#endregion
4070
+ //#region src/blocks/AIKit/ToolCall/toolCallApprovalStatus.ts
2227
4071
  const STATUS_BADGE = {
2228
4072
  pending: {
2229
4073
  label: "Needs approval",
@@ -2239,93 +4083,163 @@ const STATUS_BADGE = {
2239
4083
  },
2240
4084
  expired: {
2241
4085
  label: "Expired",
4086
+ color: "orange"
4087
+ },
4088
+ "timed-out": {
4089
+ label: "Timed out",
4090
+ color: "orange"
4091
+ },
4092
+ cancelled: {
4093
+ label: "Cancelled",
4094
+ color: "gray"
4095
+ },
4096
+ unattended: {
4097
+ label: "Unattended",
2242
4098
  color: "gray"
2243
4099
  }
2244
4100
  };
4101
+ //#endregion
4102
+ //#region src/blocks/AIKit/ToolCall/useApprovalShortcut.ts
4103
+ const isEditableTarget = (target) => target instanceof HTMLElement && (target.isContentEditable || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT");
4104
+ const isModPressed = (event) => isMac ? event.metaKey : event.ctrlKey;
4105
+ const matchesShortcut = (event, shortcutKey) => {
4106
+ if (event.key !== "Enter" || event.shiftKey || event.altKey) return false;
4107
+ if (shortcutKey === "mod-enter") return isModPressed(event);
4108
+ return !event.metaKey && !event.ctrlKey && !isEditableTarget(event.target);
4109
+ };
2245
4110
  /**
2246
- * Per-tool-call approve/reject frame — the granular sibling of `PlanApproval`
2247
- * (which approves a whole plan). Chrome + status + actions only: the app
2248
- * renders the call detail as `children` and owns the SDK/permission wiring.
4111
+ * Approves on `shortcutKey` from anywhere on the page while `isEnabled`.
4112
+ *
4113
+ * - `enter` skips keystrokes typed into a field; `mod-enter` fires from one too.
4114
+ * - Ignores key-repeat, IME composition and an event another handler already took.
2249
4115
  */
2250
- const ToolCallApproval = ({ toolName, title, status, onApprove, onReject, children, actions, isSubmitting = false, enableShortcuts = false, approveLabel = "Approve", rejectLabel = "Reject", className }) => {
2251
- const isPending = status === "pending";
2252
- const hasCustomActions = actions != null;
2253
- const showActions = isPending && (isPending && !hasCustomActions && Boolean(onApprove || onReject) || hasCustomActions);
2254
- const badge = STATUS_BADGE[status];
4116
+ const useApprovalShortcut = ({ shortcutKey, isEnabled, onApprove }) => {
2255
4117
  useEffect(() => {
2256
- if (!enableShortcuts || !isPending || isSubmitting) return;
2257
- const isEditableTarget = (target) => {
2258
- if (!(target instanceof HTMLElement)) return false;
2259
- return target.isContentEditable || target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT";
2260
- };
4118
+ if (shortcutKey === "none" || !isEnabled || !onApprove) return;
2261
4119
  const handleKeyDown = (event) => {
2262
- if (event.repeat || event.defaultPrevented || isEditableTarget(event.target)) return;
2263
- if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.metaKey && onApprove) {
2264
- event.preventDefault();
2265
- onApprove();
2266
- } else if (event.key === "Escape" && onReject) {
2267
- event.preventDefault();
2268
- onReject();
2269
- }
4120
+ if (event.repeat || event.isComposing || event.defaultPrevented) return;
4121
+ if (!matchesShortcut(event, shortcutKey)) return;
4122
+ event.preventDefault();
4123
+ onApprove();
2270
4124
  };
2271
4125
  window.addEventListener("keydown", handleKeyDown);
2272
4126
  return () => window.removeEventListener("keydown", handleKeyDown);
2273
4127
  }, [
2274
- enableShortcuts,
2275
- isPending,
2276
- isSubmitting,
2277
- onApprove,
2278
- onReject
4128
+ shortcutKey,
4129
+ isEnabled,
4130
+ onApprove
2279
4131
  ]);
4132
+ };
4133
+ //#endregion
4134
+ //#region src/blocks/AIKit/ToolCall/ToolCallApproval.tsx
4135
+ /**
4136
+ * Per-tool-call approve/reject frame — the granular sibling of `PlanApproval`.
4137
+ *
4138
+ * - Chrome, status and actions only: the app renders the call detail and owns the permission wiring.
4139
+ * - Actions render only while `pending`.
4140
+ */
4141
+ const ToolCallApproval = ({ status, statusLabel, statusMessage, density = "card", resolvedBy, resolvedAt, toolName, eyebrow, title, children, notices = [], aside, error, onApprove, onReject, extraAction, approveLabel = "Approve", rejectLabel = "Reject", approveVariant = "primary", pendingAction = null, isDisabled = false, shortcutKey = "none", isShortcutHintOnly = false, className }) => {
4142
+ const isPending = status === "pending";
4143
+ const badge = STATUS_BADGE[status];
4144
+ const label = statusLabel ?? badge.label;
4145
+ const showActions = isPending && Boolean(onApprove || onReject || extraAction);
4146
+ useApprovalShortcut({
4147
+ shortcutKey: isShortcutHintOnly ? "none" : shortcutKey,
4148
+ isEnabled: isPending && !isDisabled && pendingAction == null,
4149
+ onApprove
4150
+ });
4151
+ if (density === "record" && !isPending) return /* @__PURE__ */ jsx(ToolCallApprovalRecord, {
4152
+ status,
4153
+ label,
4154
+ subject: title ?? toolName,
4155
+ resolvedBy,
4156
+ resolvedAt,
4157
+ className
4158
+ });
4159
+ const showBadge = !isPending || eyebrow == null;
4160
+ const hasHeadline = showBadge || toolName != null;
2280
4161
  return /* @__PURE__ */ jsxs("section", {
2281
4162
  "data-testid": "tool-call-approval",
2282
4163
  "data-status": status,
2283
- "aria-label": typeof title === "string" ? title : toolName,
2284
- className: cn("tw:flex tw:w-full tw:max-w-[800px] tw:flex-col tw:gap-3 tw:overflow-hidden tw:rounded-xl tw:border tw:border-solid tw:border-border tw:bg-muted tw:p-4", className),
2285
- children: [
2286
- /* @__PURE__ */ jsxs("div", {
2287
- className: "tw:flex tw:items-center tw:gap-2",
2288
- children: [/* @__PURE__ */ jsx(Badge, {
2289
- color: badge.color,
2290
- variant: "pill",
2291
- className: "tw:normal-case tw:tracking-normal",
2292
- children: badge.label
2293
- }), /* @__PURE__ */ jsx("code", {
2294
- className: "tw:rounded tw:bg-background tw:px-1.5 tw:py-0.5 tw:font-mono tw:text-xs tw:text-foreground",
2295
- children: toolName
2296
- })]
2297
- }),
2298
- title != null && /* @__PURE__ */ jsx("p", {
2299
- className: "tw:m-0 tw:text-sm tw:font-medium tw:text-foreground",
2300
- children: title
2301
- }),
2302
- children,
2303
- showActions && /* @__PURE__ */ jsx("div", {
2304
- className: "tw:flex tw:items-center tw:justify-end tw:gap-2 tw:overflow-x-auto",
2305
- children: actions ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [onReject && /* @__PURE__ */ jsx(Button, {
2306
- variant: "outline",
2307
- size: "small",
2308
- onClick: onReject,
2309
- disabled: isSubmitting,
2310
- className: "tw:shrink-0 tw:whitespace-nowrap",
2311
- children: rejectLabel
2312
- }), onApprove && /* @__PURE__ */ jsx(Button, {
2313
- variant: "primary",
2314
- size: "small",
2315
- onClick: onApprove,
2316
- disabled: isSubmitting,
2317
- className: "tw:shrink-0 tw:whitespace-nowrap",
2318
- children: approveLabel
2319
- })] })
2320
- })
2321
- ]
4164
+ "data-density": "card",
4165
+ "aria-label": typeof title === "string" ? title : toolName ?? label,
4166
+ className: cn("tw:flex tw:w-full tw:max-w-[800px] tw:flex-col tw:overflow-hidden tw:rounded-lg tw:border tw:border-solid tw:border-border tw:bg-card tw:text-card-foreground", className),
4167
+ children: [eyebrow, /* @__PURE__ */ jsxs("div", {
4168
+ className: "tw:flex tw:flex-col tw:gap-3 tw:p-3",
4169
+ children: [
4170
+ (hasHeadline || title != null || !isPending && statusMessage != null) && /* @__PURE__ */ jsxs("div", {
4171
+ className: "tw:flex tw:flex-col tw:gap-1.5",
4172
+ children: [
4173
+ hasHeadline && /* @__PURE__ */ jsxs("div", {
4174
+ className: "tw:flex tw:flex-wrap tw:items-center tw:gap-2",
4175
+ children: [showBadge && /* @__PURE__ */ jsx(Badge, {
4176
+ color: badge.color,
4177
+ variant: "pill",
4178
+ className: "tw:normal-case tw:tracking-normal",
4179
+ children: label
4180
+ }), toolName != null && /* @__PURE__ */ jsx("code", {
4181
+ className: "tw:rounded tw:bg-muted tw:px-1.5 tw:py-0.5 tw:font-mono tw:text-xs tw:text-foreground",
4182
+ children: toolName
4183
+ })]
4184
+ }),
4185
+ title != null && /* @__PURE__ */ jsx("p", {
4186
+ className: "tw:m-0 tw:text-sm tw:font-medium tw:text-foreground",
4187
+ children: title
4188
+ }),
4189
+ !isPending && statusMessage != null && /* @__PURE__ */ jsx("p", {
4190
+ "data-testid": "tool-call-approval-status-message",
4191
+ className: "tw:m-0 tw:text-sm tw:text-muted-foreground",
4192
+ children: statusMessage
4193
+ }),
4194
+ (resolvedBy != null || resolvedAt) && !isPending && /* @__PURE__ */ jsxs("p", {
4195
+ className: "tw:m-0 tw:flex tw:flex-wrap tw:gap-x-1.5 tw:text-xs tw:text-muted-foreground",
4196
+ children: [resolvedBy != null && /* @__PURE__ */ jsx("span", { children: resolvedBy }), resolvedAt && /* @__PURE__ */ jsx("time", {
4197
+ dateTime: resolvedAt.toISOString(),
4198
+ children: resolvedAt.toLocaleString()
4199
+ })]
4200
+ })
4201
+ ]
4202
+ }),
4203
+ notices.map((notice, index) => /* @__PURE__ */ jsx(ToolCallApprovalNotice, { ...notice }, index)),
4204
+ (children != null || aside != null) && /* @__PURE__ */ jsx("div", {
4205
+ className: "tw:@container",
4206
+ children: /* @__PURE__ */ jsxs("div", {
4207
+ className: "tw:flex tw:flex-col tw:gap-3 tw:@xl:flex-row",
4208
+ children: [children != null && /* @__PURE__ */ jsx("div", {
4209
+ className: "tw:min-w-0 tw:flex-1",
4210
+ children
4211
+ }), aside != null && /* @__PURE__ */ jsx("div", {
4212
+ "data-testid": "tool-call-approval-aside",
4213
+ className: "tw:min-w-0 tw:@xl:w-1/2 tw:@xl:shrink-0",
4214
+ children: aside
4215
+ })]
4216
+ })
4217
+ }),
4218
+ error != null && /* @__PURE__ */ jsx("div", {
4219
+ role: "alert",
4220
+ className: "tw:rounded-md tw:bg-destructive/10 tw:p-2 tw:text-sm tw:text-destructive tw:whitespace-pre-wrap tw:break-words",
4221
+ children: error
4222
+ }),
4223
+ showActions && /* @__PURE__ */ jsx(ToolCallApprovalActions, {
4224
+ onApprove,
4225
+ onReject,
4226
+ extraAction,
4227
+ approveLabel,
4228
+ rejectLabel,
4229
+ approveVariant,
4230
+ pendingAction,
4231
+ isDisabled,
4232
+ shortcutKey
4233
+ })
4234
+ ]
4235
+ })]
2322
4236
  });
2323
4237
  };
2324
4238
  //#endregion
2325
4239
  //#region src/blocks/AIKit/ToolCall/ToolCallChip.tsx
2326
4240
  const chipClasses = cva(["tw:inline-flex tw:items-center tw:gap-1.5 tw:rounded-md tw:border tw:border-solid", "tw:border-border tw:bg-muted tw:px-2 tw:py-1 tw:text-xs tw:text-foreground"], {
2327
4241
  variants: { interactive: {
2328
- true: "tw:cursor-pointer tw:focus-visible:outline-hidden tw:focus-visible:ring-2 tw:focus-visible:ring-ring tw:not-disabled:hover:bg-accent",
4242
+ true: "tw:cursor-pointer tw:focus-visible:outline-hidden tw:focus-visible:ring-2 tw:focus-visible:ring-ring tw:not-disabled:hover:bg-hover",
2329
4243
  false: ""
2330
4244
  } },
2331
4245
  defaultVariants: { interactive: false }
@@ -2527,6 +4441,6 @@ const UsageStats = ({ value, alert = false, children, className, ariaLabel, alig
2527
4441
  });
2528
4442
  };
2529
4443
  //#endregion
2530
- export { AnswerActions, AnswerSummary, ChatInlineIcon, CollapsibleStepHeader, DIAGRAM_LANGUAGES, FileDownloadChip, InlineStepDetail, InlineStepGroup, InlineStepLabel, Markdown, MarkdownTable, MessageBubble, PROMPT_INPUT_CONTAINER, PlanApproval, PlanRewritingIndicator, PromptInput, Question, QuestionBlock, SuggestedPrompt, ThinkingIndicator, ToolCallApproval, ToolCallChip, ToolCallStatusHeader, UsageBar, UsageStats, UsageStatsRing, clampPercentage, findMentionToken, findSlashToken, formatPercentage, getChatInlineIconName };
4444
+ export { ANSWER_SOURCES_VISIBLE, AnswerActions, AnswerSources, AnswerSummary, AttachmentChip, CONTEXT_CHIP_EXPANDABLE_LENGTH, ChatInlineIcon, ChatNotice, CollapsibleStepHeader, ContextChip, DIAGRAM_LANGUAGES, FileDownloadChip, FileGallery, InlineStepDetail, InlineStepGroup, InlineStepLabel, JumpToLatest, Markdown, MarkdownTable, MessageBubble, ModeChip, PROMPT_INPUT_CONTAINER, PlanApproval, PlanRewritingIndicator, PromptInput, Question, QuestionBlock, SourceChip, SuggestedPrompt, ThinkingIndicator, ToolCallApproval, ToolCallChip, ToolCallStatusHeader, UsageBar, UsageStats, UsageStatsRing, clampPercentage, findMentionToken, findSlashToken, formatPercentage, getChatInlineIconName, splitSealedBlocks, withholdOpenTail };
2531
4445
 
2532
- //# sourceMappingURL=AIKit-BVCWP9Ks.js.map
4446
+ //# sourceMappingURL=AIKit-BRSZ3nY7.js.map