@kitn.ai/ui 0.15.0 → 0.16.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 (366) hide show
  1. package/README.md +9 -0
  2. package/bin/mcp.js +22 -0
  3. package/dist/bash-InADTalH.js +6 -1
  4. package/dist/components/artifact.d.ts +58 -0
  5. package/dist/components/attachments.d.ts +67 -0
  6. package/dist/components/card-fallback.d.ts +10 -0
  7. package/dist/components/card-renderer.d.ts +13 -0
  8. package/dist/components/card.d.ts +28 -0
  9. package/dist/components/chain-of-thought.d.ts +29 -0
  10. package/dist/components/chat-container.d.ts +19 -0
  11. package/dist/components/chat-scope-picker.d.ts +9 -0
  12. package/dist/components/chat-thread.d.ts +93 -0
  13. package/dist/components/checkpoint.d.ts +20 -0
  14. package/dist/components/choice-card.d.ts +77 -0
  15. package/dist/components/code-block.d.ts +16 -0
  16. package/dist/components/confirm-card.d.ts +57 -0
  17. package/dist/components/context.d.ts +65 -0
  18. package/dist/components/conversation-item.d.ts +8 -0
  19. package/dist/components/conversation-list.d.ts +11 -0
  20. package/dist/components/embed.d.ts +21 -0
  21. package/dist/components/empty.d.ts +43 -0
  22. package/dist/components/feedback-bar.d.ts +45 -0
  23. package/dist/components/file-tree.d.ts +57 -0
  24. package/dist/components/file-upload.d.ts +16 -0
  25. package/dist/components/form-widgets.d.ts +31 -0
  26. package/dist/components/form.d.ts +103 -0
  27. package/dist/components/image.d.ts +12 -0
  28. package/dist/components/link-preview.d.ts +23 -0
  29. package/dist/components/loader.d.ts +60 -0
  30. package/dist/components/markdown.d.ts +8 -0
  31. package/dist/components/message-skills.d.ts +15 -0
  32. package/dist/components/message.d.ts +54 -0
  33. package/dist/components/model-switcher.d.ts +8 -0
  34. package/dist/components/prompt-input.d.ts +38 -0
  35. package/dist/components/prompt-suggestion.d.ts +13 -0
  36. package/dist/components/reasoning.d.ts +20 -0
  37. package/dist/components/response-stream.d.ts +14 -0
  38. package/dist/components/scroll-button.d.ts +7 -0
  39. package/dist/components/slash-command.d.ts +15 -0
  40. package/dist/components/source.d.ts +23 -0
  41. package/dist/components/tasks-card.d.ts +71 -0
  42. package/dist/components/text-shimmer.d.ts +9 -0
  43. package/dist/components/thinking-bar.d.ts +10 -0
  44. package/dist/components/tool.d.ts +16 -0
  45. package/dist/components/use-card-resolution.d.ts +12 -0
  46. package/dist/components/voice-input.d.ts +7 -0
  47. package/dist/core-AYMC6_lb.js +5874 -12
  48. package/dist/css-M7EaDHN_.js +6 -1
  49. package/dist/elements/artifact.d.ts +1 -0
  50. package/dist/elements/attachments.d.ts +1 -0
  51. package/dist/elements/card.d.ts +1 -0
  52. package/dist/elements/cards.d.ts +0 -0
  53. package/dist/elements/chain-of-thought.d.ts +13 -0
  54. package/dist/elements/chat-scope-picker.d.ts +1 -0
  55. package/dist/elements/chat-types.d.ts +41 -0
  56. package/dist/elements/chat-workspace.d.ts +1 -0
  57. package/dist/elements/chat.d.ts +1 -0
  58. package/dist/elements/checkpoint.d.ts +1 -0
  59. package/dist/elements/choice.d.ts +1 -0
  60. package/dist/elements/code-block.d.ts +1 -0
  61. package/dist/elements/confirm-card.d.ts +1 -0
  62. package/dist/elements/context-meter.d.ts +1 -0
  63. package/dist/elements/conversation-list.d.ts +11 -0
  64. package/dist/elements/css.d.ts +1 -0
  65. package/dist/elements/default-input.d.ts +40 -0
  66. package/dist/elements/define.d.ts +42 -0
  67. package/dist/elements/embed.d.ts +1 -0
  68. package/dist/elements/empty.d.ts +1 -0
  69. package/dist/elements/feedback-bar.d.ts +1 -0
  70. package/dist/elements/file-tree.d.ts +1 -0
  71. package/dist/elements/file-upload.d.ts +1 -0
  72. package/dist/elements/form.d.ts +1 -0
  73. package/dist/elements/image.d.ts +1 -0
  74. package/dist/elements/link-preview.d.ts +1 -0
  75. package/dist/elements/loader.d.ts +1 -0
  76. package/dist/elements/markdown.d.ts +1 -0
  77. package/dist/elements/message-skills.d.ts +17 -0
  78. package/dist/elements/message.d.ts +1 -0
  79. package/dist/elements/model-switcher.d.ts +11 -0
  80. package/dist/elements/popover.d.ts +1 -0
  81. package/dist/elements/prompt-input.d.ts +9 -0
  82. package/dist/elements/prompt-suggestions.d.ts +7 -0
  83. package/dist/elements/reasoning.d.ts +1 -0
  84. package/dist/elements/register-impl.d.ts +0 -0
  85. package/dist/elements/register.d.ts +3 -0
  86. package/dist/elements/remote.d.ts +1 -0
  87. package/dist/elements/resizable.d.ts +17 -0
  88. package/dist/elements/response-stream.d.ts +1 -0
  89. package/dist/elements/scroll-button.d.ts +1 -0
  90. package/dist/elements/source.d.ts +18 -0
  91. package/dist/elements/switch.d.ts +1 -0
  92. package/dist/elements/tasks.d.ts +1 -0
  93. package/dist/elements/text-shimmer.d.ts +1 -0
  94. package/dist/elements/thinking-bar.d.ts +1 -0
  95. package/dist/elements/tool.d.ts +1 -0
  96. package/dist/elements/voice-input.d.ts +1 -0
  97. package/dist/elements.d.ts +732 -0
  98. package/dist/engine-javascript-vq0WuIJl.js +2516 -14
  99. package/dist/github-dark-dimmed-DUshB20C.js +4 -1
  100. package/dist/github-light-JYsPkUQd.js +4 -1
  101. package/dist/html-CPZ3oZQ7.js +10 -1
  102. package/dist/index.d.ts +112 -2
  103. package/dist/index.js +13551 -0
  104. package/dist/javascript-C25yR2R2.js +6 -1
  105. package/dist/json-DxJze_jm.js +6 -1
  106. package/dist/kitn-chat.es.js +1 -77
  107. package/dist/llms/llms-full.txt +1 -1
  108. package/dist/llms/llms.txt +1 -1
  109. package/dist/mcp.es.js +2140 -0
  110. package/dist/primitives/card-contract.d.ts +103 -0
  111. package/dist/primitives/card-host.d.ts +12 -0
  112. package/dist/primitives/card-registry.d.ts +17 -0
  113. package/dist/primitives/card-resolution.d.ts +8 -0
  114. package/dist/primitives/card-routing.d.ts +12 -0
  115. package/dist/primitives/card-validate.d.ts +25 -0
  116. package/dist/primitives/chat-config.d.ts +31 -0
  117. package/dist/primitives/controllable.d.ts +13 -0
  118. package/dist/primitives/embed-providers.d.ts +65 -0
  119. package/dist/primitives/highlighter.d.ts +23 -0
  120. package/dist/primitives/link-preview.d.ts +47 -0
  121. package/dist/primitives/pdf-preview.d.ts +50 -0
  122. package/dist/primitives/use-auto-resize.d.ts +8 -0
  123. package/dist/primitives/use-resize-observer.d.ts +1 -0
  124. package/dist/primitives/use-stick-to-bottom.d.ts +5 -0
  125. package/dist/primitives/use-text-stream.d.ts +19 -0
  126. package/dist/primitives/use-voice-recorder.d.ts +9 -0
  127. package/dist/react/index.d.ts +1118 -0
  128. package/dist/react/runtime.d.ts +15 -0
  129. package/dist/react.js +265 -0
  130. package/dist/register-impl-wCsFPoh9.js +77 -0
  131. package/dist/register-ujhr1nBS.js +1 -0
  132. package/dist/remote/host-embed.d.ts +33 -0
  133. package/dist/remote/index.d.ts +2 -0
  134. package/dist/remote/origin.d.ts +5 -0
  135. package/dist/remote/provider-runtime.d.ts +27 -0
  136. package/dist/remote/provider.d.ts +2 -0
  137. package/dist/remote/validate.d.ts +4 -0
  138. package/dist/remote/version.d.ts +4 -0
  139. package/dist/remote/wire.d.ts +53 -0
  140. package/dist/svelte--5p79yCD.js +15 -1
  141. package/dist/tsx-B8rCNbgL.js +6 -1
  142. package/dist/types.d.ts +42 -0
  143. package/dist/typescript-RycA9KXf.js +6 -1
  144. package/dist/ui/action-icons.d.ts +11 -0
  145. package/dist/ui/avatar.d.ts +8 -0
  146. package/dist/ui/badge.d.ts +9 -0
  147. package/dist/ui/button.d.ts +10 -0
  148. package/dist/ui/collapsible.d.ts +27 -0
  149. package/dist/ui/dropdown.d.ts +20 -0
  150. package/dist/ui/hover-card.d.ts +28 -0
  151. package/dist/ui/overlay.d.ts +67 -0
  152. package/dist/ui/popover.d.ts +32 -0
  153. package/dist/ui/resizable.d.ts +76 -0
  154. package/dist/ui/scroll-area.d.ts +5 -0
  155. package/dist/ui/separator.d.ts +5 -0
  156. package/dist/ui/skeleton.d.ts +5 -0
  157. package/dist/ui/switch.d.ts +19 -0
  158. package/dist/ui/textarea.d.ts +6 -0
  159. package/dist/ui/tooltip.d.ts +8 -0
  160. package/dist/utils/cn.d.ts +2 -0
  161. package/dist/vue-BmIZj4XD.js +33 -1
  162. package/frameworks/react/index.tsx +4 -0
  163. package/frameworks/react/runtime.tsx +16 -2
  164. package/llms-full.txt +1 -1
  165. package/llms.txt +1 -1
  166. package/package.json +29 -11
  167. package/src/agent-tooling/archetypes.ts +46 -0
  168. package/src/agent-tooling/integrations/cloudflare.ts +88 -0
  169. package/src/agent-tooling/integrations/langgraph.ts +60 -0
  170. package/src/agent-tooling/integrations/mastra.ts +27 -0
  171. package/src/agent-tooling/integrations/mock.ts +34 -0
  172. package/src/agent-tooling/integrations/ollama.ts +47 -0
  173. package/src/agent-tooling/integrations/openrouter.ts +26 -0
  174. package/src/agent-tooling/integrations/pi.ts +40 -0
  175. package/src/agent-tooling/integrations/pydantic-ai.ts +52 -0
  176. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +44 -0
  177. package/src/agent-tooling/mcp/manifest.ts +128 -0
  178. package/src/agent-tooling/mcp/server.ts +59 -0
  179. package/src/agent-tooling/mcp/stdio.ts +18 -0
  180. package/src/agent-tooling/mcp/tools/debug.ts +305 -0
  181. package/src/agent-tooling/mcp/tools/reference.ts +132 -0
  182. package/src/agent-tooling/mcp/tools/scaffold.ts +1627 -0
  183. package/src/agent-tooling/mcp/tools/theme.ts +340 -0
  184. package/src/agent-tooling/mcp/tools/types.ts +14 -0
  185. package/src/agent-tooling/registry.ts +41 -0
  186. package/src/agent-tooling/types.ts +30 -0
  187. package/src/elements/compiled.css +1 -1
  188. package/src/elements/define.tsx +7 -1
  189. package/src/elements/element-types.d.ts +2 -1
  190. package/src/elements/register-impl.ts +55 -0
  191. package/src/elements/register.ts +17 -51
  192. package/theme.css +4 -2
  193. package/src/components/artifact.stories.tsx +0 -138
  194. package/src/components/attachments.stories.tsx +0 -303
  195. package/src/components/attachments.test.tsx +0 -64
  196. package/src/components/chain-of-thought.stories.tsx +0 -211
  197. package/src/components/chat-container.stories.tsx +0 -187
  198. package/src/components/chat-thread.test.tsx +0 -58
  199. package/src/components/checkpoint.stories.tsx +0 -102
  200. package/src/components/code-block.stories.tsx +0 -150
  201. package/src/components/confirm-card.stories.tsx +0 -74
  202. package/src/components/context.stories.tsx +0 -229
  203. package/src/components/context.test.tsx +0 -133
  204. package/src/components/conversation-item.stories.tsx +0 -125
  205. package/src/components/conversation-list.stories.tsx +0 -133
  206. package/src/components/default-input.test.tsx +0 -111
  207. package/src/components/empty.stories.tsx +0 -434
  208. package/src/components/feedback-bar.stories.tsx +0 -138
  209. package/src/components/feedback-bar.test.tsx +0 -103
  210. package/src/components/file-tree.stories.tsx +0 -73
  211. package/src/components/file-upload.stories.tsx +0 -156
  212. package/src/components/image.stories.tsx +0 -89
  213. package/src/components/loader.stories.tsx +0 -181
  214. package/src/components/markdown.stories.tsx +0 -180
  215. package/src/components/message-action-bar.test.tsx +0 -128
  216. package/src/components/message-narrow.stories.tsx +0 -329
  217. package/src/components/message-skills.stories.tsx +0 -211
  218. package/src/components/message.stories.tsx +0 -281
  219. package/src/components/model-switcher.stories.tsx +0 -123
  220. package/src/components/model-switcher.test.tsx +0 -65
  221. package/src/components/prompt-input.stories.tsx +0 -222
  222. package/src/components/prompt-suggestion.stories.tsx +0 -142
  223. package/src/components/reasoning.stories.tsx +0 -140
  224. package/src/components/resizable.test.tsx +0 -226
  225. package/src/components/scroll-button.stories.tsx +0 -100
  226. package/src/components/slash-command.stories.tsx +0 -163
  227. package/src/components/source-list.test.tsx +0 -152
  228. package/src/components/source.stories.tsx +0 -124
  229. package/src/components/tasks-card.stories.tsx +0 -78
  230. package/src/components/text-shimmer.stories.tsx +0 -87
  231. package/src/components/thinking-bar.stories.tsx +0 -87
  232. package/src/components/tool.stories.tsx +0 -153
  233. package/src/components/voice-input.stories.tsx +0 -83
  234. package/src/elements/artifact.stories.tsx +0 -506
  235. package/src/elements/attachments.stories.tsx +0 -190
  236. package/src/elements/card.stories.tsx +0 -113
  237. package/src/elements/cards.stories.tsx +0 -59
  238. package/src/elements/catalog.stories.tsx +0 -491
  239. package/src/elements/chain-of-thought.declarative.test.tsx +0 -153
  240. package/src/elements/chain-of-thought.stories.tsx +0 -113
  241. package/src/elements/chat-scope-picker.stories.tsx +0 -73
  242. package/src/elements/chat-workspace.stories.tsx +0 -288
  243. package/src/elements/chat.stories.tsx +0 -151
  244. package/src/elements/checkpoint.stories.tsx +0 -72
  245. package/src/elements/choice.stories.tsx +0 -240
  246. package/src/elements/code-block.stories.tsx +0 -83
  247. package/src/elements/composed-shell.stories.tsx +0 -316
  248. package/src/elements/confirm-card.stories.tsx +0 -227
  249. package/src/elements/context-meter.stories.tsx +0 -151
  250. package/src/elements/conversation-list.declarative.test.tsx +0 -137
  251. package/src/elements/conversation-list.stories.tsx +0 -243
  252. package/src/elements/embed.stories.tsx +0 -213
  253. package/src/elements/empty.stories.tsx +0 -111
  254. package/src/elements/feedback-bar.stories.tsx +0 -114
  255. package/src/elements/file-tree.stories.tsx +0 -134
  256. package/src/elements/file-upload.stories.tsx +0 -82
  257. package/src/elements/form.stories.tsx +0 -248
  258. package/src/elements/image.stories.tsx +0 -71
  259. package/src/elements/link-preview.stories.tsx +0 -209
  260. package/src/elements/loader.stories.tsx +0 -88
  261. package/src/elements/markdown.stories.tsx +0 -76
  262. package/src/elements/message-skills.declarative.test.tsx +0 -104
  263. package/src/elements/message-skills.stories.tsx +0 -114
  264. package/src/elements/message.stories.tsx +0 -218
  265. package/src/elements/model-switcher.declarative.test.tsx +0 -130
  266. package/src/elements/model-switcher.stories.tsx +0 -198
  267. package/src/elements/popover.stories.tsx +0 -80
  268. package/src/elements/prompt-input-slash-command.test.tsx +0 -128
  269. package/src/elements/prompt-input-stoppable.test.tsx +0 -88
  270. package/src/elements/prompt-input-toolbar-actions.test.tsx +0 -120
  271. package/src/elements/prompt-input.stories.tsx +0 -331
  272. package/src/elements/prompt-suggestions.declarative.test.tsx +0 -164
  273. package/src/elements/prompt-suggestions.stories.tsx +0 -213
  274. package/src/elements/reasoning.stories.tsx +0 -77
  275. package/src/elements/remote.stories.tsx +0 -314
  276. package/src/elements/resizable.stories.tsx +0 -426
  277. package/src/elements/response-stream.stories.tsx +0 -79
  278. package/src/elements/scroll-button.stories.tsx +0 -202
  279. package/src/elements/scroll-button.test.tsx +0 -148
  280. package/src/elements/source-list.stories.tsx +0 -143
  281. package/src/elements/source.stories.tsx +0 -88
  282. package/src/elements/switch.stories.tsx +0 -56
  283. package/src/elements/tasks.stories.tsx +0 -237
  284. package/src/elements/text-shimmer.stories.tsx +0 -64
  285. package/src/elements/thinking-bar.stories.tsx +0 -72
  286. package/src/elements/tool.stories.tsx +0 -89
  287. package/src/elements/voice-input.stories.tsx +0 -88
  288. package/src/primitives/controllable.test.ts +0 -47
  289. package/src/stories/chat-panel-layout.stories.tsx +0 -145
  290. package/src/stories/chat-scene.tsx +0 -571
  291. package/src/stories/checkpoint-restore.stories.tsx +0 -256
  292. package/src/stories/context-usage.stories.tsx +0 -212
  293. package/src/stories/conversation-with-reasoning.stories.tsx +0 -182
  294. package/src/stories/conversation-with-sources.stories.tsx +0 -471
  295. package/src/stories/docs/Accessibility.mdx +0 -119
  296. package/src/stories/docs/ChildElements.mdx +0 -269
  297. package/src/stories/docs/ForAIAgents.mdx +0 -162
  298. package/src/stories/docs/GettingStarted.mdx +0 -65
  299. package/src/stories/docs/Installation.mdx +0 -83
  300. package/src/stories/docs/Introduction.mdx +0 -50
  301. package/src/stories/docs/SolidJsAdvanced.mdx +0 -17
  302. package/src/stories/docs/Theming.mdx +0 -85
  303. package/src/stories/docs/element-controls.ts +0 -121
  304. package/src/stories/docs/frameworks/Angular.mdx +0 -245
  305. package/src/stories/docs/frameworks/Html.mdx +0 -227
  306. package/src/stories/docs/frameworks/Overview.mdx +0 -47
  307. package/src/stories/docs/frameworks/React.mdx +0 -227
  308. package/src/stories/docs/frameworks/Solid.mdx +0 -173
  309. package/src/stories/docs/frameworks/Svelte.mdx +0 -194
  310. package/src/stories/docs/frameworks/Vue.mdx +0 -238
  311. package/src/stories/docs/generative-ui-overview.mdx +0 -186
  312. package/src/stories/docs/recipes/SpeechToText.mdx +0 -7
  313. package/src/stories/docs/recipes/Streaming.mdx +0 -68
  314. package/src/stories/docs/recipes/TextToSpeech.mdx +0 -35
  315. package/src/stories/docs/theme-editor/canvas.tsx +0 -32
  316. package/src/stories/docs/theme-editor/inspector.tsx +0 -66
  317. package/src/stories/docs/theme-editor/presets.test.ts +0 -32
  318. package/src/stories/docs/theme-editor/presets.ts +0 -64
  319. package/src/stories/docs/theme-editor/theme-css.test.ts +0 -19
  320. package/src/stories/docs/theme-editor/theme-css.ts +0 -15
  321. package/src/stories/docs/theme-editor/theme-editor.tsx +0 -146
  322. package/src/stories/docs/theme-tokens.tsx +0 -174
  323. package/src/stories/examples/ChoosingComponents.mdx +0 -106
  324. package/src/stories/examples/sample-data.ts +0 -79
  325. package/src/stories/examples/usage/checkpoint-restore.ts +0 -156
  326. package/src/stories/examples/usage/context-usage.ts +0 -591
  327. package/src/stories/examples/usage/conversation-with-reasoning.ts +0 -224
  328. package/src/stories/examples/usage/conversation-with-sources.ts +0 -589
  329. package/src/stories/examples/usage/empty-state.ts +0 -144
  330. package/src/stories/examples/usage/full-chat-app.ts +0 -277
  331. package/src/stories/examples/usage/index.ts +0 -63
  332. package/src/stories/examples/usage/message-actions.ts +0 -704
  333. package/src/stories/examples/usage/pattern-centered-conversation.ts +0 -446
  334. package/src/stories/examples/usage/pattern-chat-panel-layout.ts +0 -442
  335. package/src/stories/examples/usage/pattern-docked-widget.ts +0 -479
  336. package/src/stories/examples/usage/prompt-input-variants.ts +0 -1356
  337. package/src/stories/examples/usage/streaming-response.ts +0 -687
  338. package/src/stories/examples/usage/types.ts +0 -33
  339. package/src/stories/full-chat.stories.tsx +0 -18
  340. package/src/stories/message-actions.stories.tsx +0 -230
  341. package/src/stories/pattern-centered-conversation.stories.tsx +0 -93
  342. package/src/stories/pattern-docked-widget.stories.tsx +0 -93
  343. package/src/stories/pattern-empty-state.stories.tsx +0 -76
  344. package/src/stories/prompt-input-variants.stories.tsx +0 -456
  345. package/src/stories/streaming-response.stories.tsx +0 -438
  346. package/src/stories/theme-editor.stories.tsx +0 -16
  347. package/src/stories/token-reference.stories.tsx +0 -18
  348. package/src/stories/typography.stories.tsx +0 -78
  349. package/src/ui/avatar.stories.tsx +0 -103
  350. package/src/ui/badge.stories.tsx +0 -86
  351. package/src/ui/button.stories.tsx +0 -145
  352. package/src/ui/collapsible.stories.tsx +0 -69
  353. package/src/ui/dropdown.stories.tsx +0 -59
  354. package/src/ui/hover-card.stories.tsx +0 -77
  355. package/src/ui/hover-card.test.tsx +0 -30
  356. package/src/ui/overlay.stories.tsx +0 -115
  357. package/src/ui/popover.stories.tsx +0 -81
  358. package/src/ui/popover.test.tsx +0 -99
  359. package/src/ui/resizable.stories.tsx +0 -236
  360. package/src/ui/scroll-area.stories.tsx +0 -50
  361. package/src/ui/separator.stories.tsx +0 -81
  362. package/src/ui/skeleton.stories.tsx +0 -337
  363. package/src/ui/switch.stories.tsx +0 -51
  364. package/src/ui/switch.test.tsx +0 -59
  365. package/src/ui/textarea.stories.tsx +0 -76
  366. package/src/ui/tooltip.stories.tsx +0 -74
