@kitn.ai/ui 0.25.2 → 0.26.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 (409) hide show
  1. package/README.md +2 -2
  2. package/dist/components/attachment-types.d.ts +46 -1
  3. package/dist/components/attachments.d.ts +31 -0
  4. package/dist/components/audio-visualizer/fit-scale.d.ts +50 -0
  5. package/dist/components/audio-visualizer/index.d.ts +15 -1
  6. package/dist/components/audio-visualizer/variant-bar.d.ts +24 -1
  7. package/dist/components/chat-thread.d.ts +3 -41
  8. package/dist/components/code-block.d.ts +22 -0
  9. package/dist/components/conversation-item.d.ts +54 -1
  10. package/dist/components/conversation-list.d.ts +65 -0
  11. package/dist/components/form.d.ts +42 -0
  12. package/dist/components/message.d.ts +7 -0
  13. package/dist/components/toast.d.ts +2 -1
  14. package/dist/components/voice-input.d.ts +8 -0
  15. package/dist/components/voice-output.d.ts +12 -1
  16. package/dist/components/workspace-shell.d.ts +87 -0
  17. package/dist/{create-tween-B5Y4Im8G.js → create-tween-BFT6c-Y8.js} +1 -1
  18. package/dist/{create-tween-4z1XYLkR.js → create-tween-CJNlzoaO.js} +1 -1
  19. package/dist/{create-tween-CUzOlaMS.js → create-tween-CQED4T0g.js} +1 -1
  20. package/dist/custom-elements.json +2931 -2572
  21. package/dist/diagnostics/hook.d.ts +81 -0
  22. package/dist/diagnostics/index.d.ts +7 -0
  23. package/dist/diagnostics/report-request.d.ts +61 -0
  24. package/dist/diagnostics.d.ts +5 -0
  25. package/dist/diagnostics.js +209 -0
  26. package/dist/elements/agent-card.js +1 -1
  27. package/dist/elements/artifact.js +1 -1
  28. package/dist/elements/attachments.js +1 -1
  29. package/dist/elements/audio-visualizer.js +1 -1
  30. package/dist/elements/autoloader.js +1 -1
  31. package/dist/elements/avatar.js +1 -1
  32. package/dist/elements/badge.js +1 -1
  33. package/dist/elements/button.js +1 -1
  34. package/dist/elements/card.js +1 -1
  35. package/dist/elements/cards.js +1 -1
  36. package/dist/elements/chain-of-thought.js +1 -1
  37. package/dist/elements/chat-scope-picker.js +1 -1
  38. package/dist/elements/chat-workspace.js +1 -1
  39. package/dist/elements/chat.js +1 -1
  40. package/dist/elements/checkpoint.js +1 -1
  41. package/dist/elements/choice.js +1 -1
  42. package/dist/elements/chunks/{Icon-b5A8hlHs.js → Icon--Y8vsFal.js} +1 -1
  43. package/dist/elements/chunks/action-icons-D9u3OzYW.js +1 -0
  44. package/dist/elements/chunks/arrow-left-CVh1TeN7.js +1 -0
  45. package/dist/elements/chunks/{artifact-BSuPYFHY.js → artifact-BkYcfIlZ.js} +1 -1
  46. package/dist/elements/chunks/attachments-DwfNlubq.js +1 -0
  47. package/dist/elements/chunks/audio-visualizer-D7lB6mvR.js +1 -0
  48. package/dist/elements/chunks/{badge-TIMiJFKc.js → badge-Q0-Cmwzz.js} +1 -1
  49. package/dist/elements/chunks/{button-B9okqG73.js → button-flUbeufF.js} +1 -1
  50. package/dist/elements/chunks/{card-renderer-CSoTL_e1.js → card-renderer-3Dyl-FpN.js} +1 -1
  51. package/dist/elements/chunks/check-B5kBPKeo.js +1 -0
  52. package/dist/elements/chunks/chevron-down-DZ1GS39h.js +1 -0
  53. package/dist/elements/chunks/chevron-right-CvP7OUd2.js +1 -0
  54. package/dist/elements/chunks/{choice-card-CGMc3kek.js → choice-card-D4QUIOWZ.js} +1 -1
  55. package/dist/elements/chunks/circle-CDf9g6MQ.js +1 -0
  56. package/dist/elements/chunks/circle-check-CA47Nqbz.js +1 -0
  57. package/dist/elements/chunks/{circle-x-mhH7pOK2.js → circle-x-CJ_HVbC8.js} +1 -1
  58. package/dist/elements/chunks/code-block-BdLiTitd.js +1 -0
  59. package/dist/elements/chunks/collapsible-BzbXpPzr.js +1 -0
  60. package/dist/elements/chunks/{composer-CZZmtYOT.js → composer-Byve-n8K.js} +2 -2
  61. package/dist/elements/chunks/{confirm-card-BWjf2-xo.js → confirm-card-B0flE3MN.js} +1 -1
  62. package/dist/elements/chunks/context-VLIL3sJ-.js +1 -0
  63. package/dist/elements/chunks/controllable-DTwTIlwp.js +1 -0
  64. package/dist/elements/chunks/conversation-item-BDztezOG.js +1 -0
  65. package/dist/elements/chunks/copy-Nf-kqxXH.js +1 -0
  66. package/dist/elements/chunks/{create-tween-CUrM6D_o.js → create-tween-c4Z8TDYz.js} +2 -2
  67. package/dist/elements/chunks/default-input-C2iLFq0b.js +1 -0
  68. package/dist/elements/chunks/define-wjHR1MkU.js +1 -0
  69. package/dist/elements/chunks/disclosure-CcNbyLy0.js +1 -0
  70. package/dist/elements/chunks/{download-DnCyWFJE.js → download-C4V7tBdb.js} +1 -1
  71. package/dist/elements/chunks/dropdown-BgWTthUx.js +1 -0
  72. package/dist/elements/chunks/{ellipsis-CVxqfsDZ.js → ellipsis-7gU33mL2.js} +1 -1
  73. package/dist/elements/chunks/embed-D3KewsIJ.js +1 -0
  74. package/dist/elements/chunks/{external-link-DG2sRdQL.js → external-link-CSOEsNlx.js} +1 -1
  75. package/dist/elements/chunks/{file-text-D0J9F6M7.js → file-text-D1G8khle.js} +1 -1
  76. package/dist/elements/chunks/{file-tree-yGWNK0Js.js → file-tree-zvB2i7P9.js} +1 -1
  77. package/dist/elements/chunks/{folder-NV1pi_ud.js → folder-WW2LGLid.js} +1 -1
  78. package/dist/elements/chunks/form-CQirdC4W.js +1 -0
  79. package/dist/elements/chunks/hover-card-CJJkfHTK.js +1 -0
  80. package/dist/elements/chunks/icon-Cfgmf4fa.js +1 -0
  81. package/dist/elements/chunks/{info-C8r5Q6cc.js → info-DX3YMOt6.js} +1 -1
  82. package/dist/elements/chunks/input-BtW9U6Sq.js +1 -0
  83. package/dist/elements/chunks/{kbd-OGJDUT-O.js → kbd-LGXwuT_V.js} +1 -1
  84. package/dist/elements/chunks/{link-BdsfLB7b.js → link-2My69BqT.js} +1 -1
  85. package/dist/elements/chunks/{link-preview-Cf5KOP-i.js → link-preview-OQ9IEq0f.js} +1 -1
  86. package/dist/elements/chunks/{loader-B5QzK_8_.js → loader-iOxc_Tu-.js} +1 -1
  87. package/dist/elements/chunks/{markdown-DbozyYqa.js → markdown-DgBFaGAz.js} +1 -1
  88. package/dist/elements/chunks/{message-DtXKPqiR.js → message-VMZohrXx.js} +1 -1
  89. package/dist/elements/chunks/message-circle-CpXMM-_n.js +1 -0
  90. package/dist/elements/chunks/message-square-DxcWtETQ.js +1 -0
  91. package/dist/elements/chunks/message-tZmV2A3p.js +1 -0
  92. package/dist/elements/chunks/{minimize-2-SegJ_oku.js → minimize-2-DPIUKTrT.js} +1 -1
  93. package/dist/elements/chunks/{model-switcher-D7JrFFHp.js → model-switcher-DsT8tFB0.js} +1 -1
  94. package/dist/elements/chunks/{overlay-CV7z2YuX.js → overlay-DuiwdXjm.js} +1 -1
  95. package/dist/elements/chunks/{paperclip-Bg0ITU3b.js → paperclip-CRZVp4z_.js} +1 -1
  96. package/dist/elements/chunks/{progress-bar-D1_rzt6N.js → progress-bar-DNy66e_E.js} +1 -1
  97. package/dist/elements/chunks/{prompt-suggestion-D5Sm8xF1.js → prompt-suggestion-Bn64Nl7L.js} +1 -1
  98. package/dist/elements/chunks/reasoning-Emn9N4-T.js +1 -0
  99. package/dist/elements/chunks/{resizable-Dpzjlvp4.js → resizable-D181b4fM.js} +1 -1
  100. package/dist/elements/chunks/{rotate-cw-DzVDJOA1.js → rotate-cw-CPWe37D9.js} +1 -1
  101. package/dist/elements/chunks/{scroll-area-93A1bbeG.js → scroll-area-ClZkZxIk.js} +1 -1
  102. package/dist/elements/chunks/scroll-button-DPE-t3x_.js +1 -0
  103. package/dist/elements/chunks/{separator-DH2r_ZSZ.js → separator-CeJHQqLW.js} +1 -1
  104. package/dist/elements/chunks/{settings-BFMOd6yB.js → settings-CqaHgJF9.js} +1 -1
  105. package/dist/elements/chunks/{settings-group-B3VZKIcz.js → settings-group-CjqjLCOn.js} +1 -1
  106. package/dist/elements/chunks/share-CyfnNEqp.js +1 -0
  107. package/dist/elements/chunks/{skeleton-N8BuWYmf.js → skeleton-D8akBnKS.js} +1 -1
  108. package/dist/elements/chunks/slots-DaNjX0rc.js +1 -0
  109. package/dist/elements/chunks/{source-w9V0NnBn.js → source-UWCvmVak.js} +1 -1
  110. package/dist/elements/chunks/{star-DHvACxtm.js → star-DoHFAikQ.js} +1 -1
  111. package/dist/elements/chunks/store-Cjq0qwYN.js +1 -0
  112. package/dist/elements/chunks/{tasks-card-lPxoodlA.js → tasks-card-CmZ5xh6v.js} +1 -1
  113. package/dist/elements/chunks/{text-shimmer-BY_rI3s5.js → text-shimmer-DYn09zrz.js} +1 -1
  114. package/dist/elements/chunks/textarea-skiGHURM.js +1 -0
  115. package/dist/elements/chunks/{thumbs-up-DQe0hvkZ.js → thumbs-up-DX2Ijk3H.js} +1 -1
  116. package/dist/elements/chunks/{toast-store-VUZZBhzH.js → toast-store-CFnQpThX.js} +1 -1
  117. package/dist/elements/chunks/{tool-C1YtxwPu.js → tool-DKQ2D1DP.js} +1 -1
  118. package/dist/elements/chunks/{tooltip-BqwnFWhn.js → tooltip-B0G99y8w.js} +1 -1
  119. package/dist/elements/chunks/{triangle-alert-B6or7F5u.js → triangle-alert-F9cyUEyy.js} +1 -1
  120. package/dist/elements/chunks/{use-card-resolution-DJg32jGH.js → use-card-resolution-DoA2f_EU.js} +1 -1
  121. package/dist/elements/chunks/{variant-aurora-QXTZPyST.js → variant-aurora-qt6Zct8M.js} +2 -2
  122. package/dist/elements/chunks/variant-custom-tJp22_BQ.js +1 -0
  123. package/dist/{variant-wave-BwzrHD1s.js → elements/chunks/variant-wave-CaQ5ti39.js} +2 -2
  124. package/dist/elements/chunks/x-B6UnOiLw.js +1 -0
  125. package/dist/elements/coachmark.js +1 -1
  126. package/dist/elements/code-block.js +1 -1
  127. package/dist/elements/command.js +1 -1
  128. package/dist/elements/compare.js +1 -1
  129. package/dist/elements/composer.js +1 -1
  130. package/dist/elements/confirm-card.js +1 -1
  131. package/dist/elements/context-meter.js +1 -1
  132. package/dist/elements/conversation-item.d.ts +1 -0
  133. package/dist/elements/conversation-item.js +1 -0
  134. package/dist/elements/conversation-list.js +1 -1
  135. package/dist/elements/default-input.d.ts +3 -3
  136. package/dist/elements/define.d.ts +27 -0
  137. package/dist/elements/diagnostic-events.d.ts +74 -0
  138. package/dist/elements/dialog.js +1 -1
  139. package/dist/elements/dock.d.ts +1 -0
  140. package/dist/elements/dock.js +149 -0
  141. package/dist/elements/dropdown.d.ts +1 -0
  142. package/dist/elements/dropdown.js +1 -0
  143. package/dist/elements/editable-label.js +1 -1
  144. package/dist/elements/element-diagnostics.d.ts +81 -0
  145. package/dist/elements/embed.js +1 -1
  146. package/dist/elements/empty.js +1 -1
  147. package/dist/elements/feedback-bar.js +1 -1
  148. package/dist/elements/file-tree.js +1 -1
  149. package/dist/elements/file-upload.js +1 -1
  150. package/dist/elements/form.js +1 -1
  151. package/dist/elements/hover-card.js +1 -1
  152. package/dist/elements/icon.js +1 -1
  153. package/dist/elements/image.js +1 -1
  154. package/dist/elements/input.js +1 -1
  155. package/dist/elements/kbd.js +1 -1
  156. package/dist/elements/link-preview.js +1 -1
  157. package/dist/elements/loader.js +1 -1
  158. package/dist/elements/markdown.js +1 -1
  159. package/dist/elements/menu.js +1 -1
  160. package/dist/elements/message-skills.js +1 -1
  161. package/dist/elements/message.js +1 -1
  162. package/dist/elements/model-switcher.js +1 -1
  163. package/dist/elements/nav.js +1 -1
  164. package/dist/elements/notice.js +1 -1
  165. package/dist/elements/pane-group.js +1 -1
  166. package/dist/elements/pane.js +1 -1
  167. package/dist/elements/popover.js +1 -1
  168. package/dist/elements/progress-bar.js +1 -1
  169. package/dist/elements/prompt-dock.js +1 -1
  170. package/dist/elements/prompt-input.js +1 -1
  171. package/dist/elements/prompt-suggestions.js +1 -1
  172. package/dist/elements/reasoning.js +1 -1
  173. package/dist/elements/register.d.ts +2 -0
  174. package/dist/elements/remote.js +1 -1
  175. package/dist/elements/resizable.js +1 -1
  176. package/dist/elements/response-stream.js +3 -3
  177. package/dist/elements/screen.js +1 -1
  178. package/dist/elements/scroll-area.js +1 -1
  179. package/dist/elements/scroll-button.js +1 -1
  180. package/dist/elements/search.js +1 -1
  181. package/dist/elements/segmented.js +1 -1
  182. package/dist/elements/separator.js +1 -1
  183. package/dist/elements/setting-item.js +1 -1
  184. package/dist/elements/settings-group.js +1 -1
  185. package/dist/elements/skeleton.js +1 -1
  186. package/dist/elements/slots.d.ts +35 -3
  187. package/dist/elements/source.js +1 -1
  188. package/dist/elements/status.js +1 -1
  189. package/dist/elements/switch.js +1 -1
  190. package/dist/elements/tabs.js +1 -1
  191. package/dist/elements/tasks.js +1 -1
  192. package/dist/elements/text-shimmer.js +1 -1
  193. package/dist/elements/thinking-bar.js +1 -1
  194. package/dist/elements/thread.js +1 -1
  195. package/dist/elements/toast.js +1 -1
  196. package/dist/elements/tool.js +1 -1
  197. package/dist/elements/tooltip.js +1 -1
  198. package/dist/elements/voice-input.js +1 -1
  199. package/dist/elements/voice-output.js +1 -1
  200. package/dist/elements.d.ts +232 -148
  201. package/dist/index.d.ts +2 -0
  202. package/dist/index.js +5456 -4931
  203. package/dist/index.server.js +4142 -3669
  204. package/dist/kai-provider.es.js +40 -40
  205. package/dist/kai.es.js +1 -1
  206. package/dist/llms/llms-full.txt +184 -69
  207. package/dist/llms/llms.txt +3 -3
  208. package/dist/mcp.es.js +2669 -546
  209. package/dist/primitives/use-resize-observer.d.ts +23 -0
  210. package/dist/primitives/use-speech-recognition.d.ts +8 -0
  211. package/dist/primitives/use-text-stream.d.ts +5 -0
  212. package/dist/react/index.d.ts +115 -266
  213. package/dist/react.js +247 -221
  214. package/dist/register-impl-DpE7icIb.js +293 -0
  215. package/dist/schemas/index.d.ts +1 -1
  216. package/dist/schemas/tool-defs.d.ts +35 -0
  217. package/dist/schemas.js +403 -310
  218. package/dist/{solid-BMuDmFo_.js → solid-BtdqNZI7.js} +8435 -7394
  219. package/dist/{solid-CGYNdLRR.js → solid-CnavwBye.js} +5987 -5035
  220. package/dist/solid.d.ts +5 -0
  221. package/dist/solid.js +210 -203
  222. package/dist/solid.server.js +210 -203
  223. package/dist/state/index.d.ts +4 -0
  224. package/dist/state/persistence.d.ts +51 -0
  225. package/dist/state/stream.d.ts +50 -0
  226. package/dist/state/threads.d.ts +71 -0
  227. package/dist/state.js +269 -169
  228. package/dist/types.d.ts +4 -2
  229. package/dist/ui/dock.d.ts +109 -0
  230. package/dist/ui/hover-card.d.ts +44 -0
  231. package/dist/{variant-aurora-DwSOfraQ.js → variant-aurora-B8II3-I1.js} +27 -27
  232. package/dist/{variant-aurora-wKvAQ5wp.js → variant-aurora-CMNWOSJR.js} +28 -28
  233. package/dist/{variant-aurora-XEkPT0_z.js → variant-aurora-CcuDMWwQ.js} +2 -2
  234. package/dist/{variant-custom-eFIg2XWQ.js → variant-custom-BdvYC7af.js} +40 -40
  235. package/dist/{variant-custom-BhS9wIpU.js → variant-custom-C714mqKl.js} +65 -65
  236. package/dist/variant-custom-L16JLLZy.js +1 -0
  237. package/dist/{elements/chunks/variant-wave-DDCwhLas.js → variant-wave-2S4La2Ka.js} +2 -2
  238. package/dist/{variant-wave-CbVsmgwT.js → variant-wave-D7ruiA1W.js} +25 -25
  239. package/dist/{variant-wave-CUWGx9Bn.js → variant-wave-ecwykeyL.js} +21 -21
  240. package/dist/wire/chunk.d.ts +47 -0
  241. package/dist/wire/diagnostics.d.ts +575 -0
  242. package/dist/wire/encode-probe.d.ts +70 -0
  243. package/dist/wire/encode.d.ts +24 -0
  244. package/dist/wire/index.d.ts +2 -0
  245. package/dist/wire/sse.d.ts +10 -2
  246. package/dist/wire.js +917 -480
  247. package/frameworks/react/index.tsx +132 -93
  248. package/llms-full.txt +184 -69
  249. package/llms.txt +3 -3
  250. package/package.json +26 -6
  251. package/src/agent-tooling/archetypes.ts +41 -9
  252. package/src/agent-tooling/catalog/README.md +676 -0
  253. package/src/agent-tooling/catalog/catalog-types.ts +231 -0
  254. package/src/agent-tooling/catalog/fabrications.ts +96 -0
  255. package/src/agent-tooling/catalog/invariants.ts +284 -0
  256. package/src/agent-tooling/catalog/labs-titles.ts +114 -0
  257. package/src/agent-tooling/catalog/scenarios.ts +87 -0
  258. package/src/agent-tooling/catalog/surfaces.ts +275 -0
  259. package/src/agent-tooling/integrations/anthropic.ts +7 -1
  260. package/src/agent-tooling/integrations/cloudflare.ts +43 -2
  261. package/src/agent-tooling/integrations/langgraph.ts +7 -1
  262. package/src/agent-tooling/integrations/mastra.ts +7 -1
  263. package/src/agent-tooling/integrations/mock.ts +92 -17
  264. package/src/agent-tooling/integrations/ollama.ts +7 -1
  265. package/src/agent-tooling/integrations/openai.ts +7 -1
  266. package/src/agent-tooling/integrations/openrouter.ts +7 -1
  267. package/src/agent-tooling/integrations/pi.ts +56 -1
  268. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +7 -1
  269. package/src/agent-tooling/mcp/css-raw.d.ts +12 -0
  270. package/src/agent-tooling/mcp/manifest.ts +100 -1
  271. package/src/agent-tooling/mcp/server.ts +61 -5
  272. package/src/agent-tooling/mcp/tools/reference.ts +440 -4
  273. package/src/agent-tooling/mcp/tools/scaffold.ts +1834 -307
  274. package/src/agent-tooling/mcp/tools/theme.ts +146 -30
  275. package/src/agent-tooling/mcp/validate-args.ts +141 -0
  276. package/src/agent-tooling/registry.ts +19 -7
  277. package/src/agent-tooling/route-emit.ts +26 -2
  278. package/src/components/attachment-types.ts +47 -1
  279. package/src/components/attachments.tsx +169 -22
  280. package/src/components/audio-visualizer/fit-scale.ts +112 -0
  281. package/src/components/audio-visualizer/index.tsx +80 -3
  282. package/src/components/audio-visualizer/variant-aurora.tsx +10 -4
  283. package/src/components/audio-visualizer/variant-bar.tsx +38 -6
  284. package/src/components/audio-visualizer/variant-custom.tsx +8 -2
  285. package/src/components/audio-visualizer/variant-grid.tsx +16 -11
  286. package/src/components/audio-visualizer/variant-radial.tsx +15 -10
  287. package/src/components/audio-visualizer/variant-wave.tsx +7 -2
  288. package/src/components/chat-container.tsx +4 -1
  289. package/src/components/chat-thread.tsx +36 -7
  290. package/src/components/code-block.tsx +79 -3
  291. package/src/components/composer.tsx +8 -1
  292. package/src/components/conversation-item.tsx +114 -4
  293. package/src/components/conversation-list.tsx +212 -12
  294. package/src/components/embed.tsx +4 -0
  295. package/src/components/form.tsx +41 -14
  296. package/src/components/message.tsx +75 -5
  297. package/src/components/response-stream.tsx +13 -7
  298. package/src/components/screen.tsx +5 -0
  299. package/src/components/scroll-button.tsx +7 -0
  300. package/src/components/toast.tsx +11 -5
  301. package/src/components/voice-input.tsx +35 -3
  302. package/src/components/voice-output.tsx +46 -6
  303. package/src/components/workspace-shell.tsx +285 -0
  304. package/src/diagnostics/hook.ts +352 -0
  305. package/src/diagnostics/index.ts +82 -0
  306. package/src/diagnostics/report-request.ts +296 -0
  307. package/src/elements/attachments.tsx +6 -7
  308. package/src/elements/audio-visualizer.tsx +48 -4
  309. package/src/elements/chat-workspace.tsx +159 -287
  310. package/src/elements/chat.tsx +12 -9
  311. package/src/elements/code-block.tsx +19 -1
  312. package/src/elements/command.tsx +4 -1
  313. package/src/elements/compiled.css +1 -1
  314. package/src/elements/conversation-item.tsx +80 -0
  315. package/src/elements/conversation-list.tsx +60 -11
  316. package/src/elements/default-input.tsx +5 -5
  317. package/src/elements/define.tsx +168 -8
  318. package/src/elements/diagnostic-events.ts +114 -0
  319. package/src/elements/dialog.tsx +47 -0
  320. package/src/elements/disclosure.ts +34 -12
  321. package/src/elements/dock.tsx +204 -0
  322. package/src/elements/dropdown.tsx +123 -0
  323. package/src/elements/element-diagnostics.ts +392 -0
  324. package/src/elements/element-manifest.json +12 -0
  325. package/src/elements/element-meta.json +672 -439
  326. package/src/elements/element-nonscalar.json +170 -0
  327. package/src/elements/element-types.d.ts +232 -148
  328. package/src/elements/menu.tsx +13 -0
  329. package/src/elements/prompt-dock.tsx +2 -1
  330. package/src/elements/prompt-input.tsx +8 -8
  331. package/src/elements/register-impl.ts +46 -0
  332. package/src/elements/register.ts +28 -0
  333. package/src/elements/resizable.tsx +52 -25
  334. package/src/elements/scroll-button.tsx +5 -0
  335. package/src/elements/slots.ts +107 -12
  336. package/src/elements/styles.css +30 -6
  337. package/src/elements/thinking-bar.tsx +16 -7
  338. package/src/elements/toast.tsx +3 -0
  339. package/src/elements/tool.tsx +26 -5
  340. package/src/elements/voice-input.tsx +11 -2
  341. package/src/elements/voice-output.tsx +16 -3
  342. package/src/index.ts +2 -0
  343. package/src/primitives/use-resize-observer.ts +33 -0
  344. package/src/primitives/use-speech-recognition.ts +12 -1
  345. package/src/primitives/use-stick-to-bottom.ts +34 -2
  346. package/src/primitives/use-text-stream.ts +33 -2
  347. package/src/remote/provider-runtime.ts +6 -3
  348. package/src/schemas/index.ts +1 -0
  349. package/src/schemas/tool-defs.ts +297 -6
  350. package/src/solid.ts +5 -0
  351. package/src/state/index.ts +12 -0
  352. package/src/state/persistence.ts +180 -0
  353. package/src/state/stream.ts +71 -4
  354. package/src/state/threads.ts +142 -0
  355. package/src/types.ts +4 -2
  356. package/src/ui/dialog.tsx +6 -0
  357. package/src/ui/dock.tsx +445 -0
  358. package/src/ui/dropdown.tsx +67 -23
  359. package/src/ui/hover-card.tsx +156 -6
  360. package/src/ui/input.tsx +29 -10
  361. package/src/ui/resizable.tsx +11 -0
  362. package/src/wire/chunk.ts +47 -0
  363. package/src/wire/consume.ts +185 -26
  364. package/src/wire/diagnostics.ts +727 -0
  365. package/src/wire/encode-probe.ts +214 -0
  366. package/src/wire/encode.ts +267 -14
  367. package/src/wire/formats/anthropic.ts +8 -1
  368. package/src/wire/formats/openai.ts +5 -0
  369. package/src/wire/index.ts +22 -0
  370. package/src/wire/read.ts +250 -6
  371. package/src/wire/sse.ts +16 -3
  372. package/dist/elements/chunks/action-icons-DW9muWrY.js +0 -1
  373. package/dist/elements/chunks/arrow-left-iJVnQmxA.js +0 -1
  374. package/dist/elements/chunks/attachments-CL6TaqIb.js +0 -1
  375. package/dist/elements/chunks/audio-visualizer-BuUqQmaO.js +0 -1
  376. package/dist/elements/chunks/chat-thread-DxoKaStk.js +0 -1
  377. package/dist/elements/chunks/check-MdLzbxrm.js +0 -1
  378. package/dist/elements/chunks/chevron-down-MK7IzDqe.js +0 -1
  379. package/dist/elements/chunks/chevron-right-C5jXhmtV.js +0 -1
  380. package/dist/elements/chunks/circle-BG7VkPep.js +0 -1
  381. package/dist/elements/chunks/circle-check-GqvO2UBs.js +0 -1
  382. package/dist/elements/chunks/code-block-ClhQO9RW.js +0 -1
  383. package/dist/elements/chunks/collapsible-DBcBkFPh.js +0 -1
  384. package/dist/elements/chunks/context-hKjNqiyQ.js +0 -1
  385. package/dist/elements/chunks/conversation-list-CsGJjSKG.js +0 -1
  386. package/dist/elements/chunks/default-input-B1Mem5wu.js +0 -1
  387. package/dist/elements/chunks/define-B_tXMYRm.js +0 -1
  388. package/dist/elements/chunks/disclosure-DAoYOKew.js +0 -1
  389. package/dist/elements/chunks/dropdown-CmNna5lT.js +0 -1
  390. package/dist/elements/chunks/embed-CjAD2jSi.js +0 -1
  391. package/dist/elements/chunks/form-Cp8Y00Wn.js +0 -1
  392. package/dist/elements/chunks/hover-card-6KtXpLwT.js +0 -1
  393. package/dist/elements/chunks/icon-97R8SR2p.js +0 -1
  394. package/dist/elements/chunks/input-DrseEjhp.js +0 -1
  395. package/dist/elements/chunks/message-CE4k-qaj.js +0 -1
  396. package/dist/elements/chunks/message-square-C9ovmtOo.js +0 -1
  397. package/dist/elements/chunks/reasoning-BrxQfpfI.js +0 -1
  398. package/dist/elements/chunks/scroll-button-CrTo7wm4.js +0 -1
  399. package/dist/elements/chunks/share-DgNbkJKY.js +0 -1
  400. package/dist/elements/chunks/slots-CIw9RlAe.js +0 -1
  401. package/dist/elements/chunks/store-HIPGhNz9.js +0 -1
  402. package/dist/elements/chunks/textarea-DPfuhswM.js +0 -1
  403. package/dist/elements/chunks/variant-custom-D5ZlmziF.js +0 -1
  404. package/dist/elements/chunks/video-Bb86fVYI.js +0 -1
  405. package/dist/elements/chunks/x-BOhjBeOd.js +0 -1
  406. package/dist/primitives/card-validate-generator.testlib.d.ts +0 -20
  407. package/dist/register-impl-DGmGzrEM.js +0 -145
  408. package/dist/variant-custom-MkTNTycd.js +0 -1
  409. package/src/primitives/card-validate-generator.testlib.ts +0 -38
