@kitn.ai/ui 0.22.1 → 0.23.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 (195) hide show
  1. package/dist/{create-tween-qs-jjvNA.js → create-tween-BmBnlg25.js} +1 -1
  2. package/dist/{create-tween-COnPwAQN.js → create-tween-CTT5l1bo.js} +1 -1
  3. package/dist/{create-tween-DmTUaXWE.js → create-tween-vA7upv0h.js} +1 -1
  4. package/dist/elements/agent-card.js +1 -1
  5. package/dist/elements/artifact.js +1 -1
  6. package/dist/elements/attachments.js +1 -1
  7. package/dist/elements/audio-visualizer.js +1 -1
  8. package/dist/elements/avatar.js +1 -1
  9. package/dist/elements/badge.js +1 -1
  10. package/dist/elements/button.js +1 -1
  11. package/dist/elements/card.js +1 -1
  12. package/dist/elements/cards.js +1 -1
  13. package/dist/elements/chain-of-thought.js +1 -1
  14. package/dist/elements/chat-scope-picker.js +1 -1
  15. package/dist/elements/chat-workspace.js +1 -1
  16. package/dist/elements/chat.js +1 -1
  17. package/dist/elements/checkpoint.js +1 -1
  18. package/dist/elements/choice.js +1 -1
  19. package/dist/elements/chunks/{Icon-CHG5z5L7.js → Icon-b5A8hlHs.js} +1 -1
  20. package/dist/elements/chunks/{action-icons-BAtCEEgg.js → action-icons-DW9muWrY.js} +1 -1
  21. package/dist/elements/chunks/arrow-left-iJVnQmxA.js +1 -0
  22. package/dist/elements/chunks/{artifact-fBZOE4SO.js → artifact-BhJIuh3D.js} +1 -1
  23. package/dist/elements/chunks/{attachments-DpYMe5VQ.js → attachments-CL6TaqIb.js} +1 -1
  24. package/dist/elements/chunks/{audio-visualizer-DFwzAcuL.js → audio-visualizer-BuUqQmaO.js} +1 -1
  25. package/dist/elements/chunks/{badge-D6zodm6E.js → badge-TIMiJFKc.js} +1 -1
  26. package/dist/elements/chunks/{button-C42vrGvO.js → button-B9okqG73.js} +1 -1
  27. package/dist/elements/chunks/{card-renderer-DMZsSuvU.js → card-renderer-aNA0WRFH.js} +1 -1
  28. package/dist/elements/chunks/{chat-thread-CNNEhol-.js → chat-thread-DN9evldr.js} +1 -1
  29. package/dist/elements/chunks/check-MdLzbxrm.js +1 -0
  30. package/dist/elements/chunks/chevron-down-MK7IzDqe.js +1 -0
  31. package/dist/elements/chunks/chevron-right-C5jXhmtV.js +1 -0
  32. package/dist/elements/chunks/{choice-card-YoWjpsFV.js → choice-card-kNsjU2m8.js} +1 -1
  33. package/dist/elements/chunks/circle-BG7VkPep.js +1 -0
  34. package/dist/elements/chunks/circle-check-GqvO2UBs.js +1 -0
  35. package/dist/elements/chunks/{circle-x-QIxXda6k.js → circle-x-mhH7pOK2.js} +1 -1
  36. package/dist/elements/chunks/{code-block-CqeugReY.js → code-block-ClhQO9RW.js} +1 -1
  37. package/dist/elements/chunks/{collapsible-sTv-_5lF.js → collapsible-DBcBkFPh.js} +1 -1
  38. package/dist/elements/chunks/{composer-ldQyeVEB.js → composer-CZZmtYOT.js} +1 -1
  39. package/dist/elements/chunks/{confirm-card-C0RNOwfn.js → confirm-card-CEvFmVhz.js} +1 -1
  40. package/dist/elements/chunks/{context-5qgypNHC.js → context-hKjNqiyQ.js} +1 -1
  41. package/dist/elements/chunks/{conversation-list-DLzOacXz.js → conversation-list-CsGJjSKG.js} +1 -1
  42. package/dist/elements/chunks/{create-tween-V4TXZ93X.js → create-tween-CUrM6D_o.js} +1 -1
  43. package/dist/elements/chunks/{default-input-DeeRSDHc.js → default-input-B1Mem5wu.js} +1 -1
  44. package/dist/elements/chunks/{define-lE-QvQFn.js → define-B_tXMYRm.js} +1 -1
  45. package/dist/elements/chunks/{disclosure-DZSa9jog.js → disclosure-DAoYOKew.js} +1 -1
  46. package/dist/elements/chunks/{download-B2ZmnGqW.js → download-DnCyWFJE.js} +1 -1
  47. package/dist/elements/chunks/{dropdown-R3B7cSRE.js → dropdown-CmNna5lT.js} +1 -1
  48. package/dist/elements/chunks/{ellipsis-BmxceY4l.js → ellipsis-CVxqfsDZ.js} +1 -1
  49. package/dist/elements/chunks/{embed-BVrtTZPo.js → embed-CjAD2jSi.js} +1 -1
  50. package/dist/elements/chunks/{external-link-BbdR7YO7.js → external-link-DG2sRdQL.js} +1 -1
  51. package/dist/elements/chunks/{file-text-D7dijo1A.js → file-text-D0J9F6M7.js} +1 -1
  52. package/dist/elements/chunks/file-tree-yGWNK0Js.js +1 -0
  53. package/dist/elements/chunks/{folder-D8z58t7B.js → folder-NV1pi_ud.js} +1 -1
  54. package/dist/elements/chunks/{form-p5BKJNw5.js → form-lUuJjz6s.js} +1 -1
  55. package/dist/elements/chunks/{hover-card-CN9s2md5.js → hover-card-6KtXpLwT.js} +1 -1
  56. package/dist/elements/chunks/{icon-CNiKP37E.js → icon-97R8SR2p.js} +1 -1
  57. package/dist/elements/chunks/{info-BSnuJiHI.js → info-C8r5Q6cc.js} +1 -1
  58. package/dist/elements/chunks/{input-BauDmzxX.js → input-DrseEjhp.js} +1 -1
  59. package/dist/elements/chunks/{kbd-BlHJaP9b.js → kbd-OGJDUT-O.js} +1 -1
  60. package/dist/elements/chunks/{link-DEi5iUWC.js → link-BdsfLB7b.js} +1 -1
  61. package/dist/elements/chunks/{link-preview-BaHyM6W4.js → link-preview-C9-jNJBf.js} +1 -1
  62. package/dist/elements/chunks/{loader-CmJG_Fkq.js → loader-B5QzK_8_.js} +1 -1
  63. package/dist/elements/chunks/{markdown-D2jaMpCN.js → markdown-BpAHnBJz.js} +1 -1
  64. package/dist/elements/chunks/{message-C5UfXZ2Z.js → message-CoqLuLkd.js} +1 -1
  65. package/dist/elements/chunks/{message-CXWLmmLn.js → message-_oYx5mk1.js} +1 -1
  66. package/dist/elements/chunks/message-square-C9ovmtOo.js +1 -0
  67. package/dist/elements/chunks/{minimize-2-C9ytZFMU.js → minimize-2-SegJ_oku.js} +1 -1
  68. package/dist/elements/chunks/{model-switcher-CivGfD7-.js → model-switcher-D7JrFFHp.js} +1 -1
  69. package/dist/elements/chunks/{overlay-DJL_sbOm.js → overlay-CV7z2YuX.js} +1 -1
  70. package/dist/elements/chunks/{paperclip-CFaB8naM.js → paperclip-Bg0ITU3b.js} +1 -1
  71. package/dist/elements/chunks/{progress-bar-Br5SksfR.js → progress-bar-D1_rzt6N.js} +1 -1
  72. package/dist/elements/chunks/{prompt-suggestion-CMIvtAhU.js → prompt-suggestion-D5Sm8xF1.js} +1 -1
  73. package/dist/elements/chunks/{reasoning-DpZcNzLq.js → reasoning-D26Bb4p6.js} +1 -1
  74. package/dist/elements/chunks/{resizable-Q8i3T9a_.js → resizable-Dpzjlvp4.js} +1 -1
  75. package/dist/elements/chunks/{rotate-cw-B5IHCBFV.js → rotate-cw-DzVDJOA1.js} +1 -1
  76. package/dist/elements/chunks/{scroll-area-eLeydBje.js → scroll-area-93A1bbeG.js} +1 -1
  77. package/dist/elements/chunks/{scroll-button-DWbz0yA1.js → scroll-button-CrTo7wm4.js} +1 -1
  78. package/dist/elements/chunks/{separator-Xic78AD_.js → separator-DH2r_ZSZ.js} +1 -1
  79. package/dist/elements/chunks/{settings-B8H51pAx.js → settings-BFMOd6yB.js} +1 -1
  80. package/dist/elements/chunks/{settings-group-CsG9eYLe.js → settings-group-B3VZKIcz.js} +1 -1
  81. package/dist/elements/chunks/{share-CNTx0z__.js → share-DgNbkJKY.js} +1 -1
  82. package/dist/elements/chunks/{skeleton-CLlHTcMI.js → skeleton-N8BuWYmf.js} +1 -1
  83. package/dist/elements/chunks/{source-BoVcU5ok.js → source-eJurW_-L.js} +1 -1
  84. package/dist/elements/chunks/{star-CxQVTVhI.js → star-DHvACxtm.js} +1 -1
  85. package/dist/elements/chunks/{store-CLtS5CUG.js → store-HIPGhNz9.js} +1 -1
  86. package/dist/elements/chunks/{tasks-card-CzlkLmUt.js → tasks-card-CfqrkoVO.js} +1 -1
  87. package/dist/elements/chunks/{text-shimmer-D5NhpIQ1.js → text-shimmer-BY_rI3s5.js} +1 -1
  88. package/dist/elements/chunks/{textarea-x5Uxsmtm.js → textarea-DPfuhswM.js} +1 -1
  89. package/dist/elements/chunks/{thumbs-up-UobcJnx9.js → thumbs-up-DQe0hvkZ.js} +1 -1
  90. package/dist/elements/chunks/{toast-store-BOfs9eXq.js → toast-store-VUZZBhzH.js} +1 -1
  91. package/dist/elements/chunks/{tool-Be6Dh_dt.js → tool-C1YtxwPu.js} +1 -1
  92. package/dist/elements/chunks/{tooltip-RxMxyCLF.js → tooltip-BqwnFWhn.js} +1 -1
  93. package/dist/elements/chunks/{triangle-alert-C4yxH2XN.js → triangle-alert-B6or7F5u.js} +1 -1
  94. package/dist/elements/chunks/{use-card-resolution-jva9ehH6.js → use-card-resolution-DJg32jGH.js} +1 -1
  95. package/dist/elements/chunks/{variant-aurora-Dx4VRcjS.js → variant-aurora-QXTZPyST.js} +1 -1
  96. package/dist/elements/chunks/{variant-custom-Y1ZTWgEa.js → variant-custom-D5ZlmziF.js} +1 -1
  97. package/dist/elements/chunks/{variant-wave-B9gml9Cg.js → variant-wave-DDCwhLas.js} +1 -1
  98. package/dist/elements/chunks/{video-CrPHsytT.js → video-Bb86fVYI.js} +1 -1
  99. package/dist/elements/chunks/x-BOhjBeOd.js +1 -0
  100. package/dist/elements/coachmark.js +1 -1
  101. package/dist/elements/code-block.js +1 -1
  102. package/dist/elements/command.js +1 -1
  103. package/dist/elements/compare.js +1 -1
  104. package/dist/elements/composer.js +1 -1
  105. package/dist/elements/confirm-card.js +1 -1
  106. package/dist/elements/context-meter.js +1 -1
  107. package/dist/elements/conversation-list.js +1 -1
  108. package/dist/elements/dialog.js +1 -1
  109. package/dist/elements/editable-label.js +1 -1
  110. package/dist/elements/embed.js +1 -1
  111. package/dist/elements/empty.js +1 -1
  112. package/dist/elements/feedback-bar.js +1 -1
  113. package/dist/elements/file-tree.js +1 -1
  114. package/dist/elements/file-upload.js +1 -1
  115. package/dist/elements/form.js +1 -1
  116. package/dist/elements/hover-card.js +1 -1
  117. package/dist/elements/icon.js +1 -1
  118. package/dist/elements/image.js +1 -1
  119. package/dist/elements/input.js +1 -1
  120. package/dist/elements/kbd.js +1 -1
  121. package/dist/elements/link-preview.js +1 -1
  122. package/dist/elements/loader.js +1 -1
  123. package/dist/elements/markdown.js +1 -1
  124. package/dist/elements/menu.js +1 -1
  125. package/dist/elements/message-skills.js +1 -1
  126. package/dist/elements/message.js +1 -1
  127. package/dist/elements/model-switcher.js +1 -1
  128. package/dist/elements/nav.js +1 -1
  129. package/dist/elements/notice.js +1 -1
  130. package/dist/elements/pane-group.js +1 -1
  131. package/dist/elements/pane.js +1 -1
  132. package/dist/elements/popover.js +1 -1
  133. package/dist/elements/progress-bar.js +1 -1
  134. package/dist/elements/prompt-dock.js +1 -1
  135. package/dist/elements/prompt-input.js +1 -1
  136. package/dist/elements/prompt-suggestions.js +1 -1
  137. package/dist/elements/reasoning.js +1 -1
  138. package/dist/elements/remote.js +1 -1
  139. package/dist/elements/resizable.js +1 -1
  140. package/dist/elements/response-stream.js +1 -1
  141. package/dist/elements/screen.js +1 -1
  142. package/dist/elements/scroll-area.js +1 -1
  143. package/dist/elements/scroll-button.js +1 -1
  144. package/dist/elements/search.js +1 -1
  145. package/dist/elements/segmented.js +1 -1
  146. package/dist/elements/separator.js +1 -1
  147. package/dist/elements/setting-item.js +1 -1
  148. package/dist/elements/settings-group.js +1 -1
  149. package/dist/elements/skeleton.js +1 -1
  150. package/dist/elements/source.js +1 -1
  151. package/dist/elements/status.js +1 -1
  152. package/dist/elements/switch.js +1 -1
  153. package/dist/elements/tabs.js +1 -1
  154. package/dist/elements/tasks.js +1 -1
  155. package/dist/elements/text-shimmer.js +1 -1
  156. package/dist/elements/thinking-bar.js +1 -1
  157. package/dist/elements/thread.js +1 -1
  158. package/dist/elements/toast.js +1 -1
  159. package/dist/elements/tool.js +1 -1
  160. package/dist/elements/tooltip.js +1 -1
  161. package/dist/elements/voice-input.js +1 -1
  162. package/dist/elements/voice-output.js +1 -1
  163. package/dist/index.js +1423 -1425
  164. package/dist/index.server.js +1212 -1214
  165. package/dist/kai.es.js +1 -1
  166. package/dist/mcp.es.js +376 -118
  167. package/dist/{register-impl-AxIlqjo7.js → register-impl-C96AnOJn.js} +18 -18
  168. package/dist/{solid-Dt2nEwfl.js → solid-BuqWLn6x.js} +1034 -1036
  169. package/dist/{solid-BGYSJyHG.js → solid-C76UXJuy.js} +1719 -1721
  170. package/dist/solid.js +1 -1
  171. package/dist/solid.server.js +1 -1
  172. package/dist/{variant-aurora-Ds5ou9BH.js → variant-aurora-CD35fdJF.js} +2 -2
  173. package/dist/{variant-aurora-B44X69AG.js → variant-aurora-Dau5Z4yR.js} +1 -1
  174. package/dist/{variant-aurora-BRhV8Flk.js → variant-aurora-Dc-2RVH9.js} +2 -2
  175. package/dist/{variant-custom-D6Rtvneb.js → variant-custom-CHr_jbMU.js} +2 -2
  176. package/dist/{variant-custom-B20FhtYj.js → variant-custom-Cyu8zx4A.js} +2 -2
  177. package/dist/{variant-custom-BvA1Schl.js → variant-custom-Df7QvXyZ.js} +1 -1
  178. package/dist/{variant-wave-DXC6_LcH.js → variant-wave-BkVpxkVF.js} +2 -2
  179. package/dist/{variant-wave-BUQtkryq.js → variant-wave-CXcaP_9W.js} +1 -1
  180. package/dist/{variant-wave-BVYnqy9L.js → variant-wave-DcGqjVnK.js} +2 -2
  181. package/package.json +5 -2
  182. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +255 -14
  183. package/src/agent-tooling/mcp/tools/scaffold.ts +30 -188
  184. package/src/agent-tooling/route-emit.ts +281 -0
  185. package/src/components/file-tree.tsx +7 -3
  186. package/src/elements/compiled.css +1 -1
  187. package/dist/elements/chunks/arrow-left-CFPTeL1Y.js +0 -1
  188. package/dist/elements/chunks/check-7Q3K-iXE.js +0 -1
  189. package/dist/elements/chunks/chevron-down-CpNYLn7E.js +0 -1
  190. package/dist/elements/chunks/chevron-right-DrH85Fha.js +0 -1
  191. package/dist/elements/chunks/circle-C9KRjKYv.js +0 -1
  192. package/dist/elements/chunks/circle-check-Dmi7Y4zw.js +0 -1
  193. package/dist/elements/chunks/file-tree-0upJ5jeb.js +0 -1
  194. package/dist/elements/chunks/message-square-hXylwsy0.js +0 -1
  195. package/dist/elements/chunks/x-_-OPHept.js +0 -1