package/dist/mcp.es.js ADDED
@@ -0,0 +1,2140 @@
1
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
+ import { z } from "zod";
5
+ import { readFileSync, existsSync } from "node:fs";
6
+ import { fileURLToPath } from "node:url";
7
+ import { dirname, join } from "node:path";
8
+ function resolveManifestPath() {
9
+ const thisFile = fileURLToPath(import.meta.url);
10
+ const thisDir = dirname(thisFile);
11
+ const sibling = join(thisDir, "custom-elements.json");
12
+ if (existsSync(sibling)) {
13
+ return sibling;
14
+ }
15
+ let dir = thisDir;
16
+ for (let i = 0; i < 10; i++) {
17
+ const candidate = join(dir, "dist", "custom-elements.json");
18
+ if (existsSync(candidate)) {
19
+ return candidate;
20
+ }
21
+ const parent = dirname(dir);
22
+ if (parent === dir) break;
23
+ dir = parent;
24
+ }
25
+ throw new Error(
26
+ `Could not find custom-elements.json. Searched from: ${thisDir}`
27
+ );
28
+ }
29
+ let _manifest;
30
+ function getManifest() {
31
+ if (!_manifest) {
32
+ const path = resolveManifestPath();
33
+ const raw = readFileSync(path, "utf-8");
34
+ _manifest = JSON.parse(raw);
35
+ }
36
+ return _manifest;
37
+ }
38
+ function getDeclarations() {
39
+ return getManifest().modules.flatMap((m) => m.declarations ?? []);
40
+ }
41
+ function getElement(tag) {
42
+ return getDeclarations().find((d) => d.tagName === tag);
43
+ }
44
+ function listElements() {
45
+ return getDeclarations().filter((d) => d.tagName).map((d) => d.tagName).sort();
46
+ }
47
+ const JS_ONLY_TYPE_PATTERNS = /\[\]|\{|Record</;
48
+ function isJsOnlyType(typeText) {
49
+ return typeText ? JS_ONLY_TYPE_PATTERNS.test(typeText) : false;
50
+ }
51
+ function formatReference(tag) {
52
+ const el = getElement(tag);
53
+ if (!el) {
54
+ const all = listElements();
55
+ const sample = all.slice(0, 5).join(", ");
56
+ return `Unknown element: ${tag}
57
+
58
+ Valid tags include: ${sample} (and ${Math.max(0, all.length - 5)} more).
59
+ Call component_reference with no name (or name: "list") to list all ${all.length} elements.`;
60
+ }
61
+ const lines = [];
62
+ lines.push(`## <${tag}>`);
63
+ if (el.description) {
64
+ lines.push("", el.description.trim());
65
+ }
66
+ lines.push(
67
+ "",
68
+ "### AI/UI contract",
69
+ '`kai-*` elements accept **array and object data as JavaScript properties** (set in JavaScript via `el.property = value`, not as HTML attributes). Events are native CustomEvents — listen with `el.addEventListener("event-name", handler)` and read `event.detail` for the payload.'
70
+ );
71
+ const publicFields = (el.members ?? []).filter(
72
+ (m) => m.kind === "field" && m.privacy === "public"
73
+ );
74
+ if (publicFields.length > 0) {
75
+ lines.push("", "### Props (JavaScript properties)");
76
+ for (const field of publicFields) {
77
+ const type = field.type?.text ?? "unknown";
78
+ const jsOnly = isJsOnlyType(type);
79
+ const desc = field.description?.trim() ?? "";
80
+ const note = jsOnly ? " ⚑ set as a JS property, not an HTML attribute" : "";
81
+ lines.push(`- **${field.name}** \`${type}\`${note}`);
82
+ if (desc) {
83
+ lines.push(` ${desc}`);
84
+ }
85
+ }
86
+ }
87
+ const attrs = el.attributes ?? [];
88
+ if (attrs.length > 0) {
89
+ lines.push("", "### Attributes (HTML-safe)");
90
+ for (const attr of attrs) {
91
+ const type = attr.type?.text ?? "unknown";
92
+ const desc = attr.description?.trim() ?? "";
93
+ lines.push(`- **${attr.name}** \`${type}\``);
94
+ if (desc) {
95
+ lines.push(` ${desc}`);
96
+ }
97
+ }
98
+ }
99
+ const events = el.events ?? [];
100
+ if (events.length > 0) {
101
+ lines.push("", "### Events (CustomEvent, listen via addEventListener)");
102
+ for (const ev of events) {
103
+ const desc = ev.description?.trim() ?? "";
104
+ lines.push(`- **${ev.name}** — ${desc}`);
105
+ }
106
+ }
107
+ const cssProps = el.cssProperties ?? [];
108
+ if (cssProps.length > 0) {
109
+ lines.push("", "### CSS custom properties");
110
+ for (const prop of cssProps) {
111
+ const desc = prop.description?.trim() ?? "";
112
+ const def = prop.default ? ` (default: ${prop.default})` : "";
113
+ lines.push(`- **${prop.name}**${def}${desc ? ` — ${desc}` : ""}`);
114
+ }
115
+ }
116
+ return lines.join("\n");
117
+ }
118
+ const reference = {
119
+ name: "component_reference",
120
+ description: "Look up AI/UI (kai-*) web components: their tags, props, events, and usage examples.",
121
+ inputSchema: z.object({ name: z.string().optional() }),
122
+ handler: async (args) => {
123
+ const name = typeof args.name === "string" ? args.name.trim() : void 0;
124
+ let text2;
125
+ if (!name || name === "list") {
126
+ const tags = listElements();
127
+ text2 = `AI/UI elements (${tags.length} total):
128
+
129
+ ` + tags.map((t) => ` ${t}`).join("\n") + '\n\nCall component_reference with a specific name (e.g. { name: "kai-chat" }) for full API details.';
130
+ } else {
131
+ text2 = formatReference(name);
132
+ }
133
+ return { content: [{ type: "text", text: text2 }] };
134
+ }
135
+ };
136
+ const Category = z.enum(["provider", "gateway", "framework", "harness", "mock"]);
137
+ const Language = z.enum(["ts", "python"]);
138
+ const StreamFormat = z.enum(["openai-sse", "ai-sdk", "native"]);
139
+ const Framework = z.enum(["html", "react", "next", "vue", "svelte", "fastapi", "express", "worker", "tanstack-start"]);
140
+ const Placement = z.enum(["side", "full-page", "docked-widget", "inline"]);
141
+ z.object({
142
+ id: z.string(),
143
+ title: z.string(),
144
+ category: Category,
145
+ language: Language,
146
+ streamFormat: StreamFormat,
147
+ envVars: z.array(z.string()).default([]),
148
+ routeTemplates: z.record(z.string(), z.string()),
149
+ // keyed by Framework value → code string
150
+ streamMapping: z.string(),
151
+ // prose: how the stream maps to messages
152
+ runNote: z.string(),
153
+ docsSlug: z.string()
154
+ });
155
+ z.object({
156
+ id: z.string(),
157
+ title: z.string(),
158
+ components: z.array(z.string()),
159
+ // kai-* tags, e.g. ['kai-chat', 'kai-sources']
160
+ defaultPlacement: Placement,
161
+ docsSlug: z.string()
162
+ });
163
+ const openrouter = {
164
+ id: "openrouter",
165
+ title: "OpenRouter",
166
+ category: "gateway",
167
+ language: "ts",
168
+ streamFormat: "openai-sse",
169
+ envVars: ["OPENROUTER_API_KEY"],
170
+ routeTemplates: {
171
+ next: `export async function POST(req: Request) {
172
+ const { model, messages } = await req.json();
173
+ const upstream = await fetch('https://openrouter.ai/api/v1/chat/completions', {
174
+ method: 'POST',
175
+ headers: { Authorization: \`Bearer \${process.env.OPENROUTER_API_KEY}\`, 'Content-Type': 'application/json' },
176
+ body: JSON.stringify({ model, messages, stream: true }),
177
+ });
178
+ return new Response(upstream.body, { headers: { 'Content-Type': 'text/event-stream' } });
179
+ }`
180
+ },
181
+ streamMapping: "OpenRouter returns OpenAI-format SSE — pipe upstream.body straight to the browser; the Streaming-recipe reader handles it.",
182
+ runNote: "Set OPENROUTER_API_KEY. Model ids are vendor/model, e.g. openai/gpt-4o.",
183
+ docsSlug: "integrations/connect-any-model"
184
+ };
185
+ const vercelAiSdk = {
186
+ id: "vercel-ai-sdk",
187
+ title: "Vercel AI SDK",
188
+ category: "framework",
189
+ language: "ts",
190
+ streamFormat: "ai-sdk",
191
+ envVars: ["AI_GATEWAY_API_KEY"],
192
+ routeTemplates: {
193
+ next: `// app/api/chat/route.ts
194
+ import { streamText } from 'ai';
195
+
196
+ export const maxDuration = 30; // allow long streaming responses
197
+
198
+ export async function POST(req: Request) {
199
+ const { messages } = await req.json();
200
+
201
+ const result = streamText({
202
+ model: 'openai/gpt-4o', // AI Gateway id; needs AI_GATEWAY_API_KEY
203
+ messages,
204
+ });
205
+
206
+ const encoder = new TextEncoder();
207
+ const sse = new ReadableStream({
208
+ async start(controller) {
209
+ for await (const delta of result.textStream) {
210
+ const chunk = { choices: [{ delta: { content: delta } }] };
211
+ controller.enqueue(encoder.encode(\`data: \${JSON.stringify(chunk)}\\n\\n\`));
212
+ }
213
+ controller.enqueue(encoder.encode('data: [DONE]\\n\\n'));
214
+ controller.close();
215
+ },
216
+ });
217
+
218
+ return new Response(sse, { headers: { 'Content-Type': 'text/event-stream' } });
219
+ }`
220
+ },
221
+ streamMapping: "The Vercel AI SDK's toUIMessageStreamResponse() and toTextStreamResponse() don't emit OpenAI-format SSE. Wrap result.textStream manually: iterate text deltas and emit data: {choices:[{delta:{content}}]} frames, closing with data: [DONE]. The kai-chat SSE reader handles it.",
222
+ runNote: "Set AI_GATEWAY_API_KEY for the AI Gateway (string model id form: creator/model-name). For direct provider access, import its provider package (e.g. @ai-sdk/openai) and set the corresponding key (e.g. OPENAI_API_KEY).",
223
+ docsSlug: "integrations/vercel-ai-sdk"
224
+ };
225
+ const langgraph = {
226
+ id: "langgraph",
227
+ title: "LangGraph",
228
+ category: "framework",
229
+ language: "ts",
230
+ streamFormat: "openai-sse",
231
+ envVars: ["OPENAI_API_KEY"],
232
+ routeTemplates: {
233
+ next: `// POST /api/chat — stream a compiled LangGraph agent to the browser
234
+ import { createReactAgent } from '@langchain/langgraph/prebuilt';
235
+ import { ChatOpenAI } from '@langchain/openai';
236
+ import { tool } from '@langchain/core/tools';
237
+ import { z } from 'zod';
238
+
239
+ const getWeather = tool(
240
+ async ({ city }) => \`It's 18°C and clear in \${city}.\`,
241
+ {
242
+ name: 'get_weather',
243
+ description: 'Get the current weather for a city.',
244
+ schema: z.object({ city: z.string() }),
245
+ },
246
+ );
247
+
248
+ const agent = createReactAgent({
249
+ llm: new ChatOpenAI({ model: 'gpt-4o' }),
250
+ tools: [getWeather],
251
+ });
252
+
253
+ export async function POST(req: Request) {
254
+ const { messages } = await req.json();
255
+
256
+ const stream = await agent.stream({ messages }, { streamMode: 'messages' });
257
+
258
+ const encoder = new TextEncoder();
259
+ const body = new ReadableStream({
260
+ async start(controller) {
261
+ const send = (obj: unknown) =>
262
+ controller.enqueue(encoder.encode(\`data: \${JSON.stringify(obj)}\\n\\n\`));
263
+
264
+ for await (const [chunk] of stream) {
265
+ if (typeof chunk.content === 'string' && chunk.content) {
266
+ send({ choices: [{ delta: { content: chunk.content } }] });
267
+ }
268
+ }
269
+ controller.enqueue(encoder.encode('data: [DONE]\\n\\n'));
270
+ controller.close();
271
+ },
272
+ });
273
+
274
+ return new Response(body, { headers: { 'Content-Type': 'text/event-stream' } });
275
+ }`
276
+ },
277
+ streamMapping: "Use graph.stream(input, { streamMode: 'messages' }) to get [messageChunk, metadata] tuples. Extract chunk.content (string) and forward as OpenAI-format SSE frames: data: {choices:[{delta:{content}}]}. Close with data: [DONE]. The kai-chat reader handles it.",
278
+ runNote: "Set OPENAI_API_KEY (or the key for your chosen model provider). Install @langchain/langgraph, @langchain/openai, @langchain/core.",
279
+ docsSlug: "integrations/langgraph"
280
+ };
281
+ const cloudflare = {
282
+ id: "cloudflare",
283
+ title: "Cloudflare AI",
284
+ category: "provider",
285
+ language: "ts",
286
+ streamFormat: "openai-sse",
287
+ envVars: ["CF_ACCOUNT_ID", "CF_API_TOKEN"],
288
+ routeTemplates: {
289
+ next: `// app/api/chat/route.ts — proxy Workers AI, keep the token server-side
290
+ export async function POST(req: Request) {
291
+ const { messages } = await req.json();
292
+
293
+ const upstream = await fetch(
294
+ \`https://api.cloudflare.com/client/v4/accounts/\${process.env.CF_ACCOUNT_ID}/ai/v1/chat/completions\`,
295
+ {
296
+ method: 'POST',
297
+ headers: {
298
+ Authorization: \`Bearer \${process.env.CF_API_TOKEN}\`,
299
+ 'Content-Type': 'application/json',
300
+ },
301
+ body: JSON.stringify({
302
+ model: '@cf/meta/llama-3.1-8b-instruct',
303
+ messages,
304
+ stream: true,
305
+ }),
306
+ },
307
+ );
308
+
309
+ // Workers AI returns OpenAI-format SSE — pass it straight through.
310
+ return new Response(upstream.body, { headers: { 'Content-Type': 'text/event-stream' } });
311
+ }`,
312
+ worker: `// Worker handler — env.AI is bound in wrangler.toml
313
+ // env.AI.run emits Cloudflare-native SSE (data: {"response":"<token>"}).
314
+ // The TransformStream below re-frames each chunk to OpenAI-format SSE so
315
+ // kai-chat's reader works without any client-side changes.
316
+ export default {
317
+ async fetch(req: Request, env: Env): Promise<Response> {
318
+ const { messages } = await req.json();
319
+
320
+ const nativeStream = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
321
+ messages,
322
+ stream: true,
323
+ });
324
+
325
+ // Re-frame Cloudflare-native SSE → OpenAI-format SSE
326
+ const { readable, writable } = new TransformStream();
327
+ const writer = writable.getWriter();
328
+ const encoder = new TextEncoder();
329
+ const decoder = new TextDecoder();
330
+
331
+ (async () => {
332
+ const reader = (nativeStream as ReadableStream<Uint8Array>).getReader();
333
+ let buffer = '';
334
+ while (true) {
335
+ const { value, done } = await reader.read();
336
+ if (done) break;
337
+ buffer += decoder.decode(value, { stream: true });
338
+ const lines = buffer.split('\\n');
339
+ buffer = lines.pop() ?? '';
340
+ for (const line of lines) {
341
+ const s = line.trim();
342
+ if (!s.startsWith('data:')) continue;
343
+ const payload = s.slice(5).trim();
344
+ if (payload === '[DONE]') continue;
345
+ try {
346
+ const { response } = JSON.parse(payload) as { response?: string };
347
+ if (response == null) continue;
348
+ const openaiChunk = JSON.stringify({ choices: [{ delta: { content: response } }] });
349
+ await writer.write(encoder.encode(\`data: \${openaiChunk}\\n\\n\`));
350
+ } catch { /* skip malformed lines */ }
351
+ }
352
+ }
353
+ await writer.write(encoder.encode('data: [DONE]\\n\\n'));
354
+ await writer.close();
355
+ })();
356
+
357
+ return new Response(readable, { headers: { 'Content-Type': 'text/event-stream' } });
358
+ },
359
+ };`
360
+ },
361
+ streamMapping: `Workers AI via the OpenAI-compatible HTTP endpoint returns OpenAI-format SSE — pipe upstream.body straight to the browser; kai-chat's reader handles it. The native env.AI binding streams Cloudflare's own format (data: {"response":"...token..."}); the worker route template re-frames these chunks to OpenAI-format SSE via a TransformStream before returning.`,
362
+ runNote: 'Set CF_ACCOUNT_ID and CF_API_TOKEN. Model ids are prefixed with @cf/, e.g. @cf/meta/llama-3.1-8b-instruct. For the AI binding (worker key), add an [ai] block with binding = "AI" in wrangler.toml.',
363
+ docsSlug: "integrations/cloudflare-ai"
364
+ };
365
+ const ollama = {
366
+ id: "ollama",
367
+ title: "Ollama",
368
+ category: "provider",
369
+ language: "ts",
370
+ streamFormat: "openai-sse",
371
+ envVars: [],
372
+ routeTemplates: {
373
+ next: `// app/api/chat/route.ts — proxy the browser to local Ollama
374
+ export async function POST(req: Request) {
375
+ const { messages } = await req.json();
376
+
377
+ const upstream = await fetch('http://localhost:11434/v1/chat/completions', {
378
+ method: 'POST',
379
+ headers: { 'Content-Type': 'application/json' },
380
+ body: JSON.stringify({ model: 'llama3.2', messages, stream: true }),
381
+ });
382
+
383
+ // Ollama returns OpenAI-format SSE — stream it straight to the browser.
384
+ return new Response(upstream.body, { headers: { 'Content-Type': 'text/event-stream' } });
385
+ }`,
386
+ html: `<kai-chat id="chat"></kai-chat>
387
+
388
+ <script type="module">
389
+ import '@kitn.ai/ui/elements';
390
+ const chat = document.getElementById('chat');
391
+
392
+ chat.addEventListener('kai-submit', async (e) => {
393
+ const history = [...chat.messages, { id: crypto.randomUUID(), role: 'user', content: e.detail.value }];
394
+ chat.messages = [...history, { id: crypto.randomUUID(), role: 'assistant', content: '' }];
395
+ await fetch('/api/chat', {
396
+ method: 'POST',
397
+ headers: { 'Content-Type': 'application/json' },
398
+ body: JSON.stringify({ messages: history }),
399
+ });
400
+ // stream the reply into the last message — see the Streaming recipe
401
+ });
402
+ <\/script>`
403
+ },
404
+ streamMapping: "Ollama's OpenAI-compatible endpoint (http://localhost:11434/v1/chat/completions) returns OpenAI-format SSE — pipe upstream.body straight to the browser; kai-chat's reader handles it. No API key needed; pass any string if a client requires one (Ollama ignores it).",
405
+ runNote: "No API key required. Run: ollama serve (starts on 127.0.0.1:11434), then ollama pull <model>. For browser-direct access, set OLLAMA_ORIGINS to allow the page origin; restart Ollama after any env change.",
406
+ docsSlug: "integrations/ollama"
407
+ };
408
+ const mastra = {
409
+ id: "mastra",
410
+ title: "Mastra",
411
+ category: "harness",
412
+ language: "ts",
413
+ streamFormat: "openai-sse",
414
+ envVars: ["MASTRA_URL"],
415
+ routeTemplates: {
416
+ express: `// POST /api/chat — your server proxies a Mastra agent to the browser
417
+ import { MastraClient } from '@mastra/client-js';
418
+
419
+ const mastra = new MastraClient({ baseUrl: process.env.MASTRA_URL });
420
+
421
+ const stream = await mastra.getAgent('supportAgent').stream({ messages });
422
+ for await (const delta of stream.textStream) {
423
+ res.write(\`data: \${JSON.stringify({ choices: [{ delta: { content: delta } }] })}\\n\\n\`);
424
+ }
425
+ res.write('data: [DONE]\\n\\n');`
426
+ },
427
+ streamMapping: "Mastra agents speak Vercel AI SDK v5 and expose stream.textStream (async iterable of string deltas). Iterate textStream and emit data: {choices:[{delta:{content}}]} frames; close with data: [DONE]. kai-chat's SSE reader handles it. For tool calls and reasoning, convert the agent to a UI message stream with @mastra/ai-sdk.",
428
+ runNote: "Set MASTRA_URL to your Mastra server base URL (mastra dev exposes POST /api/agents/:agentId/stream on port 4111). Install @mastra/client-js.",
429
+ docsSlug: "integrations/harnesses"
430
+ };
431
+ const pi = {
432
+ id: "pi",
433
+ title: "Pi",
434
+ category: "harness",
435
+ language: "ts",
436
+ streamFormat: "native",
437
+ envVars: [],
438
+ routeTemplates: {
439
+ express: `import { spawn } from 'node:child_process';
440
+
441
+ // POST /api/chat — bridge a Pi RPC session to the browser as SSE
442
+ const pi = spawn('pi', ['--mode', 'rpc', '--no-session']);
443
+
444
+ // Send the user's turn. Pi commands are { type, message }.
445
+ pi.stdin.write(JSON.stringify({ type: 'prompt', message: prompt }) + '\\n');
446
+
447
+ let buffer = '';
448
+ pi.stdout.on('data', (chunk) => {
449
+ buffer += chunk.toString();
450
+ const lines = buffer.split('\\n');
451
+ buffer = lines.pop(); // hold the partial line for the next chunk
452
+ for (const line of lines) {
453
+ if (!line) continue;
454
+ const event = JSON.parse(line);
455
+ const part = event.assistantMessageEvent;
456
+ if (event.type === 'message_update' && part?.type === 'text_delta') {
457
+ res.write(\`data: \${JSON.stringify({ choices: [{ delta: { content: part.delta } }] })}\\n\\n\`);
458
+ }
459
+ }
460
+ });
461
+ pi.on('close', () => { res.write('data: [DONE]\\n\\n'); res.end(); });`
462
+ },
463
+ streamMapping: "Pi runs as a local stdio process in RPC mode (pi --mode rpc --no-session). It emits newline-delimited JSON events on stdout. Map message_update events where assistantMessageEvent.type === 'text_delta' to data: {choices:[{delta:{content:part.delta}}]} SSE frames; send data: [DONE] on close. Split stdout on \\n (not readline, which breaks on Unicode separators U+2028/U+2029). Pi also emits thinking_delta (map to reasoning) and toolcall_* events (map to tool calls).",
464
+ runNote: "Pi must be installed locally and available on PATH as 'pi'. No API key is required by the bridge itself; Pi uses its own credentials. Pi runs with full user permissions — sandbox before exposing to a public endpoint. See the RPC reference: https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/rpc.md",
465
+ docsSlug: "integrations/harnesses"
466
+ };
467
+ const pydanticAi = {
468
+ id: "pydantic-ai",
469
+ title: "Pydantic AI",
470
+ category: "framework",
471
+ language: "python",
472
+ streamFormat: "openai-sse",
473
+ envVars: ["OPENAI_API_KEY"],
474
+ routeTemplates: {
475
+ fastapi: `# main.py
476
+ import json
477
+ from fastapi import FastAPI
478
+ from fastapi.middleware.cors import CORSMiddleware
479
+ from fastapi.responses import StreamingResponse
480
+ from pydantic import BaseModel
481
+ from pydantic_ai import Agent
482
+
483
+ agent = Agent('openai:gpt-4o')
484
+
485
+ app = FastAPI()
486
+ app.add_middleware(
487
+ CORSMiddleware, allow_origins=['*'], allow_methods=['*'], allow_headers=['*']
488
+ )
489
+
490
+ class Message(BaseModel):
491
+ role: str
492
+ content: str
493
+
494
+ class ChatRequest(BaseModel):
495
+ messages: list[Message]
496
+
497
+ async def openai_sse(messages: list[Message]):
498
+ prompt = messages[-1].content if messages else ''
499
+ async with agent.run_stream(prompt) as result:
500
+ async for delta in result.stream_text(delta=True):
501
+ chunk = {'choices': [{'delta': {'content': delta}}]}
502
+ yield f'data: {json.dumps(chunk)}\\n\\n'
503
+ yield 'data: [DONE]\\n\\n'
504
+
505
+ @app.post('/api/chat')
506
+ async def chat(req: ChatRequest):
507
+ return StreamingResponse(openai_sse(req.messages), media_type='text/event-stream')`
508
+ },
509
+ streamMapping: "Pydantic AI's agent.run_stream() yields text deltas via result.stream_text(delta=True). Each delta is re-framed as a data: {choices:[{delta:{content}}]} SSE line and the stream closes with data: [DONE]. The kai-chat reader is unchanged — it sees standard OpenAI-format SSE.",
510
+ runNote: "Install: pip install pydantic-ai fastapi uvicorn. Set OPENAI_API_KEY. Run: uvicorn main:app --reload (default port 8000). Point kai-chat at http://localhost:8000/api/chat.",
511
+ docsSlug: "integrations/pydantic-ai"
512
+ };
513
+ const mock = {
514
+ id: "mock",
515
+ title: "Mock (local preview)",
516
+ category: "mock",
517
+ language: "ts",
518
+ streamFormat: "native",
519
+ envVars: [],
520
+ routeTemplates: {},
521
+ streamMapping: "No backend. onSubmit streams a canned assistant reply client-side, one token at a time, reassigning messages (new array/object reference) per chunk so kai-chat re-renders. Swap integration for a real provider when ready.",
522
+ runNote: "No backend or API key needed — replies stream locally for preview. Run the front-end as-is; swap `integration` for a real provider (e.g. openrouter, ollama) when ready.",
523
+ docsSlug: "integrations/mock"
524
+ };
525
+ const archetypes$1 = [
526
+ {
527
+ id: "drop-in-chat",
528
+ title: "Drop-in chat",
529
+ components: ["kai-chat"],
530
+ defaultPlacement: "full-page",
531
+ docsSlug: "examples/drop-in-chat"
532
+ },
533
+ {
534
+ id: "support-widget",
535
+ title: "Support widget",
536
+ components: ["kai-chat"],
537
+ defaultPlacement: "docked-widget",
538
+ docsSlug: "examples/support-widget"
539
+ },
540
+ {
541
+ id: "knowledge-base",
542
+ title: "Knowledge base / RAG",
543
+ components: ["kai-chat", "kai-sources"],
544
+ defaultPlacement: "full-page",
545
+ docsSlug: "examples/knowledge-base"
546
+ },
547
+ {
548
+ id: "agentic",
549
+ title: "Agentic assistant",
550
+ components: ["kai-chat", "kai-tool", "kai-reasoning"],
551
+ defaultPlacement: "side",
552
+ docsSlug: "examples/agentic-assistant"
553
+ },
554
+ {
555
+ id: "workspace",
556
+ title: "Agentic workspace",
557
+ components: ["kai-chat", "kai-artifact", "kai-resizable"],
558
+ defaultPlacement: "side",
559
+ docsSlug: "examples/workspace"
560
+ },
561
+ {
562
+ id: "voice",
563
+ title: "Voice assistant",
564
+ components: ["kai-chat", "kai-voice-input"],
565
+ defaultPlacement: "full-page",
566
+ docsSlug: "examples/voice-assistant"
567
+ }
568
+ ];
569
+ const integrations = [
570
+ openrouter,
571
+ vercelAiSdk,
572
+ langgraph,
573
+ cloudflare,
574
+ ollama,
575
+ mastra,
576
+ pi,
577
+ pydanticAi,
578
+ mock
579
+ ];
580
+ const archetypes = archetypes$1;
581
+ function getIntegration(id) {
582
+ return integrations.find((i) => i.id === id);
583
+ }
584
+ function getArchetype(id) {
585
+ return archetypes.find((a) => a.id === id);
586
+ }
587
+ function listIntegrations() {
588
+ return integrations;
589
+ }
590
+ function listArchetypes() {
591
+ return archetypes;
592
+ }
593
+ const text = (s) => ({
594
+ content: [{ type: "text", text: s }]
595
+ });
596
+ const FLEX_FILL = "flex: 1; min-height: 0;";
597
+ function placementStyle(placement) {
598
+ switch (placement) {
599
+ case "full-page":
600
+ return {
601
+ style: "height: 100dvh; width: 100%; display: flex; flex-direction: column;",
602
+ chatFill: FLEX_FILL,
603
+ note: "fills the viewport (100dvh)"
604
+ };
605
+ case "inline":
606
+ return {
607
+ style: "width: 100%; max-width: 720px; height: 540px; margin: 0 auto; display: flex; flex-direction: column;",
608
+ chatFill: FLEX_FILL,
609
+ note: "in-flow block (sized by parent, not fixed)"
610
+ };
611
+ case "side":
612
+ return {
613
+ style: "position: fixed; top: 0; inset-inline-end: 0; height: 100dvh; width: 380px; border-inline-start: 1px solid var(--kai-color-border); display: flex; flex-direction: column; z-index: 1000;",
614
+ chatFill: FLEX_FILL,
615
+ note: "full-height side panel, docked to the trailing edge (100dvh)",
616
+ altNote: "In-flow alternative (push content instead of overlay): drop `position`/`z-index` and make this a `flex: 0 0 380px` column inside a `display: flex` row at `height: 100dvh`."
617
+ };
618
+ case "docked-widget":
619
+ return {
620
+ style: "position: fixed; bottom: 1.5rem; inset-inline-end: 1.5rem; width: 380px; height: 600px; max-height: calc(100dvh - 3rem); border-radius: 16px; overflow: hidden; box-shadow: 0 12px 32px var(--kai-shadow-color, rgba(0,0,0,0.18)); display: flex; flex-direction: column; z-index: 1000;",
621
+ chatFill: FLEX_FILL,
622
+ note: "fixed, floating bottom-right widget"
623
+ };
624
+ default:
625
+ return {
626
+ style: "height: 100dvh; width: 100%; display: flex; flex-direction: column;",
627
+ chatFill: FLEX_FILL,
628
+ note: "fills the viewport (100dvh)"
629
+ };
630
+ }
631
+ }
632
+ const DEFAULT_SUGGESTIONS = ["What's new?", "How can you help?"];
633
+ const MOCK_REPLY = "Hi! I'm a local preview — no backend or API key needed. Swap `integration` for a real provider (openrouter, ollama, …) and I'll talk to a real model.";
634
+ function jsArray(items) {
635
+ return "[" + items.map((s) => JSON.stringify(s)).join(", ") + "]";
636
+ }
637
+ function mockStreamBody(opts) {
638
+ const { pad, read, commitInitial, commitMap, setLoading, strictRoles = false } = opts;
639
+ const asConst = strictRoles ? " as const" : "";
640
+ const mapBody = `(m.id === assistantId ? { ...m, content: answer } : m)`;
641
+ return [
642
+ `${pad}const value = e.detail.value.trim();`,
643
+ `${pad}if (!value) return;`,
644
+ `${pad}const history = [...${read}, { id: crypto.randomUUID(), role: 'user'${asConst}, content: value }];`,
645
+ `${pad}const assistantId = crypto.randomUUID();`,
646
+ `${pad}${commitInitial(`[...history, { id: assistantId, role: 'assistant'${asConst}, content: '' }]`)}`,
647
+ `${pad}${setLoading("true")}`,
648
+ `${pad}// No backend: stream a canned reply client-side, one token at a time.`,
649
+ `${pad}const reply = ${JSON.stringify(MOCK_REPLY)};`,
650
+ `${pad}const tokens = reply.split(/(\\s+)/);`,
651
+ `${pad}let answer = '';`,
652
+ `${pad}for (const tok of tokens) {`,
653
+ `${pad} await new Promise((r) => setTimeout(r, 24));`,
654
+ `${pad} answer += tok;`,
655
+ `${pad} // new array + object reference per chunk so kai-chat re-renders`,
656
+ `${pad} ${commitMap(mapBody)}`,
657
+ `${pad}}`,
658
+ `${pad}${setLoading("false")}`
659
+ ].join("\n");
660
+ }
661
+ function defaultModelFor(integration) {
662
+ const routeSrc = Object.values(integration.routeTemplates).join("\n");
663
+ if (!routeSrc.includes("model")) return void 0;
664
+ const defaults = {
665
+ openrouter: "openai/gpt-4o-mini",
666
+ ollama: "llama3.2",
667
+ "vercel-ai-sdk": "openai/gpt-4o-mini",
668
+ cloudflare: "openai/gpt-4o-mini"
669
+ };
670
+ return defaults[integration.id] ?? "openai/gpt-4o-mini";
671
+ }
672
+ const MESSAGE_EMBEDDED_TAGS = /* @__PURE__ */ new Set(["kai-tool", "kai-reasoning"]);
673
+ const WORKSPACE_STRUCTURAL_TAGS = /* @__PURE__ */ new Set(["kai-resizable", "kai-artifact"]);
674
+ function isWorkspace(archetype) {
675
+ return archetype.components.includes("kai-resizable") && archetype.components.includes("kai-artifact");
676
+ }
677
+ const SAMPLE_AGENTIC_MESSAGE = {
678
+ id: "sample-assistant",
679
+ role: "assistant",
680
+ content: "Searched the web for current pricing.",
681
+ reasoning: { text: "I should call the search tool to get up-to-date data." },
682
+ tools: [
683
+ {
684
+ type: "search",
685
+ state: "output-available",
686
+ input: { query: "current pricing" },
687
+ output: { results: ["Result A", "Result B"] },
688
+ toolCallId: "tc_001"
689
+ }
690
+ ]
691
+ };
692
+ function componentTags(archetype, chatFill) {
693
+ if (isWorkspace(archetype)) {
694
+ return [
695
+ ` <!-- SCAF-14: workspace split — chat pane left, artifact preview right. -->`,
696
+ ` <!-- kai-resizable needs kai-resizable-item children to render panels. -->`,
697
+ ` <kai-resizable orientation="horizontal" style="display:block;width:100%;height:100%">`,
698
+ ` <kai-resizable-item size="40%" min="240px">`,
699
+ ` <kai-chat id="chat" suggestion-mode="submit" style="${chatFill}"></kai-chat>`,
700
+ ` </kai-resizable-item>`,
701
+ ` <kai-resizable-item min="280px">`,
702
+ ` <!-- Replace src with your artifact URL or set .files for multi-file preview. -->`,
703
+ ` <kai-artifact id="artifact" src="https://example.com" style="width:100%;height:100%"></kai-artifact>`,
704
+ ` </kai-resizable-item>`,
705
+ ` </kai-resizable>`
706
+ ].join("\n");
707
+ }
708
+ const companionTags = archetype.components.filter(
709
+ (t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t)
710
+ );
711
+ const hasEmbedded = archetype.components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
712
+ const hasStandaloneCompanions = companionTags.length > 0;
713
+ const lines = [];
714
+ lines.push(` <kai-chat id="chat" suggestion-mode="submit" style="${chatFill}"></kai-chat>`);
715
+ if (hasEmbedded) {
716
+ lines.push(
717
+ ` <!-- kai-tool / kai-reasoning render INSIDE the thread, not as siblings.`,
718
+ ` Seed messages with { tools: [...], reasoning: { text: '...' } } — see the sample in the script below. -->`
719
+ );
720
+ }
721
+ for (const tag of companionTags) {
722
+ if (tag === "kai-sources") {
723
+ lines.push(
724
+ ` <!-- Replace sampleSources with your data. -->`,
725
+ ` <kai-sources id="sources"></kai-sources>`
726
+ );
727
+ } else {
728
+ lines.push(` <${tag}></${tag}>`);
729
+ }
730
+ }
731
+ if (hasStandaloneCompanions) {
732
+ lines.push(` <!-- wire data props — see the component_reference MCP tool -->`);
733
+ }
734
+ return lines.join("\n");
735
+ }
736
+ function htmlWiring(ctx, archetype) {
737
+ const hasEmbedded = archetype.components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
738
+ const hasSources = archetype.components.includes("kai-sources");
739
+ const seedLines = hasEmbedded ? [
740
+ ` // SCAF-9: tool calls + reasoning render INSIDE the thread — set them on the message object.`,
741
+ ` // Replace this sample with real messages from your backend.`,
742
+ ` chat.messages = [${JSON.stringify(SAMPLE_AGENTIC_MESSAGE, null, 0)}];`,
743
+ ``
744
+ ] : [];
745
+ const sourcesSetupLines = hasSources ? [
746
+ ` const sourcesEl = document.getElementById('sources');`,
747
+ ` // Replace with your real source data (set as a JS property — it's an array).`,
748
+ ` const sampleSources = [`,
749
+ ` { href: 'https://example.com/doc1', title: 'Getting started', description: 'Overview of the product.' },`,
750
+ ` { href: 'https://example.com/doc2', title: 'API reference', description: 'Full API documentation.' },`,
751
+ ` ];`,
752
+ ` sourcesEl.sources = sampleSources;`,
753
+ ``
754
+ ] : [];
755
+ const head = [
756
+ ` <script type="module">`,
757
+ ` import '@kitn.ai/ui/elements'; // registers <kai-*> — required, must come first`,
758
+ ` import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
759
+ ``,
760
+ ` // Guard: module scripts run before the DOM is ready when inlined in <head>.`,
761
+ ` // DOMContentLoaded fires synchronously when already loaded; otherwise waits.`,
762
+ ` async function init() {`,
763
+ ` const chat = document.getElementById('chat');`,
764
+ ` // SCAF-15: kai-* register via an async dynamic import (SSR-safety), so the`,
765
+ ` // element may not be upgraded yet. Wait for the upgrade before setting any`,
766
+ ` // array/object property — values set pre-upgrade are dropped on upgrade.`,
767
+ ` await customElements.whenDefined('kai-chat');`,
768
+ ` // suggestions is a JS PROPERTY (arrays can't be HTML attributes)`,
769
+ ` chat.suggestions = ${jsArray(ctx.suggestions)};`,
770
+ ` chat.suggestionMode = 'submit';`,
771
+ ``,
772
+ ...seedLines.map((l) => l.trim() === "" ? l : ` ${l}`),
773
+ ...sourcesSetupLines.map((l) => l.trim() === "" ? l : ` ${l}`)
774
+ ];
775
+ const domReadyFooter = [
776
+ ` }`,
777
+ ` if (document.readyState === 'loading') {`,
778
+ ` document.addEventListener('DOMContentLoaded', init);`,
779
+ ` } else {`,
780
+ ` init();`,
781
+ ` }`
782
+ ];
783
+ if (ctx.isMock) {
784
+ const body = mockStreamBody({
785
+ pad: " ",
786
+ read: "chat.messages",
787
+ commitInitial: (expr) => `chat.messages = ${expr};`,
788
+ // chat.messages is live (no React snapshot) — map over it directly
789
+ commitMap: (mapBody) => `chat.messages = chat.messages.map((m) => ${mapBody});`,
790
+ setLoading: (v) => `chat.loading = ${v};`
791
+ });
792
+ return [
793
+ ...head,
794
+ ` // No backend: stream a canned reply client-side (no fetch, no API key).`,
795
+ ` chat.addEventListener('kai-submit', async (e) => {`,
796
+ body,
797
+ ` });`,
798
+ ...domReadyFooter,
799
+ ` <\/script>`
800
+ ].join("\n");
801
+ }
802
+ const modelLines = ctx.defaultModel ? [
803
+ ` // SCAF-8: change this model id to any provider/model string you want to use.`,
804
+ ` const model = '${ctx.defaultModel}';`,
805
+ ``
806
+ ] : [];
807
+ const bodyPayload = ctx.defaultModel ? `{ model, messages: history.map((m) => ({ role: m.role, content: m.content })) }` : `{ messages: history.map((m) => ({ role: m.role, content: m.content })) }`;
808
+ return [
809
+ ...head,
810
+ ` chat.addEventListener('kai-submit', async (e) => {`,
811
+ ` const value = e.detail.value.trim();`,
812
+ ` if (!value) return;`,
813
+ ``,
814
+ ...modelLines,
815
+ ` // messages is a JS PROPERTY (objects can't be HTML attributes)`,
816
+ ` const history = [...chat.messages, { id: crypto.randomUUID(), role: 'user', content: value }];`,
817
+ ` const assistantId = crypto.randomUUID();`,
818
+ ` chat.messages = [...history, { id: assistantId, role: 'assistant', content: '' }];`,
819
+ ` chat.loading = true;`,
820
+ ``,
821
+ ` const res = await fetch('/api/chat', {`,
822
+ ` method: 'POST',`,
823
+ ` headers: { 'Content-Type': 'application/json' },`,
824
+ ` body: JSON.stringify(${bodyPayload}),`,
825
+ ` });`,
826
+ ``,
827
+ ` // Read the OpenAI-format SSE and stream it into the assistant message.`,
828
+ ` // This loop is the Streaming recipe — copy its exact body if you need keep-alive handling.`,
829
+ ` const reader = res.body.getReader();`,
830
+ ` const decoder = new TextDecoder();`,
831
+ ` let buffer = '', answer = '';`,
832
+ ` while (true) {`,
833
+ ` const { value: chunk, done } = await reader.read();`,
834
+ ` if (done) break;`,
835
+ ` buffer += decoder.decode(chunk, { stream: true });`,
836
+ ` const lines = buffer.split('\\n');`,
837
+ ` buffer = lines.pop();`,
838
+ ` for (const line of lines) {`,
839
+ ` const s = line.trim();`,
840
+ ` if (!s.startsWith('data:')) continue;`,
841
+ ` const payload = s.slice(5).trim();`,
842
+ ` if (payload === '[DONE]') continue;`,
843
+ ` try {`,
844
+ ` const delta = JSON.parse(payload).choices?.[0]?.delta?.content;`,
845
+ ` if (!delta) continue;`,
846
+ ` answer += delta;`,
847
+ ` chat.messages = chat.messages.map((m) => (m.id === assistantId ? { ...m, content: answer } : m));`,
848
+ ` } catch { /* skip keep-alive lines */ }`,
849
+ ` }`,
850
+ ` }`,
851
+ ` chat.loading = false;`,
852
+ ` });`,
853
+ ...domReadyFooter,
854
+ ` <\/script>`
855
+ ].join("\n");
856
+ }
857
+ function renderHtml(archetype, ctx) {
858
+ const { p, emptyHint } = ctx;
859
+ return [
860
+ `<!-- ${archetype.title} — ${p.note} -->`,
861
+ ...p.altNote ? [`<!-- ${p.altNote} -->`] : [],
862
+ `<div style="${p.style}">`,
863
+ componentTags(archetype, p.chatFill),
864
+ `</div>`,
865
+ ``,
866
+ htmlWiring(ctx, archetype),
867
+ ``,
868
+ ` <!-- empty-state hint: ${emptyHint} -->`
869
+ ].join("\n");
870
+ }
871
+ function toPascalCase(tag) {
872
+ return tag.replace(/^kai-/, "").split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
873
+ }
874
+ function renderJsx(archetype, ctx, framework) {
875
+ const { p, emptyHint, suggestions, isMock, defaultModel } = ctx;
876
+ const hasEmbedded = archetype.components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
877
+ const workspace = isWorkspace(archetype);
878
+ const renderableTags = archetype.components.filter((t) => !MESSAGE_EMBEDDED_TAGS.has(t));
879
+ const importTags = workspace ? [.../* @__PURE__ */ new Set([...renderableTags.filter((t) => t !== "kai-resizable"), "kai-resizable", "kai-resizable-item"])] : renderableTags;
880
+ const wrapperNames = importTags.map(toPascalCase);
881
+ const importList = wrapperNames.join(", ");
882
+ const standaloneCompanionTags = archetype.components.filter(
883
+ (t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t)
884
+ );
885
+ const companionJsxLines = [];
886
+ if (hasEmbedded) {
887
+ companionJsxLines.push(
888
+ ` {/* kai-tool / kai-reasoning render inside the thread. Tool calls + reasoning`,
889
+ ` are set on each message object — see the sampleMessages initializer above. */}`
890
+ );
891
+ }
892
+ for (const t of standaloneCompanionTags) {
893
+ if (t === "kai-sources") {
894
+ companionJsxLines.push(
895
+ ` {/* Replace sampleSources with your real data. */}`,
896
+ ` <Sources sources={sampleSources} />`
897
+ );
898
+ } else {
899
+ companionJsxLines.push(` {/* wire data props — see the component_reference MCP tool */}`);
900
+ companionJsxLines.push(` <${toPascalCase(t)} />`);
901
+ }
902
+ }
903
+ const companions = companionJsxLines.join("\n");
904
+ const chatMessageType = `type ChatMessage = { id: string; role: 'user' | 'assistant'; content: string; reasoning?: { text: string; label?: string }; tools?: { type: string; state: 'input-streaming' | 'input-available' | 'output-available' | 'output-error'; input?: Record<string, unknown>; output?: Record<string, unknown>; toolCallId?: string }[] };`;
905
+ const sampleMessagesInit = hasEmbedded ? [
906
+ ` // SCAF-9: tool calls and reasoning render inside the thread — set them on the message object.`,
907
+ ` // Replace with real messages streamed from your backend.`,
908
+ ` const sampleMessages: ChatMessage[] = [${JSON.stringify(SAMPLE_AGENTIC_MESSAGE)}];`,
909
+ ` const [messages, setMessages] = useState<ChatMessage[]>(sampleMessages);`
910
+ ].join("\n") : ` const [messages, setMessages] = useState<ChatMessage[]>([]);`;
911
+ const sampleSourcesInit = standaloneCompanionTags.includes("kai-sources") ? [
912
+ ` // Replace sampleSources with your real source data.`,
913
+ ` const sampleSources = [`,
914
+ ` { href: 'https://example.com/doc1', title: 'Getting started', description: 'Overview of the product.' },`,
915
+ ` { href: 'https://example.com/doc2', title: 'API reference', description: 'Full API documentation.' },`,
916
+ ` ];`
917
+ ].join("\n") : "";
918
+ const modelInit = defaultModel ? ` // SCAF-8: change this to any provider/model string you want to use.
919
+ const model = '${defaultModel}';` : "";
920
+ const bodyPayload = defaultModel ? `{ model, messages: history.map((m) => ({ role: m.role, content: m.content })) }` : `{ messages: history.map((m) => ({ role: m.role, content: m.content })) }`;
921
+ const onSubmitBody = isMock ? mockStreamBody({
922
+ pad: " ",
923
+ read: "messages",
924
+ commitInitial: (expr) => `setMessages(${expr});`,
925
+ // functional updater so each token maps over the LATEST array, not the snapshot
926
+ commitMap: (mapBody) => `setMessages((prev) => prev.map((m) => ${mapBody}));`,
927
+ setLoading: (v) => `setLoading(${v});`,
928
+ strictRoles: true
929
+ }) : [
930
+ ` const value = e.detail.value.trim();`,
931
+ ` if (!value) return;`,
932
+ ` const history: ChatMessage[] = [...messages, { id: crypto.randomUUID(), role: 'user' as const, content: value }];`,
933
+ ` const assistantId = crypto.randomUUID();`,
934
+ ` setMessages([...history, { id: assistantId, role: 'assistant' as const, content: '' }]);`,
935
+ ` setLoading(true);`,
936
+ ` const res = await fetch('/api/chat', {`,
937
+ ` method: 'POST',`,
938
+ ` headers: { 'Content-Type': 'application/json' },`,
939
+ ` body: JSON.stringify(${bodyPayload}),`,
940
+ ` });`,
941
+ ` // Stream the OpenAI-format SSE into the assistant message — see the Streaming recipe.`,
942
+ ` const reader = res.body!.getReader();`,
943
+ ` const decoder = new TextDecoder();`,
944
+ ` let buffer = '', answer = '';`,
945
+ ` while (true) {`,
946
+ ` const { value: chunk, done } = await reader.read();`,
947
+ ` if (done) break;`,
948
+ ` buffer += decoder.decode(chunk, { stream: true });`,
949
+ ` const lines = buffer.split('\\n');`,
950
+ ` buffer = lines.pop()!;`,
951
+ ` for (const line of lines) {`,
952
+ ` const s = line.trim();`,
953
+ ` if (!s.startsWith('data:')) continue;`,
954
+ ` const payload = s.slice(5).trim();`,
955
+ ` if (payload === '[DONE]') continue;`,
956
+ ` try {`,
957
+ ` const delta = JSON.parse(payload).choices?.[0]?.delta?.content;`,
958
+ ` if (!delta) continue;`,
959
+ ` answer += delta;`,
960
+ ` setMessages((ms) => ms.map((m) => (m.id === assistantId ? { ...m, content: answer } : m)));`,
961
+ ` } catch { /* skip keep-alives */ }`,
962
+ ` }`,
963
+ ` }`,
964
+ ` setLoading(false);`
965
+ ].join("\n");
966
+ const useClientDirective = framework === "next" ? [`'use client';`, ``] : [];
967
+ if (framework === "next") {
968
+ const dynamicImports = wrapperNames.map(
969
+ (name) => `const ${name} = dynamic(() => import('@kitn.ai/ui/react').then((m) => m.${name}), { ssr: false });`
970
+ );
971
+ const nextConfigNote2 = [];
972
+ return [
973
+ // 'use client' must be the very first line for Next.js App Router.
974
+ ...useClientDirective,
975
+ `import { useState } from 'react';`,
976
+ `import dynamic from 'next/dynamic';`,
977
+ `import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
978
+ `// kai-* bundle Solid's client runtime → load client-only so SSR/prerender doesn't crash`,
979
+ ...dynamicImports,
980
+ ``,
981
+ ...nextConfigNote2,
982
+ `// ${archetype.title} — ${p.note}. empty-state hint: ${emptyHint}`,
983
+ ...p.altNote ? [`// ${p.altNote}`] : [],
984
+ chatMessageType,
985
+ ``,
986
+ `export default function App() {`,
987
+ sampleMessagesInit,
988
+ ` const [loading, setLoading] = useState(false);`,
989
+ ` const suggestions = ${jsArray(suggestions)};`,
990
+ ...sampleSourcesInit ? [sampleSourcesInit] : [],
991
+ ...modelInit ? [modelInit] : [],
992
+ ``,
993
+ ` async function onSubmit(e: CustomEvent<{ value: string }>) {`,
994
+ onSubmitBody,
995
+ ` }`,
996
+ ``,
997
+ ` return (`,
998
+ ` <div style={{ ${jsxStyle(p.style)} }}>`,
999
+ ...workspace ? [
1000
+ ` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
1001
+ ` {/* Resizable needs ResizableItem children to render panels. */}`,
1002
+ ` <Resizable orientation="horizontal" style={{ display: 'block', width: '100%', height: '100%' }}>`,
1003
+ ` <ResizableItem size="40%" min="240px">`,
1004
+ ` <Chat`,
1005
+ ` messages={messages}`,
1006
+ ` loading={loading}`,
1007
+ ` suggestions={suggestions}`,
1008
+ ` suggestionMode="submit"`,
1009
+ ` onSubmit={onSubmit}`,
1010
+ ` style={{ ${jsxStyle(p.chatFill)} }}`,
1011
+ ` />`,
1012
+ ` </ResizableItem>`,
1013
+ ` <ResizableItem min="280px">`,
1014
+ ` {/* Replace src with your artifact URL or set files for multi-file preview. */}`,
1015
+ ` <Artifact src="https://example.com" style={{ width: '100%', height: '100%' }} />`,
1016
+ ` </ResizableItem>`,
1017
+ ` </Resizable>`
1018
+ ] : [
1019
+ ` <Chat`,
1020
+ ` messages={messages}`,
1021
+ ` loading={loading}`,
1022
+ ` suggestions={suggestions}`,
1023
+ ` suggestionMode="submit"`,
1024
+ ` onSubmit={onSubmit}`,
1025
+ ` style={{ ${jsxStyle(p.chatFill)} }}`,
1026
+ ` />`,
1027
+ companions
1028
+ ],
1029
+ ` </div>`,
1030
+ ` );`,
1031
+ `}`
1032
+ ].filter((l) => l !== "").join("\n");
1033
+ }
1034
+ const nextConfigNote = [];
1035
+ return [
1036
+ // SCAF-2: 'use client' must be the very first line for Next.js App Router.
1037
+ ...useClientDirective,
1038
+ // (1) REQUIRED: registers <kai-*> — the react wrappers do NOT auto-register.
1039
+ // Must come BEFORE importing the wrappers, or <kai-chat> renders empty.
1040
+ `import '@kitn.ai/ui/elements'; // registers <kai-*> — required, must come first`,
1041
+ `import { useState } from 'react';`,
1042
+ `import { ${importList} } from '@kitn.ai/ui/react';`,
1043
+ `import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
1044
+ ``,
1045
+ ...nextConfigNote,
1046
+ `// ${archetype.title} — ${p.note}. empty-state hint: ${emptyHint}`,
1047
+ ...p.altNote ? [`// ${p.altNote}`] : [],
1048
+ chatMessageType,
1049
+ ``,
1050
+ `export default function App() {`,
1051
+ sampleMessagesInit,
1052
+ ` const [loading, setLoading] = useState(false);`,
1053
+ ` const suggestions = ${jsArray(suggestions)};`,
1054
+ ...sampleSourcesInit ? [sampleSourcesInit] : [],
1055
+ ...modelInit ? [modelInit] : [],
1056
+ ``,
1057
+ ` async function onSubmit(e: CustomEvent<{ value: string }>) {`,
1058
+ onSubmitBody,
1059
+ ` }`,
1060
+ ``,
1061
+ ` return (`,
1062
+ ` <div style={{ ${jsxStyle(p.style)} }}>`,
1063
+ ...workspace ? [
1064
+ ` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
1065
+ ` {/* Resizable needs ResizableItem children to render panels. */}`,
1066
+ ` <Resizable orientation="horizontal" style={{ display: 'block', width: '100%', height: '100%' }}>`,
1067
+ ` <ResizableItem size="40%" min="240px">`,
1068
+ ` <Chat`,
1069
+ ` messages={messages}`,
1070
+ ` loading={loading}`,
1071
+ ` suggestions={suggestions}`,
1072
+ ` suggestionMode="submit"`,
1073
+ ` onSubmit={onSubmit}`,
1074
+ ` style={{ ${jsxStyle(p.chatFill)} }}`,
1075
+ ` />`,
1076
+ ` </ResizableItem>`,
1077
+ ` <ResizableItem min="280px">`,
1078
+ ` {/* Replace src with your artifact URL or set files for multi-file preview. */}`,
1079
+ ` <Artifact src="https://example.com" style={{ width: '100%', height: '100%' }} />`,
1080
+ ` </ResizableItem>`,
1081
+ ` </Resizable>`
1082
+ ] : [
1083
+ ` <Chat`,
1084
+ ` messages={messages}`,
1085
+ ` loading={loading}`,
1086
+ ` suggestions={suggestions}`,
1087
+ ` suggestionMode="submit"`,
1088
+ ` onSubmit={onSubmit}`,
1089
+ ` style={{ ${jsxStyle(p.chatFill)} }}`,
1090
+ ` />`,
1091
+ companions
1092
+ ],
1093
+ ` </div>`,
1094
+ ` );`,
1095
+ `}`
1096
+ ].filter((l) => l !== "").join("\n");
1097
+ }
1098
+ function renderVue(archetype, ctx) {
1099
+ const { p, emptyHint, suggestions, isMock, defaultModel } = ctx;
1100
+ const workspace = isWorkspace(archetype);
1101
+ const standaloneCompanionTags = archetype.components.filter(
1102
+ (t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t)
1103
+ );
1104
+ const hasEmbedded = archetype.components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
1105
+ const companionLines = [];
1106
+ if (hasEmbedded) {
1107
+ companionLines.push(
1108
+ ` <!-- kai-tool / kai-reasoning render INSIDE the thread — set tools/reasoning on each message object. -->`
1109
+ );
1110
+ }
1111
+ for (const t of standaloneCompanionTags) {
1112
+ if (t === "kai-sources") {
1113
+ companionLines.push(` <!-- Replace sampleSources with your real data (set as a JS property). -->`);
1114
+ companionLines.push(` <kai-sources ref="sourcesEl" />`);
1115
+ } else {
1116
+ companionLines.push(` <!-- wire data props — see the component_reference MCP tool -->`);
1117
+ companionLines.push(` <${t} />`);
1118
+ }
1119
+ }
1120
+ const companions = companionLines.join("\n");
1121
+ const bodyPayload = defaultModel ? `{ model, messages: history.map((m) => ({ role: m.role, content: m.content })) }` : `{ messages: history.map((m) => ({ role: m.role, content: m.content })) }`;
1122
+ const onSubmitBody = isMock ? mockStreamBody({
1123
+ pad: " ",
1124
+ read: "messages.value",
1125
+ commitInitial: (expr) => `messages.value = ${expr};`,
1126
+ // messages.value is live — map over it directly
1127
+ commitMap: (mapBody) => `messages.value = messages.value.map((m) => ${mapBody});`,
1128
+ setLoading: (v) => `loading.value = ${v};`,
1129
+ strictRoles: true
1130
+ }) : [
1131
+ ` const value = e.detail.value.trim();`,
1132
+ ` if (!value) return;`,
1133
+ ` const history: ChatMessage[] = [...messages.value, { id: crypto.randomUUID(), role: 'user' as const, content: value }];`,
1134
+ ` const assistantId = crypto.randomUUID();`,
1135
+ ` messages.value = [...history, { id: assistantId, role: 'assistant' as const, content: '' }];`,
1136
+ ` loading.value = true;`,
1137
+ ` // POST to /api/chat, then stream the OpenAI-format SSE into the`,
1138
+ ` // assistant message (reassign messages.value per chunk) — see the Streaming recipe.`,
1139
+ ...defaultModel ? [
1140
+ ` // SCAF-8: change this to any provider/model string you want to use.`,
1141
+ ` const model = '${defaultModel}';`
1142
+ ] : [],
1143
+ ` const res = await fetch('/api/chat', {`,
1144
+ ` method: 'POST',`,
1145
+ ` headers: { 'Content-Type': 'application/json' },`,
1146
+ ` body: JSON.stringify(${bodyPayload}),`,
1147
+ ` });`,
1148
+ ` // Stream the OpenAI-format SSE — see the Streaming recipe.`,
1149
+ ` const reader = res.body.getReader();`,
1150
+ ` const decoder = new TextDecoder();`,
1151
+ ` let buffer = '', answer = '';`,
1152
+ ` while (true) {`,
1153
+ ` const { value: chunk, done } = await reader.read();`,
1154
+ ` if (done) break;`,
1155
+ ` buffer += decoder.decode(chunk, { stream: true });`,
1156
+ ` const lines = buffer.split('\\n');`,
1157
+ ` buffer = lines.pop();`,
1158
+ ` for (const line of lines) {`,
1159
+ ` const s = line.trim();`,
1160
+ ` if (!s.startsWith('data:')) continue;`,
1161
+ ` const payload = s.slice(5).trim();`,
1162
+ ` if (payload === '[DONE]') continue;`,
1163
+ ` try {`,
1164
+ ` const delta = JSON.parse(payload).choices?.[0]?.delta?.content;`,
1165
+ ` if (!delta) continue;`,
1166
+ ` answer += delta;`,
1167
+ ` messages.value = messages.value.map((m) => (m.id === assistantId ? { ...m, content: answer } : m));`,
1168
+ ` } catch { /* skip keep-alives */ }`,
1169
+ ` }`,
1170
+ ` }`,
1171
+ ` loading.value = false;`
1172
+ ].join("\n");
1173
+ const chatMessageType = `type ChatMessage = { id: string; role: 'user' | 'assistant'; content: string; reasoning?: { text: string; label?: string }; tools?: { type: string; state: 'input-streaming' | 'input-available' | 'output-available' | 'output-error'; input?: Record<string, unknown>; output?: Record<string, unknown>; toolCallId?: string }[] };`;
1174
+ const sampleSeed = hasEmbedded ? [
1175
+ `// SCAF-9: tool calls + reasoning render inside the thread — set them on the message object.`,
1176
+ `// Replace with real messages streamed from your backend.`,
1177
+ `const messages = ref<ChatMessage[]>([${JSON.stringify(SAMPLE_AGENTIC_MESSAGE)}]);`
1178
+ ] : [`const messages = ref<ChatMessage[]>([]);`];
1179
+ const sourcesSeed = standaloneCompanionTags.includes("kai-sources") ? [
1180
+ `// Replace sampleSources with your real source data.`,
1181
+ `const sampleSources = [`,
1182
+ ` { href: 'https://example.com/doc1', title: 'Getting started', description: 'Overview of the product.' },`,
1183
+ ` { href: 'https://example.com/doc2', title: 'API reference', description: 'Full API documentation.' },`,
1184
+ `];`,
1185
+ `onMounted(() => {`,
1186
+ ` const el = document.querySelector('kai-sources');`,
1187
+ ` if (el) el.sources = sampleSources;`,
1188
+ `});`
1189
+ ] : [];
1190
+ const vueImports = `import { ref, onMounted } from 'vue';`;
1191
+ const workspaceTemplate = workspace ? [
1192
+ ` <!-- SCAF-14: workspace split — chat pane left, artifact preview right. -->`,
1193
+ ` <!-- kai-resizable needs kai-resizable-item children to render panels. -->`,
1194
+ ` <kai-resizable orientation="horizontal" style="display:block;width:100%;height:100%">`,
1195
+ ` <kai-resizable-item size="40%" min="240px">`,
1196
+ ` <kai-chat`,
1197
+ ` :messages.prop="messages"`,
1198
+ ` :loading.prop="loading"`,
1199
+ ` :suggestions.prop="suggestions"`,
1200
+ ` suggestion-mode="submit"`,
1201
+ ` style="${p.chatFill}"`,
1202
+ ` @kai-submit="onSubmit"`,
1203
+ ` ></kai-chat>`,
1204
+ ` </kai-resizable-item>`,
1205
+ ` <kai-resizable-item min="280px">`,
1206
+ ` <!-- Replace src with your artifact URL or set .files for multi-file preview. -->`,
1207
+ ` <kai-artifact src="https://example.com" style="width:100%;height:100%"></kai-artifact>`,
1208
+ ` </kai-resizable-item>`,
1209
+ ` </kai-resizable>`
1210
+ ] : [
1211
+ ` <kai-chat`,
1212
+ ` :messages.prop="messages"`,
1213
+ ` :loading.prop="loading"`,
1214
+ ` :suggestions.prop="suggestions"`,
1215
+ ` suggestion-mode="submit"`,
1216
+ ` style="${p.chatFill}"`,
1217
+ ` @kai-submit="onSubmit"`,
1218
+ ` ></kai-chat>`,
1219
+ companions
1220
+ ];
1221
+ return [
1222
+ `<!-- vue — ${archetype.title} — ${p.note}. empty-state hint: ${emptyHint} -->`,
1223
+ ...p.altNote ? [`<!-- ${p.altNote} -->`] : [],
1224
+ `<!-- SCAF-3: vite.config.ts — tell Vue that kai-* are custom elements (not Vue components).`,
1225
+ ` Without this, Vue warns "Unknown custom element" and .prop bindings may misbehave.`,
1226
+ ` import vue from '@vitejs/plugin-vue';`,
1227
+ ` export default { plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('kai-') } } })] }; -->`,
1228
+ `<script setup lang="ts">`,
1229
+ `import '@kitn.ai/ui/elements'; // registers <kai-*> — required, must come first`,
1230
+ `import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
1231
+ vueImports,
1232
+ ``,
1233
+ chatMessageType,
1234
+ ``,
1235
+ ...sampleSeed,
1236
+ `const loading = ref(false);`,
1237
+ `const suggestions = ${jsArray(suggestions)};`,
1238
+ ...sourcesSeed,
1239
+ ``,
1240
+ `// SCAF-15: kai-* register via an async dynamic import (SSR-safety). The .prop`,
1241
+ `// bindings can apply before the element upgrades, which drops them — re-apply once`,
1242
+ `// the element is defined so the initial messages/suggestions/loading stick.`,
1243
+ `onMounted(async () => {`,
1244
+ ` await customElements.whenDefined('kai-chat');`,
1245
+ ` const el = document.querySelector('kai-chat');`,
1246
+ ` if (el) Object.assign(el, { messages: messages.value, loading: loading.value, suggestions });`,
1247
+ `});`,
1248
+ ``,
1249
+ `async function onSubmit(e: CustomEvent<{ value: string }>) {`,
1250
+ onSubmitBody,
1251
+ `}`,
1252
+ `<\/script>`,
1253
+ ``,
1254
+ `<template>`,
1255
+ ` <div style="${p.style}">`,
1256
+ ...workspaceTemplate,
1257
+ ` </div>`,
1258
+ `</template>`
1259
+ ].filter((l) => l !== "").join("\n");
1260
+ }
1261
+ function renderSvelte(archetype, ctx) {
1262
+ const { p, emptyHint, suggestions, isMock, defaultModel } = ctx;
1263
+ const workspace = isWorkspace(archetype);
1264
+ const standaloneCompanionTags = archetype.components.filter(
1265
+ (t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t)
1266
+ );
1267
+ const hasEmbedded = archetype.components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
1268
+ const companionLinesList = [];
1269
+ if (hasEmbedded) {
1270
+ companionLinesList.push(
1271
+ ` <!-- kai-tool / kai-reasoning render INSIDE the thread — set tools/reasoning on each message object. -->`
1272
+ );
1273
+ }
1274
+ for (const t of standaloneCompanionTags) {
1275
+ if (t === "kai-sources") {
1276
+ companionLinesList.push(` <!-- Replace sampleSources with your real data. -->`);
1277
+ companionLinesList.push(` <kai-sources bind:this={sourcesEl}></kai-sources>`);
1278
+ } else {
1279
+ companionLinesList.push(` <!-- wire data props — see the component_reference MCP tool -->`);
1280
+ companionLinesList.push(` <${t}></${t}>`);
1281
+ }
1282
+ }
1283
+ const companionLines = companionLinesList.join("\n");
1284
+ const bodyPayload = defaultModel ? `{ model, messages: history.map((m) => ({ role: m.role, content: m.content })) }` : `{ messages: history.map((m) => ({ role: m.role, content: m.content })) }`;
1285
+ const onSubmitBody = isMock ? mockStreamBody({
1286
+ pad: " ",
1287
+ read: "messages",
1288
+ commitInitial: (expr) => `messages = ${expr};`,
1289
+ // `messages` is a live local — reassign to map over the latest array
1290
+ commitMap: (mapBody) => `messages = messages.map((m) => ${mapBody});`,
1291
+ setLoading: (v) => `loading = ${v};`,
1292
+ strictRoles: true
1293
+ }) : [
1294
+ ` const value = e.detail.value.trim();`,
1295
+ ` if (!value) return;`,
1296
+ ` const history = [...messages, { id: crypto.randomUUID(), role: 'user' as const, content: value }];`,
1297
+ ` const assistantId = crypto.randomUUID();`,
1298
+ ` messages = [...history, { id: assistantId, role: 'assistant' as const, content: '' }];`,
1299
+ ` loading = true;`,
1300
+ ...defaultModel ? [
1301
+ ` // SCAF-8: change this to any provider/model string you want to use.`,
1302
+ ` const model = '${defaultModel}';`
1303
+ ] : [],
1304
+ ` const res = await fetch('/api/chat', {`,
1305
+ ` method: 'POST',`,
1306
+ ` headers: { 'Content-Type': 'application/json' },`,
1307
+ ` body: JSON.stringify(${bodyPayload}),`,
1308
+ ` });`,
1309
+ ` // Stream the OpenAI-format SSE into the assistant message — see the Streaming recipe.`,
1310
+ ` const reader = res.body.getReader();`,
1311
+ ` const decoder = new TextDecoder();`,
1312
+ ` let buffer = '', answer = '';`,
1313
+ ` while (true) {`,
1314
+ ` const { value: chunk, done } = await reader.read();`,
1315
+ ` if (done) break;`,
1316
+ ` buffer += decoder.decode(chunk, { stream: true });`,
1317
+ ` const lines = buffer.split('\\n');`,
1318
+ ` buffer = lines.pop();`,
1319
+ ` for (const line of lines) {`,
1320
+ ` const s = line.trim();`,
1321
+ ` if (!s.startsWith('data:')) continue;`,
1322
+ ` const payload = s.slice(5).trim();`,
1323
+ ` if (payload === '[DONE]') continue;`,
1324
+ ` try {`,
1325
+ ` const delta = JSON.parse(payload).choices?.[0]?.delta?.content;`,
1326
+ ` if (!delta) continue;`,
1327
+ ` answer += delta;`,
1328
+ ` messages = messages.map((m) => (m.id === assistantId ? { ...m, content: answer } : m));`,
1329
+ ` } catch { /* skip keep-alives */ }`,
1330
+ ` }`,
1331
+ ` }`,
1332
+ ` loading = false;`
1333
+ ].join("\n");
1334
+ const chatMessageType = ` type ChatMessage = { id: string; role: 'user' | 'assistant'; content: string; reasoning?: { text: string; label?: string }; tools?: { type: string; state: 'input-streaming' | 'input-available' | 'output-available' | 'output-error'; input?: Record<string, unknown>; output?: Record<string, unknown>; toolCallId?: string }[] };`;
1335
+ const sampleMessagesInit = hasEmbedded ? [
1336
+ ` // SCAF-9: tool calls + reasoning render INSIDE the thread — set them on the message object.`,
1337
+ ` // Replace with real messages streamed from your backend.`,
1338
+ ` let messages: ChatMessage[] = [${JSON.stringify(SAMPLE_AGENTIC_MESSAGE)}];`
1339
+ ] : [` let messages: ChatMessage[] = [];`];
1340
+ const sourcesEl = standaloneCompanionTags.includes("kai-sources") ? [` let sourcesEl: HTMLElement | undefined;`] : [];
1341
+ const sourcesReactive = standaloneCompanionTags.includes("kai-sources") ? [
1342
+ ` // Replace sampleSources with your real source data.`,
1343
+ ` const sampleSources = [`,
1344
+ ` { href: 'https://example.com/doc1', title: 'Getting started', description: 'Overview of the product.' },`,
1345
+ ` { href: 'https://example.com/doc2', title: 'API reference', description: 'Full API documentation.' },`,
1346
+ ` ];`,
1347
+ ` $: if (sourcesEl) { sourcesEl.sources = sampleSources; }`
1348
+ ] : [];
1349
+ const workspaceMarkup = workspace ? [
1350
+ ` <!-- SCAF-14: workspace split — chat pane left, artifact preview right. -->`,
1351
+ ` <!-- kai-resizable needs kai-resizable-item children to render panels. -->`,
1352
+ ` <kai-resizable orientation="horizontal" style="display:block;width:100%;height:100%">`,
1353
+ ` <kai-resizable-item size="40%" min="240px">`,
1354
+ ` <kai-chat bind:this={chatEl} suggestion-mode="submit" style="${p.chatFill}" on:kai-submit={onSubmit}></kai-chat>`,
1355
+ ` </kai-resizable-item>`,
1356
+ ` <kai-resizable-item min="280px">`,
1357
+ ` <!-- Replace src with your artifact URL or set .files for multi-file preview. -->`,
1358
+ ` <kai-artifact src="https://example.com" style="width:100%;height:100%"></kai-artifact>`,
1359
+ ` </kai-resizable-item>`,
1360
+ ` </kai-resizable>`
1361
+ ] : [
1362
+ ` <kai-chat bind:this={chatEl} suggestion-mode="submit" style="${p.chatFill}" on:kai-submit={onSubmit}></kai-chat>`,
1363
+ companionLines
1364
+ ];
1365
+ return [
1366
+ `<!-- svelte — ${archetype.title} — ${p.note}. empty-state hint: ${emptyHint} -->`,
1367
+ ...p.altNote ? [`<!-- ${p.altNote} -->`] : [],
1368
+ `<!-- SCAF-5: This uses Svelte-4 syntax ($:, on:event). Works in Svelte 5 via legacy mode;`,
1369
+ ` runes-mode users should adapt to $state/$effect and onkai-submit event handlers. -->`,
1370
+ `<script lang="ts">`,
1371
+ ` import '@kitn.ai/ui/elements'; // registers <kai-*> — required, must come first`,
1372
+ ` import type { KaiChatElement } from '@kitn.ai/ui/elements';`,
1373
+ ` import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
1374
+ ` import { onMount } from 'svelte';`,
1375
+ chatMessageType,
1376
+ ` let chatEl: KaiChatElement | undefined;`,
1377
+ ` // SCAF-15: kai-* register via an async dynamic import (SSR-safety). Gate the`,
1378
+ ` // reactive property block on the upgrade so the first application isn't dropped`,
1379
+ ` // (props set on a not-yet-upgraded element are lost on upgrade).`,
1380
+ ` let defined = false;`,
1381
+ ` onMount(async () => { await customElements.whenDefined('kai-chat'); defined = true; });`,
1382
+ ...sourcesEl,
1383
+ ...sampleMessagesInit,
1384
+ ` let loading: boolean = false;`,
1385
+ ` const suggestions: string[] = ${jsArray(suggestions)};`,
1386
+ ` // suggestions/messages are JS PROPERTIES (arrays/objects can't be attributes)`,
1387
+ ` $: if (chatEl && defined) { chatEl.messages = messages; chatEl.loading = loading; chatEl.suggestions = suggestions; }`,
1388
+ ...sourcesReactive,
1389
+ ``,
1390
+ ` async function onSubmit(e: CustomEvent<{ value: string }>) {`,
1391
+ onSubmitBody,
1392
+ ` }`,
1393
+ `<\/script>`,
1394
+ ``,
1395
+ `<div style="${p.style}">`,
1396
+ ...workspaceMarkup,
1397
+ `</div>`
1398
+ ].filter((l, i, arr) => !(l === "" && arr[i - 1] === "" && i === arr.length - 1)).join("\n");
1399
+ }
1400
+ function renderTanstackStart(archetype, ctx) {
1401
+ const { p, emptyHint, suggestions, isMock, defaultModel } = ctx;
1402
+ const hasEmbedded = archetype.components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
1403
+ const workspace = isWorkspace(archetype);
1404
+ const renderableTags = archetype.components.filter((t) => !MESSAGE_EMBEDDED_TAGS.has(t));
1405
+ const importTags = workspace ? [.../* @__PURE__ */ new Set([...renderableTags.filter((t) => t !== "kai-resizable"), "kai-resizable", "kai-resizable-item"])] : renderableTags;
1406
+ const wrapperNames = importTags.map(toPascalCase);
1407
+ const importList = wrapperNames.join(", ");
1408
+ const standaloneCompanionTags = archetype.components.filter(
1409
+ (t) => t !== "kai-chat" && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t)
1410
+ );
1411
+ const companionJsxLines = [];
1412
+ if (hasEmbedded) {
1413
+ companionJsxLines.push(
1414
+ ` {/* kai-tool / kai-reasoning render inside the thread. Tool calls + reasoning`,
1415
+ ` are set on each message object — see the sampleMessages initializer above. */}`
1416
+ );
1417
+ }
1418
+ for (const t of standaloneCompanionTags) {
1419
+ if (t === "kai-sources") {
1420
+ companionJsxLines.push(
1421
+ ` {/* Replace sampleSources with your real data. */}`,
1422
+ ` <Sources sources={sampleSources} />`
1423
+ );
1424
+ } else {
1425
+ companionJsxLines.push(` {/* wire data props — see the component_reference MCP tool */}`);
1426
+ companionJsxLines.push(` <${toPascalCase(t)} />`);
1427
+ }
1428
+ }
1429
+ const companions = companionJsxLines.join("\n");
1430
+ const chatMessageType = `type ChatMessage = { id: string; role: 'user' | 'assistant'; content: string; reasoning?: { text: string; label?: string }; tools?: { type: string; state: 'input-streaming' | 'input-available' | 'output-available' | 'output-error'; input?: Record<string, unknown>; output?: Record<string, unknown>; toolCallId?: string }[] };`;
1431
+ const sampleMessagesInit = hasEmbedded ? [
1432
+ ` // SCAF-9: tool calls and reasoning render inside the thread — set them on the message object.`,
1433
+ ` // Replace with real messages streamed from your backend.`,
1434
+ ` const sampleMessages: ChatMessage[] = [${JSON.stringify(SAMPLE_AGENTIC_MESSAGE)}];`,
1435
+ ` const [messages, setMessages] = useState<ChatMessage[]>(sampleMessages);`
1436
+ ].join("\n") : ` const [messages, setMessages] = useState<ChatMessage[]>([]);`;
1437
+ const sampleSourcesInit = standaloneCompanionTags.includes("kai-sources") ? [
1438
+ ` // Replace sampleSources with your real source data.`,
1439
+ ` const sampleSources = [`,
1440
+ ` { href: 'https://example.com/doc1', title: 'Getting started', description: 'Overview of the product.' },`,
1441
+ ` { href: 'https://example.com/doc2', title: 'API reference', description: 'Full API documentation.' },`,
1442
+ ` ];`
1443
+ ].join("\n") : "";
1444
+ const modelInit = defaultModel ? ` // SCAF-8: change this to any provider/model string you want to use.
1445
+ const model = '${defaultModel}';` : "";
1446
+ const bodyPayload = defaultModel ? `{ model, messages: history.map((m) => ({ role: m.role, content: m.content })) }` : `{ messages: history.map((m) => ({ role: m.role, content: m.content })) }`;
1447
+ const onSubmitBody = isMock ? mockStreamBody({
1448
+ pad: " ",
1449
+ read: "messages",
1450
+ commitInitial: (expr) => `setMessages(${expr});`,
1451
+ commitMap: (mapBody) => `setMessages((prev) => prev.map((m) => ${mapBody}));`,
1452
+ setLoading: (v) => `setLoading(${v});`,
1453
+ strictRoles: true
1454
+ }) : [
1455
+ ` const value = e.detail.value.trim();`,
1456
+ ` if (!value) return;`,
1457
+ ` const history: ChatMessage[] = [...messages, { id: crypto.randomUUID(), role: 'user' as const, content: value }];`,
1458
+ ` const assistantId = crypto.randomUUID();`,
1459
+ ` setMessages([...history, { id: assistantId, role: 'assistant' as const, content: '' }]);`,
1460
+ ` setLoading(true);`,
1461
+ ` const res = await fetch('/api/chat', {`,
1462
+ ` method: 'POST',`,
1463
+ ` headers: { 'Content-Type': 'application/json' },`,
1464
+ ` body: JSON.stringify(${bodyPayload}),`,
1465
+ ` });`,
1466
+ ` // Stream the OpenAI-format SSE into the assistant message — see the Streaming recipe.`,
1467
+ ` const reader = res.body!.getReader();`,
1468
+ ` const decoder = new TextDecoder();`,
1469
+ ` let buffer = '', answer = '';`,
1470
+ ` while (true) {`,
1471
+ ` const { value: chunk, done } = await reader.read();`,
1472
+ ` if (done) break;`,
1473
+ ` buffer += decoder.decode(chunk, { stream: true });`,
1474
+ ` const lines = buffer.split('\\n');`,
1475
+ ` buffer = lines.pop()!;`,
1476
+ ` for (const line of lines) {`,
1477
+ ` const s = line.trim();`,
1478
+ ` if (!s.startsWith('data:')) continue;`,
1479
+ ` const payload = s.slice(5).trim();`,
1480
+ ` if (payload === '[DONE]') continue;`,
1481
+ ` try {`,
1482
+ ` const delta = JSON.parse(payload).choices?.[0]?.delta?.content;`,
1483
+ ` if (!delta) continue;`,
1484
+ ` answer += delta;`,
1485
+ ` setMessages((ms) => ms.map((m) => (m.id === assistantId ? { ...m, content: answer } : m)));`,
1486
+ ` } catch { /* skip keep-alives */ }`,
1487
+ ` }`,
1488
+ ` }`,
1489
+ ` setLoading(false);`
1490
+ ].join("\n");
1491
+ const filePathNote = [
1492
+ `// TanStack Start route file — save as: src/routes/chat.tsx`,
1493
+ `// Scaffold command: npx @tanstack/cli@latest create <app-name> --framework react --no-git --package-manager npm -y`,
1494
+ `// Then: npm install @kitn.ai/ui`,
1495
+ `// Dev: npm run dev (port 3000) Build: npm run build Preview: npm run preview`,
1496
+ `// Note: there is no 'npm start' script — use 'npm run dev' or 'npm run preview'.`,
1497
+ `// Backend: place TanStack server functions in src/server/chat.ts (createServerFn).`,
1498
+ ``
1499
+ ];
1500
+ return [
1501
+ ...filePathNote,
1502
+ // TanStack Start uses @tanstack/react-router's createFileRoute
1503
+ `import { createFileRoute } from '@tanstack/react-router'`,
1504
+ `import { useState } from 'react'`,
1505
+ // Elements registration: the library is SSR-import-safe; top-level import is safe here
1506
+ `import '@kitn.ai/ui/elements'; // registers <kai-*> — required, must come first`,
1507
+ `import { ${importList} } from '@kitn.ai/ui/react'`,
1508
+ `import '@kitn.ai/ui/theme.tokens.css' // compiled token defaults`,
1509
+ ``,
1510
+ `// ${archetype.title} — ${p.note}. empty-state hint: ${emptyHint}`,
1511
+ ...p.altNote ? [`// ${p.altNote}`] : [],
1512
+ chatMessageType,
1513
+ ``,
1514
+ `// ssr: false keeps the Solid-based web component client-only.`,
1515
+ `// Server HTML for /chat omits <kai-chat> → no hydration mismatch.`,
1516
+ `export const Route = createFileRoute('/chat')({`,
1517
+ ` ssr: false,`,
1518
+ ` component: ChatPage,`,
1519
+ `})`,
1520
+ ``,
1521
+ `function ChatPage() {`,
1522
+ sampleMessagesInit,
1523
+ ` const [loading, setLoading] = useState(false);`,
1524
+ ` const suggestions = ${jsArray(suggestions)};`,
1525
+ ...sampleSourcesInit ? [sampleSourcesInit] : [],
1526
+ ...modelInit ? [modelInit] : [],
1527
+ ``,
1528
+ ` async function onSubmit(e: CustomEvent<{ value: string }>) {`,
1529
+ onSubmitBody,
1530
+ ` }`,
1531
+ ``,
1532
+ ` return (`,
1533
+ ` <main style={{ ${jsxStyle(p.style)} }}>`,
1534
+ ...workspace ? [
1535
+ ` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
1536
+ ` {/* Resizable needs ResizableItem children to render panels. */}`,
1537
+ ` <Resizable orientation="horizontal" style={{ display: 'block', width: '100%', height: '100%' }}>`,
1538
+ ` <ResizableItem size="40%" min="240px">`,
1539
+ ` <Chat`,
1540
+ ` messages={messages}`,
1541
+ ` loading={loading}`,
1542
+ ` suggestions={suggestions}`,
1543
+ ` suggestionMode="submit"`,
1544
+ ` onSubmit={onSubmit}`,
1545
+ ` style={{ ${jsxStyle(p.chatFill)} }}`,
1546
+ ` />`,
1547
+ ` </ResizableItem>`,
1548
+ ` <ResizableItem min="280px">`,
1549
+ ` {/* Replace src with your artifact URL or set files for multi-file preview. */}`,
1550
+ ` <Artifact src="https://example.com" style={{ width: '100%', height: '100%' }} />`,
1551
+ ` </ResizableItem>`,
1552
+ ` </Resizable>`
1553
+ ] : [
1554
+ ` <Chat`,
1555
+ ` messages={messages}`,
1556
+ ` loading={loading}`,
1557
+ ` suggestions={suggestions}`,
1558
+ ` suggestionMode="submit"`,
1559
+ ` onSubmit={onSubmit}`,
1560
+ ` style={{ ${jsxStyle(p.chatFill)} }}`,
1561
+ ` />`,
1562
+ companions
1563
+ ],
1564
+ ` </main>`,
1565
+ ` );`,
1566
+ `}`
1567
+ ].filter((l) => l !== "").join("\n");
1568
+ }
1569
+ function jsxStyle(style) {
1570
+ return style.split(";").map((d) => d.trim()).filter(Boolean).map((d) => {
1571
+ const [prop, ...rest] = d.split(":");
1572
+ const camel = prop.trim().replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1573
+ return `${camel}: '${rest.join(":").trim()}'`;
1574
+ }).join(", ");
1575
+ }
1576
+ function renderFrontend(framework, archetype, placement, emptyHint, suggestions, isMock, defaultModel) {
1577
+ const ctx = { p: placementStyle(placement), emptyHint, suggestions, isMock, defaultModel };
1578
+ switch (framework) {
1579
+ case "react":
1580
+ case "next":
1581
+ return renderJsx(archetype, ctx, framework);
1582
+ case "vue":
1583
+ return renderVue(archetype, ctx);
1584
+ case "svelte":
1585
+ return renderSvelte(archetype, ctx);
1586
+ case "tanstack-start":
1587
+ return renderTanstackStart(archetype, ctx);
1588
+ case "html":
1589
+ default:
1590
+ return renderHtml(archetype, ctx);
1591
+ }
1592
+ }
1593
+ const RUNTIME_LABEL = {
1594
+ next: "Next.js route handler (Node/Edge)",
1595
+ express: "Express handler (Node)",
1596
+ worker: "Cloudflare Worker",
1597
+ fastapi: "FastAPI (Python)",
1598
+ html: "browser-direct (no server route)",
1599
+ "tanstack-start": "TanStack Start server function (Node)"
1600
+ };
1601
+ function preferredKeyFor(integration) {
1602
+ return integration.language === "python" ? ["fastapi"] : ["next", "express", "worker"];
1603
+ }
1604
+ function chooseRoute(integration, framework) {
1605
+ const templates = integration.routeTemplates;
1606
+ if (templates[framework]) {
1607
+ return { framework, template: templates[framework], runtime: RUNTIME_LABEL[framework] ?? framework, exact: true };
1608
+ }
1609
+ for (const key of preferredKeyFor(integration)) {
1610
+ if (templates[key]) {
1611
+ return { framework: key, template: templates[key], runtime: RUNTIME_LABEL[key] ?? key, exact: false };
1612
+ }
1613
+ }
1614
+ for (const [key, template] of Object.entries(templates)) {
1615
+ if (key === "html") continue;
1616
+ return { framework: key, template, runtime: RUNTIME_LABEL[key] ?? key, exact: false };
1617
+ }
1618
+ return void 0;
1619
+ }
1620
+ function compose(archetype, integration, placement, framework, suggestions, audience) {
1621
+ const audienceHint = audience ? `tuned for ${audience} — keep the empty state and tone audience-appropriate` : "add an empty-state prompt that fits your product";
1622
+ const isMock = integration.id === "mock";
1623
+ const defaultModel = isMock ? void 0 : defaultModelFor(integration);
1624
+ const frontend = renderFrontend(framework, archetype, placement, audienceHint, suggestions, isMock, defaultModel);
1625
+ const route = isMock ? void 0 : chooseRoute(integration, framework);
1626
+ const header = [
1627
+ `# AI/UI scaffold — ${archetype.title} × ${integration.title}`,
1628
+ `combo: ${archetype.id} × ${integration.id} × ${placement} × ${framework}`,
1629
+ `stream: ${integration.streamMapping}`
1630
+ ].join("\n");
1631
+ const block1 = [
1632
+ `=== (1) FRONT-END (${framework}, ${placementStyle(placement).note}) ===`,
1633
+ ``,
1634
+ frontend
1635
+ ].join("\n");
1636
+ const block2Parts = [`=== (2) BACKEND ROUTE ===`, ``];
1637
+ if (isMock) {
1638
+ block2Parts.push(
1639
+ `# No backend or API key needed — replies stream locally for preview (see the`,
1640
+ `# front-end onSubmit above). Swap \`integration\` for a real provider (openrouter,`,
1641
+ `# ollama, vercel-ai-sdk, …) when ready, and this block becomes its route handler.`
1642
+ );
1643
+ } else if (route) {
1644
+ if (!route.exact) {
1645
+ block2Parts.push(
1646
+ `# Note: ${integration.title} has no template for "${framework}". Emitting its native`,
1647
+ `# ${route.runtime} route instead (matches the integration's ${integration.language} language).`
1648
+ );
1649
+ if (framework === "react") {
1650
+ block2Parts.push(
1651
+ `#`,
1652
+ `# WARNING: this is a Next.js route handler — it will NOT run in a Vite SPA`,
1653
+ `# (a Vite \`react\` app has no /api routes). To make the front-end above work, either:`,
1654
+ `# • use Next.js (framework: "next"), or`,
1655
+ `# • add a Vite dev-server middleware/proxy to a server, or`,
1656
+ `# • run a separate server (framework: "express" | "worker"), or`,
1657
+ `# • use integration: "mock" for a zero-config local stream (no backend, no key).`
1658
+ );
1659
+ }
1660
+ block2Parts.push(``);
1661
+ } else {
1662
+ block2Parts.push(`# Runtime: ${route.runtime}`, ``);
1663
+ }
1664
+ block2Parts.push(route.template);
1665
+ } else {
1666
+ block2Parts.push(
1667
+ `# ${integration.title} ships no usable backend route template. See its docs`,
1668
+ `# (${integration.docsSlug}) — wire the route by hand following the streamMapping above.`
1669
+ );
1670
+ }
1671
+ const block2 = block2Parts.join("\n");
1672
+ const envLines = integration.envVars.length ? integration.envVars.map((v) => ` - ${v}`).join("\n") : " (none)";
1673
+ const block3 = [
1674
+ `=== (3) RUN NOTE ===`,
1675
+ ``,
1676
+ integration.runNote,
1677
+ ``,
1678
+ `Env vars to set:`,
1679
+ envLines
1680
+ ].join("\n");
1681
+ return [header, block1, block2, block3].join("\n\n");
1682
+ }
1683
+ function rejectIntegration(id) {
1684
+ const valid = listIntegrations().map((i) => `${i.id} (${i.title})`).join(", ");
1685
+ return [
1686
+ `Unknown integration: "${id}".`,
1687
+ ``,
1688
+ `Valid integrations: ${valid}.`,
1689
+ `Pick one of those ids and call scaffold again.`
1690
+ ].join("\n");
1691
+ }
1692
+ function rejectUseCase(id) {
1693
+ const valid = listArchetypes().map((a) => `${a.id} (${a.title})`).join(", ");
1694
+ return [
1695
+ `Unknown useCase: "${id}".`,
1696
+ ``,
1697
+ `Valid useCases (archetypes): ${valid}.`,
1698
+ `Pick one of those ids and call scaffold again.`
1699
+ ].join("\n");
1700
+ }
1701
+ const scaffold = {
1702
+ name: "scaffold",
1703
+ description: 'Scaffold a working AI/UI chat surface from four axes: useCase (archetype) × integration × placement × framework. Emits a copy-pasteable front-end (kai-* components wired with messages + kai-submit + starter suggestions), the backend route for the chosen framework, and a run note with env vars. Use integration: "mock" for a zero-config local preview.',
1704
+ inputSchema: z.object({
1705
+ // useCase + integration are dynamic catalog ids — kept as strings and
1706
+ // validated against the registry in the handler (the handler is called
1707
+ // directly in tests, bypassing this schema). Use component_reference / the
1708
+ // catalogs to discover valid ids.
1709
+ useCase: z.string().describe(
1710
+ 'Archetype id, e.g. "drop-in-chat", "support-widget", "knowledge-base", "agentic", "workspace", "voice".'
1711
+ ),
1712
+ integration: z.string().describe(
1713
+ 'Backend integration id, e.g. "openrouter", "vercel-ai-sdk", "langgraph", "cloudflare", "ollama", "mastra", "pi", "pydantic-ai".'
1714
+ ),
1715
+ placement: Placement.describe(
1716
+ "Where the surface lives: full-page | side | docked-widget | inline."
1717
+ ),
1718
+ framework: Framework.describe(
1719
+ "Target front-end/back-end framework: html | react | next | vue | svelte | fastapi | express | worker | tanstack-start."
1720
+ ),
1721
+ suggestions: z.array(z.string()).optional().describe(
1722
+ "Optional starter prompts shown above the input when the thread is empty (real kai-chat prop). Defaults to a generic pair if omitted so the feature always shows."
1723
+ ),
1724
+ audience: z.string().optional().describe("Optional audience hint (tweaks the empty-state comment only).")
1725
+ }),
1726
+ handler: async (args) => {
1727
+ const useCase = String(args.useCase ?? "");
1728
+ const integrationId = String(args.integration ?? "");
1729
+ const placement = String(args.placement ?? "");
1730
+ const framework = String(args.framework ?? "html");
1731
+ const audience = args.audience ? String(args.audience) : void 0;
1732
+ const suggestions = Array.isArray(args.suggestions) && args.suggestions.length > 0 ? args.suggestions.map(String) : DEFAULT_SUGGESTIONS;
1733
+ const archetype = getArchetype(useCase);
1734
+ if (!archetype) return text(rejectUseCase(useCase));
1735
+ const integration = getIntegration(integrationId);
1736
+ if (!integration) return text(rejectIntegration(integrationId));
1737
+ const effectivePlacement = placement || archetype.defaultPlacement;
1738
+ return text(compose(archetype, integration, effectivePlacement, framework, suggestions, audience));
1739
+ }
1740
+ };
1741
+ function parseHex(hex) {
1742
+ const s = hex.trim().replace(/^#/, "");
1743
+ if (s.length === 3) {
1744
+ const r = parseInt(s[0] + s[0], 16);
1745
+ const g = parseInt(s[1] + s[1], 16);
1746
+ const b = parseInt(s[2] + s[2], 16);
1747
+ if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
1748
+ return { r, g, b };
1749
+ }
1750
+ if (s.length === 6) {
1751
+ const r = parseInt(s.slice(0, 2), 16);
1752
+ const g = parseInt(s.slice(2, 4), 16);
1753
+ const b = parseInt(s.slice(4, 6), 16);
1754
+ if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
1755
+ return { r, g, b };
1756
+ }
1757
+ return null;
1758
+ }
1759
+ function toHex(r, g, b) {
1760
+ return "#" + [r, g, b].map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0")).join("");
1761
+ }
1762
+ function relativeLuminance(r, g, b) {
1763
+ const lin = (c) => {
1764
+ const s = c / 255;
1765
+ return s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
1766
+ };
1767
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
1768
+ }
1769
+ function foreground(r, g, b) {
1770
+ const lum = relativeLuminance(r, g, b);
1771
+ const contrastWhite = 1.05 / (lum + 0.05);
1772
+ const contrastBlack = (lum + 0.05) / 0.05;
1773
+ return contrastWhite >= contrastBlack ? "#ffffff" : "#000000";
1774
+ }
1775
+ function lighten(r, g, b, ratio) {
1776
+ return toHex(
1777
+ r + (255 - r) * ratio,
1778
+ g + (255 - g) * ratio,
1779
+ b + (255 - b) * ratio
1780
+ );
1781
+ }
1782
+ function darken(r, g, b, ratio) {
1783
+ return toHex(r * (1 - ratio), g * (1 - ratio), b * (1 - ratio));
1784
+ }
1785
+ function accentSurface(r, g, b) {
1786
+ return lighten(r, g, b, 0.88);
1787
+ }
1788
+ function darkModeBrand(r, g, b) {
1789
+ const lum = relativeLuminance(r, g, b);
1790
+ if (lum > 0.25) return toHex(r, g, b);
1791
+ return lighten(r, g, b, 0.35);
1792
+ }
1793
+ function darkAccentSurface(r, g, b) {
1794
+ return darken(r, g, b, 0.75);
1795
+ }
1796
+ const KEYWORD_COLORS = {
1797
+ blue: "#3b82f6",
1798
+ navy: "#1e3a8a",
1799
+ sky: "#0ea5e9",
1800
+ cyan: "#06b6d4",
1801
+ teal: "#14b8a6",
1802
+ green: "#22c55e",
1803
+ emerald: "#10b981",
1804
+ lime: "#84cc16",
1805
+ yellow: "#eab308",
1806
+ amber: "#f59e0b",
1807
+ orange: "#f97316",
1808
+ red: "#ef4444",
1809
+ rose: "#f43f5e",
1810
+ pink: "#ec4899",
1811
+ purple: "#a855f7",
1812
+ violet: "#7c3aed",
1813
+ indigo: "#6366f1",
1814
+ slate: "#64748b",
1815
+ gray: "#6b7280",
1816
+ zinc: "#71717a",
1817
+ neutral: "#737373",
1818
+ stone: "#78716c",
1819
+ white: "#f8fafc",
1820
+ black: "#0f172a"
1821
+ };
1822
+ const DEFAULT_BRAND = "#6366f1";
1823
+ function resolveBrand(brand, description) {
1824
+ if (brand) {
1825
+ const parsed2 = parseHex(brand);
1826
+ if (parsed2) {
1827
+ return { hex: brand.startsWith("#") ? brand : `#${brand}`, ...parsed2, note: "" };
1828
+ }
1829
+ const fallback = parseHex(DEFAULT_BRAND);
1830
+ return {
1831
+ hex: DEFAULT_BRAND,
1832
+ ...fallback,
1833
+ note: `Could not parse \`${brand}\` as a hex color — defaulted to **${DEFAULT_BRAND}** (indigo). Pass a valid \`#rgb\` or \`#rrggbb\` value to use your color.`
1834
+ };
1835
+ }
1836
+ if (description) {
1837
+ const lower = description.toLowerCase();
1838
+ for (const [keyword, hex] of Object.entries(KEYWORD_COLORS)) {
1839
+ if (lower.includes(keyword)) {
1840
+ const parsed3 = parseHex(hex);
1841
+ return {
1842
+ hex,
1843
+ ...parsed3,
1844
+ note: `No brand color provided — inferred **${hex}** (${keyword}) from description. Override with \`brand: '#rrggbb'\` to use an exact color.`
1845
+ };
1846
+ }
1847
+ }
1848
+ const parsed2 = parseHex(DEFAULT_BRAND);
1849
+ return {
1850
+ hex: DEFAULT_BRAND,
1851
+ ...parsed2,
1852
+ note: `No recognizable color keyword in description — defaulted to **${DEFAULT_BRAND}** (indigo). Override with \`brand: '#rrggbb'\` for an exact color.`
1853
+ };
1854
+ }
1855
+ const parsed = parseHex(DEFAULT_BRAND);
1856
+ return {
1857
+ hex: DEFAULT_BRAND,
1858
+ ...parsed,
1859
+ note: `No brand color or description provided — defaulted to **${DEFAULT_BRAND}** (indigo, the kit's own brand). Pass \`brand: '#rrggbb'\` to use your color.`
1860
+ };
1861
+ }
1862
+ function buildTokenSet(r, g, b) {
1863
+ const primary = toHex(r, g, b);
1864
+ const primaryFg = foreground(r, g, b);
1865
+ const accent = accentSurface(r, g, b);
1866
+ const accentFgColor = foreground(
1867
+ ...[accent].map((h) => parseHex(h)).flatMap((c) => [c.r, c.g, c.b])
1868
+ );
1869
+ return {
1870
+ primary,
1871
+ primaryFg,
1872
+ ring: primary,
1873
+ accent,
1874
+ accentFg: accentFgColor
1875
+ };
1876
+ }
1877
+ function buildDarkTokenSet(r, g, b) {
1878
+ const darkBrand = darkModeBrand(r, g, b);
1879
+ const parsed = parseHex(darkBrand);
1880
+ const primary = darkBrand;
1881
+ const primaryFg = foreground(parsed.r, parsed.g, parsed.b);
1882
+ const accent = darkAccentSurface(r, g, b);
1883
+ const accentFgParsed = parseHex(accent);
1884
+ return {
1885
+ primary,
1886
+ primaryFg,
1887
+ ring: darkBrand,
1888
+ accent,
1889
+ accentFg: foreground(accentFgParsed.r, accentFgParsed.g, accentFgParsed.b)
1890
+ };
1891
+ }
1892
+ function cssBlock(selector, tokens) {
1893
+ return [
1894
+ `${selector} {`,
1895
+ ` --kai-color-primary: ${tokens.primary};`,
1896
+ ` --kai-color-primary-foreground: ${tokens.primaryFg};`,
1897
+ ` --kai-color-ring: ${tokens.ring};`,
1898
+ ` --kai-color-accent: ${tokens.accent};`,
1899
+ ` --kai-color-accent-foreground: ${tokens.accentFg};`,
1900
+ `}`
1901
+ ].join("\n");
1902
+ }
1903
+ const theme = {
1904
+ name: "theme",
1905
+ description: "Generate a --kai-* CSS token override block from a brand color or description. Paste the block into your global stylesheet to brand every kai-* element in one shot.",
1906
+ inputSchema: z.object({
1907
+ brand: z.string().optional().describe('Brand color as #rgb or #rrggbb hex (e.g. "#7c3aed")'),
1908
+ description: z.string().optional().describe(
1909
+ "Natural-language description of your product (used to infer a color when `brand` is omitted)"
1910
+ ),
1911
+ mode: z.enum(["light", "dark", "both"]).optional().describe(
1912
+ '"light" → :root only (default); "dark" → .dark only; "both" → :root + .dark'
1913
+ )
1914
+ }),
1915
+ handler: async (args) => {
1916
+ const {
1917
+ brand,
1918
+ description,
1919
+ mode = "light"
1920
+ } = args;
1921
+ const { hex, r, g, b, note } = resolveBrand(brand, description);
1922
+ const lightTokens = buildTokenSet(r, g, b);
1923
+ const darkTokens = buildDarkTokenSet(r, g, b);
1924
+ const blocks = [];
1925
+ if (mode === "light" || mode === "both") {
1926
+ blocks.push(cssBlock(":root", lightTokens));
1927
+ }
1928
+ if (mode === "dark" || mode === "both") {
1929
+ blocks.push(cssBlock(".dark", darkTokens));
1930
+ }
1931
+ const cssOutput = blocks.join("\n\n");
1932
+ const noteLabel = note.startsWith("Could not parse") ? "Note" : "Assumption";
1933
+ const noteSection = note ? `
1934
+ > **${noteLabel}:** ${note}
1935
+ ` : "";
1936
+ const text2 = `## AI/UI theme override — \`${hex}\`
1937
+ ${noteSection}
1938
+ Paste this block into your **global stylesheet** (before the \`@import\` of \`@kitn.ai/ui\` or after — order doesn't matter since \`--kai-*\` tokens are resolved at runtime via \`var()\` fallbacks). For \`mode:'dark'\` overrides, the \`.dark\` class must be on a parent element (e.g. \`<html class="dark">\`) for them to take effect.
1939
+
1940
+ \`\`\`css
1941
+ ${cssOutput}
1942
+ \`\`\`
1943
+
1944
+ ### How it works
1945
+
1946
+ The \`--kai-color-*\` tokens pierce the Shadow DOM via CSS custom-property inheritance. Every \`kai-*\` element reads them through a \`var(--kai-color-primary, <default>)\` fallback chain defined in \`theme.css\`. Setting them on \`:root\` (or a wrapper element) is enough to rebrand everything globally.
1947
+
1948
+ **Tokens emitted** (verified names from \`theme.css\`):
1949
+ - \`--kai-color-primary\` — primary brand color
1950
+ - \`--kai-color-primary-foreground\` — AA-contrast foreground on primary (auto-selected black/white)
1951
+ - \`--kai-color-ring\` — keyboard focus ring
1952
+ - \`--kai-color-accent\` — tinted accent surface
1953
+ - \`--kai-color-accent-foreground\` — text on accent surface
1954
+
1955
+ ### Apply to a subtree only
1956
+
1957
+ \`\`\`css
1958
+ /* scope to a specific container instead of :root */
1959
+ .my-chat-widget {
1960
+ ${blocks[0]?.split("\n").slice(1, -1).map((l) => " " + l).join("\n")}
1961
+ }
1962
+ \`\`\`
1963
+
1964
+ ### Fine-tune the full palette
1965
+
1966
+ The theme editor lets you tweak every token interactively and copy the result:
1967
+
1968
+ **→ [Open Theme Editor](https://ui.kitn.ai/theme/editor/)**
1969
+
1970
+ For the complete token list see the [theming guide](https://ui.kitn.ai/guides/theming/) and the source \`theme.css\` in the package root.
1971
+ `;
1972
+ return { content: [{ type: "text", text: text2 }] };
1973
+ }
1974
+ };
1975
+ const RULES = [
1976
+ {
1977
+ // Rule 1 — array/object data set as an HTML attribute
1978
+ // Source: for-ai-agents.mdx §1; context7.json rule 2
1979
+ id: "array-as-attribute",
1980
+ test: (t) => /\b(messages|models|context|suggestions|slashCommands)\s*=\s*["']/.test(t),
1981
+ title: "Array/object prop set as an HTML attribute (silent failure)",
1982
+ cause: "An HTML attribute is always a string. Passing `messages`, `models`, `context`, `suggestions`, or `slashCommands` as an HTML attribute silently fails — the element receives a stringified value it cannot parse.",
1983
+ fix: "Set the property in JavaScript, not as an HTML attribute. Only scalar props (`placeholder`, `loading`, `theme`) work as attributes.\n\n```js\n// ✅ Works — set messages in JavaScript as a property\nconst chat = document.querySelector('kai-chat');\nchat.messages = [{ id: '1', role: 'assistant', content: 'Hello!' }];\n```\n\n```html\n<!-- ❌ Fails — messages cannot be an HTML attribute -->\n<kai-chat messages=\"[...]\"></kai-chat>\n```"
1984
+ },
1985
+ {
1986
+ // Rule 2 — in-place mutation → no re-render
1987
+ // Source: for-ai-agents.mdx §3; context7.json rule 4
1988
+ id: "in-place-mutation",
1989
+ test: (t) => /don'?t\s+update|doesn'?t\s+re.?render|no\s+re.?render|\.push\(|\bpush(?:es|ing)?\s+(?:to|into|onto)\b|mutate|in.place/.test(t),
1990
+ title: "In-place mutation does not trigger a re-render",
1991
+ cause: "Mutating an existing message object or array in place (e.g. `chat.messages.push(…)` or `chat.messages[i].content = …`) does not trigger a re-render. The element only reacts when it detects a new array/object reference.",
1992
+ fix: "Assign a NEW array (and a new object) on every change — never mutate in place.\n\n```js\n// ✅ Triggers re-render — new array + new object reference\nchat.messages = [\n ...chat.messages,\n { id: crypto.randomUUID(), role: 'user', content: userText },\n];\n\n// ✅ During streaming — replace with a new array + new object on every chunk\nchat.messages = chat.messages.map((m) =>\n m.id === assistantId ? { ...m, content: accumulated } : m\n);\n\n// ❌ Does NOT trigger re-render\nchat.messages.push(newMsg);\nchat.messages[i].content = accumulated;\n```"
1993
+ },
1994
+ {
1995
+ // Rule 3 — listening for events on a parent / wrong element
1996
+ // Source: for-ai-agents.mdx §2; context7.json rule 3
1997
+ id: "event-bubbling",
1998
+ test: (t) => /event.*not\s+fir|not\s+fir.*event|listen.*document|document.*listen|listen.*parent|parent.*listen|event.*bubbl/i.test(
1999
+ t
2000
+ ),
2001
+ title: "Listening for events on the wrong element (events are non-bubbling)",
2002
+ cause: "`kai-*` events are non-bubbling CustomEvents. Adding a listener to `document`, `window`, or a parent container will never fire because the event does not bubble up.",
2003
+ fix: "Listen directly on the `kai-*` element. The submit event is `kai-submit` with `event.detail.value`.\n\n```js\nconst chat = document.querySelector('kai-chat');\n\n// ✅ Listen directly on the element\nchat.addEventListener('kai-submit', (e) => {\n console.log(e.detail.value); // the text the user typed\n});\n\n// ❌ Never fires — kai-submit does not bubble\ndocument.addEventListener('kai-submit', handler);\n```\n\nCommon events: `kai-submit`, `kai-feedback`, `kai-model-change`, `kai-new-chat`, `kai-select`."
2004
+ },
2005
+ {
2006
+ // Rule 4 — wrong element prefix kitn-
2007
+ // Source: context7.json rule 1
2008
+ id: "wrong-prefix",
2009
+ test: (t) => /\bkitn-/.test(t),
2010
+ title: "Wrong element prefix `kitn-` (should be `kai-`)",
2011
+ cause: "The custom element prefix is `kai-` (e.g. `<kai-chat>`, `<kai-artifact>`). `kitn-chat` is only a bundle filename — it is not a registered element name. Using `<kitn-chat>` results in an unknown element that renders nothing.",
2012
+ fix: "Replace the `kitn-` prefix with `kai-` everywhere.\n\n```html\n<!-- ✅ Correct element prefix -->\n<kai-chat></kai-chat>\n<kai-artifact></kai-artifact>\n\n<!-- ❌ Wrong — kitn-chat is a bundle filename, not an element -->\n<kitn-chat></kitn-chat>\n```"
2013
+ },
2014
+ {
2015
+ // Rule 6 — custom elements not registered / renders nothing (React #1 failure)
2016
+ // Source: field-test reports; for-ai-agents.mdx §"Import order matters"
2017
+ id: "elements-not-registered",
2018
+ test: (t) => {
2019
+ if (/renders?\s+nothing|nothing\s+renders?|not\s+registered|unregistered|not\s+upgraded|unknown\s+element|customElements\.get|undefined\s+element|no\s+shadow\s+root/.test(t))
2020
+ return true;
2021
+ if (/\b(empty|blank)\b/.test(t) && /render|element|component|kai-|<[a-z]+-|shadow/.test(t))
2022
+ return true;
2023
+ if (/doesn'?t\s+render|won'?t\s+render/.test(t) && /kai-|element|component|custom.?element/.test(t))
2024
+ return true;
2025
+ return false;
2026
+ },
2027
+ title: "Custom elements not registered — renders nothing / empty box",
2028
+ cause: "The `@kitn.ai/ui/react` wrappers (and bare `<kai-*>` tags) do NOT register the custom elements by themselves. Without the element-registration side-effect import, `<kai-chat>` / `<Chat>` is an un-upgraded unknown element — an empty box. `customElements.get('kai-chat') === undefined`.",
2029
+ fix: "Import the elements bundle for its side effect BEFORE your first render — it must run before the component mounts.\n\n```tsx\nimport '@kitn.ai/ui/elements' // registers <kai-*> — REQUIRED, must come first\nimport { Chat } from '@kitn.ai/ui/react'\nimport '@kitn.ai/ui/theme.css'\n```\n\nIn plain HTML: `import '@kitn.ai/ui/elements'` in your module script. The import is a side effect — keep it even if your linter flags it as \"unused\"."
2030
+ },
2031
+ {
2032
+ // Rule 7 — tsc errors inside node_modules/@kitn.ai/ui/src (SolidJS source pulled in)
2033
+ // Source: field-test reports; packaging gap (tracked upstream)
2034
+ id: "tsc-source-pull",
2035
+ test: (t) => /node_modules\/@kitn\.ai\/ui/.test(t) && /tsc|TS2786|cannot\s+be\s+used\s+as\s+a\s+jsx\s+component|Show\b|Portal\b|Dynamic\b|error\s+TS|type\s+error/.test(
2036
+ t
2037
+ ),
2038
+ title: "tsc errors inside node_modules/@kitn.ai/ui/src (SolidJS source compiled under React)",
2039
+ cause: "The package currently ships TypeScript/TSX source, and a type entry value-re-exports from it, so the consumer's `tsc` resolves and compiles the library's SolidJS internals (`src/ui/*.tsx`) under the app's React JSX config — `Show`/`Portal`/`Dynamic` aren't React components, causing TS2786 / \"cannot be used as a JSX component\" errors. `vite`/esbuild build fine (they strip types); only `tsc` breaks. `skipLibCheck` does not help (these are `.tsx` source, not `.d.ts`).",
2040
+ fix: 'Redirect the type resolution for that subpath in your tsconfig (Vite ignores tsconfig `paths`, so runtime is unaffected):\n\n```jsonc\n// tsconfig (app)\n"baseUrl": ".",\n"paths": { "@kitn.ai/ui/elements": ["./src/stubs/kitn-elements.d.ts"] }\n```\n\n```ts\n// src/stubs/kitn-elements.d.ts\nexport {}\n```\n\n(This is a known packaging gap being tracked upstream.)'
2041
+ },
2042
+ {
2043
+ // Rule 8 — fetch('/api/chat') 404 in a Vite SPA (no server-side routes)
2044
+ // Source: field-test reports; common scaffold confusion
2045
+ id: "vite-api-404",
2046
+ test: (t) => {
2047
+ if (/\/api\/chat/.test(t) && /\b404\b|not\s+found/i.test(t)) return true;
2048
+ if (/\bvite\b/.test(t) && /api\s+route|route\s+handler|\bPOST\b.*not\s+work/.test(t)) return true;
2049
+ if (/next\.?js.*route|route.*next\.?js/.test(t) && /\bvite\b/.test(t)) return true;
2050
+ return false;
2051
+ },
2052
+ title: "fetch('/api/chat') 404 — Vite SPA has no server-side API routes",
2053
+ cause: "A plain Vite/CRA React SPA has no server — there are no `/api` routes. A scaffolded Next.js route handler (`export async function POST`) does not run there, so `fetch('/api/chat')` 404s.",
2054
+ fix: "Either run the backend somewhere real, or skip it entirely for local dev:\n\n```ts\n// Option A — use Next.js where route handlers are supported\n// app/api/chat/route.ts: export async function POST(req) { ... }\n\n// Option B — add a Vite dev-server middleware/proxy\n// vite.config.ts: server: { proxy: { '/api': 'http://localhost:3001' } }\n\n// Option C — run a separate Express/Hono server\n// framework: 'express' in your harness config\n\n// Option D — zero-config local dev with mock integration (no backend needed)\n// Use `integration: 'mock'` in the scaffold tool\n```"
2055
+ },
2056
+ {
2057
+ // Rule 5 — SSR / server component / document is not defined
2058
+ // Source: for-ai-agents.mdx (client-only import); context7.json rule 2 (property rule requires DOM)
2059
+ id: "ssr-server-component",
2060
+ test: (t) => {
2061
+ if (/\bssr\b|server\s+component|document\s+is\s+not\s+defined|window\s+is\s+not\s+defined|next\.?js.*server|server.*next\.?js/.test(t)) return true;
2062
+ if (/hydration/.test(t) && /kai|web.?component|custom.?element|<[a-z]+-/.test(t)) return true;
2063
+ return false;
2064
+ },
2065
+ title: "SSR / server-side rendering — element requires the browser DOM",
2066
+ cause: '`kai-*` elements are client-side web components. They require `document` and `customElements` to register and render. Importing them in a server component (Next.js App Router server component, Nuxt SSR, etc.) throws "document is not defined" or silently produces no output.',
2067
+ fix: "Register the element on the client only. Use your framework's \"client-only\" / island / dynamic-import pattern.\n\n```js\n// ✅ Plain HTML / vanilla — import in a <script type=\"module\">\nimport '@kitn.ai/ui/elements';\n\n// ✅ Next.js App Router — mark the component with \"use client\"\n'use client';\nimport '@kitn.ai/ui/elements';\n\n// ✅ Next.js — dynamic import with ssr: false\nimport dynamic from 'next/dynamic';\nconst KaiChat = dynamic(() => import('@kitn.ai/ui/elements').then(() => 'kai-chat'), { ssr: false });\n\n// ✅ React wrapper (already client-safe)\nimport { Chat } from '@kitn.ai/ui/react';\n```"
2068
+ }
2069
+ ];
2070
+ function buildText(matched) {
2071
+ if (matched.length === 0) {
2072
+ return "No known failure pattern matched. Suggested next steps:\n\n1. Use the `component_reference` tool to look up the real API for the element (prop names, event names, attribute vs. property distinction).\n2. Check the **Streaming recipe** in `llms-full.txt` (`node_modules/@kitn.ai/ui/llms-full.txt` or https://ui.kitn.ai/llms-full.txt) for correct streaming wiring.\n3. Paste `https://ui.kitn.ai/llms.txt` into your prompt for a compact orientation.";
2073
+ }
2074
+ const sections = matched.map((rule, i) => {
2075
+ const n = i + 1;
2076
+ return `## ${n}. ${rule.title}
2077
+
2078
+ **Cause:** ${rule.cause}
2079
+
2080
+ **Fix:** ${rule.fix}`;
2081
+ });
2082
+ const header = matched.length === 1 ? "1 likely cause found:\n\n" : `${matched.length} likely causes found:
2083
+
2084
+ `;
2085
+ return header + sections.join("\n\n---\n\n");
2086
+ }
2087
+ const debug = {
2088
+ name: "debug",
2089
+ description: "Diagnose common AI/UI (kai-*) integration issues. Provide a `symptom` (prose description) and/or a `snippet` (code) and receive the likely cause + fix for classic kai-* failure modes.",
2090
+ inputSchema: z.object({
2091
+ symptom: z.string().optional(),
2092
+ snippet: z.string().optional()
2093
+ }),
2094
+ handler: async (args) => {
2095
+ const combined = `${args.symptom ?? ""} ${args.snippet ?? ""}`;
2096
+ const matched = RULES.filter((rule) => rule.test(combined));
2097
+ return {
2098
+ content: [{ type: "text", text: buildText(matched) }]
2099
+ };
2100
+ }
2101
+ };
2102
+ const tools = [reference, scaffold, theme, debug];
2103
+ function createServer() {
2104
+ const server = new Server(
2105
+ { name: "@kitn.ai/ui", version: "0.15.0" },
2106
+ { capabilities: { tools: {} } }
2107
+ );
2108
+ const byName = new Map(tools.map((t) => [t.name, t]));
2109
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2110
+ tools: tools.map((t) => ({
2111
+ name: t.name,
2112
+ description: t.description,
2113
+ // The protocol requires a JSON Schema. We use zod 4's native converter
2114
+ // (the repo is on zod 4); the SDK-bundled zod-to-json-schema only
2115
+ // understands zod 3 and emits an empty schema for our zod 4 objects.
2116
+ inputSchema: z.toJSONSchema(t.inputSchema)
2117
+ }))
2118
+ }));
2119
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2120
+ const tool = byName.get(request.params.name);
2121
+ if (!tool) {
2122
+ return {
2123
+ isError: true,
2124
+ content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }]
2125
+ };
2126
+ }
2127
+ return tool.handler(request.params.arguments ?? {});
2128
+ });
2129
+ server.__listToolsForTest = () => tools.map((t) => t.name);
2130
+ return server;
2131
+ }
2132
+ async function startStdioServer() {
2133
+ const server = createServer();
2134
+ const transport = new StdioServerTransport();
2135
+ await server.connect(transport);
2136
+ }
2137
+ startStdioServer();
2138
+ export {
2139
+ startStdioServer
2140
+ };