@kitn.ai/ui 0.25.2 → 0.27.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 (603) hide show
  1. package/README.md +17 -6
  2. package/bin/mcp.js +16 -3
  3. package/bin/route.js +23 -0
  4. package/bin/route.test.js +58 -0
  5. package/dist/{elements/chunks/Icon-b5A8hlHs.js → Icon-BnFKqqtY.js} +1 -1
  6. package/dist/action-icons-CBskfBMz.js +1 -0
  7. package/dist/arrow-down-Br0qtpsz.js +1 -0
  8. package/dist/arrow-left-Dj7xgE12.js +1 -0
  9. package/dist/artifact-CvXEm1lS.js +1 -0
  10. package/dist/assets/dev-5XzXYR21.js +91 -0
  11. package/dist/attachments-C43ZegEq.js +1 -0
  12. package/dist/audio-visualizer-B-79y1Tj.js +1 -0
  13. package/dist/badge-BpUbq8c0.js +1 -0
  14. package/dist/bash-InADTalH.js +1 -6
  15. package/dist/bell-sn17Ip9v.js +1 -0
  16. package/dist/{elements/chunks/button-B9okqG73.js → button-BH6rN0U-.js} +1 -1
  17. package/dist/card-renderer-Cb1gQmth.js +1 -0
  18. package/dist/card-routing-DifQgS7n.js +1 -0
  19. package/dist/check-DPeeMUYx.js +1 -0
  20. package/dist/checkbox-Dc9Yov_B.js +1 -0
  21. package/dist/checkbox-group-B1kDLLYk.js +1 -0
  22. package/dist/chevron-down-CHSWk7ZT.js +1 -0
  23. package/dist/chevron-right-B7l_fMgx.js +1 -0
  24. package/dist/choice-card-BT3c1S8g.js +1 -0
  25. package/dist/circle-Bb0yOpqM.js +1 -0
  26. package/dist/circle-check-BGZpCiz7.js +1 -0
  27. package/dist/circle-x-wobWkoSx.js +1 -0
  28. package/dist/{elements/chunks/cn-CU7UAWzk.js → cn-DN5AWfiS.js} +1 -1
  29. package/dist/code-block-CUQ5mtv0.js +1 -0
  30. package/dist/collapsible-CPJbf6FM.js +1 -0
  31. package/dist/components/attachment-types.d.ts +53 -1
  32. package/dist/components/attachments.d.ts +31 -0
  33. package/dist/components/audio-visualizer/fit-scale.d.ts +50 -0
  34. package/dist/components/audio-visualizer/index.d.ts +15 -1
  35. package/dist/components/audio-visualizer/variant-bar.d.ts +24 -1
  36. package/dist/components/card-renderer.d.ts +13 -0
  37. package/dist/components/card.d.ts +4 -0
  38. package/dist/components/chat-thread.d.ts +49 -42
  39. package/dist/components/code-block.d.ts +22 -0
  40. package/dist/components/conversation-item.d.ts +66 -1
  41. package/dist/components/conversation-list.d.ts +77 -0
  42. package/dist/components/form-widgets.d.ts +67 -2
  43. package/dist/components/form.d.ts +89 -0
  44. package/dist/components/message.d.ts +29 -0
  45. package/dist/components/reasoning.d.ts +6 -0
  46. package/dist/components/scroll-button.d.ts +11 -0
  47. package/dist/components/thread.d.ts +5 -0
  48. package/dist/components/toast.d.ts +2 -1
  49. package/dist/components/tool-types.d.ts +9 -0
  50. package/dist/components/voice-input.d.ts +8 -0
  51. package/dist/components/voice-output.d.ts +12 -1
  52. package/dist/components/workspace-shell.d.ts +87 -0
  53. package/dist/composer-CjngLzal.js +106 -0
  54. package/dist/confirm-card-DCx_A4bU.js +1 -0
  55. package/dist/construct-cli.es.js +1169 -0
  56. package/dist/context-BY8V-Nlp.js +1 -0
  57. package/dist/controllable-D43BKmtX.js +1 -0
  58. package/dist/conversation-list-D4lLU_yS.js +1 -0
  59. package/dist/copy-B6DO4PqB.js +1 -0
  60. package/dist/core-AYMC6_lb.js +12 -5874
  61. package/dist/{create-tween-CUzOlaMS.js → create-tween-BPzaefTp.js} +1 -1
  62. package/dist/{create-tween-B5Y4Im8G.js → create-tween-CnMHviKB.js} +1 -1
  63. package/dist/{create-tween-4z1XYLkR.js → create-tween-D1aiGZZh.js} +1 -1
  64. package/dist/{elements/chunks/create-tween-CUrM6D_o.js → create-tween-DUSQUKpr.js} +2 -2
  65. package/dist/css-M7EaDHN_.js +1 -6
  66. package/dist/custom-elements.json +3468 -1927
  67. package/dist/default-input-CucQEOqH.js +1 -0
  68. package/dist/define-CGGX-7Vg.js +1 -0
  69. package/dist/define.d.ts +4 -0
  70. package/dist/define.js +510 -0
  71. package/dist/define.server.d.ts +5 -0
  72. package/dist/define.server.js +506 -0
  73. package/dist/diagnostics/hook.d.ts +81 -0
  74. package/dist/diagnostics/index.d.ts +7 -0
  75. package/dist/diagnostics/report-request.d.ts +61 -0
  76. package/dist/diagnostics.d.ts +5 -0
  77. package/dist/diagnostics.js +209 -0
  78. package/dist/disclosure-CbLtajUQ.js +1 -0
  79. package/dist/dropdown-BwhViJFd.js +1 -0
  80. package/dist/elements/agent-card.js +1 -1
  81. package/dist/elements/artifact.js +1 -1
  82. package/dist/elements/attachments.js +1 -1
  83. package/dist/elements/audio-visualizer.js +1 -1
  84. package/dist/elements/autoloader.js +1 -1
  85. package/dist/elements/avatar.js +1 -1
  86. package/dist/elements/badge.js +1 -1
  87. package/dist/elements/button.js +1 -1
  88. package/dist/elements/card.js +1 -1
  89. package/dist/elements/cards.js +1 -1
  90. package/dist/elements/chain-of-thought.js +1 -1
  91. package/dist/elements/chat-scope-picker.js +1 -1
  92. package/dist/elements/chat-workspace.js +1 -1
  93. package/dist/elements/chat.js +1 -1
  94. package/dist/elements/checkbox-group.d.ts +1 -0
  95. package/dist/elements/checkbox-group.js +1 -0
  96. package/dist/elements/checkbox.d.ts +1 -0
  97. package/dist/elements/checkbox.js +1 -0
  98. package/dist/elements/checkpoint.js +1 -1
  99. package/dist/elements/choice.js +1 -1
  100. package/dist/elements/coachmark.js +1 -1
  101. package/dist/elements/code-block.js +1 -1
  102. package/dist/elements/command.js +1 -1
  103. package/dist/elements/compare.js +1 -1
  104. package/dist/elements/composer.js +1 -1
  105. package/dist/elements/confirm-card.js +1 -1
  106. package/dist/elements/context-meter.js +1 -1
  107. package/dist/elements/conversation-item.d.ts +1 -0
  108. package/dist/elements/conversation-item.js +1 -0
  109. package/dist/elements/conversation-list.js +1 -1
  110. package/dist/elements/default-input.d.ts +3 -3
  111. package/dist/elements/define-entry.d.ts +12 -0
  112. package/dist/elements/define.d.ts +27 -0
  113. package/dist/elements/diagnostic-events.d.ts +74 -0
  114. package/dist/elements/dialog.js +1 -1
  115. package/dist/elements/dock.d.ts +1 -0
  116. package/dist/elements/dock.js +243 -0
  117. package/dist/elements/dropdown.d.ts +1 -0
  118. package/dist/elements/dropdown.js +1 -0
  119. package/dist/elements/editable-label.js +1 -1
  120. package/dist/elements/element-data-types.d.ts +37 -0
  121. package/dist/elements/element-diagnostics.d.ts +81 -0
  122. package/dist/elements/embed.js +1 -1
  123. package/dist/elements/empty.js +1 -1
  124. package/dist/elements/feedback-bar.js +1 -1
  125. package/dist/elements/file-tree.js +1 -1
  126. package/dist/elements/file-upload.js +1 -1
  127. package/dist/elements/form.js +1 -1
  128. package/dist/elements/hover-card.js +1 -1
  129. package/dist/elements/icon.js +1 -1
  130. package/dist/elements/image.js +1 -1
  131. package/dist/elements/input.js +1 -1
  132. package/dist/elements/kbd.js +1 -1
  133. package/dist/elements/link-preview.js +1 -1
  134. package/dist/elements/loader.js +1 -1
  135. package/dist/elements/markdown.js +1 -1
  136. package/dist/elements/menu.js +1 -1
  137. package/dist/elements/message-skills.js +1 -1
  138. package/dist/elements/message.js +1 -1
  139. package/dist/elements/model-switcher.js +1 -1
  140. package/dist/elements/nav.js +1 -1
  141. package/dist/elements/notice.js +1 -1
  142. package/dist/elements/pane-grid.d.ts +1 -0
  143. package/dist/elements/pane-grid.js +1 -0
  144. package/dist/elements/pane-group.js +1 -1
  145. package/dist/elements/pane.js +1 -1
  146. package/dist/elements/popover.js +1 -1
  147. package/dist/elements/progress-bar.js +1 -1
  148. package/dist/elements/prompt-dock.js +1 -1
  149. package/dist/elements/prompt-input.js +1 -1
  150. package/dist/elements/prompt-suggestions.js +1 -1
  151. package/dist/elements/radio-group.d.ts +1 -0
  152. package/dist/elements/radio-group.js +1 -0
  153. package/dist/elements/reasoning.js +1 -1
  154. package/dist/elements/register.d.ts +2 -0
  155. package/dist/elements/remote.js +1 -1
  156. package/dist/elements/resizable.js +1 -1
  157. package/dist/elements/response-stream.js +3 -3
  158. package/dist/elements/screen.js +1 -1
  159. package/dist/elements/scroll-area.js +1 -1
  160. package/dist/elements/scroll-button.js +1 -1
  161. package/dist/elements/search.js +1 -1
  162. package/dist/elements/segmented.js +1 -1
  163. package/dist/elements/select.d.ts +1 -0
  164. package/dist/elements/select.js +1 -0
  165. package/dist/elements/separator.js +1 -1
  166. package/dist/elements/setting-item.js +1 -1
  167. package/dist/elements/settings-group.js +1 -1
  168. package/dist/elements/skeleton.js +1 -1
  169. package/dist/elements/slider.d.ts +1 -0
  170. package/dist/elements/slider.js +1 -0
  171. package/dist/elements/slots.d.ts +36 -4
  172. package/dist/elements/source.js +1 -1
  173. package/dist/elements/status.js +1 -1
  174. package/dist/elements/switch.js +1 -1
  175. package/dist/elements/tabs.js +1 -1
  176. package/dist/elements/tasks.js +1 -1
  177. package/dist/elements/text-shimmer.js +1 -1
  178. package/dist/elements/thinking-bar.js +1 -1
  179. package/dist/elements/thread.js +1 -1
  180. package/dist/elements/toast.js +1 -1
  181. package/dist/elements/tool.js +1 -1
  182. package/dist/elements/tooltip.js +1 -1
  183. package/dist/elements/voice-input.js +1 -1
  184. package/dist/elements/voice-output.js +1 -1
  185. package/dist/elements.d.ts +589 -167
  186. package/dist/{elements/chunks/ellipsis-CVxqfsDZ.js → ellipsis-DFr4Vqr3.js} +1 -1
  187. package/dist/embed-5s_vU4J0.js +1 -0
  188. package/dist/engine-javascript-vq0WuIJl.js +14 -2516
  189. package/dist/{elements/chunks/external-link-DG2sRdQL.js → external-link-u9d85ORQ.js} +1 -1
  190. package/dist/file-text-CX7_x42o.js +1 -0
  191. package/dist/{elements/chunks/file-tree-yGWNK0Js.js → file-tree-D3kBjXI5.js} +1 -1
  192. package/dist/folder-V-YdIL8f.js +1 -0
  193. package/dist/form-BRNUMCN2.js +1 -0
  194. package/dist/github-dark-dimmed-DUshB20C.js +1 -4
  195. package/dist/github-light-JYsPkUQd.js +1 -4
  196. package/dist/hover-card-DaeiYrTA.js +1 -0
  197. package/dist/html-CPZ3oZQ7.js +1 -10
  198. package/dist/icon-BpixdZNq.js +1 -0
  199. package/dist/{elements/chunks/index-BCRhi84a.js → index-xu_pdcPi.js} +1 -1
  200. package/dist/index.d.ts +3 -1
  201. package/dist/index.js +8473 -6470
  202. package/dist/index.server.js +6495 -4958
  203. package/dist/info-8mFsIjS3.js +1 -0
  204. package/dist/input-d1Yuu-hz.js +1 -0
  205. package/dist/javascript-C25yR2R2.js +1 -6
  206. package/dist/json-DxJze_jm.js +1 -6
  207. package/dist/kai-provider.es.js +40 -40
  208. package/dist/kai.es.js +1 -1
  209. package/dist/kbd-961zMPFB.js +1 -0
  210. package/dist/link-DlZZBGaR.js +1 -0
  211. package/dist/{elements/chunks/link-preview-Cf5KOP-i.js → link-preview-DIIbV2Sd.js} +1 -1
  212. package/dist/loader-YQszHFaW.js +1 -0
  213. package/dist/{elements/chunks/markdown-DbozyYqa.js → markdown-CVk1MPsj.js} +1 -1
  214. package/dist/mcp.es.js +3873 -544
  215. package/dist/message-CwKVEdZg.js +1 -0
  216. package/dist/message-DS8lzHc1.js +1 -0
  217. package/dist/message-circle-Xf8Q-vZp.js +1 -0
  218. package/dist/message-square-Cxnwgkns.js +1 -0
  219. package/dist/{elements/chunks/minimize-2-SegJ_oku.js → minimize-2-UEZmP2QT.js} +1 -1
  220. package/dist/{elements/chunks/model-switcher-D7JrFFHp.js → model-switcher-Kk1jEQ5t.js} +1 -1
  221. package/dist/{elements/chunks/overlay-CV7z2YuX.js → overlay-C1od6LOz.js} +1 -1
  222. package/dist/panel-right-ryET0yyn.js +1 -0
  223. package/dist/paperclip-BDNoZFJP.js +1 -0
  224. package/dist/play-OFIvYXUQ.js +1 -0
  225. package/dist/primitives/card-data-types.d.ts +49 -0
  226. package/dist/primitives/card-routing.d.ts +1 -33
  227. package/dist/primitives/card-validate.d.ts +2 -2
  228. package/dist/primitives/chat-config.d.ts +29 -2
  229. package/dist/primitives/field-mask.d.ts +96 -0
  230. package/dist/primitives/field-semantics.d.ts +36 -0
  231. package/dist/primitives/input-mask.d.ts +46 -0
  232. package/dist/primitives/url-scheme-policy.d.ts +34 -0
  233. package/dist/primitives/use-resize-observer.d.ts +23 -0
  234. package/dist/primitives/use-speech-recognition.d.ts +8 -0
  235. package/dist/primitives/use-text-stream.d.ts +5 -0
  236. package/dist/progress-bar-BvYX0aoy.js +1 -0
  237. package/dist/prompt-suggestion-BRFu2lcu.js +1 -0
  238. package/dist/radio-DcL23cNJ.js +1 -0
  239. package/dist/react/index.d.ts +306 -283
  240. package/dist/react.js +309 -247
  241. package/dist/reasoning-Cackq4Rh.js +1 -0
  242. package/dist/register-impl-C7rENvPK.js +424 -0
  243. package/dist/{elements/chunks/resizable-Dpzjlvp4.js → resizable-Cp9HUYih.js} +1 -1
  244. package/dist/rotate-ccw-0DLRDHH5.js +1 -0
  245. package/dist/rotate-cw-DOB9IGcU.js +1 -0
  246. package/dist/schemas/form.schema.json +21 -1
  247. package/dist/schemas/index.d.ts +1 -1
  248. package/dist/schemas/tool-defs.d.ts +45 -0
  249. package/dist/schemas.js +398 -305
  250. package/dist/scroll-area-emVmWP4K.js +1 -0
  251. package/dist/scroll-button-CeP_gv1x.js +1 -0
  252. package/dist/select-CWmu00tS.js +1 -0
  253. package/dist/{elements/chunks/separator-DH2r_ZSZ.js → separator-CE6vly6z.js} +1 -1
  254. package/dist/{elements/chunks/settings-BFMOd6yB.js → settings-CE-78SvZ.js} +1 -1
  255. package/dist/{elements/chunks/settings-group-B3VZKIcz.js → settings-group-ZFWNxFQR.js} +1 -1
  256. package/dist/{elements/chunks/skeleton-N8BuWYmf.js → skeleton-BebdP4mf.js} +1 -1
  257. package/dist/slider-BAKRYUlz.js +1 -0
  258. package/dist/slots-DaNjX0rc.js +1 -0
  259. package/dist/{solid-BMuDmFo_.js → solid-BE5ui6sy.js} +10672 -8116
  260. package/dist/{solid-CGYNdLRR.js → solid-DFhsce3O.js} +8137 -6056
  261. package/dist/solid.d.ts +15 -0
  262. package/dist/solid.js +248 -235
  263. package/dist/solid.server.js +248 -235
  264. package/dist/source-B0FlguSx.js +1 -0
  265. package/dist/star-CBMZvLuN.js +1 -0
  266. package/dist/state/index.d.ts +5 -1
  267. package/dist/state/mock.d.ts +32 -2
  268. package/dist/state/persistence.d.ts +51 -0
  269. package/dist/state/stream.d.ts +50 -0
  270. package/dist/state/threads.d.ts +71 -0
  271. package/dist/state.js +297 -172
  272. package/dist/store-7eoadydQ.js +1 -0
  273. package/dist/svelte--5p79yCD.js +1 -15
  274. package/dist/switch-CViARZCy.js +1 -0
  275. package/dist/tasks-card-MjCcOhR_.js +1 -0
  276. package/dist/text-shimmer-DKlwRF2G.js +1 -0
  277. package/dist/textarea-3ay2Me2-.js +1 -0
  278. package/dist/theme.tokens.css +82 -13
  279. package/dist/{elements/chunks/thumbs-up-DQe0hvkZ.js → thumbs-up-DQcu2HMO.js} +1 -1
  280. package/dist/toast-store-C6yKOhbk.js +1 -0
  281. package/dist/{elements/chunks/tool-C1YtxwPu.js → tool-BCYNlfKy.js} +1 -1
  282. package/dist/{elements/chunks/tooltip-BqwnFWhn.js → tooltip-05zI4hjs.js} +1 -1
  283. package/dist/trash-2-BR1RxGky.js +1 -0
  284. package/dist/{elements/chunks/triangle-alert-B6or7F5u.js → triangle-alert-DHohDOCy.js} +1 -1
  285. package/dist/tsx-B8rCNbgL.js +1 -6
  286. package/dist/types.d.ts +4 -2
  287. package/dist/typescript-RycA9KXf.js +1 -6
  288. package/dist/ui/checkbox-group.d.ts +83 -0
  289. package/dist/ui/checkbox.d.ts +40 -0
  290. package/dist/ui/dock.d.ts +131 -0
  291. package/dist/ui/hover-card.d.ts +44 -0
  292. package/dist/ui/icon.d.ts +3 -0
  293. package/dist/ui/input.d.ts +48 -2
  294. package/dist/ui/radio.d.ts +85 -0
  295. package/dist/ui/select.d.ts +71 -0
  296. package/dist/ui/slider.d.ts +65 -0
  297. package/dist/ui/switch.d.ts +9 -2
  298. package/dist/upload-BbGW6A_u.js +1 -0
  299. package/dist/url-scheme-policy-DHpoTJwB.js +1 -0
  300. package/dist/use-card-resolution-CJW4grFa.js +1 -0
  301. package/dist/{variant-aurora-DwSOfraQ.js → variant-aurora-BUOVgPOB.js} +27 -27
  302. package/dist/{variant-aurora-wKvAQ5wp.js → variant-aurora-CUsUJvFI.js} +28 -28
  303. package/dist/{variant-aurora-XEkPT0_z.js → variant-aurora-CW2i4bfr.js} +2 -2
  304. package/dist/{elements/chunks/variant-aurora-QXTZPyST.js → variant-aurora-xiMPty8f.js} +2 -2
  305. package/dist/variant-custom-BQtOnmeO.js +1 -0
  306. package/dist/{variant-custom-BhS9wIpU.js → variant-custom-CAm9L-E5.js} +65 -65
  307. package/dist/{variant-custom-eFIg2XWQ.js → variant-custom-CAnzzppW.js} +40 -40
  308. package/dist/variant-custom-D91HjAKz.js +1 -0
  309. package/dist/{variant-wave-CUWGx9Bn.js → variant-wave-Ca1Ls4_y.js} +21 -21
  310. package/dist/{variant-wave-CbVsmgwT.js → variant-wave-CxX39DKd.js} +25 -25
  311. package/dist/{variant-wave-BwzrHD1s.js → variant-wave-DYyzIIZv.js} +2 -2
  312. package/dist/{elements/chunks/variant-wave-DDCwhLas.js → variant-wave-uY5crU_K.js} +2 -2
  313. package/dist/vue-BmIZj4XD.js +1 -33
  314. package/dist/wire/chunk.d.ts +47 -0
  315. package/dist/wire/diagnostics.d.ts +575 -0
  316. package/dist/wire/encode-probe.d.ts +70 -0
  317. package/dist/wire/encode.d.ts +24 -0
  318. package/dist/wire/index.d.ts +2 -0
  319. package/dist/wire/sse.d.ts +10 -2
  320. package/dist/wire.js +917 -480
  321. package/dist/x-C5m1JFgi.js +1 -0
  322. package/frameworks/react/index.tsx +325 -108
  323. package/llms-full.txt +1354 -82
  324. package/llms.txt +6 -3
  325. package/package.json +39 -6
  326. package/src/agent-tooling/archetypes.ts +41 -9
  327. package/src/agent-tooling/catalog/README.md +676 -0
  328. package/src/agent-tooling/catalog/catalog-types.ts +231 -0
  329. package/src/agent-tooling/catalog/fabrications.ts +96 -0
  330. package/src/agent-tooling/catalog/invariants.ts +284 -0
  331. package/src/agent-tooling/catalog/labs-titles.ts +114 -0
  332. package/src/agent-tooling/catalog/scenarios.ts +87 -0
  333. package/src/agent-tooling/catalog/surfaces.ts +347 -0
  334. package/src/agent-tooling/construct/cli-entry.ts +8 -0
  335. package/src/agent-tooling/construct/cli.ts +131 -0
  336. package/src/agent-tooling/construct/codegen.ts +1353 -0
  337. package/src/agent-tooling/construct/construct.v1.schema.json +250 -0
  338. package/src/agent-tooling/construct/dev.ts +148 -0
  339. package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +7 -0
  340. package/src/agent-tooling/construct/fixtures/ops-console.construct.json +44 -0
  341. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +23 -0
  342. package/src/agent-tooling/construct/schema.ts +371 -0
  343. package/src/agent-tooling/integrations/anthropic.ts +7 -1
  344. package/src/agent-tooling/integrations/cloudflare.ts +43 -2
  345. package/src/agent-tooling/integrations/langgraph.ts +7 -1
  346. package/src/agent-tooling/integrations/mastra.ts +7 -1
  347. package/src/agent-tooling/integrations/mock.ts +92 -17
  348. package/src/agent-tooling/integrations/ollama.ts +7 -1
  349. package/src/agent-tooling/integrations/openai.ts +7 -1
  350. package/src/agent-tooling/integrations/openrouter.ts +7 -1
  351. package/src/agent-tooling/integrations/pi.ts +56 -1
  352. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +7 -1
  353. package/src/agent-tooling/mcp/css-raw.d.ts +12 -0
  354. package/src/agent-tooling/mcp/manifest.ts +100 -1
  355. package/src/agent-tooling/mcp/server.ts +63 -6
  356. package/src/agent-tooling/mcp/tools/construct.ts +130 -0
  357. package/src/agent-tooling/mcp/tools/debug.ts +126 -4
  358. package/src/agent-tooling/mcp/tools/reference.ts +542 -5
  359. package/src/agent-tooling/mcp/tools/scaffold.ts +1834 -307
  360. package/src/agent-tooling/mcp/tools/theme.ts +146 -30
  361. package/src/agent-tooling/mcp/validate-args.ts +141 -0
  362. package/src/agent-tooling/recipes/composed-thread.ts +714 -0
  363. package/src/agent-tooling/recipes/index.ts +22 -0
  364. package/src/agent-tooling/recipes/types.ts +32 -0
  365. package/src/agent-tooling/registry.ts +24 -7
  366. package/src/agent-tooling/route-emit.ts +26 -2
  367. package/src/components/attachment-types.ts +54 -1
  368. package/src/components/attachments.tsx +169 -22
  369. package/src/components/audio-visualizer/fit-scale.ts +112 -0
  370. package/src/components/audio-visualizer/index.tsx +80 -3
  371. package/src/components/audio-visualizer/variant-aurora.tsx +10 -4
  372. package/src/components/audio-visualizer/variant-bar.tsx +38 -6
  373. package/src/components/audio-visualizer/variant-custom.tsx +8 -2
  374. package/src/components/audio-visualizer/variant-grid.tsx +16 -11
  375. package/src/components/audio-visualizer/variant-radial.tsx +15 -10
  376. package/src/components/audio-visualizer/variant-wave.tsx +7 -2
  377. package/src/components/card-renderer.tsx +39 -8
  378. package/src/components/card.tsx +10 -2
  379. package/src/components/chat-container.tsx +4 -1
  380. package/src/components/chat-scope-picker.tsx +2 -2
  381. package/src/components/chat-thread.tsx +105 -13
  382. package/src/components/choice-card.tsx +102 -37
  383. package/src/components/coachmark.tsx +1 -1
  384. package/src/components/code-block.tsx +81 -5
  385. package/src/components/composer.tsx +89 -8
  386. package/src/components/confirm-card.tsx +20 -4
  387. package/src/components/conversation-item.tsx +138 -4
  388. package/src/components/conversation-list.tsx +230 -15
  389. package/src/components/embed.tsx +4 -0
  390. package/src/components/empty.tsx +14 -3
  391. package/src/components/file-tree.tsx +5 -1
  392. package/src/components/form-widgets.tsx +257 -195
  393. package/src/components/form.tsx +336 -21
  394. package/src/components/loader.tsx +12 -5
  395. package/src/components/message-skills.tsx +1 -1
  396. package/src/components/message.tsx +142 -12
  397. package/src/components/prompt-input.tsx +7 -1
  398. package/src/components/prompt-suggestion.tsx +4 -1
  399. package/src/components/reasoning.tsx +30 -3
  400. package/src/components/response-compare.tsx +2 -2
  401. package/src/components/response-stream.tsx +13 -7
  402. package/src/components/screen.tsx +5 -0
  403. package/src/components/scroll-button.tsx +63 -4
  404. package/src/components/source.tsx +6 -1
  405. package/src/components/tasks-card.tsx +58 -16
  406. package/src/components/thread.tsx +10 -1
  407. package/src/components/toast.tsx +11 -5
  408. package/src/components/tool-types.ts +9 -0
  409. package/src/components/voice-input.tsx +35 -3
  410. package/src/components/voice-output.tsx +46 -6
  411. package/src/components/workspace-shell.tsx +285 -0
  412. package/src/diagnostics/hook.ts +352 -0
  413. package/src/diagnostics/index.ts +82 -0
  414. package/src/diagnostics/report-request.ts +296 -0
  415. package/src/elements/attachments.tsx +9 -8
  416. package/src/elements/audio-visualizer.tsx +48 -4
  417. package/src/elements/chat-workspace.tsx +159 -287
  418. package/src/elements/chat.tsx +31 -10
  419. package/src/elements/checkbox-group.tsx +164 -0
  420. package/src/elements/checkbox.tsx +134 -0
  421. package/src/elements/code-block.tsx +19 -1
  422. package/src/elements/command.tsx +4 -1
  423. package/src/elements/compiled.css +1 -1
  424. package/src/elements/composer.tsx +12 -1
  425. package/src/elements/conversation-item.tsx +120 -0
  426. package/src/elements/conversation-list.tsx +60 -11
  427. package/src/elements/default-input.tsx +5 -5
  428. package/src/elements/define-entry.ts +12 -0
  429. package/src/elements/define.tsx +168 -8
  430. package/src/elements/diagnostic-events.ts +114 -0
  431. package/src/elements/dialog.tsx +47 -0
  432. package/src/elements/disclosure.ts +34 -12
  433. package/src/elements/dock.tsx +215 -0
  434. package/src/elements/dropdown.tsx +123 -0
  435. package/src/elements/element-data-types.ts +40 -0
  436. package/src/elements/element-diagnostics.ts +392 -0
  437. package/src/elements/element-manifest.json +36 -0
  438. package/src/elements/element-meta.json +1483 -560
  439. package/src/elements/element-nonscalar.json +185 -0
  440. package/src/elements/element-types.d.ts +589 -167
  441. package/src/elements/feedback-bar.tsx +5 -0
  442. package/src/elements/icon-names.json +29 -0
  443. package/src/elements/input.tsx +312 -23
  444. package/src/elements/menu.tsx +13 -0
  445. package/src/elements/message.tsx +2 -0
  446. package/src/elements/pane-grid.tsx +111 -0
  447. package/src/elements/prompt-dock.tsx +2 -1
  448. package/src/elements/prompt-input.tsx +16 -10
  449. package/src/elements/radio-group.tsx +129 -0
  450. package/src/elements/register-impl.ts +52 -0
  451. package/src/elements/register.ts +28 -0
  452. package/src/elements/resizable.tsx +52 -25
  453. package/src/elements/scroll-button.tsx +59 -6
  454. package/src/elements/select.tsx +165 -0
  455. package/src/elements/slider.tsx +171 -0
  456. package/src/elements/slots.ts +108 -13
  457. package/src/elements/styles.css +119 -9
  458. package/src/elements/thinking-bar.tsx +16 -7
  459. package/src/elements/thread.tsx +2 -0
  460. package/src/elements/toast.tsx +17 -1
  461. package/src/elements/tool.tsx +26 -5
  462. package/src/elements/voice-input.tsx +11 -2
  463. package/src/elements/voice-output.tsx +16 -3
  464. package/src/index.ts +4 -1
  465. package/src/primitives/card-data-types.ts +49 -0
  466. package/src/primitives/card-host.tsx +4 -2
  467. package/src/primitives/card-routing.ts +7 -56
  468. package/src/primitives/card-schemas/form.schema.json +21 -1
  469. package/src/primitives/card-validate.ts +10 -2
  470. package/src/primitives/chat-config.tsx +33 -6
  471. package/src/primitives/field-mask.ts +256 -0
  472. package/src/primitives/field-semantics.ts +115 -0
  473. package/src/primitives/input-mask.ts +853 -0
  474. package/src/primitives/toast-store.ts +66 -4
  475. package/src/primitives/url-scheme-policy.ts +70 -0
  476. package/src/primitives/use-resize-observer.ts +33 -0
  477. package/src/primitives/use-speech-recognition.ts +12 -1
  478. package/src/primitives/use-stick-to-bottom.ts +34 -2
  479. package/src/primitives/use-text-stream.ts +33 -2
  480. package/src/remote/provider-runtime.ts +6 -3
  481. package/src/schemas/index.ts +1 -0
  482. package/src/schemas/tool-defs.ts +307 -6
  483. package/src/solid.ts +15 -0
  484. package/src/state/index.ts +13 -1
  485. package/src/state/mock.ts +92 -6
  486. package/src/state/persistence.ts +180 -0
  487. package/src/state/stream.ts +71 -4
  488. package/src/state/threads.ts +142 -0
  489. package/src/types.ts +4 -2
  490. package/src/ui/agent-card.tsx +2 -2
  491. package/src/ui/avatar.tsx +1 -1
  492. package/src/ui/checkbox-group.tsx +153 -0
  493. package/src/ui/checkbox.tsx +63 -0
  494. package/src/ui/dialog.tsx +6 -0
  495. package/src/ui/dock.tsx +584 -0
  496. package/src/ui/dropdown.tsx +67 -23
  497. package/src/ui/hover-card.tsx +156 -6
  498. package/src/ui/icon.tsx +53 -0
  499. package/src/ui/input.tsx +275 -16
  500. package/src/ui/kbd.tsx +1 -1
  501. package/src/ui/nav.tsx +1 -1
  502. package/src/ui/pane-group.tsx +2 -2
  503. package/src/ui/radio.tsx +150 -0
  504. package/src/ui/resizable.tsx +11 -0
  505. package/src/ui/select.tsx +168 -0
  506. package/src/ui/slider.tsx +178 -0
  507. package/src/ui/switch.tsx +49 -18
  508. package/src/utils/cn.ts +11 -5
  509. package/src/wire/chunk.ts +47 -0
  510. package/src/wire/consume.ts +185 -26
  511. package/src/wire/diagnostics.ts +727 -0
  512. package/src/wire/encode-probe.ts +214 -0
  513. package/src/wire/encode.ts +267 -14
  514. package/src/wire/formats/anthropic.ts +8 -1
  515. package/src/wire/formats/openai.ts +5 -0
  516. package/src/wire/index.ts +22 -0
  517. package/src/wire/read.ts +250 -6
  518. package/src/wire/sse.ts +16 -3
  519. package/theme.css +84 -13
  520. package/dist/elements/chunks/action-icons-DW9muWrY.js +0 -1
  521. package/dist/elements/chunks/arrow-left-iJVnQmxA.js +0 -1
  522. package/dist/elements/chunks/artifact-BSuPYFHY.js +0 -1
  523. package/dist/elements/chunks/attachments-CL6TaqIb.js +0 -1
  524. package/dist/elements/chunks/audio-visualizer-BuUqQmaO.js +0 -1
  525. package/dist/elements/chunks/badge-TIMiJFKc.js +0 -1
  526. package/dist/elements/chunks/bash-InADTalH.js +0 -1
  527. package/dist/elements/chunks/card-renderer-CSoTL_e1.js +0 -1
  528. package/dist/elements/chunks/card-routing-BK09BdmL.js +0 -1
  529. package/dist/elements/chunks/chat-thread-DxoKaStk.js +0 -1
  530. package/dist/elements/chunks/check-MdLzbxrm.js +0 -1
  531. package/dist/elements/chunks/chevron-down-MK7IzDqe.js +0 -1
  532. package/dist/elements/chunks/chevron-right-C5jXhmtV.js +0 -1
  533. package/dist/elements/chunks/choice-card-CGMc3kek.js +0 -1
  534. package/dist/elements/chunks/circle-BG7VkPep.js +0 -1
  535. package/dist/elements/chunks/circle-check-GqvO2UBs.js +0 -1
  536. package/dist/elements/chunks/circle-x-mhH7pOK2.js +0 -1
  537. package/dist/elements/chunks/code-block-ClhQO9RW.js +0 -1
  538. package/dist/elements/chunks/collapsible-DBcBkFPh.js +0 -1
  539. package/dist/elements/chunks/composer-CZZmtYOT.js +0 -69
  540. package/dist/elements/chunks/confirm-card-BWjf2-xo.js +0 -1
  541. package/dist/elements/chunks/context-hKjNqiyQ.js +0 -1
  542. package/dist/elements/chunks/conversation-list-CsGJjSKG.js +0 -1
  543. package/dist/elements/chunks/core-AYMC6_lb.js +0 -12
  544. package/dist/elements/chunks/css-M7EaDHN_.js +0 -1
  545. package/dist/elements/chunks/default-input-B1Mem5wu.js +0 -1
  546. package/dist/elements/chunks/define-B_tXMYRm.js +0 -1
  547. package/dist/elements/chunks/disclosure-DAoYOKew.js +0 -1
  548. package/dist/elements/chunks/download-DnCyWFJE.js +0 -1
  549. package/dist/elements/chunks/dropdown-CmNna5lT.js +0 -1
  550. package/dist/elements/chunks/embed-CjAD2jSi.js +0 -1
  551. package/dist/elements/chunks/engine-javascript-vq0WuIJl.js +0 -141
  552. package/dist/elements/chunks/file-text-D0J9F6M7.js +0 -1
  553. package/dist/elements/chunks/folder-NV1pi_ud.js +0 -1
  554. package/dist/elements/chunks/form-Cp8Y00Wn.js +0 -1
  555. package/dist/elements/chunks/github-dark-dimmed-DUshB20C.js +0 -1
  556. package/dist/elements/chunks/github-light-JYsPkUQd.js +0 -1
  557. package/dist/elements/chunks/hover-card-6KtXpLwT.js +0 -1
  558. package/dist/elements/chunks/html-CPZ3oZQ7.js +0 -1
  559. package/dist/elements/chunks/icon-97R8SR2p.js +0 -1
  560. package/dist/elements/chunks/info-C8r5Q6cc.js +0 -1
  561. package/dist/elements/chunks/input-DrseEjhp.js +0 -1
  562. package/dist/elements/chunks/javascript-C25yR2R2.js +0 -1
  563. package/dist/elements/chunks/json-DxJze_jm.js +0 -1
  564. package/dist/elements/chunks/kbd-OGJDUT-O.js +0 -1
  565. package/dist/elements/chunks/link-BdsfLB7b.js +0 -1
  566. package/dist/elements/chunks/loader-B5QzK_8_.js +0 -1
  567. package/dist/elements/chunks/message-CE4k-qaj.js +0 -1
  568. package/dist/elements/chunks/message-DtXKPqiR.js +0 -1
  569. package/dist/elements/chunks/message-square-C9ovmtOo.js +0 -1
  570. package/dist/elements/chunks/paperclip-Bg0ITU3b.js +0 -1
  571. package/dist/elements/chunks/progress-bar-D1_rzt6N.js +0 -1
  572. package/dist/elements/chunks/prompt-suggestion-D5Sm8xF1.js +0 -1
  573. package/dist/elements/chunks/reasoning-BrxQfpfI.js +0 -1
  574. package/dist/elements/chunks/rotate-cw-DzVDJOA1.js +0 -1
  575. package/dist/elements/chunks/scroll-area-93A1bbeG.js +0 -1
  576. package/dist/elements/chunks/scroll-button-CrTo7wm4.js +0 -1
  577. package/dist/elements/chunks/share-DgNbkJKY.js +0 -1
  578. package/dist/elements/chunks/slots-CIw9RlAe.js +0 -1
  579. package/dist/elements/chunks/source-w9V0NnBn.js +0 -1
  580. package/dist/elements/chunks/star-DHvACxtm.js +0 -1
  581. package/dist/elements/chunks/store-HIPGhNz9.js +0 -1
  582. package/dist/elements/chunks/svelte--5p79yCD.js +0 -1
  583. package/dist/elements/chunks/tasks-card-lPxoodlA.js +0 -1
  584. package/dist/elements/chunks/text-shimmer-BY_rI3s5.js +0 -1
  585. package/dist/elements/chunks/textarea-DPfuhswM.js +0 -1
  586. package/dist/elements/chunks/toast-store-VUZZBhzH.js +0 -1
  587. package/dist/elements/chunks/tsx-B8rCNbgL.js +0 -1
  588. package/dist/elements/chunks/typescript-RycA9KXf.js +0 -1
  589. package/dist/elements/chunks/use-card-resolution-DJg32jGH.js +0 -1
  590. package/dist/elements/chunks/variant-custom-D5ZlmziF.js +0 -1
  591. package/dist/elements/chunks/video-Bb86fVYI.js +0 -1
  592. package/dist/elements/chunks/vue-BmIZj4XD.js +0 -1
  593. package/dist/elements/chunks/x-BOhjBeOd.js +0 -1
  594. package/dist/llms/llms-full.txt +0 -2834
  595. package/dist/llms/llms.txt +0 -156
  596. package/dist/primitives/card-validate-generator.testlib.d.ts +0 -20
  597. package/dist/register-impl-DGmGzrEM.js +0 -145
  598. package/dist/ui/stat.d.ts +0 -16
  599. package/dist/variant-custom-MkTNTycd.js +0 -1
  600. package/src/primitives/card-validate-generator.testlib.ts +0 -38
  601. package/src/ui/stat.tsx +0 -41
  602. /package/dist/{elements/chunks/card-tags-D8lZ-C_U.js → card-tags-D8lZ-C_U.js} +0 -0
  603. /package/dist/{elements/chunks/link-preview-DNILK391.js → link-preview-DNILK391.js} +0 -0