@@ -8,6 +8,13 @@ import {
8
8
  listArchetypes,
9
9
  listIntegrations,
10
10
  } from '../../registry';
11
+ // The route-emission facts, read rather than restated — and deliberately living
12
+ // OUTSIDE this file. This module builds its zod input schema at module scope, so
13
+ // a bundler asked for one of these three has to keep all 5,300 lines of it and
14
+ // all of zod with them; `create-kai`'s CLI bundle went 203 kB -> 904 kB when they
15
+ // were exported from here. See the header of `route-emit.ts`. Import FROM there,
16
+ // never re-export through here.
17
+ import { chatRoutePreamble, defaultModelFor } from '../../route-emit';
11
18
  // The kit's media-type declaration, read rather than restated. This is the one
12
19
  // import in agent-tooling that reaches outside itself, and the module it reaches
13
20
  // for is the reason: `wire/media-types.ts` is pure (no I/O, no DOM, no solid-js),
@@ -68,11 +75,11 @@ interface PlacementStyle {
68
75
  altNote?: string[];
69
76
  }
70
77
 
71
- // The chat element must fill its container. In a `display: flex; flex-direction:
72
- // column` shell it's a flex child (`flex: 1; min-height: 0`); in a plain block
73
- // container it fills via `height: 100%`.
78
+ // The chat element must fill its container. Every placement below is a
79
+ // `display: flex; flex-direction: column` shell, so the element is always a flex
80
+ // child and this is the only fill there is. A `height: 100%` variant for a plain
81
+ // block container used to sit here beside it; nothing ever selected it.
74
82
  const FLEX_FILL = 'flex: 1; min-height: 0;';
