@kitn.ai/ui 0.25.1 → 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 +5 -7
  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 +27 -7
  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
@@ -4,7 +4,7 @@ import { normalizeVolumeBands } from '../../primitives/audio-bands';
4
4
  import { useSequencer } from '../../primitives/use-sequencer';
5
5
  import { gridSequence } from '../../primitives/visualizer-sequences';
6
6
  import { GRID_CELL, GRID_GAP, defaultGridCount } from './sizes';
7
- import type { VariantProps } from './variant-bar';
7
+ import { amplitudeRenderState, type VariantProps } from './variant-bar';
8
8
 
9
9
  /*
10
10
  * NOTE: The speaking thresholds below DIVERGE from upstream's GridCell math,
@@ -64,20 +64,25 @@ export function GridVisualizer(
64
64
  const interval = () => props.interval ?? 100;
65
65
  const items = () => Array.from({ length: rows() * cols() }, (_, i) => i);
66
66
 
67
+ // The state the rendering follows: `listeningAmplitude` folds `listening`
68
+ // into the speaking presentation. See amplitudeRenderState in variant-bar.
69
+ const renderState = () => amplitudeRenderState(props.state, props.listeningAmplitude);
70
+
67
71
  const tick = useSequencer(() =>
68
72
  // Speaking is driven by audio, not the clock; freezing also parks it.
69
- props.frozen || props.state === 'speaking' ? Infinity : interval(),
73
+ props.frozen || renderState() === 'speaking' ? Infinity : interval(),
70
74
  );
71
75
 
72
- const sequence = () => gridSequence(props.state, rows(), cols(), props.spread);
76
+ const sequence = () => gridSequence(renderState(), rows(), cols(), props.spread);
73
77
  const active = () => sequence()[tick() % sequence().length] ?? { x: -1, y: -1 };
74
78
 
75
- // Bands only mean anything while speaking. Everywhere else the sequence is
76
- // the whole story, so a stale level never leaks into a scripted state (same
77
- // guard as BarVisualizer, so the render-prop's `value` means one thing
78
- // across every variant).
79
+ // Bands only mean anything while speaking (or listening under the
80
+ // `listeningAmplitude` opt-in, folded in by renderState). Everywhere else
81
+ // the sequence is the whole story, so a stale level never leaks into a
82
+ // scripted state (same guard as BarVisualizer, so the render-prop's `value`
83
+ // means one thing across every variant).
79
84
  const levels = () =>
80
- props.state === 'speaking' ? normalizeVolumeBands(props.bands, cols()) : new Array(cols()).fill(0);
85
+ renderState() === 'speaking' ? normalizeVolumeBands(props.bands, cols()) : new Array(cols()).fill(0);
81
86
 
82
87
  /**
83
88
  * While speaking, a cell lights when its column's level clears a threshold
@@ -87,7 +92,7 @@ export function GridVisualizer(
87
92
  * both per the divergence note at the top of this file.
88
93
  */