@@ -30,6 +30,160 @@ export function CollapsedRail(props: CollapsedRailProps) {
30
30
  );
31
31
  }
32
32
 
33
+ /** Read a conversation item's identity the way the container does: the
34
+ * `conversationId` property, else the `conversation-id` attribute, else the
35
+ * host `id`. The property is what a framework loop sets; the attributes are
36
+ * the plain-HTML spellings (host `id` matches the `<kai-conversation>`
37
+ * data-carrier precedent). */
38
+ export function readConversationItemId(el: Element): string {
39
+ const prop = (el as Element & { conversationId?: unknown }).conversationId;
40
+ if (typeof prop === 'string' && prop) return prop;
41
+ return el.getAttribute('conversation-id') ?? el.id;
42
+ }
43
+
44
+ export interface ConversationItemsControllerOptions {
45
+ /** The current slotted item hosts, in DOM order. */
46
+ getItems: () => HTMLElement[];
47
+ /** The container's active conversation id. */
48
+ getActiveId: () => string | undefined;
49
+ /** Item activation (click / Enter / Space). Surfaces as
50
+ * `kai-conversation-select` on the element. */
51
+ onSelect: (id: string) => void;
52
+ }
53
+
54
+ export interface ConversationItemsController {
55
+ /** Re-derive the ARIA/roving-tabindex bookkeeping over the current items.
56
+ * Call on slotchange / child mutation and whenever `activeId` changes. */
57
+ sync(): void;
58
+ /** The container's items-region click handler (composed-path aware). */
59
+ handleClick(e: MouseEvent): void;
60
+ /** The container's items-region keydown handler (arrows, Home/End,
61
+ * Enter/Space). */
62
+ handleKeyDown(e: KeyboardEvent): void;
63
+ }
64
+
65
+ /**
66
+ * The parent-item contract of item mode (spec 2026-08-20 § 2a), as a pure-DOM
67
+ * controller so it is host-agnostic: the `kai-conversations` facade wires it over
68
+ * its slotted `kai-conversation-item` children, and the jsdom contract tests
69
+ * drive it over plain nodes. Solid context cannot cross the element boundary
70
+ * (each facade is its own Solid root), so the channel is DOM traversal by
71
+ * construction:
72
+ *
73
+ * - selection flows container to item — exactly one item's BODY node (the
74
+ * shadow body of a `kai-conversation-item`, else the node itself; see
75
+ * `bodyOf`) is `aria-current="true"`, plus the `active` property on the
76
+ * host for the item's own styling hook;
77
+ * - `role="button"` is ensured on each item's body node (an authored role is
78
+ * left alone);
79
+ * - roving tabindex — exactly one body node `tabindex="0"` (the active
80
+ * item's, else the first's), the rest `-1`, re-derived on every `sync()`;
81
+ * menu content keeps its natural tab order (it is the body's SIBLING, per
82
+ * the ratified 2026-08-20 sibling restructure);
83
+ * - activation (click / Enter / Space) calls `onSelect` with the item's id, and
84
+ * is SUPPRESSED when the composed path crosses the item's `menu` region
85
+ * (light-DOM `slot="menu"` content or the shadow `data-kai-item-menu`
86
+ * wrapper), so the consumer's own popover never also selects the row;
87
+ * - ArrowUp/ArrowDown/Home/End move focus item-to-item, tabindex following the
88
+ * focused item.
89
+ */
90
+ export function createConversationItemsController(
91
+ opts: ConversationItemsControllerOptions,
92
+ ): ConversationItemsController {
93
+ const itemFromEvent = (e: Event): HTMLElement | undefined => {
94
+ const items = opts.getItems();
95
+ return e.composedPath().find((n): n is HTMLElement => items.includes(n as HTMLElement));
96
+ };
97
+ /** The item's ACTIVATION node — the target of role/aria-current/tabindex/
98
+ * focus. For a `kai-conversation-item` host that is its shadow body (the
99
+ * sibling restructure, ratified 2026-08-20: the host is the row listitem
100
+ * wrapping the body AND the consumer's tabbable menu, so the control
101
+ * semantics must sit below it). A bare node with no such body is its own
102
+ * control. */
103
+ const bodyOf = (item: HTMLElement): HTMLElement =>
104
+ (item.shadowRoot?.querySelector('[data-kai-item-body]') as HTMLElement | null) ?? item;
105
+ const menuInPath = (e: Event): boolean =>
106
+ e.composedPath().some(
107
+ (n) =>
108
+ n instanceof Element &&
109
+ (n.hasAttribute('data-kai-item-menu') || n.getAttribute('slot') === 'menu'),
110
+ );
111
+ // Write-on-change only. `setAttribute` records a mutation even when the value
112
+ // is identical, and the facade re-syncs from a MutationObserver over these very
113
+ // nodes — unconditional writes would feed the observer forever.
114
+ const setAttr = (el: Element, name: string, value: string) => {
115
+ if (el.getAttribute(name) !== value) el.setAttribute(name, value);
116
+ };
117
+ /** An item whose shadow body has not RENDERED yet must not be stamped: the
118
+ * fallback would write control semantics onto the HOST, they would stick
119
+ * (the item facade defers to an authored role), and axe then sees exactly
120
+ * the role="button"-host-with-focusable-menu shape the restructure removed.
121
+ * Measured in the focus-order probe: the container's first sync can run
122
+ * before the item elements upgrade. A later sync catches them — the
123
+ * facade's own mount mutates host attributes, which re-runs sync through
124
+ * the container's MutationObserver read(). Bare nodes (no dash: the jsdom
125
+ * stand-ins) are always ready. */
126
+ const readyBodies = (items: HTMLElement[]) => {
127
+ const out = new Map<HTMLElement, HTMLElement>();
128
+ for (const item of items) {
129
+ const body = bodyOf(item);
130
+ if (body === item && item.localName.includes('-')) continue;
131
+ out.set(item, body);
132
+ }
133
+ return out;
134
+ };
135
+
136
+ const rove = (items: HTMLElement[], target: HTMLElement | undefined) => {
137
+ for (const [item, body] of readyBodies(items)) setAttr(body, 'tabindex', item === target ? '0' : '-1');
138
+ };
139
+
140
+ const sync = () => {
141
+ const items = opts.getItems();
142
+ const activeId = opts.getActiveId();
143
+ const bodies = readyBodies(items);
144
+ let anchor: HTMLElement | undefined;
145
+ for (const [item, body] of bodies) {
146
+ const isActive = activeId !== undefined && readConversationItemId(item) === activeId;
147
+ if (!body.hasAttribute('role')) body.setAttribute('role', 'button');
148
+ setAttr(body, 'aria-current', isActive ? 'true' : 'false');
149
+ const host = item as HTMLElement & { active?: boolean };
150
+ if (host.active !== isActive) host.active = isActive;
151
+ if (isActive) anchor = item;
152
+ }
153
+ rove(items, anchor ?? (bodies.keys().next().value as HTMLElement | undefined));
154
+ };
155
+
156
+ return {
157
+ sync,
158
+ handleClick(e) {
159
+ const item = itemFromEvent(e);
160
+ if (!item || menuInPath(e)) return;
161
+ opts.onSelect(readConversationItemId(item));
162
+ },
163
+ handleKeyDown(e) {
164
+ const items = opts.getItems();
165
+ if (items.length === 0) return;
166
+ const item = itemFromEvent(e);
167
+ if (e.key === 'Enter' || e.key === ' ') {
168
+ if (!item || menuInPath(e)) return;
169
+ e.preventDefault();
170
+ opts.onSelect(readConversationItemId(item));
171
+ return;
172
+ }
173
+ let next: HTMLElement | undefined;
174
+ const idx = item ? items.indexOf(item) : items.findIndex((i) => bodyOf(i).getAttribute('tabindex') === '0');
175
+ if (e.key === 'ArrowDown') next = items[Math.min(idx + 1, items.length - 1)];
176
+ else if (e.key === 'ArrowUp') next = items[Math.max(idx - 1, 0)];
177
+ else if (e.key === 'Home') next = items[0];
178
+ else if (e.key === 'End') next = items[items.length - 1];
179
+ if (!next) return;
180
+ e.preventDefault();
181
+ rove(items, next);
182
+ bodyOf(next).focus();
183
+ },
184
+ };
185
+ }
186
+
33
187
  export interface ConversationListProps {
34
188
  groups: ConversationGroup[];
35
189
  conversations: ConversationSummary[];
@@ -51,6 +205,20 @@ export interface ConversationListProps {
51
205
  /** Receive the imperative controller once mounted. The `kai-conversations`
52
206
  * facade uses it to focus / clear the internal search input. */
53
207
  controllerRef?: (controller: ConversationListController) => void;
208
+ /** Item mode (spec 2026-08-20 § 2a): the consumer's OWN rows, rendered inside
209
+ * a list region in place of the data rows. When set, the built-in search
210
+ * filter, grouping and empty/no-match states do not apply — the consumer's
211
+ * loop owns them — while the chrome (header, search box, new-chat, footer)
212
+ * still renders and `onSearchChange` still reports queries. The
213
+ * `kai-conversations` facade passes its default `<slot>` here when it detects
214
+ * `kai-conversation-item` children. */
215
+ items?: JSX.Element;
216
+ /** Keydown handler for the item-mode list region (the facade wires
217
+ * `createConversationItemsController.handleKeyDown`). */
218
+ itemsKeyDown?: (e: KeyboardEvent) => void;
219
+ /** Click handler for the item-mode list region (the facade wires
220
+ * `createConversationItemsController.handleClick`). */
221
+ itemsClick?: (e: MouseEvent) => void;
54
222
  class?: string;
55
223
  }
56
224
 
@@ -64,8 +232,10 @@ export interface ConversationListController {
64
232
  }
65
233
 
66
234
  export function ConversationList(props: ConversationListProps) {
67
- const [local] = splitProps(props, ['groups', 'conversations', 'activeId', 'onSelect', 'onNewChat', 'onToggleSidebar', 'header', 'footer', 'empty', 'compact', 'onSearchChange', 'controllerRef', 'class']);
235
+ const [local] = splitProps(props, ['groups', 'conversations', 'activeId', 'onSelect', 'onNewChat', 'onToggleSidebar', 'header', 'footer', 'empty', 'compact', 'onSearchChange', 'controllerRef', 'items', 'itemsKeyDown', 'itemsClick', 'class']);
68
236
  const [searchQuery, setSearchQuery] = createSignal('');
237
+ // Item mode: the consumer's own rows replace the data rendering wholesale.
238
+ const itemMode = createMemo(() => local.items != null);
69
239
  const isEmpty = createMemo(() => local.conversations.length === 0);
70
240
  // The search query is owned here; setQuery is the single mutation point so both
71
241
  // typing and the imperative clearSearch() notify the facade (→ kai-search).
@@ -133,7 +303,7 @@ export function ConversationList(props: ConversationListProps) {
133
303
  >
134
304
  {local.header}
135
305
  </Show>
136
- <Show when={!isEmpty()}>
306
+ <Show when={itemMode() || !isEmpty()}>
137
307
  <div class="px-3 pb-2">
138
308
  <div class="flex items-center gap-2 rounded-md bg-surface-strong px-2.5 py-1.5">
139
309
  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-muted-foreground"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
@@ -143,22 +313,51 @@ export function ConversationList(props: ConversationListProps) {
143
313
  </div>
144
314
  </div>
145
315
  </Show>
316
+ {/* Item mode: the consumer's own rows in a list region. The container's
317
+ filter/grouping/empty states do not apply — the consumer's loop owns
318
+ them (spec § 2a batteries boundary). */}
319
+ <Show when={itemMode()}>
320
+ <ScrollArea class="flex-1 px-2">
321
+ <div
322
+ role="list"
323
+ aria-label="Conversations"
324
+ part="items"
325
+ class="space-y-0.5 py-1"
326
+ onKeyDown={(e) => local.itemsKeyDown?.(e)}
327
+ onClick={(e) => local.itemsClick?.(e)}
328
+ >
329
+ {local.items}
330
+ </div>
331
+ </ScrollArea>
332
+ </Show>
146
333
  {/* list, or the empty state (replace) when there are no conversations. */}
147
334
  <Show
148
- when={!isEmpty()}
335
+ when={!itemMode() && !isEmpty()}
149
336
  fallback={
150
- <Show
151
- when={local.empty}
152
- fallback={
153
- <div class="flex flex-1 flex-col items-center justify-center gap-1 p-6 text-center text-sm text-muted-foreground">
154
- No conversations yet
155
- </div>
156
- }
157
- >
158
- {local.empty}
337
+ <Show when={!itemMode()}>
338
+ <Show
339
+ when={local.empty}
340
+ fallback={
341
+ <div class="flex flex-1 flex-col items-center justify-center gap-1 p-6 text-center text-sm text-muted-foreground">
342
+ No conversations yet
343
+ </div>
344
+ }
345
+ >
346
+ {local.empty}
347
+ </Show>
159
348
  </Show>
160
349
  }
161
350
  >
351
+ {/* F-04, decide loudly: a query matching nothing renders a VISIBLE
352
+ no-match state, keyed off the FILTERED count — distinct from the
353
+ zero-conversations empty state above, which keys off the unfiltered
354
+ list (and still owns the `empty` override). */}
355
+ <Show when={filteredConversations().length === 0}>
356
+ <div class="flex flex-1 flex-col items-center justify-center gap-1 p-6 text-center text-sm text-muted-foreground">
357
+ No conversations match your search
358
+ </div>
359
+ </Show>
360
+ <Show when={filteredConversations().length > 0}>
162
361
  <ScrollArea class="flex-1 px-2">
163
362
  <For each={local.groups}>
164
363
  {(group) => {
@@ -183,6 +382,7 @@ export function ConversationList(props: ConversationListProps) {
183
382
  </Show>
184
383
  </Show>
185
384
  </ScrollArea>
385
+ </Show>
186
386
  </Show>
187
387
  {/* footer (inject): a row below the list (account / settings / …). */}
188
388
  <Show when={local.footer}>
@@ -159,6 +159,10 @@ export function Embed(props: EmbedProps): JSX.Element {
159
159
  </button>
160
160
  }
161
161
  >
162
+ {/* `outline-none` is DELIBERATE here and must stay: a `tabindex="-1"`
163
+ wrapper focused programmatically after the user activates the
164
+ player, so keyboard context lands on the embed. Not a tab stop —
165
+ the iframe inside it takes focus from there. */}
162
166
  <div ref={(el) => (playerRegion = el)} tabindex="-1" class="absolute inset-0 outline-none">
163
167
  <iframe
164
168
  src={(resolved() as { value: ResolvedEmbed }).value.embedUrl}
@@ -748,19 +748,7 @@ function FieldRow(props: FieldRowProps): JSX.Element {
748
748
  .filter(Boolean)
749
749
  .join(' ') || undefined;
750
750
 
751
- const common = () => ({
752
- id,
753
- value: props.value(),
754
- field: props.field,
755
- disabled: props.disabled || props.field.readOnly === true,
756
- placeholder: placeholder(),
757
- required: props.required,
758
- invalid: Boolean(props.error()),
759
- describedBy: describedBy(),
760
- label: label(),
761
- onInput: props.onInput,
762
- onBlur: props.onBlur,
763
- });
751
+ const common = fieldCommon(props, id, placeholder, label, describedBy);
764
752
 
765
753
  // A nested fieldset / repeater / checkbox-group provide their own grouping
766
754
  // label, so the row's <label> is only rendered for simple single controls.
@@ -784,7 +772,7 @@ function FieldRow(props: FieldRowProps): JSX.Element {
784
772
  </p>
785
773
  </Show>
786
774
 
787
- <WidgetSwitch widget={widget()} common={common()} fieldKey={props.fieldKey} />
775
+ <WidgetSwitch widget={widget()} common={common} fieldKey={props.fieldKey} />
788
776
 
789
777
  <Show when={props.error()}>
790
778
  <p id={errorId} role="alert" class="text-xs text-destructive dark:text-red-400">
@@ -795,6 +783,45 @@ function FieldRow(props: FieldRowProps): JSX.Element {
795
783
  );
796
784
  }
797
785
 
786
+ /**
787
+ * The widget prop bag for one field row. **Internal** — exported only so the
788
+ * subscription contract can be pinned directly
789
+ * (`tests/components/form-field-subscriptions.test.tsx`); it is not re-exported
790
+ * from the package.
791
+ *
792
+ * GETTERS, NOT AN OBJECT FACTORY, and this is the whole point (K-D12b). The
793
+ * factory read `props.value()` alongside everything else and rebuilt the bag on
794
+ * every call, so a reader of ANY prop — `invalid`, say — subscribed to `value`
795
+ * too and re-ran on every keystroke. Combined with the `<input>` being rebuilt
796
+ * when its class expression re-ran (K-D12a), that is what made typing into a
797
+ * `kai-form` text field lose focus after each character.
798
+ *
799
+ * One stable object with one getter per prop: each reader tracks exactly the
800
+ * signal behind the prop it read. The object identity is constant, so the
801
+ * `WidgetSwitch` prop below never changes either.
802
+ */
803
+ export function fieldCommon(
804
+ props: FieldRowProps,
805
+ id: string,
806
+ placeholder: () => string | undefined,
807
+ label: () => string,
808
+ describedBy: () => string | undefined = () => undefined,
809
+ ): ReturnType<FieldRowCommon> {
810
+ return {
811
+ id,
812
+ get value() { return props.value(); },
813
+ get field() { return props.field; },
814
+ get disabled() { return props.disabled || props.field.readOnly === true; },
815
+ get placeholder() { return placeholder(); },
816
+ get required() { return props.required; },
817
+ get invalid() { return Boolean(props.error()); },
818
+ get describedBy() { return describedBy(); },
819
+ get label() { return label(); },
820
+ onInput: (v) => props.onInput(v),
821
+ onBlur: () => props.onBlur(),
822
+ };
823
+ }
824
+
798
825
  interface WidgetSwitchProps {
799
826
  widget: WidgetKind;
800
827
  fieldKey: string;
@@ -9,7 +9,17 @@ import type { ChatMessageAction, CustomAction, FeedbackVote, MessagePart, Messag
9
9
  import { useChatConfig, textClass } from "../primitives/chat-config";
10
10
  import { Reasoning, ReasoningTrigger, ReasoningContent } from "./reasoning";
11
11
  import { Tool } from "./tool";
12
- import { Attachments, Attachment, AttachmentPreview, AttachmentInfo } from "./attachments";
12
+ import {
13
+ Attachments,
14
+ Attachment,
15
+ AttachmentPreview,
16
+ AttachmentInfo,
17
+ AttachmentHoverCard,
18
+ AttachmentHoverCardTrigger,
19
+ AttachmentHoverCardContent,
20
+ getAttachmentLabel,
21
+ getMediaCategory,
22
+ } from "./attachments";
13
23
  import { Source, SourceTrigger, SourceContent, SourceList } from "./source";
14
24
  import { CardRenderer, type CardSchemaMap } from "./card-renderer";
15
25
  import type { CardComponentMap } from "../primitives/card-registry";
@@ -336,6 +346,13 @@ export interface MessageBodyProps {
336
346
  activeFeedback?: FeedbackVote;
337
347
  /** When true, the copy button shows its "copied" check icon. */
338
348
  copied?: boolean;
349
+ /** Whether this message is the one currently streaming. Forwarded to each
350
+ * reasoning part's `<Reasoning>` so the disclosure auto-opens while the
351
+ * model is thinking and settles back once the stream ends (F-21: without it
352
+ * the user watches a static collapsed "Reasoning" label for the whole
353
+ * thinking window). The caller owns the definition of "streaming" — for
354
+ * `ChatThread` that is `loading` + being the last assistant message. */
355
+ isStreaming?: boolean;
339
356
  /** INJECT slot projected at the TOP of the body — above the reasoning / tools /
340
357
  * content. A per-message header: a model-name label, a role + timestamp line.
341
358
  * In the `<kai-message>` shadow this is `<slot name="before-body" />`. */
@@ -477,15 +494,68 @@ function MessageBody(props: MessageBodyProps) {
477
494
  <Switch fallback={null}>
478
495
  <Match when={groupAs(group(), 'files')}>
479
496
  {(g) => (
480
- <Attachments variant="inline" class={props.isUser ? 'mb-2 justify-end' : 'mb-2'}>
497
+ /* `grid`, NOT `inline`. The inline chip gives an image a 20x20
498
+ preview — a thumbnail nobody can read — while the 96px tile
499
+ and the hover-card full preview both already existed here and
500
+ the thread used neither.
501
+
502
+ WHY GRID AND NOT LIST, since `list` shows the filename
503
+ outright: `list` stacks (`flex-col`), so a message carrying
504
+ four attachments becomes four full-width rows and pushes the
505
+ conversation off screen. `grid` is `flex-wrap w-fit`, so the
506
+ same four are one 96px-tall row of tiles. Density is what
507
+ keeps a thread readable, and it is the whole reason the
508
+ thread is not the composer's chip strip.
509
+
510
+ A grid tile draws no filename of its own, and hiding one
511
+ behind the hover card is not good enough: that serves a
512
+ pointer and nothing else. So `<AttachmentInfo>` renders a
513
+ VISIBLE truncated caption on every non-image tile — no hover,
514
+ no focus, no tap required — and the hover card is an upgrade
515
+ to the full name and media type rather than the only way to
516
+ get either. */
517
+ <Attachments variant="grid" class={props.isUser ? 'mb-2 ml-auto' : 'mb-2'}>
481
518
  {/* Reference-keyed <For> is right HERE: the run's part objects
482
519
  are carried over untouched by the folds, and an attachment
483
520
  holds no state worth preserving. */}
484
521
  <For each={g().parts}>
485
522
  {(fp) => (
486
523
  <Attachment data={fp.attachment}>
487
- <AttachmentPreview />
488
- <AttachmentInfo />
524
+ <AttachmentHoverCard>
525
+ <AttachmentHoverCardTrigger class="block size-full">
526
+ <AttachmentPreview />
527
+ <AttachmentInfo />
528
+ </AttachmentHoverCardTrigger>
529
+ <AttachmentHoverCardContent>
530
+ {/* An image gets the full preview; everything else
531
+ gets the name and type the tile could not fit. */}
532
+ <Show
533
+ when={
534
+ getMediaCategory(fp.attachment) === 'image' &&
535
+ fp.attachment.type === 'file' &&
536
+ fp.attachment.url
537
+ }
538
+ fallback={
539
+ <>
540
+ <div class="text-body font-medium">
541
+ {getAttachmentLabel(fp.attachment)}
542
+ </div>
543
+ <Show when={fp.attachment.mediaType}>
544
+ <div class="text-muted-foreground text-caption">
545
+ {fp.attachment.mediaType}
546
+ </div>
547
+ </Show>
548
+ </>
549
+ }
550
+ >
551
+ <img
552
+ alt={getAttachmentLabel(fp.attachment)}
553
+ class="block max-h-64 max-w-xs rounded object-contain"
554
+ src={fp.attachment.url}
555
+ />
556
+ </Show>
557
+ </AttachmentHoverCardContent>
558
+ </AttachmentHoverCard>
489
559
  </Attachment>
490
560
  )}
491
561
  </For>
@@ -553,7 +623,7 @@ function MessageBody(props: MessageBodyProps) {
553
623
  <Match when={carrierOnly()}>{null}</Match>
554
624
  <Match when={shownReasoning()}>
555
625
  {(p) => (
556
- <Reasoning class="mb-2 w-full">
626
+ <Reasoning class="mb-2 w-full" isStreaming={props.isStreaming}>
557
627
  <ReasoningTrigger>{p().label ?? 'Reasoning'}</ReasoningTrigger>
558
628
  <ReasoningContent markdown>{p().text}</ReasoningContent>
559
629
  </Reasoning>
@@ -1,6 +1,7 @@
1
1
  import { splitProps, Show, For, createEffect, on } from 'solid-js';
2
+ import { Dynamic } from 'solid-js/web';
2
3
  import { cn } from '../utils/cn';
3
- import { useTextStream } from '../primitives/use-text-stream';
4
+ import { useTextStream, defaultFadeDuration, defaultSegmentDelay } from '../primitives/use-text-stream';
4
5
 
5
6
  export type Mode = 'typewriter' | 'fade';
6
7
 
@@ -30,6 +31,7 @@ function ResponseStream(props: ResponseStreamProps) {
30
31
  speed: speed(),
31
32
  characterChunkSize: local.characterChunkSize,
32
33
  fadeDuration: local.fadeDuration,
34
+ segmentDelay: local.segmentDelay,
33
35
  });
34
36
 
35
37
  createEffect(on(
@@ -39,15 +41,20 @@ function ResponseStream(props: ResponseStreamProps) {
39
41
  }
40
42
  ));
41
43
 
44
+ // Deferred: `isComplete` INITIALISES true (nothing has streamed yet), so an
45
+ // undeferred first run would announce completion at mount — the same
46
+ // no-event-on-mount contract kai-tool pins for kai-open-change. Only the
47
+ // false -> true transition of a real stream may call onComplete.
42
48
  createEffect(on(
43
49
  () => stream.isComplete(),
44
50
  (complete) => {
45
51
  if (complete) local.onComplete?.();
46
- }
52
+ },
53
+ { defer: true }
47
54
  ));
48
55
 
49
56
  const fadeStyle = () => {
50
- const dur = local.fadeDuration ?? Math.round(1000 / Math.sqrt(Math.min(100, Math.max(1, speed()))));
57
+ const dur = local.fadeDuration ?? defaultFadeDuration(speed());
51
58
  return `
52
59
  @keyframes fadeIn {
53
60
  from { opacity: 0; }
@@ -66,12 +73,11 @@ function ResponseStream(props: ResponseStreamProps) {
66
73
 
67
74
  const segDelay = () => {
68
75
  if (typeof local.segmentDelay === 'number') return Math.max(0, local.segmentDelay);
69
- const normalizedSpeed = Math.min(100, Math.max(1, speed()));
70
- return Math.max(1, Math.round(100 / Math.sqrt(normalizedSpeed)));
76
+ return defaultSegmentDelay(speed());
71
77
  };
72
78
 
73
79
  return (
74
- <div class={local.class}>
80
+ <Dynamic component={local.as ?? 'div'} class={local.class}>
75
81
  <Show
76
82
  when={mode() === 'fade'}
77
83
  fallback={<>{stream.displayedText()}</>}
@@ -96,7 +102,7 @@ function ResponseStream(props: ResponseStreamProps) {
96
102
  </For>
97
103
  </div>
98
104
  </Show>
99
- </div>
105
+ </Dynamic>
100
106
  );
101
107
  }
102
108
 
@@ -144,6 +144,11 @@ export function Screen(props: ScreenProps) {
144
144
  data-expanded={presence.state() === 'open' ? '' : undefined}
145
145
  data-closed={presence.state() === 'closed' ? '' : undefined}
146
146
  onKeyDown={onKeyDown}
147
+ // `outline-none` is DELIBERATE here and must stay: this is the
148
+ // `tabindex="-1"` screen container, focused programmatically on open so
149
+ // keyboard and screen-reader context follows the screen. It is not a
150
+ // tab stop, so a ring around the whole surface would be noise. Its
151
+ // interactive children carry their own.
147
152
  class={cn(
148
153
  'absolute inset-0 z-40 flex flex-col bg-background text-foreground outline-none',
149
154
  'animate-in fade-in-0 slide-in-from-bottom-2 motion-reduce:animate-none',
@@ -17,6 +17,13 @@ function ScrollButton(props: ScrollButtonProps) {
17
17
  variant={props.variant ?? 'outline'}
18
18
  size={props.size ?? 'sm'}
19
19
  aria-label="Scroll to bottom"
20
+ // At the bottom of the thread this button is fully transparent and
21
+ // pointer-inert, but it stayed in the tab order — a keyboard user landed
22
+ // on a control they could not see, with a focus ring painted on nothing.
23
+ // Take it out of the tab order and hide it from assistive tech while it
24
+ // is invisible; both revert the moment it animates back in.
25
+ tabindex={isAtBottom() ? -1 : 0}
26
+ aria-hidden={isAtBottom() ? 'true' : undefined}
20
27
  class={cn(
21
28
  'h-10 w-10 rounded-full transition-all duration-150 ease-out',
22
29
  !isAtBottom()
@@ -320,11 +320,17 @@ function anchorStyle(position: ToastPosition, r: TargetRect): Record<string, str
320
320
  }
321
321
  }
322
322
 
323
+ /** The overlay layer both region roots sit on (F-20). One token, one default:
324
+ * your app chrome must stay below the toast layer; set `--kai-toast-z`
325
+ * (default 100) on the host to move the layer. */
326
+ const TOAST_Z = 'var(--kai-toast-z, 100)';
327
+
323
328
  /**
324
329
  * Renders a stack of toasts at a viewport edge. Caps the visible count at `max`
325
330
  * (newest on top); overflow waits in a queue and is promoted as visible toasts
326
331
  * leave. `aria-live=polite` / `role=region` so screen readers announce new
327
- * toasts without stealing focus.
332
+ * toasts without stealing focus. Your app chrome must stay below the toast
333
+ * layer; set `--kai-toast-z` (default 100) on the host to move the layer.
328
334
  */
329
335
  export function ToastRegion(props: ToastRegionProps) {
330
336
  const max = () => props.max ?? 3;
@@ -425,12 +431,12 @@ export function ToastRegion(props: ToastRegionProps) {
425
431
  aria-live="polite"
426
432
  data-stack="expanded"
427
433
  class={cn(
428
- 'pointer-events-none fixed z-[100] flex flex-col gap-2',
434
+ 'pointer-events-none fixed flex flex-col gap-2',
429
435
  anchor()
430
436
  ? ANCHOR_FLEX[position()]
431
437
  : cn('max-w-[min(30rem,calc(100vw-2rem))]', POSITION_CLASSES[position()]),
432
438
  )}
433
- style={anchor() ? anchorStyle(position(), anchor()!) : undefined}
439
+ style={{ 'z-index': TOAST_Z, ...(anchor() ? anchorStyle(position(), anchor()!) : undefined) }}
434
440
  >
435
441
  <For each={visible()}>
436
442
  {(item) => (
@@ -456,8 +462,8 @@ export function ToastRegion(props: ToastRegionProps) {
456
462
  onPointerLeave={() => setHovered(false)}
457
463
  onFocusIn={() => setFocused(true)}
458
464
  onFocusOut={() => setFocused(false)}
459
- class={cn('pointer-events-none fixed z-[100]', !anchor() && POSITION_CLASSES[position()])}
460
- style={anchor() ? anchorStyle(position(), anchor()!) : undefined}
465
+ class={cn('pointer-events-none fixed', !anchor() && POSITION_CLASSES[position()])}
466
+ style={{ 'z-index': TOAST_Z, ...(anchor() ? anchorStyle(position(), anchor()!) : undefined) }}
461
467
  >
462
468
  {/* Relative stage with an explicit height so it's ONE continuous hover
463
469
  surface (pointer-events-auto) — no flicker when the cursor crosses gaps
@@ -38,6 +38,10 @@ export interface VoiceInputProps {
38
38
  /** Receive the imperative controller once mounted. The `<kai-voice-input>`
39
39
  * facade forwards these as element methods (start/stop). */
40
40
  controllerRef?: (controller: VoiceInputController) => void;
41
+ /** A recognition session failed or produced nothing. `error` is the platform
42
+ * error code, the thrown exception's name, or `no-result` when the session
43
+ * ended with no error and no text. The facade maps this to kai-voice-error. */
44
+ onError?: (detail: { source: 'recognition'; error: string; message: string }) => void;
41
45
  }
42
46
 
43
47
  export function VoiceInput(props: VoiceInputProps) {
@@ -59,15 +63,43 @@ export function VoiceInput(props: VoiceInputProps) {
59
63
 
60
64
  // Native path: SpeechRecognition resolves with the final transcript when it
61
65
  // ends (click again or controller.stop()). Final text → onTranscription.
66
+ // Every other outcome reaches onError, so a session can never end in silence
67
+ // (W4 symptom 1: runtime errors set a signal nothing read, and an empty
68
+ // result fired no event at all).
62
69
  async function beginRecognition() {
70
+ let errored = false;
63
71
  try {
64
72
  const text = await speech.start({
65
73
  lang: props.lang,
66
74
  onInterim: props.interim ? (t) => props.onInterim?.(t) : undefined,
75
+ onError: (error, message) => {
76
+ errored = true;
77
+ props.onError?.({ source: 'recognition', error, message });
78
+ },
67
79
  });
68
- if (text.trim()) local.onTranscription(text.trim());
69
- } catch {
70
- /* surfaced via speech.error; the mic returns to idle */
80
+ if (text.trim()) {
81
+ local.onTranscription(text.trim());
82
+ } else if (!errored) {
83
+ // No error, no text: the session ended cleanly having heard nothing.
84
+ // Reported through the SAME error event (as `no-result`) rather than an
85
+ // empty kai-transcription, so kai-transcription keeps its contract
86
+ // (final text exists) and one listener covers every failed session; the
87
+ // code distinguishes "user said nothing" from "recognition broke".
88
+ props.onError?.({
89
+ source: 'recognition',
90
+ error: 'no-result',
91
+ message: 'Speech recognition ended without capturing any text',
92
+ });
93
+ }
94
+ } catch (err) {
95
+ // start() itself threw (unsupported, or the platform refused to start).
96
+ if (!errored) {
97
+ props.onError?.({
98
+ source: 'recognition',
99
+ error: err instanceof Error ? err.name : 'unknown',
100
+ message: err instanceof Error ? err.message : String(err),
101
+ });
102
+ }
71
103
  }
72
104
  }
73
105