75
- const BLOCK_FILL = 'height: 100%; width: 100%;';
76
83
 
77
84
  /**
78
85
  * full-page, and it has to be true in a STOCK starter — not just in an empty page.
@@ -808,65 +815,6 @@ function realBodyPayload(opts: { defaultModel?: string; tools: boolean }): (thre
808
815
  };
809
816
  }
810
817
 
811
- // ── SCAF-8: per-integration default model ids ─────────────────────────────────
812
-
813
- /**
814
- * Default model id per integration whose route forwards one.
815
- *
816
- * THE ID IS HOST-SPECIFIC, and there is no such thing as a safe generic one.
817
- * This used to fall through to `'openai/gpt-4o-mini'` for anything unlisted, on
818
- * the reasoning that "a route that forwards the client's model is by definition
819
- * pointed at an OpenAI-compatible endpoint". That was false twice over the
820
- * moment a first-party provider landed: `openai/gpt-4o-mini` is an OPENROUTER
821
- * slug — api.openai.com 404s the prefixed form, and api.anthropic.com rejects it
822
- * outright — so a scaffold generated for the provider it names could not run
823
- * against it.
824
- *
825
- * tsc cannot see any of this; every one of those strings compiles. The guard is
826
- * `scaffold.test.ts` → "the emitted model id is valid for the host its route
827
- * POSTs to", which reads the id out of the EMITTED scaffold and the host out of
828
- * the route source, so a new integration cannot reintroduce a wrong one.
829
- */
830
- const CLIENT_MODEL_IDS: Record<string, string> = {
831
- // Vendor-prefixed `vendor/model`: OpenRouter's own id space, and the ONLY one
832
- // of the three where the prefix belongs.
833
- openrouter: 'openai/gpt-4o-mini',
834
- // No vendor prefix. This is what the route already pinned, so moving the knob
835
- // to the client changes the wire not at all.
836
- openai: 'gpt-4o-mini',
837
- // Anthropic's id space. Matches what the route pinned; 'claude-sonnet-5' and
838
- // 'claude-haiku-4-5' are the cheaper swaps (see this integration's runNote).
839
- anthropic: 'claude-opus-5',
840
- };
841
-
842
- /**
843
- * The default model id for an integration whose ROUTE reads the client's `model`
844
- * field, and undefined for every other one.
845
- *
846
- * This used to be a substring test (`routeSrc.includes('model')`), which is true
847
- * of any template that so much as writes `model: 'llama3.2'`. That emitted an
848
- * editable `const model` into ollama, langgraph, vercel-ai-sdk and cloudflare
849
- * scaffolds whose routes pin their own model and never read the field, so
850
- * changing it did nothing, and cloudflare's default was not even a valid Workers
851
- * AI id. `forwardsFromClient` states the fact instead of guessing at it.
852
- */
853
- function defaultModelFor(integration: Integration): string | undefined {
854
- if (!integration.forwardsFromClient.includes('model')) return undefined;
855
- const id = CLIENT_MODEL_IDS[integration.id];
856
- // No fallback, deliberately — the old `?? 'openai/gpt-4o-mini'` is what let a
857
- // first-party provider inherit an OpenRouter slug and emit a scaffold that
858
- // 404s on its own host. A model id is a per-host fact; an integration that
859
- // forwards one has to say which.
860
- if (id === undefined) {
861
- throw new Error(
862
- `Integration '${integration.id}' forwards the client's 'model' but has no CLIENT_MODEL_IDS entry, so the ` +
863
- `scaffold would emit a model id that is not valid for the host its route POSTs to. Add one in ` +
864
- `mcp/tools/scaffold.ts.`,
865
- );
866
- }
867
- return id;
868
- }
869
-
870
818
  /**
871
819
  * True when the scaffold should declare tool schemas and put them in the body.
872
820
  *
@@ -3499,7 +3447,17 @@ function renderSolid(components: readonly string[], ctx: RenderCtx): string {
3499
3447
  ` // shortened the array.`,
3500
3448
  ` <Show when={messages()[i()]}>`,
3501
3449
  ` {(m) => (`,
3502
- ` <Message class={\`mx-auto flex w-full max-w-3xl flex-col gap-2 px-6 \${m().role === 'user' ? 'items-end' : 'items-start'}\`}>`,
3450
+ ` <Message`,
3451
+ ` // \`role\` is the SPEAKER, not an ARIA role — <Message> turns it`,
3452
+ ` // into role="article" plus an aria-label naming the speaker, and`,
3453
+ ` // a data-role hook for styling. Omit it and the row is an`,
3454
+ ` // unlabelled <div> that tells a screen reader nothing about who`,
3455
+ ` // is talking. The alignment class below reads the SAME value: if`,
3456
+ ` // you are deriving CSS from a semantic field, the semantics have`,
3457
+ ` // to reach the DOM too.`,
3458
+ ` role={m().role}`,
3459
+ ` class={\`mx-auto flex w-full max-w-3xl flex-col gap-2 px-6 \${m().role === 'user' ? 'items-end' : 'items-start'}\`}`,
3460
+ ` >`,
3503
3461
  ` {/* <Index>, not <For>, INSIDE a message. The state folds only`,
3504
3462
  ` ever append a part or patch one in place — they never`,
3505
3463
  ` reorder — so a part's POSITION is a stable identity, and`,
@@ -4478,122 +4436,6 @@ const WEB_ROUTE_ADAPTERS: Record<string, WebRouteAdapter> = {
4478
4436
  },
4479
4437
  };
4480
4438
 
4481
- /**
4482
- * The request body, declared once per route file.
4483
- *
4484
- * `await request.json()` is `unknown` — it is whatever the client sent — so
4485
- * destructuring it directly is TS2339 on EVERY field. That is not pedantry: it
4486
- * is a hard `npm run build` failure the moment a Node-typed project compiles the
4487
- * route, where `Request` comes from undici (`json(): Promise<unknown>`) rather
4488
- * than from the DOM lib (`json(): Promise<any>`). A stock Vite app does exactly
4489
- * that — `tsc -b` walks vite.config.ts → vite-chat-api.ts → src/server/chat.ts
4490
- * with `lib` and no DOM — so the route ran fine and the build did not.
4491
- *
4492
- * `messages` is typed as the kit's OWN encoder output rather than restated
4493
- * structurally, which keeps the two halves of the scaffold pinned to one type:
4494
- * the front end sends `toOpenAIMessages(thread)`, and this is what that returns.
4495
- * The import is type-only and erases at build time, so the route ships no
4496
- * runtime dependency on the kit.
4497
- */
4498
- const CHAT_REQUEST_BODY_IMPORT = `import type { OpenAIWireMessage } from '@kitn.ai/ui/wire';`;
4499
- const CHAT_REQUEST_BODY_DECL = [
4500
- `/**`,
4501
- ` * What the front end POSTs. \`request.json()\` is \`unknown\` (it is whatever the`,
4502
- ` * client sent), so the body is narrowed once here instead of at every use —`,
4503
- ` * without it this route does not compile under a server tsconfig. Widen it as`,
4504
- ` * you add fields of your own.`,
4505
- ` */`,
4506
- `type ChatRequestBody = {`,
4507
- ` messages: OpenAIWireMessage[];`,
4508
- ` model?: string;`,
4509
- ` tools?: unknown[];`,
4510
- `};`,
4511
- ``,
4512
- `/** Narrow the JSON body once, at the edge. */`,
4513
- `async function readChatRequest(request: Request): Promise<ChatRequestBody> {`,
4514
- ` return (await request.json()) as ChatRequestBody;`,
4515
- `}`,
4516
- ];
4517
-
4518
- /**
4519
- * Attachments, on the way IN.
4520
- *
4521
- * A user turn's `content` is a plain string until it carries a file, at which
4522
- * point `toOpenAIMessages` emits the ARRAY form. Every route that re-maps
4523
- * messages into some other SDK's shape has to handle both, and the three that do
4524
- * (anthropic, mastra, vercel-ai-sdk) were each written when only the string form
4525
- * existed — so each one would have quietly dropped the attachment while still
4526
- * compiling, which is the same defect the encoder was just fixed for.
4527
- *
4528
- * These two helpers are the shared half of that: flattening the wire shape is
4529
- * identical everywhere, while the target shape is not, so each route maps
4530
- * `WirePart[]` into its own SDK itself rather than inheriting a lowest common
4531
- * denominator.
4532
- *
4533
- * Only injected into routes that actually call them — the eight pass-through
4534
- * integrations forward `messages` untouched and need none of this, and an unused
4535
- * declaration is a hard error under the gate's `--noUnusedLocals`.
4536
- */
4537
- const CONTENT_PARTS_DECL = [
4538
- `/** Where an attachment's bytes are: inline base64, or an address the PROVIDER`,
4539
- ` * fetches. Never both. */`,
4540
- `type WireFileSource = { type: 'data'; data: string } | { type: 'url'; url: string };`,
4541
- ``,
4542
- `/** One piece of a turn, with the string and array content forms flattened into`,
4543
- ` * a single shape. */`,
4544
- `type WirePart =`,
4545
- ` | { kind: 'text'; text: string }`,
4546
- ` | { kind: 'file'; mediaType: string; filename?: string; source: WireFileSource };`,
4547
- ``,
4548
- `const DATA_URI = /^data:([^;,]+);base64,([\\s\\S]*)$/;`,
4549
- ``,
4550
- `/**`,
4551
- ` * Flatten a wire message's content into parts.`,
4552
- ` *`,
4553
- ` * An image sent by URL has no media type here — \`image_url\` carries only the`,
4554
- ` * address — so it reports the top-level segment \`'image'\`, which is all a URL`,
4555
- ` * source needs. Only images can reach that branch: the kit refuses to encode a`,
4556
- ` * remote PDF rather than guess at one.`,
4557
- ` */`,
4558
- `function wireParts(content: OpenAIWireMessage['content']): WirePart[] {`,
4559
- ` if (content == null) return [];`,
4560
- ` if (typeof content === 'string') return content === '' ? [] : [{ kind: 'text', text: content }];`,
4561
- ` return content.map((part): WirePart => {`,
4562
- ` if (part.type === 'text') return { kind: 'text', text: part.text };`,
4563
- ` if (part.type === 'image_url') {`,
4564
- ` const asData = DATA_URI.exec(part.image_url.url);`,
4565
- ` return asData`,
4566
- ` ? { kind: 'file', mediaType: asData[1], source: { type: 'data', data: asData[2] } }`,
4567
- ` : { kind: 'file', mediaType: 'image', source: { type: 'url', url: part.image_url.url } };`,
4568
- ` }`,
4569
- ` const asData = DATA_URI.exec(part.file.file_data);`,
4570
- ` if (!asData) {`,
4571
- ` // LOUD on purpose. \`file_data\` is a data URI on this wire; anything else`,
4572
- ` // cannot be turned into bytes without fetching it, and forwarding a turn`,
4573
- ` // with the attachment quietly missing is the bug this whole path exists`,
4574
- ` // to prevent.`,
4575
- ` throw new Error(`,
4576
- ` 'Unsupported file content part: file_data must be a data: URI of the form data:<media type>;base64,<data>.',`,
4577
- ` );`,
4578
- ` }`,
4579
- ` return {`,
4580
- ` kind: 'file',`,
4581
- ` mediaType: asData[1],`,
4582
- ` filename: part.file.filename,`,
4583
- ` source: { type: 'data', data: asData[2] },`,
4584
- ` };`,
4585
- ` });`,
4586
- `}`,
4587
- ``,
4588
- `/** Just the text of a turn. System, assistant and tool messages are text-only`,
4589
- ` * on this wire, so this collapses the array form for them. */`,
4590
- `function wireText(content: OpenAIWireMessage['content']): string {`,
4591
- ` return wireParts(content)`,
4592
- ` .map((p) => (p.kind === 'text' ? p.text : ''))`,
4593
- ` .join('');`,
4594
- `}`,
4595
- ];
4596
-
4597
4439
  /**
4598
4440
  * Slot the body type in just above `chatHandler`.
4599
4441
  *
@@ -4603,12 +4445,7 @@ const CONTENT_PARTS_DECL = [
4603
4445
  * block that documents it — puts the declaration where a person would have
4604
4446
  * written it.
4605
4447
  */
