@kitn.ai/ui 0.26.0 → 0.28.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 (488) hide show
  1. package/README.md +25 -6
  2. package/bin/mcp.js +16 -3
  3. package/bin/route.js +23 -0
  4. package/bin/route.test.js +58 -0
  5. package/dist/{elements/chunks/Icon--Y8vsFal.js → Icon-DObhsRtm.js} +1 -1
  6. package/dist/action-icons-CcgNN2oT.js +1 -0
  7. package/dist/arrow-down-CjRU3JgV.js +1 -0
  8. package/dist/arrow-left-Bnpb1UEW.js +1 -0
  9. package/dist/artifact-Bwt7OgcR.js +1 -0
  10. package/dist/assets/dev-5XzXYR21.js +91 -0
  11. package/dist/attachments-Qr4kg-2b.js +1 -0
  12. package/dist/{elements/chunks/audio-visualizer-D7lB6mvR.js → audio-visualizer-DToYdfSc.js} +1 -1
  13. package/dist/badge-CdEu_jn2.js +1 -0
  14. package/dist/bash-InADTalH.js +1 -6
  15. package/dist/bell-DQAAOpDz.js +1 -0
  16. package/dist/{elements/chunks/button-flUbeufF.js → button-DkuOnGLy.js} +1 -1
  17. package/dist/card-renderer-Jx70m6_l.js +1 -0
  18. package/dist/card-routing-0YidER_3.js +1 -0
  19. package/dist/check-njkVdZu2.js +1 -0
  20. package/dist/checkbox-D7iRB3nL.js +1 -0
  21. package/dist/checkbox-group-B2_rpoPM.js +1 -0
  22. package/dist/chevron-down-B72s6PXR.js +1 -0
  23. package/dist/chevron-right-BatuHzUm.js +1 -0
  24. package/dist/choice-card-m0BYqqOK.js +1 -0
  25. package/dist/circle-check-B62pODB3.js +1 -0
  26. package/dist/circle-x-BPG0iPst.js +1 -0
  27. package/dist/circle-zx0ByzY9.js +1 -0
  28. package/dist/{elements/chunks/cn-CU7UAWzk.js → cn-DN5AWfiS.js} +1 -1
  29. package/dist/code-block-ClMLKb7C.js +1 -0
  30. package/dist/{elements/chunks/collapsible-BzbXpPzr.js → collapsible-B4DlWicL.js} +1 -1
  31. package/dist/components/attachment-types.d.ts +7 -0
  32. package/dist/components/audio-visualizer/fit-scale.d.ts +1 -1
  33. package/dist/components/card-renderer.d.ts +13 -0
  34. package/dist/components/card.d.ts +4 -0
  35. package/dist/components/chat-thread.d.ts +135 -2
  36. package/dist/components/conversation-item.d.ts +35 -7
  37. package/dist/components/conversation-list.d.ts +17 -5
  38. package/dist/components/conversation-panel.d.ts +29 -0
  39. package/dist/components/form-widgets.d.ts +67 -2
  40. package/dist/components/form.d.ts +48 -1
  41. package/dist/components/home-panel.d.ts +34 -0
  42. package/dist/components/message.d.ts +25 -3
  43. package/dist/components/reasoning.d.ts +6 -0
  44. package/dist/components/scroll-button.d.ts +11 -0
  45. package/dist/components/thread.d.ts +5 -0
  46. package/dist/components/tool-types.d.ts +9 -0
  47. package/dist/components/widget-tab-bar.d.ts +16 -0
  48. package/dist/composer-sQ0HcvyG.js +106 -0
  49. package/dist/confirm-card-rQWDQsnC.js +1 -0
  50. package/dist/construct-cli.es.js +1337 -0
  51. package/dist/{elements/chunks/context-VLIL3sJ-.js → context-0kg4ZX1J.js} +1 -1
  52. package/dist/controllable-CnGpMF6r.js +1 -0
  53. package/dist/conversation-item-BxwQLaFJ.js +1 -0
  54. package/dist/conversation-list-BCfGN1JU.js +1 -0
  55. package/dist/copy-n9u9rqpi.js +1 -0
  56. package/dist/core-AYMC6_lb.js +12 -5874
  57. package/dist/{create-tween-BFT6c-Y8.js → create-tween-C-q-JCM-.js} +1 -1
  58. package/dist/{create-tween-CJNlzoaO.js → create-tween-CRrJXxBd.js} +1 -1
  59. package/dist/{create-tween-CQED4T0g.js → create-tween-DZ3RRDHD.js} +1 -1
  60. package/dist/{elements/chunks/create-tween-c4Z8TDYz.js → create-tween-uPkArjIG.js} +2 -2
  61. package/dist/css-M7EaDHN_.js +1 -6
  62. package/dist/custom-elements.json +1433 -202
  63. package/dist/default-input-CaMrJkgH.js +1 -0
  64. package/dist/define-By-uL588.js +1 -0
  65. package/dist/define.d.ts +4 -0
  66. package/dist/define.js +510 -0
  67. package/dist/define.server.d.ts +5 -0
  68. package/dist/define.server.js +506 -0
  69. package/dist/{elements/chunks/disclosure-CcNbyLy0.js → disclosure-WUImBbDv.js} +1 -1
  70. package/dist/{elements/chunks/dropdown-BgWTthUx.js → dropdown-wZVeJOMd.js} +1 -1
  71. package/dist/elements/agent-card.js +1 -1
  72. package/dist/elements/artifact.js +1 -1
  73. package/dist/elements/attachments.js +1 -1
  74. package/dist/elements/audio-visualizer.js +1 -1
  75. package/dist/elements/autoloader.js +1 -1
  76. package/dist/elements/avatar.js +1 -1
  77. package/dist/elements/badge.js +1 -1
  78. package/dist/elements/button.js +1 -1
  79. package/dist/elements/card.js +1 -1
  80. package/dist/elements/cards.js +1 -1
  81. package/dist/elements/chain-of-thought.js +1 -1
  82. package/dist/elements/chat-scope-picker.js +1 -1
  83. package/dist/elements/chat-workspace.js +1 -1
  84. package/dist/elements/chat.js +1 -1
  85. package/dist/elements/checkbox-group.d.ts +1 -0
  86. package/dist/elements/checkbox-group.js +1 -0
  87. package/dist/elements/checkbox.d.ts +1 -0
  88. package/dist/elements/checkbox.js +1 -0
  89. package/dist/elements/checkpoint.js +1 -1
  90. package/dist/elements/choice.js +1 -1
  91. package/dist/elements/coachmark.js +1 -1
  92. package/dist/elements/code-block.js +1 -1
  93. package/dist/elements/command.js +1 -1
  94. package/dist/elements/compare.js +1 -1
  95. package/dist/elements/composer.js +1 -1
  96. package/dist/elements/confirm-card.js +1 -1
  97. package/dist/elements/context-meter.js +1 -1
  98. package/dist/elements/conversation-item.js +1 -1
  99. package/dist/elements/conversation-list.js +1 -1
  100. package/dist/elements/define-entry.d.ts +12 -0
  101. package/dist/elements/dialog.js +1 -1
  102. package/dist/elements/dock.js +102 -8
  103. package/dist/elements/dropdown.js +1 -1
  104. package/dist/elements/editable-label.js +1 -1
  105. package/dist/elements/element-data-types.d.ts +37 -0
  106. package/dist/elements/embed.js +1 -1
  107. package/dist/elements/empty.js +1 -1
  108. package/dist/elements/feedback-bar.js +1 -1
  109. package/dist/elements/file-tree.js +1 -1
  110. package/dist/elements/file-upload.js +1 -1
  111. package/dist/elements/form.js +1 -1
  112. package/dist/elements/hover-card.js +1 -1
  113. package/dist/elements/icon.js +1 -1
  114. package/dist/elements/image.js +1 -1
  115. package/dist/elements/input.js +1 -1
  116. package/dist/elements/kbd.js +1 -1
  117. package/dist/elements/link-preview.js +1 -1
  118. package/dist/elements/loader.js +1 -1
  119. package/dist/elements/markdown.js +1 -1
  120. package/dist/elements/menu.js +1 -1
  121. package/dist/elements/message-skills.js +1 -1
  122. package/dist/elements/message.js +1 -1
  123. package/dist/elements/model-switcher.js +1 -1
  124. package/dist/elements/nav.js +1 -1
  125. package/dist/elements/notice.js +1 -1
  126. package/dist/elements/pane-grid.d.ts +1 -0
  127. package/dist/elements/pane-grid.js +1 -0
  128. package/dist/elements/pane-group.js +1 -1
  129. package/dist/elements/pane.js +1 -1
  130. package/dist/elements/popover.js +1 -1
  131. package/dist/elements/progress-bar.js +1 -1
  132. package/dist/elements/prompt-dock.js +1 -1
  133. package/dist/elements/prompt-input.js +1 -1
  134. package/dist/elements/prompt-suggestions.js +1 -1
  135. package/dist/elements/radio-group.d.ts +1 -0
  136. package/dist/elements/radio-group.js +1 -0
  137. package/dist/elements/reasoning.js +1 -1
  138. package/dist/elements/remote.js +1 -1
  139. package/dist/elements/resizable.js +1 -1
  140. package/dist/elements/response-stream.js +2 -2
  141. package/dist/elements/screen.js +1 -1
  142. package/dist/elements/scroll-area.js +1 -1
  143. package/dist/elements/scroll-button.js +1 -1
  144. package/dist/elements/search.js +1 -1
  145. package/dist/elements/segmented.js +1 -1
  146. package/dist/elements/select.d.ts +1 -0
  147. package/dist/elements/select.js +1 -0
  148. package/dist/elements/separator.js +1 -1
  149. package/dist/elements/setting-item.js +1 -1
  150. package/dist/elements/settings-group.js +1 -1
  151. package/dist/elements/skeleton.js +1 -1
  152. package/dist/elements/slider.d.ts +1 -0
  153. package/dist/elements/slider.js +1 -0
  154. package/dist/elements/slots.d.ts +1 -1
  155. package/dist/elements/source.js +1 -1
  156. package/dist/elements/status.js +1 -1
  157. package/dist/elements/switch.js +1 -1
  158. package/dist/elements/tabs.js +1 -1
  159. package/dist/elements/tasks.js +1 -1
  160. package/dist/elements/text-shimmer.js +1 -1
  161. package/dist/elements/thinking-bar.js +1 -1
  162. package/dist/elements/thread.js +1 -1
  163. package/dist/elements/toast.js +1 -1
  164. package/dist/elements/tool.js +1 -1
  165. package/dist/elements/tooltip.js +1 -1
  166. package/dist/elements/voice-input.js +1 -1
  167. package/dist/elements/voice-output.js +1 -1
  168. package/dist/elements.d.ts +380 -26
  169. package/dist/{elements/chunks/ellipsis-7gU33mL2.js → ellipsis-CbOpJnU3.js} +1 -1
  170. package/dist/embed-CrfjkbUG.js +1 -0
  171. package/dist/engine-javascript-vq0WuIJl.js +14 -2516
  172. package/dist/{elements/chunks/external-link-CSOEsNlx.js → external-link-ByXAwBBQ.js} +1 -1
  173. package/dist/file-text-DMib2qwt.js +1 -0
  174. package/dist/{elements/chunks/file-tree-zvB2i7P9.js → file-tree-CJqM2aP7.js} +1 -1
  175. package/dist/folder-_BAb929C.js +1 -0
  176. package/dist/form-DSZgZQZk.js +1 -0
  177. package/dist/github-dark-dimmed-DUshB20C.js +1 -4
  178. package/dist/github-light-JYsPkUQd.js +1 -4
  179. package/dist/hover-card-BQbul9eq.js +1 -0
  180. package/dist/html-CPZ3oZQ7.js +1 -10
  181. package/dist/icon-CdcfRhYI.js +1 -0
  182. package/dist/{elements/chunks/index-BCRhi84a.js → index-xu_pdcPi.js} +1 -1
  183. package/dist/index.d.ts +4 -2
  184. package/dist/index.js +7474 -5857
  185. package/dist/index.server.js +5965 -4762
  186. package/dist/info-DsFjGuI_.js +1 -0
  187. package/dist/input-DKbmkMos.js +1 -0
  188. package/dist/javascript-C25yR2R2.js +1 -6
  189. package/dist/json-DxJze_jm.js +1 -6
  190. package/dist/kai.es.js +1 -1
  191. package/dist/kbd-PvdGhX4s.js +1 -0
  192. package/dist/{elements/chunks/link-preview-OQ9IEq0f.js → link-preview-DbM1866N.js} +1 -1
  193. package/dist/link-zF8PzWfF.js +1 -0
  194. package/dist/loader-BM4CEz4G.js +1 -0
  195. package/dist/{elements/chunks/markdown-DgBFaGAz.js → markdown-LMvmAyBk.js} +7 -7
  196. package/dist/mcp.es.js +1361 -81
  197. package/dist/message-BSsgvHvf.js +1 -0
  198. package/dist/message-BYZECyek.js +1 -0
  199. package/dist/message-circle-DruwObfX.js +1 -0
  200. package/dist/message-square-BG6q6mlm.js +1 -0
  201. package/dist/{elements/chunks/minimize-2-DPIUKTrT.js → minimize-2-BQRoBV1V.js} +1 -1
  202. package/dist/{elements/chunks/model-switcher-DsT8tFB0.js → model-switcher-CMpCOYcB.js} +1 -1
  203. package/dist/{elements/chunks/overlay-DuiwdXjm.js → overlay-Cb-MkyDa.js} +1 -1
  204. package/dist/panel-right-i8warFxT.js +1 -0
  205. package/dist/paperclip-ByY3UVO6.js +1 -0
  206. package/dist/play-BvKKq3fX.js +1 -0
  207. package/dist/primitives/card-data-types.d.ts +49 -0
  208. package/dist/primitives/card-routing.d.ts +1 -33
  209. package/dist/primitives/card-validate.d.ts +2 -2
  210. package/dist/primitives/chat-config.d.ts +29 -2
  211. package/dist/primitives/conversation-store.d.ts +54 -0
  212. package/dist/primitives/field-mask.d.ts +96 -0
  213. package/dist/primitives/field-semantics.d.ts +36 -0
  214. package/dist/primitives/input-mask.d.ts +46 -0
  215. package/dist/primitives/url-scheme-policy.d.ts +34 -0
  216. package/dist/primitives/use-resize-observer.d.ts +2 -2
  217. package/dist/progress-bar-jVAHvMUf.js +1 -0
  218. package/dist/prompt-suggestion-CD-qtNgC.js +1 -0
  219. package/dist/radio-BsoBMhkF.js +1 -0
  220. package/dist/react/index.d.ts +586 -19
  221. package/dist/react.js +223 -187
  222. package/dist/reasoning-4kBPtlTx.js +1 -0
  223. package/dist/register-impl-B1hLkrI8.js +424 -0
  224. package/dist/{elements/chunks/resizable-D181b4fM.js → resizable-uPmY9VY-.js} +1 -1
  225. package/dist/rotate-ccw-B2xn93py.js +1 -0
  226. package/dist/rotate-cw-CMVLoCyH.js +1 -0
  227. package/dist/schemas/form.schema.json +21 -1
  228. package/dist/schemas/tool-defs.d.ts +11 -1
  229. package/dist/schemas.js +81 -81
  230. package/dist/scroll-area-BqLUo8ZZ.js +1 -0
  231. package/dist/scroll-button-DslXiJyL.js +1 -0
  232. package/dist/select-2cLRtPGT.js +1 -0
  233. package/dist/{elements/chunks/separator-CeJHQqLW.js → separator-BWQtNJcM.js} +1 -1
  234. package/dist/{elements/chunks/settings-CqaHgJF9.js → settings-BBR63jLg.js} +1 -1
  235. package/dist/{elements/chunks/settings-group-CjqjLCOn.js → settings-group-B2ciy-Iw.js} +1 -1
  236. package/dist/{elements/chunks/skeleton-D8akBnKS.js → skeleton-CkHI-HgC.js} +1 -1
  237. package/dist/slider-DDR9U-i0.js +1 -0
  238. package/dist/{solid-BtdqNZI7.js → solid-BzCFdL25.js} +9771 -7594
  239. package/dist/{solid-CnavwBye.js → solid-DruVOXnf.js} +7893 -6183
  240. package/dist/solid.d.ts +16 -2
  241. package/dist/solid.js +256 -243
  242. package/dist/solid.server.js +256 -243
  243. package/dist/source-GEnFF528.js +1 -0
  244. package/dist/star-CilHq8GP.js +1 -0
  245. package/dist/state/index.d.ts +1 -1
  246. package/dist/state/mock.d.ts +32 -2
  247. package/dist/state.js +133 -108
  248. package/dist/store-Ciyk6eZG.js +1 -0
  249. package/dist/svelte--5p79yCD.js +1 -15
  250. package/dist/switch-KkeQIbuH.js +1 -0
  251. package/dist/tasks-card-BrN4bggJ.js +1 -0
  252. package/dist/text-shimmer-Dcj1MHuq.js +1 -0
  253. package/dist/textarea-Dpvbngbg.js +1 -0
  254. package/dist/theme.tokens.css +110 -13
  255. package/dist/{elements/chunks/thumbs-up-DX2Ijk3H.js → thumbs-up-CsT9Wzb9.js} +1 -1
  256. package/dist/toast-store-tgK6MuMv.js +1 -0
  257. package/dist/{elements/chunks/tool-DKQ2D1DP.js → tool-DzUjIBT8.js} +1 -1
  258. package/dist/{elements/chunks/tooltip-B0G99y8w.js → tooltip-Bkkg_4t0.js} +1 -1
  259. package/dist/trash-2-jBKxjGGQ.js +1 -0
  260. package/dist/{elements/chunks/triangle-alert-F9cyUEyy.js → triangle-alert-DlQ68ppf.js} +1 -1
  261. package/dist/tsx-B8rCNbgL.js +1 -6
  262. package/dist/types.d.ts +48 -3
  263. package/dist/typescript-RycA9KXf.js +1 -6
  264. package/dist/ui/checkbox-group.d.ts +83 -0
  265. package/dist/ui/checkbox.d.ts +40 -0
  266. package/dist/ui/dock.d.ts +51 -0
  267. package/dist/ui/icon.d.ts +3 -0
  268. package/dist/ui/input.d.ts +48 -2
  269. package/dist/ui/radio.d.ts +85 -0
  270. package/dist/ui/select.d.ts +71 -0
  271. package/dist/ui/slider.d.ts +65 -0
  272. package/dist/ui/switch.d.ts +9 -2
  273. package/dist/upload-TcAHXv9W.js +1 -0
  274. package/dist/url-scheme-policy-DR5DgvkM.js +1 -0
  275. package/dist/use-card-resolution-DaZqAdWf.js +1 -0
  276. package/dist/{elements/chunks/variant-aurora-qt6Zct8M.js → variant-aurora-BhLuYxJR.js} +1 -1
  277. package/dist/{variant-aurora-B8II3-I1.js → variant-aurora-CNmfHhcx.js} +2 -2
  278. package/dist/{variant-aurora-CMNWOSJR.js → variant-aurora-D6GsdTPA.js} +2 -2
  279. package/dist/{variant-aurora-CcuDMWwQ.js → variant-aurora-DbW_OWL2.js} +1 -1
  280. package/dist/{elements/chunks/variant-custom-tJp22_BQ.js → variant-custom-BRvaNHSi.js} +1 -1
  281. package/dist/{variant-custom-L16JLLZy.js → variant-custom-BvYHR62b.js} +1 -1
  282. package/dist/{variant-custom-BdvYC7af.js → variant-custom-DMTrEO5w.js} +2 -2
  283. package/dist/{variant-custom-C714mqKl.js → variant-custom-DV2x254s.js} +2 -2
  284. package/dist/{variant-wave-ecwykeyL.js → variant-wave-BNMGrDfj.js} +2 -2
  285. package/dist/{variant-wave-2S4La2Ka.js → variant-wave-BpDd8t3Z.js} +1 -1
  286. package/dist/{variant-wave-D7ruiA1W.js → variant-wave-CyBueXj9.js} +2 -2
  287. package/dist/{elements/chunks/variant-wave-CaQ5ti39.js → variant-wave-sp-qwsdI.js} +1 -1
  288. package/dist/vue-BmIZj4XD.js +1 -33
  289. package/dist/x-Z5UntkIJ.js +1 -0
  290. package/frameworks/react/index.tsx +210 -22
  291. package/llms-full.txt +1184 -22
  292. package/llms.txt +6 -3
  293. package/package.json +17 -4
  294. package/src/agent-tooling/README.md +192 -0
  295. package/src/agent-tooling/catalog/invariants.ts +3 -3
  296. package/src/agent-tooling/catalog/surfaces.ts +72 -0
  297. package/src/agent-tooling/construct/cli-entry.ts +8 -0
  298. package/src/agent-tooling/construct/cli.ts +147 -0
  299. package/src/agent-tooling/construct/codegen.ts +1635 -0
  300. package/src/agent-tooling/construct/construct.v1.schema.json +319 -0
  301. package/src/agent-tooling/construct/dev.ts +148 -0
  302. package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +7 -0
  303. package/src/agent-tooling/construct/fixtures/ops-console.construct.json +44 -0
  304. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +32 -0
  305. package/src/agent-tooling/construct/schema.ts +452 -0
  306. package/src/agent-tooling/mcp/server.ts +2 -1
  307. package/src/agent-tooling/mcp/tools/construct.ts +130 -0
  308. package/src/agent-tooling/mcp/tools/debug.ts +126 -4
  309. package/src/agent-tooling/mcp/tools/reference.ts +104 -3
  310. package/src/agent-tooling/recipes/composed-thread.ts +714 -0
  311. package/src/agent-tooling/recipes/index.ts +22 -0
  312. package/src/agent-tooling/recipes/types.ts +32 -0
  313. package/src/agent-tooling/registry.ts +5 -0
  314. package/src/components/attachment-types.ts +7 -0
  315. package/src/components/audio-visualizer/fit-scale.ts +1 -1
  316. package/src/components/card-renderer.tsx +39 -8
  317. package/src/components/card.tsx +10 -2
  318. package/src/components/chat-scope-picker.tsx +2 -2
  319. package/src/components/chat-thread.tsx +602 -43
  320. package/src/components/choice-card.tsx +102 -37
  321. package/src/components/coachmark.tsx +1 -1
  322. package/src/components/code-block.tsx +2 -2
  323. package/src/components/composer.tsx +89 -15
  324. package/src/components/confirm-card.tsx +20 -4
  325. package/src/components/conversation-item.tsx +56 -9
  326. package/src/components/conversation-list.tsx +25 -10
  327. package/src/components/conversation-panel.tsx +118 -0
  328. package/src/components/empty.tsx +14 -3
  329. package/src/components/file-tree.tsx +5 -1
  330. package/src/components/form-widgets.tsx +257 -195
  331. package/src/components/form.tsx +297 -9
  332. package/src/components/home-panel.tsx +159 -0
  333. package/src/components/loader.tsx +12 -5
  334. package/src/components/message-skills.tsx +1 -1
  335. package/src/components/message.tsx +71 -11
  336. package/src/components/prompt-input.tsx +7 -1
  337. package/src/components/prompt-suggestion.tsx +4 -1
  338. package/src/components/reasoning.tsx +30 -3
  339. package/src/components/response-compare.tsx +2 -2
  340. package/src/components/scroll-button.tsx +56 -4
  341. package/src/components/source.tsx +6 -1
  342. package/src/components/tasks-card.tsx +58 -16
  343. package/src/components/thread.tsx +10 -1
  344. package/src/components/toast.tsx +1 -1
  345. package/src/components/tool-types.ts +9 -0
  346. package/src/components/widget-tab-bar.tsx +73 -0
  347. package/src/elements/attachments.tsx +3 -1
  348. package/src/elements/chat-workspace.tsx +1 -1
  349. package/src/elements/chat.tsx +115 -5
  350. package/src/elements/checkbox-group.tsx +164 -0
  351. package/src/elements/checkbox.tsx +134 -0
  352. package/src/elements/compiled.css +1 -1
  353. package/src/elements/composer.tsx +12 -1
  354. package/src/elements/conversation-item.tsx +62 -22
  355. package/src/elements/conversation-list.tsx +2 -2
  356. package/src/elements/define-entry.ts +12 -0
  357. package/src/elements/dock.tsx +11 -0
  358. package/src/elements/element-data-types.ts +40 -0
  359. package/src/elements/element-manifest.json +24 -0
  360. package/src/elements/element-meta.json +767 -34
  361. package/src/elements/element-nonscalar.json +17 -0
  362. package/src/elements/element-types.d.ts +380 -26
  363. package/src/elements/feedback-bar.tsx +5 -0
  364. package/src/elements/icon-names.json +29 -0
  365. package/src/elements/input.tsx +312 -23
  366. package/src/elements/message.tsx +2 -0
  367. package/src/elements/pane-grid.tsx +111 -0
  368. package/src/elements/prompt-input.tsx +8 -2
  369. package/src/elements/radio-group.tsx +129 -0
  370. package/src/elements/register-impl.ts +6 -0
  371. package/src/elements/scroll-button.tsx +54 -6
  372. package/src/elements/select.tsx +165 -0
  373. package/src/elements/slider.tsx +171 -0
  374. package/src/elements/slots.ts +1 -1
  375. package/src/elements/styles.css +89 -3
  376. package/src/elements/thread.tsx +2 -0
  377. package/src/elements/toast.tsx +14 -1
  378. package/src/index.ts +5 -2
  379. package/src/primitives/card-data-types.ts +49 -0
  380. package/src/primitives/card-host.tsx +4 -2
  381. package/src/primitives/card-routing.ts +7 -56
  382. package/src/primitives/card-schemas/form.schema.json +21 -1
  383. package/src/primitives/card-validate.ts +10 -2
  384. package/src/primitives/chat-config.tsx +33 -6
  385. package/src/primitives/conversation-store.ts +248 -0
  386. package/src/primitives/field-mask.ts +256 -0
  387. package/src/primitives/field-semantics.ts +115 -0
  388. package/src/primitives/input-mask.ts +853 -0
  389. package/src/primitives/toast-store.ts +66 -4
  390. package/src/primitives/url-scheme-policy.ts +70 -0
  391. package/src/primitives/use-resize-observer.ts +2 -2
  392. package/src/schemas/tool-defs.ts +12 -2
  393. package/src/solid.ts +20 -2
  394. package/src/state/index.ts +1 -1
  395. package/src/state/mock.ts +92 -6
  396. package/src/types.ts +45 -3
  397. package/src/ui/agent-card.tsx +2 -2
  398. package/src/ui/avatar.tsx +1 -1
  399. package/src/ui/checkbox-group.tsx +153 -0
  400. package/src/ui/checkbox.tsx +63 -0
  401. package/src/ui/dock.tsx +192 -7
  402. package/src/ui/icon.tsx +53 -0
  403. package/src/ui/input.tsx +249 -9
  404. package/src/ui/kbd.tsx +1 -1
  405. package/src/ui/nav.tsx +1 -1
  406. package/src/ui/pane-group.tsx +2 -2
  407. package/src/ui/radio.tsx +150 -0
  408. package/src/ui/select.tsx +168 -0
  409. package/src/ui/slider.tsx +178 -0
  410. package/src/ui/switch.tsx +49 -18
  411. package/src/utils/cn.ts +11 -5
  412. package/theme.css +112 -13
  413. package/dist/elements/chunks/action-icons-D9u3OzYW.js +0 -1
  414. package/dist/elements/chunks/arrow-left-CVh1TeN7.js +0 -1
  415. package/dist/elements/chunks/artifact-BkYcfIlZ.js +0 -1
  416. package/dist/elements/chunks/attachments-DwfNlubq.js +0 -1
  417. package/dist/elements/chunks/badge-Q0-Cmwzz.js +0 -1
  418. package/dist/elements/chunks/bash-InADTalH.js +0 -1
  419. package/dist/elements/chunks/card-renderer-3Dyl-FpN.js +0 -1
  420. package/dist/elements/chunks/card-routing-BK09BdmL.js +0 -1
  421. package/dist/elements/chunks/check-B5kBPKeo.js +0 -1
  422. package/dist/elements/chunks/chevron-down-DZ1GS39h.js +0 -1
  423. package/dist/elements/chunks/chevron-right-CvP7OUd2.js +0 -1
  424. package/dist/elements/chunks/choice-card-D4QUIOWZ.js +0 -1
  425. package/dist/elements/chunks/circle-CDf9g6MQ.js +0 -1
  426. package/dist/elements/chunks/circle-check-CA47Nqbz.js +0 -1
  427. package/dist/elements/chunks/circle-x-CJ_HVbC8.js +0 -1
  428. package/dist/elements/chunks/code-block-BdLiTitd.js +0 -1
  429. package/dist/elements/chunks/composer-Byve-n8K.js +0 -69
  430. package/dist/elements/chunks/confirm-card-B0flE3MN.js +0 -1
  431. package/dist/elements/chunks/controllable-DTwTIlwp.js +0 -1
  432. package/dist/elements/chunks/conversation-item-BDztezOG.js +0 -1
  433. package/dist/elements/chunks/copy-Nf-kqxXH.js +0 -1
  434. package/dist/elements/chunks/core-AYMC6_lb.js +0 -12
  435. package/dist/elements/chunks/css-M7EaDHN_.js +0 -1
  436. package/dist/elements/chunks/default-input-C2iLFq0b.js +0 -1
  437. package/dist/elements/chunks/define-wjHR1MkU.js +0 -1
  438. package/dist/elements/chunks/download-C4V7tBdb.js +0 -1
  439. package/dist/elements/chunks/embed-D3KewsIJ.js +0 -1
  440. package/dist/elements/chunks/engine-javascript-vq0WuIJl.js +0 -141
  441. package/dist/elements/chunks/file-text-D1G8khle.js +0 -1
  442. package/dist/elements/chunks/folder-WW2LGLid.js +0 -1
  443. package/dist/elements/chunks/form-CQirdC4W.js +0 -1
  444. package/dist/elements/chunks/github-dark-dimmed-DUshB20C.js +0 -1
  445. package/dist/elements/chunks/github-light-JYsPkUQd.js +0 -1
  446. package/dist/elements/chunks/hover-card-CJJkfHTK.js +0 -1
  447. package/dist/elements/chunks/html-CPZ3oZQ7.js +0 -1
  448. package/dist/elements/chunks/icon-Cfgmf4fa.js +0 -1
  449. package/dist/elements/chunks/info-DX3YMOt6.js +0 -1
  450. package/dist/elements/chunks/input-BtW9U6Sq.js +0 -1
  451. package/dist/elements/chunks/javascript-C25yR2R2.js +0 -1
  452. package/dist/elements/chunks/json-DxJze_jm.js +0 -1
  453. package/dist/elements/chunks/kbd-LGXwuT_V.js +0 -1
  454. package/dist/elements/chunks/link-2My69BqT.js +0 -1
  455. package/dist/elements/chunks/loader-iOxc_Tu-.js +0 -1
  456. package/dist/elements/chunks/message-VMZohrXx.js +0 -1
  457. package/dist/elements/chunks/message-circle-CpXMM-_n.js +0 -1
  458. package/dist/elements/chunks/message-square-DxcWtETQ.js +0 -1
  459. package/dist/elements/chunks/message-tZmV2A3p.js +0 -1
  460. package/dist/elements/chunks/paperclip-CRZVp4z_.js +0 -1
  461. package/dist/elements/chunks/progress-bar-DNy66e_E.js +0 -1
  462. package/dist/elements/chunks/prompt-suggestion-Bn64Nl7L.js +0 -1
  463. package/dist/elements/chunks/reasoning-Emn9N4-T.js +0 -1
  464. package/dist/elements/chunks/rotate-cw-CPWe37D9.js +0 -1
  465. package/dist/elements/chunks/scroll-area-ClZkZxIk.js +0 -1
  466. package/dist/elements/chunks/scroll-button-DPE-t3x_.js +0 -1
  467. package/dist/elements/chunks/share-CyfnNEqp.js +0 -1
  468. package/dist/elements/chunks/source-UWCvmVak.js +0 -1
  469. package/dist/elements/chunks/star-DoHFAikQ.js +0 -1
  470. package/dist/elements/chunks/store-Cjq0qwYN.js +0 -1
  471. package/dist/elements/chunks/svelte--5p79yCD.js +0 -1
  472. package/dist/elements/chunks/tasks-card-CmZ5xh6v.js +0 -1
  473. package/dist/elements/chunks/text-shimmer-DYn09zrz.js +0 -1
  474. package/dist/elements/chunks/textarea-skiGHURM.js +0 -1
  475. package/dist/elements/chunks/toast-store-CFnQpThX.js +0 -1
  476. package/dist/elements/chunks/tsx-B8rCNbgL.js +0 -1
  477. package/dist/elements/chunks/typescript-RycA9KXf.js +0 -1
  478. package/dist/elements/chunks/use-card-resolution-DoA2f_EU.js +0 -1
  479. package/dist/elements/chunks/vue-BmIZj4XD.js +0 -1
  480. package/dist/elements/chunks/x-B6UnOiLw.js +0 -1
  481. package/dist/llms/llms-full.txt +0 -2949
  482. package/dist/llms/llms.txt +0 -156
  483. package/dist/register-impl-DpE7icIb.js +0 -293
  484. package/dist/ui/stat.d.ts +0 -16
  485. package/src/ui/stat.tsx +0 -41
  486. /package/dist/{elements/chunks/card-tags-D8lZ-C_U.js → card-tags-D8lZ-C_U.js} +0 -0
  487. /package/dist/{elements/chunks/link-preview-DNILK391.js → link-preview-DNILK391.js} +0 -0
  488. /package/dist/{elements/chunks/slots-DaNjX0rc.js → slots-DaNjX0rc.js} +0 -0
