@kitn.ai/ui 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (488) hide show
  1. package/README.md +25 -6
  2. package/bin/mcp.js +16 -3
  3. package/bin/route.js +23 -0
  4. package/bin/route.test.js +58 -0
  5. package/dist/{elements/chunks/Icon--Y8vsFal.js → Icon-DObhsRtm.js} +1 -1
  6. package/dist/action-icons-CcgNN2oT.js +1 -0
  7. package/dist/arrow-down-CjRU3JgV.js +1 -0
  8. package/dist/arrow-left-Bnpb1UEW.js +1 -0
  9. package/dist/artifact-Bwt7OgcR.js +1 -0
  10. package/dist/assets/dev-5XzXYR21.js +91 -0
  11. package/dist/attachments-Qr4kg-2b.js +1 -0
  12. package/dist/{elements/chunks/audio-visualizer-D7lB6mvR.js → audio-visualizer-DToYdfSc.js} +1 -1
  13. package/dist/badge-CdEu_jn2.js +1 -0
  14. package/dist/bash-InADTalH.js +1 -6
  15. package/dist/bell-DQAAOpDz.js +1 -0
  16. package/dist/{elements/chunks/button-flUbeufF.js → button-DkuOnGLy.js} +1 -1
  17. package/dist/card-renderer-Jx70m6_l.js +1 -0
  18. package/dist/card-routing-0YidER_3.js +1 -0
  19. package/dist/check-njkVdZu2.js +1 -0
  20. package/dist/checkbox-D7iRB3nL.js +1 -0
  21. package/dist/checkbox-group-B2_rpoPM.js +1 -0
  22. package/dist/chevron-down-B72s6PXR.js +1 -0
  23. package/dist/chevron-right-BatuHzUm.js +1 -0
  24. package/dist/choice-card-m0BYqqOK.js +1 -0
  25. package/dist/circle-check-B62pODB3.js +1 -0
  26. package/dist/circle-x-BPG0iPst.js +1 -0
  27. package/dist/circle-zx0ByzY9.js +1 -0
  28. package/dist/{elements/chunks/cn-CU7UAWzk.js → cn-DN5AWfiS.js} +1 -1
  29. package/dist/code-block-ClMLKb7C.js +1 -0
  30. package/dist/{elements/chunks/collapsible-BzbXpPzr.js → collapsible-B4DlWicL.js} +1 -1
  31. package/dist/components/attachment-types.d.ts +7 -0
  32. package/dist/components/audio-visualizer/fit-scale.d.ts +1 -1
  33. package/dist/components/card-renderer.d.ts +13 -0
  34. package/dist/components/card.d.ts +4 -0
  35. package/dist/components/chat-thread.d.ts +135 -2
  36. package/dist/components/conversation-item.d.ts +35 -7
  37. package/dist/components/conversation-list.d.ts +17 -5
  38. package/dist/components/conversation-panel.d.ts +29 -0
  39. package/dist/components/form-widgets.d.ts +67 -2
  40. package/dist/components/form.d.ts +48 -1
  41. package/dist/components/home-panel.d.ts +34 -0
  42. package/dist/components/message.d.ts +25 -3
  43. package/dist/components/reasoning.d.ts +6 -0
  44. package/dist/components/scroll-button.d.ts +11 -0
  45. package/dist/components/thread.d.ts +5 -0
  46. package/dist/components/tool-types.d.ts +9 -0
  47. package/dist/components/widget-tab-bar.d.ts +16 -0
  48. package/dist/composer-sQ0HcvyG.js +106 -0
  49. package/dist/confirm-card-rQWDQsnC.js +1 -0
  50. package/dist/construct-cli.es.js +1337 -0
  51. package/dist/{elements/chunks/context-VLIL3sJ-.js → context-0kg4ZX1J.js} +1 -1
  52. package/dist/controllable-CnGpMF6r.js +1 -0
  53. package/dist/conversation-item-BxwQLaFJ.js +1 -0
  54. package/dist/conversation-list-BCfGN1JU.js +1 -0
  55. package/dist/copy-n9u9rqpi.js +1 -0
  56. package/dist/core-AYMC6_lb.js +12 -5874
  57. package/dist/{create-tween-BFT6c-Y8.js → create-tween-C-q-JCM-.js} +1 -1
  58. package/dist/{create-tween-CJNlzoaO.js → create-tween-CRrJXxBd.js} +1 -1
  59. package/dist/{create-tween-CQED4T0g.js → create-tween-DZ3RRDHD.js} +1 -1
  60. package/dist/{elements/chunks/create-tween-c4Z8TDYz.js → create-tween-uPkArjIG.js} +2 -2
  61. package/dist/css-M7EaDHN_.js +1 -6
  62. package/dist/custom-elements.json +1433 -202
  63. package/dist/default-input-CaMrJkgH.js +1 -0
  64. package/dist/define-By-uL588.js +1 -0
  65. package/dist/define.d.ts +4 -0
  66. package/dist/define.js +510 -0
  67. package/dist/define.server.d.ts +5 -0
  68. package/dist/define.server.js +506 -0
  69. package/dist/{elements/chunks/disclosure-CcNbyLy0.js → disclosure-WUImBbDv.js} +1 -1
  70. package/dist/{elements/chunks/dropdown-BgWTthUx.js → dropdown-wZVeJOMd.js} +1 -1
  71. package/dist/elements/agent-card.js +1 -1
  72. package/dist/elements/artifact.js +1 -1
  73. package/dist/elements/attachments.js +1 -1
  74. package/dist/elements/audio-visualizer.js +1 -1
  75. package/dist/elements/autoloader.js +1 -1
  76. package/dist/elements/avatar.js +1 -1
  77. package/dist/elements/badge.js +1 -1
  78. package/dist/elements/button.js +1 -1
  79. package/dist/elements/card.js +1 -1
  80. package/dist/elements/cards.js +1 -1
  81. package/dist/elements/chain-of-thought.js +1 -1
  82. package/dist/elements/chat-scope-picker.js +1 -1
  83. package/dist/elements/chat-workspace.js +1 -1
  84. package/dist/elements/chat.js +1 -1
  85. package/dist/elements/checkbox-group.d.ts +1 -0
  86. package/dist/elements/checkbox-group.js +1 -0
  87. package/dist/elements/checkbox.d.ts +1 -0
  88. package/dist/elements/checkbox.js +1 -0
  89. package/dist/elements/checkpoint.js +1 -1
  90. package/dist/elements/choice.js +1 -1
  91. package/dist/elements/coachmark.js +1 -1
  92. package/dist/elements/code-block.js +1 -1
  93. package/dist/elements/command.js +1 -1
  94. package/dist/elements/compare.js +1 -1
  95. package/dist/elements/composer.js +1 -1
  96. package/dist/elements/confirm-card.js +1 -1
  97. package/dist/elements/context-meter.js +1 -1
  98. package/dist/elements/conversation-item.js +1 -1
  99. package/dist/elements/conversation-list.js +1 -1
  100. package/dist/elements/define-entry.d.ts +12 -0
  101. package/dist/elements/dialog.js +1 -1
  102. package/dist/elements/dock.js +102 -8
  103. package/dist/elements/dropdown.js +1 -1
  104. package/dist/elements/editable-label.js +1 -1
  105. package/dist/elements/element-data-types.d.ts +37 -0
  106. package/dist/elements/embed.js +1 -1
  107. package/dist/elements/empty.js +1 -1
  108. package/dist/elements/feedback-bar.js +1 -1
  109. package/dist/elements/file-tree.js +1 -1
  110. package/dist/elements/file-upload.js +1 -1
  111. package/dist/elements/form.js +1 -1
  112. package/dist/elements/hover-card.js +1 -1
  113. package/dist/elements/icon.js +1 -1
  114. package/dist/elements/image.js +1 -1
  115. package/dist/elements/input.js +1 -1
  116. package/dist/elements/kbd.js +1 -1
  117. package/dist/elements/link-preview.js +1 -1
  118. package/dist/elements/loader.js +1 -1
  119. package/dist/elements/markdown.js +1 -1
  120. package/dist/elements/menu.js +1 -1
  121. package/dist/elements/message-skills.js +1 -1
  122. package/dist/elements/message.js +1 -1
  123. package/dist/elements/model-switcher.js +1 -1
  124. package/dist/elements/nav.js +1 -1
  125. package/dist/elements/notice.js +1 -1
  126. package/dist/elements/pane-grid.d.ts +1 -0
  127. package/dist/elements/pane-grid.js +1 -0
  128. package/dist/elements/pane-group.js +1 -1
  129. package/dist/elements/pane.js +1 -1
  130. package/dist/elements/popover.js +1 -1
  131. package/dist/elements/progress-bar.js +1 -1
  132. package/dist/elements/prompt-dock.js +1 -1
  133. package/dist/elements/prompt-input.js +1 -1
  134. package/dist/elements/prompt-suggestions.js +1 -1
  135. package/dist/elements/radio-group.d.ts +1 -0
  136. package/dist/elements/radio-group.js +1 -0
  137. package/dist/elements/reasoning.js +1 -1
  138. package/dist/elements/remote.js +1 -1
  139. package/dist/elements/resizable.js +1 -1
  140. package/dist/elements/response-stream.js +2 -2
  141. package/dist/elements/screen.js +1 -1
  142. package/dist/elements/scroll-area.js +1 -1
  143. package/dist/elements/scroll-button.js +1 -1
  144. package/dist/elements/search.js +1 -1
  145. package/dist/elements/segmented.js +1 -1
  146. package/dist/elements/select.d.ts +1 -0
  147. package/dist/elements/select.js +1 -0
  148. package/dist/elements/separator.js +1 -1
  149. package/dist/elements/setting-item.js +1 -1
  150. package/dist/elements/settings-group.js +1 -1
  151. package/dist/elements/skeleton.js +1 -1
  152. package/dist/elements/slider.d.ts +1 -0
  153. package/dist/elements/slider.js +1 -0
  154. package/dist/elements/slots.d.ts +1 -1
  155. package/dist/elements/source.js +1 -1
  156. package/dist/elements/status.js +1 -1
  157. package/dist/elements/switch.js +1 -1
  158. package/dist/elements/tabs.js +1 -1
  159. package/dist/elements/tasks.js +1 -1
  160. package/dist/elements/text-shimmer.js +1 -1
  161. package/dist/elements/thinking-bar.js +1 -1
  162. package/dist/elements/thread.js +1 -1
  163. package/dist/elements/toast.js +1 -1
  164. package/dist/elements/tool.js +1 -1
  165. package/dist/elements/tooltip.js +1 -1
  166. package/dist/elements/voice-input.js +1 -1
  167. package/dist/elements/voice-output.js +1 -1
  168. package/dist/elements.d.ts +380 -26
  169. package/dist/{elements/chunks/ellipsis-7gU33mL2.js → ellipsis-CbOpJnU3.js} +1 -1
  170. package/dist/embed-CrfjkbUG.js +1 -0
  171. package/dist/engine-javascript-vq0WuIJl.js +14 -2516
  172. package/dist/{elements/chunks/external-link-CSOEsNlx.js → external-link-ByXAwBBQ.js} +1 -1
  173. package/dist/file-text-DMib2qwt.js +1 -0
  174. package/dist/{elements/chunks/file-tree-zvB2i7P9.js → file-tree-CJqM2aP7.js} +1 -1
  175. package/dist/folder-_BAb929C.js +1 -0
  176. package/dist/form-DSZgZQZk.js +1 -0
  177. package/dist/github-dark-dimmed-DUshB20C.js +1 -4
  178. package/dist/github-light-JYsPkUQd.js +1 -4
  179. package/dist/hover-card-BQbul9eq.js +1 -0
  180. package/dist/html-CPZ3oZQ7.js +1 -10
  181. package/dist/icon-CdcfRhYI.js +1 -0
  182. package/dist/{elements/chunks/index-BCRhi84a.js → index-xu_pdcPi.js} +1 -1
  183. package/dist/index.d.ts +4 -2
  184. package/dist/index.js +7474 -5857
  185. package/dist/index.server.js +5965 -4762
  186. package/dist/info-DsFjGuI_.js +1 -0
  187. package/dist/input-DKbmkMos.js +1 -0
  188. package/dist/javascript-C25yR2R2.js +1 -6
  189. package/dist/json-DxJze_jm.js +1 -6
  190. package/dist/kai.es.js +1 -1
  191. package/dist/kbd-PvdGhX4s.js +1 -0
  192. package/dist/{elements/chunks/link-preview-OQ9IEq0f.js → link-preview-DbM1866N.js} +1 -1
  193. package/dist/link-zF8PzWfF.js +1 -0
  194. package/dist/loader-BM4CEz4G.js +1 -0
  195. package/dist/{elements/chunks/markdown-DgBFaGAz.js → markdown-LMvmAyBk.js} +7 -7
  196. package/dist/mcp.es.js +1361 -81
  197. package/dist/message-BSsgvHvf.js +1 -0
  198. package/dist/message-BYZECyek.js +1 -0
  199. package/dist/message-circle-DruwObfX.js +1 -0
  200. package/dist/message-square-BG6q6mlm.js +1 -0
  201. package/dist/{elements/chunks/minimize-2-DPIUKTrT.js → minimize-2-BQRoBV1V.js} +1 -1
  202. package/dist/{elements/chunks/model-switcher-DsT8tFB0.js → model-switcher-CMpCOYcB.js} +1 -1
  203. package/dist/{elements/chunks/overlay-DuiwdXjm.js → overlay-Cb-MkyDa.js} +1 -1
  204. package/dist/panel-right-i8warFxT.js +1 -0
  205. package/dist/paperclip-ByY3UVO6.js +1 -0
  206. package/dist/play-BvKKq3fX.js +1 -0
  207. package/dist/primitives/card-data-types.d.ts +49 -0
  208. package/dist/primitives/card-routing.d.ts +1 -33
  209. package/dist/primitives/card-validate.d.ts +2 -2
  210. package/dist/primitives/chat-config.d.ts +29 -2
  211. package/dist/primitives/conversation-store.d.ts +54 -0
  212. package/dist/primitives/field-mask.d.ts +96 -0
  213. package/dist/primitives/field-semantics.d.ts +36 -0
  214. package/dist/primitives/input-mask.d.ts +46 -0
  215. package/dist/primitives/url-scheme-policy.d.ts +34 -0
  216. package/dist/primitives/use-resize-observer.d.ts +2 -2
  217. package/dist/progress-bar-jVAHvMUf.js +1 -0
  218. package/dist/prompt-suggestion-CD-qtNgC.js +1 -0
  219. package/dist/radio-BsoBMhkF.js +1 -0
  220. package/dist/react/index.d.ts +586 -19
  221. package/dist/react.js +223 -187
  222. package/dist/reasoning-4kBPtlTx.js +1 -0
  223. package/dist/register-impl-B1hLkrI8.js +424 -0
  224. package/dist/{elements/chunks/resizable-D181b4fM.js → resizable-uPmY9VY-.js} +1 -1
  225. package/dist/rotate-ccw-B2xn93py.js +1 -0
  226. package/dist/rotate-cw-CMVLoCyH.js +1 -0
  227. package/dist/schemas/form.schema.json +21 -1
  228. package/dist/schemas/tool-defs.d.ts +11 -1
  229. package/dist/schemas.js +81 -81
  230. package/dist/scroll-area-BqLUo8ZZ.js +1 -0
  231. package/dist/scroll-button-DslXiJyL.js +1 -0
  232. package/dist/select-2cLRtPGT.js +1 -0
  233. package/dist/{elements/chunks/separator-CeJHQqLW.js → separator-BWQtNJcM.js} +1 -1
  234. package/dist/{elements/chunks/settings-CqaHgJF9.js → settings-BBR63jLg.js} +1 -1
  235. package/dist/{elements/chunks/settings-group-CjqjLCOn.js → settings-group-B2ciy-Iw.js} +1 -1
  236. package/dist/{elements/chunks/skeleton-D8akBnKS.js → skeleton-CkHI-HgC.js} +1 -1
  237. package/dist/slider-DDR9U-i0.js +1 -0
  238. package/dist/{solid-BtdqNZI7.js → solid-BzCFdL25.js} +9771 -7594
  239. package/dist/{solid-CnavwBye.js → solid-DruVOXnf.js} +7893 -6183
  240. package/dist/solid.d.ts +16 -2
  241. package/dist/solid.js +256 -243
  242. package/dist/solid.server.js +256 -243
  243. package/dist/source-GEnFF528.js +1 -0
  244. package/dist/star-CilHq8GP.js +1 -0
  245. package/dist/state/index.d.ts +1 -1
  246. package/dist/state/mock.d.ts +32 -2
  247. package/dist/state.js +133 -108
  248. package/dist/store-Ciyk6eZG.js +1 -0
  249. package/dist/svelte--5p79yCD.js +1 -15
  250. package/dist/switch-KkeQIbuH.js +1 -0
  251. package/dist/tasks-card-BrN4bggJ.js +1 -0
  252. package/dist/text-shimmer-Dcj1MHuq.js +1 -0
  253. package/dist/textarea-Dpvbngbg.js +1 -0
  254. package/dist/theme.tokens.css +110 -13
  255. package/dist/{elements/chunks/thumbs-up-DX2Ijk3H.js → thumbs-up-CsT9Wzb9.js} +1 -1
  256. package/dist/toast-store-tgK6MuMv.js +1 -0
  257. package/dist/{elements/chunks/tool-DKQ2D1DP.js → tool-DzUjIBT8.js} +1 -1
  258. package/dist/{elements/chunks/tooltip-B0G99y8w.js → tooltip-Bkkg_4t0.js} +1 -1
  259. package/dist/trash-2-jBKxjGGQ.js +1 -0
  260. package/dist/{elements/chunks/triangle-alert-F9cyUEyy.js → triangle-alert-DlQ68ppf.js} +1 -1
  261. package/dist/tsx-B8rCNbgL.js +1 -6
  262. package/dist/types.d.ts +48 -3
  263. package/dist/typescript-RycA9KXf.js +1 -6
  264. package/dist/ui/checkbox-group.d.ts +83 -0
  265. package/dist/ui/checkbox.d.ts +40 -0
  266. package/dist/ui/dock.d.ts +51 -0
  267. package/dist/ui/icon.d.ts +3 -0
  268. package/dist/ui/input.d.ts +48 -2
  269. package/dist/ui/radio.d.ts +85 -0
  270. package/dist/ui/select.d.ts +71 -0
  271. package/dist/ui/slider.d.ts +65 -0
  272. package/dist/ui/switch.d.ts +9 -2
  273. package/dist/upload-TcAHXv9W.js +1 -0
  274. package/dist/url-scheme-policy-DR5DgvkM.js +1 -0
  275. package/dist/use-card-resolution-DaZqAdWf.js +1 -0
  276. package/dist/{elements/chunks/variant-aurora-qt6Zct8M.js → variant-aurora-BhLuYxJR.js} +1 -1
  277. package/dist/{variant-aurora-B8II3-I1.js → variant-aurora-CNmfHhcx.js} +2 -2
  278. package/dist/{variant-aurora-CMNWOSJR.js → variant-aurora-D6GsdTPA.js} +2 -2
  279. package/dist/{variant-aurora-CcuDMWwQ.js → variant-aurora-DbW_OWL2.js} +1 -1
  280. package/dist/{elements/chunks/variant-custom-tJp22_BQ.js → variant-custom-BRvaNHSi.js} +1 -1
  281. package/dist/{variant-custom-L16JLLZy.js → variant-custom-BvYHR62b.js} +1 -1
  282. package/dist/{variant-custom-BdvYC7af.js → variant-custom-DMTrEO5w.js} +2 -2
  283. package/dist/{variant-custom-C714mqKl.js → variant-custom-DV2x254s.js} +2 -2
  284. package/dist/{variant-wave-ecwykeyL.js → variant-wave-BNMGrDfj.js} +2 -2
  285. package/dist/{variant-wave-2S4La2Ka.js → variant-wave-BpDd8t3Z.js} +1 -1
  286. package/dist/{variant-wave-D7ruiA1W.js → variant-wave-CyBueXj9.js} +2 -2
  287. package/dist/{elements/chunks/variant-wave-CaQ5ti39.js → variant-wave-sp-qwsdI.js} +1 -1
  288. package/dist/vue-BmIZj4XD.js +1 -33
  289. package/dist/x-Z5UntkIJ.js +1 -0
  290. package/frameworks/react/index.tsx +210 -22
  291. package/llms-full.txt +1184 -22
  292. package/llms.txt +6 -3
  293. package/package.json +17 -4
  294. package/src/agent-tooling/README.md +192 -0
  295. package/src/agent-tooling/catalog/invariants.ts +3 -3
  296. package/src/agent-tooling/catalog/surfaces.ts +72 -0
  297. package/src/agent-tooling/construct/cli-entry.ts +8 -0
  298. package/src/agent-tooling/construct/cli.ts +147 -0
  299. package/src/agent-tooling/construct/codegen.ts +1635 -0
  300. package/src/agent-tooling/construct/construct.v1.schema.json +319 -0
  301. package/src/agent-tooling/construct/dev.ts +148 -0
  302. package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +7 -0
  303. package/src/agent-tooling/construct/fixtures/ops-console.construct.json +44 -0
  304. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +32 -0
  305. package/src/agent-tooling/construct/schema.ts +452 -0
  306. package/src/agent-tooling/mcp/server.ts +2 -1
  307. package/src/agent-tooling/mcp/tools/construct.ts +130 -0
  308. package/src/agent-tooling/mcp/tools/debug.ts +126 -4
  309. package/src/agent-tooling/mcp/tools/reference.ts +104 -3
  310. package/src/agent-tooling/recipes/composed-thread.ts +714 -0
  311. package/src/agent-tooling/recipes/index.ts +22 -0
  312. package/src/agent-tooling/recipes/types.ts +32 -0
  313. package/src/agent-tooling/registry.ts +5 -0
  314. package/src/components/attachment-types.ts +7 -0
  315. package/src/components/audio-visualizer/fit-scale.ts +1 -1
  316. package/src/components/card-renderer.tsx +39 -8
  317. package/src/components/card.tsx +10 -2
  318. package/src/components/chat-scope-picker.tsx +2 -2
  319. package/src/components/chat-thread.tsx +602 -43
  320. package/src/components/choice-card.tsx +102 -37
  321. package/src/components/coachmark.tsx +1 -1
  322. package/src/components/code-block.tsx +2 -2
  323. package/src/components/composer.tsx +89 -15
  324. package/src/components/confirm-card.tsx +20 -4
  325. package/src/components/conversation-item.tsx +56 -9
  326. package/src/components/conversation-list.tsx +25 -10
  327. package/src/components/conversation-panel.tsx +118 -0
  328. package/src/components/empty.tsx +14 -3
  329. package/src/components/file-tree.tsx +5 -1
  330. package/src/components/form-widgets.tsx +257 -195
  331. package/src/components/form.tsx +297 -9
  332. package/src/components/home-panel.tsx +159 -0
  333. package/src/components/loader.tsx +12 -5
  334. package/src/components/message-skills.tsx +1 -1
  335. package/src/components/message.tsx +71 -11
  336. package/src/components/prompt-input.tsx +7 -1
  337. package/src/components/prompt-suggestion.tsx +4 -1
  338. package/src/components/reasoning.tsx +30 -3
  339. package/src/components/response-compare.tsx +2 -2
  340. package/src/components/scroll-button.tsx +56 -4
  341. package/src/components/source.tsx +6 -1
  342. package/src/components/tasks-card.tsx +58 -16
  343. package/src/components/thread.tsx +10 -1
  344. package/src/components/toast.tsx +1 -1
  345. package/src/components/tool-types.ts +9 -0
  346. package/src/components/widget-tab-bar.tsx +73 -0
  347. package/src/elements/attachments.tsx +3 -1
  348. package/src/elements/chat-workspace.tsx +1 -1
  349. package/src/elements/chat.tsx +115 -5
  350. package/src/elements/checkbox-group.tsx +164 -0
  351. package/src/elements/checkbox.tsx +134 -0
  352. package/src/elements/compiled.css +1 -1
  353. package/src/elements/composer.tsx +12 -1
  354. package/src/elements/conversation-item.tsx +62 -22
  355. package/src/elements/conversation-list.tsx +2 -2
  356. package/src/elements/define-entry.ts +12 -0
  357. package/src/elements/dock.tsx +11 -0
  358. package/src/elements/element-data-types.ts +40 -0
  359. package/src/elements/element-manifest.json +24 -0
  360. package/src/elements/element-meta.json +767 -34
  361. package/src/elements/element-nonscalar.json +17 -0
  362. package/src/elements/element-types.d.ts +380 -26
  363. package/src/elements/feedback-bar.tsx +5 -0
  364. package/src/elements/icon-names.json +29 -0
  365. package/src/elements/input.tsx +312 -23
  366. package/src/elements/message.tsx +2 -0
  367. package/src/elements/pane-grid.tsx +111 -0
  368. package/src/elements/prompt-input.tsx +8 -2
  369. package/src/elements/radio-group.tsx +129 -0
  370. package/src/elements/register-impl.ts +6 -0
  371. package/src/elements/scroll-button.tsx +54 -6
  372. package/src/elements/select.tsx +165 -0
  373. package/src/elements/slider.tsx +171 -0
  374. package/src/elements/slots.ts +1 -1
  375. package/src/elements/styles.css +89 -3
  376. package/src/elements/thread.tsx +2 -0
  377. package/src/elements/toast.tsx +14 -1
  378. package/src/index.ts +5 -2
  379. package/src/primitives/card-data-types.ts +49 -0
  380. package/src/primitives/card-host.tsx +4 -2
  381. package/src/primitives/card-routing.ts +7 -56
  382. package/src/primitives/card-schemas/form.schema.json +21 -1
  383. package/src/primitives/card-validate.ts +10 -2
  384. package/src/primitives/chat-config.tsx +33 -6
  385. package/src/primitives/conversation-store.ts +248 -0
  386. package/src/primitives/field-mask.ts +256 -0
  387. package/src/primitives/field-semantics.ts +115 -0
  388. package/src/primitives/input-mask.ts +853 -0
  389. package/src/primitives/toast-store.ts +66 -4
  390. package/src/primitives/url-scheme-policy.ts +70 -0
  391. package/src/primitives/use-resize-observer.ts +2 -2
  392. package/src/schemas/tool-defs.ts +12 -2
  393. package/src/solid.ts +20 -2
  394. package/src/state/index.ts +1 -1
  395. package/src/state/mock.ts +92 -6
  396. package/src/types.ts +45 -3
  397. package/src/ui/agent-card.tsx +2 -2
  398. package/src/ui/avatar.tsx +1 -1
  399. package/src/ui/checkbox-group.tsx +153 -0
  400. package/src/ui/checkbox.tsx +63 -0
  401. package/src/ui/dock.tsx +192 -7
  402. package/src/ui/icon.tsx +53 -0
  403. package/src/ui/input.tsx +249 -9
  404. package/src/ui/kbd.tsx +1 -1
  405. package/src/ui/nav.tsx +1 -1
  406. package/src/ui/pane-group.tsx +2 -2
  407. package/src/ui/radio.tsx +150 -0
  408. package/src/ui/select.tsx +168 -0
  409. package/src/ui/slider.tsx +178 -0
  410. package/src/ui/switch.tsx +49 -18
  411. package/src/utils/cn.ts +11 -5
  412. package/theme.css +112 -13
  413. package/dist/elements/chunks/action-icons-D9u3OzYW.js +0 -1
  414. package/dist/elements/chunks/arrow-left-CVh1TeN7.js +0 -1
  415. package/dist/elements/chunks/artifact-BkYcfIlZ.js +0 -1
  416. package/dist/elements/chunks/attachments-DwfNlubq.js +0 -1
  417. package/dist/elements/chunks/badge-Q0-Cmwzz.js +0 -1
  418. package/dist/elements/chunks/bash-InADTalH.js +0 -1
  419. package/dist/elements/chunks/card-renderer-3Dyl-FpN.js +0 -1
  420. package/dist/elements/chunks/card-routing-BK09BdmL.js +0 -1
  421. package/dist/elements/chunks/check-B5kBPKeo.js +0 -1
  422. package/dist/elements/chunks/chevron-down-DZ1GS39h.js +0 -1
  423. package/dist/elements/chunks/chevron-right-CvP7OUd2.js +0 -1
  424. package/dist/elements/chunks/choice-card-D4QUIOWZ.js +0 -1
  425. package/dist/elements/chunks/circle-CDf9g6MQ.js +0 -1
  426. package/dist/elements/chunks/circle-check-CA47Nqbz.js +0 -1
  427. package/dist/elements/chunks/circle-x-CJ_HVbC8.js +0 -1
  428. package/dist/elements/chunks/code-block-BdLiTitd.js +0 -1
  429. package/dist/elements/chunks/composer-Byve-n8K.js +0 -69
  430. package/dist/elements/chunks/confirm-card-B0flE3MN.js +0 -1
  431. package/dist/elements/chunks/controllable-DTwTIlwp.js +0 -1
  432. package/dist/elements/chunks/conversation-item-BDztezOG.js +0 -1
  433. package/dist/elements/chunks/copy-Nf-kqxXH.js +0 -1
  434. package/dist/elements/chunks/core-AYMC6_lb.js +0 -12
  435. package/dist/elements/chunks/css-M7EaDHN_.js +0 -1
  436. package/dist/elements/chunks/default-input-C2iLFq0b.js +0 -1
  437. package/dist/elements/chunks/define-wjHR1MkU.js +0 -1
  438. package/dist/elements/chunks/download-C4V7tBdb.js +0 -1
  439. package/dist/elements/chunks/embed-D3KewsIJ.js +0 -1
  440. package/dist/elements/chunks/engine-javascript-vq0WuIJl.js +0 -141
  441. package/dist/elements/chunks/file-text-D1G8khle.js +0 -1
  442. package/dist/elements/chunks/folder-WW2LGLid.js +0 -1
  443. package/dist/elements/chunks/form-CQirdC4W.js +0 -1
  444. package/dist/elements/chunks/github-dark-dimmed-DUshB20C.js +0 -1
  445. package/dist/elements/chunks/github-light-JYsPkUQd.js +0 -1
  446. package/dist/elements/chunks/hover-card-CJJkfHTK.js +0 -1
  447. package/dist/elements/chunks/html-CPZ3oZQ7.js +0 -1
  448. package/dist/elements/chunks/icon-Cfgmf4fa.js +0 -1
  449. package/dist/elements/chunks/info-DX3YMOt6.js +0 -1
  450. package/dist/elements/chunks/input-BtW9U6Sq.js +0 -1
  451. package/dist/elements/chunks/javascript-C25yR2R2.js +0 -1
  452. package/dist/elements/chunks/json-DxJze_jm.js +0 -1
  453. package/dist/elements/chunks/kbd-LGXwuT_V.js +0 -1
  454. package/dist/elements/chunks/link-2My69BqT.js +0 -1
  455. package/dist/elements/chunks/loader-iOxc_Tu-.js +0 -1
  456. package/dist/elements/chunks/message-VMZohrXx.js +0 -1
  457. package/dist/elements/chunks/message-circle-CpXMM-_n.js +0 -1
  458. package/dist/elements/chunks/message-square-DxcWtETQ.js +0 -1
  459. package/dist/elements/chunks/message-tZmV2A3p.js +0 -1
  460. package/dist/elements/chunks/paperclip-CRZVp4z_.js +0 -1
  461. package/dist/elements/chunks/progress-bar-DNy66e_E.js +0 -1
  462. package/dist/elements/chunks/prompt-suggestion-Bn64Nl7L.js +0 -1
  463. package/dist/elements/chunks/reasoning-Emn9N4-T.js +0 -1
  464. package/dist/elements/chunks/rotate-cw-CPWe37D9.js +0 -1
  465. package/dist/elements/chunks/scroll-area-ClZkZxIk.js +0 -1
  466. package/dist/elements/chunks/scroll-button-DPE-t3x_.js +0 -1
  467. package/dist/elements/chunks/share-CyfnNEqp.js +0 -1
  468. package/dist/elements/chunks/source-UWCvmVak.js +0 -1
  469. package/dist/elements/chunks/star-DoHFAikQ.js +0 -1
  470. package/dist/elements/chunks/store-Cjq0qwYN.js +0 -1
  471. package/dist/elements/chunks/svelte--5p79yCD.js +0 -1
  472. package/dist/elements/chunks/tasks-card-CmZ5xh6v.js +0 -1
  473. package/dist/elements/chunks/text-shimmer-DYn09zrz.js +0 -1
  474. package/dist/elements/chunks/textarea-skiGHURM.js +0 -1
  475. package/dist/elements/chunks/toast-store-CFnQpThX.js +0 -1
  476. package/dist/elements/chunks/tsx-B8rCNbgL.js +0 -1
  477. package/dist/elements/chunks/typescript-RycA9KXf.js +0 -1
  478. package/dist/elements/chunks/use-card-resolution-DoA2f_EU.js +0 -1
  479. package/dist/elements/chunks/vue-BmIZj4XD.js +0 -1
  480. package/dist/elements/chunks/x-B6UnOiLw.js +0 -1
  481. package/dist/llms/llms-full.txt +0 -2949
  482. package/dist/llms/llms.txt +0 -156
  483. package/dist/register-impl-DpE7icIb.js +0 -293
  484. package/dist/ui/stat.d.ts +0 -16
  485. package/src/ui/stat.tsx +0 -41
  486. /package/dist/{elements/chunks/card-tags-D8lZ-C_U.js → card-tags-D8lZ-C_U.js} +0 -0
  487. /package/dist/{elements/chunks/link-preview-DNILK391.js → link-preview-DNILK391.js} +0 -0
  488. /package/dist/{elements/chunks/slots-DaNjX0rc.js → slots-DaNjX0rc.js} +0 -0