@@ -19,6 +19,7 @@ import type { ProseSize } from '../primitives/chat-config';
19
19
  import type { ModelOption } from '../types';
20
20
  import type { CardComponentMap } from '../primitives/card-registry';
21
21
  import type { CardSchemaMap } from './card-renderer';
22
+ import type { JSX } from 'solid-js';
22
23
 
23
24
  export interface ChatThreadContextUsage {
24
25
  usedTokens: number;
@@ -45,6 +46,11 @@ export interface ChatThreadProps {
45
46
  * the kit checks its own seven built-ins and leaves your own card type
46
47
  * unvalidated. A schema here WINS over a built-in of the same name. */
47
48
  cardSchemas?: CardSchemaMap;
49
+ /** The custom-element host node to emit card events off when no `CardProvider`
50
+ * is present, forwarded through `MessageBody` to `CardRenderer`. The
51
+ * element facades pass their own host element so card events leave as the
52
+ * bubbling `kai-card` CustomEvent. */
53
+ cardHostElement?: HTMLElement;
48
54
  /** Value of the input. A **string** is controlled (the host owns the text and
49
55
  * updates it on `kai-value-change`). A **ComposerDoc** is a one-time seed that
50
56
  * pre-populates pills; the user then edits freely. Leave unset for uncontrolled. */
@@ -73,6 +79,19 @@ export interface ChatThreadProps {
73
79
  /** Enable Shiki syntax highlighting in code blocks. Turn off to render plain
74
80
  * `<pre>` blocks (lighter, no highlighter load). Default true. */
75
81
  codeHighlight?: boolean;
82
+ /** How `reasoning` parts render across the thread. `'full'` (default) is the
83
+ * current collapsible-disclosure behavior; `'compact'` shows only a shimmer
84
+ * loader while a reasoning part streams and nothing once it settles (no
85
+ * expandable detail); `'off'` renders reasoning parts not at all. Forwarded
86
+ * to every `MessageBody` as `reasoningMode`. */
87
+ reasoning?: 'full' | 'compact' | 'off';
88
+ /** Seeds the reasoning disclosure open AND keeps it tracking the stream
89
+ * (open while streaming, closes when it settles): the pre-Task-19f `full`
90
+ * behavior. Default false/absent: the panel starts closed (just the
91
+ * "Thinking" shimmer chip) and only opens on click, the current default
92
+ * (owner ruling, 2026-08-26). Meaningless when `reasoning` is `'compact'`
93
+ * or `'off'`. Forwarded to every `MessageBody` as `reasoningDefaultOpen`. */
94
+ reasoningOpen?: boolean;
76
95
  /** Optional header title shown on the left of the header. */
77
96
  chatTitle?: string;
78
97
  /** Optional model list. When set (>1 model) a ModelSwitcher is shown in the
@@ -90,6 +109,17 @@ export interface ChatThreadProps {
90
109
  headerStart?: boolean;
91
110
  /** Whether the host has `slot="header-end"` content (right of the controls). */
92
111
  headerEnd?: boolean;
112
+ /** Extra content rendered in the header-end region, AFTER `slot="header-end"`.
113
+ * This is a JSX escape hatch for a caller composing `ChatThread` directly as a
114
+ * Solid component (no shadow-DOM host, so there is no light-DOM node to slot) —
115
+ * the `kai-dock`-docked construct widget is the motivating case: it needs its
116
+ * own close affordance to sit IN the header row, sharing it with the title
117
+ * instead of floating as a second control with no visible relationship to the
118
+ * chat surface. Renders alongside the named slot rather than replacing it, so a
119
+ * real `slot="header-end"` consumer and this prop can both be present. Counts
120
+ * toward `showHeader()` the same as `headerEnd`, so a construct with no title
121
+ * and only this content still gets a header row to sit in. */
122
+ headerEndContent?: JSX.Element;
93
123
  // ── Composition slots ─────────────────────────────────────────────────────
94
124
  // Each flag below is set by the `<kai-chat>` facade when matching light-DOM
95
125
  // `slot="…"` content is projected, and gates one composition slot. Two kinds:
@@ -106,6 +136,17 @@ export interface ChatThreadProps {
106
136
  sidebar?: boolean;
107
137
  /** REPLACE: custom zero-state rendered in the message area while the thread is empty (replaces the empty message list only; the composer and its suggestions still render). */
108
138
  empty?: boolean;
139
+ /** REPLACE, JSX form: the empty-state content itself, for a caller composing
140
+ * `ChatThread` directly as a Solid component rather than through the `<kai-chat>`
141
+ * shadow-DOM boundary that `empty`/`slot="empty"` targets. Renders INSIDE this
142
+ * component's own tree — so a caller passing the kit's own `<Empty>` composition
143
+ * (`components/empty.tsx`) gets it fully styled by the adopted stylesheet, unlike
144
+ * `slot="empty"`: that slot only ever receives LIGHT-DOM children of the shadow
145
+ * HOST, and light-DOM nodes are outside the shadow root's adopted stylesheets, so
146
+ * Tailwind-utility-class content projected there renders bare. Takes priority
147
+ * over `empty`/`slot="empty"` when both are set — the two are alternate delivery
148
+ * mechanisms for the same region, not additive like `headerEndContent`. */
149
+ emptyContent?: JSX.Element;
109
150
  /** REPLACE: full custom composer in place of the built-in prompt input. The
110
151
  * projected content wires its own submit (the data-flow boundary). */
111
152
  composer?: boolean;
@@ -120,8 +161,12 @@ export interface ChatThreadProps {
120
161
  accept?: MediaTypeFilter;
121
162
  /** Files the composer refused because `accept` excluded them. */
122
163
  onAttachmentsRejected?: (rejected: RejectedAttachment[]) => void;
123
- /** Show a Search (Globe) button in the input toolbar; fires a `search` event. */
124
- search?: boolean;
164
+ /** When `false`, hides the built-in paperclip attach button. Defaults to
165
+ * `true` (undeclared keeps today's behavior: attach visible), matching
166
+ * `DefaultPromptInput`'s own default: only an explicit `false` hides it. */
167
+ attach?: boolean;
168
+ /** Show a web-search (Globe) button in the input toolbar; calls `onWebSearch`. */
169
+ webSearch?: boolean;
125
170
  /** Show a Voice (Mic) button in the input toolbar; fires a `voice` event. */
126
171
  voice?: boolean;
127
172
  /** Rich entity triggers. Each `{ char, kind, items }` opens a caret-anchored
@@ -140,7 +185,7 @@ export interface ChatThreadProps {
140
185
  onSuggestionClick?: (value: string) => void;
141
186
  onModelChange?: (modelId: string) => void;
142
187
  onMessageAction?: (detail: MessageActionDetail) => void;
143
- onSearch?: () => void;
188
+ onWebSearch?: () => void;
144
189
  onVoice?: () => void;
145
190
  /** Receive the imperative controller once mounted. The kai-chat facade forwards
146
191
  * these as element methods (focus/clear/send/scrollToBottom). */
@@ -194,6 +239,30 @@ export interface ChatThreadController {
194
239
  * Contract: `id` must be unique per message. It already had to be — the
195
240
  * feedback/copy state above this list is keyed by it.
196
241
  */
242
+ /** How an ASSISTANT row aligns its parts across the column (K-D11).
243
+ *
244
+ * `stretch`, not `start`. An assistant turn is a `flex flex-col` box, so under
245
+ * `items-start` every part is a flex item with a fit-content cross size —
246
+ * `min(max-content, column)`. Prose is wider than the column so a text bubble
247
+ * looked right, and a generative-UI card was as wide as its widest button: the
248
+ * ops-console parameters form measured 285px inside a 768px column while the
249
+ * approval card beside it filled all 768. A card in a chat thread filling its
250
+ * column is a fact about the medium, and no consumer can reach it — the card
251
+ * element is created inside `<kai-chat>`'s shadow root.
252
+ *
253
+ * Stretch rather than `w-full` on the cards, for two reasons: it is one lever
254
+ * instead of one per card surface (the Solid `Card` root AND every `kai-*`
255
+ * card host, whose shadow wrapper is `display: contents`), and stretching only
256
+ * applies where the cross size is `auto`, so a part that states its own width
257
+ * is untouched — `Attachments variant="grid"` stays `w-fit`. `Tool` and
258
+ * `Reasoning` already asked for `w-full` explicitly; this is the same
259
+ * intention, applied once.
260
+ *
261
+ * USER rows keep `items-end`: their bubble is `max-w-[85%]` and right aligned,
262
+ * and stretching it would break both.
263
+ */
264
+ const ASSISTANT_ALIGN = 'items-stretch';
265
+
197
266
  export function ChatThread(props: ChatThreadProps) {
198
267
  const outer = useChatConfig();
199
268
  const reveal = () => (props.actionsReveal === 'hover' ? 'hover' : 'always');
@@ -228,7 +297,7 @@ export function ChatThread(props: ChatThreadProps) {
228
297
  if ((props.suggestionMode ?? 'submit') === 'fill') { handleChange(v); props.onSuggestionClick?.(v); }
229
298
  else { props.onSubmit?.({ value: v, attachments: attachments() }); afterSubmit(); }
230
299
  };
231
- const showHeader = () => !!(props.chatTitle || props.models || props.context || props.headerStart || props.headerEnd);
300
+ const showHeader = () => !!(props.chatTitle || props.models || props.context || props.headerStart || props.headerEnd || props.headerEndContent);
232
301
  // Suggestions are conversation starters: show only on an empty thread unless
233
302
  // the host opts into persisting them.
234
303
  const visibleSuggestions = () =>
@@ -304,6 +373,11 @@ export function ChatThread(props: ChatThreadProps) {
304
373
  {/* Consumer-injected trailing controls (share, settings, …).
305
374
  Projects light-DOM `slot="header-end"` children of <kai-chat>. */}
306
375
  <slot name="header-end" />
376
+ {/* JSX escape hatch for a Solid-composed caller with no shadow-DOM
377
+ host to slot into (see the prop doc) — a docked widget's own
378
+ close affordance is the motivating case, sharing this row with
379
+ the title instead of floating as an unrelated second control. */}
380
+ {props.headerEndContent}
307
381
  </div>
308
382
  </header>
309
383
  </Show>
@@ -314,11 +388,15 @@ export function ChatThread(props: ChatThreadProps) {
314
388
  <div class="relative flex-1 overflow-hidden">
315
389
  <ChatContainer class="h-full px-4 py-3">
316
390
  <ChatContainerContent class="mx-auto w-full max-w-3xl space-y-4">
317
- {/* REPLACE — custom empty-state slot, shown only while the thread is
391
+ {/* REPLACE — custom empty-state content, shown only while the thread is
318
392
  empty. The component still owns WHEN it shows (data state); the
319
- consumer owns WHAT it looks like. */}
320
- <Show when={props.empty && props.messages.length === 0}>
321
- <slot name="empty" />
393
+ consumer owns WHAT it looks like. `emptyContent` (JSX, rendered
394
+ in-tree and fully styled) wins over `empty`/`slot="empty"`
395
+ (light-DOM projection) when both are set — see the prop doc. */}
396
+ <Show when={(props.empty || props.emptyContent) && props.messages.length === 0}>
397
+ <Show when={props.emptyContent} fallback={<slot name="empty" />}>
398
+ {props.emptyContent}
399
+ </Show>
322
400
  </Show>
323
401
  {/* Keyed by message id (see the note above this component), so a
324
402
  streaming delta updates the row instead of replacing it. */}
@@ -335,8 +413,19 @@ export function ChatThread(props: ChatThreadProps) {
335
413
  const body = (
336
414
  <MessageBody
337
415
  parts={m().parts}
416
+ /* F-21: streaming-ness = the thread's ONE existing
417
+ loading signal + being the last message and an
418
+ assistant turn. No second streaming source. The
419
+ reasoning disclosure no longer auto-opens on it
420
+ by default (Task 19f, owner ruling 2026-08-26) —
421
+ only the trigger's shimmer reflects streaming
422
+ unless `reasoningOpen` opts back in. */
423
+ isStreaming={props.loading === true && m().role === 'assistant' && i() === props.messages.length - 1}
424
+ reasoningMode={props.reasoning}
425
+ reasoningDefaultOpen={props.reasoningOpen}
338
426
  cardTypes={props.cardTypes}
339
427
  cardSchemas={props.cardSchemas}
428
+ cardHostElement={props.cardHostElement}
340
429
  isUser={m().role === 'user'}
341
430
  markdown={m().role === 'assistant'}
342
431
  actions={m().actions}
@@ -356,7 +445,7 @@ export function ChatThread(props: ChatThreadProps) {
356
445
  <Show
357
446
  when={m().avatar}
358
447
  fallback={
359
- <Message role={m().role} class={`${rowGroup()}${m().role === 'user' ? 'flex-col items-end' : 'flex-col items-start'}`}>
448
+ <Message role={m().role} class={`${rowGroup()}${m().role === 'user' ? 'flex-col items-end' : `flex-col ${ASSISTANT_ALIGN}`}`}>
360
449
  {body}
361
450
  </Message>
362
451
  }
@@ -364,7 +453,7 @@ export function ChatThread(props: ChatThreadProps) {
364
453
  {(av) => (
365
454
  <Message role={m().role} class={rowGroup()}>
366
455
  <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'}`}>
456
+ <div class={`flex min-w-0 flex-1 flex-col ${m().role === 'user' ? 'items-end' : ASSISTANT_ALIGN}`}>
368
457
  {body}
369
458
  </div>
370
459
  </Message>
@@ -379,7 +468,10 @@ export function ChatThread(props: ChatThreadProps) {
379
468
  </ChatContainerContent>
380
469
  <Show when={showScrollButton()}>
381
470
  <div class="absolute bottom-4 left-1/2 flex w-full max-w-3xl -translate-x-1/2 justify-center px-5">
382
- <ScrollButton class="shadow-sm" />
471
+ {/* The button now owns its elevation (kai-elevation); a `shadow-sm`
472
+ here would set box-shadow a second time and the winner would
473
+ be stylesheet order, not this call site. */}
474
+ <ScrollButton />
383
475
  </div>
384
476
  </Show>
385
477
  </ChatContainer>
@@ -401,11 +493,11 @@ export function ChatThread(props: ChatThreadProps) {
401
493
  value={current()} placeholder={props.placeholder} loading={props.loading === true}
402
494
  suggestions={visibleSuggestions()} attachments={attachments()}
403
495
  accept={props.accept} onAttachmentsRejected={props.onAttachmentsRejected}
404
- search={props.search === true} voice={props.voice === true}
496
+ attach={props.attach} webSearch={props.webSearch === true} voice={props.voice === true}
405
497
  triggers={props.triggers} kindIcons={props.kindIcons}
406
498
  onValueChange={handleChange} onSubmit={handleSubmit} onSuggestionClick={handleSuggestionClick}
407
499
  onAttachmentsChange={(a) => { setAttachments(a); props.onAttachmentsChange?.(a); }}
408
- onSearch={() => props.onSearch?.()} onVoice={() => props.onVoice?.()}
500
+ onWebSearch={() => props.onWebSearch?.()} onVoice={() => props.onVoice?.()}
409
501
  />
410
502
  }
411
503
  >
@@ -14,6 +14,7 @@ import {
14
14
  } from 'solid-js';
15
15
  import { cn } from '../utils/cn';
16
16
  import { Button } from '../ui/button';
17
+ import { Radio } from '../ui/radio';
17
18
  import { HoverCard } from '../ui/hover-card';
18
19
  import { Card } from './card';
19
20
  import { DismissedStub } from './dismissed-stub';
@@ -21,7 +22,7 @@ import type { CardEnvelope, CardEvent, CardHost, CardResolution } from '../primi
21
22
  import { useCardResolution } from './use-card-resolution';
22
23
  import { emitCardEvent } from '../primitives/card-routing';
23
24
  import { useCardHost } from '../primitives/card-host';
24
- import { Check, X } from 'lucide-solid';
25
+ import { Check } from 'lucide-solid';
25
26
 
26
27
  // ─────────────────────────────────────────────────────────────────────────────
27
28
  // Types (choice.schema.json) — AUTHORED IN ../primitives/card-data-types.ts.
@@ -430,6 +431,10 @@ export function ChoiceCard(props: ChoiceCardProps): JSX.Element {
430
431
  const groupLabel = (): string => local.heading ?? local.data?.prompt ?? 'Choose an option';
431
432
  const promptId = `kai-choice-prompt-${uid}`;
432
433
  const otherInputId = `kai-choice-other-${uid}`;
434
+ // The shared `name` is what makes the rows ONE native control: mutual exclusion,
435
+ // form participation and "2 of 4" from a screen reader all come from it. Scoped to
436
+ // this card's uid so two ChoiceCards on a page never de-select each other.
437
+ const radioName = `kai-choice-${uid}`;
433
438
 
434
439
  return (
435
440
  <Show when={valid()} fallback={<Card heading={local.heading} errorMessage={errorMessage()} />}>
@@ -443,14 +448,18 @@ export function ChoiceCard(props: ChoiceCardProps): JSX.Element {
443
448
  when={!res.isDeferred()}
444
449
  fallback={<DismissedStub type={CHOICE_CARD_TYPE} title={local.heading} onReopen={onReopen} />}
445
450
  >
446
- <Card heading={local.heading}>
451
+ {/* `prompt` is the card's supporting sentence — the same role `form`'s
452
+ `description` plays — so it goes through the shell's `description`
453
+ slot rather than being re-implemented in the body. That is what gives
454
+ it the muted colour: at full `text-foreground` it was the same colour
455
+ as the heading above it, which flattened the hierarchy. `descriptionId`
456
+ keeps the radiogroup's `aria-describedby` pointing at it. */}
457
+ <Card
458
+ heading={local.heading}
459
+ description={local.data?.prompt}
460
+ descriptionId={promptId}
461
+ >
447
462
  <div class={cn('flex flex-col gap-3', local.class)}>
448
- <Show when={local.data?.prompt}>
449
- <p id={promptId} class="text-sm text-foreground">
450
- {local.data?.prompt}
451
- </p>
452
- </Show>
453
-
454
463
  <Show
455
464
  when={!res.isResolved()}
456
465
  fallback={<ResolvedChoice choice={resolvedChoice()!} optimistic={res.isOptimistic()} />}
@@ -462,7 +471,7 @@ export function ChoiceCard(props: ChoiceCardProps): JSX.Element {
462
471
  aria-describedby={local.data?.prompt ? promptId : undefined}
463
472
  aria-disabled={isDisabled() ? 'true' : undefined}
464
473
  class={cn(
465
- 'divide-y divide-border overflow-hidden rounded-lg border border-input',
474
+ 'divide-y divide-border overflow-hidden rounded-lg border border-border',
466
475
  isDisabled() && 'cursor-not-allowed opacity-60',
467
476
  )}
468
477
  onKeyDown={onGroupKeyDown}
@@ -477,8 +486,10 @@ export function ChoiceCard(props: ChoiceCardProps): JSX.Element {
477
486
  return (
478
487
  <ListRow
479
488
  opt={opt}
489
+ name={radioName}
480
490
  checked={checked()}
481
491
  tabStop={tabStop()}
492
+ groupDisabled={isDisabled()}
482
493
  descId={descId}
483
494
  hasDesc={hasDesc()}
484
495
  onPick={() => {
@@ -518,14 +529,23 @@ export function ChoiceCard(props: ChoiceCardProps): JSX.Element {
518
529
 
519
530
  <div class="flex w-full flex-wrap items-center justify-between gap-2">
520
531
  <Show when={local.data?.dismissible === true}>
532
+ {/* The contract `dismiss` verb — a footer ACTION (it emits
533
+ `{kind:'dismiss'}` and collapses the card to a re-openable
534
+ stub), not the chrome close in `Card`'s own `dismissible`
535
+ prop, which the contract cards deliberately never set.
536
+ Labelled rather than a bare ghost ✕: every other control in
537
+ this row says what it does, and an unlabelled 28px glyph
538
+ 300px from the primary button reads as stray chrome. Same
539
+ shape in all four cards — `form` already looked like this.
540
+ `aria-label` is kept (redundant with the text) so the
541
+ element tests that select on it keep working. */}
521
542
  <Button
522
543
  type="button"
523
544
  variant="ghost"
524
- size="icon-sm"
525
545
  aria-label="Dismiss"
526
546
  onClick={onDismiss}
527
547
  >
528
- <X size={16} aria-hidden="true" />
548
+ Dismiss
529
549
  </Button>
530
550
  </Show>
531
551
  <Button type="button" class="ml-auto" disabled={!canSubmit()} onClick={submit}>
@@ -542,14 +562,19 @@ export function ChoiceCard(props: ChoiceCardProps): JSX.Element {
542
562
  }
543
563
 
544
564
  // ─────────────────────────────────────────────────────────────────────────────
545
- // Internal list-row presentation: a `role="radio"` with the kit's
546
- // selectable-list styling.
565
+ // Internal list-row presentation: a `<label>` row wrapping a REAL
566
+ // `<input type="radio">` (the `Radio` primitive), with the kit's selectable-list
567
+ // styling.
547
568
  // ─────────────────────────────────────────────────────────────────────────────
548
569
 
549
570
  interface RowProps {
550
571
  opt: ChoiceOption;
572
+ /** The shared form-control name every radio in this card's group carries. */
573
+ name: string;
551
574
  checked: boolean;
552
575
  tabStop: boolean;
576
+ /** The whole group is frozen (`disabled` prop), not just this row. */
577
+ groupDisabled: boolean;
553
578
  descId: string;
554
579
  hasDesc: boolean;
555
580
  onPick: () => void;
@@ -558,7 +583,7 @@ interface RowProps {
558
583
 
559
584
  function RecommendedPill(): JSX.Element {
560
585
  return (
561
- <span class="inline-flex items-center rounded-full bg-[var(--color-primary)] px-1.5 py-0.5 text-[0.625rem] font-medium uppercase leading-none tracking-wide text-[var(--color-primary-foreground,white)]">
586
+ <span class="inline-flex items-center rounded-full bg-[var(--color-primary)] px-1.5 py-0.5 text-micro font-medium uppercase leading-none tracking-wide text-[var(--color-primary-foreground,white)]">
562
587
  Recommended
563
588
  </span>
564
589
  );
@@ -587,25 +612,76 @@ function IconBadge(props: { name: string }): JSX.Element {
587
612
  );
588
613
  }
589
614
 
615
+ /**
616
+ * One row of the radiogroup.
617
+ *
618
+ * The control is a REAL `<input type="radio">` (the `Radio` primitive over
619
+ * `.kai-radio`), not the `aria-hidden` ring-shaped `<span>` this row used to draw.
620
+ * That is what buys form participation — a `name`, a `value`, and a real entry in a
621
+ * native `FormData` — which a `<div role="radio">` can never have, and it removes the
622
+ * fourth hand-rolled radio from the kit.
623
+ *
624
+ * WHY `role="radio"` IS ON THE INPUT AND NOT ON THE ROW. `choice-card.tsx`'s own
625
+ * keyboard handler and `response-compare.tsx` both drive `[role="radio"]`, an
626
+ * ATTRIBUTE selector that a native radio's implicit role does not match, so the role
627
+ * has to be written somewhere. Putting it on the `<label>` wrapper (which the plan
628
+ * suggested) would nest a radio inside a radio and announce the row twice; putting it
629
+ * on the input states the role the input already has. `aria-checked` / `aria-disabled`
630
+ * are likewise redundant with `checked` / `disabled` and kept in lockstep with them —
631
+ * they are the state the element tests read, and a redundant ARIA attribute that never
632
+ * disagrees with the DOM is harmless where a second radio in the a11y tree is not.
633
+ *
634
+ * The row's `<label>` is what makes a click anywhere on it select the option, which is
635
+ * the browser's own behaviour rather than the `onClick` re-implementation it replaces.
636
+ * The `onClick` is kept as well: it is the path the existing tests fire, and both paths
637
+ * land on the same idempotent `select()`.
638
+ */
590
639
  function ListRow(props: RowProps): JSX.Element {
640
+ // The row is inert when the option itself is disabled OR the whole group is frozen.
641
+ // The group case has to reach the native `disabled` attribute: an enabled input
642
+ // would visibly check itself on click while `select()` refuses the state change, and
643
+ // the DOM would then be lying about the selection.
644
+ const inert = (): boolean => props.opt.disabled === true || props.groupDisabled;
645
+ const pick = (): void => {
646
+ if (inert()) return;
647
+ props.onPick();
648
+ };
591
649
  return (
592
- <div
593
- role="radio"
594
- aria-checked={props.checked}
595
- aria-disabled={props.opt.disabled ? 'true' : undefined}
596
- aria-describedby={props.hasDesc ? props.descId : undefined}
597
- data-option-id={props.opt.id}
598
- tabindex={props.opt.disabled ? -1 : props.tabStop ? 0 : -1}
599
- onClick={() => !props.opt.disabled && props.onPick()}
600
- onFocus={props.onFocus}
650
+ <label
651
+ onClick={pick}
601
652
  class={cn(
602
- 'flex items-center gap-3 px-3 py-2.5 text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring',
653
+ 'flex items-center gap-3 px-3 py-2.5 text-sm transition-colors',
654
+ // The focus ring stays on the ROW and stays inset — the same 2px inset ring the
655
+ // focusable `div` painted. Focus now lands on the input inside it, so the
656
+ // selector moves from `focus-visible:` to `has-[:focus-visible]:`; the input's
657
+ // own outline is suppressed below so there is one ring, not two. That is the
658
+ // pattern src/elements/styles.css sanctions at its `:focus-visible` rule.
659
+ 'has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-inset has-[:focus-visible]:ring-ring',
603
660
  props.opt.disabled
604
661
  ? 'cursor-not-allowed opacity-60 text-foreground'
605
662
  : 'cursor-pointer hover:bg-muted/50',
606
663
  props.checked ? 'bg-accent font-medium text-accent-foreground' : 'text-foreground',
607
664
  )}
608
665
  >
666
+ {/* FIRST in the row, ahead of any media. It used to sit AFTER the thumbnail/icon
667
+ badge, so a list where only some options carried media had a ragged control
668
+ column (measured 98px vs 50px in the `WithOther` story). The control column is
669
+ the one thing every row shares, so it leads. */}
670
+ <Radio
671
+ role="radio"
672
+ name={props.name}
673
+ value={props.opt.id}
674
+ checked={props.checked}
675
+ disabled={inert()}
676
+ aria-checked={props.checked}
677
+ aria-disabled={props.opt.disabled ? 'true' : undefined}
678
+ aria-describedby={props.hasDesc ? props.descId : undefined}
679
+ data-option-id={props.opt.id}
680
+ tabindex={props.opt.disabled ? -1 : props.tabStop ? 0 : -1}
681
+ class="focus-visible:outline-none"
682
+ onChange={pick}
683
+ onFocus={props.onFocus}
684
+ />
609
685
  <Show when={props.opt.media?.image}>
610
686
  <HoverCard
611
687
  openDelay={150}
@@ -623,17 +699,6 @@ function ListRow(props: RowProps): JSX.Element {
623
699
  <Show when={!props.opt.media?.image && props.opt.media?.icon}>
624
700
  <IconBadge name={props.opt.media!.icon!} />
625
701
  </Show>
626
- <span
627
- aria-hidden="true"
628
- class={cn(
629
- 'flex h-[1.125rem] w-[1.125rem] shrink-0 items-center justify-center rounded-full border-[1.5px] transition-colors',
630
- props.checked ? 'border-[var(--color-primary)]' : 'border-input bg-background',
631
- )}
632
- >
633
- <Show when={props.checked}>
634
- <span class="h-2 w-2 rounded-full bg-[var(--color-primary)]" />
635
- </Show>
636
- </span>
637
702
  <span class="flex min-w-0 flex-col gap-0.5">
638
703
  <span class="flex items-center gap-2">
639
704
  <span class="truncate">{props.opt.label}</span>
@@ -652,7 +717,7 @@ function ListRow(props: RowProps): JSX.Element {
652
717
  {props.opt.meta}
653
718
  </span>
654
719
  </Show>
655
- </div>
720
+ </label>
656
721
  );
657
722
  }
658
723
 
@@ -663,7 +728,7 @@ function ResolvedChoice(props: {
663
728
  const c = props.choice;
664
729
  return (
665
730
  <div
666
- class="flex items-center gap-3 rounded-lg border border-input bg-accent px-3 py-2.5 text-sm font-medium text-accent-foreground"
731
+ class="flex items-center gap-3 rounded-lg border border-border bg-accent px-3 py-2.5 text-sm font-medium text-accent-foreground"
667
732
  role={props.optimistic ? 'status' : undefined}
668
733
  >
669
734
  <Check size={16} aria-hidden="true" />
@@ -228,7 +228,7 @@ export function Coachmark(props: CoachmarkProps) {
228
228
  <Show when={props.badge}>
229
229
  <span
230
230
  part="badge"
231
- class={cn('inline-flex items-center rounded-full px-1.5 py-0.5 text-[0.625rem] font-medium uppercase leading-none tracking-wide', BADGE_CLS)}
231
+ class={cn('inline-flex items-center rounded-full px-1.5 py-0.5 text-micro font-medium uppercase leading-none tracking-wide', BADGE_CLS)}
232
232
  >
233
233
  {props.badge}
234
234
  </span>
@@ -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
  );
@@ -53,8 +122,8 @@ function CodeBlockCode(props: CodeBlockCodeProps) {
53
122
 
54
123
  const codeTextSize = () => {
55
124
  switch (config.proseSize()) {
56
- case 'xs': return 'text-[11px]';
57
- case 'sm': return 'text-[13px]';
125
+ case 'xs': return 'text-caption';
126
+ case 'sm': return 'text-compact';
58
127
  case 'base': return 'text-sm';
59
128
  case 'lg': return 'text-base';
60
129
  }
@@ -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 };