package/llms-full.txt CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # @kitn.ai/ui
5
5
 
6
- > Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. 83 custom elements, every one prefixed `kai-` (e.g. `<kai-chat>`, `<kai-artifact>`): streaming responses, markdown + code rendering, reasoning/tool panels, attachments, conversation sidebar, voice input. Zero framework dependency for consumers; the SolidJS runtime it is authored in is bundled in, so the host needs nothing.
6
+ > Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. 89 custom elements, every one prefixed `kai-` (e.g. `<kai-chat>`, `<kai-artifact>`): streaming responses, markdown + code rendering, reasoning/tool panels, attachments, conversation sidebar, voice input. Zero framework dependency for consumers; the SolidJS runtime it is authored in is bundled in, so the host needs nothing.
7
7
 
8
8
  ## Install
9
9
 
@@ -37,7 +37,7 @@ Drop an element into any framework (React, Vue, plain HTML). Data in via JS prop
37
37
  - `<kai-prompt-input>` — standalone composer with send button.
38
38
 
39
39
  **Layer 2 — composable primitives** (`import { … } from '@kitn.ai/ui'`):
40
- All 83 elements are also exported individually. Use them for custom layouts or features `<kai-chat>` does not expose (ChainOfThought, FeedbackBar, ThinkingBar, VoiceInput, …). Your bundler tree-shakes the rest.
40
+ All 89 elements are also exported individually. Use them for custom layouts or features `<kai-chat>` does not expose (ChainOfThought, FeedbackBar, ThinkingBar, VoiceInput, …). Your bundler tree-shakes the rest.
41
41
 
42
42
  ## Key rules for the web components
43
43
 
@@ -151,7 +151,10 @@ For Tailwind builds: `@import "@kitn.ai/ui/theme.css"` in your CSS.
151
151
 
152
152
  ## Docs
153
153
 
154
- - Full element reference (all 83 elements, every prop/event): ./llms-full.txt — https://kitn.dev/llms-full.txt
154
+ - Element reference (all 89 elements, every prop/event/method/slot/part): the "Element reference" section of ./llms-full.txt — https://kitn.dev/llms-full.txt
155
+ - Programmatic layer (`@kitn.ai/ui/state` + `@kitn.ai/ui/wire`: streaming folds, the mock responder, the SSE readers/encoders): the "Programmatic layer" section of llms-full.txt
156
+ - How to build a chat app in 5 steps (install → pick a layer → handle submit + stream → wire features → theme, with working code): the "How to build a chat app in 5 steps" section of llms-full.txt
157
+ - Streaming recipe (the two rules that bite: reassign new references per chunk, fold deltas onto the trailing text part): the "Streaming recipe" section of llms-full.txt
155
158
  - Machine-readable Custom Elements Manifest: https://unpkg.com/@kitn.ai/ui/dist/custom-elements.json
156
159
  - Working examples: https://github.com/kitn-ai/ui/tree/main/examples
157
160
  - Storybook: https://storybook.kitn.dev