89
94
  function isLit(index: number): boolean {
90
- if (props.state === 'speaking') {
95
+ if (renderState() === 'speaking') {
91
96
  const y = Math.floor(index / cols());
92
97
  const mid = Math.floor(rows() / 2);
93
98
  const scaled = (Math.abs(mid - y) / (mid + 1)) * SPEECH_LEVEL_CEILING;
@@ -100,13 +105,13 @@ export function GridVisualizer(
100
105
  // upstream rests one stationary centre cell here. 'disconnected' (first-
101
106
  // class 2026-08-10) mirrors idle's dark grid for now, pending the LiveKit
102
107
  // disconnected-state measurement.
103
- if (props.state === 'idle' || props.state === 'disconnected') return false;
108
+ if (renderState() === 'idle' || renderState() === 'disconnected') return false;
104
109
  return active().x === index % cols() && active().y === Math.floor(index / cols());
105
110
  }
106
111
 
107
112
  /** Snap on, fade off: highlighted cells transition 10x faster than they decay. */
108
113
  function transition(index: number): string {
109
- if (props.state === 'speaking') return '150ms';
114
+ if (renderState() === 'speaking') return '150ms';
110
115
  return `${interval() / (isLit(index) ? 1000 : 100)}s`;
111
116
  }
112
117
 
@@ -4,7 +4,7 @@ import { normalizeVolumeBands } from '../../primitives/audio-bands';
4
4
  import { useSequencer } from '../../primitives/use-sequencer';
5
5
  import { radialSequence, radialInterval } from '../../primitives/visualizer-sequences';
6
6
  import { CONTAINER_HEIGHT, RADIAL_RADIUS, defaultRadialBarCount } from './sizes';
7
- import type { VariantProps } from './variant-bar';
7
+ import { amplitudeRenderState, type VariantProps } from './variant-bar';
8
8
 
9
9
  /**
10
10
  * Bars arranged around a circle, growing outward with the audio.
@@ -40,10 +40,14 @@ export function RadialVisualizer(
40
40
  // many bars are on the ring.
41
41
  const dotSize = () => (radius() * Math.PI) / count();
42
42
 
43
+ // The state the rendering follows: `listeningAmplitude` folds `listening`
44
+ // into the speaking presentation. See amplitudeRenderState in variant-bar.
45
+ const renderState = () => amplitudeRenderState(props.state, props.listeningAmplitude);
46
+
43
47
  // Frozen (reduced motion) parks the sequence on frame 0 rather than stopping
44
48
  // the component: the shape still reads, it just does not move.
45
- const tick = useSequencer(() => (props.frozen ? Infinity : radialInterval(props.state)));
46
- const sequence = () => radialSequence(props.state, count());
49
+ const tick = useSequencer(() => (props.frozen ? Infinity : radialInterval(renderState())));
50
+ const sequence = () => radialSequence(renderState(), count());
47
51
 
48
52
  // `thinking` parks the sequencer itself (radialInterval returns Infinity)
49
53
  // and spins the whole ring in CSS instead, so every bar reads as lit rather
@@ -52,20 +56,21 @@ export function RadialVisualizer(
52
56
  // purpose: they interleave the ring so it still looks even while rotating,
53
57
  // not so a subset blinks).
54
58
  const highlighted = () =>
55
- props.state === 'thinking'
59
+ renderState() === 'thinking'
56
60
  ? Array.from({ length: count() }, (_, i) => i)
57
61
  : (sequence()[tick() % sequence().length] ?? []);
58
62
 
59
- // Bands only mean anything while speaking. Everywhere else the sequence is
60
- // the whole story, so a stale level never leaks into a scripted state (same
61
- // guard as the other two variants, so the render-prop's `value` means one
62
- // thing across every variant).
63
+ // Bands only mean anything while speaking (or listening under the
64
+ // `listeningAmplitude` opt-in, folded in by renderState). Everywhere else
65
+ // the sequence is the whole story, so a stale level never leaks into a
66
+ // scripted state (same guard as the other two variants, so the
67
+ // render-prop's `value` means one thing across every variant).
63
68
  const levels = () =>
64
- props.state === 'speaking'
69
+ renderState() === 'speaking'
65
70
  ? normalizeVolumeBands(props.bands, count())
66
71
  : new Array(count()).fill(0);
67
72
 
68
- const spinning = () => props.state === 'thinking' && !props.frozen;
73
+ const spinning = () => renderState() === 'thinking' && !props.frozen;
69
74
 
70
75
  return (
71
76
  <div
@@ -3,6 +3,7 @@ import { ShaderCanvas, hexToRgb, DEFAULT_SHADER_COLOR } from './shader-canvas';
3
3
  import { createTween } from '../../primitives/create-tween';
4
4
  import { waveTargets } from '../../primitives/visualizer-sequences';
5
5
  import { CONTAINER_HEIGHT } from './sizes';
6
+ import { amplitudeRenderState } from './variant-bar';
6
7
  import type { ShaderVariantProps } from './index';
7
8
  import waveShader from './wave.glsl';
8
9
 
@@ -26,7 +27,11 @@ export default function WaveVisualizer(props: ShaderVariantProps): JSX.Element {
26
27
  const frequency = createTween(0);
27
28
  const opacity = createTween(1);
28
29
 
29
- const targets = () => waveTargets(props.state);
30
+ // The state the rendering follows: `listeningAmplitude` folds `listening`
31
+ // into the speaking presentation (live-volume amplitude/frequency). See
32
+ // amplitudeRenderState in variant-bar. `data-kai-state` keeps the real state.
33
+ const renderState = () => amplitudeRenderState(props.state, props.listeningAmplitude);
34
+ const targets = () => waveTargets(renderState());
30
35
 
31
36
  // Reduced motion: land on the target immediately and skip every pulse.
32
37
  const transition = () =>
@@ -61,7 +66,7 @@ export default function WaveVisualizer(props: ShaderVariantProps): JSX.Element {
61
66
  // effect, one writer, ordering can no longer matter. Same effect-race
62
67
  // class as b5795ac's shared() finding.
63
68
  createEffect(() => {
64
- if (props.state === 'speaking') {
69
+ if (renderState() === 'speaking') {
65
70
  // Live volume drives amplitude and frequency instantly while
66
71
  // speaking, so the line never lags the audio -- and lands at the
67
72
  // override immediately on re-entry, matching upstream's same-commit
@@ -28,7 +28,10 @@ function ChatContainerRoot(props: ChatContainerRootProps) {
28
28
  <ChatContainerContext.Provider value={{ isAtBottom, scrollToBottom }}>
29
29
  <div
30
30
  ref={ref}
31
- class={cn('flex flex-col overflow-y-auto', local.class)}
31
+ // `kai-focus-inset`: the log fills a clipping container, so the default
32
+ // focus outline (drawn outside the border box) was clipped away
33
+ // entirely — this region was tabbable with no visible indicator.
34
+ class={cn('flex flex-col overflow-y-auto kai-focus-inset', local.class)}
32
35
  role="log"
33
36
  // Keyboard users must be able to scroll the conversation even when no
34
37
  // message contains a focusable control (WCAG 2.1.1 — axe
@@ -120,8 +120,8 @@ export interface ChatThreadProps {
120
120
  accept?: MediaTypeFilter;
121
121
  /** Files the composer refused because `accept` excluded them. */
122
122
  onAttachmentsRejected?: (rejected: RejectedAttachment[]) => void;
123
- /** Show a Search (Globe) button in the input toolbar; fires a `search` event. */
124
- search?: boolean;
123
+ /** Show a web-search (Globe) button in the input toolbar; calls `onWebSearch`. */
124
+ webSearch?: boolean;
125
125
  /** Show a Voice (Mic) button in the input toolbar; fires a `voice` event. */
126
126
  voice?: boolean;
127
127
  /** Rich entity triggers. Each `{ char, kind, items }` opens a caret-anchored
@@ -140,7 +140,7 @@ export interface ChatThreadProps {
140
140
  onSuggestionClick?: (value: string) => void;
141
141
  onModelChange?: (modelId: string) => void;
142
142
  onMessageAction?: (detail: MessageActionDetail) => void;
143
- onSearch?: () => void;
143
+ onWebSearch?: () => void;
144
144
  onVoice?: () => void;
145
145
  /** Receive the imperative controller once mounted. The kai-chat facade forwards
146
146
  * these as element methods (focus/clear/send/scrollToBottom). */
@@ -194,6 +194,30 @@ export interface ChatThreadController {
194
194
  * Contract: `id` must be unique per message. It already had to be — the
195
195
  * feedback/copy state above this list is keyed by it.
196
196
  */
197
+ /** How an ASSISTANT row aligns its parts across the column (K-D11).
198
+ *
199
+ * `stretch`, not `start`. An assistant turn is a `flex flex-col` box, so under
200
+ * `items-start` every part is a flex item with a fit-content cross size —
201
+ * `min(max-content, column)`. Prose is wider than the column so a text bubble
202
+ * looked right, and a generative-UI card was as wide as its widest button: the
203
+ * ops-console parameters form measured 285px inside a 768px column while the
204
+ * approval card beside it filled all 768. A card in a chat thread filling its
205
+ * column is a fact about the medium, and no consumer can reach it — the card
206
+ * element is created inside `<kai-chat>`'s shadow root.
207
+ *
208
+ * Stretch rather than `w-full` on the cards, for two reasons: it is one lever
209
+ * instead of one per card surface (the Solid `Card` root AND every `kai-*`
210
+ * card host, whose shadow wrapper is `display: contents`), and stretching only
211
+ * applies where the cross size is `auto`, so a part that states its own width
212
+ * is untouched — `Attachments variant="grid"` stays `w-fit`. `Tool` and
213
+ * `Reasoning` already asked for `w-full` explicitly; this is the same
214
+ * intention, applied once.
215
+ *
216
+ * USER rows keep `items-end`: their bubble is `max-w-[85%]` and right aligned,
217
+ * and stretching it would break both.
218
+ */
219
+ const ASSISTANT_ALIGN = 'items-stretch';
220
+
197
221
  export function ChatThread(props: ChatThreadProps) {
198
222
  const outer = useChatConfig();
199
223
  const reveal = () => (props.actionsReveal === 'hover' ? 'hover' : 'always');
@@ -335,6 +359,11 @@ export function ChatThread(props: ChatThreadProps) {
335
359
  const body = (
336
360
  <MessageBody
337
361
  parts={m().parts}
362
+ /* F-21: streaming-ness = the thread's ONE existing
363
+ loading signal + being the last message and an
364
+ assistant turn. No second streaming source; the
365
+ reasoning disclosure auto-opens on it. */
366
+ isStreaming={props.loading === true && m().role === 'assistant' && i() === props.messages.length - 1}
338
367
  cardTypes={props.cardTypes}
339
368
  cardSchemas={props.cardSchemas}
340
369
  isUser={m().role === 'user'}
@@ -356,7 +385,7 @@ export function ChatThread(props: ChatThreadProps) {
356
385
  <Show
357
386
  when={m().avatar}
358
387
  fallback={
359
- <Message role={m().role} class={`${rowGroup()}${m().role === 'user' ? 'flex-col items-end' : 'flex-col items-start'}`}>
388
+ <Message role={m().role} class={`${rowGroup()}${m().role === 'user' ? 'flex-col items-end' : `flex-col ${ASSISTANT_ALIGN}`}`}>
360
389
  {body}
361
390
  </Message>
362
391
  }
@@ -364,7 +393,7 @@ export function ChatThread(props: ChatThreadProps) {
364
393
  {(av) => (
365
394
  <Message role={m().role} class={rowGroup()}>
366
395
  <MessageAvatar src={av().src ?? ''} alt={av().alt ?? ''} fallback={av().fallback} />
367
- <div class={`flex min-w-0 flex-1 flex-col ${m().role === 'user' ? 'items-end' : 'items-start'}`}>
396
+ <div class={`flex min-w-0 flex-1 flex-col ${m().role === 'user' ? 'items-end' : ASSISTANT_ALIGN}`}>
368
397
  {body}
369
398
  </div>
370
399
  </Message>
@@ -401,11 +430,11 @@ export function ChatThread(props: ChatThreadProps) {
401
430
  value={current()} placeholder={props.placeholder} loading={props.loading === true}
402
431
  suggestions={visibleSuggestions()} attachments={attachments()}
403
432
  accept={props.accept} onAttachmentsRejected={props.onAttachmentsRejected}
404
- search={props.search === true} voice={props.voice === true}
433
+ webSearch={props.webSearch === true} voice={props.voice === true}
405
434
  triggers={props.triggers} kindIcons={props.kindIcons}
406
435
  onValueChange={handleChange} onSubmit={handleSubmit} onSuggestionClick={handleSuggestionClick}
407
436
  onAttachmentsChange={(a) => { setAttachments(a); props.onAttachmentsChange?.(a); }}
408
- onSearch={() => props.onSearch?.()} onVoice={() => props.onVoice?.()}
437
+ onWebSearch={() => props.onWebSearch?.()} onVoice={() => props.onVoice?.()}
409
438
  />
410
439
  }
411
440
  >
@@ -1,4 +1,5 @@
1
- import { type JSX, splitProps, createResource, Show } from 'solid-js';
1
+ import { type JSX, splitProps, createResource, createSignal, onCleanup, Show } from 'solid-js';
2
+ import { Copy, Check } from 'lucide-solid';
2
3
  import { cn } from '../utils/cn';
3
4
  import { useChatConfig } from '../primitives/chat-config';
4
5
  import { highlight, isCodeHighlightingEnabled } from '../primitives/highlighter';
@@ -7,10 +8,69 @@ import { highlight, isCodeHighlightingEnabled } from '../primitives/highlighter'
7
8
 
8
9
  export interface CodeBlockProps extends JSX.HTMLAttributes<HTMLDivElement> {
9
10
  children?: JSX.Element;
11
+ /**
12
+ * Render a copy button in a header row above the code.
13
+ *
14
+ * DEFAULT OFF, and deliberately. This component is shared by three surfaces —
15
+ * `markdown.tsx` (every fenced block in every assistant message), `artifact.tsx`
16
+ * (the code panel), and the `<kai-code-block>` facade — and only the ELEMENT is
17
+ * documented as shipping a copy button. Defaulting on would add one to every code
18
+ * block in every message, a kit-wide visible change. The facade opts in; the other
19
+ * two are untouched, which `tests/elements/code-block.test.tsx` pins both
20
+ * behaviourally and at the call sites.
21
+ */
22
+ copy?: boolean;
23
+ /**
24
+ * The exact text the copy button puts on the clipboard.
25
+ *
26
+ * Named for what it DOES rather than what it holds, because the distinction is the
27
+ * whole contract: by the time the button is clicked the source has been through
28
+ * Shiki, so anything read back out of the DOM is either `<span>`-laden HTML or a
29
+ * `textContent` reconstruction — and the reconstruction is where tabs, blank lines
30
+ * and trailing newlines quietly change. This prop never goes near the highlighter.
31
+ */
32
+ copyText?: string;
33
+ }
34
+
35
+ /** How long the button stays in its acknowledged state. Matches MessageCopyButton. */
36
+ const COPIED_MS = 2000;
37
+
38
+ function CodeBlockCopyButton(props: { text: string }) {
39
+ const [copied, setCopied] = createSignal(false);
40
+ let timer: ReturnType<typeof setTimeout> | undefined;
41
+ // A click just before unmount would otherwise set state on a disposed owner.
42
+ onCleanup(() => clearTimeout(timer));
43
+
44
+ return (
45
+ <button
46
+ type="button"
47
+ part="copy"
48
+ // The name carries the state, so a screen-reader user gets the same
49
+ // acknowledgement the icon swap gives a sighted one. The glyph is decorative.
50
+ aria-label={copied() ? 'Copied' : 'Copy code'}
51
+ class={cn(
52
+ 'inline-flex items-center justify-center rounded-md p-1.5',
53
+ 'text-muted-foreground transition-colors hover:bg-muted hover:text-foreground',
54
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
55
+ )}
56
+ onClick={() => {
57
+ // Same mechanism as MessageCopyButton. A rejected promise (no permission,
58
+ // insecure context) must not become an unhandled rejection.
59
+ void Promise.resolve(navigator.clipboard?.writeText(props.text)).catch(() => {});
60
+ setCopied(true);
61
+ clearTimeout(timer);
62
+ timer = setTimeout(() => setCopied(false), COPIED_MS);
63
+ }}
64
+ >
65
+ <Show when={copied()} fallback={<Copy size={14} aria-hidden="true" />}>
66
+ <Check size={14} aria-hidden="true" class="text-emerald-500" />
67
+ </Show>
68
+ </button>
69
+ );
10
70
  }
11
71
 
12
72
  function CodeBlock(props: CodeBlockProps) {
13
- const [local, rest] = splitProps(props, ['children', 'class']);
73
+ const [local, rest] = splitProps(props, ['children', 'class', 'copy', 'copyText']);
14
74
  return (
15
75
  <div
16
76
  class={cn(
@@ -23,6 +83,15 @@ function CodeBlock(props: CodeBlockProps) {
23
83
  )}
24
84
  {...rest}
25
85
  >
86
+ <Show when={local.copy}>
87
+ {/* A header row, NOT an overlay on the code: the code region scrolls
88
+ horizontally, so a button inside it would ride off-screen with the code.
89
+ Always visible — hover-only affordances fail touch outright and are
90
+ undiscoverable everywhere else. */}
91
+ <CodeBlockGroup class="justify-end border-b border-border px-2 py-1">
92
+ <CodeBlockCopyButton text={local.copyText ?? ''} />
93
+ </CodeBlockGroup>
94
+ </Show>
26
95
  {local.children}
27
96
  </div>
28
97
  );
@@ -62,7 +131,11 @@ function CodeBlockCode(props: CodeBlockCodeProps) {
62
131
 
63
132
  const classNames = () =>
64
133
  cn(
65
- 'w-full overflow-x-auto [&>pre]:px-4 [&>pre]:py-4',
134
+ // `kai-focus-inset`: this region fills a rounded `overflow: clip` wrapper,
135
+ // so the default focus outline (offset 2px, i.e. entirely outside the
136
+ // border box) was clipped away and keyboard users saw nothing. The inset
137
+ // ring is drawn inside the box, where the wrapper cannot erase it.
138
+ 'w-full overflow-x-auto kai-focus-inset [&>pre]:px-4 [&>pre]:py-4',
66
139
  codeTextSize(),
67
140
  local.class
68
141
  );
@@ -102,4 +175,7 @@ function CodeBlockGroup(props: CodeBlockGroupProps) {
102
175
  );
103
176
  }
104
177
 
178
+ // CodeBlockCopyButton is deliberately NOT exported: `src/index.ts` re-exports this
179
+ // module's public trio, and adding a fourth symbol here that index.ts does not carry
180
+ // would create a half-public component. It is an implementation detail of `copy`.
105
181
  export { CodeBlock, CodeBlockCode, CodeBlockGroup };
@@ -799,9 +799,16 @@ export function Composer(props: ComposerProps): JSX.Element {
799
799
  // This is covered by updateTriggerState() in handleInput.
800
800
 
801
801
  const maxH = () => props.maxHeight ?? 240;
802
+ // No `outline-none` in the default class. This element is `contenteditable`
803
+ // with `role="textbox"` — a real tab stop, and the primary one in the kit —
804
+ // and suppressing the outline left keyboard users with no indication that the
805
+ // message input was focused. `kai-focus-inset` draws the ring inside the
806
+ // border box, because the editable fills a rounded, clipping composer shell
807
+ // that would otherwise erase an outset one. A consumer passing
808
+ // `editableClass` still replaces this wholesale.
802
809
  const editableCls = () =>
803
810
  props.editableClass ??
804
- 'text-foreground min-h-[44px] w-full overflow-y-auto outline-none whitespace-pre-wrap break-words';
811
+ 'text-foreground min-h-[44px] w-full overflow-y-auto kai-focus-inset whitespace-pre-wrap break-words';
805
812
 
806
813
  const inner = (
807
814
  <>
@@ -1,4 +1,4 @@
1
- import { Show, splitProps, createMemo } from 'solid-js';
1
+ import { Show, splitProps, createMemo, type JSX } from 'solid-js';
2
2
  import { MessageSquare } from 'lucide-solid';
3
3
  import { cn } from '../utils/cn';
4
4
  import type { ConversationSummary } from '../types';
@@ -31,12 +31,122 @@ export function relativeTimeShort(iso?: string, now: number = Date.now()): strin
31
31
  return `${Math.floor(hours / 24)}d ago`;
32
32
  }
33
33
 
34
+ /**
35
+ * The slotted-item shape rendered by `<kai-conversation-item>` — the composed
36
+ * row of the consumer-owned loop (spec 2026-08-20 § 2a). Distinct from the
37
+ * data-mode `ConversationItem` above, which batteries mode keeps rendering
38
+ * unchanged: this one takes REGIONS, not a `ConversationSummary`.
39
+ *
40
+ * Regions map 1:1 to the element's slots: `children` = the default slot (the
41
+ * title), plus `leading`, `meta`, and `menu`. The `menu` region takes the
42
+ * consumer's OWN popover (rename / fork / archive live there); the component
43
+ * provides only the region plus focus and ARIA plumbing, never a declarative
44
+ * actions prop. Activation semantics live in the CONTAINER
45
+ * (`createConversationItemsController` in conversation-list.tsx): this row is
46
+ * presentational, and the `data-kai-item-menu` marker on the menu region is what
47
+ * the container's activation guard keys off so a click in the consumer's menu
48
+ * never also selects the row.
49
+ *
50
+ * ARIA contract for direct Solid use: the row renders `role="listitem"` holding
51
+ * a `role="button"` body (`aria-current` marks the active row, the same dialect
52
+ * as the batteries-mode row above) with the menu as the body's tabbable SIBLING
53
+ * (ratified 2026-08-20; axe `nested-interactive` bans focusable descendants of
54
+ * a control, and `aria-required-children` outlaws non-option content anywhere
55
+ * inside a listbox, which is why the vocabulary is not listbox/option). Render
56
+ * your loop's rows inside a `role="list"` ancestor; element mode has no such
57
+ * burden: `<kai-conversations>`' items region provides it for slotted items.
58
+ */
59
+ export interface SlottedConversationItemProps {
60
+ /** The row's identity, handed to the container's selection contract. In the
61
+ * element this is the `conversation-id` attribute (host `id` is the fallback). */
62
+ conversationId?: string;
63
+ /** Selected state. Reflected as `aria-current` on the row body and a
64
+ * `data-active` styling hook on the row; the container drives it from its
65
+ * `activeId`. */
66
+ active?: boolean;
67
+ /** Dense single-line row padding. */
68
+ compact?: boolean;
69
+ /** Leading region before the title (an icon or avatar). */
70
+ leading?: JSX.Element;
71
+ /** Meta region under the title (a timestamp or status line). */
72
+ meta?: JSX.Element;
73
+ /** Your own row menu (a popover trigger). Never selects the row. */
74
+ menu?: JSX.Element;
75
+ /** The title (the element's default slot). */
76
+ children?: JSX.Element;
77
+ /** Set when the element HOST carries the row-group semantics: the inner row
78
+ * then renders `role="presentation"` so the accessibility tree sees one
79
+ * group (the host), never two. The `kai-conversation-item` facade sets it;
80
+ * Solid consumers rendering the component directly leave it off and the row
81
+ * itself is the group. */
82
+ hostSemantics?: boolean;
83
+ class?: string;
84
+ }
85
+
86
+ export function SlottedConversationItem(props: SlottedConversationItemProps) {
87
+ const [local] = splitProps(props, ['conversationId', 'active', 'compact', 'leading', 'meta', 'menu', 'children', 'hostSemantics', 'class']);
88
+ return (
89
+ // The sibling restructure (ratified 2026-08-20): axe nested-interactive
90
+ // bans focusable descendants of an activation control, so the control role
91
+ // sits on the row BODY and the consumer's menu is its tabbable SIBLING
92
+ // (the nav.tsx TrailingActions precedent). The vocabulary is
93
+ // list/listitem/button + aria-current — NOT listbox/option, because axe
94
+ // aria-required-children lets a listbox subtree own nothing but options
95
+ // (and groups of options), which outlaws a sibling menu ANYWHERE inside
96
+ // it; button + aria-current is also exactly what the batteries-mode
97
+ // ConversationItem row above renders, so the two modes speak one dialect.
98
+ <div
99
+ part="row"
100
+ role={local.hostSemantics ? 'presentation' : 'listitem'}
101
+ data-active={local.active ? '' : undefined}
102
+ data-conversation-id={local.conversationId}
103
+ class={cn(
104
+ 'flex w-full items-center gap-2.5 rounded-lg text-left transition-colors',
105
+ local.compact ? 'px-2.5 py-1.5' : 'px-2.5 py-2',
106
+ local.active ? 'bg-muted' : 'hover:bg-muted/50',
107
+ local.class,
108
+ )}
109
+ >
110
+ <div
111
+ part="body"
112
+ role="button"
113
+ data-kai-item-body
114
+ aria-current={local.active ? 'true' : 'false'}
115
+ class="flex min-w-0 flex-1 cursor-pointer items-center gap-2.5 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
116
+ >
117
+ <Show when={local.leading}>
118
+ <span part="leading" class="flex shrink-0 items-center text-muted-foreground">{local.leading}</span>
119
+ </Show>
120
+ <div class="min-w-0 flex-1">
121
+ <div part="title" class={cn('truncate text-sm', local.active ? 'font-medium text-foreground' : 'text-foreground/80')}>
122
+ {local.children}
123
+ </div>
124
+ <Show when={local.meta}>
125
+ <div part="meta" class="mt-0.5 truncate text-xs text-muted-foreground">{local.meta}</div>
126
+ </Show>
127
+ </div>
128
+ </div>
129
+ <Show when={local.menu}>
130
+ <span part="menu" data-kai-item-menu class="ml-auto flex shrink-0 items-center">{local.menu}</span>
131
+ </Show>
132
+ </div>
133
+ );
134
+ }
135
+
34
136
  export function ConversationItem(props: ConversationItemProps) {
35
137
  const [local] = splitProps(props, ['conversation', 'isActive', 'onSelect', 'compact', 'class']);
36
138
  // The trailing text: the consumer's own `trailing` field, else an auto relative
37
- // time from updatedAt (fallback lastMessageAt). REACTIVITY: this (and the auto
38
- // time) update when the consumer re-assigns the `conversations` array with a new
39
- // reference — the kit's standard reactivity model — not via an internal clock.
139
+ // time from updatedAt (fallback lastMessageAt). Never an internal clock — it is a
140
+ // render-time snapshot.
141
+ //
142
+ // REACTIVITY, and the weaker version of this note is what #224 was filed against:
143
+ // a new `conversations` array reference is NOT sufficient. `ConversationList` renders
144
+ // these rows through a reference-keyed `<For>` that captures `conv` as a VALUE, so a
145
+ // row whose item object is unchanged is never re-invoked and never re-reads anything
146
+ // below. The consumer needs BOTH — a new array (which is what notifies at all) and a
147
+ // new object for the item that changed (which is what this row can see). Adds,
148
+ // removes and reorders are fine on a fresh array alone, since those rows' identities
149
+ // already differ. Pinned by `src/components/reactivity-contract.test.tsx`.
40
150
  const trailing = createMemo(
41
151
  () => local.conversation.trailing ?? relativeTimeShort(local.conversation.updatedAt ?? local.conversation.lastMessageAt),
42
152
  );