@@ -0,0 +1,1337 @@
1
+ import { existsSync, readFileSync, rmSync, mkdirSync, writeFileSync, copyFileSync } from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import { join, dirname, resolve } from "node:path";
4
+ import { z } from "zod";
5
+ import { createRequire } from "node:module";
6
+ const SAFE_SCHEMES = ["http:", "https:", "mailto:"];
7
+ function schemeOf(url) {
8
+ try {
9
+ return new URL(url, "http://_invalid_base").protocol;
10
+ } catch {
11
+ return void 0;
12
+ }
13
+ }
14
+ function isSafeUrl(url) {
15
+ const scheme = schemeOf(url);
16
+ return scheme !== void 0 && SAFE_SCHEMES.includes(scheme);
17
+ }
18
+ const TAG_RE = /^[a-z][a-z0-9]*-[a-z0-9-]+$/;
19
+ const ProviderSchema = z.discriminatedUnion("mode", [
20
+ z.object({ mode: z.literal("mock") }).strict(),
21
+ z.object({
22
+ mode: z.literal("endpoint"),
23
+ /** The CONSUMER's chat route. Kit parses, consumer fetches. */
24
+ url: z.string().min(1),
25
+ wire: z.enum(["openai", "anthropic"])
26
+ }).strict()
27
+ ]);
28
+ const ConstructSchema = z.object({
29
+ $schema: z.string().optional(),
30
+ /** The emitted tag: <acme-support>. Must satisfy customElements.define. */
31
+ name: z.string().regex(TAG_RE, 'must be a valid custom-element tag: lowercase, with a hyphen (e.g. "acme-support")'),
32
+ // Widened progressively: fullscreen/aside/split landed in Task 12, custom in Task 13.
33
+ layout: z.enum(["widget", "fullscreen", "aside", "split", "custom"]),
34
+ provider: ProviderSchema,
35
+ /** Plain (unsigned) identity passthrough — no signing/auth infra (owner
36
+ * ruling, 2026-08-26: signed JWT/HMAC identity is later additive
37
+ * vocabulary, not this field). TOP-LEVEL, not nested under `provider`:
38
+ * local history scoping (`capabilities.history.persistence: 'local'`)
39
+ * needs userId independent of provider mode — nesting it inside the
40
+ * provider union would make it silently inert for `mode: 'mock'` +
41
+ * local history, a real and common combination. Threaded as the
42
+ * `x-kai-user-id` header on every emitted fetch to the CONSUMER's own
43
+ * backend (the endpoint provider's chat POST, history's endpoint
44
+ * GET/PUT), and folded into the localStorage key for `local` history so
45
+ * different users on the same browser profile don't share one thread.
46
+ * Constrained to printable-ASCII (no CR/LF, no non-ISO-8859-1 code
47
+ * points) beyond the usual construct-authored-text escaping: this value
48
+ * reaches an HTTP HEADER VALUE, not just a JS string literal, and
49
+ * `fetch()` throws at RUNTIME on a header value containing CR/LF or
50
+ * outside ISO-8859-1 — a bad userId would surface as an opaque crash in
51
+ * the CONSUMER's generated app, not a construct-validation error. Reject
52
+ * loudly here instead, where the author gets a path + message. */
53
+ userId: z.string().min(1).regex(
54
+ /^[\x20-\x7E]+$/,
55
+ "must be printable ASCII with no line breaks (it becomes an HTTP header value)"
56
+ ).optional(),
57
+ theme: z.object({
58
+ /** Any CSS color; becomes --kai-color-primary on the host. */
59
+ accent: z.string().optional(),
60
+ /** Any CSS color; becomes --kai-color-unread on the host (owner
61
+ * ruling, 2026-08-26 — the unread-indicator round). Same treatment
62
+ * as `accent` in every way that matters for safety: construct-
63
+ * authored/untrusted text, carried to the emitted facade via
64
+ * `ctx.element.style.setProperty('--kai-color-unread',
65
+ * JSON.stringify(...))` — never string-interpolated into CSS
66
+ * text — so it can never break out into a new declaration or rule
67
+ * (see emitElement's doc on why `accent` uses setProperty at all).
68
+ * Unlike `accent`, this has no paired -foreground to compute: the
69
+ * three surfaces that read --color-unread (the conversation-list
70
+ * row dot, the header toggle's badge, Dock's own closed-launcher
71
+ * badge) are all small filled dots with no text sitting ON them, so
72
+ * there is nothing to contrast-pair — the value is set and used
73
+ * as-is. */
74
+ unreadColor: z.string().optional(),
75
+ mode: z.enum(["light", "dark", "system"]).default("system")
76
+ }).strict().optional(),
77
+ /** Construct-wide header chrome, valid on every layout (not layout-scoped
78
+ * like `widget`, and not a capability toggle — a header is a construct-
79
+ * wide fact, like `theme`). Rendered inside ChatThread's own built-in
80
+ * header bar; a logo/icon projects through the kit's EXISTING
81
+ * `header-start` named slot (`slots` vocabulary above), not a second
82
+ * image-prop convention here. */
83
+ header: z.object({
84
+ /** Rendered in ChatThread's built-in header bar (left side). Construct-
85
+ * authored/untrusted text, like theme.accent/provider.url — JSON.stringify'd
86
+ * at its one emit site, never a raw JSX attribute string. */
87
+ title: z.string().min(1).optional()
88
+ }).strict().optional(),
89
+ /** Greeting shown while the thread is empty (no messages yet) — the
90
+ * proven "welcome screen" pattern (Intercom-class): title + optional
91
+ * description + optional icon above, with `capabilities.starters`'
92
+ * chips and the composer still rendering below (ChatThread's `empty`
93
+ * REPLACE slot only stands in for the empty MESSAGE LIST — see
94
+ * chat-thread.tsx's own doc comment on `empty` — so the chips are
95
+ * never lost). Construct-wide like `header`, not a capability: it's a
96
+ * fact about the empty state, not a toggleable affordance.
97
+ * `title`/`description` are construct-authored/untrusted text, like
98
+ * `header.title`/`theme.accent`/`provider.url` — JSON.stringify'd at
99
+ * their one emit site, never a raw JSX attribute string. `icon` is a
100
+ * URL reaching an `<img src>` sink in emitted code, exactly like
101
+ * `widget.launcherIcon` — same `isSafeUrl` policy, same superRefine
102
+ * shape, below. */
103
+ empty: z.object({
104
+ title: z.string().min(1),
105
+ description: z.string().min(1).optional(),
106
+ icon: z.string().min(1).optional()
107
+ }).strict().optional(),
108
+ /** Home screen (spec 2026-08-27, H-1..H-5): Intercom-style landing view behind
109
+ * a Home/Messages tab bar. PRESENCE of `home` enables the tab chrome (H-4) —
110
+ * no `enabled` boolean, like `header`/`empty`. Every sub-key optional;
111
+ * `home: {}` still means "tabs on, defaults". Never requires another
112
+ * capability (H-3): the recent card simply renders nothing without
113
+ * `capabilities.conversations` (the CLI warns — see cli.ts). All strings are
114
+ * construct-authored/untrusted: JSON.stringify'd at every emit site; hrefs
115
+ * and URL-shaped icons through isSafeUrl in superRefine below. */
116
+ home: z.object({
117
+ greeting: z.object({ title: z.string().min(1).optional(), subtitle: z.string().min(1).optional() }).strict().optional(),
118
+ recentConversation: z.literal(true).optional(),
119
+ newConversation: z.object({ label: z.string().min(1).optional() }).strict().optional(),
120
+ links: z.array(
121
+ z.object({
122
+ label: z.string().min(1),
123
+ href: z.string().min(1).optional(),
124
+ description: z.string().min(1).optional(),
125
+ /** renderIcon name (e.g. 'book-open') or a safe URL — the icon NAME
126
+ * list is renderIcon's own vocabulary, never restated here; unknown
127
+ * names warn loudly at DEV runtime. */
128
+ icon: z.string().min(1).optional()
129
+ }).strict()
130
+ ).optional()
131
+ }).strict().optional(),
132
+ // Capability vocabulary, widened one field at a time by later tasks.
133
+ capabilities: z.object({
134
+ /** Starter prompts shown on the empty thread; clicking one sends it.
135
+ * 1-6 non-empty strings — construct-authored text, escaped like
136
+ * `theme.accent`/`provider.url` at every emit interpolation site. */
137
+ starters: z.array(z.string().min(1)).min(1).max(6).optional(),
138
+ /** Enables the paperclip attach affordance; accept is a non-empty
139
+ * list of media types/globs, e.g. ["image/*", "application/pdf"] —
140
+ * WHETHER stays with the construct author (this field), HOW stays
141
+ * with the kit (ChatThread's own attach/accept props, threaded
142
+ * through by codegen). */
143
+ attachments: z.object({
144
+ /** Accept-list of media types/globs, e.g. ["image/*", "application/pdf"]. */
145
+ accept: z.array(z.string().min(1)).min(1)
146
+ }).strict().optional(),
147
+ /** Conversation persistence. `none` (default, nothing emitted): the
148
+ * thread lives only in memory for the tab's lifetime. `local`:
149
+ * persisted to this browser's localStorage, keyed by the construct's
150
+ * tag — a mechanism decision (WHERE); what to retain and for how
151
+ * long stays an app decision (component-scope-boundary), so no
152
+ * retention count/quota lands here. `endpoint`: the CONSUMER's own
153
+ * thread route (GET returns ChatMessage[], PUT stores them) —
154
+ * requires `url`; `url` is rejected for any other persistence
155
+ * (superRefine below, both directions loud). */
156
+ history: z.object({
157
+ persistence: z.enum(["none", "local", "endpoint"]),
158
+ /** endpoint persistence only: the CONSUMER's thread routes (GET returns
159
+ * ChatMessage[], PUT stores them). Refined below. */
160
+ url: z.string().min(1).optional()
161
+ }).strict().optional(),
162
+ /** How the model's thinking (reasoning parts) renders. `'full'`
163
+ * (the default when omitted — see codegen.ts's emitReasoningProp)
164
+ * is the collapsible "Thinking" disclosure, shimmering while it
165
+ * streams. `'compact'` shows only a shimmer/typing loader while
166
+ * reasoning streams, with no expandable detail. `'off'` hides
167
+ * reasoning entirely. This is HOW an existing medium fact (the
168
+ * model's thinking) displays — the kit's call — so it maps straight
169
+ * onto ChatThread's own `reasoning` prop; there is no app-layer
170
+ * quota or retention decision hiding in it. Left `.optional()`
171
+ * rather than `.default('full')`, matching every sibling field in
172
+ * this object: a zod `.default()` here would make `reasoning`
173
+ * REQUIRED on the inferred input type (z.infer is the output type),
174
+ * breaking every `capabilities: {...}` object literal in this file
175
+ * and its tests that doesn't mention it. */
176
+ reasoning: z.enum(["full", "compact", "off"]).optional(),
177
+ /** Seeds the reasoning disclosure open AND keeps it tracking the
178
+ * stream (open while streaming, closes when it settles) — the
179
+ * pre-Task-19f `full` behavior. Default false/absent: the panel
180
+ * starts closed (just the "Thinking" shimmer chip) and only opens
181
+ * on click — the current default (owner ruling, 2026-08-26).
182
+ * Meaningless when `reasoning` is `'compact'` (no expandable
183
+ * content exists) or `'off'` (nothing renders); rejected on both,
184
+ * loudly, below. */
185
+ reasoningOpen: z.boolean().optional(),
186
+ /** Turns on the prior-conversations list (C-1..C-9 of the
187
+ * conversations design). `true` only — there is no `false` form,
188
+ * matching this schema's other presence-only capability flags.
189
+ * Requires `capabilities.history.persistence` to be `local` or
190
+ * `endpoint` (superRefine below, loud): a conversation list with
191
+ * nowhere to persist conversations is not a coherent construct.
192
+ * WHAT persists (this field, plus history) stays construct
193
+ * vocabulary; HOW it persists (the ConversationStore adapter,
194
+ * localStorage vs. a fetch endpoint) is codegen's call, never
195
+ * vocabulary here (C-3 — no transport-layer vocabulary). */
196
+ conversations: z.literal(true).optional()
197
+ }).strict().optional(),
198
+ /** Named generative-UI card definitions the model can emit as tool calls,
199
+ * rendered in the thread. Top-level (not a capability): a card is
200
+ * something the construct's model can DO at any point once a card is
201
+ * declared, not a thread-affordance toggle like starters/attachments/
202
+ * reasoning. Reuses the kit's own card-tool projection end to end
203
+ * (`cardTools`/`toOpenAITools`/`toAnthropicTools`, `@kitn.ai/ui/schemas`)
204
+ * — never a second one authored here. `schema` is validated structurally
205
+ * only; deep card validation (incl. `x-kai-format` mask hints) is the
206
+ * kit's own card contract at render time. */
207
+ cards: z.array(
208
+ z.object({
209
+ /** Tool-facing card name, e.g. "refund_approval". */
210
+ name: z.string().regex(/^[a-z][a-z0-9_]*$/),
211
+ /** The kit's card schema JSON (incl. x-kai-format mask hints).
212
+ * Validated structurally here; deep card validation is the kit's
213
+ * own card contract at render time. */
214
+ schema: z.record(z.string(), z.unknown())
215
+ }).strict()
216
+ ).min(1).optional(),
217
+ /** Named `<slot>` projection points the emitted web component exposes —
218
+ * the format's ONLY escape hatch (named slots, no code-in-JSON). Each
219
+ * name must be a valid HTML slot-attribute value AND a legible
220
+ * identifier: kebab-case, starting with a letter (the same shape as a
221
+ * CSS custom-ident) — `slot name="Header!"` is rejected, not sanitized,
222
+ * matching the format's loud-rejection discipline everywhere else.
223
+ * 1-8 entries, no duplicates (superRefine below — a regex alone can't
224
+ * see across array entries). `layout: 'custom'` requires at least one
225
+ * declared slot (superRefine below): `custom` IS the slots grain — a
226
+ * `custom` layout with nothing to project into is not meaningfully
227
+ * different from `fullscreen`. */
228
+ slots: z.array(z.string().regex(/^[a-z][a-z0-9-]*$/, "slot names must be kebab-case, starting with a letter")).min(1).max(8).optional(),
229
+ /** Layout-scoped FAB chrome, `layout: 'widget'` only (superRefine below).
230
+ * Purely additive sibling to `layout` (widen-never-restructure) — mirrors
231
+ * Dock's own props verbatim, threaded through by codegen's emitLayoutOpen. */
232
+ widget: z.object({
233
+ /** Which corner. Mirrors Dock's own DockPosition enum verbatim — not a
234
+ * new left/right binary. Omitted keeps Dock's own default (bottom-end). */
235
+ position: z.enum(["bottom-end", "bottom-start", "top-end", "top-start"]).optional(),
236
+ /** An image URL for the closed-state launcher glyph, replacing Dock's
237
+ * built-in chat-bubble icon. Construct-authored/untrusted text, like
238
+ * theme.accent and provider.url — escaped the same way at its one emit site. */
239
+ launcherIcon: z.string().min(1).optional(),
240
+ /** Seed Dock's open state at mount. Uncontrolled — never steals focus (Dock's
241
+ * own focus contract, dock.tsx). Omitted keeps Dock's own default (closed). */
242
+ defaultOpen: z.boolean().optional()
243
+ }).strict().optional()
244
+ }).strict().superRefine((construct, ctx) => {
245
+ if (construct.slots) {
246
+ const seen = /* @__PURE__ */ new Set();
247
+ construct.slots.forEach((name, i) => {
248
+ if (seen.has(name)) {
249
+ ctx.addIssue({
250
+ code: z.ZodIssueCode.custom,
251
+ path: ["slots", i],
252
+ message: `duplicate slot name "${name}"`
253
+ });
254
+ }
255
+ seen.add(name);
256
+ });
257
+ }
258
+ if (construct.layout === "custom" && (!construct.slots || construct.slots.length === 0)) {
259
+ ctx.addIssue({
260
+ code: z.ZodIssueCode.custom,
261
+ path: ["slots"],
262
+ message: '"custom" layout requires at least one declared slot — custom IS the slots grain'
263
+ });
264
+ }
265
+ if (construct.layout === "split" && construct.slots) {
266
+ const i = construct.slots.indexOf("pane");
267
+ if (i !== -1) {
268
+ ctx.addIssue({
269
+ code: z.ZodIssueCode.custom,
270
+ path: ["slots", i],
271
+ message: `"pane" collides with the "split" layout's own fixed <slot name="pane"> — choose a different slot name`
272
+ });
273
+ }
274
+ }
275
+ if (construct.widget && construct.layout !== "widget") {
276
+ ctx.addIssue({
277
+ code: z.ZodIssueCode.custom,
278
+ path: ["widget"],
279
+ message: '"widget" is only valid on layout: "widget"'
280
+ });
281
+ }
282
+ if (construct.widget?.launcherIcon && !isSafeUrl(construct.widget.launcherIcon)) {
283
+ ctx.addIssue({
284
+ code: z.ZodIssueCode.custom,
285
+ path: ["widget", "launcherIcon"],
286
+ message: "launcherIcon must be an http(s)/mailto or relative URL — no javascript:/data: schemes"
287
+ });
288
+ }
289
+ if (construct.empty?.icon && !isSafeUrl(construct.empty.icon)) {
290
+ ctx.addIssue({
291
+ code: z.ZodIssueCode.custom,
292
+ path: ["empty", "icon"],
293
+ message: "icon must be an http(s)/mailto or relative URL — no javascript:/data: schemes"
294
+ });
295
+ }
296
+ const reasoning = construct.capabilities?.reasoning;
297
+ const reasoningOpen = construct.capabilities?.reasoningOpen;
298
+ if (reasoningOpen !== void 0 && (reasoning === "compact" || reasoning === "off")) {
299
+ ctx.addIssue({
300
+ code: z.ZodIssueCode.custom,
301
+ path: ["capabilities", "reasoningOpen"],
302
+ message: '"reasoningOpen" only applies when reasoning is "full" or omitted — "compact"/"off" have no disclosure to open'
303
+ });
304
+ }
305
+ const history = construct.capabilities?.history;
306
+ if (construct.capabilities?.conversations && (!history || history.persistence === "none")) {
307
+ ctx.addIssue({
308
+ code: z.ZodIssueCode.custom,
309
+ path: ["capabilities", "conversations"],
310
+ message: '"conversations" requires capabilities.history.persistence to be "local" or "endpoint" — a conversation list needs somewhere to persist conversations'
311
+ });
312
+ }
313
+ const URL_SHAPED = /^[a-zA-Z][a-zA-Z0-9+.-]*:|^\/\//;
314
+ for (const [i, link] of (construct.home?.links ?? []).entries()) {
315
+ if (link.href && !isSafeUrl(link.href)) {
316
+ ctx.addIssue({
317
+ code: z.ZodIssueCode.custom,
318
+ path: ["home", "links", i, "href"],
319
+ message: "href must be an http(s)/mailto or relative URL — no javascript:/data: schemes"
320
+ });
321
+ }
322
+ if (link.icon && URL_SHAPED.test(link.icon) && !isSafeUrl(link.icon)) {
323
+ ctx.addIssue({
324
+ code: z.ZodIssueCode.custom,
325
+ path: ["home", "links", i, "icon"],
326
+ message: "icon must be a kit icon name or an http(s)/relative URL — no javascript:/data: schemes"
327
+ });
328
+ }
329
+ }
330
+ if (!history) return;
331
+ if (history.persistence === "endpoint" && !history.url) {
332
+ ctx.addIssue({
333
+ code: z.ZodIssueCode.custom,
334
+ path: ["capabilities", "history", "url"],
335
+ message: '"endpoint" persistence requires a url'
336
+ });
337
+ }
338
+ if (history.persistence !== "endpoint" && history.url !== void 0) {
339
+ ctx.addIssue({
340
+ code: z.ZodIssueCode.custom,
341
+ path: ["capabilities", "history", "url"],
342
+ message: 'url is only valid with "endpoint" persistence'
343
+ });
344
+ }
345
+ });
346
+ function validateConstruct(input) {
347
+ const parsed = ConstructSchema.safeParse(input);
348
+ if (parsed.success) return { ok: true, construct: parsed.data };
349
+ return {
350
+ ok: false,
351
+ problems: parsed.error.issues.flatMap((issue) => {
352
+ if (issue.code === "unrecognized_keys") {
353
+ return issue.keys.map((key) => ({
354
+ path: [...issue.path.map(String), key].join("."),
355
+ message: `"${key}" is not construct vocabulary`
356
+ }));
357
+ }
358
+ return [
359
+ {
360
+ path: issue.path.map(String).join("."),
361
+ message: issue.message
362
+ }
363
+ ];
364
+ })
365
+ };
366
+ }
367
+ function kitVersion() {
368
+ const require2 = createRequire(import.meta.url);
369
+ const pkg = require2("@kitn.ai/ui/package.json");
370
+ return pkg.version;
371
+ }
372
+ const themeMode = (c) => c.theme?.mode === "light" ? "light" : c.theme?.mode === "dark" ? "dark" : "auto";
373
+ function srgbChannelToLinear(c) {
374
+ const cs = c / 255;
375
+ return cs <= 0.03928 ? cs / 12.92 : Math.pow((cs + 0.055) / 1.055, 2.4);
376
+ }
377
+ function relativeLuminance(r, g, b) {
378
+ return 0.2126 * srgbChannelToLinear(r) + 0.7152 * srgbChannelToLinear(g) + 0.0722 * srgbChannelToLinear(b);
379
+ }
380
+ function hslToRgb(h, s, l) {
381
+ const hue = (h % 360 + 360) % 360;
382
+ const chroma = (1 - Math.abs(2 * l - 1)) * s;
383
+ const x = chroma * (1 - Math.abs(hue / 60 % 2 - 1));
384
+ const m = l - chroma / 2;
385
+ const [r1, g1, b1] = hue < 60 ? [chroma, x, 0] : hue < 120 ? [x, chroma, 0] : hue < 180 ? [0, chroma, x] : hue < 240 ? [0, x, chroma] : hue < 300 ? [x, 0, chroma] : [chroma, 0, x];
386
+ return [Math.round((r1 + m) * 255), Math.round((g1 + m) * 255), Math.round((b1 + m) * 255)];
387
+ }
388
+ function parseAccentRgb(accent) {
389
+ const s = accent.trim();
390
+ const hex3 = /^#([0-9a-fA-F]{3})$/.exec(s);
391
+ if (hex3) {
392
+ const [r, g, b] = hex3[1].split("").map((ch) => parseInt(ch + ch, 16));
393
+ return [r, g, b];
394
+ }
395
+ const hex6 = /^#([0-9a-fA-F]{6})([0-9a-fA-F]{2})?$/.exec(s);
396
+ if (hex6) {
397
+ const hex = hex6[1];
398
+ return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
399
+ }
400
+ const rgbFn = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*[\d.]+\s*)?\)$/.exec(s);
401
+ if (rgbFn) {
402
+ const [r, g, b] = [rgbFn[1], rgbFn[2], rgbFn[3]].map(Number);
403
+ return [r, g, b].every((v) => v >= 0 && v <= 255) ? [r, g, b] : null;
404
+ }
405
+ const hslFn = /^hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%\s*(?:,\s*[\d.]+\s*)?\)$/.exec(s);
406
+ if (hslFn) {
407
+ return hslToRgb(Number(hslFn[1]), Number(hslFn[2]) / 100, Number(hslFn[3]) / 100);
408
+ }
409
+ return null;
410
+ }
411
+ function resolveContrastForeground(accent) {
412
+ const rgb = parseAccentRgb(accent);
413
+ if (!rgb) return null;
414
+ const luminance = relativeLuminance(...rgb);
415
+ return luminance <= 0.5 ? "#ffffff" : "#000000";
416
+ }
417
+ function commentSafe(text) {
418
+ return text.replace(/[\r\n]/g, " ").replace(/\*\//g, "* /");
419
+ }
420
+ const CONTRAST_COLOR_SUPPORTS = "@supports (color: contrast-color(red))";
421
+ function accentContrastNotice(construct) {
422
+ const accent = construct.theme?.accent;
423
+ if (!accent || resolveContrastForeground(accent) !== null) return null;
424
+ return `accent '${commentSafe(accent)}' not parseable for contrast; foreground left at theme default in browsers without CSS contrast-color() support`;
425
+ }
426
+ function generateProject(construct, opts = {}) {
427
+ const uiSpec = opts.uiSpec ?? `^${kitVersion()}`;
428
+ const files = [
429
+ { path: "package.json", code: emitPackageJson(construct, uiSpec) },
430
+ { path: "tsconfig.json", code: emitTsconfig() },
431
+ { path: "vite.config.ts", code: emitViteDev() },
432
+ { path: "vite.config.lib.ts", code: emitViteLib(construct) },
433
+ { path: "index.html", code: emitIndexHtml(construct) },
434
+ { path: "src/element.tsx", code: emitElement(construct) },
435
+ { path: "src/App.tsx", code: emitApp(construct) }
436
+ ];
437
+ if (construct.cards) files.push({ path: "src/cards.ts", code: emitCardsRegistry(construct.cards) });
438
+ return files;
439
+ }
440
+ function emitCardsRegistry(cards) {
441
+ const entries = cards.map((card) => ` ${card.name}: ${JSON.stringify(card.schema, null, 2).split("\n").join("\n ")},`).join("\n");
442
+ return `// src/cards.ts — the construct's card registry, verbatim from the construct.
443
+ // Tool definitions for YOUR backend derive from this same object via
444
+ // @kitn.ai/ui/schemas (cardTools / toOpenAITools / toAnthropicTools) — one
445
+ // projection, shared with the kit.
446
+ export const cards = {
447
+ ${entries}
448
+ } as const;
449
+ `;
450
+ }
451
+ function emitCardComponentImport(c) {
452
+ return c.cards ? ", BUILTIN_CARD_COMPONENTS" : "";
453
+ }
454
+ function emitCardsImport(c) {
455
+ if (!c.cards) return "";
456
+ const toolsImport = c.provider.mode === "endpoint" ? c.provider.wire === "openai" ? ", toOpenAITools" : ", toAnthropicTools" : "";
457
+ return `import { cards } from './cards';
458
+ // Generative-UI cards, v1: every declared card renders as the kit's own
459
+ // schema-driven FORM (BUILTIN_CARD_COMPONENTS.form, components/form.tsx) — it
460
+ // walks the card's JSON Schema into real input fields, honoring
461
+ // x-kai-format/x-kai-mask/x-kai-mask-guide hints itself. ChatThread's own
462
+ // MessageBody already matches \`part.type === 'card'\` in its part rendering and
463
+ // draws it with the kit's own \`CardRenderer\` (components/card-renderer.tsx),
464
+ // which picks the component from \`cardTypes\` (below) by envelope.type — so
465
+ // there is nothing to hand-compose beyond that one map. Turning a model's tool
466
+ // call into that renderable part is \`cardFromToolCall\` (the inverse of
467
+ // \`cardTools\`), applied once per settled turn below; its data is then replaced
468
+ // with the DECLARED card schema (not the model's call arguments) — the fields
469
+ // on screen are the construct's own vocabulary, not whatever shape a model
470
+ // happened to send.
471
+ //
472
+ // UPDATE (CD-1, owner ruling 2026-08-26, Task 19g): the field SHAPE (title/
473
+ // type/widget/validation) still comes from the construct's own declared
474
+ // schema, never the model's — that part is unchanged. But discarding the
475
+ // model's call arguments wholesale also threw away any VALUE it wanted to
476
+ // pre-fill, breaking "model proposes, user confirms" (kai_refund_approval
477
+ // {amount:50} rendered an empty form). So the model's args are now
478
+ // shallow-merged onto the declared schema's field \`default\`s below
479
+ // (mergeToolArgsIntoFormDefaults) before the card is added — see
480
+ // emitApplyCardTools.
481
+ import { cardFromToolCall${toolsImport} } from '@kitn.ai/ui/schemas';
482
+
483
+ // Every declared card name routes to the SAME form renderer — cardFromToolCall
484
+ // makes envelope.type equal the card's own name (kai_refund_approval ->
485
+ // 'refund_approval'), and CardRenderer resolves a type's component from this
486
+ // map.
487
+ //
488
+ // Deliberately NOT also wiring ChatThread's \`cardSchemas\` prop to this
489
+ // registry below. That prop validates envelope.data AGAINST the named schema,
490
+ // and this card's data IS \`cards[name]\` itself (see emitApplyCardTools) — the
491
+ // construct's declared field schema, not values shaped like it. Wiring it as
492
+ // its own validator asks "does this FormDefinition itself have an \`amount\`
493
+ // key" and a well-formed FormDefinition never does, so every card would
494
+ // render the HARD validation-failure fallback instead of the form (caught
495
+ // live: eject + kai dev showed exactly that "(root).amount: required"
496
+ // failure before this comment existed). The construct's own schema.ts
497
+ // already checks \`cards\` structurally at validate time; there is nothing
498
+ // left for a second, self-referential check here to catch.
499
+ const cardTypes = Object.fromEntries(Object.keys(cards).map((name) => [name, BUILTIN_CARD_COMPONENTS.form] as const));
500
+
501
+ // CD-1 (owner ruling 2026-08-26, Task 19g): shallow-merge a model's card
502
+ // tool-call args onto a DECLARED form schema's field defaults — "model
503
+ // proposes, user confirms". Only top-level keys the args AND the schema both
504
+ // name are touched; the schema's own field shape (title/type/widget/
505
+ // validation) is never altered, and a key the model sent that isn't a
506
+ // declared field is ignored (the construct's vocabulary wins, not the
507
+ // model's). One level deep only — a nested object field's own defaults are
508
+ // not recursed into; no evidence of need yet (vocabulary-on-evidence).
509
+ function mergeToolArgsIntoFormDefaults(
510
+ schema: Record<string, unknown> & { properties?: Record<string, unknown> },
511
+ args: Record<string, unknown>,
512
+ ): Record<string, unknown> {
513
+ const declared = schema.properties;
514
+ if (!declared || typeof declared !== 'object') return schema;
515
+ const patched: Record<string, unknown> = {};
516
+ for (const [key, value] of Object.entries(args)) {
517
+ if (!(key in declared)) continue;
518
+ patched[key] = { ...(declared[key] as Record<string, unknown>), default: value };
519
+ }
520
+ return { ...schema, properties: { ...schema.properties, ...patched } };
521
+ }`;
522
+ }
523
+ function emitCardTypesProp(c) {
524
+ return c.cards ? " cardTypes={cardTypes}" : "";
525
+ }
526
+ function emitApplyCardTools(c) {
527
+ if (!c.cards) return "";
528
+ return `
529
+ for (const part of chat.messages().find((m) => m.id === stream.id)?.parts ?? []) {
530
+ if (part.type !== 'tool' || part.tool.state !== 'input-available') continue;
531
+ const card = cardFromToolCall(part.tool.type, part.tool.input, { id: part.tool.toolCallId ?? crypto.randomUUID() });
532
+ if (card && card.type in cards) {
533
+ const declared = cards[card.type as keyof typeof cards];
534
+ const args = card.data as Record<string, unknown> | undefined;
535
+ const merged = args && typeof args === 'object'
536
+ ? mergeToolArgsIntoFormDefaults(declared, args)
537
+ : declared;
538
+ stream.addCard({ ...card, data: merged });
539
+ }
540
+ }`;
541
+ }
542
+ function emitToolsField(c) {
543
+ if (!c.cards || c.provider.mode !== "endpoint") return "";
544
+ const toolsFn = c.provider.wire === "openai" ? "toOpenAITools" : "toAnthropicTools";
545
+ return `, tools: ${toolsFn}(cards)`;
546
+ }
547
+ function emitPackageJson(c, uiSpec) {
548
+ return `${JSON.stringify(
549
+ {
550
+ name: c.name,
551
+ private: true,
552
+ type: "module",
553
+ scripts: {
554
+ dev: "vite",
555
+ build: "vite build --config vite.config.lib.ts",
556
+ typecheck: "tsc --noEmit"
557
+ },
558
+ dependencies: {
559
+ "@kitn.ai/ui": uiSpec,
560
+ "solid-js": "^1.9.0"
561
+ },
562
+ devDependencies: {
563
+ typescript: "^5.6.0",
564
+ vite: "^6.0.0",
565
+ "vite-plugin-solid": "^2.11.0"
566
+ }
567
+ },
568
+ null,
569
+ 2
570
+ )}
571
+ `;
572
+ }
573
+ function emitTsconfig() {
574
+ return `${JSON.stringify(
575
+ {
576
+ compilerOptions: {
577
+ target: "ES2022",
578
+ module: "ESNext",
579
+ moduleResolution: "bundler",
580
+ jsx: "preserve",
581
+ jsxImportSource: "solid-js",
582
+ strict: true,
583
+ noUnusedLocals: true,
584
+ skipLibCheck: true,
585
+ types: ["vite/client"]
586
+ },
587
+ include: ["src"]
588
+ },
589
+ null,
590
+ 2
591
+ )}
592
+ `;
593
+ }
594
+ function emitViteDev() {
595
+ return `import { defineConfig } from 'vite';
596
+ import solid from 'vite-plugin-solid';
597
+
598
+ export default defineConfig({ plugins: [solid()] });
599
+ `;
600
+ }
601
+ function emitViteLib(c) {
602
+ return `import { defineConfig } from 'vite';
603
+ import solid from 'vite-plugin-solid';
604
+
605
+ // kai compile: ONE self-registering .js. Everything is inlined (no externals):
606
+ // the consumer installs nothing but this output.
607
+ export default defineConfig({
608
+ plugins: [solid()],
609
+ build: {
610
+ lib: { entry: 'src/element.tsx', formats: ['es'], fileName: () => '${c.name}.js' },
611
+ },
612
+ });
613
+ `;
614
+ }
615
+ function emitIndexHtml(c) {
616
+ const hint = c.layout === "widget" ? `
617
+ <p style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); margin: 0; color: #94a3b8; font: 14px system-ui, sans-serif; text-align: center; max-width: 28rem; padding: 0 1rem;">This blank page stands in for your site. The chat widget is in the bottom-right corner.</p>` : "";
618
+ const slotDemo = (c.slots ?? []).map(
619
+ (name) => `
620
+ <div slot="${name}" style="padding: 0.5rem 1rem; font: 13px system-ui, sans-serif; color: #64748b;">Projected into slot "${name}" — replace with your own markup.</div>`
621
+ ).join("");
622
+ const body = slotDemo ? `
623
+ <${c.name}>${slotDemo}
624
+ </${c.name}>` : `
625
+ <${c.name}></${c.name}>`;
626
+ return `<!doctype html>
627
+ <html lang="en">
628
+ <head>
629
+ <meta charset="utf-8" />
630
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
631
+ <title>${c.name} — construct preview</title>
632
+ </head>
633
+ <body style="margin: 0;">${hint}${body}
634
+ <script type="module" src="/src/element.tsx"><\/script>
635
+ </body>
636
+ </html>
637
+ `;
638
+ }
639
+ function emitElement(c) {
640
+ const accent = c.theme?.accent;
641
+ const unreadColor = c.theme?.unreadColor;
642
+ if (!accent && !unreadColor) {
643
+ const facade2 = "() => <App />";
644
+ return `import { defineWebComponent } from '@kitn.ai/ui/define';
645
+ import { App } from './App';
646
+
647
+ // The one facade. Interior stays pure Solid (no nested element registrations);
648
+ // the kit CSS is injected into the shadow root by defineWebComponent itself.
649
+ defineWebComponent('${c.name}', { theme: '${themeMode(c)}' as 'light' | 'dark' | 'auto' }, ${facade2});
650
+ `;
651
+ }
652
+ const unreadColorSetProperty = unreadColor ? `
653
+ ctx.element.style.setProperty('--kai-color-unread', ${JSON.stringify(unreadColor)});` : "";
654
+ if (!accent) {
655
+ const facade2 = `(_props, ctx) => {${unreadColorSetProperty}
656
+ return <App />;
657
+ }`;
658
+ return `import { defineWebComponent } from '@kitn.ai/ui/define';
659
+ import { App } from './App';
660
+
661
+ // The one facade. Interior stays pure Solid (no nested element registrations);
662
+ // the kit CSS is injected into the shadow root by defineWebComponent itself.
663
+ defineWebComponent('${c.name}', { theme: '${themeMode(c)}' as 'light' | 'dark' | 'auto' }, ${facade2});
664
+ `;
665
+ }
666
+ const foreground = resolveContrastForeground(accent);
667
+ const foregroundCss = foreground !== null ? `:host { --kai-color-primary-foreground: ${foreground}; }
668
+ ` : (
669
+ // Not guessed at: an unparseable accent (var(), a named color, a
670
+ // color-mix()/oklch() call, …) leaves NO base declaration, so the
671
+ // kit's own theme default stands — except in a browser new enough to
672
+ // resolve contrast-color() itself, where the @supports block below
673
+ // still gets it right natively.
674
+ `/* NOTICE: accent '${commentSafe(accent)}' not parseable for contrast at generation time; the paired foreground falls back to the theme default in browsers without CSS contrast-color() support. */
675
+ `
676
+ );
677
+ const styleText = foregroundCss + `${CONTRAST_COLOR_SUPPORTS} {
678
+ :host { --kai-color-primary-foreground: contrast-color(var(--kai-color-primary)); }
679
+ }`;
680
+ const facade = `(_props, ctx) => {
681
+ ctx.element.style.setProperty('--kai-color-primary', ${JSON.stringify(accent)});${unreadColorSetProperty}
682
+ return (
683
+ <>
684
+ <style>{${JSON.stringify(styleText)}}</style>
685
+ <App />
686
+ </>
687
+ );
688
+ }`;
689
+ return `import { defineWebComponent } from '@kitn.ai/ui/define';
690
+ import { App } from './App';
691
+
692
+ // The one facade. Interior stays pure Solid (no nested element registrations);
693
+ // the kit CSS is injected into the shadow root by defineWebComponent itself.
694
+ defineWebComponent('${c.name}', { theme: '${themeMode(c)}' as 'light' | 'dark' | 'auto' }, ${facade});
695
+ `;
696
+ }
697
+ function emitApp(c) {
698
+ if (c.layout === "custom") return emitCustomApp(c);
699
+ return `${emitSolidJsImport(c)}${emitConversationsSignalsImport(c)}import { ChatThread, createKaiChat${emitLayoutImport(c)}${emitCardComponentImport(c)}${emitEmptyComponentImport(c)}${emitHeaderCloseImport(c)} } from '@kitn.ai/ui/solid';
700
+ import type { AttachmentData${emitHistoryTypeImport(c)}${emitConversationsResetTypeImport(c)} } from '@kitn.ai/ui/solid';
701
+ ${emitProviderImports(c)}
702
+ ${emitCardsImport(c)}
703
+ ${emitConversationsImport(c)}
704
+
705
+ ${emitProviderSetup(c)}
706
+ ${emitHistorySetup(c)}
707
+
708
+ // ChatThread is the kit's own MOST-INTEGRATED chat surface — the same
709
+ // composition <kai-chat>'s facade renders (src/elements/chat.tsx). It owns
710
+ // the message list, the composer (padding, focus ring, the send button) and
711
+ // their layout AS ONE UNIT, so nothing here re-derives spacing, alignment or
712
+ // focus styling by hand: every prior version of this file that hand-composed
713
+ // Thread + PromptInput + Button was restating layout the kit already owns,
714
+ // and every visual defect the owner hit (flush composer, a clipped focus
715
+ // ring) traced back to that restatement. Composing ChatThread directly
716
+ // leaves NOTHING here to restate it with.
717
+ //
718
+ // Capability gating (format rule: an undeclared capability's affordance must
719
+ // be OFF). The construct schema carries ONE capability field so far
720
+ // (capabilities.starters, Task 8) — every other affordance below is gated to
721
+ // "off" unconditionally, not per-construct, until there's a field to gate ON.
722
+ // - webSearch / voice: real ChatThreadProps booleans, default OFF when
723
+ // omitted — set to \`false\` explicitly rather than left implicit, so the
724
+ // gating decision is visible in the emitted source, not just inferred
725
+ // from an absent prop.
726
+ // - suggestions: ChatThread ALREADY owns starter prompts end to end — its
727
+ // own \`suggestions\` prop renders the chips, hides them once
728
+ // \`messages\` is non-empty, and (default \`suggestionMode="submit"\`)
729
+ // calls \`onSubmit\` with the clicked text exactly like a typed submit.
730
+ // So capabilities.starters threads straight into that prop; there is
731
+ // nothing to hand-compose. Omitted (undefined) when no starters are
732
+ // declared, same off-by-default effect as the booleans above.
733
+ // - models: omitted (undefined) — no model switcher; no capabilities field yet.
734
+ // - attachments (the paperclip): gated via ChatThread's \`attach\`/\`accept\`
735
+ // props (kit gap closed — ChatThread forwards both to DefaultPromptInput,
736
+ // mirroring webSearch/voice). ChatThread ALREADY owns the whole
737
+ // round-trip end to end — the paperclip button, staged previews, staging
738
+ // each file as a data URI (never a blob object URL; see
739
+ // AttachmentData.url's doc in components/attachment-types.ts), and
740
+ // handing the staged list back via onSubmit's \`attachments\` — and its
741
+ // Message component ALREADY groups consecutive file parts into one
742
+ // attachment row (message.tsx). So there is nothing to hand-compose
743
+ // here, same lesson as suggestions above: hand-rolling a second picker or
744
+ // a second file-part renderer would restate what ChatThread/Message
745
+ // already own. capabilities.attachments threads straight into
746
+ // attach/accept; the only App.tsx-owned piece is folding the picked
747
+ // attachments into the outgoing message's parts at the submit site
748
+ // (see emitProviderSetup) since createKaiChat's own append/streamAssistant
749
+ // ops don't do that folding themselves.
750
+ // - reasoning: gated via ChatThread's own \`reasoning\` prop (kit gap closed
751
+ // — ChatThread forwards it to every MessageBody as \`reasoningMode\`,
752
+ // mirroring attach/accept). \`'full'\` is both the schema default and
753
+ // ChatThread's own default, so it and an absent field emit no prop at
754
+ // all — the SAME off-by-default convention as every other capability
755
+ // here, just anchored on the medium's existing default instead of an
756
+ // "off" value, since a reasoning disclosure is normal chat behavior, not
757
+ // an opt-in affordance like the paperclip or a starter chip.
758
+ // - empty (the welcome-screen greeting, Task 14): gated via ChatThread's
759
+ // own \`emptyContent\` prop, plain JSX rendered in the SAME shadow tree
760
+ // this file's App already composes ChatThread inside of (see
761
+ // emitEmptyContentProp's own doc for why that boundary needs no Portal
762
+ // at all). \`capabilities.starters\`' chips and the composer still render
763
+ // underneath it: ChatThread's own doc comment on \`emptyContent\` is
764
+ // explicit that it replaces only the empty MESSAGE LIST.
765
+ // - the widget close control (owner feedback on the live demo): a declared
766
+ // \`header.title\` on a \`widget\` layout gets its close button threaded
767
+ // into ChatThread's own header row via \`headerEndContent\`, wired back to
768
+ // Dock's \`controllerRef\` seam through a local closure — see
769
+ // emitDockCloseVar/emitHeaderEndContentProp's docs. No header means no
770
+ // row for it to sit in, so that case is untouched and Dock's own built-in
771
+ // mobile X keeps covering it.
772
+ // - conversations (Task 5): gated via ChatThread's own \`conversations\`/
773
+ // \`store\` props (kit-owned end to end — the prior-conversations list,
774
+ // list/load/save, autosave on every \`chat.messages()\` change). Requires
775
+ // capabilities.history persistence \`local\` or \`endpoint\` (schema
776
+ // superRefine, C-4) and SUBSUMES this file's hand-rolled history effect
777
+ // when on — see emitHistorySetup's own doc for the persistence-ownership
778
+ // decision: the store prop is the ONLY persistence mechanism emitted,
779
+ // never both. ChatThread never mutates \`messages\` itself, so
780
+ // \`onConversationLoad\` is ALSO wired here (\`chat.setMessages(() =>
781
+ // messages)\`) — without it, select/new/mount-restore all update
782
+ // ChatThread's own internal view/list state while the rendered thread
783
+ // never changes (see emitConversationsProps's own doc for the Task 6
784
+ // live-browser bug this fixes).
785
+ // - conversations + widget (owner follow-up): closing the widget while its
786
+ // list view is open must not leave it there for the next open — see
787
+ // widgetHasConversationsChrome/emitDockOnOpenChangeProp's docs. Wired only
788
+ // for \`widget\`, the one layout with something that closes/reopens at all.
789
+ export function App() {
790
+ ${emitDockCloseVar(c, " ")}${emitChatControllerVar(c, " ")}${emitConversationsSignalsVar(c, " ")} return (
791
+ ${emitLayoutOpen(c)}${emitSlots(c.slots, " ")} <ChatThread messages={chat.messages()} loading={chat.loading()} placeholder="Ask anything" onSubmit={submit} webSearch={false} voice={false}${emitHeaderProp(c)}${emitHeaderEndContentProp(c)}${emitAttachProps(c)}${emitStartersProp(c)}${emitReasoningProp(c)}${emitReasoningOpenProp(c)}${emitEmptyContentProp(c)}${emitCardTypesProp(c)}${emitHomeProp(c)}${emitConversationsProps(c)}${emitChatControllerRefProp(c)}${emitChatThreadUnreadProps(c)} />
792
+ ${emitLayoutClose(c)} );
793
+ }
794
+ `;
795
+ }
796
+ function emitCustomApp(c) {
797
+ const slots = c.slots ?? [];
798
+ const [headerSlot, ...restSlots] = slots;
799
+ const history = c.capabilities?.history;
800
+ const solidJsNames = ["createSignal", ...history && history.persistence !== "none" ? ["createEffect"] : []];
801
+ return `import { ${solidJsNames.join(", ")} } from 'solid-js';
802
+ import { Thread, PromptInput, PromptInputTextarea, PromptInputActions, Button, createKaiChat${emitCardComponentImport(c)} } from '@kitn.ai/ui/solid';
803
+ import type { AttachmentData${emitHistoryTypeImport(c)} } from '@kitn.ai/ui/solid';
804
+ ${emitProviderImports(c)}
805
+ ${emitCardsImport(c)}
806
+
807
+ ${emitProviderSetup(c)}
808
+ ${emitHistorySetup(c)}
809
+
810
+ // layout: custom — minimal chrome, no ChatThread/Dock/PaneGroup. The bare
811
+ // spine (Thread + PromptInput) plus the declared slots, positioned by hand so
812
+ // YOU own the surrounding DOM. Capabilities beyond the spine (starters,
813
+ // attachments, reasoning display-mode, reasoningOpen, header.title, empty,
814
+ // conversations) are NOT wired here in v1 — this file is the eject artifact; add them the
815
+ // way ChatThread composes them (components/chat-thread.tsx in the kit's own
816
+ // source) if this construct needs them on a custom layout.
817
+ export function App() {
818
+ const [value, setValue] = createSignal('');
819
+
820
+ const handleSubmit = () => {
821
+ const text = value();
822
+ if (!text.trim() || chat.loading()) return;
823
+ setValue('');
824
+ void submit({ value: text, attachments: [] });
825
+ };
826
+
827
+ return (
828
+ <div style={{ height: '100dvh', display: 'flex', 'flex-direction': 'column' }}>
829
+ ${emitSlots(headerSlot ? [headerSlot] : void 0, " ")} <Thread messages={chat.messages()} loading={chat.loading()} class="min-h-0 flex-1"${emitCardTypesProp(c)} />
830
+ {/* Slot placement rule: first declared slot above the thread, every other
831
+ declared slot below the composer, in declaration order. Reorder by
832
+ ejecting — this is the whole grain dimmer. */}
833
+ <PromptInput value={value()} onValueChange={setValue} isLoading={chat.loading()} onSubmit={handleSubmit}>
834
+ <PromptInputTextarea placeholder="Ask anything" />
835
+ <PromptInputActions>
836
+ <Button onClick={handleSubmit}>Send</Button>
837
+ </PromptInputActions>
838
+ </PromptInput>
839
+ ${emitSlots(restSlots, " ")} </div>
840
+ );
841
+ }
842
+ `;
843
+ }
844
+ function emitHeaderProp(c) {
845
+ const title = c.header?.title;
846
+ if (!title) return "";
847
+ return ` chatTitle={${JSON.stringify(title)}}`;
848
+ }
849
+ function emitEmptyContentProp(c) {
850
+ const empty = c.empty;
851
+ if (!empty) return "";
852
+ const title = `<EmptyTitle>{${JSON.stringify(empty.title)}}</EmptyTitle>`;
853
+ const icon = empty.icon ? `<EmptyMedia><img src={${JSON.stringify(empty.icon)}} alt="" style={{ width: '40px', height: '40px', 'border-radius': '9999px' }} /></EmptyMedia>` : "";
854
+ const description = empty.description ? `<EmptyDescription>{${JSON.stringify(empty.description)}}</EmptyDescription>` : "";
855
+ return ` emptyContent={<Empty><EmptyHeader>${icon}${title}${description}</EmptyHeader></Empty>}`;
856
+ }
857
+ function emitEmptyComponentImport(c) {
858
+ if (!c.empty) return "";
859
+ let names = ", Empty, EmptyHeader, EmptyTitle";
860
+ if (c.empty.icon) names += ", EmptyMedia";
861
+ if (c.empty.description) names += ", EmptyDescription";
862
+ return names;
863
+ }
864
+ function widgetHasHeaderClose(c) {
865
+ return c.layout === "widget" && !!c.header?.title;
866
+ }
867
+ function widgetHasConversationsChrome(c) {
868
+ return c.layout === "widget" && (!!c.capabilities?.conversations || !!c.home);
869
+ }
870
+ function emitChatControllerVar(c, indent) {
871
+ return widgetHasConversationsChrome(c) ? `${indent}let chatController: ChatThreadController | undefined;
872
+ ` : "";
873
+ }
874
+ function emitChatControllerRefProp(c) {
875
+ return widgetHasConversationsChrome(c) ? " controllerRef={(api) => (chatController = api)}" : "";
876
+ }
877
+ function emitDockOnOpenChangeProp(c) {
878
+ return widgetHasConversationsChrome(c) ? " onOpenChange={(open) => { setDockOpen(open); if (!open) chatController?.closeConversationsList(); }}" : "";
879
+ }
880
+ function emitConversationsResetTypeImport(c) {
881
+ return widgetHasConversationsChrome(c) ? ", ChatThreadController" : "";
882
+ }
883
+ function emitConversationsSignalsImport(c) {
884
+ return widgetHasConversationsChrome(c) ? `import { createSignal } from 'solid-js';
885
+ ` : "";
886
+ }
887
+ function emitConversationsSignalsVar(c, indent) {
888
+ if (!widgetHasConversationsChrome(c)) return "";
889
+ const w = c.layout === "widget" ? c.widget : void 0;
890
+ const defaultOpen = w?.defaultOpen === true ? "true" : "false";
891
+ return `${indent}const [dockOpen, setDockOpen] = createSignal(${defaultOpen});
892
+ ${indent}const [anyUnread, setAnyUnread] = createSignal(false);
893
+ `;
894
+ }
895
+ function emitChatThreadUnreadProps(c) {
896
+ return widgetHasConversationsChrome(c) ? " hostOpen={dockOpen()} onUnreadChange={setAnyUnread}" : "";
897
+ }
898
+ function emitDockUnreadProp(c) {
899
+ return widgetHasConversationsChrome(c) ? " unread={anyUnread()}" : "";
900
+ }
901
+ function emitDockCloseVar(c, indent) {
902
+ return widgetHasHeaderClose(c) ? `${indent}let dockClose: (() => void) | undefined;
903
+ ` : "";
904
+ }
905
+ function emitDockControllerRef(c) {
906
+ return widgetHasHeaderClose(c) ? " controllerRef={(api) => (dockClose = () => api.setOpen(false))}" : "";
907
+ }
908
+ function emitDockHideClose(c) {
909
+ return widgetHasHeaderClose(c) ? " hideClose={true}" : "";
910
+ }
911
+ function emitHeaderEndContentProp(c) {
912
+ if (!widgetHasHeaderClose(c)) return "";
913
+ return ` headerEndContent={<Button variant="ghost" size="icon-sm" aria-label="Close ${c.name}" onClick={() => dockClose?.()}><DockCloseGlyph /></Button>}`;
914
+ }
915
+ function emitHeaderCloseImport(c) {
916
+ return widgetHasHeaderClose(c) ? ", Button, DockCloseGlyph" : "";
917
+ }
918
+ function emitAttachProps(c) {
919
+ const attachments = c.capabilities?.attachments;
920
+ if (!attachments) return " attach={false}";
921
+ return ` attach={true} accept={${JSON.stringify(attachments.accept.join(","))}}`;
922
+ }
923
+ function emitStartersProp(c) {
924
+ const starters = c.capabilities?.starters;
925
+ if (!starters || starters.length === 0) return "";
926
+ return ` suggestions={${JSON.stringify(starters)}}`;
927
+ }
928
+ function emitReasoningProp(c) {
929
+ const reasoning = c.capabilities?.reasoning;
930
+ if (!reasoning || reasoning === "full") return "";
931
+ return ` reasoning="${reasoning}"`;
932
+ }
933
+ function emitReasoningOpenProp(c) {
934
+ return c.capabilities?.reasoningOpen === true ? " reasoningOpen={true}" : "";
935
+ }
936
+ function emitHomeProp(c) {
937
+ if (!c.home) return "";
938
+ return ` home={${JSON.stringify(c.home)}}`;
939
+ }
940
+ function emitConversationsProps(c) {
941
+ if (!c.capabilities?.conversations) return "";
942
+ const history = c.capabilities.history;
943
+ const storeCall = history?.persistence === "endpoint" ? `fetchStore(${JSON.stringify(history.url)}${c.userId ? `, ${JSON.stringify(c.userId)}` : ""})` : `localStorageStore('${c.name}'${c.userId ? `, ${JSON.stringify(c.userId)}` : ""})`;
944
+ return ` conversations={true} store={${storeCall}} onConversationLoad={(messages) => chat.setMessages(() => messages)}`;
945
+ }
946
+ function emitConversationsImport(c) {
947
+ if (!c.capabilities?.conversations) return "";
948
+ const name = c.capabilities.history?.persistence === "endpoint" ? "fetchStore" : "localStorageStore";
949
+ return `import { ${name} } from '@kitn.ai/ui/solid';`;
950
+ }
951
+ function emitSolidJsImport(c) {
952
+ const history = c.capabilities?.history;
953
+ if (!history || history.persistence === "none" || c.layout !== "custom" && c.capabilities?.conversations) return "";
954
+ return `import { createEffect } from 'solid-js';
955
+ `;
956
+ }
957
+ function emitHistoryTypeImport(c) {
958
+ const history = c.capabilities?.history;
959
+ if (!history || history.persistence === "none" || c.layout !== "custom" && c.capabilities?.conversations) return "";
960
+ return ", ChatMessage";
961
+ }
962
+ function emitUserIdHeaderEntry(c) {
963
+ return c.userId ? `, 'x-kai-user-id': ${JSON.stringify(c.userId)}` : "";
964
+ }
965
+ function emitHistorySetup(c) {
966
+ const history = c.capabilities?.history;
967
+ if (!history || history.persistence === "none") return "";
968
+ if (c.layout !== "custom" && c.capabilities?.conversations) return "";
969
+ if (history.persistence === "local") {
970
+ const key = JSON.stringify(c.userId ? `kai:${c.name}:${c.userId}:thread` : `kai:${c.name}:thread`);
971
+ return `
972
+ // History: persisted locally in this browser, keyed by the element tag. What to
973
+ // retain and for how long is an app decision — clear the key to reset.
974
+ const THREAD_KEY = ${key};
975
+ try {
976
+ const saved = localStorage.getItem(THREAD_KEY);
977
+ if (saved) {
978
+ const parsed: unknown = JSON.parse(saved);
979
+ if (Array.isArray(parsed)) {
980
+ chat.setMessages(() => parsed as ChatMessage[]);
981
+ } else {
982
+ console.warn(\`[\${THREAD_KEY}] stored history was not an array; ignoring and starting fresh\`);
983
+ }
984
+ }
985
+ } catch { /* storage unavailable or corrupt: run in-memory */ }
986
+ createEffect(() => {
987
+ try {
988
+ localStorage.setItem(THREAD_KEY, JSON.stringify(chat.messages()));
989
+ } catch { /* storage unavailable: run in-memory */ }
990
+ });
991
+ `;
992
+ }
993
+ const url = JSON.stringify(history.url);
994
+ return `
995
+ // History: persisted to your endpoint (GET on mount, PUT on every change) —
996
+ // the kit PARSES, this app FETCHES; your route owns the storage and what to
997
+ // retain and for how long. \`hydrated\` guards the mount-load from immediately
998
+ // PUTting back what it just loaded, but still flips on a FAILED load — one
999
+ // offline/CORS/DNS blip degrades to "start fresh, keep saving", not
1000
+ // "never save again".
1001
+ let hydrated = false;
1002
+ (async () => {
1003
+ try {
1004
+ const r = await fetch(${url}${c.userId ? `, { headers: { 'x-kai-user-id': ${JSON.stringify(c.userId)} } }` : ""});
1005
+ const saved: unknown = r.ok ? await r.json() : [];
1006
+ if (Array.isArray(saved)) {
1007
+ chat.setMessages(() => saved as ChatMessage[]);
1008
+ } else {
1009
+ console.warn('history endpoint returned a non-array body; ignoring and starting fresh');
1010
+ }
1011
+ } catch (err) {
1012
+ console.error('history endpoint GET failed; starting fresh (will keep saving)', err);
1013
+ } finally {
1014
+ hydrated = true;
1015
+ }
1016
+ })();
1017
+ createEffect(() => {
1018
+ const snapshot = chat.messages();
1019
+ if (!hydrated) return;
1020
+ fetch(${url}, {
1021
+ method: 'PUT',
1022
+ headers: { 'content-type': 'application/json'${emitUserIdHeaderEntry(c)} },
1023
+ body: JSON.stringify(snapshot),
1024
+ }).catch((err) => {
1025
+ console.error('history endpoint PUT failed; this change was not persisted', err);
1026
+ });
1027
+ });
1028
+ `;
1029
+ }
1030
+ function emitProviderImports(c) {
1031
+ if (c.provider.mode === "mock") {
1032
+ return `import { createMockResponder } from '@kitn.ai/ui/state';
1033
+ import { readOpenAIStream } from '@kitn.ai/ui/wire';`;
1034
+ }
1035
+ const read = c.provider.wire === "openai" ? "readOpenAIStream" : "readAnthropicStream";
1036
+ const encode = c.provider.wire === "openai" ? "toOpenAIMessages" : "toAnthropicMessages";
1037
+ return `import { ${read}, ${encode} } from '@kitn.ai/ui/wire';`;
1038
+ }
1039
+ function emitProviderSetup(c) {
1040
+ if (c.provider.mode === "mock") {
1041
+ const cardsNote = c.cards ? `
1042
+ // Cards demo keylessly: createMockResponder() can already SCRIPT a tool call
1043
+ // (\`replies: [{ toolCalls: [...] }]\`, F-35), so a scripted turn calling
1044
+ // \`kai_<card name>\` renders exactly like a live model's would, below.` : "";
1045
+ return `// Provider seam: mock — keyless, streams locally, announces itself once.
1046
+ // Swap for provider.mode "endpoint" in the construct and re-run kai dev; the
1047
+ // generated fetch keeps this exact shape (the seam is the point).${cardsNote}
1048
+ const respond = createMockResponder();
1049
+ const chat = createKaiChat();
1050
+
1051
+ async function submit(detail: { value: string; attachments: AttachmentData[] }) {
1052
+ if (!detail.value.trim() || chat.loading()) return;
1053
+ chat.append({
1054
+ id: crypto.randomUUID(),
1055
+ role: 'user',
1056
+ parts: [
1057
+ { type: 'text', text: detail.value },
1058
+ ...detail.attachments.map((attachment) => ({ type: 'file' as const, attachment })),
1059
+ ],
1060
+ });
1061
+ const stream = chat.streamAssistant();
1062
+ try {
1063
+ await readOpenAIStream(respond(detail.value), stream);${emitApplyCardTools(c)}
1064
+ stream.done();
1065
+ } catch (err) {
1066
+ stream.abort(err instanceof Error ? err.message : String(err));
1067
+ }
1068
+ }`;
1069
+ }
1070
+ const { url, wire } = c.provider;
1071
+ const read = wire === "openai" ? "readOpenAIStream" : "readAnthropicStream";
1072
+ const encode = wire === "openai" ? "toOpenAIMessages" : "toAnthropicMessages";
1073
+ return `// Provider seam: YOUR endpoint (${wire} wire, see the fetch call below
1074
+ // for the URL). The kit PARSES, this component FETCHES — no key, no
1075
+ // provider SDK, no client in here. Your route holds the key and re-frames to
1076
+ // the provider; the kai MCP scaffold tool emits one for your framework.
1077
+ const chat = createKaiChat();
1078
+
1079
+ async function submit(detail: { value: string; attachments: AttachmentData[] }) {
1080
+ if (!detail.value.trim() || chat.loading()) return;
1081
+ chat.append({
1082
+ id: crypto.randomUUID(),
1083
+ role: 'user',
1084
+ parts: [
1085
+ { type: 'text', text: detail.value },
1086
+ ...detail.attachments.map((attachment) => ({ type: 'file' as const, attachment })),
1087
+ ],
1088
+ });
1089
+ const stream = chat.streamAssistant();
1090
+ try {
1091
+ const response = await fetch(${JSON.stringify(url)}, {
1092
+ method: 'POST',
1093
+ headers: { 'content-type': 'application/json'${emitUserIdHeaderEntry(c)} },
1094
+ body: JSON.stringify({ messages: ${encode}(chat.messages())${emitToolsField(c)} }),
1095
+ });
1096
+ if (!response.ok) throw new Error(\`endpoint responded \${response.status}\`);
1097
+ await ${read}(response, stream);${emitApplyCardTools(c)}
1098
+ stream.done();
1099
+ } catch (err) {
1100
+ stream.abort(err instanceof Error ? err.message : String(err));
1101
+ }
1102
+ }`;
1103
+ }
1104
+ function emitLayoutImport(c) {
1105
+ switch (c.layout) {
1106
+ case "widget":
1107
+ return `, Dock${hasLauncherIcon(c) ? ", DockLauncherImage" : ""}`;
1108
+ case "split":
1109
+ return ", WorkspaceShell";
1110
+ case "fullscreen":
1111
+ case "aside":
1112
+ return "";
1113
+ case "custom":
1114
+ return "";
1115
+ }
1116
+ }
1117
+ function emitSlots(slots, indent) {
1118
+ if (!slots || slots.length === 0) return "";
1119
+ return slots.map((name) => `${indent}<slot name="${name}" />
1120
+ `).join("");
1121
+ }
1122
+ function emitDockPosition(c) {
1123
+ const w = c.layout === "widget" ? c.widget : void 0;
1124
+ return w?.position ? ` position="${w.position}"` : "";
1125
+ }
1126
+ function hasLauncherIcon(c) {
1127
+ return c.layout === "widget" && !!c.widget?.launcherIcon;
1128
+ }
1129
+ function emitDockLauncher(c) {
1130
+ const w = c.layout === "widget" ? c.widget : void 0;
1131
+ if (!w?.launcherIcon) return "";
1132
+ return ` launcher={<DockLauncherImage src={${JSON.stringify(w.launcherIcon)}} />}`;
1133
+ }
1134
+ function emitDockDefaultOpen(c) {
1135
+ const w = c.layout === "widget" ? c.widget : void 0;
1136
+ return w?.defaultOpen === true ? " defaultOpen={true}" : "";
1137
+ }
1138
+ function emitLayoutOpen(c) {
1139
+ switch (c.layout) {
1140
+ case "widget":
1141
+ return ` <Dock label="${c.name}"${emitDockPosition(c)}${emitDockLauncher(c)}${emitDockDefaultOpen(c)}${emitDockHideClose(c)}${emitDockControllerRef(c)}${emitDockOnOpenChangeProp(c)}${emitDockUnreadProp(c)}>
1142
+ `;
1143
+ case "fullscreen":
1144
+ return ` <div style={{ height: '100dvh', display: 'flex', 'flex-direction': 'column' }}>
1145
+ `;
1146
+ case "aside":
1147
+ return ` <aside data-kai-layout="aside" style={{ position: 'fixed', 'inset-block': '0', 'inset-inline-end': '0', width: '380px', display: 'flex', 'flex-direction': 'column', 'border-inline-start': '1px solid var(--kai-color-border)' }}>
1148
+ {/* Mirrors Dock's own narrow-viewport full-bleed rule (ui/dock.tsx:229-240)
1149
+ — aside has no dedicated kit component (see the emitLayoutOpen doc
1150
+ comment above), so this is the honest hand-rolled equivalent, not a
1151
+ new responsive strategy. */}
1152
+ <style>{\`@media (max-width: 480px) { [data-kai-layout="aside"] { inset: 0; width: auto; height: auto; border-inline-start: 0; } }\`}</style>
1153
+ `;
1154
+ case "split":
1155
+ return ` <div style={{ height: '100dvh' }}>
1156
+ <WorkspaceShell class="h-full" drawerBelow={480} end={
1157
+ <div style={{ height: '100%', overflow: 'auto' }}>
1158
+ <slot name="pane" />
1159
+ </div>
1160
+ }>
1161
+ `;
1162
+ case "custom":
1163
+ return "";
1164
+ }
1165
+ }
1166
+ function emitLayoutClose(c) {
1167
+ switch (c.layout) {
1168
+ case "widget":
1169
+ return ` </Dock>
1170
+ `;
1171
+ case "fullscreen":
1172
+ return ` </div>
1173
+ `;
1174
+ case "aside":
1175
+ return ` </aside>
1176
+ `;
1177
+ case "split":
1178
+ return ` </WorkspaceShell>
1179
+ </div>
1180
+ `;
1181
+ case "custom":
1182
+ return "";
1183
+ }
1184
+ }
1185
+ function emitTypes(c) {
1186
+ return `declare global {
1187
+ interface HTMLElementTagNameMap {
1188
+ '${c.name}': HTMLElement & { theme: 'light' | 'dark' | 'auto' };
1189
+ }
1190
+ }
1191
+ export {};
1192
+ `;
1193
+ }
1194
+ const MANIFEST = ".kai-manifest.json";
1195
+ function writeProject(files, dir) {
1196
+ const manifestPath = join(dir, MANIFEST);
1197
+ const previous = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, "utf8")) : [];
1198
+ const current = new Set(files.map((f) => f.path));
1199
+ for (const stale of previous) {
1200
+ if (!current.has(stale)) rmSync(join(dir, stale), { force: true });
1201
+ }
1202
+ const overwritten = [];
1203
+ for (const f of files) {
1204
+ const abs = join(dir, f.path);
1205
+ if (existsSync(abs)) overwritten.push(f.path);
1206
+ mkdirSync(dirname(abs), { recursive: true });
1207
+ writeFileSync(abs, f.code);
1208
+ }
1209
+ writeFileSync(manifestPath, `${JSON.stringify([...current].sort(), null, 2)}
1210
+ `);
1211
+ return overwritten;
1212
+ }
1213
+ const defaultIo = { log: (s) => console.log(s), error: (s) => console.error(s) };
1214
+ const USAGE = `usage: kai <command>
1215
+
1216
+ kai validate <construct.json> check a construct, print problems with paths
1217
+ kai eject <construct.json> <outDir> write the generated Solid project (it's yours)
1218
+ kai dev <construct.json> live preview with reload-on-edit (Task 5)
1219
+ kai compile <construct.json> [outDir] one self-registering .js (Task 6)
1220
+ `;
1221
+ function homeRecentConversationWarning(construct) {
1222
+ if (construct.home?.recentConversation && !construct.capabilities?.conversations) {
1223
+ return "warning: home.recentConversation is set but capabilities.conversations is not — the recent-conversation card will render nothing without it.";
1224
+ }
1225
+ return null;
1226
+ }
1227
+ function loadConstruct(path, io) {
1228
+ const abs = resolve(path);
1229
+ let raw;
1230
+ try {
1231
+ raw = readFileSync(abs, "utf8");
1232
+ } catch {
1233
+ io.error(`cannot read ${abs}`);
1234
+ return null;
1235
+ }
1236
+ let json;
1237
+ try {
1238
+ json = JSON.parse(raw);
1239
+ } catch (err) {
1240
+ io.error(`${abs} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
1241
+ return null;
1242
+ }
1243
+ const out = validateConstruct(json);
1244
+ if (!out.ok) {
1245
+ io.error(`${abs} is not a valid construct:`);
1246
+ for (const p of out.problems) io.error(` ${p.path || "(root)"}: ${p.message}`);
1247
+ return null;
1248
+ }
1249
+ return out.construct;
1250
+ }
1251
+ function parseUiFlag(rest) {
1252
+ const uiFlag = rest.indexOf("--ui");
1253
+ const uiSpec = uiFlag >= 0 ? rest[uiFlag + 1] : void 0;
1254
+ const positional = uiFlag >= 0 ? rest.filter((_, i) => i !== uiFlag && i !== uiFlag + 1) : rest;
1255
+ return { uiSpec, positional };
1256
+ }
1257
+ async function runCli(argv, io = defaultIo) {
1258
+ const [command, ...rest] = argv;
1259
+ switch (command) {
1260
+ case "validate": {
1261
+ const construct = loadConstruct(rest[0] ?? "", io);
1262
+ if (!construct) return 1;
1263
+ io.log(`valid construct: <${construct.name}> (layout: ${construct.layout}, provider: ${construct.provider.mode})`);
1264
+ const warning = homeRecentConversationWarning(construct);
1265
+ if (warning) io.log(warning);
1266
+ return 0;
1267
+ }
1268
+ case "eject": {
1269
+ const { uiSpec, positional } = parseUiFlag(rest);
1270
+ const [path, outDir] = positional;
1271
+ if (!path || !outDir) {
1272
+ io.error(USAGE);
1273
+ return 2;
1274
+ }
1275
+ const construct = loadConstruct(path, io);
1276
+ if (!construct) return 1;
1277
+ const overwritten = writeProject(generateProject(construct, { uiSpec }), resolve(outDir));
1278
+ if (overwritten.length > 0) {
1279
+ io.log(`overwriting ${overwritten.length} existing file(s)`);
1280
+ }
1281
+ const notice = accentContrastNotice(construct);
1282
+ if (notice) io.log(notice);
1283
+ io.log(`ejected <${construct.name}> to ${resolve(outDir)} — npm install && npm run dev. The source is yours.`);
1284
+ return 0;
1285
+ }
1286
+ case "dev": {
1287
+ const { uiSpec, positional } = parseUiFlag(rest);
1288
+ const path = positional[0];
1289
+ if (!path) {
1290
+ io.error(USAGE);
1291
+ return 2;
1292
+ }
1293
+ const { dev } = await import("./assets/dev-5XzXYR21.js");
1294
+ await dev(path, { io, uiSpec });
1295
+ return 0;
1296
+ }
1297
+ case "compile": {
1298
+ const { uiSpec, positional } = parseUiFlag(rest);
1299
+ const [path, outArg] = positional;
1300
+ if (!path) {
1301
+ io.error(USAGE);
1302
+ return 2;
1303
+ }
1304
+ const construct = loadConstruct(path, io);
1305
+ if (!construct) return 1;
1306
+ const outDir = resolve(outArg ?? "dist-construct");
1307
+ const { workDirFor, ensureInstalled } = await import("./assets/dev-5XzXYR21.js");
1308
+ const dir = workDirFor(construct.name, process.cwd());
1309
+ const files = generateProject(construct, { uiSpec });
1310
+ writeProject(files, dir);
1311
+ await ensureInstalled(dir, files, io);
1312
+ await new Promise((done, fail) => {
1313
+ const child = spawn("npm", ["run", "build"], { cwd: dir, stdio: "inherit" });
1314
+ child.on("exit", (code) => code === 0 ? done() : fail(new Error(`vite build exited ${code}`)));
1315
+ });
1316
+ mkdirSync(outDir, { recursive: true });
1317
+ copyFileSync(join(dir, "dist", `${construct.name}.js`), join(outDir, `${construct.name}.js`));
1318
+ writeFileSync(join(outDir, `${construct.name}.d.ts`), emitTypes(construct));
1319
+ writeProject(files, join(outDir, "source"));
1320
+ io.log(`compiled <${construct.name}> → ${outDir}/${construct.name}.js (source beside it in source/).`);
1321
+ io.log(`endpoint backends: the kai MCP scaffold tool emits a matching route — see its output for your framework.`);
1322
+ return 0;
1323
+ }
1324
+ default:
1325
+ io.error(USAGE);
1326
+ return 2;
1327
+ }
1328
+ }
1329
+ runCli(process.argv.slice(2)).then((code) => {
1330
+ process.exitCode = code;
1331
+ });
1332
+ export {
1333
+ accentContrastNotice as a,
1334
+ generateProject as g,
1335
+ validateConstruct as v,
1336
+ writeProject as w
1337
+ };