@@ -248,7 +251,979 @@ The same reassign rule applies to every array/object property (`models`, `contex
248
251
 
249
252
  ---
250
253
 
251
- ## Element reference (83 elements, generated from custom-elements.json)
254
+ <!-- kai:programmatic:start -->
255
+ ## Programmatic layer — `@kitn.ai/ui/state` + `@kitn.ai/ui/wire`
256
+
257
+ <!-- generated by scripts/gen-llms-programmatic.mjs from the shipped dist/*.d.ts — do not edit by hand -->
258
+
259
+ The API you write a HOST against — everything below is what a hand-composed surface
260
+ (no `<kai-chat>`) wires together. Signatures and docs below are the shipped declaration
261
+ files themselves, so they cannot drift from what your editor shows.
262
+
263
+ The streaming loop, end to end:
264
+
265
+ ```ts
266
+ import { createAssistantStream, createMockResponder } from '@kitn.ai/ui/state';
267
+ import { readOpenAIStream } from '@kitn.ai/ui/wire';
268
+
269
+ // 1. A setter with the ONE universal contract: functional updater, new array out.
270
+ const stream = createAssistantStream((update) => { el.messages = update(el.messages ?? []); });
271
+
272
+ // 2. You fetch (or preview with the mock — real SSE frames, no provider, no key):
273
+ const mock = createMockResponder(); // or: fetch("/api/chat", …).then(r => r.body)
274
+ const result = await readOpenAIStream(mock(prompt), stream);
275
+
276
+ // 3. THE HOST RESOLVES TOOL CALLS. A provider (and the mock) only ANNOUNCES a tool
277
+ // call — the part sits at state "input-available" forever unless your code answers
278
+ // it. Executing the tool is the app's decision, and this is the call that answers.
279
+ // The ONE exception: a call with `providerExecuted: true` (see ModelToolCall below)
280
+ // was already run by the provider, in-stream — the host must NOT execute those.
281
+ for (const call of result.toolCalls) {
282
+ if (call.providerExecuted) continue;
283
+ stream.upsertTool(call.id, { state: 'output-available', output: await runTool(call) });
284
+ }
285
+ stream.done(); // seal the turn; late sink calls are dropped
286
+ ```
287
+
288
+ Scripting a mock tool call (so the tool panel renders with zero backend):
289
+
290
+ ```ts
291
+ const mock = createMockResponder({
292
+ replies: ['Plain text turn', { text: 'Let me check.', toolCalls: [{ name: 'search_docs', arguments: { query: 'threads' } }] }],
293
+ });
294
+ ```
295
+
296
+ ### `@kitn.ai/ui/state`
297
+
298
+ I/O-free pure folds over `ChatMessage[]`. No client, no fetch — you own the transport; these functions own the array-identity discipline the elements re-render on.
299
+
300
+ Every export (56, derived from `dist/state/index.d.ts`):
301
+
302
+ | Export | Kind | Module |
303
+ |---|---|---|
304
+ | `appendMessage` | value | `messages` |
305
+ | `upsertMessage` | value | `messages` |
306
+ | `updateMessage` | value | `messages` |
307
+ | `removeMessage` | value | `messages` |
308
+ | `appendText` | value | `messages` |
309
+ | `textMessage` | value | `messages` |
310
+ | `partsToText` | value | `messages` |
311
+ | `addSuggestion` | value | `suggestions` |
312
+ | `removeSuggestion` | value | `suggestions` |
313
+ | `createAssistantStream` | value | `stream` |
314
+ | `onStreamSettled` | value | `stream` |
315
+ | `SetMessages` | type | `stream` |
316
+ | `AssistantStream` | type | `stream` |
317
+ | `appendTextPart` | value | `parts` |
318
+ | `appendReasoningPart` | value | `parts` |
319
+ | `upsertToolPart` | value | `parts` |
320
+ | `upsertCardPart` | value | `parts` |
321
+ | `fingerprint` | value | `parts` |
322
+ | `ReasoningOpts` | type | `parts` |
323
+ | `updateThreadMessages` | value | `threads` |
324
+ | `bindThreadMessages` | value | `threads` |
325
+ | `createThreadSessions` | value | `threads` |
326
+ | `ThreadLike` | type | `threads` |
327
+ | `SetThreads` | type | `threads` |
328
+ | `BindThreadOptions` | type | `threads` |
329
+ | `ThreadSessions` | type | `threads` |
330
+ | `parseStoredThread` | value | `persistence` |
331
+ | `createSaveScheduler` | value | `persistence` |
332
+ | `ParsedThread` | type | `persistence` |
333
+ | `DroppedStored` | type | `persistence` |
334
+ | `SaveScheduler` | type | `persistence` |
335
+ | `SaveSchedulerOptions` | type | `persistence` |
336
+ | `createMockResponder` | value | `mock` |
337
+ | `DEFAULT_MOCK_REPLIES` | value | `mock` |
338
+ | `MOCK_BANNER` | value | `mock` |
339
+ | `MOCK_MARKER` | value | `mock` |
340
+ | `MOCK_MARKER_KEY` | value | `mock` |
341
+ | `MOCK_MODEL_ID` | value | `mock` |
342
+ | `MockReply` | type | `mock` |
343
+ | `MockResponder` | type | `mock` |
344
+ | `MockResponderOptions` | type | `mock` |
345
+ | `MockToolCall` | type | `mock` |
346
+ | `MockTurn` | type | `mock` |
347
+ | `ChatMessage` | type | `../elements/chat-types` |
348
+ | `ChatMessageAction` | type | `../elements/chat-types` |
349
+ | `CustomAction` | type | `../elements/chat-types` |
350
+ | `AvatarData` | type | `../elements/chat-types` |
351
+ | `FeedbackVote` | type | `../elements/chat-types` |
352
+ | `MessagePart` | type | `../elements/chat-types` |
353
+ | `MessageSource` | type | `../elements/chat-types` |
354
+ | `RawOrigin` | type | `../elements/chat-types` |
355
+ | `ToolPart` | type | `../components/tool-types` |
356
+ | `ToolKind` | type | `../components/tool-classify` |
357
+ | `classifyTool` | value | `../components/tool-classify` |
358
+ | `CardEnvelope` | type | `../primitives/card-contract` |
359
+ | `AttachmentData` | type | `../components/attachment-types` |
360
+
361
+ #### `@kitn.ai/ui/state` · `stream` — the shipped declarations
362
+
363
+ ```ts
364
+ /** The one universal contract: a functional-updater setter (React setState shape). */
365
+ export type SetMessages = (updater: (prev: ChatMessage[]) => ChatMessage[]) => void;
366
+ /** Every OBJECT payload a `MessagePart` variant carries. `type`/`raw` are the
367
+ * variant's own bookkeeping, not payload; the primitive payloads (`text`,
368
+ * `label`, `index`, ...) drop out at `Extract<..., object>`. */
369
+ type PartPayload = Extract<MessagePart extends infer P ? (P extends object ? P[Exclude<keyof P, 'type' | 'raw'>] : never) : never, object>;
370
+ /** Every bag one of these mutators takes: the part payloads plus the options
371
+ * bags that are not payloads themselves. */
372
+ type MutatorBag = PartPayload | ReasoningOpts;
373
+ /** `keyof` over a union member-by-member. The bare `keyof (A | B)` is the
374
+ * INTERSECTION of their keys, which is the opposite of what this needs. */
375
+ type KeysOf<T> = T extends unknown ? keyof T : never;
376
+ /** `Shape`, but any key that belongs exclusively to a SIBLING bag is a compile
377
+ * error. Keys `Shape` never heard of are untouched, so a consumer's own
378
+ * superset of a citation still passes — only the mix-ups fail. That is a
379
+ * denylist, not an exact type, and deliberately so: boundary point 1. */
380
+ type Unmixed<Shape> = Shape & {
381
+ [K in Exclude<KeysOf<MutatorBag>, keyof Shape>]?: never;
382
+ };
383
+ /** A fluent builder for one in-flight assistant message. Owns no state. */
384
+ export interface AssistantStream {
385
+ readonly id: string;
386
+ appendText(delta: string): AssistantStream;
387
+ appendReasoning(delta: string, opts?: Unmixed<ReasoningOpts>): AssistantStream;
388
+ upsertTool(toolCallId: string, patch: Unmixed<Partial<ToolPart>>): AssistantStream;
389
+ /** Adds a card, or REPLACES the existing one with the same `envelope.id`. A
390
+ * model that revises a card mid-turn re-sends the whole envelope, so a second
391
+ * call with a known id revises that card in place rather than rendering a
392
+ * second copy of it. See `upsertCardPart`. */
393
+ addCard(envelope: CardEnvelope): AssistantStream;
394
+ addSource(source: Unmixed<Source>): AssistantStream;
395
+ addFile(attachment: AttachmentData): AssistantStream;
396
+ done(): void;
397
+ /** Settles the turn as FAILED and puts `reason` where the reader can find it.
398
+ *
399
+ * WHERE THE REASON LANDS, in order:
400
+ * 1. every tool part that has NOT produced a result flips to `output-error`
401
+ * with `errorText: reason`, so no panel spins forever. A tool ALREADY in
402
+ * `output-error` with its own non-empty `errorText` is left exactly as it
403
+ * is: "search index offline" is the answer to what went wrong and
404
+ * "Connection lost." is the generic outer symptom, so overwriting the
405
+ * specific with the generic loses the only actionable thing on the
406
+ * message. It still counts as carrying the failure. A tool in
407
+ * `output-error` with NO text does get filled in — an error panel with
408
+ * nothing in it says no more than a blank bubble does.
409
+ * 2. if no part was able to carry it, the reason is APPENDED as its own text
410
+ * part.
411
+ *
412
+ * Never both — a turn does not report the same failure twice.
413
+ *
414
+ * "Where the reader can FIND it" is deliberate, and rule 1 and rule 2 are not
415
+ * equally loud. Rule 2 is text in the thread: read without any interaction.
416
+ * Rule 1 is a tool panel, which renders COLLAPSED — the header shows an error
417
+ * icon and a badge, so the failure is unmissable, but the `errorText` itself
418
+ * is one click away. That is the right default for a failed tool inside an
419
+ * otherwise readable answer, and it is why rule 2 exists rather than always
420
+ * stamping the panel and calling it reported.
421
+ *
422
+ * Rule 2 is the whole point and it used to be missing. `abort` only ever did
423
+ * rule 1, so a TEXT-ONLY turn — every turn of a text-only support widget —
424
+ * had nothing to stamp: the string was discarded and a failed request
425
+ * rendered an EMPTY assistant bubble, while the consumer that passed a
426
+ * perfectly good sentence believed it had reported the failure. That is the
427
+ * repo's "decide loudly" rule broken on the one path where being quiet is
428
+ * worst, and it shipped because the discard is invisible from the call site.
429
+ *
430
+ * It is a NEW part, never a merge onto the trailing text: gluing "Connection
431
+ * lost." onto the model's half-finished sentence reads as the model saying
432
+ * it. Text that already streamed stays exactly where it is.
433
+ *
434
+ * The reason is TRIMMED, and `abort()` with no reason — or one that is empty
435
+ * or all whitespace — appends nothing: there is no reason to discard, and the
436
+ * kit will not invent copy the consumer did not write. Rule 1 still runs,
437
+ * with `errorText: undefined`. Whitespace is not a pedantic case here: the
438
+ * scaffold hands over `err.message`, and an `Error` is free to carry `''`,
439
+ * which would otherwise render an INVISIBLE text part — a blank bubble that
440
+ * also claims to have said something.
441
+ *
442
+ * Settled is settled: after `done()` or a first `abort()`, this is a no-op
443
+ * like every other mutator, so the reason cannot be appended twice.
444
+ *
445
+ * The reason is CONSUMER-facing text and is rendered as markdown like any
446
+ * other text part. Pass a sentence a visitor can read, not a stack trace. */
447
+ abort(reason?: string): void;
448
+ }
449
+ /** Start an assistant message and drive it through `set`. New refs on every mutation. */
450
+ export declare function createAssistantStream(set: SetMessages, init?: Partial<ChatMessage>): AssistantStream;
451
+ /** Wrap a stream so `onSettle` fires on done/abort (used to toggle a `loading` flag).
452
+ * Preserves the fluent chain by returning the wrapper from every mutator. */
453
+ export declare function onStreamSettled(inner: AssistantStream, onSettle: () => void): AssistantStream;
454
+ ```
455
+
456
+ #### `@kitn.ai/ui/state` · `parts` — the shipped declarations
457
+
458
+ ```ts
459
+ /** Stable structural fingerprint. Key order independent, so an identical snapshot
460
+ * arriving twice compares equal and can be skipped. */
461
+ export declare function fingerprint(value: unknown): string;
462
+ /** Appends to the trailing text part, or OPENS A NEW ONE if the last part is not
463
+ * text. This is what stops a post-tool answer being glued onto the pre-tool text. */
464
+ export declare function appendTextPart(parts: MessagePart[], delta: string): MessagePart[];
465
+ export interface ReasoningOpts {
466
+ index?: number;
467
+ /** Namespaces `index` to one provider response stream. Producers that read more
468
+ * than one stream into the SAME message must set it. See `appendReasoningPart`. */
469
+ streamId?: string;
470
+ label?: string;
471
+ signature?: string;
472
+ raw?: RawOrigin;
473
+ }
474
+ /** Keyed by `(streamId, index)` so parallel reasoning blocks stay distinct.
475
+ *
476
+ * WHY THE KEY IS A PAIR. A block index alone is NOT unique inside one `parts`
477
+ * array. Anthropic numbers content blocks per MESSAGE and restarts at 0 on the
478
+ * next one, while a tool loop folds every round into a single assistant turn.
479
+ * Keyed on index alone, round 2's thinking block (index 0) merges into round 1's
480
+ * part: the text concatenates and `raw: opts.raw ?? cur.raw` OVERWRITES round 1's
481
+ * verbatim provider payload with round 2's. `toAnthropicMessages` then emits one
482
+ * thinking block where two belong, carrying round 2's signature in round 1's
483
+ * position -- a modified-and-filtered thinking block, which is exactly the 400
484
+ * the verbatim `raw` channel exists to prevent.
485
+ *
486
+ * `streamId` is the namespace: one value per provider response stream, attached
487
+ * by `consumeModelStream`. Two rounds are two streams, so their index 0s are two
488
+ * parts. Producers that drive a sink from a single stream can omit it; `undefined`
489
+ * is its own namespace and behaves exactly as before.
490
+ *
491
+ * Returns the SAME array reference when the merge produces an identical part,
492
+ * for the same reason `upsertToolPart` does: a new `parts` array is the
493
+ * re-render signal, so handing one back for a delta that changed nothing is a
494
+ * spurious render.
495
+ *
496
+ * An EMPTY delta is not a no-op and must still reach here: it is how a redacted
497
+ * reasoning block, a `signature_delta` and an assembled `content_block_stop`
498
+ * block arrive, and how a format opens a block at the right position so block
499
+ * ORDER survives into `parts`. Those carry a new `raw`/`signature`/index and so
500
+ * compare unequal and DO rebuild. What the check absorbs is the other empty
501
+ * frame: one carrying nothing new, which a provider is free to send repeatedly.
502
+ *
503
+ * `signature` and `raw` resolve with `??`, so an explicit `undefined` from a
504
+ * later delta never blanks a value an earlier one established. Pass a DEFINED
505
+ * value to replace either; there is no way to clear them. */
506
+ export declare function appendReasoningPart(parts: MessagePart[], delta: string, opts?: ReasoningOpts): MessagePart[];
507
+ /** Creates or REPLACES a card part, keyed on `envelope.id`. Returns the SAME array
508
+ * reference when the incoming envelope is structurally identical to the current
509
+ * one, for the same reason `upsertToolPart` does: a new `parts` array is the
510
+ * re-render signal, so handing one back for a revision that changed nothing is a
511
+ * spurious render.
512
+ *
513
+ * WHY THIS REPLACES WHERE `upsertToolPart` MERGES. A tool part is patched
514
+ * fragment-by-fragment as its arguments stream in, which is why that function
515
+ * needs carry-forward rules for `raw` and `kind` — a later patch that omits a
516
+ * field is not asserting the field is gone. A card envelope is the opposite: it
517
+ * arrives WHOLE, as one complete tool result, so an omitted field IS an
518
+ * assertion. Last-write-wins is both simpler and the only semantics under which
519
+ * a host can CLEAR `resolution` to re-open a dismissed card — a field-by-field
520
+ * merge can only ever set that field, never unset it, so `CardPolicy.onReopen`
521
+ * (see `primitives/card-contract.ts`) would have no way to express its result.
522
+ *
523
+ * The PART-level `raw` is preserved across a revision. It is a different field
524
+ * from anything inside the envelope: the untranslated provider payload the part
525
+ * was built from, attached once by the producer, which a fresh envelope carries
526
+ * no opinion about.
527
+ *
528
+ * Position is preserved: a revised card stays where it first appeared in the
529
+ * thread rather than jumping past the text that followed it. */
530
+ export declare function upsertCardPart(parts: MessagePart[], envelope: CardEnvelope): MessagePart[];
531
+ /** Creates or merges a tool part. Returns the SAME array reference when the merge
532
+ * produces an identical tool, so repeated snapshots do not trigger a re-render.
533
+ *
534
+ * Two fields do NOT follow plain spread semantics, because a streaming provider
535
+ * hands them over on one fragment and then keeps patching the rest:
536
+ * - `kind`: a value the consumer set is preserved across later patches instead
537
+ * of being reverted to `classifyTool(type)` (see `resolveKind`).
538
+ * - `raw`: an explicit `raw: undefined` never blanks a `raw` an earlier patch
539
+ * established. Pass a DEFINED `raw` to replace it; there is no way to clear it. */
540
+ export declare function upsertToolPart(parts: MessagePart[], toolCallId: string, patch: Partial<ToolPart>): MessagePart[];
541
+ ```
542
+
543
+ #### `@kitn.ai/ui/state` · `mock` — the shipped declarations
544
+
545
+ ```ts
546
+ /** The `model` every mock frame reports. Not a model any provider serves — see
547
+ * tell 3 in the header. */
548
+ export declare const MOCK_MODEL_ID = "kai-mock";
549
+ /** The marker field carried by every mock frame. Tell 2. */
550
+ export declare const MOCK_MARKER_KEY = "_kai_mock";
551
+ /** The value of that marker: a whole sentence, because it is read by a human
552
+ * staring at a logged frame and wondering where the reply came from. */
553
+ export declare const MOCK_MARKER = "no provider was contacted \u2014 this reply was generated locally by createMockResponder() from @kitn.ai/ui/state";
554
+ /** The SSE comment that opens every mock stream. Tell 1. */
555
+ export declare const MOCK_BANNER = ": kai-mock \u2014 NO PROVIDER WAS CONTACTED. no provider was contacted \u2014 this reply was generated locally by createMockResponder() from @kitn.ai/ui/state.";
556
+ /** The default canned replies, cycled per turn so a multi-turn preview stays
557
+ * coherent instead of repeating one line forever. */
558
+ export declare const DEFAULT_MOCK_REPLIES: readonly string[];
559
+ /** One scripted tool call for a mock turn. Framed exactly the way the OpenAI
560
+ * chat-completions wire frames a real one — an announce fragment carrying
561
+ * `id`/`function.name`, then the argument JSON streamed in fragments — so the
562
+ * kit's own reader (`readOpenAIStream`) reassembles it through the same path a
563
+ * real provider's call takes. */
564
+ export interface MockToolCall {
565
+ /** The tool name, e.g. `'get_weather'` or a card tool like `'kai_confirm'`. */
566
+ name: string;
567
+ /** The call's arguments. Serialized with `JSON.stringify` and streamed as
568
+ * fragments, like a real provider. Defaults to `{}`. */
569
+ arguments?: unknown;
570
+ /** Explicit tool-call id. Defaults to `call_kai-mock-<turn>-<n>`, which keeps
571
+ * the mock's naming tell (see tell 3): no provider issues ids in that shape. */
572
+ id?: string;
573
+ }
574
+ /** A scripted mock turn: optional text, then optional tool calls. A turn with
575
+ * tool calls finishes `finish_reason: 'tool_calls'`, exactly as a real
576
+ * tool-calling turn does; a turn without them finishes `'stop'`. */
577
+ export interface MockTurn {
578
+ /** Text streamed (token by token) before the tool calls. */
579
+ text?: string;
580
+ /** Tool calls announced this turn, in order. */
581
+ toolCalls?: readonly MockToolCall[];
582
+ }
583
+ /** A canned reply: plain text, or a scripted turn. A string is exactly
584
+ * `{ text }` — the pre-tool-call API unchanged. */
585
+ export type MockReply = string | MockTurn;
586
+ export interface MockResponderOptions {
587
+ /** Canned replies, cycled one per turn. Plain strings stream as text; a
588
+ * `MockTurn` can also script tool calls (`{ text, toolCalls }`), which is
589
+ * what lets the zero-config mock exercise the kit's tool/card path without
590
+ * hand-rolled SSE framing. Defaults to `DEFAULT_MOCK_REPLIES`. */
591
+ replies?: readonly MockReply[];
592
+ /** Delay between chunks, in ms. Defaults to 24 — fast enough to feel alive,
593
+ * slow enough that the streaming is visible. `0` streams as fast as the
594
+ * event loop allows, which is what tests want. */
595
+ delayMs?: number;
596
+ /** How many whitespace-delimited tokens ride in each frame. Defaults to 1
597
+ * (token by token). Larger values coarsen the cadence. */
598
+ chunkSize?: number;
599
+ /** Log a one-time notice on the first turn. Defaults to `true`: the point of
600
+ * this module is that a mock reply is hard to mistake for a real one, and a
601
+ * console line is the fastest way for a human to notice. Pass `false` in
602
+ * tests, or wherever the banner and the frame markers are tell enough. */
603
+ announce?: boolean;
604
+ }
605
+ /** Produces one turn's worth of SSE frames. Structurally a `StreamSource`, so it
606
+ * goes straight into `readOpenAIStream(responder(text), stream)`. */
607
+ export type MockResponder = (prompt?: string) => AsyncIterable<string>;
608
+ /**
609
+ * Build a mock responder.
610
+ *
611
+ * ```ts
612
+ * import { createAssistantStream, createMockResponder } from '@kitn.ai/ui/state';
613
+ * import { readOpenAIStream } from '@kitn.ai/ui/wire';
614
+ *
615
+ * const mockResponse = createMockResponder();
616
+ * const stream = createAssistantStream(setMessages);
617
+ * await readOpenAIStream(mockResponse(value), stream); // <- swap for fetch()
618
+ * stream.done();
619
+ * ```
620
+ */
621
+ export declare function createMockResponder(options?: MockResponderOptions): MockResponder;
622
+ ```
623
+
624
+ ### `@kitn.ai/ui/wire`
625
+
626
+ The model-stream adapter. The kit PARSES, the consumer FETCHES: you make the request (your endpoint, your key), hand the response body to a reader, and it folds provider SSE onto message parts. The encoders turn the thread back into provider messages.
627
+
628
+ Every export (66, derived from `dist/wire/index.d.ts`):
629
+
630
+ | Export | Kind | Module |
631
+ |---|---|---|
632
+ | `readModelStream` | value | `read` |
633
+ | `readOpenAIStream` | value | `read` |
634
+ | `readAnthropicStream` | value | `read` |
635
+ | `WireError` | value | `read` |
636
+ | `StreamSource` | type | `read` |
637
+ | `ReadOptions` | type | `read` |
638
+ | `consumeModelStream` | value | `consume` |
639
+ | `createToolCallAccumulator` | value | `consume` |
640
+ | `applyToolOutput` | value | `sink-helpers` |
641
+ | `applyToolFailure` | value | `sink-helpers` |
642
+ | `bufferText` | value | `sink-helpers` |
643
+ | `toOpenAIMessages` | value | `encode` |
644
+ | `toAnthropicMessages` | value | `encode` |
645
+ | `WireEncodeError` | value | `encode` |
646
+ | `AnthropicContentBlock` | type | `encode` |
647
+ | `AnthropicEncodeOptions` | type | `encode` |
648
+ | `AnthropicWireMessage` | type | `encode` |
649
+ | `FileEncodeOptions` | type | `encode` |
650
+ | `OpenAIContentPart` | type | `encode` |
651
+ | `OpenAIEncodeOptions` | type | `encode` |
652
+ | `OpenAIReasoningDetail` | type | `encode` |
653
+ | `OpenAIToolCall` | type | `encode` |
654
+ | `OpenAIWireMessage` | type | `encode` |
655
+ | `UnencodableFilePolicy` | type | `encode` |
656
+ | `encodableMediaTypes` | value | `media-types` |
657
+ | `resolveMediaPolicy` | value | `media-types` |
658
+ | `EncodableKind` | type | `media-types` |
659
+ | `MediaDecision` | type | `media-types` |
660
+ | `MediaPolicy` | type | `media-types` |
661
+ | `MediaPolicyOptions` | type | `media-types` |
662
+ | `MediaTypeFilter` | type | `media-types` |
663
+ | `openaiChatFormat` | value | `formats/openai` |
664
+ | `anthropicMessagesFormat` | value | `formats/anthropic` |
665
+ | `sseDataFrames` | value | `sse` |
666
+ | `sseJson` | value | `sse` |
667
+ | `readableToAsyncIterable` | value | `sse` |
668
+ | `ByteSource` | type | `sse` |
669
+ | `subscribeWireDiagnostics` | value | `diagnostics` |
670
+ | `AppRequestEvent` | type | `diagnostics` |
671
+ | `EncodeAttachmentReport` | type | `diagnostics` |
672
+ | `EncodeDroppedEvent` | type | `diagnostics` |
673
+ | `EncodeRequestEvent` | type | `diagnostics` |
674
+ | `WireCloseEvent` | type | `diagnostics` |
675
+ | `WireDiagnosticBase` | type | `diagnostics` |
676
+ | `WireDiagnosticEvent` | type | `diagnostics` |
677
+ | `WireFailedEvent` | type | `diagnostics` |
678
+ | `WireFrameEvent` | type | `diagnostics` |
679
+ | `WireInterruptedEvent` | type | `diagnostics` |
680
+ | `WireOpenEvent` | type | `diagnostics` |
681
+ | `WirePartEvent` | type | `diagnostics` |
682
+ | `normalizeStopReason` | value | `chunk` |
683
+ | `AssistantStreamSink` | type | `chunk` |
684
+ | `ConsumeOptions` | type | `chunk` |
685
+ | `ModelStreamChunk` | type | `chunk` |
686
+ | `ModelToolCall` | type | `chunk` |
687
+ | `ModelToolCallDelta` | type | `chunk` |
688
+ | `ModelTurn` | type | `chunk` |
689
+ | `ModelUsage` | type | `chunk` |
690
+ | `StopReason` | type | `chunk` |
691
+ | `WireFormat` | type | `chunk` |
692
+ | `WireFormatReader` | type | `chunk` |
693
+ | `ChatMessage` | type | `../elements/chat-types` |
694
+ | `MessagePart` | type | `../elements/chat-types` |
695
+ | `MessageSource` | type | `../elements/chat-types` |
696
+ | `RawOrigin` | type | `../elements/chat-types` |
697
+ | `ToolPart` | type | `../components/tool-types` |
698
+
699
+ #### `@kitn.ai/ui/wire` · `read` — the shipped declarations
700
+
701
+ ```ts
702
+ export type StreamSource = Response | ReadableStream<Uint8Array> | AsyncIterable<Uint8Array | string>;
703
+ export interface ReadOptions extends ConsumeOptions {
704
+ format: WireFormat;
705
+ }
706
+ /** A non-ok HTTP response from the model endpoint, with the provider's own error
707
+ * body attached when there is one. Thrown before a single chunk is read, so a
708
+ * caller can distinguish "the request failed" from "the stream carried an
709
+ * error", which is `ModelTurn.error`. */
710
+ export declare class WireError extends Error {
711
+ readonly status: number;
712
+ readonly statusText: string;
713
+ /** The response body parsed as JSON, or undefined when it was not JSON (an
714
+ * HTML error page from a proxy, most often). */
715
+ readonly body: unknown;
716
+ /** The raw response body, always. */
717
+ readonly bodyText: string;
718
+ constructor(status: number, statusText: string, bodyText: string, body: unknown);
719
+ }
720
+ /** Read one turn off the wire in `opts.format` and drive `sink` with it. */
721
+ export declare function readModelStream(source: StreamSource, sink: AssistantStreamSink, opts: ReadOptions): Promise<ModelTurn>;
722
+ /** OpenAI chat-completions SSE. Also what all nine catalog integrations except
723
+ * `mock` re-frame to server-side, so this is the common path. */
724
+ export declare function readOpenAIStream(source: StreamSource, sink: AssistantStreamSink, opts?: ConsumeOptions): Promise<ModelTurn>;
725
+ /** Anthropic Messages SSE. */
726
+ export declare function readAnthropicStream(source: StreamSource, sink: AssistantStreamSink, opts?: ConsumeOptions): Promise<ModelTurn>;
727
+ ```
728
+
729
+ #### `@kitn.ai/ui/wire` · `encode` — the shipped declarations
730
+
731
+ ```ts
732
+ export interface OpenAIToolCall {
733
+ id: string;
734
+ type: 'function';
735
+ function: {
736
+ name: string;
737
+ arguments: string;
738
+ };
739
+ }
740
+ /** One `reasoning_details` entry. Provider-owned open shape, kept as a record
741
+ * for the same reason `AnthropicContentBlock` is: an opaque entry has to pass
742
+ * through UNTOUCHED, and a closed type would be a list of the fields we happen
743
+ * to have seen. */
744
+ export type OpenAIReasoningDetail = Record<string, unknown>;
745
+ /** A multimodal user message's content entries. `image_url` takes an https URL
746
+ * or a `data:` URI in the same field; `file` takes `file_data`, which is a DATA
747
+ * URI on this wire (`data:application/pdf;base64,...`) and not bare base64. */
748
+ export type OpenAIContentPart = {
749
+ type: 'text';
750
+ text: string;
751
+ } | {
752
+ type: 'image_url';
753
+ image_url: {
754
+ url: string;
755
+ };
756
+ } | {
757
+ type: 'file';
758
+ file: {
759
+ filename?: string;
760
+ file_data: string;
761
+ };
762
+ };
763
+ export interface OpenAIWireMessage {
764
+ role: 'system' | 'user' | 'assistant' | 'tool';
765
+ /** An ARRAY only when the turn carries an encodable `file` part. A text-only
766
+ * turn stays a plain string, so adding attachment support changed nothing
767
+ * about what an existing thread puts on the wire. */
768
+ content: string | OpenAIContentPart[] | null;
769
+ tool_calls?: OpenAIToolCall[];
770
+ tool_call_id?: string;
771
+ name?: string;
772
+ /** Only ever present when `toOpenAIMessages` was asked for it. See
773
+ * `OpenAIEncodeOptions.reasoning`. */
774
+ reasoning_details?: OpenAIReasoningDetail[];
775
+ }
776
+ /** EXTENDS rather than restates the file options: `onUnencodableFile` and
777
+ * `accept` mean the same thing on both wires, and a second declaration of them
778
+ * here is a second place to forget to update. */
779
+ export interface OpenAIEncodeOptions extends FileEncodeOptions {
780
+ /**
781
+ * Whether to send the assistant's own reasoning back with the thread.
782
+ *
783
+ * DEFAULT `'omit'`, and that default is a measurement, not caution. Omitting
784
+ * reasoning is accepted by every configuration tested -- five live omission
785
+ * trials plus 28 recorded live requests per configuration across the spike's
786
+ * conformance sweep, zero 400s -- so the path that ships today demonstrably
787
+ * works, while including reasoning cost about 25% more prompt tokens per round
788
+ * when measured (665 -> 834 on a two-round loop). A library does not get to
789
+ * raise every consumer's bill and add a new provider-validation surface as a
790
+ * side effect of a bug fix.
791
+ *
792
+ * `'include'` is for a multi-round TOOL loop, which is where OpenRouter says it
793
+ * pays: "when you post tool results, including the original reasoning ensures
794
+ * the model can continue its reasoning from where it left off". Measured
795
+ * accepted (HTTP 200) for a signed Anthropic block and for an OpenAI encrypted
796
+ * block, over the OpenAI-compatible wire.
797
+ *
798
+ * The Anthropic wire has no such knob because it has no such choice: a filtered
799
+ * or rebuilt thinking block there is a hard 400.
800
+ */
801
+ reasoning?: 'omit' | 'include';
802
+ }
803
+ /**
804
+ * What to do with a `file` part this wire cannot carry.
805
+ *
806
+ * DEFAULT `'throw'`, and the default is the whole point. Skipping is how
807
+ * attachments came to render perfectly in the thread and reach the model as
808
+ * nothing: the developer wires up upload, watches it work, and ships a model
809
+ * that cannot see the file. A throw here names the message, the part and the
810
+ * reason, which is strictly more than a 400 at request time would tell you.
811
+ *
812
+ * `'skip'` restores the lenient behaviour for a host that would rather send a
813
+ * degraded turn than fail one. It is silent, but it is silence the developer
814
+ * asked for by name, which is the difference that matters.
815
+ */
816
+ export type UnencodableFilePolicy = 'throw' | 'skip';
817
+ export interface FileEncodeOptions {
818
+ onUnencodableFile?: UnencodableFilePolicy;
819
+ /**
820
+ * Narrow which attachment media types reach the wire, as HTML `accept` syntax
821
+ * (`'image/*,application/pdf'`) or an array of the same.
822
+ *
823
+ * THE SAME STRING the composer takes as `<kai-chat accept="...">`, resolved by
824
+ * the same function against the same declaration -- so a developer writes the
825
+ * set once as a constant and hands it to both ends. Omitted means the kit's
826
+ * full capability set, which is `encodableMediaTypes()`.
827
+ *
828
+ * It can only NARROW. Naming a type the encoders cannot represent does not
829
+ * enable it; that would just move the failure to a provider 400.
830
+ */
831
+ accept?: MediaTypeFilter;
832
+ /**
833
+ * The app's own id for the logical turn this encode belongs to, carried onto
834
+ * every diagnostic event the encode emits. Purely diagnostic: nothing here
835
+ * branches on it and it never reaches a provider.
836
+ *
837
+ * THE SAME FIELD, THE SAME MEANING, as `ConsumeOptions.traceId` -- and that
838
+ * symmetry is the whole payoff. Encoding happens BEFORE a read opens, so
839
+ * there is no stream to attach an encode to and the kit will not invent one.
840
+ * Pass the same id to both halves:
841
+ *
842
+ * const body = toOpenAIMessages(messages, { traceId: 'turn-42' });
843
+ * readOpenAIStream(res, sink, { traceId: 'turn-42' });
844
+ *
845
+ * and the request and the response it produced sit together, with a tool loop
846
+ * or a sub-agent fan-out grouping into one trace. Without it you still see
847
+ * both halves; they are simply unlinked, which is the honest rendering --
848
+ * pinning an encode to "the next stream that opens" would be a guess, and an
849
+ * encode may be followed by no stream at all.
850
+ */
851
+ traceId?: string;
852
+ /** The app's name for this call inside its trace (`'planner'`, `'retry-2'`).
853
+ * Same field and same meaning as `ConsumeOptions.label`. Absent when not
854
+ * supplied. */
855
+ label?: string;
856
+ }
857
+ export type AnthropicEncodeOptions = FileEncodeOptions;
858
+ /** Anthropic content blocks are an open, provider-owned union. Keeping them as
859
+ * records is what lets a verbatim `thinking` payload pass through UNTOUCHED,
860
+ * which is the entire point of this encoder. */
861
+ export type AnthropicContentBlock = Record<string, unknown>;
862
+ export interface AnthropicWireMessage {
863
+ role: 'user' | 'assistant';
864
+ content: AnthropicContentBlock[];
865
+ }
866
+ /** A message cannot be encoded without losing something the provider will reject.
867
+ * Thrown at encode time, on purpose: a throw here beats a 400 at request time,
868
+ * because here you still know which message and which part caused it. */
869
+ export declare class WireEncodeError extends Error {
870
+ readonly messageId: string;
871
+ readonly partIndex: number;
872
+ constructor(message: string, messageId: string, partIndex: number);
873
+ }
874
+ /**
875
+ * ChatMessage[] to an OpenAI chat-completions `messages` array.
876
+ *
877
+ * ONE ChatMessage CAN BECOME SEVERAL WIRE MESSAGES. The kit streams a whole
878
+ * assistant turn into a single message, so text, a tool call and the model's
879
+ * answer to that call all live in one `parts` array. The OpenAI wire has no such
880
+ * shape: a `role:'tool'` result must sit between the assistant message that
881
+ * announced the call and whatever the model said afterwards. So the turn is
882
+ * SPLIT at each tool boundary, into
883
+ *
884
+ * assistant(pre-tool text + tool_calls) -> tool(result)... -> assistant(answer)
885
+ *
886
+ * Flattening instead would put the model's answer BEFORE the result it was based
887
+ * on. No endpoint rejects that, which is exactly why it is worth spelling out:
888
+ * it quietly degrades every later round of a tool loop.
889
+ *
890
+ * Consecutive tool parts stay in ONE assistant message, because parallel calls
891
+ * are announced together and their results follow together.
892
+ *
893
+ * A turn that encodes to nothing is SKIPPED, never sent as `{ content: null }`
894
+ * with no `tool_calls`: OpenAI treats `content` as required unless `tool_calls`
895
+ * is present, and strict-compatible endpoints reject it.
896
+ *
897
+ * REASONING IS OPT-IN, and off by default. OpenRouter's OpenAI-compatible
898
+ * endpoint does have a channel on the way back in -- `reasoning_details` on the
899
+ * assistant message -- and `{ reasoning: 'include' }` uses it, one entry per
900
+ * reasoning part, in part order, reassembled by `reasoningDetailOf` rather than
901
+ * echoed out of `part.raw`. Read that function for which blocks make it and why.
902
+ * The default omits, because omitting is measured-accepted everywhere and costs
903
+ * about 25% fewer prompt tokens per round; see `OpenAIEncodeOptions.reasoning`.
904
+ *
905
+ * Reasoning alone still encodes to NOTHING. A block is content the model already
906
+ * produced, not a reason to send a turn, so a message carrying reasoning and no
907
+ * text and no settled tool is skipped exactly as before, rather than becoming
908
+ * `{ content: null }` with no `tool_calls`.
909
+ *
910
+ * `card` and `source` parts are never encoded; they are kit-side.
911
+ *
912
+ * `file` parts ARE encoded, on a USER turn, and a turn carrying nothing but an
913
+ * attachment is now a real message rather than nothing. Images become
914
+ * `image_url` (https URL or `data:` URI alike); a base64 PDF becomes a `file`
915
+ * part whose `file_data` is the data URI. Two cases have no form here and THROW
916
+ * by default: a remote PDF, because this wire's `file` part has no URL variant,
917
+ * and anything that is neither -- see `UnencodableFilePolicy` for why the
918
+ * default is a throw and not a skip.
919
+ *
920
+ * A `file` part on an ASSISTANT turn is still dropped. Neither API accepts image
921
+ * or document content in an assistant message, so there is nothing to encode it
922
+ * to; attachments belong to the user turn that sent them.
923
+ */
924
+ export declare function toOpenAIMessages(messages: ChatMessage[], options?: OpenAIEncodeOptions): OpenAIWireMessage[];
925
+ /**
926
+ * ChatMessage[] to an Anthropic Messages `messages` array. THE ROUND-TRIP
927
+ * ENCODER.
928
+ *
929
+ * A reasoning block is emitted as `part.raw.payload` verbatim and is NEVER
930
+ * rebuilt from `text` plus `signature`: Anthropic returns 400 if a thinking
931
+ * block in the most recent assistant message is modified, reordered, filtered or
932
+ * reconstructed. A reasoning part with no `raw`, or with a `raw` captured from
933
+ * some other format, therefore THROWS rather than silently producing a request
934
+ * that will fail.
935
+ *
936
+ * Block order follows part order, which follows stream order, with no filtering,
937
+ * because the API validates order too. An empty-text reasoning part (an omitted
938
+ * or redacted block) is still emitted: the docs require sending back every block
939
+ * "including any blocks with empty thinking fields".
940
+ *
941
+ * ONE ChatMessage CAN BECOME SEVERAL WIRE MESSAGES, for the same reason as
942
+ * `toOpenAIMessages`: the kit streams a whole assistant turn into one message, so
943
+ * the tool call and the model's answer to it share a `parts` array, but Anthropic
944
+ * carries the result in a SEPARATE user message that has to sit between them. So
945
+ * the turn is SPLIT at each tool boundary, into
946
+ *
947
+ * assistant(pre-tool blocks + tool_use) -> user(tool_result)... -> assistant(answer)
948
+ *
949
+ * Flattening instead puts the model's answer BEFORE the result it was based on,
950
+ * and strands every later round's thinking block in the first assistant message.
951
+ * Consecutive tool parts stay in ONE assistant message, because parallel calls are
952
+ * announced together and their results come back together.
953
+ *
954
+ * Adjacent user messages are MERGED. The API combines consecutive same-role turns
955
+ * itself rather than rejecting them, so this is not what stands between you and a
956
+ * 400; it is emitted anyway because the tool-result turn and a following user turn
957
+ * are one turn, several OpenAI-compatible Anthropic proxies do enforce strict
958
+ * alternation, and the merged form is what the models are trained on. Ordering is
959
+ * safe by construction: `results` is only non-empty when `blocks` is, so a
960
+ * tool_result message always follows its assistant message and can never be
961
+ * appended after a plain user turn.
962
+ *
963
+ * `file` parts on a USER turn become `image` and `document` blocks, in part
964
+ * order. Both take `source: {type:'base64'}` and `source: {type:'url'}`, so this
965
+ * wire can carry a remote PDF that `toOpenAIMessages` has to refuse. Anything
966
+ * neither API accepts as message content THROWS by default; see
967
+ * `UnencodableFilePolicy`. A `file` part on an ASSISTANT turn is dropped, because
968
+ * an assistant message here carries only text, thinking and tool_use.
969
+ *
970
+ * Asymmetry worth knowing: `tool_use.input` is a parsed OBJECT on this wire, not
971
+ * a string, so it uses `input` and not `rawInput`. Only thinking blocks carry a
972
+ * verbatim requirement.
973
+ */
974
+ export declare function toAnthropicMessages(messages: ChatMessage[], options?: AnthropicEncodeOptions): AnthropicWireMessage[];
975
+ ```
976
+
977
+ #### `@kitn.ai/ui/wire` · `chunk` — the shipped declarations
978
+
979
+ ```ts
980
+ /** One fragment of a tool call. */
981
+ export interface ModelToolCallDelta {
982
+ /**
983
+ * The ONLY thing correlating fragments, and its NAMESPACE IS FORMAT-DEFINED.
984
+ * `openaiChatFormat` uses the position in `delta.tool_calls`;
985
+ * `anthropicMessagesFormat` uses the content-block index. Both are correct and
986
+ * both are stable within one stream, but they are not the same number, so a
987
+ * third-party format must pick one and stay consistent with itself.
988
+ */
989
+ index: number;
990
+ id?: string;
991
+ /** Usually whole on the first fragment; a few providers split it. */
992
+ name?: string;
993
+ /** A FRAGMENT of the JSON arguments string, not valid JSON on its own. */
994
+ arguments?: string;
995
+ /** A result the PROVIDER executed (Anthropic web_search_tool_result, an OpenAI
996
+ * built-in). Completes the panel with no host work. */
997
+ output?: Record<string, unknown>;
998
+ /** A provider-executed tool that failed. */
999
+ outputError?: string;
1000
+ }
1001
+ /** Field names are deliberately provider-neutral. OpenAI says prompt/completion,
1002
+ * Anthropic says input/output; input/output is the one that reads correctly for
1003
+ * both. */
1004
+ export interface ModelUsage {
1005
+ inputTokens?: number;
1006
+ outputTokens?: number;
1007
+ totalTokens?: number;
1008
+ /** Non-zero proves the model reasoned even when no reasoning text streamed. */
1009
+ reasoningTokens?: number;
1010
+ cachedInputTokens?: number;
1011
+ costUsd?: number;
1012
+ }
1013
+ export interface ModelStreamChunk {
1014
+ text?: string;
1015
+ /**
1016
+ * The model id the RESPONSE stated, verbatim; REPORT, NEVER INFER.
1017
+ *
1018
+ * Read from the response rather than the request, which is what makes it work
1019
+ * at all when the app builds its own fetch and the kit never sees what was
1020
+ * asked for. Providers commonly resolve an alias (ask for `gpt-4o`, get
1021
+ * `gpt-4o-2024-08-06`); through a gateway the value is the gateway's own id.
1022
+ * Both are reasons to pass the string through untouched.
1023
+ *
1024
+ * It is NOT guaranteed. A proxy can strip or rewrite it and a custom endpoint
1025
+ * may omit it, so a consumer renders it as ABSENT when it is absent. Filling
1026
+ * the gap with the requested id would lie in exactly the requested-vs-served
1027
+ * mismatch this field exists to catch.
1028
+ */
1029
+ model?: string;
1030
+ /**
1031
+ * Reasoning delta. `''` is MEANINGFUL, not a no-op: a redacted block has no
1032
+ * readable text but still carries a payload that must round-trip, and a format
1033
+ * uses an empty delta to OPEN a reasoning part at the right position in the
1034
+ * stream so block order survives into `parts`.
1035
+ */
1036
+ reasoning?: string;
1037
+ /** The provider's BLOCK index. Keeps parallel reasoning blocks distinct.
1038
+ * Omitted means block 0, the single-block case every provider degrades to. */
1039
+ reasoningIndex?: number;
1040
+ /**
1041
+ * The UNTRANSLATED provider payload for this reasoning block. Valid on a chunk
1042
+ * with NO reasoning text at all, which is the whole point: Anthropic returns
1043
+ * 400 if a `thinking` block is modified, reordered or RECONSTRUCTED, so an
1044
+ * encoder has to echo the original block rather than rebuild one from `text`
1045
+ * plus `signature`.
1046
+ */
1047
+ reasoningRaw?: RawOrigin;
1048
+ /** Informational. `reasoningRaw` is the round-trip channel, not this. */
1049
+ reasoningSignature?: string;
1050
+ toolCalls?: ModelToolCallDelta[];
1051
+ /** Citations the model produced. A run of consecutive `source` parts renders
1052
+ * as one citation row (`part="citations"`), outside the message bubble. */
1053
+ sources?: MessageSource[];
1054
+ /** Provider VERBATIM: 'stop' | 'tool_calls' | 'end_turn' | 'max_tokens' | ...
1055
+ * Normalizing in place would destroy information consumers branch on. */
1056
+ finishReason?: string | null;
1057
+ usage?: ModelUsage;
1058
+ /** An in-band provider error (the HTTP response was already 200). */
1059
+ error?: {
1060
+ code?: string | number;
1061
+ message: string;
1062
+ };
1063
+ }
1064
+ /** One vocabulary across formats, for code that has to BRANCH. `finishReason`
1065
+ * stays beside it, verbatim, for code that has to REPORT. */
1066
+ export type StopReason = 'stop' | 'length' | 'tool-calls' | 'content-filter' | 'error' | 'other';
1067
+ /** Unknown reasons degrade to 'other' rather than throwing: providers add stop
1068
+ * reasons without warning and a new one must not take a turn down. */
1069
+ export declare function normalizeStopReason(finishReason: string | null | undefined): StopReason | undefined;
1070
+ /**
1071
+ * The subset of the kit's `AssistantStream` the adapter drives. Declared
1072
+ * STRUCTURALLY so the adapter has no runtime dependency on a stream
1073
+ * implementation and can be tested against a recorder. The kit's real
1074
+ * `AssistantStream` satisfies it as-is: same method names, same arities, and its
1075
+ * `AssistantStream` returns are assignable to `unknown`.
1076
+ *
1077
+ * `addSource` is optional so a hand-rolled three-method sink still compiles.
1078
+ */
1079
+ export interface AssistantStreamSink {
1080
+ appendText(delta: string): unknown;
1081
+ appendReasoning(delta: string, opts?: ReasoningOpts): unknown;
1082
+ /** Create-or-merge. There is no separate "announce" call: handing a patch for
1083
+ * an unknown `toolCallId` creates the ToolPart, and every later patch merges. */
1084
+ upsertTool(toolCallId: string, patch: Partial<ToolPart>): unknown;
1085
+ addSource?(source: MessageSource): unknown;
1086
+ }
1087
+ /** One tool call reassembled out of the stream's fragments. */
1088
+ export interface ModelToolCall {
1089
+ /** The delta index that correlated this call's fragments. */
1090
+ index: number;
1091
+ /** Provider call id (synthesised as `call_<index>` if the provider omits it). */
1092
+ id: string;
1093
+ name: string;
1094
+ /** The RAW accumulated argument fragments. Echo THIS back on the next turn,
1095
+ * not a re-stringified parse. */
1096
+ argumentsText: string;
1097
+ /** Parsed arguments: present only when `argumentsText` was a valid JSON object. */
1098
+ input?: Record<string, unknown>;
1099
+ /** Present only for a call the PROVIDER executed. */
1100
+ output?: Record<string, unknown>;
1101
+ /** True when the provider ran the tool and returned its result in-stream. The
1102
+ * host must NOT execute these. */
1103
+ providerExecuted?: boolean;
1104
+ /** Why this call is unusable (malformed or truncated args, missing name). */
1105
+ error?: string;
1106
+ }
1107
+ /** Everything one assistant turn produced. */
1108
+ export interface ModelTurn {
1109
+ /** The turn as ORDERED MESSAGE PARTS, built with the kit's own part builders,
1110
+ * so it is exactly what the sink was driven with. Covers this turn only. */
1111
+ parts: MessagePart[];
1112
+ /** Flat concatenation of the text deltas. The provider wire format is a flat
1113
+ * string, so this is kept for encoders. Not the content model. */
1114
+ text: string;
1115
+ /** Flat concatenation of the reasoning deltas, for the same reason. */
1116
+ reasoning: string;
1117
+ toolCalls: ModelToolCall[];
1118
+ sources: MessageSource[];
1119
+ /** The provider's own word for why it stopped. Never normalized. */
1120
+ finishReason: string | null;
1121
+ /** The same fact in one vocabulary. Branch on this. */
1122
+ stopReason?: StopReason;
1123
+ error?: {
1124
+ code?: string | number;
1125
+ message: string;
1126
+ };
1127
+ usage?: ModelUsage;
1128
+ /** How many chunks carried a NON-EMPTY reasoning delta. Zero with a non-zero
1129
+ * `usage.reasoningTokens` means the provider hid the thinking text. */
1130
+ reasoningChunks: number;
1131
+ chunks: number;
1132
+ }
1133
+ export interface ConsumeOptions {
1134
+ /** Label for the reasoning disclosure. Defaults to 'Thinking'. */
1135
+ reasoningLabel?: string;
1136
+ /**
1137
+ * Correlates diagnostics and namespaces reasoning parts for this consume call;
1138
+ * assigned automatically when absent.
1139
+ *
1140
+ * Supply one only to tie a read to an id you already hold. Two reads into the
1141
+ * SAME sink must not share a value: the id is what keeps a second round's
1142
+ * block 0 from merging into the first round's reasoning part.
1143
+ */
1144
+ streamId?: string;
1145
+ /**
1146
+ * The app's own grouping of several reads into ONE logical turn, carried onto
1147
+ * every diagnostic event this read emits.
1148
+ *
1149
+ * THE KIT REPORTS WHAT THE APP DECLARES AND GROUPS NOTHING ON ITS OWN. A chat
1150
+ * app running a tool loop, or fanning out to sub-agents, makes several model
1151
+ * calls that belong to one turn; the kit sees one Response at a time and has
1152
+ * no way to know which ones those are. So it does not guess:
1153
+ *
1154
+ * readOpenAIStream(res, sink, { traceId: 'turn-42', label: 'planner' })
1155
+ *
1156
+ * Absent when not supplied -- the key is not present on the events at all,
1157
+ * rather than present and undefined. Purely diagnostic: nothing in the parse
1158
+ * branches on it and it never reaches a provider.
1159
+ */
1160
+ traceId?: string;
1161
+ /** The app's name for THIS read inside its trace (`'planner'`,
1162
+ * `'executor'`, `'retry-2'`). Carried onto every diagnostic event, and
1163
+ * absent when not supplied. Never derived from the format or the model.
1164
+ *
1165
+ * Not to be confused with `reasoningLabel`, which is UI copy for the
1166
+ * reasoning disclosure; this one is never rendered to an end user. */
1167
+ label?: string;
1168
+ /** Fires once per tool call the moment its arguments parse cleanly. This is
1169
+ * the hook a host's tool loop waits on. There is deliberately no
1170
+ * per-fragment callback: `ToolPart.rawInput` is written on every fragment,
1171
+ * so the streaming text is already on the part. */
1172
+ onToolCallReady?: (call: ModelToolCall) => void;
1173
+ }
1174
+ /** Per-stream state for one format. */
1175
+ export interface WireFormatReader {
1176
+ /**
1177
+ * Map one decoded frame onto zero or more neutral chunks. Returns an ARRAY
1178
+ * because the mapping is not one-to-one: an Anthropic `message_start` yields
1179
+ * usage, a `content_block_start` for `tool_use` yields an id-plus-name delta,
1180
+ * a `ping` yields nothing.
1181
+ *
1182
+ * MUST NOT throw on an unrecognized frame. Return `[]` instead: providers add
1183
+ * event types without warning.
1184
+ */
1185
+ push(frame: unknown): ModelStreamChunk[];
1186
+ }
1187
+ /** A pluggable wire format. Values, not a flag, so a third party can add one
1188
+ * without a PR to this repo. */
1189
+ export interface WireFormat {
1190
+ readonly id: string;
1191
+ /** Called once per stream so a format can hold per-stream state. Two calls
1192
+ * must share NOTHING. */
1193
+ open(): WireFormatReader;
1194
+ }
1195
+ ```
1196
+
1197
+ ### The `ChatRequestBody` preamble (what your route receives)
1198
+
1199
+ Every backend route the `kai` MCP scaffolds narrows `await request.json()` ONCE at the
1200
+ edge, through this type — `request.json()` is `Promise<unknown>` under a Node/undici
1201
+ tsconfig, so destructuring it raw fails a stock `npm run build` even though it ran fine
1202
+ in dev. The front end sends `toOpenAIMessages(thread)`; this is what that produces, so
1203
+ the two halves stay pinned to one type. Do not hand-roll a second narrowing.
1204
+
1205
+ ```ts
1206
+ /**
1207
+ * What the front end POSTs. `request.json()` is `unknown` (it is whatever the
1208
+ * client sent), so the body is narrowed once here instead of at every use —
1209
+ * without it this route does not compile under a server tsconfig. Widen it as
1210
+ * you add fields of your own.
1211
+ */
1212
+ type ChatRequestBody = {
1213
+ messages: OpenAIWireMessage[];
1214
+ model?: string;
1215
+ tools?: unknown[];
1216
+ };
1217
+ ```
1218
+
1219
+ The scaffolded routes pair it with a `readChatRequest(request)` guard that turns a bare
1220
+ GET or malformed JSON into a status response instead of an unhandled throw — re-scaffold
1221
+ any integration with the `kai` MCP `scaffold` tool to get the full preamble.
1222
+ <!-- kai:programmatic:end -->
1223
+
1224
+ ---
1225
+
1226
+ ## Element reference (89 elements, generated from custom-elements.json)
252
1227
 
253
1228
  Every element also accepts the `theme` attribute. Array/object properties are marked with a `—` attribute: they must be set as JS properties.
254
1229
 
@@ -339,7 +1314,7 @@ Every element also accepts the `theme` attribute. Array/object properties are ma
339
1314
  | Property | Attribute | Type | Description |
340
1315
  |---|---|---|---|
341
1316
  | `theme` | `theme` | `"light" \| "dark" \| "auto"` | Color mode (`auto` follows prefers-color-scheme). |
342
- | `items` | — | `undefined \| { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]` | The attachments to render. Omit (or pass an empty array) for the empty state, which shows `emptyText` if set and nothing otherwise. Set as a JS property (array). |
1317
+ | `items` | — | `undefined \| { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]` | The attachments to render. Omit (or pass an empty array) for the empty state, which shows `emptyText` if set and nothing otherwise. Set as a JS property (array). Each item's `url` must be a `data:` URI or an https URL, never `URL.createObjectURL`: a `blob:` URL previews here but the wire encoders (`toOpenAIMessages`/`toAnthropicMessages`) refuse it. |
343
1318
  | `variant` | `variant` | `undefined \| "grid" \| "inline" \| "list"` | Layout: `grid` = visual tiles, `inline` = icon + label chips, `list` = rows. |
344
1319
  | `hoverCard` | `hover-card` | `undefined \| false \| true` | Wrap each item in a hover card that previews its details. |
345
1320
  | `removable` | `removable` | `undefined \| false \| true` | Show a remove button per item; clicking it fires a `kai-remove` event. |
@@ -545,7 +1520,7 @@ _No events._
545
1520
  | `cards` | — | `undefined \| { type: string; id: string; data: unknown; title?: undefined \| string; resolution?: undefined \| { kind: "action"; action: string; payload?: unknown; at?: undefined \| string } \| { kind: "submit"; data: unknown; at?: undefined \| string } \| { kind: "dismissed"; at?: undefined \| string } \| { kind: "expired"; reason?: undefined \| string; at?: undefined \| string } }[]` | The stream of card envelopes to render. Set as a JS PROPERTY: `el.cards = [...]`. |
546
1521
  | `types` | — | `undefined \| Record<string, string>` | Optional type→tag overrides/additions (merged over the built-ins). Property: `el.types`. Typed as a plain string map (not the `CardTagMap` alias) so the generated React wrapper inlines it instead of emitting an unresolved named type. |
547
1522
  | `schemas` | — | `undefined \| Record<string, object>` | JSON Schemas for the card types this app renders, keyed by envelope type. The companion of `types`, which says what DRAWS a card while this says what a VALID one looks like. An OBJECT, so it is a JS property only: `el.schemas = { 'pricing-table': pricingSchema }`, never an attribute. `createCardRegistry(...).validationSchemas` is exactly this shape. Without it the kit validates its own seven built-ins and leaves your own card type, the one your app actually cares about, as the only unchecked thing on screen. A schema here WINS over a built-in of the same name, matching `mergeCardTags`, where your entry is spread over ours. Typed `Record<string, object>` rather than `Record<string, JsonSchema>` deliberately: an imported `.json` schema widens `"type"` to `string`, and an authored one carries `$schema`/`title`/`description`/`additionalProperties`, so the tighter type would reject both of the normal ways to supply one. See `CardSchemaMap` in components/card-renderer.tsx. |
548
- | `policy` | — | `undefined \| { onSubmit?: undefined \| (cardId: string, data: unknown) => void; onAction?: undefined \| (cardId: string, action: string, payload?: unknown) => void; onSendPrompt?: undefined \| (text: string, opts: { mode: "compose" \| "send"; context?: unknown; }) => void; onOpen?: undefined \| (url: string, target: "tab" \| "artifact") => void; onState?: undefined \| (cardId: string, patch: unknown) => void; onDismiss?: undefined \| (cardId: string) => void; onReopen?: undefined \| (cardId: string) => void; onError?: undefined \| (cardId: string, message: string) => void; maxSendPromptMode?: undefined \| "compose" \| "send" }` | Optional CardPolicy handling child events. Property: `el.policy`. |
1523
+ | `policy` | — | `undefined \| { onSubmit?: undefined \| ((cardId: string, data: unknown) => void); onAction?: undefined \| ((cardId: string, action: string, payload?: unknown) => void); onSendPrompt?: undefined \| ((text: string, opts: { mode: "compose" \| "send"; context?: unknown }) => void); onOpen?: undefined \| ((url: string, target: "tab" \| "artifact") => void); onState?: undefined \| ((cardId: string, patch: unknown) => void); onDismiss?: undefined \| ((cardId: string) => void); onReopen?: undefined \| ((cardId: string) => void); onError?: undefined \| ((cardId: string, message: string) => void); maxSendPromptMode?: undefined \| "compose" \| "send" }` | Optional CardPolicy handling child events. Property: `el.policy`. |
549
1524
  | `validateCards` | `validate-cards` | `undefined \| false \| true` | Validate each envelope's `data` against the schema for its type before rendering it, using a built-in's own schema or yours from `schemas`. Default `true`; set `validate-cards="false"` (or `el.validateCards = false`) to opt out. A hard failure (wrong type, a missing required field) renders a diagnostic naming the field instead of the card; a soft failure (bounds) renders the card unchanged. Both emit a contract `error` event. On in production too: a model emitting a bad shape is a production failure mode, so stripping the check there would hide it from exactly the person who needs to see it. |
550
1525
 
551
1526
  **Events** (non-bubbling `CustomEvent`s — listen directly on the element):
@@ -614,6 +1589,8 @@ _No events._
614
1589
  | `proseSize` | `prose-size` | `undefined \| "xs" \| "sm" \| "base" \| "lg"` | Body/prose font scale for rendered markdown (`'xs' \| 'sm' \| 'base' \| 'lg'`). Defaults to `'sm'`. |
615
1590
  | `codeTheme` | `code-theme` | `undefined \| string` | Shiki theme name for syntax-highlighted code blocks (e.g. `'github-dark-dimmed'`). |
616
1591
  | `codeHighlight` | `code-highlight` | `undefined \| false \| true` | Enable Shiki syntax highlighting in code blocks. Turn off to render plain `<pre>` blocks (lighter, no highlighter load). Default true. |
1592
+ | `reasoning` | `reasoning` | `undefined \| "full" \| "compact" \| "off"` | How `reasoning` parts render across the thread. `'full'` (default) is the current collapsible-disclosure behavior; `'compact'` shows only a shimmer loader while a reasoning part streams and nothing once it settles (no expandable detail); `'off'` renders reasoning parts not at all. Forwarded to every `MessageBody` as `reasoningMode`. |
1593
+ | `reasoningOpen` | `reasoning-open` | `undefined \| false \| true` | Seeds the reasoning disclosure open AND keeps it tracking the stream (open while streaming, closes when it settles): the pre-Task-19f `full` behavior. Default false/absent: the panel starts closed (just the "Thinking" shimmer chip) and only opens on click, the current default (owner ruling, 2026-08-26). Meaningless when `reasoning` is `'compact'` or `'off'`. Forwarded to every `MessageBody` as `reasoningDefaultOpen`. |
617
1594
  | `chatTitle` | `chat-title` | `undefined \| string` | Optional header title shown on the left of the header. |
618
1595
  | `models` | — | `undefined \| { id: string; name: string; provider?: undefined \| string; description?: undefined \| string; group?: undefined \| string }[]` | Optional model list. When set (>1 model) a ModelSwitcher is shown in the header and a `kai-model-change` event fires on selection. |
619
1596
  | `currentModel` | `current-model` | `undefined \| string` | The currently selected model id (pairs with `models`). |
@@ -627,6 +1604,7 @@ _No events._
627
1604
  | `composer` | `composer` | `undefined \| false \| true` | REPLACE: full custom composer in place of the built-in prompt input. The projected content wires its own submit (the data-flow boundary). |
628
1605
  | `composerActions` | `composer-actions` | `undefined \| false \| true` | INJECT: accessory row just above the composer (e.g. extra actions). |
629
1606
  | `footer` | `footer` | `undefined \| false \| true` | INJECT: footer row below the composer (disclaimers, token meter, …). |
1607
+ | `attach` | `attach` | `undefined \| false \| true` | When `false`, hides the built-in paperclip attach button. Defaults to `true` (undeclared keeps today's behavior: attach visible), matching `DefaultPromptInput`'s own default: only an explicit `false` hides it. |
630
1608
  | `webSearch` | `web-search` | `undefined \| false \| true` | Show a web-search (Globe) button in the input toolbar; calls `onWebSearch`. |
631
1609
  | `voice` | `voice` | `undefined \| false \| true` | Show a Voice (Mic) button in the input toolbar; fires a `voice` event. |
632
1610
  | `triggers` | — | `undefined \| { char: string; kind: string; items?: undefined \| { id: string; label: string; icon?: undefined \| string; description?: undefined \| string; group?: undefined \| string; kind?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> }[] }[]` | Rich entity triggers. Each `{ char, kind, items }` opens a caret-anchored menu that inserts an atomic pill (`/` skills, `@` agents/plugins). Set as a JS property; forwarded to the input. |
@@ -636,6 +1614,9 @@ _No events._
636
1614
  | `messages` | — | `undefined \| { id: string; role: "user" \| "assistant"; parts: ({ type: "text"; text: string; raw?: undefined \| { source: string; payload: unknown } } \| { type: "reasoning"; text: string; label?: undefined \| string; index?: undefined \| number; streamId?: undefined \| string; signature?: undefined \| string; raw?: undefined \| { source: string; payload: unknown } } \| { type: "tool"; tool: { type: string; kind?: undefined \| "command" \| "file-change" \| "search" \| "fetch" \| "mcp" \| "image" \| "generic"; state: "input-streaming" \| "input-available" \| "output-available" \| "output-error"; input?: undefined \| Record<string, unknown>; rawInput?: undefined \| string; output?: undefined \| Record<string, unknown>; toolCallId?: undefined \| string; errorText?: undefined \| string; raw?: undefined \| { source: string; payload: unknown } }; raw?: undefined \| { source: string; payload: unknown } } \| { type: "card"; envelope: { type: string; id: string; data: unknown; title?: undefined \| string; resolution?: undefined \| { kind: "action"; action: string; payload?: unknown; at?: undefined \| string } \| { kind: "submit"; data: unknown; at?: undefined \| string } \| { kind: "dismissed"; at?: undefined \| string } \| { kind: "expired"; reason?: undefined \| string; at?: undefined \| string } }; raw?: undefined \| { source: string; payload: unknown } } \| { type: "source"; source: { id?: undefined \| string; url?: undefined \| string; title?: undefined \| string; snippet?: undefined \| string; index?: undefined \| number }; raw?: undefined \| { source: string; payload: unknown } } \| { type: "file"; attachment: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }; raw?: undefined \| { source: string; payload: unknown } })[]; actions?: undefined \| ("copy" \| "like" \| "dislike" \| "regenerate" \| "edit" \| { id: string; label: string; icon?: undefined \| string; tooltip?: undefined \| string })[]; avatar?: undefined \| { src?: undefined \| string; fallback?: undefined \| string; alt?: undefined \| string }; feedback?: undefined \| "like" \| "dislike" }[]` | The full message thread to render, newest last. Each entry carries its role, ordered `parts`, and optional actions/avatar/feedback. Set as a JS property (`el.messages = [...]`); a NEW array reference per streaming chunk re-renders (mutating in place does not). Omit for an empty thread. Re-declared here (rather than inherited from `ChatThreadProps`) because the ELEMENT registers a `[]` default and renders the empty state without it, while the SolidJS `<ChatThread>` component still requires it. The facade hands it a validated array either way. Matches `<kai-thread>`. |
637
1615
  | `cardTypes` | — | `undefined \| Record<string, string>` | Optional card type -> custom-element tag overrides/additions for `card` parts (merged over the built-ins). Property: `el.cardTypes`. Typed as a plain string map (not the `CardTagMap` alias) so the generated React wrapper inlines it instead of emitting an unresolved named type. |
638
1616
  | `cardSchemas` | — | `undefined \| Record<string, object>` | JSON Schemas for the card types this app renders, keyed by envelope type. The companion of `cardTypes`, which says what DRAWS a card while this says what a VALID one looks like. An OBJECT, so it is a JS property only: `el.cardSchemas = { 'pricing-table': pricingSchema }`, never an attribute. `createCardRegistry(...).validationSchemas` is exactly this shape. Without it the kit validates its own seven built-ins and leaves your own card type, the one your app actually cares about, as the only unchecked thing on screen. A schema here WINS over a built-in of the same name. Typed `Record<string, object>` rather than `Record<string, JsonSchema>` deliberately: an imported `.json` schema widens `"type"` to `string`, and an authored one carries `$schema`/`title`/`description`/`additionalProperties`, so the tighter type would reject both of the normal ways to supply one. |
1617
+ | `conversations` | `conversations` | `undefined \| false \| true` | Turns on the prior-conversations list (a list-toggle button in the header, plus a second list view sharing the panel, C-1). Attribute- settable like every other boolean flag on this element: `<kai-chat conversations>`. Requires `store`. A row select, "new conversation," or the visitor's mount-time auto-restore all deliver their messages the same way: listen for `kai-conversation-load` and set `el.messages` from `event.detail.messages` (a fresh array): this element does not update `messages` for you. Set with no `store`, the underlying `ChatThread` decides loudly (one console.error) and stays visually off; this facade always supplies its own internal load handler (the `kai-conversation-load` dispatch below), so the second ChatThread guard, missing `onConversationLoad`, never trips here, even for a consumer who never listens for the event. Default false. |
1618
+ | `store` | — | `undefined \| { list: () => Promise<{ id: string; title: string; groupId?: undefined \| string; scope?: undefined \| { type: "document" \| "collection"; documentId?: undefined \| string; filters?: undefined \| { tags?: undefined \| string[]; authors?: undefined \| string[]; contentType?: undefined \| "transcript" \| "markdown"; dateRange?: undefined \| { from: string; to: string } } }; messageCount: number; lastMessageAt?: undefined \| string; updatedAt: string; trailing?: undefined \| string; lastReadAt?: undefined \| string }[]>; load: (id: string) => Promise<{ id: string; role: "user" \| "assistant"; parts: ({ type: "text"; text: string; raw?: undefined \| { source: string; payload: unknown } } \| { type: "reasoning"; text: string; label?: undefined \| string; index?: undefined \| number; streamId?: undefined \| string; signature?: undefined \| string; raw?: undefined \| { source: string; payload: unknown } } \| { type: "tool"; tool: { type: string; kind?: undefined \| "command" \| "file-change" \| "search" \| "fetch" \| "mcp" \| "image" \| "generic"; state: "input-streaming" \| "input-available" \| "output-available" \| "output-error"; input?: undefined \| Record<string, unknown>; rawInput?: undefined \| string; output?: undefined \| Record<string, unknown>; toolCallId?: undefined \| string; errorText?: undefined \| string; raw?: undefined \| { source: string; payload: unknown } }; raw?: undefined \| { source: string; payload: unknown } } \| { type: "card"; envelope: { type: string; id: string; data: unknown; title?: undefined \| string; resolution?: undefined \| { kind: "action"; action: string; payload?: unknown; at?: undefined \| string } \| { kind: "submit"; data: unknown; at?: undefined \| string } \| { kind: "dismissed"; at?: undefined \| string } \| { kind: "expired"; reason?: undefined \| string; at?: undefined \| string } }; raw?: undefined \| { source: string; payload: unknown } } \| { type: "source"; source: { id?: undefined \| string; url?: undefined \| string; title?: undefined \| string; snippet?: undefined \| string; index?: undefined \| number }; raw?: undefined \| { source: string; payload: unknown } } \| { type: "file"; attachment: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }; raw?: undefined \| { source: string; payload: unknown } })[]; actions?: undefined \| ("copy" \| "like" \| "dislike" \| "regenerate" \| "edit" \| { id: string; label: string; icon?: undefined \| string; tooltip?: undefined \| string })[]; avatar?: undefined \| { src?: undefined \| string; fallback?: undefined \| string; alt?: undefined \| string }; feedback?: undefined \| "like" \| "dislike" }[]>; save: (id: string, messages: { id: string; role: "user" \| "assistant"; parts: ({ type: "text"; text: string; raw?: undefined \| { source: string; payload: unknown } } \| { type: "reasoning"; text: string; label?: undefined \| string; index?: undefined \| number; streamId?: undefined \| string; signature?: undefined \| string; raw?: undefined \| { source: string; payload: unknown } } \| { type: "tool"; tool: { type: string; kind?: undefined \| "command" \| "file-change" \| "search" \| "fetch" \| "mcp" \| "image" \| "generic"; state: "input-streaming" \| "input-available" \| "output-available" \| "output-error"; input?: undefined \| Record<string, unknown>; rawInput?: undefined \| string; output?: undefined \| Record<string, unknown>; toolCallId?: undefined \| string; errorText?: undefined \| string; raw?: undefined \| { source: string; payload: unknown } }; raw?: undefined \| { source: string; payload: unknown } } \| { type: "card"; envelope: { type: string; id: string; data: unknown; title?: undefined \| string; resolution?: undefined \| { kind: "action"; action: string; payload?: unknown; at?: undefined \| string } \| { kind: "submit"; data: unknown; at?: undefined \| string } \| { kind: "dismissed"; at?: undefined \| string } \| { kind: "expired"; reason?: undefined \| string; at?: undefined \| string } }; raw?: undefined \| { source: string; payload: unknown } } \| { type: "source"; source: { id?: undefined \| string; url?: undefined \| string; title?: undefined \| string; snippet?: undefined \| string; index?: undefined \| number }; raw?: undefined \| { source: string; payload: unknown } } \| { type: "file"; attachment: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }; raw?: undefined \| { source: string; payload: unknown } })[]; actions?: undefined \| ("copy" \| "like" \| "dislike" \| "regenerate" \| "edit" \| { id: string; label: string; icon?: undefined \| string; tooltip?: undefined \| string })[]; avatar?: undefined \| { src?: undefined \| string; fallback?: undefined \| string; alt?: undefined \| string }; feedback?: undefined \| "like" \| "dislike" }[]) => Promise<void>; markRead?: undefined \| ((id: string) => Promise<void>) }` | The adapter this thread persists conversations through: an object of three functions (`list`/`load`/`save`; `ConversationStore`, exported from `@kitn.ai/ui`'s `primitives/conversation-store`). A JS PROPERTY ONLY: `el.store = myAdapter`. It can never be an attribute, since a function-bearing object has no HTML string form, the same reasoning that keeps `messages`/`cardSchemas` property-only (the kai- contract: array/object props are JS properties, never attributes). Two built-ins ship: `localStorageStore(name, userId?)` and `fetchStore(url, userId?)`. |
1619
+ | `home` | — | `undefined \| { greeting?: undefined \| { title?: undefined \| string; subtitle?: undefined \| string }; recentConversation?: undefined \| false \| true; newConversation?: undefined \| { label?: undefined \| string }; links?: undefined \| { label: string; href?: undefined \| string; description?: undefined \| string; icon?: undefined \| string }[] }` | Turns on the widget home screen (Intercom-pattern): the panel boots into a `home` view, with a greeting, most-recent-conversation card, a "new conversation" CTA, and host-defined links, plus a Home/Messages tab bar for switching back to the thread. An OBJECT, so it is a JS property only: `el.home = { greeting: { title: 'Hey' }, links: [...] }`, never an attribute (the kai- contract: array/object props are JS properties). A `links` entry with no `href` fires `kai-home-link` with that entry when tapped, rather than navigating; one WITH `href` opens it directly (only when the URL passes the kit's own scheme allowlist). Omit for the no-home widget (chat view only, unchanged). |
639
1620
 
640
1621
  **Events** (non-bubbling `CustomEvent`s — listen directly on the element):
641
1622
 
@@ -643,6 +1624,8 @@ _No events._
643
1624
  |---|---|---|
644
1625
  | `kai-attachments-change` | `CustomEvent<{ attachments: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[] }>` | The staged attachments changed (file added or removed). Carries the full current list so a consumer can react in real time. |
645
1626
  | `kai-attachments-rejected` | `CustomEvent<{ rejected: { filename: string; mediaType: string; reason: "filtered" \| "unsupported" }[] }>` | One or more picked files were refused because `accept` excluded them. The element renders NO message of its own: it reports the facts (name, media type, whether the kit could have sent it) and what the user should see is the application's call. Only ever fires when `accept` is set. |
1627
+ | `kai-conversation-load` | `CustomEvent<{ id: string \| undefined; messages: { id: string; role: "user" \| "assistant"; parts: ({ type: "text"; text: string; raw?: undefined \| { source: string; payload: unknown } } \| { type: "reasoning"; text: string; label?: undefined \| string; index?: undefined \| number; streamId?: undefined \| string; signature?: undefined \| string; raw?: undefined \| { source: string; payload: unknown } } \| { type: "tool"; tool: { type: string; kind?: undefined \| "command" \| "file-change" \| "search" \| "fetch" \| "mcp" \| "image" \| "generic"; state: "input-streaming" \| "input-available" \| "output-available" \| "output-error"; input?: undefined \| Record<string, unknown>; rawInput?: undefined \| string; output?: undefined \| Record<string, unknown>; toolCallId?: undefined \| string; errorText?: undefined \| string; raw?: undefined \| { source: string; payload: unknown } }; raw?: undefined \| { source: string; payload: unknown } } \| { type: "card"; envelope: { type: string; id: string; data: unknown; title?: undefined \| string; resolution?: undefined \| { kind: "action"; action: string; payload?: unknown; at?: undefined \| string } \| { kind: "submit"; data: unknown; at?: undefined \| string } \| { kind: "dismissed"; at?: undefined \| string } \| { kind: "expired"; reason?: undefined \| string; at?: undefined \| string } }; raw?: undefined \| { source: string; payload: unknown } } \| { type: "source"; source: { id?: undefined \| string; url?: undefined \| string; title?: undefined \| string; snippet?: undefined \| string; index?: undefined \| number }; raw?: undefined \| { source: string; payload: unknown } } \| { type: "file"; attachment: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }; raw?: undefined \| { source: string; payload: unknown } })[]; actions?: undefined \| ("copy" \| "like" \| "dislike" \| "regenerate" \| "edit" \| { id: string; label: string; icon?: undefined \| string; tooltip?: undefined \| string })[]; avatar?: undefined \| { src?: undefined \| string; fallback?: undefined \| string; alt?: undefined \| string }; feedback?: undefined \| "like" \| "dislike" }[] }>` | A conversation's history loaded: a row tap in the list, "new conversation," or the visitor's own mount-time auto-restore of their most recent thread (only fires when `conversations` is on and a `store` is set). `detail.id` is that conversation's id, `undefined` for the "new conversation" case (no id exists until the first message mints one, C-6). Set `el.messages = event.detail.messages` (already a fresh array) to actually render it, since this element does not do that for you; `messages` stays your own state like everywhere else on this element. |
1628
+ | `kai-home-link` | `CustomEvent<{ entry: { label: string; href?: undefined \| string; description?: undefined \| string; icon?: undefined \| string } }>` | A `home.links` entry with no `href` was activated (tapped/clicked/Enter). Meaningful only when `home` is set. |
646
1629
  | `kai-message-action` | `CustomEvent<{ messageId: string; action: string; state?: undefined \| "on" \| "off" }>` | An action button on a message was clicked. `action` is the built-in name or custom id. `state` is present only for the toggleable feedback votes: `'on'` when a like/dislike is set, `'off'` when re-tapped to clear. |
647
1630
  | `kai-model-change` | `CustomEvent<{ modelId: string }>` | The header model switcher changed. |
648
1631
  | `kai-submit` | `CustomEvent<{ value: string; attachments: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[] }>` | User submitted a message. |
@@ -685,6 +1668,64 @@ _No events._
685
1668
 
686
1669
  ---
687
1670
 
1671
+ ### `kai-checkbox` / `Checkbox`
1672
+
1673
+ **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):
1674
+
1675
+ | Property | Attribute | Type | Description |
1676
+ |---|---|---|---|
1677
+ | `theme` | `theme` | `"light" \| "dark" \| "auto"` | Color mode (`auto` follows prefers-color-scheme). |
1678
+ | `checked` | `checked` | `undefined \| false \| true` | Controlled checked state. Settable and reflected to the `checked` attribute. `el.checked = true` (or `<kai-checkbox checked>`) drives it; ticking the box updates it and fires `kai-change`. Read `el.checked` for live state. |
1679
+ | `defaultChecked` | `default-checked` | `undefined \| false \| true` | Initial checked state on mount (uncontrolled seed). Bare attribute (`<kai-checkbox default-checked>`) turns it on. |
1680
+ | `indeterminate` | `indeterminate` | `undefined \| false \| true` | The mixed state, for a parent box whose children are partly ticked. Visual plus an accessibility hint: the box still reports `checked === false`. |
1681
+ | `disabled` | `disabled` | `undefined \| false \| true` | Disable interaction. |
1682
+ | `required` | `required` | `undefined \| false \| true` | Set the native `required` attribute, and nothing more. Whether an unticked box is an error is the application's rule, not the kit's. |
1683
+ | `label` | `label` | `undefined \| string` | Accessible label. The visible text beside the box is the consumer's to render. |
1684
+ | `name` | `name` | `undefined \| string` | Form-control name (paired with `value`). |
1685
+ | `value` | `value` | `undefined \| string` | Submitted value when checked (paired with `name`). Defaults to `'on'`. |
1686
+
1687
+ **Events** (non-bubbling `CustomEvent`s — listen directly on the element):
1688
+
1689
+ | Event | `detail` type | Description |
1690
+ |---|---|---|
1691
+ | `kai-change` | `CustomEvent<{ checked: false \| true }>` | The box was ticked or unticked. |
1692
+
1693
+ **Methods** (call on the element instance: `document.querySelector('kai-checkbox').toggle()`):
1694
+
1695
+ | Method | Signature | Description |
1696
+ |---|---|---|
1697
+ | `toggle` | `(): void` | Flip the box and fire `kai-change` (no-op while disabled). |
1698
+ | `focus` | `(options?: FocusOptions): void` | Focus the inner input (the host element can't reach it). |
1699
+
1700
+ ---
1701
+
1702
+ ### `kai-checkbox-group` / `CheckboxGroup`
1703
+
1704
+ **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):
1705
+
1706
+ | Property | Attribute | Type | Description |
1707
+ |---|---|---|---|
1708
+ | `theme` | `theme` | `"light" \| "dark" \| "auto"` | Color mode (`auto` follows prefers-color-scheme). |
1709
+ | `options` | — | `{ value: string; label: string; description?: undefined \| string; disabled?: undefined \| false \| true }[]` | The choices, top to bottom. Set as a JS PROPERTY (array), never an attribute. Rendered in full: the kit never truncates, re-orders or de-duplicates them. |
1710
+ | `value` | `value` | `undefined \| string` | The FIRST selected value. Settable and reflected to the `value` attribute, so `:host([value])` and `el.value` see live state, and a seed can be written in markup. Writing it makes that the whole selection; to read or drive the rest, use `el.values`. |
1711
+ | `name` | `name` | `undefined \| string` | The shared form-control name every box carries, so `FormData.getAll(name)` reads the whole selection back under one key. NO DEFAULT, unlike `<kai-radio-group>`. A radio set needs a shared `name` for the browser to make it exclusive and arrow-navigable, so one is generated when none is given; checkboxes are independent controls and behave correctly with no name at all. Generating one here would submit the selection under a random key, which is worse than submitting nothing. The element is NOT form-associated (no `ElementInternals`, no `setFormValue()`), the same known gap `<kai-input>` records: the boxes live in a shadow root, so a surrounding `<form>` collects nothing from them whether or not `name` is set. Read `el.values`. The name still lands on every inner input, so it is right the day form association arrives. |
1712
+ | `disabled` | `disabled` | `undefined \| false \| true` | Disable every row. Individual rows carry their own `disabled`. |
1713
+ | `label` | `label` | `undefined \| string` | Accessible name for the group. |
1714
+
1715
+ **Events** (non-bubbling `CustomEvent`s — listen directly on the element):
1716
+
1717
+ | Event | `detail` type | Description |
1718
+ |---|---|---|
1719
+ | `kai-change` | `CustomEvent<{ value: string; values: string[] }>` | A row was ticked or unticked. `values` is the whole selection after the change, which is what a multi-select control needs; `value` is the first of them (empty when nothing is selected). Both are always present, so neither shape silently loses the other. This is `<kai-select>`'s detail, deliberately. |
1720
+
1721
+ **Methods** (call on the element instance: `document.querySelector('kai-checkbox-group').focus(…)`):
1722
+
1723
+ | Method | Signature | Description |
1724
+ |---|---|---|
1725
+ | `focus` | `(options?: FocusOptions): void` | Focus the group's first box. Not "the first ticked one", which is `<kai-radio-group>`'s rule: a radio group is ONE tab stop that lands on the selection, while every checkbox here is its own tab stop, so the entry point is simply the top of the list. |
1726
+
1727
+ ---
1728
+
688
1729
  ### `kai-checkpoint` / `Checkpoint`
689
1730
 
690
1731
  **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):
@@ -903,7 +1944,7 @@ _No events._
903
1944
  | `kai-entity-add` | `CustomEvent<{ entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> } }>` | An entity pill was inserted into the composer. |
904
1945
  | `kai-entity-remove` | `CustomEvent<{ entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> } }>` | An entity pill was deleted from the composer. |
905
1946
  | `kai-focus` | `CustomEvent<{ originalEvent: FocusEvent }>` | The composer gained focus. `focus`/`blur` are NOT composed natively, so they don't escape the shadow root; these re-expose them on the host. (For `keydown`/`paste`/`focusin`/`focusout`, listen NATIVELY on `<kai-composer>`: they're composed and already cross the shadow boundary.) |
906
- | `kai-submit` | `CustomEvent<{ doc: ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> } })[]; text: string; entities: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> }[] }>` | The user submitted the composer (Enter or programmatic submit). |
1947
+ | `kai-submit` | `CustomEvent<{ doc: ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> } })[]; text: string; entities: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> }[] }>` | The user submitted the composer (Enter or programmatic submit). Note the detail carries no `attachments`; `<kai-composer>` is the bare editing surface: no send button, toolbar, or attachments. For a drop-in composer row with all three, reach for `<kai-prompt-input>`, which is built on this. |
907
1948
  | `kai-trigger` | `CustomEvent<{ char: string; query: string; rect: DOMRect }>` | A trigger character was detected at the caret (e.g. `/` or `@`). |
908
1949
  | `kai-trigger-close` | `CustomEvent<Record<string, never>>` | The active trigger was dismissed (Escape, space, or outside click). |
909
1950
  | `kai-value-change` | `CustomEvent<{ doc: ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> } })[]; text: string; entities: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> }[] }>` | The content changed (fires on every input event). |
@@ -979,11 +2020,15 @@ _No events._
979
2020
  | Property | Attribute | Type | Description |
980
2021
  |---|---|---|---|
981
2022
  | `theme` | `theme` | `"light" \| "dark" \| "auto"` | Color mode (`auto` follows prefers-color-scheme). |
982
- | `conversationId` | `conversation-id` | `undefined \| string` | The row's identity, handed to the container's selection contract. In the element this is the `conversation-id` attribute (host `id` is the fallback). |
983
- | `active` | `active` | `undefined \| false \| true` | Selected state. Reflected as `aria-current` on the row body and a `data-active` styling hook on the row; the container drives it from its `activeId`. |
2023
+ | `conversationId` | `conversation-id` | `undefined \| string` | The row's identity: the `conversation-id` attribute (host `id` is the fallback). Inside `<kai-conversations>` it is handed to the container's selection contract (`kai-conversation-select`); standalone it is the `id` in this element's own `kai-select` detail. |
2024
+ | `active` | `active` | `undefined \| false \| true` | Selected state. Reflected as `aria-current` on the row body and a `data-active` styling hook on the row; inside a container the container drives it from its `activeId`, standalone you set it yourself. |
984
2025
  | `compact` | `compact` | `undefined \| false \| true` | Dense single-line row padding. |
985
2026
 
986
- _No events._
2027
+ **Events** (non-bubbling `CustomEvent`s — listen directly on the element):
2028
+
2029
+ | Event | `detail` type | Description |
2030
+ |---|---|---|
2031
+ | `kai-select` | `CustomEvent<{ id: string }>` | STANDALONE activation only: the row was activated (click, Enter or Space on its body) while the item is NOT a direct child of `<kai-conversations>`. `id` is the row's identity: the `conversation-id` attribute, else the host `id`. Inside a container this never fires: activation surfaces once, as `kai-conversation-select` on the container. |
987
2032
 
988
2033
  **Slots** (project your own markup via `slot="name"` on a light-DOM child):
989
2034
 
@@ -1015,7 +2060,7 @@ _No events._
1015
2060
  |---|---|---|---|
1016
2061
  | `theme` | `theme` | `"light" \| "dark" \| "auto"` | Color mode (`auto` follows prefers-color-scheme). |
1017
2062
  | `groups` | — | `undefined \| { id: string; userId?: undefined \| string; teamId?: undefined \| string; name: string; sortOrder: number; createdAt: string }[]` | The list's section headers (`{ id, name, sortOrder, createdAt }`), rendered in array order. A group carries no conversations of its own; it is matched against `conversations` by id, so the two props are complementary rather than alternatives. Omit for an ungrouped list. Set as a JS property. |
1018
- | `conversations` | — | `undefined \| { id: string; title: string; groupId?: undefined \| string; scope?: undefined \| { type: "document" \| "collection"; documentId?: undefined \| string; filters?: undefined \| { tags?: undefined \| string[]; authors?: undefined \| string[]; contentType?: undefined \| "transcript" \| "markdown"; dateRange?: undefined \| { from: string; to: string } } }; messageCount: number; lastMessageAt?: undefined \| string; updatedAt: string; trailing?: undefined \| string }[]` | Every conversation the list renders, flat. Each one is filed under the group whose `id` equals its `groupId`; one with no `groupId`, or with a `groupId` matching no entry in `groups`, falls into a trailing "Ungrouped" section, so nothing you pass in is ever dropped. There is no recency bucketing. Set as a JS property. Omit to supply them as `<kai-conversation>` light-DOM children instead, or for the empty state. A search query that matches nothing shows a visible "No conversations match your search" state, distinct from the zero-conversations empty state. Slotted `<kai-conversation-item>` children switch the list into item mode instead: your own rows win and this array is not rendered. |
2063
+ | `conversations` | — | `undefined \| { id: string; title: string; groupId?: undefined \| string; scope?: undefined \| { type: "document" \| "collection"; documentId?: undefined \| string; filters?: undefined \| { tags?: undefined \| string[]; authors?: undefined \| string[]; contentType?: undefined \| "transcript" \| "markdown"; dateRange?: undefined \| { from: string; to: string } } }; messageCount: number; lastMessageAt?: undefined \| string; updatedAt: string; trailing?: undefined \| string; lastReadAt?: undefined \| string }[]` | Every conversation the list renders, flat. Each one is filed under the group whose `id` equals its `groupId`; one with no `groupId`, or with a `groupId` matching no entry in `groups`, falls into a trailing "Ungrouped" section, so nothing you pass in is ever dropped. There is no recency bucketing. Set as a JS property. Omit to supply them as `<kai-conversation>` light-DOM children instead, or for the empty state. A search query that matches nothing shows a visible "No conversations match your search" state, distinct from the zero-conversations empty state. Slotted `<kai-conversation-item>` children switch the list into item mode instead: your own rows win and this array is not rendered. |
1019
2064
  | `activeId` | `active-id` | `undefined \| string` | The id of the currently-open conversation, highlighted in the list. |
1020
2065
  | `collapsed` | `collapsed` | `undefined \| false \| true` | Controlled collapsed state. Set as a JS property (`el.collapsed = true`) to drive the rail from your app, updating it in response to `kai-collapse-toggle`. Omit for uncontrolled (the element manages it). Collapsed shrinks the rail to a floating reopen button. |
1021
2066
  | `defaultCollapsed` | `default-collapsed` | `undefined \| false \| true` | Initial collapsed state when uncontrolled (default false). Use the `default-collapsed` attribute to start collapsed in plain HTML. |
@@ -1055,7 +2100,7 @@ _No events._
1055
2100
 
1056
2101
  | Child element | Attributes | Text content | Notes |
1057
2102
  |---|---|---|---|
1058
- | `<kai-conversation>` | `group-id`, `id` | yes | Parse a single light-DOM `<kai-conversation>` element into a `ConversationSummary`. Attribute mapping: - `id` → ConversationSummary.id - `group-id` → ConversationSummary.groupId (optional) - textContent → ConversationSummary.title Fields not expressible as HTML attributes are NOT fabricated (F-10): the optional `scope` and `lastMessageAt` stay absent, and the required `messageCount`/`updatedAt` get honest defaults — zero messages, and an empty `updatedAt` from which no trailing relative time is derived (the epoch it used to fabricate rendered a bogus "many days ago" on every declarative row). |
2103
+ | `<kai-conversation>` | `group-id`, `id` | yes | Parse a single light-DOM `<kai-conversation>` element into a `ConversationSummary`. Attribute mapping: - `id` → ConversationSummary.id - `group-id` → ConversationSummary.groupId (optional) - textContent → ConversationSummary.title Fields not expressible as HTML attributes are NOT fabricated: the optional `scope` and `lastMessageAt` stay absent, and the required `messageCount`/`updatedAt` get honest defaults — zero messages, and an empty `updatedAt` from which no trailing relative time is derived (the epoch it used to fabricate rendered a bogus "many days ago" on every declarative row). |
1059
2104
 
1060
2105
  **Styleable parts** (restyle from outside via `kai-conversations::part(name)`):
1061
2106
 
@@ -1127,6 +2172,7 @@ _No events._
1127
2172
  | `closeLabel` | `close-label` | `undefined \| string` | i18n override for the launcher's name while open (default `Close ${label}`). |
1128
2173
  | `unread` | `unread` | `undefined \| false \| true` | Show the unread dot. YOURS: it renders only while closed, and the dock never writes it back. Clear it in your `kai-open-change` handler. |
1129
2174
  | `disabled` | `disabled` | `undefined \| false \| true` | Disable the launcher; `show()` and `toggle()` are gated on it. |
2175
+ | `hideClose` | `hide-close` | `undefined \| false \| true` | Suppress the dock's own built-in mobile close X. Set this when your slotted panel content supplies its own close affordance (e.g. a `<kai-chat slot="header-end">` close button), otherwise the two stack. TRADEOFF: the mobile panel reserves a padding band above its content so the built-in X never paints over slotted content; that band stays reserved unless you set this true, so only set it once your own control is actually in place. Attribute: `hide-close`. |
1130
2176
  | `focusOnOpen` | `focus-on-open` | `undefined \| "content" \| "panel" \| "none"` | Where focus lands on open: `content` (default, the first element you slotted), `panel`, or `none`. Attribute: `focus-on-open`. |
1131
2177
 
1132
2178
  **Events** (non-bubbling `CustomEvent`s — listen directly on the element):
@@ -1360,7 +2406,7 @@ _No events._
1360
2406
  | Property | Attribute | Type | Description |
1361
2407
  |---|---|---|---|
1362
2408
  | `theme` | `theme` | `"light" \| "dark" \| "auto"` | Color mode (`auto` follows prefers-color-scheme). |
1363
- | `data` | — | `undefined \| { type: "object"; title?: undefined \| string; description?: undefined \| string; required?: undefined \| string[]; properties: Record<string, { type: "string" \| "number" \| "integer" \| "boolean" \| "array" \| "object"; title?: undefined \| string; description?: undefined \| string; default?: unknown; enum?: undefined \| unknown[]; format?: undefined \| "email" \| "uri" \| "url" \| "date" \| "date-time" \| "time"; minimum?: undefined \| number; maximum?: undefined \| number; minLength?: undefined \| number; maxLength?: undefined \| number; pattern?: undefined \| string; minItems?: undefined \| number; maxItems?: undefined \| number; items?: undefined \| Record<string, unknown> \| { enum: unknown[] }; properties?: undefined \| Record<string, Record<string, unknown>>; required?: undefined \| string[]; readOnly?: undefined \| false \| true; "x-kai-widget"?: undefined \| "textarea" \| "slider" \| "rating" \| "radio" \| "select" \| "checkbox" \| "password" \| "switch"; "x-kai-placeholder"?: undefined \| string; "x-kai-step"?: undefined \| number }>; "x-kai-order"?: undefined \| string[]; "x-kai-inlineMax"?: undefined \| number; "x-kai-submitLabel"?: undefined \| string; "x-kai-dismissible"?: undefined \| false \| true; "x-kai-actions"?: undefined \| { id: string; label: string; variant?: undefined \| "default" \| "ghost" \| "outline" }[] }` | The form definition: a JSON Schema (`type:'object'`) + `x-kai-*` UI hints (the CardEnvelope.data). Set as a JS PROPERTY: `el.data = { type:'object', properties:{…} }`. Import the `FormDefinition` type from `@kitn.ai/ui` for the full shape. It IS self-referential (`FormField.properties` is another `FormField` map), and the generated `element-types.d.ts` inlines every named type, so the shipped declaration bottoms out in a `Record<string, unknown>` placeholder one level down rather than carrying the recursion. That is why `FormDefinition` is a `type` alias: an interface gets no implicit index signature, so it would not be assignable to that placeholder. |
2409
+ | `data` | — | `undefined \| { type: "object"; title?: undefined \| string; description?: undefined \| string; required?: undefined \| string[]; properties: Record<string, { type: "string" \| "number" \| "integer" \| "boolean" \| "array" \| "object"; title?: undefined \| string; description?: undefined \| string; default?: unknown; enum?: undefined \| unknown[]; format?: undefined \| "email" \| "uri" \| "url" \| "date" \| "date-time" \| "time"; minimum?: undefined \| number; maximum?: undefined \| number; minLength?: undefined \| number; maxLength?: undefined \| number; pattern?: undefined \| string; minItems?: undefined \| number; maxItems?: undefined \| number; items?: undefined \| Record<string, unknown> \| { enum: unknown[] }; properties?: undefined \| Record<string, Record<string, unknown>>; required?: undefined \| string[]; readOnly?: undefined \| false \| true; "x-kai-widget"?: undefined \| "textarea" \| "slider" \| "rating" \| "radio" \| "select" \| "checkbox" \| "password" \| "switch"; "x-kai-placeholder"?: undefined \| string; "x-kai-step"?: undefined \| number; "x-kai-format"?: undefined \| "tel" \| "ssn" \| "credit-card" \| "custom"; "x-kai-mask"?: undefined \| string; "x-kai-mask-guide"?: undefined \| string }>; "x-kai-order"?: undefined \| string[]; "x-kai-inlineMax"?: undefined \| number; "x-kai-submitLabel"?: undefined \| string; "x-kai-dismissible"?: undefined \| false \| true; "x-kai-actions"?: undefined \| { id: string; label: string; variant?: undefined \| "default" \| "ghost" \| "outline" }[] }` | The form definition: a JSON Schema (`type:'object'`) + `x-kai-*` UI hints (the CardEnvelope.data). Set as a JS PROPERTY: `el.data = { type:'object', properties:{…} }`. Import the `FormDefinition` type from `@kitn.ai/ui` for the full shape. It IS self-referential (`FormField.properties` is another `FormField` map), and the generated `element-types.d.ts` inlines every named type, so the shipped declaration bottoms out in a `Record<string, unknown>` placeholder one level down rather than carrying the recursion. That is why `FormDefinition` is a `type` alias: an interface gets no implicit index signature, so it would not be assignable to that placeholder. |
1364
2410
  | `cardId` | `card-id` | `undefined \| string` | Stable card id correlating every emitted CardEvent. Attribute: `card-id`. |
1365
2411
  | `heading` | `heading` | `undefined \| string` | Heading rendered in the card chrome (= CardEnvelope.title). Attribute: `heading`. |
1366
2412
  | `resolution` | — | `undefined \| Record<string, unknown>` | Set when the user resolved this card; renders the read-only view. Property: `el.resolution = { kind:'submit', data:{…} }`. |
@@ -1468,7 +2514,7 @@ _No events._
1468
2514
  |---|---|---|---|
1469
2515
  | `theme` | `theme` | `"light" \| "dark" \| "auto"` | Color mode (`auto` follows prefers-color-scheme). |
1470
2516
  | `type` | `type` | `undefined \| string` | Native input type: `text` (default) · `email` · `url` · `search` · `tel` · `password` · `number`. Single-line only. |
1471
- | `value` | `value` | `undefined \| string` | Controlled value. Settable and reflected to the `value` attribute. `el.value = 'hi'` drives it (no event); typing updates it and fires `kai-input`. Read `el.value` for live state. |
2517
+ | `value` | `value` | `undefined \| string` | Controlled value, and always the CANONICAL one when a mask is active: digits for `tel` / `ssn` / `credit-card`, the formatted text for `custom`. Settable and reflected to the `value` attribute. `el.value = '5551234567'` drives it (no event) and is re-fitted to the mask on the way in, so the field shows `555-123-4567`. Read `el.value` for live state; the formatted text rides along on every `kai-input` / `kai-change` detail as `formattedValue`. |
1472
2518
  | `placeholder` | `placeholder` | `undefined \| string` | Placeholder shown when empty. |
1473
2519
  | `label` | `label` | `undefined \| string` | Field label, linked to the input. |
1474
2520
  | `hint` | `hint` | `undefined \| string` | Helper text below the control. |
@@ -1481,13 +2527,19 @@ _No events._
1481
2527
  | `name` | `name` | `undefined \| string` | Form-control name. |
1482
2528
  | `autocomplete` | `autocomplete` | `undefined \| string` | Autofill hint forwarded to the inner input (e.g. `email`, `current-password`). |
1483
2529
  | `inputmode` | `inputmode` | `undefined \| string` | Virtual-keyboard hint forwarded to the inner input (e.g. `numeric`, `email`). |
2530
+ | `format` | `format` | `undefined \| string` | Mask pattern: `#` a digit, `@` a letter or digit, `*` an obscurable letter or digit, and every other character a positional literal (`@@@-####` → `CHG-4821`). The literal `default` is the opt-in sentinel: it resolves to the default format of `semantic` (`tel` → `###-###-####`). A bare `semantic` never starts masking on its own, so an opt-in token is what turns tier 2 on. |
2531
+ | `guide` | `guide` | `undefined \| string` | Placeholder guide shown at unfilled positions, aligned position for position with `format`: `mm/dd/yyyy` against `##/##/####`. Spaces are a valid guide character, so a guide of blanks and separators is how a phone field shows its shape without showing letters. Without a guide the field shows only up to the last typed character. A guide is a visual aid, never an accessible name: keep the `hint` text as well. |
2532
+ | `semantic` | `semantic` | `undefined \| "credit-card" \| "custom" \| "ssn" \| "tel"` | Semantic field type: `tel` · `ssn` · `credit-card` · `custom`. On its own it sets `inputmode` / `autocomplete` / `spellcheck` / `autocorrect` / `autocapitalize` and decides the canonical value; it never starts masking by itself. |
2533
+ | `caseMode` | `case-mode` | `undefined \| "preserve" \| "upper" \| "lower"` | Case folding applied to typed and pasted text: `preserve` (default) · `upper` · `lower`. Attribute: `case-mode`. |
2534
+ | `copyPolicy` | `copy-policy` | `undefined \| "formatted" \| "canonical" \| "obscured" \| "blocked"` | What a copy or cut of a masked field puts on the clipboard: `canonical` (default) · `formatted` · `obscured` · `blocked`. Attribute: `copy-policy`. |
1484
2535
 