4606
- function withChatRequestBody(fragment: string): string {
4607
- // The content helpers ride along only where the route calls them; see
4608
- // CONTENT_PARTS_DECL for why an unconditional injection would not compile.
4609
- const decl = /\bwire(?:Parts|Text)\s*\(/.test(fragment)
4610
- ? [...CHAT_REQUEST_BODY_DECL, ``, ...CONTENT_PARTS_DECL]
4611
- : CHAT_REQUEST_BODY_DECL;
4448
+ function withChatRequestBody(fragment: string, decl: readonly string[]): string {
4612
4449
  const lines = fragment.split('\n');
4613
4450
  let at = lines.findIndex((l) => /^(?:export\s+)?async function chatHandler\b/.test(l));
4614
4451
  if (at < 0) return [...decl, ``, ...lines].join('\n');
@@ -4625,17 +4462,22 @@ function webRouteFor(integration: Integration, framework: string): RouteChoice |
4625
4462
  // pull in an import to do it (SvelteKit's $env accessor). Both land at the top
4626
4463
  // of the file, beside the adapter's own `before` lines.
4627
4464
  const adapted = adapter.adaptFragment?.(fragment) ?? { fragment, imports: [] };
4465
+ // One preamble for the whole file: its `imports` go at the top beside the
4466
+ // adapter's own, its `decl` immediately above the handler. Both halves come
4467
+ // from `chatRoutePreamble` rather than from a constant here, so this emitter
4468
+ // and `create-kai`'s cannot answer "what does this route need" differently.
4469
+ const preamble = chatRoutePreamble(adapted.fragment);
4628
4470
  return {
4629
4471
  framework,
4630
4472
  runtime: adapter.runtime,
4631
4473
  exact: true,
4632
4474
  template: [
4633
4475
  `// ${adapter.file}`,
4634
- CHAT_REQUEST_BODY_IMPORT,
4476
+ ...preamble.imports,
4635
4477
  ...(adapter.before ?? []),
4636
4478
  ...adapted.imports,
4637
4479
  ``,
4638
- withChatRequestBody(adapted.fragment),
4480
+ withChatRequestBody(adapted.fragment, preamble.decl),
4639
4481
  ...adapter.after,
4640
4482
  ].join('\n'),
4641
4483
  };
@@ -0,0 +1,281 @@
1
+ /**
2
+ * What a second emitter needs in order to write a chat route to a real file.
3
+ *
4
+ * Three facts live here, and they are the three a consumer that writes handlers
5
+ * to disk cannot do without: `chatRoutePreamble` (what goes above a bare
6
+ * `Integration.webRoute` so it compiles), and `CLIENT_MODEL_IDS` +
7
+ * `defaultModelFor` (whether the front end sends a model, and which id is valid
8
+ * for the host its route POSTs to). The `kai` MCP's own emitter in
9
+ * `mcp/tools/scaffold.ts` imports them from here, so there is one code path and
10
+ * the two emitters cannot disagree.
11
+ *
12
+ * WHY THIS IS ITS OWN MODULE, AND WHY IT MUST STAY A LEAF. These three were
13
+ * exported from `mcp/tools/scaffold.ts`, which is ~5,300 lines and builds a zod
14
+ * schema at MODULE SCOPE (`scaffold.inputSchema = z.object({…})`). A side effect
15
+ * at module scope is not tree-shakeable, so a bundler asked for
16
+ * `chatRoutePreamble` alone had to keep the whole file and all of zod with it.
17
+ * Measured on `create-kai`, whose CLI is one bundled zero-dependency file:
18
+ * **203 kB -> 904 kB**, of which 505 kB was zod the CLI never executes, on every
19
+ * `npx create-kai`. It also pulled 5,300 lines of kit source into `create-kai`'s
20
+ * `tsc` program under `--noUnusedLocals`, a flag the kit's own typecheck does not
21
+ * run, which forced an unrelated deletion in the kit to keep the CLI green.
22
+ *
23
+ * So the rule for this file is a rule about its IMPORTS, not its size: nothing
24
+ * here may import a module that reaches zod, the registry, or the integration
25
+ * catalog. `import type` is fine — it erases. Today that means exactly one
26
+ * import, and it is type-only. `create-kai`'s build guard
27
+ * (`bundleGraphProblem` in its `src/build-guards.ts`) fails the CLI build if zod
28
+ * ever reappears in the bundle, which is the check that turns this paragraph
29
+ * into something that can go red.
30
+ *
31
+ * NOT RE-EXPORTED from `mcp/tools/scaffold.ts`, deliberately. A second import
32
+ * path for the same symbol is how the fat one gets picked again.
33
+ */
34
+ import type { Integration } from './types';
35
+
36
+ // ── SCAF-8: per-integration default model ids ─────────────────────────────────
37
+
38
+ /**
39
+ * Default model id per integration whose route forwards one.
40
+ *
41
+ * THE ID IS HOST-SPECIFIC, and there is no such thing as a safe generic one.
42
+ * This used to fall through to `'openai/gpt-4o-mini'` for anything unlisted, on
43
+ * the reasoning that "a route that forwards the client's model is by definition
44
+ * pointed at an OpenAI-compatible endpoint". That was false twice over the
45
+ * moment a first-party provider landed: `openai/gpt-4o-mini` is an OPENROUTER
46
+ * slug — api.openai.com 404s the prefixed form, and api.anthropic.com rejects it
47
+ * outright — so a scaffold generated for the provider it names could not run
48
+ * against it.
49
+ *
50
+ * tsc cannot see any of this; every one of those strings compiles. The guard is
51
+ * `scaffold.test.ts` → "the emitted model id is valid for the host its route
52
+ * POSTs to", which reads the id out of the EMITTED scaffold and the host out of
53
+ * the route source, so a new integration cannot reintroduce a wrong one.
54
+ *
55
+ * EXPORTED because a route is only half of the fact. `openrouter`'s handler puts
56
+ * the client's `model` straight into its upstream payload, so a front end that
57
+ * posts `{ messages }` alone sends no model, OpenRouter answers 400, and nothing
58
+ * in a build or a typecheck can see it. Any second emitter of these routes needs
59
+ * the same table; `defaultModelFor` below is the accessor, and the raw table is
60
+ * exported alongside it so a consumer can also check it for drift at ITS build
61
+ * time rather than discovering a missing row when a user types.
62
+ */
63
+ export const CLIENT_MODEL_IDS: Record<string, string> = {
64
+ // Vendor-prefixed `vendor/model`: OpenRouter's own id space, and the ONLY one
65
+ // of the three where the prefix belongs.
66
+ openrouter: 'openai/gpt-4o-mini',
67
+ // No vendor prefix. This is what the route already pinned, so moving the knob
68
+ // to the client changes the wire not at all.
69
+ openai: 'gpt-4o-mini',
70
+ // Anthropic's id space. Matches what the route pinned; 'claude-sonnet-5' and
71
+ // 'claude-haiku-4-5' are the cheaper swaps (see this integration's runNote).
72
+ anthropic: 'claude-opus-5',
73
+ };
74
+
75
+ /**
76
+ * The default model id for an integration whose ROUTE reads the client's `model`
77
+ * field, and undefined for every other one.
78
+ *
79
+ * This used to be a substring test (`routeSrc.includes('model')`), which is true
80
+ * of any template that so much as writes `model: 'llama3.2'`. That emitted an
81
+ * editable `const model` into ollama, langgraph, vercel-ai-sdk and cloudflare
82
+ * scaffolds whose routes pin their own model and never read the field, so
83
+ * changing it did nothing, and cloudflare's default was not even a valid Workers
84
+ * AI id. `forwardsFromClient` states the fact instead of guessing at it.
85
+ *
86
+ * Exported for the same reason as `CLIENT_MODEL_IDS`: a second emitter of these
87
+ * routes has to answer "does this front end send a model, and which" the same
88
+ * way, and both halves of that answer live here.
89
+ */
90
+ export function defaultModelFor(integration: Integration): string | undefined {
91
+ if (!integration.forwardsFromClient.includes('model')) return undefined;
92
+ const id = CLIENT_MODEL_IDS[integration.id];
93
+ // No fallback, deliberately — the old `?? 'openai/gpt-4o-mini'` is what let a
94
+ // first-party provider inherit an OpenRouter slug and emit a scaffold that
95
+ // 404s on its own host. A model id is a per-host fact; an integration that
96
+ // forwards one has to say which.
97
+ if (id === undefined) {
98
+ throw new Error(
99
+ `Integration '${integration.id}' forwards the client's 'model' but has no CLIENT_MODEL_IDS entry, so the ` +
100
+ `scaffold would emit a model id that is not valid for the host its route POSTs to. Add one in ` +
101
+ `agent-tooling/route-emit.ts.`,
102
+ );
103
+ }
104
+ return id;
105
+ }
106
+
107
+ /**
108
+ * The request body, declared once per route file.
109
+ *
110
+ * `await request.json()` is `unknown` — it is whatever the client sent — so
111
+ * destructuring it directly is TS2339 on EVERY field. That is not pedantry: it
112
+ * is a hard `npm run build` failure the moment a Node-typed project compiles the
113
+ * route, where `Request` comes from undici (`json(): Promise<unknown>`) rather
114
+ * than from the DOM lib (`json(): Promise<any>`). A stock Vite app does exactly
115
+ * that — `tsc -b` walks vite.config.ts → vite-chat-api.ts → src/server/chat.ts
116
+ * with `lib` and no DOM — so the route ran fine and the build did not.
117
+ *
118
+ * `messages` is typed as the kit's OWN encoder output rather than restated
119
+ * structurally, which keeps the two halves of the scaffold pinned to one type:
120
+ * the front end sends `toOpenAIMessages(thread)`, and this is what that returns.
121
+ * The import is type-only and erases at build time, so the route ships no
122
+ * runtime dependency on the kit.
123
+ */
124
+ const CHAT_REQUEST_BODY_IMPORT = `import type { OpenAIWireMessage } from '@kitn.ai/ui/wire';`;
125
+ const CHAT_REQUEST_BODY_DECL = [
126
+ `/**`,
127
+ ` * What the front end POSTs. \`request.json()\` is \`unknown\` (it is whatever the`,
128
+ ` * client sent), so the body is narrowed once here instead of at every use —`,
129
+ ` * without it this route does not compile under a server tsconfig. Widen it as`,
130
+ ` * you add fields of your own.`,
131
+ ` */`,
132
+ `type ChatRequestBody = {`,
133
+ ` messages: OpenAIWireMessage[];`,
134
+ ` model?: string;`,
135
+ ` tools?: unknown[];`,
136
+ `};`,
137
+ ``,
138
+ `/** Narrow the JSON body once, at the edge. */`,
139
+ `async function readChatRequest(request: Request): Promise<ChatRequestBody> {`,
140
+ ` return (await request.json()) as ChatRequestBody;`,
141
+ `}`,
142
+ ];
143
+
144
+ /**
145
+ * Attachments, on the way IN.
146
+ *
147
+ * A user turn's `content` is a plain string until it carries a file, at which
148
+ * point `toOpenAIMessages` emits the ARRAY form. Every route that re-maps
149
+ * messages into some other SDK's shape has to handle both, and the three that do
150
+ * (anthropic, mastra, vercel-ai-sdk) were each written when only the string form
151
+ * existed — so each one would have quietly dropped the attachment while still
152
+ * compiling, which is the same defect the encoder was just fixed for.
153
+ *
154
+ * These two helpers are the shared half of that: flattening the wire shape is
155
+ * identical everywhere, while the target shape is not, so each route maps
156
+ * `WirePart[]` into its own SDK itself rather than inheriting a lowest common
157
+ * denominator.
158
+ *
159
+ * Only injected into routes that actually call them — the eight pass-through
160
+ * integrations forward `messages` untouched and need none of this, and an unused
161
+ * declaration is a hard error under the gate's `--noUnusedLocals`.
162
+ */
163
+ const CONTENT_PARTS_DECL = [
164
+ `/** Where an attachment's bytes are: inline base64, or an address the PROVIDER`,
165
+ ` * fetches. Never both. */`,
166
+ `type WireFileSource = { type: 'data'; data: string } | { type: 'url'; url: string };`,
167
+ ``,
168
+ `/** One piece of a turn, with the string and array content forms flattened into`,
169
+ ` * a single shape. */`,
170
+ `type WirePart =`,
171
+ ` | { kind: 'text'; text: string }`,
172
+ ` | { kind: 'file'; mediaType: string; filename?: string; source: WireFileSource };`,
173
+ ``,
174
+ `const DATA_URI = /^data:([^;,]+);base64,([\\s\\S]*)$/;`,
175
+ ``,
176
+ `/**`,
177
+ ` * Flatten a wire message's content into parts.`,
178
+ ` *`,
179
+ ` * An image sent by URL has no media type here — \`image_url\` carries only the`,
180
+ ` * address — so it reports the top-level segment \`'image'\`, which is all a URL`,
181
+ ` * source needs. Only images can reach that branch: the kit refuses to encode a`,
182
+ ` * remote PDF rather than guess at one.`,
183
+ ` */`,
184
+ `function wireParts(content: OpenAIWireMessage['content']): WirePart[] {`,
185
+ ` if (content == null) return [];`,
186
+ ` if (typeof content === 'string') return content === '' ? [] : [{ kind: 'text', text: content }];`,
187
+ ` return content.map((part): WirePart => {`,
188
+ ` if (part.type === 'text') return { kind: 'text', text: part.text };`,
189
+ ` if (part.type === 'image_url') {`,
190
+ ` const asData = DATA_URI.exec(part.image_url.url);`,
191
+ ` return asData`,
192
+ ` ? { kind: 'file', mediaType: asData[1], source: { type: 'data', data: asData[2] } }`,
193
+ ` : { kind: 'file', mediaType: 'image', source: { type: 'url', url: part.image_url.url } };`,
194
+ ` }`,
195
+ ` const asData = DATA_URI.exec(part.file.file_data);`,
196
+ ` if (!asData) {`,
197
+ ` // LOUD on purpose. \`file_data\` is a data URI on this wire; anything else`,
198
+ ` // cannot be turned into bytes without fetching it, and forwarding a turn`,
199
+ ` // with the attachment quietly missing is the bug this whole path exists`,
200
+ ` // to prevent.`,
201
+ ` throw new Error(`,
202
+ ` 'Unsupported file content part: file_data must be a data: URI of the form data:<media type>;base64,<data>.',`,
203
+ ` );`,
204
+ ` }`,
205
+ ` return {`,
206
+ ` kind: 'file',`,
207
+ ` mediaType: asData[1],`,
208
+ ` filename: part.file.filename,`,
209
+ ` source: { type: 'data', data: asData[2] },`,
210
+ ` };`,
211
+ ` });`,
212
+ `}`,
213
+ ``,
214
+ `/** Just the text of a turn. System, assistant and tool messages are text-only`,
215
+ ` * on this wire, so this collapses the array form for them. */`,
216
+ `function wireText(content: OpenAIWireMessage['content']): string {`,
217
+ ` return wireParts(content)`,
218
+ ` .map((p) => (p.kind === 'text' ? p.text : ''))`,
219
+ ` .join('');`,
220
+ `}`,
221
+ ];
222
+
223
+ /**
224
+ * What a bare `Integration.webRoute` needs above it before it will compile.
225
+ *
226
+ * WHY THIS IS A FUNCTION AND NOT TWO EXPORTED CONSTANTS. `webRoute` reads like a
227
+ * self-contained handler and is not one: every fragment in the catalog calls
228
+ * `readChatRequest`, and three of them (anthropic, mastra, vercel-ai-sdk) also
229
+ * call `wireParts` / `wireText` and annotate with `WirePart`. Measured over the
230
+ * catalog, a bare fragment under `tsc --strict` is TS2304 on five distinct names.
231
+ * A consumer handed `CHAT_REQUEST_BODY_DECL` alone would therefore emit five
232
+ * routes that compile and three that do not, and would find out only for the
233
+ * ones it happened to wire first. Asking the question the caller actually has —
234
+ * "what goes above THIS fragment" — is the shape that cannot be answered
235
+ * half-right, so that is what is exported.
236
+ *
237
+ * `symbols` is DERIVED from `decl` rather than listed beside it, so a rename
238
+ * inside either declaration block moves it and a consumer's drift guard grades
239
+ * the real text.
240
+ */
241
+ export interface ChatRoutePreamble {
242
+ /** import lines the declarations need, emitted at the top of the route file */
243
+ readonly imports: readonly string[];
244
+ /** the declaration lines, emitted immediately above the handler */
245
+ readonly decl: readonly string[];
246
+ /** every symbol `decl` declares, read back out of `decl` */
247
+ readonly symbols: readonly string[];
248
+ }
249
+
250
+ /** A top-level declaration in a preamble block. Indented lines are bodies. */
251
+ const PREAMBLE_DECLARATION = /^(?:export\s+)?(?:async\s+)?(?:function|type|interface|const|class)\s+([A-Za-z_$][\w$]*)/;
252
+
253
+ /**
254
+ * The preamble a given handler fragment needs.
255
+ *
256
+ * Used by the MCP below to build its own route blocks, and exported for
257
+ * `create-kai`, which writes the same handler to a real file that has to
258
+ * compile. One code path, so the two cannot disagree about what a route needs.
259
+ */
260
+ export function chatRoutePreamble(fragment: string): ChatRoutePreamble {
261
+ // The content helpers ride along only where the route calls them; see
262
+ // CONTENT_PARTS_DECL for why an unconditional injection would not compile.
263
+ //
264
+ // KNOWN GAP, live but not yet reachable: this tests for a CALL, so a fragment
265
+ // that only annotates with `WirePart` / `WireFileSource` and never calls
266
+ // either helper would be told it needs nothing and would not compile. No
267
+ // catalog route is shaped that way today — vercel-ai-sdk is the only one that
268
+ // names the type, and it calls `wireParts` two lines later — so widening the
269
+ // test would change no emitted byte, and that is exactly why it is left alone
270
+ // here rather than fixed blind. `scaffold.test.ts` → "webRoute has no symbol
271
+ // its own preamble fails to declare" is the check that turns this from a
272
+ // comment into a failing build the day an integration lands in that shape.
273
+ const decl = /\bwire(?:Parts|Text)\s*\(/.test(fragment)
274
+ ? [...CHAT_REQUEST_BODY_DECL, ``, ...CONTENT_PARTS_DECL]
275
+ : [...CHAT_REQUEST_BODY_DECL];
276
+ return {
277
+ imports: [CHAT_REQUEST_BODY_IMPORT],
278
+ decl,
279
+ symbols: decl.flatMap((line) => PREAMBLE_DECLARATION.exec(line)?.[1] ?? []),
280
+ };
281
+ }
@@ -5,7 +5,6 @@ import {
5
5
  splitProps,
6
6
  createSignal,
7
7
  createMemo,
8
- mergeProps,
9
8
  } from 'solid-js';
10
9
  import { cn } from '../utils/cn';
11
10
  import {
@@ -208,8 +207,13 @@ export interface FileTreeProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>,
208
207
  * calls `onSelect(path, file)`.
209
208
  */
210
209
  export function FileTree(props: FileTreeProps): JSX.Element {
211
- const merged = mergeProps({ files: [] as FileTreeFile[] }, props);
212
- const [local, rest] = splitProps(merged, [
210
+ // `files` is REQUIRED, so it is read straight off `props` — no `mergeProps`
211
+ // default. Defaulting it to `[]` here would turn a caller who forgot it into a
212
+ // silently empty tree, which fails far from the mistake. The `<kai-file-tree>`
213
+ // facade declares its own `files: []` default (elements/file-tree.tsx), so the
214
+ // web-component path is covered where the property genuinely arrives late.
215
+ // Same shape as `Segmented`, which reads `merged.options` directly.
216
+ const [local, rest] = splitProps(props, [
213
217
  'files',
214
218
  'activeFile',
215
219
  'onSelect',