@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
@@ -0,0 +1,1627 @@
1
+ import { z } from 'zod';
2
+ import type { Tool } from './types';
3
+ import { Placement, Framework } from '../../types';
4
+ import type { Integration, Archetype } from '../../types';
5
+ import {
6
+ getArchetype,
7
+ getIntegration,
8
+ listArchetypes,
9
+ listIntegrations,
10
+ } from '../../registry';
11
+
12
+ /**
13
+ * scaffold — the keystone tool. Composes a working chat surface from four axes:
14
+ *
15
+ * useCase (archetype) × integration × placement × framework
16
+ *
17
+ * and emits three labeled blocks an AI consumer can paste straight in:
18
+ * (1) Front-end — the archetype's kai-* components, rendered for the chosen
19
+ * framework and sized for the placement, wired with the
20
+ * `messages` property + `kai-submit` per the Streaming recipe.
21
+ * (2) Backend — the integration's route template for the framework (with a
22
+ * language-aware fallback when there's no exact match).
23
+ * (3) Run note — how to run it + the env vars to set.
24
+ *
25
+ * The handler is called directly in tests (bypassing MCP's zod validation), so it
26
+ * validates `useCase` + `integration` against the registry itself and returns
27
+ * graceful, self-correcting error text when either is unknown.
28
+ */
29
+
30
+ const text = (s: string) => ({
31
+ content: [{ type: 'text' as const, text: s }],
32
+ });
33
+
34
+ // ── placement sizing ──────────────────────────────────────────────────────────
35
+
36
+ interface PlacementStyle {
37
+ /** container style for an HTML/web-components wrapper */
38
+ style: string;
39
+ /** the chat element's own style — it must FILL the container */
40
+ chatFill: string;
41
+ /** one-line human description of the layout */
42
+ note: string;
43
+ /** optional extra comment lines describing an alternative layout form */
44
+ altNote?: string;
45
+ }
46
+
47
+ // The chat element must fill its container. In a `display: flex; flex-direction:
48
+ // column` shell it's a flex child (`flex: 1; min-height: 0`); in a plain block
49
+ // container it fills via `height: 100%`.
50
+ const FLEX_FILL = 'flex: 1; min-height: 0;';
51
+ const BLOCK_FILL = 'height: 100%; width: 100%;';
52
+
53
+ function placementStyle(placement: string): PlacementStyle {
54
+ switch (placement) {
55
+ case 'full-page':
56
+ return {
57
+ style: 'height: 100dvh; width: 100%; display: flex; flex-direction: column;',
58
+ chatFill: FLEX_FILL,
59
+ note: 'fills the viewport (100dvh)',
60
+ };
61
+ case 'inline':
62
+ return {
63
+ style: 'width: 100%; max-width: 720px; height: 540px; margin: 0 auto; display: flex; flex-direction: column;',
64
+ chatFill: FLEX_FILL,
65
+ note: 'in-flow block (sized by parent, not fixed)',
66
+ };
67
+ case 'side':
68
+ // A full-height side panel docked to the trailing (right) edge — overlays
69
+ // content. messages/loading/suggestions are real kai-chat props.
70
+ return {
71
+ style:
72
+ 'position: fixed; top: 0; inset-inline-end: 0; height: 100dvh; width: 380px; ' +
73
+ 'border-inline-start: 1px solid var(--kai-color-border); display: flex; flex-direction: column; z-index: 1000;',
74
+ chatFill: FLEX_FILL,
75
+ note: 'full-height side panel, docked to the trailing edge (100dvh)',
76
+ altNote:
77
+ 'In-flow alternative (push content instead of overlay): drop `position`/`z-index` and ' +
78
+ 'make this a `flex: 0 0 380px` column inside a `display: flex` row at `height: 100dvh`.',
79
+ };
80
+ case 'docked-widget':
81
+ // The bottom-right floating bubble — rounded, elevated, fixed size.
82
+ return {
83
+ style:
84
+ 'position: fixed; bottom: 1.5rem; inset-inline-end: 1.5rem; width: 380px; height: 600px; ' +
85
+ 'max-height: calc(100dvh - 3rem); border-radius: 16px; overflow: hidden; ' +
86
+ 'box-shadow: 0 12px 32px var(--kai-shadow-color, rgba(0,0,0,0.18)); display: flex; flex-direction: column; z-index: 1000;',
87
+ chatFill: FLEX_FILL,
88
+ note: 'fixed, floating bottom-right widget',
89
+ };
90
+ default:
91
+ // Unknown placement falls back to full-page (full height) rather than the bubble,
92
+ // so a future Placement enum member doesn't silently render as a widget.
93
+ return {
94
+ style: 'height: 100dvh; width: 100%; display: flex; flex-direction: column;',
95
+ chatFill: FLEX_FILL,
96
+ note: 'fills the viewport (100dvh)',
97
+ };
98
+ }
99
+ }
100
+
101
+ // ── suggestions + mock streaming ───────────────────────────────────────────────
102
+
103
+ /** Default starter prompts so the suggestions feature always shows. */
104
+ const DEFAULT_SUGGESTIONS = ["What's new?", 'How can you help?'];
105
+
106
+ /** A canned assistant reply the mock integration streams back token-by-token. */
107
+ const MOCK_REPLY =
108
+ "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.";
109
+
110
+ /** Render a string[] as a JS array literal (JSON-quoted — keeps apostrophes readable). */
111
+ function jsArray(items: string[]): string {
112
+ return '[' + items.map((s) => JSON.stringify(s)).join(', ') + ']';
113
+ }
114
+
115
+ /**
116
+ * The shared client-side mock stream body, parameterised by how each framework
117
+ * commits a messages update. Two operations keep the contract correct:
118
+ * - `commitInitial(expr)` appends the user + empty-assistant pair.
119
+ * - `commitMap(mapBody)` replaces messages with `prev.map((m) => mapBody)` —
120
+ * each framework supplies how `prev` resolves (the React functional updater,
121
+ * or the live local variable for html/vue/svelte) so the streamed content is
122
+ * applied to the LATEST array, never a stale snapshot.
123
+ * Each commit produces a NEW array (and a new object for the streamed message)
124
+ * so kai-chat re-renders per chunk.
125
+ *
126
+ * Indented with `pad` so it drops cleanly into each framework's onSubmit.
127
+ */
128
+ function mockStreamBody(opts: {
129
+ pad: string;
130
+ /** read the current messages array (for building `history`) */
131
+ read: string;
132
+ /** commit the initial user + empty-assistant pair */
133
+ commitInitial: (expr: string) => string;
134
+ /** commit a `prev.map(...)` update; `mapBody` is the body of `.map((m) => …)` */
135
+ commitMap: (mapBody: string) => string;
136
+ /** set loading true/false */
137
+ setLoading: (v: 'true' | 'false') => string;
138
+ /**
139
+ * Emit `as const` on role literals so they narrow to 'user'|'assistant' under
140
+ * strict TS. Set to true for TypeScript frameworks (react/next); false for
141
+ * plain-JS contexts (html) where `as const` is invalid syntax.
142
+ */
143
+ strictRoles?: boolean;
144
+ }): string {
145
+ const { pad, read, commitInitial, commitMap, setLoading, strictRoles = false } = opts;
146
+ const asConst = strictRoles ? ' as const' : '';
147
+ const mapBody = `(m.id === assistantId ? { ...m, content: answer } : m)`;
148
+ return [
149
+ `${pad}const value = e.detail.value.trim();`,
150
+ `${pad}if (!value) return;`,
151
+ `${pad}const history = [...${read}, { id: crypto.randomUUID(), role: 'user'${asConst}, content: value }];`,
152
+ `${pad}const assistantId = crypto.randomUUID();`,
153
+ `${pad}${commitInitial(`[...history, { id: assistantId, role: 'assistant'${asConst}, content: '' }]`)}`,
154
+ `${pad}${setLoading('true')}`,
155
+ `${pad}// No backend: stream a canned reply client-side, one token at a time.`,
156
+ `${pad}const reply = ${JSON.stringify(MOCK_REPLY)};`,
157
+ `${pad}const tokens = reply.split(/(\\s+)/);`,
158
+ `${pad}let answer = '';`,
159
+ `${pad}for (const tok of tokens) {`,
160
+ `${pad} await new Promise((r) => setTimeout(r, 24));`,
161
+ `${pad} answer += tok;`,
162
+ `${pad} // new array + object reference per chunk so kai-chat re-renders`,
163
+ `${pad} ${commitMap(mapBody)}`,
164
+ `${pad}}`,
165
+ `${pad}${setLoading('false')}`,
166
+ ].join('\n');
167
+ }
168
+
169
+ // ── SCAF-8: per-integration default model ids ─────────────────────────────────
170
+
171
+ /**
172
+ * Return a sensible default model id for integrations whose route forwards a
173
+ * `model` field to the upstream provider. The dev can change this const.
174
+ * Returns undefined for integrations whose route does not use a model param.
175
+ */
176
+ function defaultModelFor(integration: Integration): string | undefined {
177
+ // Detect: any route template destructures `model` from the request body.
178
+ const routeSrc = Object.values(integration.routeTemplates).join('\n');
179
+ if (!routeSrc.includes('model')) return undefined;
180
+
181
+ const defaults: Record<string, string> = {
182
+ openrouter: 'openai/gpt-4o-mini',
183
+ ollama: 'llama3.2',
184
+ 'vercel-ai-sdk': 'openai/gpt-4o-mini',
185
+ cloudflare: 'openai/gpt-4o-mini',
186
+ };
187
+ return defaults[integration.id] ?? 'openai/gpt-4o-mini';
188
+ }
189
+
190
+ // ── SCAF-9: message-embedded companion logic ──────────────────────────────────
191
+
192
+ /**
193
+ * Tags that live INSIDE a kai-chat message object (not standalone siblings).
194
+ * Rendering them as bare elements is a TS error (required props missing) and
195
+ * non-idiomatic — the chat thread carries them on each message.
196
+ */
197
+ const MESSAGE_EMBEDDED_TAGS = new Set(['kai-tool', 'kai-reasoning']);
198
+
199
+ // ── SCAF-14: workspace structural/layout logic ────────────────────────────────
200
+
201
+ /**
202
+ * Tags that participate in the workspace layout structure — kai-resizable is the
203
+ * container (needs kai-resizable-item children), kai-artifact is the preview pane.
204
+ * Neither should be emitted as a bare sibling of kai-chat — the idiomatic structure
205
+ * is a resizable split with chat in one pane and artifact in another.
206
+ */
207
+ const WORKSPACE_STRUCTURAL_TAGS = new Set(['kai-resizable', 'kai-artifact']);
208
+
209
+ /** True when the archetype is the resizable split workspace (chat + artifact). */
210
+ function isWorkspace(archetype: Archetype): boolean {
211
+ return archetype.components.includes('kai-resizable') && archetype.components.includes('kai-artifact');
212
+ }
213
+
214
+ /**
215
+ * A sample assistant message that demonstrates embedded tool + reasoning so the
216
+ * agentic archetype renders correctly out of the box.
217
+ */
218
+ const SAMPLE_AGENTIC_MESSAGE = {
219
+ id: 'sample-assistant',
220
+ role: 'assistant' as const,
221
+ content: 'Searched the web for current pricing.',
222
+ reasoning: { text: 'I should call the search tool to get up-to-date data.' },
223
+ tools: [
224
+ {
225
+ type: 'search',
226
+ state: 'output-available' as const,
227
+ input: { query: 'current pricing' },
228
+ output: { results: ['Result A', 'Result B'] },
229
+ toolCallId: 'tc_001',
230
+ },
231
+ ],
232
+ };
233
+
234
+ // ── front-end rendering ───────────────────────────────────────────────────────
235
+
236
+ interface RenderCtx {
237
+ p: PlacementStyle;
238
+ emptyHint: string;
239
+ suggestions: string[];
240
+ /** mock = stream the reply client-side (no fetch, no backend, no key) */
241
+ isMock: boolean;
242
+ /** SCAF-8: non-undefined when the integration forwards a model param */
243
+ defaultModel?: string;
244
+ }
245
+
246
+ /** The kai-* tags for the archetype, in order, as opening/closing markup.
247
+ *
248
+ * SCAF-9: message-embedded companion types (kai-tool, kai-reasoning) are NOT
249
+ * emitted as standalone siblings — they live inside a kai-chat message object.
250
+ * Only standalone companions (kai-sources, etc.) are rendered here with sample data.
251
+ *
252
+ * SCAF-14: workspace structural types (kai-resizable, kai-artifact) are emitted
253
+ * as a properly composed split layout — chat in one pane, artifact in the other.
254
+ */
255
+ function componentTags(archetype: Archetype, chatFill: string): string {
256
+ // SCAF-14: workspace is a structural/layout archetype — emit a runnable split.
257
+ if (isWorkspace(archetype)) {
258
+ return [
259
+ ` <!-- SCAF-14: workspace split — chat pane left, artifact preview right. -->`,
260
+ ` <!-- kai-resizable needs kai-resizable-item children to render panels. -->`,
261
+ ` <kai-resizable orientation="horizontal" style="display:block;width:100%;height:100%">`,
262
+ ` <kai-resizable-item size="40%" min="240px">`,
263
+ ` <kai-chat id="chat" suggestion-mode="submit" style="${chatFill}"></kai-chat>`,
264
+ ` </kai-resizable-item>`,
265
+ ` <kai-resizable-item min="280px">`,
266
+ ` <!-- Replace src with your artifact URL or set .files for multi-file preview. -->`,
267
+ ` <kai-artifact id="artifact" src="https://example.com" style="width:100%;height:100%"></kai-artifact>`,
268
+ ` </kai-resizable-item>`,
269
+ ` </kai-resizable>`,
270
+ ].join('\n');
271
+ }
272
+
273
+ const companionTags = archetype.components.filter(
274
+ (t) => t !== 'kai-chat' && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t),
275
+ );
276
+ const hasEmbedded = archetype.components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
277
+ const hasStandaloneCompanions = companionTags.length > 0;
278
+
279
+ const lines: string[] = [];
280
+ lines.push(` <kai-chat id="chat" suggestion-mode="submit" style="${chatFill}"></kai-chat>`);
281
+
282
+ if (hasEmbedded) {
283
+ lines.push(
284
+ ` <!-- kai-tool / kai-reasoning render INSIDE the thread, not as siblings.`,
285
+ ` Seed messages with { tools: [...], reasoning: { text: '...' } } — see the sample in the script below. -->`,
286
+ );
287
+ }
288
+
289
+ for (const tag of companionTags) {
290
+ if (tag === 'kai-sources') {
291
+ // kai-sources is genuinely standalone — emit with realistic sample data.
292
+ lines.push(
293
+ ` <!-- Replace sampleSources with your data. -->`,
294
+ ` <kai-sources id="sources"></kai-sources>`,
295
+ );
296
+ } else {
297
+ lines.push(` <${tag}></${tag}>`);
298
+ }
299
+ }
300
+
301
+ if (hasStandaloneCompanions) {
302
+ lines.push(` <!-- wire data props — see the component_reference MCP tool -->`);
303
+ }
304
+
305
+ return lines.join('\n');
306
+ }
307
+
308
+ /** The HTML <script> wiring — mock streams client-side; everything else fetches /api/chat. */
309
+ function htmlWiring(ctx: RenderCtx, archetype: Archetype): string {
310
+ const hasEmbedded = archetype.components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
311
+ const hasSources = archetype.components.includes('kai-sources');
312
+
313
+ // SCAF-9: seed the sample agentic message so tool+reasoning render immediately.
314
+ const seedLines = hasEmbedded
315
+ ? [
316
+ ` // SCAF-9: tool calls + reasoning render INSIDE the thread — set them on the message object.`,
317
+ ` // Replace this sample with real messages from your backend.`,
318
+ ` chat.messages = [${JSON.stringify(SAMPLE_AGENTIC_MESSAGE, null, 0)}];`,
319
+ ``,
320
+ ]
321
+ : [];
322
+
323
+ const sourcesSetupLines = hasSources
324
+ ? [
325
+ ` const sourcesEl = document.getElementById('sources');`,
326
+ ` // Replace with your real source data (set as a JS property — it's an array).`,
327
+ ` const sampleSources = [`,
328
+ ` { href: 'https://example.com/doc1', title: 'Getting started', description: 'Overview of the product.' },`,
329
+ ` { href: 'https://example.com/doc2', title: 'API reference', description: 'Full API documentation.' },`,
330
+ ` ];`,
331
+ ` sourcesEl.sources = sampleSources;`,
332
+ ``,
333
+ ]
334
+ : [];
335
+
336
+ const head = [
337
+ ` <script type="module">`,
338
+ ` import '@kitn.ai/ui/elements'; // registers <kai-*> — required, must come first`,
339
+ ` import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
340
+ ``,
341
+ ` // Guard: module scripts run before the DOM is ready when inlined in <head>.`,
342
+ ` // DOMContentLoaded fires synchronously when already loaded; otherwise waits.`,
343
+ ` async function init() {`,
344
+ ` const chat = document.getElementById('chat');`,
345
+ ` // SCAF-15: kai-* register via an async dynamic import (SSR-safety), so the`,
346
+ ` // element may not be upgraded yet. Wait for the upgrade before setting any`,
347
+ ` // array/object property — values set pre-upgrade are dropped on upgrade.`,
348
+ ` await customElements.whenDefined('kai-chat');`,
349
+ ` // suggestions is a JS PROPERTY (arrays can't be HTML attributes)`,
350
+ ` chat.suggestions = ${jsArray(ctx.suggestions)};`,
351
+ ` chat.suggestionMode = 'submit';`,
352
+ ``,
353
+ ...seedLines.map((l) => (l.trim() === '' ? l : ` ${l}`)),
354
+ ...sourcesSetupLines.map((l) => (l.trim() === '' ? l : ` ${l}`)),
355
+ ];
356
+
357
+ // DOMContentLoaded footer — closes init() and wires it safely.
358
+ const domReadyFooter = [
359
+ ` }`,
360
+ ` if (document.readyState === 'loading') {`,
361
+ ` document.addEventListener('DOMContentLoaded', init);`,
362
+ ` } else {`,
363
+ ` init();`,
364
+ ` }`,
365
+ ];
366
+
367
+ if (ctx.isMock) {
368
+ const body = mockStreamBody({
369
+ pad: ' ',
370
+ read: 'chat.messages',
371
+ commitInitial: (expr) => `chat.messages = ${expr};`,
372
+ // chat.messages is live (no React snapshot) — map over it directly
373
+ commitMap: (mapBody) => `chat.messages = chat.messages.map((m) => ${mapBody});`,
374
+ setLoading: (v) => `chat.loading = ${v};`,
375
+ });
376
+ return [
377
+ ...head,
378
+ ` // No backend: stream a canned reply client-side (no fetch, no API key).`,
379
+ ` chat.addEventListener('kai-submit', async (e) => {`,
380
+ body,
381
+ ` });`,
382
+ ...domReadyFooter,
383
+ ` </script>`,
384
+ ].join('\n');
385
+ }
386
+
387
+ // SCAF-8: include model in the POST body when the integration forwards it.
388
+ const modelLines = ctx.defaultModel
389
+ ? [
390
+ ` // SCAF-8: change this model id to any provider/model string you want to use.`,
391
+ ` const model = '${ctx.defaultModel}';`,
392
+ ``,
393
+ ]
394
+ : [];
395
+ const bodyPayload = ctx.defaultModel
396
+ ? `{ model, messages: history.map((m) => ({ role: m.role, content: m.content })) }`
397
+ : `{ messages: history.map((m) => ({ role: m.role, content: m.content })) }`;
398
+
399
+ return [
400
+ ...head,
401
+ ` chat.addEventListener('kai-submit', async (e) => {`,
402
+ ` const value = e.detail.value.trim();`,
403
+ ` if (!value) return;`,
404
+ ``,
405
+ ...modelLines,
406
+ ` // messages is a JS PROPERTY (objects can't be HTML attributes)`,
407
+ ` const history = [...chat.messages, { id: crypto.randomUUID(), role: 'user', content: value }];`,
408
+ ` const assistantId = crypto.randomUUID();`,
409
+ ` chat.messages = [...history, { id: assistantId, role: 'assistant', content: '' }];`,
410
+ ` chat.loading = true;`,
411
+ ``,
412
+ ` const res = await fetch('/api/chat', {`,
413
+ ` method: 'POST',`,
414
+ ` headers: { 'Content-Type': 'application/json' },`,
415
+ ` body: JSON.stringify(${bodyPayload}),`,
416
+ ` });`,
417
+ ``,
418
+ ` // Read the OpenAI-format SSE and stream it into the assistant message.`,
419
+ ` // This loop is the Streaming recipe — copy its exact body if you need keep-alive handling.`,
420
+ ` const reader = res.body.getReader();`,
421
+ ` const decoder = new TextDecoder();`,
422
+ ` let buffer = '', answer = '';`,
423
+ ` while (true) {`,
424
+ ` const { value: chunk, done } = await reader.read();`,
425
+ ` if (done) break;`,
426
+ ` buffer += decoder.decode(chunk, { stream: true });`,
427
+ ` const lines = buffer.split('\\n');`,
428
+ ` buffer = lines.pop();`,
429
+ ` for (const line of lines) {`,
430
+ ` const s = line.trim();`,
431
+ ` if (!s.startsWith('data:')) continue;`,
432
+ ` const payload = s.slice(5).trim();`,
433
+ ` if (payload === '[DONE]') continue;`,
434
+ ` try {`,
435
+ ` const delta = JSON.parse(payload).choices?.[0]?.delta?.content;`,
436
+ ` if (!delta) continue;`,
437
+ ` answer += delta;`,
438
+ ` chat.messages = chat.messages.map((m) => (m.id === assistantId ? { ...m, content: answer } : m));`,
439
+ ` } catch { /* skip keep-alive lines */ }`,
440
+ ` }`,
441
+ ` }`,
442
+ ` chat.loading = false;`,
443
+ ` });`,
444
+ ...domReadyFooter,
445
+ ` </script>`,
446
+ ].join('\n');
447
+ }
448
+
449
+ function renderHtml(archetype: Archetype, ctx: RenderCtx): string {
450
+ const { p, emptyHint } = ctx;
451
+ return [
452
+ `<!-- ${archetype.title} — ${p.note} -->`,
453
+ ...(p.altNote ? [`<!-- ${p.altNote} -->`] : []),
454
+ `<div style="${p.style}">`,
455
+ componentTags(archetype, p.chatFill),
456
+ `</div>`,
457
+ ``,
458
+ htmlWiring(ctx, archetype),
459
+ ``,
460
+ ` <!-- empty-state hint: ${emptyHint} -->`,
461
+ ].join('\n');
462
+ }
463
+
464
+ /** Convert a kebab-case custom-element tag to its PascalCase React wrapper name. */
465
+ function toPascalCase(tag: string): string {
466
+ // e.g. "kai-chat" → "Chat", "kai-sources" → "Sources"
467
+ return tag
468
+ .replace(/^kai-/, '')
469
+ .split('-')
470
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
471
+ .join('');
472
+ }
473
+
474
+ /** JSX usage for react/next: uses the official @kitn.ai/ui/react wrappers. */
475
+ function renderJsx(archetype: Archetype, ctx: RenderCtx, framework: string): string {
476
+ const { p, emptyHint, suggestions, isMock, defaultModel } = ctx;
477
+
478
+ const hasEmbedded = archetype.components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
479
+ const workspace = isWorkspace(archetype);
480
+
481
+ // SCAF-9: exclude message-embedded tags from import list.
482
+ // SCAF-14: workspace uses Resizable+ResizableItem+Artifact — keep them in the import list.
483
+ const renderableTags = archetype.components.filter((t) => !MESSAGE_EMBEDDED_TAGS.has(t));
484
+ // For workspace: replace 'kai-resizable' with 'kai-resizable-item' so we get ResizableItem too.
485
+ const importTags = workspace
486
+ ? [...new Set([...renderableTags.filter((t) => t !== 'kai-resizable'), 'kai-resizable', 'kai-resizable-item'])]
487
+ : renderableTags;
488
+ const wrapperNames = importTags.map(toPascalCase);
489
+ const importList = wrapperNames.join(', ');
490
+
491
+ // SCAF-9: standalone companion tags (not kai-chat, not message-embedded, not workspace-structural).
492
+ const standaloneCompanionTags = archetype.components.filter(
493
+ (t) => t !== 'kai-chat' && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t),
494
+ );
495
+
496
+ // Build companion JSX: only standalone companions with real props.
497
+ // SCAF-14: workspace gets its own structural JSX block (not companion lines).
498
+ const companionJsxLines: string[] = [];
499
+ if (hasEmbedded) {
500
+ companionJsxLines.push(
501
+ ` {/* kai-tool / kai-reasoning render inside the thread. Tool calls + reasoning`,
502
+ ` are set on each message object — see the sampleMessages initializer above. */}`,
503
+ );
504
+ }
505
+ for (const t of standaloneCompanionTags) {
506
+ if (t === 'kai-sources') {
507
+ companionJsxLines.push(
508
+ ` {/* Replace sampleSources with your real data. */}`,
509
+ ` <Sources sources={sampleSources} />`,
510
+ );
511
+ } else {
512
+ companionJsxLines.push(` {/* wire data props — see the component_reference MCP tool */}`);
513
+ companionJsxLines.push(` <${toPascalCase(t)} />`);
514
+ }
515
+ }
516
+ const companions = companionJsxLines.join('\n');
517
+
518
+ // SCAF-4: Inline ChatMessage type for strict-TS consumers; avoids implicit-any on useState/handler.
519
+ // SCAF-11: state must be the library's 4-value union (not bare string); reasoning carries optional label.
520
+ 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 }[] };`;
521
+
522
+ // SCAF-9: seed sample messages for agentic archetype so tool+reasoning render immediately.
523
+ const sampleMessagesInit = hasEmbedded
524
+ ? [
525
+ ` // SCAF-9: tool calls and reasoning render inside the thread — set them on the message object.`,
526
+ ` // Replace with real messages streamed from your backend.`,
527
+ ` const sampleMessages: ChatMessage[] = [${JSON.stringify(SAMPLE_AGENTIC_MESSAGE)}];`,
528
+ ` const [messages, setMessages] = useState<ChatMessage[]>(sampleMessages);`,
529
+ ].join('\n')
530
+ : ` const [messages, setMessages] = useState<ChatMessage[]>([]);`;
531
+
532
+ // SCAF-9: sample sources data for knowledge-base archetype.
533
+ const sampleSourcesInit =
534
+ standaloneCompanionTags.includes('kai-sources')
535
+ ? [
536
+ ` // Replace sampleSources with your real source data.`,
537
+ ` const sampleSources = [`,
538
+ ` { href: 'https://example.com/doc1', title: 'Getting started', description: 'Overview of the product.' },`,
539
+ ` { href: 'https://example.com/doc2', title: 'API reference', description: 'Full API documentation.' },`,
540
+ ` ];`,
541
+ ].join('\n')
542
+ : '';
543
+
544
+ // SCAF-8: model const for integrations that forward model to the upstream provider.
545
+ const modelInit = defaultModel
546
+ ? ` // SCAF-8: change this to any provider/model string you want to use.\n const model = '${defaultModel}';`
547
+ : '';
548
+ const bodyPayload = defaultModel
549
+ ? `{ model, messages: history.map((m) => ({ role: m.role, content: m.content })) }`
550
+ : `{ messages: history.map((m) => ({ role: m.role, content: m.content })) }`;
551
+
552
+ // onSubmit body: mock streams a canned reply client-side; otherwise fetch /api/chat.
553
+ const onSubmitBody = isMock
554
+ ? mockStreamBody({
555
+ pad: ' ',
556
+ read: 'messages',
557
+ commitInitial: (expr) => `setMessages(${expr});`,
558
+ // functional updater so each token maps over the LATEST array, not the snapshot
559
+ commitMap: (mapBody) => `setMessages((prev) => prev.map((m) => ${mapBody}));`,
560
+ setLoading: (v) => `setLoading(${v});`,
561
+ strictRoles: true,
562
+ })
563
+ : [
564
+ ` const value = e.detail.value.trim();`,
565
+ ` if (!value) return;`,
566
+ ` const history: ChatMessage[] = [...messages, { id: crypto.randomUUID(), role: 'user' as const, content: value }];`,
567
+ ` const assistantId = crypto.randomUUID();`,
568
+ ` setMessages([...history, { id: assistantId, role: 'assistant' as const, content: '' }]);`,
569
+ ` setLoading(true);`,
570
+ ` const res = await fetch('/api/chat', {`,
571
+ ` method: 'POST',`,
572
+ ` headers: { 'Content-Type': 'application/json' },`,
573
+ ` body: JSON.stringify(${bodyPayload}),`,
574
+ ` });`,
575
+ ` // Stream the OpenAI-format SSE into the assistant message — see the Streaming recipe.`,
576
+ ` const reader = res.body!.getReader();`,
577
+ ` const decoder = new TextDecoder();`,
578
+ ` let buffer = '', answer = '';`,
579
+ ` while (true) {`,
580
+ ` const { value: chunk, done } = await reader.read();`,
581
+ ` if (done) break;`,
582
+ ` buffer += decoder.decode(chunk, { stream: true });`,
583
+ ` const lines = buffer.split('\\n');`,
584
+ ` buffer = lines.pop()!;`,
585
+ ` for (const line of lines) {`,
586
+ ` const s = line.trim();`,
587
+ ` if (!s.startsWith('data:')) continue;`,
588
+ ` const payload = s.slice(5).trim();`,
589
+ ` if (payload === '[DONE]') continue;`,
590
+ ` try {`,
591
+ ` const delta = JSON.parse(payload).choices?.[0]?.delta?.content;`,
592
+ ` if (!delta) continue;`,
593
+ ` answer += delta;`,
594
+ ` setMessages((ms) => ms.map((m) => (m.id === assistantId ? { ...m, content: answer } : m)));`,
595
+ ` } catch { /* skip keep-alives */ }`,
596
+ ` }`,
597
+ ` }`,
598
+ ` setLoading(false);`,
599
+ ].join('\n');
600
+
601
+ // SCAF-2: Next.js App Router requires 'use client' for components that use hooks/interactivity.
602
+ const useClientDirective = framework === 'next' ? [`'use client';`, ``] : [];
603
+
604
+ // SCAF-6: For Next.js ONLY — use next/dynamic with { ssr: false } so the elements
605
+ // bundle (which calls delegateEvents(events, doc = window.document) at module-eval)
606
+ // never runs on the server during prerender → avoids "window is not defined" crash.
607
+ // Plain `react` (Vite) has no SSR and keeps the top-level imports unchanged.
608
+ if (framework === 'next') {
609
+ // Build dynamic() calls for every renderable wrapper in the archetype.
610
+ const dynamicImports = wrapperNames.map(
611
+ (name) =>
612
+ `const ${name} = dynamic(() => import('@kitn.ai/ui/react').then((m) => m.${name}), { ssr: false });`,
613
+ );
614
+
615
+ // SCAF-2: Next.js config note — @kitn.ai/ui ships compiled entry points; no transpilePackages needed.
616
+ const nextConfigNote: string[] = [];
617
+
618
+ return [
619
+ // 'use client' must be the very first line for Next.js App Router.
620
+ ...useClientDirective,
621
+ `import { useState } from 'react';`,
622
+ `import dynamic from 'next/dynamic';`,
623
+ `import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
624
+ `// kai-* bundle Solid's client runtime → load client-only so SSR/prerender doesn't crash`,
625
+ ...dynamicImports,
626
+ ``,
627
+ ...nextConfigNote,
628
+ `// ${archetype.title} — ${p.note}. empty-state hint: ${emptyHint}`,
629
+ ...(p.altNote ? [`// ${p.altNote}`] : []),
630
+ chatMessageType,
631
+ ``,
632
+ `export default function App() {`,
633
+ sampleMessagesInit,
634
+ ` const [loading, setLoading] = useState(false);`,
635
+ ` const suggestions = ${jsArray(suggestions)};`,
636
+ ...(sampleSourcesInit ? [sampleSourcesInit] : []),
637
+ ...(modelInit ? [modelInit] : []),
638
+ ``,
639
+ ` async function onSubmit(e: CustomEvent<{ value: string }>) {`,
640
+ onSubmitBody,
641
+ ` }`,
642
+ ``,
643
+ ` return (`,
644
+ ` <div style={{ ${jsxStyle(p.style)} }}>`,
645
+ ...(workspace
646
+ ? [
647
+ ` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
648
+ ` {/* Resizable needs ResizableItem children to render panels. */}`,
649
+ ` <Resizable orientation="horizontal" style={{ display: 'block', width: '100%', height: '100%' }}>`,
650
+ ` <ResizableItem size="40%" min="240px">`,
651
+ ` <Chat`,
652
+ ` messages={messages}`,
653
+ ` loading={loading}`,
654
+ ` suggestions={suggestions}`,
655
+ ` suggestionMode="submit"`,
656
+ ` onSubmit={onSubmit}`,
657
+ ` style={{ ${jsxStyle(p.chatFill)} }}`,
658
+ ` />`,
659
+ ` </ResizableItem>`,
660
+ ` <ResizableItem min="280px">`,
661
+ ` {/* Replace src with your artifact URL or set files for multi-file preview. */}`,
662
+ ` <Artifact src="https://example.com" style={{ width: '100%', height: '100%' }} />`,
663
+ ` </ResizableItem>`,
664
+ ` </Resizable>`,
665
+ ]
666
+ : [
667
+ ` <Chat`,
668
+ ` messages={messages}`,
669
+ ` loading={loading}`,
670
+ ` suggestions={suggestions}`,
671
+ ` suggestionMode="submit"`,
672
+ ` onSubmit={onSubmit}`,
673
+ ` style={{ ${jsxStyle(p.chatFill)} }}`,
674
+ ` />`,
675
+ companions,
676
+ ]),
677
+ ` </div>`,
678
+ ` );`,
679
+ `}`,
680
+ ]
681
+ .filter((l) => l !== '')
682
+ .join('\n');
683
+ }
684
+
685
+ // SCAF-2: Next.js transpilePackages note (needed until @kitn.ai/ui ships prebuilt JS on "." + "./react").
686
+ const nextConfigNote: string[] = [];
687
+
688
+ return [
689
+ // SCAF-2: 'use client' must be the very first line for Next.js App Router.
690
+ ...useClientDirective,
691
+ // (1) REQUIRED: registers <kai-*> — the react wrappers do NOT auto-register.
692
+ // Must come BEFORE importing the wrappers, or <kai-chat> renders empty.
693
+ `import '@kitn.ai/ui/elements'; // registers <kai-*> — required, must come first`,
694
+ `import { useState } from 'react';`,
695
+ `import { ${importList} } from '@kitn.ai/ui/react';`,
696
+ `import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
697
+ ``,
698
+ ...nextConfigNote,
699
+ `// ${archetype.title} — ${p.note}. empty-state hint: ${emptyHint}`,
700
+ ...(p.altNote ? [`// ${p.altNote}`] : []),
701
+ chatMessageType,
702
+ ``,
703
+ `export default function App() {`,
704
+ sampleMessagesInit,
705
+ ` const [loading, setLoading] = useState(false);`,
706
+ ` const suggestions = ${jsArray(suggestions)};`,
707
+ ...(sampleSourcesInit ? [sampleSourcesInit] : []),
708
+ ...(modelInit ? [modelInit] : []),
709
+ ``,
710
+ ` async function onSubmit(e: CustomEvent<{ value: string }>) {`,
711
+ onSubmitBody,
712
+ ` }`,
713
+ ``,
714
+ ` return (`,
715
+ ` <div style={{ ${jsxStyle(p.style)} }}>`,
716
+ ...(workspace
717
+ ? [
718
+ ` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
719
+ ` {/* Resizable needs ResizableItem children to render panels. */}`,
720
+ ` <Resizable orientation="horizontal" style={{ display: 'block', width: '100%', height: '100%' }}>`,
721
+ ` <ResizableItem size="40%" min="240px">`,
722
+ ` <Chat`,
723
+ ` messages={messages}`,
724
+ ` loading={loading}`,
725
+ ` suggestions={suggestions}`,
726
+ ` suggestionMode="submit"`,
727
+ ` onSubmit={onSubmit}`,
728
+ ` style={{ ${jsxStyle(p.chatFill)} }}`,
729
+ ` />`,
730
+ ` </ResizableItem>`,
731
+ ` <ResizableItem min="280px">`,
732
+ ` {/* Replace src with your artifact URL or set files for multi-file preview. */}`,
733
+ ` <Artifact src="https://example.com" style={{ width: '100%', height: '100%' }} />`,
734
+ ` </ResizableItem>`,
735
+ ` </Resizable>`,
736
+ ]
737
+ : [
738
+ ` <Chat`,
739
+ ` messages={messages}`,
740
+ ` loading={loading}`,
741
+ ` suggestions={suggestions}`,
742
+ ` suggestionMode="submit"`,
743
+ ` onSubmit={onSubmit}`,
744
+ ` style={{ ${jsxStyle(p.chatFill)} }}`,
745
+ ` />`,
746
+ companions,
747
+ ]),
748
+ ` </div>`,
749
+ ` );`,
750
+ `}`,
751
+ ]
752
+ .filter((l) => l !== '')
753
+ .join('\n');
754
+ }
755
+
756
+ /** Vue: bind messages/suggestions as properties, listen for kai-submit with @. */
757
+ function renderVue(archetype: Archetype, ctx: RenderCtx): string {
758
+ const { p, emptyHint, suggestions, isMock, defaultModel } = ctx;
759
+
760
+ // SCAF-9: exclude message-embedded tags from companion rendering.
761
+ // SCAF-14: also exclude workspace structural tags (handled by the workspace block below).
762
+ const workspace = isWorkspace(archetype);
763
+ const standaloneCompanionTags = archetype.components.filter(
764
+ (t) => t !== 'kai-chat' && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t),
765
+ );
766
+ const hasEmbedded = archetype.components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
767
+
768
+ const companionLines: string[] = [];
769
+ if (hasEmbedded) {
770
+ companionLines.push(
771
+ ` <!-- kai-tool / kai-reasoning render INSIDE the thread — set tools/reasoning on each message object. -->`,
772
+ );
773
+ }
774
+ for (const t of standaloneCompanionTags) {
775
+ if (t === 'kai-sources') {
776
+ companionLines.push(` <!-- Replace sampleSources with your real data (set as a JS property). -->`);
777
+ companionLines.push(` <kai-sources ref="sourcesEl" />`);
778
+ } else {
779
+ companionLines.push(` <!-- wire data props — see the component_reference MCP tool -->`);
780
+ companionLines.push(` <${t} />`);
781
+ }
782
+ }
783
+ const companions = companionLines.join('\n');
784
+
785
+ // SCAF-8: include model when the integration forwards it.
786
+ const bodyPayload = defaultModel
787
+ ? `{ model, messages: history.map((m) => ({ role: m.role, content: m.content })) }`
788
+ : `{ messages: history.map((m) => ({ role: m.role, content: m.content })) }`;
789
+
790
+ const onSubmitBody = isMock
791
+ ? mockStreamBody({
792
+ pad: ' ',
793
+ read: 'messages.value',
794
+ commitInitial: (expr) => `messages.value = ${expr};`,
795
+ // messages.value is live — map over it directly
796
+ commitMap: (mapBody) => `messages.value = messages.value.map((m) => ${mapBody});`,
797
+ setLoading: (v) => `loading.value = ${v};`,
798
+ strictRoles: true,
799
+ })
800
+ : [
801
+ ` const value = e.detail.value.trim();`,
802
+ ` if (!value) return;`,
803
+ ` const history: ChatMessage[] = [...messages.value, { id: crypto.randomUUID(), role: 'user' as const, content: value }];`,
804
+ ` const assistantId = crypto.randomUUID();`,
805
+ ` messages.value = [...history, { id: assistantId, role: 'assistant' as const, content: '' }];`,
806
+ ` loading.value = true;`,
807
+ ` // POST to /api/chat, then stream the OpenAI-format SSE into the`,
808
+ ` // assistant message (reassign messages.value per chunk) — see the Streaming recipe.`,
809
+ ...(defaultModel
810
+ ? [
811
+ ` // SCAF-8: change this to any provider/model string you want to use.`,
812
+ ` const model = '${defaultModel}';`,
813
+ ]
814
+ : []),
815
+ ` const res = await fetch('/api/chat', {`,
816
+ ` method: 'POST',`,
817
+ ` headers: { 'Content-Type': 'application/json' },`,
818
+ ` body: JSON.stringify(${bodyPayload}),`,
819
+ ` });`,
820
+ ` // Stream the OpenAI-format SSE — see the Streaming recipe.`,
821
+ ` const reader = res.body.getReader();`,
822
+ ` const decoder = new TextDecoder();`,
823
+ ` let buffer = '', answer = '';`,
824
+ ` while (true) {`,
825
+ ` const { value: chunk, done } = await reader.read();`,
826
+ ` if (done) break;`,
827
+ ` buffer += decoder.decode(chunk, { stream: true });`,
828
+ ` const lines = buffer.split('\\n');`,
829
+ ` buffer = lines.pop();`,
830
+ ` for (const line of lines) {`,
831
+ ` const s = line.trim();`,
832
+ ` if (!s.startsWith('data:')) continue;`,
833
+ ` const payload = s.slice(5).trim();`,
834
+ ` if (payload === '[DONE]') continue;`,
835
+ ` try {`,
836
+ ` const delta = JSON.parse(payload).choices?.[0]?.delta?.content;`,
837
+ ` if (!delta) continue;`,
838
+ ` answer += delta;`,
839
+ ` messages.value = messages.value.map((m) => (m.id === assistantId ? { ...m, content: answer } : m));`,
840
+ ` } catch { /* skip keep-alives */ }`,
841
+ ` }`,
842
+ ` }`,
843
+ ` loading.value = false;`,
844
+ ].join('\n');
845
+
846
+ // SCAF-10: ChatMessage type for strict-TS Vue consumers — matches the React SCAF-4 type.
847
+ // SCAF-11: state must be the library's 4-value union (not bare string); reasoning carries optional label.
848
+ 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 }[] };`;
849
+
850
+ // SCAF-9: sample seeding for agentic archetype.
851
+ const sampleSeed = hasEmbedded
852
+ ? [
853
+ `// SCAF-9: tool calls + reasoning render inside the thread — set them on the message object.`,
854
+ `// Replace with real messages streamed from your backend.`,
855
+ `const messages = ref<ChatMessage[]>([${JSON.stringify(SAMPLE_AGENTIC_MESSAGE)}]);`,
856
+ ]
857
+ : [`const messages = ref<ChatMessage[]>([]);`];
858
+
859
+ // SCAF-9: sample sources setup.
860
+ const sourcesSeed = standaloneCompanionTags.includes('kai-sources')
861
+ ? [
862
+ `// Replace sampleSources with your real source data.`,
863
+ `const sampleSources = [`,
864
+ ` { href: 'https://example.com/doc1', title: 'Getting started', description: 'Overview of the product.' },`,
865
+ ` { href: 'https://example.com/doc2', title: 'API reference', description: 'Full API documentation.' },`,
866
+ `];`,
867
+ `onMounted(() => {`,
868
+ ` const el = document.querySelector('kai-sources');`,
869
+ ` if (el) el.sources = sampleSources;`,
870
+ `});`,
871
+ ]
872
+ : [];
873
+
874
+ // SCAF-15: always import onMounted — we re-apply props after the element upgrades
875
+ // (the .prop bindings can apply before the async element registration resolves).
876
+ const vueImports = `import { ref, onMounted } from 'vue';`;
877
+
878
+ // SCAF-14: workspace template block — resizable split with chat + artifact panes.
879
+ const workspaceTemplate = workspace
880
+ ? [
881
+ ` <!-- SCAF-14: workspace split — chat pane left, artifact preview right. -->`,
882
+ ` <!-- kai-resizable needs kai-resizable-item children to render panels. -->`,
883
+ ` <kai-resizable orientation="horizontal" style="display:block;width:100%;height:100%">`,
884
+ ` <kai-resizable-item size="40%" min="240px">`,
885
+ ` <kai-chat`,
886
+ ` :messages.prop="messages"`,
887
+ ` :loading.prop="loading"`,
888
+ ` :suggestions.prop="suggestions"`,
889
+ ` suggestion-mode="submit"`,
890
+ ` style="${p.chatFill}"`,
891
+ ` @kai-submit="onSubmit"`,
892
+ ` ></kai-chat>`,
893
+ ` </kai-resizable-item>`,
894
+ ` <kai-resizable-item min="280px">`,
895
+ ` <!-- Replace src with your artifact URL or set .files for multi-file preview. -->`,
896
+ ` <kai-artifact src="https://example.com" style="width:100%;height:100%"></kai-artifact>`,
897
+ ` </kai-resizable-item>`,
898
+ ` </kai-resizable>`,
899
+ ]
900
+ : [
901
+ ` <kai-chat`,
902
+ ` :messages.prop="messages"`,
903
+ ` :loading.prop="loading"`,
904
+ ` :suggestions.prop="suggestions"`,
905
+ ` suggestion-mode="submit"`,
906
+ ` style="${p.chatFill}"`,
907
+ ` @kai-submit="onSubmit"`,
908
+ ` ></kai-chat>`,
909
+ companions,
910
+ ];
911
+
912
+ return [
913
+ `<!-- vue — ${archetype.title} — ${p.note}. empty-state hint: ${emptyHint} -->`,
914
+ ...(p.altNote ? [`<!-- ${p.altNote} -->`] : []),
915
+ `<!-- SCAF-3: vite.config.ts — tell Vue that kai-* are custom elements (not Vue components).`,
916
+ ` Without this, Vue warns "Unknown custom element" and .prop bindings may misbehave.`,
917
+ ` import vue from '@vitejs/plugin-vue';`,
918
+ ` export default { plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('kai-') } } })] }; -->`,
919
+ `<script setup lang="ts">`,
920
+ `import '@kitn.ai/ui/elements'; // registers <kai-*> — required, must come first`,
921
+ `import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
922
+ vueImports,
923
+ ``,
924
+ chatMessageType,
925
+ ``,
926
+ ...sampleSeed,
927
+ `const loading = ref(false);`,
928
+ `const suggestions = ${jsArray(suggestions)};`,
929
+ ...sourcesSeed,
930
+ ``,
931
+ `// SCAF-15: kai-* register via an async dynamic import (SSR-safety). The .prop`,
932
+ `// bindings can apply before the element upgrades, which drops them — re-apply once`,
933
+ `// the element is defined so the initial messages/suggestions/loading stick.`,
934
+ `onMounted(async () => {`,
935
+ ` await customElements.whenDefined('kai-chat');`,
936
+ ` const el = document.querySelector('kai-chat');`,
937
+ ` if (el) Object.assign(el, { messages: messages.value, loading: loading.value, suggestions });`,
938
+ `});`,
939
+ ``,
940
+ `async function onSubmit(e: CustomEvent<{ value: string }>) {`,
941
+ onSubmitBody,
942
+ `}`,
943
+ `</script>`,
944
+ ``,
945
+ `<template>`,
946
+ ` <div style="${p.style}">`,
947
+ ...workspaceTemplate,
948
+ ` </div>`,
949
+ `</template>`,
950
+ ]
951
+ .filter((l) => l !== '')
952
+ .join('\n');
953
+ }
954
+
955
+ /** Svelte: use bind:this to set array/object properties reactively; on:kai-submit for the event. */
956
+ function renderSvelte(archetype: Archetype, ctx: RenderCtx): string {
957
+ const { p, emptyHint, suggestions, isMock, defaultModel } = ctx;
958
+
959
+ // SCAF-9: exclude message-embedded tags from companion rendering.
960
+ // SCAF-14: also exclude workspace structural tags (handled by the workspace block below).
961
+ const workspace = isWorkspace(archetype);
962
+ const standaloneCompanionTags = archetype.components.filter(
963
+ (t) => t !== 'kai-chat' && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t),
964
+ );
965
+ const hasEmbedded = archetype.components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
966
+
967
+ const companionLinesList: string[] = [];
968
+ if (hasEmbedded) {
969
+ companionLinesList.push(
970
+ ` <!-- kai-tool / kai-reasoning render INSIDE the thread — set tools/reasoning on each message object. -->`,
971
+ );
972
+ }
973
+ for (const t of standaloneCompanionTags) {
974
+ if (t === 'kai-sources') {
975
+ companionLinesList.push(` <!-- Replace sampleSources with your real data. -->`);
976
+ companionLinesList.push(` <kai-sources bind:this={sourcesEl}></kai-sources>`);
977
+ } else {
978
+ companionLinesList.push(` <!-- wire data props — see the component_reference MCP tool -->`);
979
+ companionLinesList.push(` <${t}></${t}>`);
980
+ }
981
+ }
982
+ const companionLines = companionLinesList.join('\n');
983
+
984
+ // SCAF-8: include model when the integration forwards it.
985
+ const bodyPayload = defaultModel
986
+ ? `{ model, messages: history.map((m) => ({ role: m.role, content: m.content })) }`
987
+ : `{ messages: history.map((m) => ({ role: m.role, content: m.content })) }`;
988
+
989
+ const onSubmitBody = isMock
990
+ ? mockStreamBody({
991
+ pad: ' ',
992
+ read: 'messages',
993
+ commitInitial: (expr) => `messages = ${expr};`,
994
+ // `messages` is a live local — reassign to map over the latest array
995
+ commitMap: (mapBody) => `messages = messages.map((m) => ${mapBody});`,
996
+ setLoading: (v) => `loading = ${v};`,
997
+ strictRoles: true,
998
+ })
999
+ : [
1000
+ ` const value = e.detail.value.trim();`,
1001
+ ` if (!value) return;`,
1002
+ ` const history = [...messages, { id: crypto.randomUUID(), role: 'user' as const, content: value }];`,
1003
+ ` const assistantId = crypto.randomUUID();`,
1004
+ ` messages = [...history, { id: assistantId, role: 'assistant' as const, content: '' }];`,
1005
+ ` loading = true;`,
1006
+ ...(defaultModel
1007
+ ? [
1008
+ ` // SCAF-8: change this to any provider/model string you want to use.`,
1009
+ ` const model = '${defaultModel}';`,
1010
+ ]
1011
+ : []),
1012
+ ` const res = await fetch('/api/chat', {`,
1013
+ ` method: 'POST',`,
1014
+ ` headers: { 'Content-Type': 'application/json' },`,
1015
+ ` body: JSON.stringify(${bodyPayload}),`,
1016
+ ` });`,
1017
+ ` // Stream the OpenAI-format SSE into the assistant message — see the Streaming recipe.`,
1018
+ ` const reader = res.body.getReader();`,
1019
+ ` const decoder = new TextDecoder();`,
1020
+ ` let buffer = '', answer = '';`,
1021
+ ` while (true) {`,
1022
+ ` const { value: chunk, done } = await reader.read();`,
1023
+ ` if (done) break;`,
1024
+ ` buffer += decoder.decode(chunk, { stream: true });`,
1025
+ ` const lines = buffer.split('\\n');`,
1026
+ ` buffer = lines.pop();`,
1027
+ ` for (const line of lines) {`,
1028
+ ` const s = line.trim();`,
1029
+ ` if (!s.startsWith('data:')) continue;`,
1030
+ ` const payload = s.slice(5).trim();`,
1031
+ ` if (payload === '[DONE]') continue;`,
1032
+ ` try {`,
1033
+ ` const delta = JSON.parse(payload).choices?.[0]?.delta?.content;`,
1034
+ ` if (!delta) continue;`,
1035
+ ` answer += delta;`,
1036
+ ` messages = messages.map((m) => (m.id === assistantId ? { ...m, content: answer } : m));`,
1037
+ ` } catch { /* skip keep-alives */ }`,
1038
+ ` }`,
1039
+ ` }`,
1040
+ ` loading = false;`,
1041
+ ].join('\n');
1042
+
1043
+ // SCAF-10: ChatMessage type for strict-TS Svelte consumers — matches the React SCAF-4 type.
1044
+ // SCAF-11: state must be the library's 4-value union (not bare string); reasoning carries optional label.
1045
+ 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 }[] };`;
1046
+
1047
+ // SCAF-9: seed sample messages for agentic archetype.
1048
+ const sampleMessagesInit = hasEmbedded
1049
+ ? [
1050
+ ` // SCAF-9: tool calls + reasoning render INSIDE the thread — set them on the message object.`,
1051
+ ` // Replace with real messages streamed from your backend.`,
1052
+ ` let messages: ChatMessage[] = [${JSON.stringify(SAMPLE_AGENTIC_MESSAGE)}];`,
1053
+ ]
1054
+ : [` let messages: ChatMessage[] = [];`];
1055
+
1056
+ // SCAF-9: sources element ref + sample data.
1057
+ const sourcesEl = standaloneCompanionTags.includes('kai-sources')
1058
+ ? [` let sourcesEl: HTMLElement | undefined;`]
1059
+ : [];
1060
+ const sourcesReactive = standaloneCompanionTags.includes('kai-sources')
1061
+ ? [
1062
+ ` // Replace sampleSources with your real source data.`,
1063
+ ` const sampleSources = [`,
1064
+ ` { href: 'https://example.com/doc1', title: 'Getting started', description: 'Overview of the product.' },`,
1065
+ ` { href: 'https://example.com/doc2', title: 'API reference', description: 'Full API documentation.' },`,
1066
+ ` ];`,
1067
+ ` $: if (sourcesEl) { sourcesEl.sources = sampleSources; }`,
1068
+ ]
1069
+ : [];
1070
+
1071
+ // SCAF-14: workspace template block — resizable split with chat + artifact panes.
1072
+ const workspaceMarkup = workspace
1073
+ ? [
1074
+ ` <!-- SCAF-14: workspace split — chat pane left, artifact preview right. -->`,
1075
+ ` <!-- kai-resizable needs kai-resizable-item children to render panels. -->`,
1076
+ ` <kai-resizable orientation="horizontal" style="display:block;width:100%;height:100%">`,
1077
+ ` <kai-resizable-item size="40%" min="240px">`,
1078
+ ` <kai-chat bind:this={chatEl} suggestion-mode="submit" style="${p.chatFill}" on:kai-submit={onSubmit}></kai-chat>`,
1079
+ ` </kai-resizable-item>`,
1080
+ ` <kai-resizable-item min="280px">`,
1081
+ ` <!-- Replace src with your artifact URL or set .files for multi-file preview. -->`,
1082
+ ` <kai-artifact src="https://example.com" style="width:100%;height:100%"></kai-artifact>`,
1083
+ ` </kai-resizable-item>`,
1084
+ ` </kai-resizable>`,
1085
+ ]
1086
+ : [
1087
+ ` <kai-chat bind:this={chatEl} suggestion-mode="submit" style="${p.chatFill}" on:kai-submit={onSubmit}></kai-chat>`,
1088
+ companionLines,
1089
+ ];
1090
+
1091
+ return [
1092
+ `<!-- svelte — ${archetype.title} — ${p.note}. empty-state hint: ${emptyHint} -->`,
1093
+ ...(p.altNote ? [`<!-- ${p.altNote} -->`] : []),
1094
+ `<!-- SCAF-5: This uses Svelte-4 syntax ($:, on:event). Works in Svelte 5 via legacy mode;`,
1095
+ ` runes-mode users should adapt to $state/$effect and onkai-submit event handlers. -->`,
1096
+ `<script lang="ts">`,
1097
+ ` import '@kitn.ai/ui/elements'; // registers <kai-*> — required, must come first`,
1098
+ ` import type { KaiChatElement } from '@kitn.ai/ui/elements';`,
1099
+ ` import '@kitn.ai/ui/theme.tokens.css'; // compiled token defaults; use theme.css only for Tailwind-source apps`,
1100
+ ` import { onMount } from 'svelte';`,
1101
+ chatMessageType,
1102
+ ` let chatEl: KaiChatElement | undefined;`,
1103
+ ` // SCAF-15: kai-* register via an async dynamic import (SSR-safety). Gate the`,
1104
+ ` // reactive property block on the upgrade so the first application isn't dropped`,
1105
+ ` // (props set on a not-yet-upgraded element are lost on upgrade).`,
1106
+ ` let defined = false;`,
1107
+ ` onMount(async () => { await customElements.whenDefined('kai-chat'); defined = true; });`,
1108
+ ...sourcesEl,
1109
+ ...sampleMessagesInit,
1110
+ ` let loading: boolean = false;`,
1111
+ ` const suggestions: string[] = ${jsArray(suggestions)};`,
1112
+ ` // suggestions/messages are JS PROPERTIES (arrays/objects can't be attributes)`,
1113
+ ` $: if (chatEl && defined) { chatEl.messages = messages; chatEl.loading = loading; chatEl.suggestions = suggestions; }`,
1114
+ ...sourcesReactive,
1115
+ ``,
1116
+ ` async function onSubmit(e: CustomEvent<{ value: string }>) {`,
1117
+ onSubmitBody,
1118
+ ` }`,
1119
+ `</script>`,
1120
+ ``,
1121
+ `<div style="${p.style}">`,
1122
+ ...workspaceMarkup,
1123
+ `</div>`,
1124
+ ]
1125
+ .filter((l, i, arr) => !(l === '' && arr[i - 1] === '' && i === arr.length - 1))
1126
+ .join('\n');
1127
+ }
1128
+
1129
+ /**
1130
+ * TanStack Start: emits a file-based route (`src/routes/chat.tsx`) that renders
1131
+ * the Chat surface client-only via `ssr: false` on `createFileRoute`.
1132
+ *
1133
+ * Verified pattern: `ssr: false` prevents the Solid-based web-component runtime
1134
+ * from running on the server — no `window is not defined` crash, no hydration
1135
+ * mismatch. The library is SSR-import-safe (customElements.define is guarded),
1136
+ * so the import itself is safe; only the *render* needs to be client-only.
1137
+ *
1138
+ * Scaffold command (official TanStack CLI, non-interactive):
1139
+ * npx @tanstack/cli@latest create <app-name> --framework react --no-git --package-manager npm -y
1140
+ *
1141
+ * After scaffolding, run `npm install @kitn.ai/ui`, then drop this file into
1142
+ * `src/routes/chat.tsx`. Start the dev server with `npm run dev` (port 3000).
1143
+ * Build: `npm run build`; preview: `npm run preview` (or `node dist/server/server.js`).
1144
+ * Note: `npm start` does NOT exist in TanStack Start projects — use `npm run dev` / `npm run preview`.
1145
+ *
1146
+ * Backend: TanStack Start supports server-side routes via `createServerFn` in
1147
+ * `src/server/`. For a chat API, place the route in `src/server/chat.ts` and call
1148
+ * it from your route component. The emitted scaffold uses `fetch('/api/chat')` as a
1149
+ * placeholder pointing at a standard route — swap for your TanStack server function
1150
+ * or an external endpoint.
1151
+ */
1152
+ function renderTanstackStart(archetype: Archetype, ctx: RenderCtx): string {
1153
+ const { p, emptyHint, suggestions, isMock, defaultModel } = ctx;
1154
+
1155
+ // TanStack Start is React — reuse all the React composition logic:
1156
+ // same ChatMessage type, same state/loading/suggestions, same mock stream body,
1157
+ // same real-backend SSE streaming. The ONLY delta from plain `react` is:
1158
+ // 1. `import { createFileRoute } from '@tanstack/react-router'` instead of no-op router import
1159
+ // 2. `export const Route = createFileRoute('/chat')({ ssr: false, component: ChatPage })`
1160
+ // 3. The page function is named `ChatPage` (not `App`) — no export-default clash with createFileRoute
1161
+ // 4. No `import '@kitn.ai/ui/elements'` needed as a top-level import (same as next's dynamic approach
1162
+ // is not needed here — the library is SSR-import-safe, but we include elements for safety)
1163
+
1164
+ const hasEmbedded = archetype.components.some((t) => MESSAGE_EMBEDDED_TAGS.has(t));
1165
+ const workspace = isWorkspace(archetype);
1166
+
1167
+ const renderableTags = archetype.components.filter((t) => !MESSAGE_EMBEDDED_TAGS.has(t));
1168
+ const importTags = workspace
1169
+ ? [...new Set([...renderableTags.filter((t) => t !== 'kai-resizable'), 'kai-resizable', 'kai-resizable-item'])]
1170
+ : renderableTags;
1171
+ const wrapperNames = importTags.map(toPascalCase);
1172
+ const importList = wrapperNames.join(', ');
1173
+
1174
+ const standaloneCompanionTags = archetype.components.filter(
1175
+ (t) => t !== 'kai-chat' && !MESSAGE_EMBEDDED_TAGS.has(t) && !WORKSPACE_STRUCTURAL_TAGS.has(t),
1176
+ );
1177
+
1178
+ const companionJsxLines: string[] = [];
1179
+ if (hasEmbedded) {
1180
+ companionJsxLines.push(
1181
+ ` {/* kai-tool / kai-reasoning render inside the thread. Tool calls + reasoning`,
1182
+ ` are set on each message object — see the sampleMessages initializer above. */}`,
1183
+ );
1184
+ }
1185
+ for (const t of standaloneCompanionTags) {
1186
+ if (t === 'kai-sources') {
1187
+ companionJsxLines.push(
1188
+ ` {/* Replace sampleSources with your real data. */}`,
1189
+ ` <Sources sources={sampleSources} />`,
1190
+ );
1191
+ } else {
1192
+ companionJsxLines.push(` {/* wire data props — see the component_reference MCP tool */}`);
1193
+ companionJsxLines.push(` <${toPascalCase(t)} />`);
1194
+ }
1195
+ }
1196
+ const companions = companionJsxLines.join('\n');
1197
+
1198
+ 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 }[] };`;
1199
+
1200
+ const sampleMessagesInit = hasEmbedded
1201
+ ? [
1202
+ ` // SCAF-9: tool calls and reasoning render inside the thread — set them on the message object.`,
1203
+ ` // Replace with real messages streamed from your backend.`,
1204
+ ` const sampleMessages: ChatMessage[] = [${JSON.stringify(SAMPLE_AGENTIC_MESSAGE)}];`,
1205
+ ` const [messages, setMessages] = useState<ChatMessage[]>(sampleMessages);`,
1206
+ ].join('\n')
1207
+ : ` const [messages, setMessages] = useState<ChatMessage[]>([]);`;
1208
+
1209
+ const sampleSourcesInit =
1210
+ standaloneCompanionTags.includes('kai-sources')
1211
+ ? [
1212
+ ` // Replace sampleSources with your real source data.`,
1213
+ ` const sampleSources = [`,
1214
+ ` { href: 'https://example.com/doc1', title: 'Getting started', description: 'Overview of the product.' },`,
1215
+ ` { href: 'https://example.com/doc2', title: 'API reference', description: 'Full API documentation.' },`,
1216
+ ` ];`,
1217
+ ].join('\n')
1218
+ : '';
1219
+
1220
+ const modelInit = defaultModel
1221
+ ? ` // SCAF-8: change this to any provider/model string you want to use.\n const model = '${defaultModel}';`
1222
+ : '';
1223
+ const bodyPayload = defaultModel
1224
+ ? `{ model, messages: history.map((m) => ({ role: m.role, content: m.content })) }`
1225
+ : `{ messages: history.map((m) => ({ role: m.role, content: m.content })) }`;
1226
+
1227
+ const onSubmitBody = isMock
1228
+ ? mockStreamBody({
1229
+ pad: ' ',
1230
+ read: 'messages',
1231
+ commitInitial: (expr) => `setMessages(${expr});`,
1232
+ commitMap: (mapBody) => `setMessages((prev) => prev.map((m) => ${mapBody}));`,
1233
+ setLoading: (v) => `setLoading(${v});`,
1234
+ strictRoles: true,
1235
+ })
1236
+ : [
1237
+ ` const value = e.detail.value.trim();`,
1238
+ ` if (!value) return;`,
1239
+ ` const history: ChatMessage[] = [...messages, { id: crypto.randomUUID(), role: 'user' as const, content: value }];`,
1240
+ ` const assistantId = crypto.randomUUID();`,
1241
+ ` setMessages([...history, { id: assistantId, role: 'assistant' as const, content: '' }]);`,
1242
+ ` setLoading(true);`,
1243
+ ` const res = await fetch('/api/chat', {`,
1244
+ ` method: 'POST',`,
1245
+ ` headers: { 'Content-Type': 'application/json' },`,
1246
+ ` body: JSON.stringify(${bodyPayload}),`,
1247
+ ` });`,
1248
+ ` // Stream the OpenAI-format SSE into the assistant message — see the Streaming recipe.`,
1249
+ ` const reader = res.body!.getReader();`,
1250
+ ` const decoder = new TextDecoder();`,
1251
+ ` let buffer = '', answer = '';`,
1252
+ ` while (true) {`,
1253
+ ` const { value: chunk, done } = await reader.read();`,
1254
+ ` if (done) break;`,
1255
+ ` buffer += decoder.decode(chunk, { stream: true });`,
1256
+ ` const lines = buffer.split('\\n');`,
1257
+ ` buffer = lines.pop()!;`,
1258
+ ` for (const line of lines) {`,
1259
+ ` const s = line.trim();`,
1260
+ ` if (!s.startsWith('data:')) continue;`,
1261
+ ` const payload = s.slice(5).trim();`,
1262
+ ` if (payload === '[DONE]') continue;`,
1263
+ ` try {`,
1264
+ ` const delta = JSON.parse(payload).choices?.[0]?.delta?.content;`,
1265
+ ` if (!delta) continue;`,
1266
+ ` answer += delta;`,
1267
+ ` setMessages((ms) => ms.map((m) => (m.id === assistantId ? { ...m, content: answer } : m)));`,
1268
+ ` } catch { /* skip keep-alives */ }`,
1269
+ ` }`,
1270
+ ` }`,
1271
+ ` setLoading(false);`,
1272
+ ].join('\n');
1273
+
1274
+ // File path guidance for TanStack Start (file-based routing)
1275
+ const filePathNote = [
1276
+ `// TanStack Start route file — save as: src/routes/chat.tsx`,
1277
+ `// Scaffold command: npx @tanstack/cli@latest create <app-name> --framework react --no-git --package-manager npm -y`,
1278
+ `// Then: npm install @kitn.ai/ui`,
1279
+ `// Dev: npm run dev (port 3000) Build: npm run build Preview: npm run preview`,
1280
+ `// Note: there is no 'npm start' script — use 'npm run dev' or 'npm run preview'.`,
1281
+ `// Backend: place TanStack server functions in src/server/chat.ts (createServerFn).`,
1282
+ ``,
1283
+ ];
1284
+
1285
+ return [
1286
+ ...filePathNote,
1287
+ // TanStack Start uses @tanstack/react-router's createFileRoute
1288
+ `import { createFileRoute } from '@tanstack/react-router'`,
1289
+ `import { useState } from 'react'`,
1290
+ // Elements registration: the library is SSR-import-safe; top-level import is safe here
1291
+ `import '@kitn.ai/ui/elements'; // registers <kai-*> — required, must come first`,
1292
+ `import { ${importList} } from '@kitn.ai/ui/react'`,
1293
+ `import '@kitn.ai/ui/theme.tokens.css' // compiled token defaults`,
1294
+ ``,
1295
+ `// ${archetype.title} — ${p.note}. empty-state hint: ${emptyHint}`,
1296
+ ...(p.altNote ? [`// ${p.altNote}`] : []),
1297
+ chatMessageType,
1298
+ ``,
1299
+ `// ssr: false keeps the Solid-based web component client-only.`,
1300
+ `// Server HTML for /chat omits <kai-chat> → no hydration mismatch.`,
1301
+ `export const Route = createFileRoute('/chat')({`,
1302
+ ` ssr: false,`,
1303
+ ` component: ChatPage,`,
1304
+ `})`,
1305
+ ``,
1306
+ `function ChatPage() {`,
1307
+ sampleMessagesInit,
1308
+ ` const [loading, setLoading] = useState(false);`,
1309
+ ` const suggestions = ${jsArray(suggestions)};`,
1310
+ ...(sampleSourcesInit ? [sampleSourcesInit] : []),
1311
+ ...(modelInit ? [modelInit] : []),
1312
+ ``,
1313
+ ` async function onSubmit(e: CustomEvent<{ value: string }>) {`,
1314
+ onSubmitBody,
1315
+ ` }`,
1316
+ ``,
1317
+ ` return (`,
1318
+ ` <main style={{ ${jsxStyle(p.style)} }}>`,
1319
+ ...(workspace
1320
+ ? [
1321
+ ` {/* SCAF-14: workspace split — chat pane left, artifact preview right. */}`,
1322
+ ` {/* Resizable needs ResizableItem children to render panels. */}`,
1323
+ ` <Resizable orientation="horizontal" style={{ display: 'block', width: '100%', height: '100%' }}>`,
1324
+ ` <ResizableItem size="40%" min="240px">`,
1325
+ ` <Chat`,
1326
+ ` messages={messages}`,
1327
+ ` loading={loading}`,
1328
+ ` suggestions={suggestions}`,
1329
+ ` suggestionMode="submit"`,
1330
+ ` onSubmit={onSubmit}`,
1331
+ ` style={{ ${jsxStyle(p.chatFill)} }}`,
1332
+ ` />`,
1333
+ ` </ResizableItem>`,
1334
+ ` <ResizableItem min="280px">`,
1335
+ ` {/* Replace src with your artifact URL or set files for multi-file preview. */}`,
1336
+ ` <Artifact src="https://example.com" style={{ width: '100%', height: '100%' }} />`,
1337
+ ` </ResizableItem>`,
1338
+ ` </Resizable>`,
1339
+ ]
1340
+ : [
1341
+ ` <Chat`,
1342
+ ` messages={messages}`,
1343
+ ` loading={loading}`,
1344
+ ` suggestions={suggestions}`,
1345
+ ` suggestionMode="submit"`,
1346
+ ` onSubmit={onSubmit}`,
1347
+ ` style={{ ${jsxStyle(p.chatFill)} }}`,
1348
+ ` />`,
1349
+ companions,
1350
+ ]),
1351
+ ` </main>`,
1352
+ ` );`,
1353
+ `}`,
1354
+ ]
1355
+ .filter((l) => l !== '')
1356
+ .join('\n');
1357
+ }
1358
+
1359
+ /** Translate an inline CSS string into JSX style-object entries. */
1360
+ function jsxStyle(style: string): string {
1361
+ return style
1362
+ .split(';')
1363
+ .map((d) => d.trim())
1364
+ .filter(Boolean)
1365
+ .map((d) => {
1366
+ const [prop, ...rest] = d.split(':');
1367
+ const camel = prop.trim().replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1368
+ return `${camel}: '${rest.join(':').trim()}'`;
1369
+ })
1370
+ .join(', ');
1371
+ }
1372
+
1373
+ function renderFrontend(
1374
+ framework: string,
1375
+ archetype: Archetype,
1376
+ placement: string,
1377
+ emptyHint: string,
1378
+ suggestions: string[],
1379
+ isMock: boolean,
1380
+ defaultModel?: string,
1381
+ ): string {
1382
+ const ctx: RenderCtx = { p: placementStyle(placement), emptyHint, suggestions, isMock, defaultModel };
1383
+ switch (framework) {
1384
+ case 'react':
1385
+ case 'next':
1386
+ return renderJsx(archetype, ctx, framework);
1387
+ case 'vue':
1388
+ return renderVue(archetype, ctx);
1389
+ case 'svelte':
1390
+ return renderSvelte(archetype, ctx);
1391
+ case 'tanstack-start':
1392
+ return renderTanstackStart(archetype, ctx);
1393
+ case 'html':
1394
+ default:
1395
+ // html, and any backend-only framework (fastapi/express/worker) gets the
1396
+ // framework-agnostic web-components surface.
1397
+ return renderHtml(archetype, ctx);
1398
+ }
1399
+ }
1400
+
1401
+ // ── backend route selection (with language-aware fallback) ─────────────────────
1402
+
1403
+ interface RouteChoice {
1404
+ framework: string;
1405
+ template: string;
1406
+ runtime: string;
1407
+ exact: boolean;
1408
+ }
1409
+
1410
+ const RUNTIME_LABEL: Record<string, string> = {
1411
+ next: 'Next.js route handler (Node/Edge)',
1412
+ express: 'Express handler (Node)',
1413
+ worker: 'Cloudflare Worker',
1414
+ fastapi: 'FastAPI (Python)',
1415
+ html: 'browser-direct (no server route)',
1416
+ 'tanstack-start': 'TanStack Start server function (Node)',
1417
+ };
1418
+
1419
+ /** Prefer the language's canonical server framework when there's no exact match. */
1420
+ function preferredKeyFor(integration: Integration): string[] {
1421
+ return integration.language === 'python'
1422
+ ? ['fastapi']
1423
+ : ['next', 'express', 'worker'];
1424
+ }
1425
+
1426
+ function chooseRoute(integration: Integration, framework: string): RouteChoice | undefined {
1427
+ const templates = integration.routeTemplates;
1428
+
1429
+ // 1. exact match
1430
+ if (templates[framework]) {
1431
+ return { framework, template: templates[framework], runtime: RUNTIME_LABEL[framework] ?? framework, exact: true };
1432
+ }
1433
+
1434
+ // 2. language-canonical fallback (python → fastapi; ts → next/express/worker)
1435
+ for (const key of preferredKeyFor(integration)) {
1436
+ if (templates[key]) {
1437
+ return { framework: key, template: templates[key], runtime: RUNTIME_LABEL[key] ?? key, exact: false };
1438
+ }
1439
+ }
1440
+
1441
+ // 3. anything usable that isn't a pure front-end snippet
1442
+ for (const [key, template] of Object.entries(templates)) {
1443
+ if (key === 'html') continue;
1444
+ return { framework: key, template, runtime: RUNTIME_LABEL[key] ?? key, exact: false };
1445
+ }
1446
+
1447
+ return undefined;
1448
+ }
1449
+
1450
+ // ── compose ───────────────────────────────────────────────────────────────────
1451
+
1452
+ function compose(
1453
+ archetype: Archetype,
1454
+ integration: Integration,
1455
+ placement: string,
1456
+ framework: string,
1457
+ suggestions: string[],
1458
+ audience?: string,
1459
+ ): string {
1460
+ const audienceHint = audience
1461
+ ? `tuned for ${audience} — keep the empty state and tone audience-appropriate`
1462
+ : 'add an empty-state prompt that fits your product';
1463
+
1464
+ const isMock = integration.id === 'mock';
1465
+ // SCAF-8: compute the default model only for non-mock integrations that forward model.
1466
+ const defaultModel = isMock ? undefined : defaultModelFor(integration);
1467
+ const frontend = renderFrontend(framework, archetype, placement, audienceHint, suggestions, isMock, defaultModel);
1468
+ const route = isMock ? undefined : chooseRoute(integration, framework);
1469
+
1470
+ const header = [
1471
+ `# AI/UI scaffold — ${archetype.title} × ${integration.title}`,
1472
+ `combo: ${archetype.id} × ${integration.id} × ${placement} × ${framework}`,
1473
+ `stream: ${integration.streamMapping}`,
1474
+ ].join('\n');
1475
+
1476
+ const block1 = [
1477
+ `=== (1) FRONT-END (${framework}, ${placementStyle(placement).note}) ===`,
1478
+ ``,
1479
+ frontend,
1480
+ ].join('\n');
1481
+
1482
+ const block2Parts: string[] = [`=== (2) BACKEND ROUTE ===`, ``];
1483
+ if (isMock) {
1484
+ // The mock integration has no backend — the front-end streams locally.
1485
+ block2Parts.push(
1486
+ `# No backend or API key needed — replies stream locally for preview (see the`,
1487
+ `# front-end onSubmit above). Swap \`integration\` for a real provider (openrouter,`,
1488
+ `# ollama, vercel-ai-sdk, …) when ready, and this block becomes its route handler.`,
1489
+ );
1490
+ } else if (route) {
1491
+ if (!route.exact) {
1492
+ block2Parts.push(
1493
+ `# Note: ${integration.title} has no template for "${framework}". Emitting its native`,
1494
+ `# ${route.runtime} route instead (matches the integration's ${integration.language} language).`,
1495
+ );
1496
+ // Honest warning: a Next.js/server route will NOT run inside a Vite SPA.
1497
+ if (framework === 'react') {
1498
+ block2Parts.push(
1499
+ `#`,
1500
+ `# WARNING: this is a Next.js route handler — it will NOT run in a Vite SPA`,
1501
+ `# (a Vite \`react\` app has no /api routes). To make the front-end above work, either:`,
1502
+ `# • use Next.js (framework: "next"), or`,
1503
+ `# • add a Vite dev-server middleware/proxy to a server, or`,
1504
+ `# • run a separate server (framework: "express" | "worker"), or`,
1505
+ `# • use integration: "mock" for a zero-config local stream (no backend, no key).`,
1506
+ );
1507
+ }
1508
+ block2Parts.push(``);
1509
+ } else {
1510
+ block2Parts.push(`# Runtime: ${route.runtime}`, ``);
1511
+ }
1512
+ block2Parts.push(route.template);
1513
+ } else {
1514
+ block2Parts.push(
1515
+ `# ${integration.title} ships no usable backend route template. See its docs`,
1516
+ `# (${integration.docsSlug}) — wire the route by hand following the streamMapping above.`,
1517
+ );
1518
+ }
1519
+ const block2 = block2Parts.join('\n');
1520
+
1521
+ const envLines = integration.envVars.length
1522
+ ? integration.envVars.map((v) => ` - ${v}`).join('\n')
1523
+ : ' (none)';
1524
+ const block3 = [
1525
+ `=== (3) RUN NOTE ===`,
1526
+ ``,
1527
+ integration.runNote,
1528
+ ``,
1529
+ `Env vars to set:`,
1530
+ envLines,
1531
+ ].join('\n');
1532
+
1533
+ return [header, block1, block2, block3].join('\n\n');
1534
+ }
1535
+
1536
+ // ── error text ────────────────────────────────────────────────────────────────
1537
+
1538
+ function rejectIntegration(id: string): string {
1539
+ const valid = listIntegrations()
1540
+ .map((i) => `${i.id} (${i.title})`)
1541
+ .join(', ');
1542
+ return [
1543
+ `Unknown integration: "${id}".`,
1544
+ ``,
1545
+ `Valid integrations: ${valid}.`,
1546
+ `Pick one of those ids and call scaffold again.`,
1547
+ ].join('\n');
1548
+ }
1549
+
1550
+ function rejectUseCase(id: string): string {
1551
+ const valid = listArchetypes()
1552
+ .map((a) => `${a.id} (${a.title})`)
1553
+ .join(', ');
1554
+ return [
1555
+ `Unknown useCase: "${id}".`,
1556
+ ``,
1557
+ `Valid useCases (archetypes): ${valid}.`,
1558
+ `Pick one of those ids and call scaffold again.`,
1559
+ ].join('\n');
1560
+ }
1561
+
1562
+ // ── tool ──────────────────────────────────────────────────────────────────────
1563
+
1564
+ export const scaffold: Tool = {
1565
+ name: 'scaffold',
1566
+ description:
1567
+ 'Scaffold a working AI/UI chat surface from four axes: useCase (archetype) × integration × placement × framework. ' +
1568
+ 'Emits a copy-pasteable front-end (kai-* components wired with messages + kai-submit + starter suggestions), the backend ' +
1569
+ 'route for the chosen framework, and a run note with env vars. Use integration: "mock" for a zero-config local preview.',
1570
+ inputSchema: z.object({
1571
+ // useCase + integration are dynamic catalog ids — kept as strings and
1572
+ // validated against the registry in the handler (the handler is called
1573
+ // directly in tests, bypassing this schema). Use component_reference / the
1574
+ // catalogs to discover valid ids.
1575
+ useCase: z
1576
+ .string()
1577
+ .describe(
1578
+ 'Archetype id, e.g. "drop-in-chat", "support-widget", "knowledge-base", "agentic", "workspace", "voice".',
1579
+ ),
1580
+ integration: z
1581
+ .string()
1582
+ .describe(
1583
+ 'Backend integration id, e.g. "openrouter", "vercel-ai-sdk", "langgraph", "cloudflare", "ollama", "mastra", "pi", "pydantic-ai".',
1584
+ ),
1585
+ placement: Placement.describe(
1586
+ 'Where the surface lives: full-page | side | docked-widget | inline.',
1587
+ ),
1588
+ framework: Framework.describe(
1589
+ 'Target front-end/back-end framework: html | react | next | vue | svelte | fastapi | express | worker | tanstack-start.',
1590
+ ),
1591
+ suggestions: z
1592
+ .array(z.string())
1593
+ .optional()
1594
+ .describe(
1595
+ 'Optional starter prompts shown above the input when the thread is empty (real kai-chat prop). ' +
1596
+ 'Defaults to a generic pair if omitted so the feature always shows.',
1597
+ ),
1598
+ audience: z
1599
+ .string()
1600
+ .optional()
1601
+ .describe('Optional audience hint (tweaks the empty-state comment only).'),
1602
+ }),
1603
+ handler: async (args) => {
1604
+ const useCase = String(args.useCase ?? '');
1605
+ const integrationId = String(args.integration ?? '');
1606
+ const placement = String(args.placement ?? '');
1607
+ const framework = String(args.framework ?? 'html');
1608
+ const audience = args.audience ? String(args.audience) : undefined;
1609
+ // Default the suggestions so the feature always shows; honour a passed array.
1610
+ const suggestions =
1611
+ Array.isArray(args.suggestions) && args.suggestions.length > 0
1612
+ ? args.suggestions.map(String)
1613
+ : DEFAULT_SUGGESTIONS;
1614
+
1615
+ // Validate against the registry BEFORE composing — graceful, self-correcting text.
1616
+ const archetype = getArchetype(useCase);
1617
+ if (!archetype) return text(rejectUseCase(useCase));
1618
+
1619
+ const integration = getIntegration(integrationId);
1620
+ if (!integration) return text(rejectIntegration(integrationId));
1621
+
1622
+ // Fall back to the archetype's default placement only if none was provided.
1623
+ const effectivePlacement = placement || archetype.defaultPlacement;
1624
+
1625
+ return text(compose(archetype, integration, effectivePlacement, framework, suggestions, audience));
1626
+ },
1627
+ };