1485
2536
  **Events** (non-bubbling `CustomEvent`s — listen directly on the element):
1486
2537
 
1487
2538
  | Event | `detail` type | Description |
1488
2539
  |---|---|---|
1489
- | `kai-change` | `CustomEvent<{ value: string }>` | The value was committed (blur). |
1490
- | `kai-input` | `CustomEvent<{ value: string }>` | The value changed per keystroke. |
2540
+ | `kai-change` | `CustomEvent<{ value: string; formattedValue: string }>` | The value was committed (blur). Same detail shape as `kai-input`. |
2541
+ | `kai-input` | `CustomEvent<{ value: string; formattedValue: string }>` | The value changed per keystroke. `value` is the canonical value (what a backend wants); `formattedValue` is the text on screen. With no mask the two are equal. |
2542
+ | `kai-input-rejected` | `CustomEvent<{ reason: "full" \| "wrong-class" \| "over-capacity" \| "format-change-clipped"; data: string }>` | A mask refused, or partly refused, some content. The reasons are `full` (no free position left), `wrong-class` (a letter into a digit position), `over-capacity` (a paste longer than the mask holds; what fits was kept), and `format-change-clipped` (the `format` changed under a value that no longer fits). `data` is the content that was refused. The first three are USER-INPUT errors, and are the ones worth announcing in a polite live region. `format-change-clipped` is not one: it follows the app changing its own configuration, so it reports and nothing more. None of the four touches validity, so `invalid` and `error` stay the consumer decision. |
1491
2543
 
1492
2544
  **Methods** (call on the element instance: `document.querySelector('kai-input').focus(…)`):
1493
2545
 
@@ -1495,7 +2547,9 @@ _No events._
1495
2547
  |---|---|---|
1496
2548
  | `focus` | `(options?: FocusOptions): void` | Focus the inner input (the host can't reach into the shadow root). |
1497
2549
  | `select` | `(): void` | Select the inner input's text. |
1498
- | `clear` | `(): void` | Empty the value and fire `kai-change` with `''`. |
2550
+ | `getRawValue` | `(): string` | The canonical value: digits for `tel` / `ssn` / `credit-card`, the formatted text for `custom`, and the field text when no mask is on. Identical to reading `el.value`, under the name backends use for the submitted form of a masked field. The mask engine has a third, narrower notion of raw (the fill characters with no literals at all) and that one is internal: it is not what any backend wants and it is not exposed here. |
2551
+ | `getFormattedValue` | `(): string` | The text on screen, literals and guide included. The counterpart to `formattedValue` on the `kai-input` / `kai-change` details, for a consumer that needs it outside an event. |
2552
+ | `clear` | `(): void` | Empty the value and fire `kai-change` with `''`. On a masked field this resets the mask itself, not just the text on screen, so the next character starts over. |
1499
2553
 
1500
2554
  **Slots** (project your own markup via `slot="name"` on a light-DOM child):
1501
2555
 
@@ -1843,6 +2897,23 @@ _No events._
1843
2897
 
1844
2898
  ---
1845
2899
 
2900
+ ### `kai-pane-grid` / `PaneGrid`
2901
+
2902
+ **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):
2903
+
2904
+ | Property | Attribute | Type | Description |
2905
+ |---|---|---|---|
2906
+ | `theme` | `theme` | `"light" \| "dark" \| "auto"` | Color mode (`auto` follows prefers-color-scheme). |
2907
+ | `minPaneWidth` | `min-pane-width` | `undefined \| number` | Minimum width of every pane, in px, before columns drop / the grid scrolls. Defaults to `280`. Attribute: `min-pane-width`. |
2908
+ | `minPaneHeight` | `min-pane-height` | `undefined \| number` | Minimum height of every pane, in px, before the grid scrolls vertically. Defaults to `200`. Attribute: `min-pane-height`. |
2909
+ | `maxColumns` | `max-columns` | `undefined \| number` | Column cap when the container is wide (default `3`). Attribute: `max-columns`. |
2910
+ | `gap` | `gap` | `undefined \| string` | Gap between panes, any CSS length. Defaults to the kit gap (`var(--kai-pane-grid-gap, 0.5rem)`). Attribute: `gap`. |
2911
+ | `maximizedIndex` | — | `undefined \| number \| null` | When set to a valid child index, render ONLY that pane full-bleed: a simple maximize hook the consumer drives (pair it with `<kai-pane>`'s `kai-maximize` event). Clear it (or point out of range) for the full tiled grid. Attribute: `maximized-index`. |
2912
+
2913
+ _No events._
2914
+
2915
+ ---
2916
+
1846
2917
  ### `kai-pane-group` / `PaneGroup`
1847
2918
 
1848
2919
  **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):
@@ -1994,7 +3065,7 @@ _No events._
1994
3065
  | `stoppable` | `stoppable` | `undefined \| false \| true` | When set and `loading` is true, the send button is replaced by a Stop button (square icon, "Stop" aria-label). Clicking it fires `kai-stop`. |
1995
3066
  | `submit` | `submit` | `undefined \| "always" \| "auto"` | Send-button visibility. `'always'` (default) always shows it; `'auto'` shows it only when there's text/attachments (an empty composer hides it, though Enter still submits). To hide it entirely (Enter-only), it's pure CSS: `::part(send){display:none}`, no prop needed. Restyle via `::part(send)`. The Stop button (`stoppable` + `loading`) is unaffected. |
1996
3067
  | `attach` | `attach` | `undefined \| false \| true` | When `false`, hides the built-in paperclip attach button even though the element otherwise supports attachments. Use this when a `+` menu in `toolbar-start` already exposes "Add files", to avoid a duplicate control. Defaults to `true`. |
1997
- | `attachments` | — | `undefined \| { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]` | Attachments to seed the input with (so a consumer can pre-populate staged files without an upload). Set as a JS property; the element then manages its own attachment state from there (add via the paperclip, remove per chip). |
3068
+ | `attachments` | — | `undefined \| { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[]` | Attachments to seed the input with (so a consumer can pre-populate staged files without an upload). Set as a JS property; the element then manages its own attachment state from there (add via the paperclip, remove per chip). Each item's `url` must be a `data:` URI or an https URL, never `URL.createObjectURL`: a `blob:` URL previews perfectly and is meaningless outside this tab, so `toOpenAIMessages`/`toAnthropicMessages` refuse it. (The built-in paperclip already stages files as `data:` URIs.) |
1998
3069
  | `triggers` | — | `undefined \| { char: string; kind: string; items?: undefined \| { id: string; label: string; icon?: undefined \| string; description?: undefined \| string; group?: undefined \| string; kind?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> }[] }[]` | Rich entity triggers. Each `{ char, kind, items }` opens a caret-anchored menu that inserts an atomic pill. Convention: `/` → skills, `@` → agents (plugins are the grouping/provenance of those items). Set as a JS property. |
1999
3070
  | `kindIcons` | — | `undefined \| Record<string, string>` | Default icon per entity kind (kind → image URL/data-URI) for pills/menu items without their own `icon`. Overrides the built-in agent/plugin glyphs. JS property. |
2000
3071
 
@@ -2004,7 +3075,7 @@ _No events._
2004
3075
  |---|---|---|
2005
3076
  | `kai-attachments-change` | `CustomEvent<{ attachments: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[] }>` | The staged attachments changed: a file was added (via the paperclip) or removed (per-chip ×). Carries the full current list so a consumer can react in real time (validate, show upload progress, toggle the send button). |
2006
3077
  | `kai-stop` | `CustomEvent<Record<string, never>>` | The Stop button was clicked while `stoppable` and `loading` are both true. |
2007
- | `kai-submit` | `CustomEvent<{ value: string; doc: ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> } })[]; entities: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> }[]; attachments: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[] }>` | The user submitted the prompt (Enter or send button). `value` is the flattened text (back-compat); `doc` is the structured document and `entities` the inserted pills (skills/agents) for downstream expansion. |
3078
+ | `kai-submit` | `CustomEvent<{ value: string; doc: ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> } })[]; entities: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> }[]; attachments: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[] }>` | The user submitted the prompt (Enter or send button). `value` is the flattened text (back-compat); `doc` is the structured document and `entities` the inserted pills (skills/agents) for downstream expansion. `<kai-prompt-input>` is the batteries-included composer row (send button, toolbar, attachment staging) built on `<kai-composer>`, the bare editor. |
2008
3079
  | `kai-suggestion-click` | `CustomEvent<{ value: string }>` | A suggestion was clicked while `suggestion-mode="fill"`. |
2009
3080
  | `kai-toolbar-action` | `CustomEvent<{ action: string }>` | A custom `<kai-action>` toolbar button was clicked. `action` is the `id` of the `<kai-action>` element that was clicked. |
2010
3081
  | `kai-value-change` | `CustomEvent<{ value: string; doc: ({ type: "text"; text: string } \| { type: "entity"; entity: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> } })[]; entities: { kind: string; id: string; label: string; icon?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> }[] }>` | The input changed (fires on every edit). Carries the flattened `value` plus the structured `doc` + `entities`. |
@@ -2042,6 +3113,33 @@ _No events._
2042
3113
 
2043
3114
  ---
2044
3115
 
3116
+ ### `kai-radio-group` / `RadioGroup`
3117
+
3118
+ **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):
3119
+
3120
+ | Property | Attribute | Type | Description |
3121
+ |---|---|---|---|
3122
+ | `theme` | `theme` | `"light" \| "dark" \| "auto"` | Color mode (`auto` follows prefers-color-scheme). |
3123
+ | `options` | — | `{ value: string; label: string; description?: undefined \| string; disabled?: undefined \| false \| true }[]` | The choices, top to bottom. Set as a JS PROPERTY (array), never an attribute. |
3124
+ | `value` | `value` | `undefined \| string` | Controlled selected `value`. Settable and reflected to the `value` attribute. `el.value = 'degraded'` drives it; choosing a row updates it and fires `kai-change`. Read `el.value` for live state. |
3125
+ | `name` | `name` | `undefined \| string` | Shared form-control name for every radio in the group. Defaults to a generated id, so the group is exclusive and keyboard-navigable even when nothing is submitted. |
3126
+ | `disabled` | `disabled` | `undefined \| false \| true` | Disable every row. Individual rows carry their own `disabled`. |
3127
+ | `label` | `label` | `undefined \| string` | Accessible name for the group. |
3128
+
3129
+ **Events** (non-bubbling `CustomEvent`s — listen directly on the element):
3130
+
3131
+ | Event | `detail` type | Description |
3132
+ |---|---|---|
3133
+ | `kai-change` | `CustomEvent<{ value: string }>` | A row was chosen. |
3134
+
3135
+ **Methods** (call on the element instance: `document.querySelector('kai-radio-group').focus(…)`):
3136
+
3137
+ | Method | Signature | Description |
3138
+ |---|---|---|
3139
+ | `focus` | `(options?: FocusOptions): void` | Focus the group's tab stop. That is the selected radio, or the first row when nothing is selected yet. |
3140
+
3141
+ ---
3142
+
2045
3143
  ### `kai-reasoning` / `Reasoning`
2046
3144
 
2047
3145
  **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):
@@ -2287,6 +3385,8 @@ _No events._
2287
3385
  | `for` | `for` | `undefined \| string` | CSS id of the scroll container to control. When omitted the element walks up the DOM (outside its own shadow root) to find the nearest scrollable ancestor. Mirrors the `for` convention of `<label for="...">`. |
2288
3386
  | `variant` | `variant` | `undefined \| "outline" \| "ghost" \| "default"` | Button visual variant: `'outline' \| 'ghost' \| 'default'`. Defaults to `'outline'`. |
2289
3387
  | `size` | `size` | `undefined \| "sm" \| "md" \| "lg" \| "icon" \| "icon-sm"` | Button size token. Defaults to `'icon'` (square). |
3388
+ | `label` | `label` | `undefined \| string` | The button's accessible name. It is announced whether or not the label is visible, so the text is always localisable. Defaults to `'Scroll to bottom'`. |
3389
+ | `showLabel` | `show-label` | `undefined \| false \| true` | Also render `label` visibly beside the icon. Defaults to `false`, which is the icon-only button. When the text is visible it IS the accessible name, so nothing gets announced twice. |
2290
3390
 
2291
3391
  **Events** (non-bubbling `CustomEvent`s — listen directly on the element):
2292
3392
 
@@ -2361,6 +3461,37 @@ _No events._
2361
3461
 
2362
3462
  ---
2363
3463
 
3464
+ ### `kai-select` / `Select`
3465
+
3466
+ **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):
3467
+
3468
+ | Property | Attribute | Type | Description |
3469
+ |---|---|---|---|
3470
+ | `theme` | `theme` | `"light" \| "dark" \| "auto"` | Color mode (`auto` follows prefers-color-scheme). |
3471
+ | `options` | — | `{ value: string; label?: undefined \| string; disabled?: undefined \| false \| true }[]` | The choices, in display order. Set as a JS PROPERTY (array), never an attribute. Rendered in full: the kit never truncates, re-orders or de-duplicates them. |
3472
+ | `value` | `value` | `undefined \| string` | Controlled selected value. Settable and reflected to the `value` attribute. `el.value = 'high'` drives it; choosing an option updates it and fires `kai-change`. Read `el.value` for live state; for a `multiple` select read `el.values` instead. |
3473
+ | `placeholder` | `placeholder` | `undefined \| string` | Text for a leading, disabled, empty option: the "nothing chosen yet" row. Omitted means no such row at all; there is no default wording, because inventing one would put words in your UI. |
3474
+ | `multiple` | `multiple` | `undefined \| false \| true` | Allow more than one selection. Turns the control into the platform's list box, so the kit's chevron is not drawn. |
3475
+ | `invalid` | `invalid` | `undefined \| false \| true` | Force the invalid (destructive-border) state. |
3476
+ | `disabled` | `disabled` | `undefined \| false \| true` | Disable interaction. |
3477
+ | `required` | `required` | `undefined \| false \| true` | Set the native `required` attribute, and nothing more. Whether an empty select is an error is the application's rule, not the kit's. |
3478
+ | `label` | `label` | `undefined \| string` | Accessible label for the control. |
3479
+ | `name` | `name` | `undefined \| string` | Form-control name, for a native form submit. |
3480
+
3481
+ **Events** (non-bubbling `CustomEvent`s — listen directly on the element):
3482
+
3483
+ | Event | `detail` type | Description |
3484
+ |---|---|---|
3485
+ | `kai-change` | `CustomEvent<{ value: string; values: string[] }>` | A choice was made. `value` is the first selected option (empty when nothing is selected); `values` is every selected option, which is what a `multiple` select needs. Both are always present, so neither shape silently loses the other. |
3486
+
3487
+ **Methods** (call on the element instance: `document.querySelector('kai-select').focus(…)`):
3488
+
3489
+ | Method | Signature | Description |
3490
+ |---|---|---|
3491
+ | `focus` | `(options?: FocusOptions): void` | Focus the inner select (the host element can't reach it). |
3492
+
3493
+ ---
3494
+
2364
3495
  ### `kai-separator` / `Separator`
2365
3496
 
2366
3497
  **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):
@@ -2475,6 +3606,37 @@ _No events._
2475
3606
 
2476
3607
  ---
2477
3608
 
3609
+ ### `kai-slider` / `Slider`
3610
+
3611
+ **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):
3612
+
3613
+ | Property | Attribute | Type | Description |
3614
+ |---|---|---|---|
3615
+ | `theme` | `theme` | `"light" \| "dark" \| "auto"` | Color mode (`auto` follows prefers-color-scheme). |
3616
+ | `min` | `min` | `undefined \| number` | Lowest selectable value. Required: a range with no bounds is a guess, and the guess belongs to whoever knows what the number means. |
3617
+ | `max` | `max` | `undefined \| number` | Highest selectable value. Required, for the same reason as `min`. |
3618
+ | `step` | `step` | `undefined \| number \| "any"` | Granularity. Omitted means the native default of 1; `any` means continuous. |
3619
+ | `value` | `value` | `undefined \| number` | Controlled value. Settable and reflected to the `value` attribute. `el.value = 40` drives it; dragging updates it and fires `kai-input` per step, `kai-change` on release. Read `el.value` for live state. |
3620
+ | `disabled` | `disabled` | `undefined \| false \| true` | Disable interaction. |
3621
+ | `label` | `label` | `undefined \| string` | Accessible label for the slider. |
3622
+ | `name` | `name` | `undefined \| string` | Form-control name, for a native form submit. |
3623
+ | `valueLabel` | — | `undefined \| false \| true \| ((value: number) => string)` | Show the current value beside the track. Off by default. Two ways in, because one of them is not a scalar. As a bare ATTRIBUTE (`<kai-slider value-label>`) it renders the raw number. As a JS PROPERTY it also accepts a formatter function (`el.valueLabel = (v) => v + '%'`), for a slider that is not counting bare numbers. A function cannot survive an attribute, so that half is property-only. The readout is hidden from assistive tech: the slider already reports the same number, and an exposed copy would be announced twice. |
3624
+
3625
+ **Events** (non-bubbling `CustomEvent`s — listen directly on the element):
3626
+
3627
+ | Event | `detail` type | Description |
3628
+ |---|---|---|
3629
+ | `kai-change` | `CustomEvent<{ value: number }>` | The value was committed: pointer released, or a key press finished. |
3630
+ | `kai-input` | `CustomEvent<{ value: number }>` | The thumb moved. Fires per step during a drag or a key press. |
3631
+
3632
+ **Methods** (call on the element instance: `document.querySelector('kai-slider').focus(…)`):
3633
+
3634
+ | Method | Signature | Description |
3635
+ |---|---|---|
3636
+ | `focus` | `(options?: FocusOptions): void` | Focus the inner range input (the host element can't reach it). |
3637
+
3638
+ ---
3639
+
2478
3640
  ### `kai-source` / `Source`
2479
3641
 
2480
3642
  **Properties** (every element also accepts `theme="light|dark|auto"`; only scalar props work as HTML attributes):
@@ -2743,7 +3905,7 @@ _No events._
2743
3905
  | Property | Attribute | Type | Description |
2744
3906
  |---|---|---|---|
2745
3907
  | `theme` | `theme` | `"light" \| "dark" \| "auto"` | Color mode (`auto` follows prefers-color-scheme). |
2746
- | `toasts` | — | `undefined \| { id: string; message: string; variant?: undefined \| "neutral" \| "success" \| "warning" \| "error" \| "info"; appearance?: undefined \| "pill" \| "card"; inverse?: undefined \| false \| true; description?: undefined \| string; action?: undefined \| { label: string; onAction: () => void \| false }; duration?: undefined \| number; dismissible?: undefined \| false \| true; target?: undefined \| HTMLElement }[]` | The toasts to render. Newest is shown on top. Set as a JS property (array); pass a new array reference to update. Omit for an empty region, which is the normal resting state and how the imperative `toast()` API starts. |
3908
+ | `toasts` | — | `undefined \| { id: string; message: string; variant?: undefined \| "neutral" \| "success" \| "warning" \| "error" \| "info"; appearance?: undefined \| "pill" \| "card"; inverse?: undefined \| false \| true; description?: undefined \| string; action?: undefined \| { label: string; onAction: () => void \| false }; duration?: undefined \| number; dismissible?: undefined \| false \| true; target?: undefined \| HTMLElement }[]` | The toasts to render. Newest is shown on top. Set as a JS property (array); pass a new array reference to update. Omit for an empty region, which is the normal resting state and how the imperative `toast()` API starts. Note the handover: the first `toast()` call ADOPTS a region you placed in markup (no second region mounts) and binds the imperative store to this property, replacing any array you set. Drive a region as data OR via `toast()`, not both at once. |
2747
3909
  | `position` | `position` | `undefined \| "top-center" \| "top-right" \| "top-left" \| "bottom-center" \| "bottom-right" \| "bottom-left"` | Stack anchor: `'top-center'` (default), `'top-right'`, `'bottom-center'`, … |
2748
3910
  | `max` | `max` | `undefined \| number` | Max simultaneously-visible toasts; the rest queue. Defaults to `3`. |
2749
3911
  | `stack` | `stack` | `undefined \| "expanded" \| "collapsed"` | Stacking: 'expanded' (default, full column) \| 'collapsed' (Sonner-style pile that expands on hover/focus). Attribute: stack. |
@@ -2832,7 +3994,7 @@ _No events._
2832
3994
  | Property | Attribute | Type | Description |
2833
3995
  |---|---|---|---|
2834
3996
  | `theme` | `theme` | `"light" \| "dark" \| "auto"` | Color mode (`auto` follows prefers-color-scheme). |
2835
- | `transcribe` | — | `undefined \| (audio: Blob) => Promise<string>` | Transcriber the host supplies: records audio, returns the text. This is a **function-valued property** (`el.transcribe = async blob => '...'`) because a value-returning callback can't be modelled as a fire-and-forget event. |
3997
+ | `transcribe` | — | `undefined \| ((audio: Blob) => Promise<string>)` | Transcriber the host supplies: records audio, returns the text. This is a **function-valued property** (`el.transcribe = async blob => '...'`) because a value-returning callback can't be modelled as a fire-and-forget event. |
2836
3998
  | `disabled` | `disabled` | `undefined \| false \| true` | Disable the mic button (non-interactive). |
2837
3999
  | `recognitionLang` | `recognition-lang` | `undefined \| string` | BCP-47 language tag for the native `SpeechRecognition` path (e.g. `en-US`). Attribute: `recognition-lang` (the plain `lang` attribute is reserved by `HTMLElement` and can't be a custom-element property). No effect when `transcribe` is set or the browser lacks SpeechRecognition. |
2838
4000
  | `interim` | `interim` | `undefined \| false \| true` | Emit live partial transcripts (`kai-transcript-interim`) during native recognition. Attribute: `interim`. No-op on the transcribe/fallback paths. |
@@ -2865,7 +4027,7 @@ _No events._
2865
4027
  | `theme` | `theme` | `"light" \| "dark" \| "auto"` | Color mode (`auto` follows prefers-color-scheme). |
2866
4028
  | `text` | `text` | `undefined \| string` | The utterance to read aloud. |
2867
4029
  | `autoplay` | `autoplay` | `undefined \| false \| true` | Speak automatically when `text` is set/changed. |
2868
- | `synthesize` | — | `undefined \| (text: string) => Promise<Blob>` | TTS model seam the host supplies: given text, returns an audio `Blob` to play. This is a **function-valued property** (`el.synthesize = async text => blob`); when set, the native `speechSynthesis` path is bypassed. Mirrors `<kai-voice-input>`'s `transcribe`. A value-returning callback can't be modelled as a fire-and-forget event, hence a property. |
4030
+ | `synthesize` | — | `undefined \| ((text: string) => Promise<Blob>)` | TTS model seam the host supplies: given text, returns an audio `Blob` to play. This is a **function-valued property** (`el.synthesize = async text => blob`); when set, the native `speechSynthesis` path is bypassed. Mirrors `<kai-voice-input>`'s `transcribe`. A value-returning callback can't be modelled as a fire-and-forget event, hence a property. |
2869
4031
  | `disabled` | `disabled` | `undefined \| false \| true` | Disable the button (non-interactive). |
2870
4032
 
2871
4033
  **Events** (non-bubbling `CustomEvent`s — listen directly on the element):