@kitn.ai/ui 0.21.0 → 0.22.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 (243) hide show
  1. package/dist/components/chat-thread.d.ts +9 -0
  2. package/dist/{create-tween-D3Y5qiij.js → create-tween-COnPwAQN.js} +1 -1
  3. package/dist/{create-tween-DGKY1FDY.js → create-tween-DmTUaXWE.js} +1 -1
  4. package/dist/{create-tween-paho5Rh-.js → create-tween-qs-jjvNA.js} +1 -1
  5. package/dist/custom-elements.json +43 -7
  6. package/dist/elements/agent-card.js +1 -1
  7. package/dist/elements/artifact.js +1 -1
  8. package/dist/elements/attachments.js +1 -1
  9. package/dist/elements/audio-visualizer.js +1 -1
  10. package/dist/elements/avatar.js +1 -1
  11. package/dist/elements/badge.js +1 -1
  12. package/dist/elements/button.js +1 -1
  13. package/dist/elements/card.js +1 -1
  14. package/dist/elements/cards.js +1 -1
  15. package/dist/elements/chain-of-thought.js +1 -1
  16. package/dist/elements/chat-scope-picker.js +1 -1
  17. package/dist/elements/chat-workspace.js +1 -1
  18. package/dist/elements/chat.js +1 -1
  19. package/dist/elements/checkpoint.js +1 -1
  20. package/dist/elements/choice.js +1 -1
  21. package/dist/elements/chunks/{Icon-C-2LvGDG.js → Icon-CHG5z5L7.js} +1 -1
  22. package/dist/elements/chunks/{action-icons-CaYoImEk.js → action-icons-BAtCEEgg.js} +1 -1
  23. package/dist/elements/chunks/arrow-left-CFPTeL1Y.js +1 -0
  24. package/dist/elements/chunks/{artifact-BBIscOxJ.js → artifact-fBZOE4SO.js} +1 -1
  25. package/dist/elements/chunks/{attachments-BB56zoji.js → attachments-DpYMe5VQ.js} +1 -1
  26. package/dist/elements/chunks/{audio-visualizer-BxOx4THz.js → audio-visualizer-DFwzAcuL.js} +1 -1
  27. package/dist/elements/chunks/{badge-SxLSZ19R.js → badge-D6zodm6E.js} +1 -1
  28. package/dist/elements/chunks/{button-CmkUkz6c.js → button-C42vrGvO.js} +1 -1
  29. package/dist/elements/chunks/{card-renderer-Db4ffkWv.js → card-renderer-DMZsSuvU.js} +1 -1
  30. package/dist/elements/chunks/{chat-thread-B3Ks-cKr.js → chat-thread-CNNEhol-.js} +1 -1
  31. package/dist/elements/chunks/check-7Q3K-iXE.js +1 -0
  32. package/dist/elements/chunks/chevron-down-CpNYLn7E.js +1 -0
  33. package/dist/elements/chunks/chevron-right-DrH85Fha.js +1 -0
  34. package/dist/elements/chunks/{choice-card-Q9cYYDyM.js → choice-card-YoWjpsFV.js} +1 -1
  35. package/dist/elements/chunks/circle-C9KRjKYv.js +1 -0
  36. package/dist/elements/chunks/circle-check-Dmi7Y4zw.js +1 -0
  37. package/dist/elements/chunks/{circle-x-D0MEfxOM.js → circle-x-QIxXda6k.js} +1 -1
  38. package/dist/elements/chunks/{code-block-C8VzAraE.js → code-block-CqeugReY.js} +1 -1
  39. package/dist/elements/chunks/{collapsible-Cvj61obE.js → collapsible-sTv-_5lF.js} +1 -1
  40. package/dist/elements/chunks/{composer-D86ztyL0.js → composer-ldQyeVEB.js} +1 -1
  41. package/dist/elements/chunks/{confirm-card-BdyDAGLw.js → confirm-card-C0RNOwfn.js} +1 -1
  42. package/dist/elements/chunks/context-5qgypNHC.js +1 -0
  43. package/dist/elements/chunks/conversation-list-DLzOacXz.js +1 -0
  44. package/dist/elements/chunks/{create-tween-C_A_B638.js → create-tween-V4TXZ93X.js} +1 -1
  45. package/dist/elements/chunks/default-input-DeeRSDHc.js +1 -0
  46. package/dist/elements/chunks/{define-BzPnNsXg.js → define-lE-QvQFn.js} +1 -1
  47. package/dist/elements/chunks/{disclosure-DVKsoc1F.js → disclosure-DZSa9jog.js} +1 -1
  48. package/dist/elements/chunks/{download-BjA54IMJ.js → download-B2ZmnGqW.js} +1 -1
  49. package/dist/elements/chunks/{dropdown-BjVVHWXH.js → dropdown-R3B7cSRE.js} +1 -1
  50. package/dist/elements/chunks/{ellipsis-BNedmqfi.js → ellipsis-BmxceY4l.js} +1 -1
  51. package/dist/elements/chunks/{embed-DQsl4XEq.js → embed-BVrtTZPo.js} +1 -1
  52. package/dist/elements/chunks/{external-link-Di-dw2CU.js → external-link-BbdR7YO7.js} +1 -1
  53. package/dist/elements/chunks/{file-text-B3sAODCV.js → file-text-D7dijo1A.js} +1 -1
  54. package/dist/elements/chunks/{file-tree-Dgto-bMn.js → file-tree-0upJ5jeb.js} +1 -1
  55. package/dist/elements/chunks/{folder-D6rMadFV.js → folder-D8z58t7B.js} +1 -1
  56. package/dist/elements/chunks/{form-CILxvhQ7.js → form-p5BKJNw5.js} +1 -1
  57. package/dist/elements/chunks/{hover-card-BtiF4XiU.js → hover-card-CN9s2md5.js} +1 -1
  58. package/dist/elements/chunks/{icon-DKkmze3y.js → icon-CNiKP37E.js} +1 -1
  59. package/dist/elements/chunks/{info-CWRjZkuy.js → info-BSnuJiHI.js} +1 -1
  60. package/dist/elements/chunks/{input--QJBK6BA.js → input-BauDmzxX.js} +1 -1
  61. package/dist/elements/chunks/{kbd-C3zuKh8R.js → kbd-BlHJaP9b.js} +1 -1
  62. package/dist/elements/chunks/{link-BL5JA7Q0.js → link-DEi5iUWC.js} +1 -1
  63. package/dist/elements/chunks/{link-preview-CU37bbXh.js → link-preview-BaHyM6W4.js} +1 -1
  64. package/dist/elements/chunks/{loader-XmpyxyRH.js → loader-CmJG_Fkq.js} +1 -1
  65. package/dist/elements/chunks/{markdown-3Gv5UZ_o.js → markdown-D2jaMpCN.js} +1 -1
  66. package/dist/elements/chunks/{message-X5NZbq-W.js → message-C5UfXZ2Z.js} +1 -1
  67. package/dist/elements/chunks/{message-CnBA2OUt.js → message-CXWLmmLn.js} +1 -1
  68. package/dist/elements/chunks/message-square-hXylwsy0.js +1 -0
  69. package/dist/elements/chunks/{minimize-2-DwyrUTcM.js → minimize-2-C9ytZFMU.js} +1 -1
  70. package/dist/elements/chunks/model-switcher-CivGfD7-.js +1 -0
  71. package/dist/elements/chunks/{overlay-Cepd-KcS.js → overlay-DJL_sbOm.js} +1 -1
  72. package/dist/elements/chunks/{paperclip-CdwKjrlu.js → paperclip-CFaB8naM.js} +1 -1
  73. package/dist/elements/chunks/{progress-bar-DMVXBD3b.js → progress-bar-Br5SksfR.js} +1 -1
  74. package/dist/elements/chunks/{prompt-suggestion-Di_WZE_5.js → prompt-suggestion-CMIvtAhU.js} +1 -1
  75. package/dist/elements/chunks/{reasoning-D0jMch5u.js → reasoning-DpZcNzLq.js} +1 -1
  76. package/dist/elements/chunks/{resizable-1znbL1Gv.js → resizable-Q8i3T9a_.js} +1 -1
  77. package/dist/elements/chunks/{rotate-cw-DVhhgx8n.js → rotate-cw-B5IHCBFV.js} +1 -1
  78. package/dist/elements/chunks/{scroll-area-Bs4bFS1P.js → scroll-area-eLeydBje.js} +1 -1
  79. package/dist/elements/chunks/{scroll-button-Bet5NLc4.js → scroll-button-DWbz0yA1.js} +1 -1
  80. package/dist/elements/chunks/{separator-C5Gh8qGC.js → separator-Xic78AD_.js} +1 -1
  81. package/dist/elements/chunks/{settings-5vmEFg0K.js → settings-B8H51pAx.js} +1 -1
  82. package/dist/elements/chunks/{settings-group-BWLB38kz.js → settings-group-CsG9eYLe.js} +1 -1
  83. package/dist/elements/chunks/{share-BZ3G_DaB.js → share-CNTx0z__.js} +1 -1
  84. package/dist/elements/chunks/{skeleton-DafPJQTp.js → skeleton-CLlHTcMI.js} +1 -1
  85. package/dist/elements/chunks/{source-D_qj_6uJ.js → source-BoVcU5ok.js} +1 -1
  86. package/dist/elements/chunks/{star-Cuv7j2IE.js → star-CxQVTVhI.js} +1 -1
  87. package/dist/elements/chunks/{store-C1StPHfW.js → store-CLtS5CUG.js} +1 -1
  88. package/dist/elements/chunks/{tasks-card-EyoBZfpA.js → tasks-card-CzlkLmUt.js} +1 -1
  89. package/dist/elements/chunks/{text-shimmer-D9gRSh34.js → text-shimmer-D5NhpIQ1.js} +1 -1
  90. package/dist/elements/chunks/{textarea-DuhtHfcm.js → textarea-x5Uxsmtm.js} +1 -1
  91. package/dist/elements/chunks/{thumbs-up-B9hwv43w.js → thumbs-up-UobcJnx9.js} +1 -1
  92. package/dist/elements/chunks/{toast-store-Bm3DUEDe.js → toast-store-BOfs9eXq.js} +1 -1
  93. package/dist/elements/chunks/{tool-CDtW7L0z.js → tool-Be6Dh_dt.js} +1 -1
  94. package/dist/elements/chunks/{tooltip-eB226mGK.js → tooltip-RxMxyCLF.js} +1 -1
  95. package/dist/elements/chunks/{triangle-alert-C1SNzTqK.js → triangle-alert-C4yxH2XN.js} +1 -1
  96. package/dist/elements/chunks/{use-card-resolution-YZh2MvI5.js → use-card-resolution-jva9ehH6.js} +1 -1
  97. package/dist/elements/chunks/{variant-aurora-Cj1F7D0l.js → variant-aurora-Dx4VRcjS.js} +1 -1
  98. package/dist/elements/chunks/{variant-custom-T9gpt7QE.js → variant-custom-Y1ZTWgEa.js} +1 -1
  99. package/dist/elements/chunks/{variant-wave-CxU9z-TE.js → variant-wave-B9gml9Cg.js} +1 -1
  100. package/dist/elements/chunks/{video-CK4R7Rus.js → video-CrPHsytT.js} +1 -1
  101. package/dist/elements/chunks/x-_-OPHept.js +1 -0
  102. package/dist/elements/coachmark.js +1 -1
  103. package/dist/elements/code-block.js +1 -1
  104. package/dist/elements/command.js +1 -1
  105. package/dist/elements/compare.js +1 -1
  106. package/dist/elements/composer.js +1 -1
  107. package/dist/elements/confirm-card.js +1 -1
  108. package/dist/elements/context-meter.js +1 -1
  109. package/dist/elements/conversation-list.js +1 -1
  110. package/dist/elements/default-input.d.ts +27 -0
  111. package/dist/elements/dialog.js +1 -1
  112. package/dist/elements/editable-label.js +1 -1
  113. package/dist/elements/embed.js +1 -1
  114. package/dist/elements/empty.js +1 -1
  115. package/dist/elements/feedback-bar.js +1 -1
  116. package/dist/elements/file-tree.js +1 -1
  117. package/dist/elements/file-upload.js +1 -1
  118. package/dist/elements/form.js +1 -1
  119. package/dist/elements/hover-card.js +1 -1
  120. package/dist/elements/icon.js +1 -1
  121. package/dist/elements/image.js +1 -1
  122. package/dist/elements/input.js +1 -1
  123. package/dist/elements/kbd.js +1 -1
  124. package/dist/elements/link-preview.js +1 -1
  125. package/dist/elements/loader.js +1 -1
  126. package/dist/elements/markdown.js +1 -1
  127. package/dist/elements/menu.js +1 -1
  128. package/dist/elements/message-skills.js +1 -1
  129. package/dist/elements/message.js +1 -1
  130. package/dist/elements/model-switcher.js +1 -1
  131. package/dist/elements/nav.js +1 -1
  132. package/dist/elements/notice.js +1 -1
  133. package/dist/elements/pane-group.js +1 -1
  134. package/dist/elements/pane.js +1 -1
  135. package/dist/elements/popover.js +1 -1
  136. package/dist/elements/progress-bar.js +1 -1
  137. package/dist/elements/prompt-dock.js +1 -1
  138. package/dist/elements/prompt-input.js +1 -1
  139. package/dist/elements/prompt-suggestions.js +1 -1
  140. package/dist/elements/reasoning.js +1 -1
  141. package/dist/elements/remote.js +1 -1
  142. package/dist/elements/resizable.js +1 -1
  143. package/dist/elements/response-stream.js +2 -2
  144. package/dist/elements/screen.js +1 -1
  145. package/dist/elements/scroll-area.js +1 -1
  146. package/dist/elements/scroll-button.js +1 -1
  147. package/dist/elements/search.js +1 -1
  148. package/dist/elements/segmented.js +1 -1
  149. package/dist/elements/separator.js +1 -1
  150. package/dist/elements/setting-item.js +1 -1
  151. package/dist/elements/settings-group.js +1 -1
  152. package/dist/elements/skeleton.js +1 -1
  153. package/dist/elements/slot-text.d.ts +51 -0
  154. package/dist/elements/slots.d.ts +5 -0
  155. package/dist/elements/source.js +1 -1
  156. package/dist/elements/status.js +1 -1
  157. package/dist/elements/switch.js +1 -1
  158. package/dist/elements/tabs.js +1 -1
  159. package/dist/elements/tasks.js +1 -1
  160. package/dist/elements/text-shimmer.js +1 -1
  161. package/dist/elements/thinking-bar.js +1 -1
  162. package/dist/elements/thread.js +1 -1
  163. package/dist/elements/toast.js +1 -1
  164. package/dist/elements/tool.js +1 -1
  165. package/dist/elements/tooltip.js +1 -1
  166. package/dist/elements/voice-input.js +1 -1
  167. package/dist/elements/voice-output.js +1 -1
  168. package/dist/elements.d.ts +12 -6
  169. package/dist/index.js +1535 -1487
  170. package/dist/index.server.js +2330 -2296
  171. package/dist/kai.es.js +1 -1
  172. package/dist/llms/llms-full.txt +12 -3
  173. package/dist/mcp.es.js +809 -75
  174. package/dist/react/index.d.ts +13 -3
  175. package/dist/react.js +14 -14
  176. package/dist/register-impl-AxIlqjo7.js +145 -0
  177. package/dist/{solid-BCJUQqwS.js → solid-BGYSJyHG.js} +4901 -4691
  178. package/dist/{solid-BdENBcEq.js → solid-Dt2nEwfl.js} +4406 -4320
  179. package/dist/solid.js +1 -1
  180. package/dist/solid.server.js +1 -1
  181. package/dist/{variant-aurora-DOb_o4pC.js → variant-aurora-B44X69AG.js} +1 -1
  182. package/dist/{variant-aurora-BKHHiOgL.js → variant-aurora-BRhV8Flk.js} +2 -2
  183. package/dist/{variant-aurora-1wMcXRUS.js → variant-aurora-Ds5ou9BH.js} +2 -2
  184. package/dist/{variant-custom-DWSSgmd2.js → variant-custom-B20FhtYj.js} +2 -2
  185. package/dist/{variant-custom-CJGB0Kbw.js → variant-custom-BvA1Schl.js} +1 -1
  186. package/dist/{variant-custom-Co0CkXJF.js → variant-custom-D6Rtvneb.js} +2 -2
  187. package/dist/{variant-wave-CEODq-9q.js → variant-wave-BUQtkryq.js} +1 -1
  188. package/dist/{variant-wave-C3-45cL6.js → variant-wave-BVYnqy9L.js} +2 -2
  189. package/dist/{variant-wave-jdXBnCUZ.js → variant-wave-DXC6_LcH.js} +2 -2
  190. package/dist/wire/encode.d.ts +79 -6
  191. package/dist/wire/files.d.ts +116 -0
  192. package/dist/wire/index.d.ts +3 -1
  193. package/dist/wire/media-types.d.ts +106 -0
  194. package/dist/wire.js +596 -368
  195. package/frameworks/react/index.tsx +9 -5
  196. package/llms-full.txt +12 -3
  197. package/package.json +2 -1
  198. package/src/agent-tooling/archetypes.ts +43 -12
  199. package/src/agent-tooling/integrations/anthropic.ts +29 -5
  200. package/src/agent-tooling/integrations/cloudflare.ts +5 -0
  201. package/src/agent-tooling/integrations/langgraph.ts +13 -0
  202. package/src/agent-tooling/integrations/mastra.ts +46 -5
  203. package/src/agent-tooling/integrations/mock.ts +4 -0
  204. package/src/agent-tooling/integrations/ollama.ts +7 -0
  205. package/src/agent-tooling/integrations/openai.ts +2 -0
  206. package/src/agent-tooling/integrations/openrouter.ts +2 -0
  207. package/src/agent-tooling/integrations/pi.ts +26 -1
  208. package/src/agent-tooling/integrations/pydantic-ai.ts +37 -3
  209. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +42 -6
  210. package/src/agent-tooling/mcp/tools/scaffold.ts +853 -32
  211. package/src/agent-tooling/registry.ts +57 -0
  212. package/src/agent-tooling/types.ts +132 -0
  213. package/src/components/chat-thread.tsx +10 -1
  214. package/src/components/context.tsx +18 -12
  215. package/src/components/conversation-item.tsx +6 -0
  216. package/src/components/model-switcher.tsx +38 -8
  217. package/src/components/toast.tsx +6 -1
  218. package/src/elements/button.tsx +26 -3
  219. package/src/elements/chat.tsx +28 -2
  220. package/src/elements/default-input.tsx +128 -5
  221. package/src/elements/element-meta.json +30 -4
  222. package/src/elements/element-types.d.ts +12 -6
  223. package/src/elements/menu.tsx +30 -3
  224. package/src/elements/slot-text.ts +73 -0
  225. package/src/elements/slots.ts +10 -0
  226. package/src/index.ts +9 -5
  227. package/src/wire/encode.ts +229 -14
  228. package/src/wire/files.ts +342 -0
  229. package/src/wire/index.ts +21 -0
  230. package/src/wire/media-types.ts +344 -0
  231. package/dist/elements/chunks/arrow-left-Bb46H7w9.js +0 -1
  232. package/dist/elements/chunks/check-C8desc_h.js +0 -1
  233. package/dist/elements/chunks/chevron-down-DKtc2HNB.js +0 -1
  234. package/dist/elements/chunks/chevron-right-Dw4GPhwL.js +0 -1
  235. package/dist/elements/chunks/circle-BeokBsvB.js +0 -1
  236. package/dist/elements/chunks/circle-check-wJSqt859.js +0 -1
  237. package/dist/elements/chunks/context-WHDl-xSx.js +0 -1
  238. package/dist/elements/chunks/conversation-list-CdR14_Y4.js +0 -1
  239. package/dist/elements/chunks/default-input-CSZqUM81.js +0 -1
  240. package/dist/elements/chunks/message-square-CnpAm_P0.js +0 -1
  241. package/dist/elements/chunks/model-switcher-CclaJEHt.js +0 -1
  242. package/dist/elements/chunks/x-D1qx0aLd.js +0 -1
  243. package/dist/register-impl-bzrZ44ft.js +0 -145
@@ -203,7 +203,7 @@ export interface ButtonProps extends WebComponentProps {
203
203
  icon?: string;
204
204
  /** Trailing icon, after the label (e.g. `"chevron-down"` for a menu affordance). */
205
205
  iconTrailing?: string;
206
- /** Accessible name. REQUIRED for icon-only buttons (no visible text); ignored when you slot visible text, which already names the button. */
206
+ /** Accessible name. REQUIRED for icon-only buttons (no visible text); ignored when you slot visible text, which already names the button. An `aria-label` on top of visible text REPLACES that name rather than adding to it, so a button reading "Save" that answers to "Submit" is unusable by speech input (WCAG 2.5.3, Label in Name). The visible text wins. An `icon` / `icon-sm` size hides the slot, which makes the button icon-only whatever you slotted, so `label` is what names it there. */
207
207
  label?: string;
208
208
  /** Disable the button (non-interactive, dimmed). */
209
209
  disabled?: boolean;
@@ -353,6 +353,8 @@ export interface ChatProps extends WebComponentProps {
353
353
  kindIcons?: Record<string, string>;
354
354
  /** Whether each message's action bar is always visible (`'always'`, default) or only revealed on hover of that message row (`'hover'`). */
355
355
  actionsReveal?: "always" | "hover";
356
+ /** Which attachment media types the user may stage, in HTML `accept` syntax: `<kai-chat accept="image/*,application/pdf">`. A plain string, so unlike `messages` it DOES work as an attribute. Omitted means no filter. MEDIA TYPES ONLY -- exact (`image/png`) or subtype wildcard (`text/*`). HTML allows a file extension here and this does not: `accept=".py"` THROWS with the entry named, rather than silently resolving to a picker that accepts nothing. It can only NARROW what the kit can already encode: `accept="image/*"` resolves to the four image formats both APIs take, not to every image type the OS offers. Pass the SAME string to `toOpenAIMessages(msgs, { accept })` and the picker and the wire cannot disagree -- both resolve it through `resolveMediaPolicy` against one declaration. That declaration is readable as `encodableMediaTypes()` from `@kitn.ai/ui/wire`, if you would rather build your own picker than use this prop. */
357
+ accept?: string;
356
358
  /** The full message thread to render, newest last. Each entry carries its role, ordered `parts`, and optional actions/avatar/feedback. Set as a JS property (`el.messages = [...]`); a NEW array reference per streaming chunk re-renders (mutating in place does not). Omit for an empty thread. Re-declared here (rather than inherited from `ChatThreadProps`) because the ELEMENT registers a `[]` default and renders the empty state without it, while the SolidJS `<ChatThread>` component still requires it. The facade hands it a validated array either way. Matches `<kai-thread>`. */
357
359
  messages?: { id: string; role: "user" | "assistant"; parts: ({ type: "text"; text: string; raw?: { source: string; payload: unknown } } | { type: "reasoning"; text: string; label?: string; index?: number; streamId?: string; signature?: string; raw?: { source: string; payload: unknown } } | { type: "tool"; tool: { type: string; kind?: "command" | "file-change" | "search" | "fetch" | "mcp" | "image" | "generic"; state: "input-streaming" | "input-available" | "output-available" | "output-error"; input?: Record<string, unknown>; rawInput?: string; output?: Record<string, unknown>; toolCallId?: string; errorText?: string; raw?: { source: string; payload: unknown } }; raw?: { source: string; payload: unknown } } | { type: "card"; envelope: { type: string; id: string; data: unknown; title?: string; resolution?: { kind: "action"; action: string; payload?: unknown; at?: string } | { kind: "submit"; data: unknown; at?: string } | { kind: "dismissed"; at?: string } | { kind: "expired"; reason?: string; at?: string } }; raw?: { source: string; payload: unknown } } | { type: "source"; source: { id?: string; url?: string; title?: string; snippet?: string; index?: number }; raw?: { source: string; payload: unknown } } | { type: "file"; attachment: { id: string; type: "file" | "source-document"; filename?: string; mediaType?: string; url?: string; title?: string }; raw?: { source: string; payload: unknown } })[]; actions?: ("copy" | "like" | "dislike" | "regenerate" | "edit" | { id: string; label: string; icon?: string; tooltip?: string })[]; avatar?: { src?: string; fallback?: string; alt?: string }; feedback?: "like" | "dislike" }[];
358
360
  /** Optional card type -> custom-element tag overrides/additions for `card` parts (merged over the built-ins). Property: `el.cardTypes`. Typed as a plain string map (not the `CardTagMap` alias) so the generated React wrapper inlines it instead of emitting an unresolved named type. */
@@ -361,6 +363,8 @@ export interface ChatProps extends WebComponentProps {
361
363
  cardSchemas?: Record<string, object>;
362
364
  /** The staged attachments changed (file added or removed). Carries the full current list so a consumer can react in real time. */
363
365
  onAttachmentsChange?: (event: CustomEvent<{ attachments: { id: string; type: "file" | "source-document"; filename?: undefined | string; mediaType?: undefined | string; url?: undefined | string; title?: undefined | string }[] }>) => void;
366
+ /** One or more picked files were refused because `accept` excluded them. The element renders NO message of its own: it reports the facts (name, media type, whether the kit could have sent it) and what the user should see is the application's call. Only ever fires when `accept` is set. */
367
+ onAttachmentsRejected?: (event: CustomEvent<{ rejected: { filename: string; mediaType: string; reason: "filtered" | "unsupported" }[] }>) => void;
364
368
  /** An action button on a message was clicked. `action` is the built-in name or custom id. `state` is present only for the toggleable feedback votes: `'on'` when a like/dislike is set, `'off'` when re-tapped to clear. */
365
369
  onMessageAction?: (event: CustomEvent<{ messageId: string; action: string; state?: undefined | "on" | "off" }>) => void;
366
370
  /** The header model switcher changed. */
@@ -379,8 +383,8 @@ export interface ChatProps extends WebComponentProps {
379
383
 
380
384
  export const Chat = /*#__PURE__*/ createWebComponent<ChatProps>(
381
385
  'kai-chat',
382
- ["theme","search","value","placeholder","loading","suggestions","suggestionMode","persistSuggestions","proseSize","codeTheme","codeHighlight","chatTitle","models","currentModel","context","scrollButton","headerStart","headerEnd","headerFull","sidebar","empty","composer","composerActions","footer","voice","triggers","kindIcons","actionsReveal","messages","cardTypes","cardSchemas"],
383
- { onAttachmentsChange: 'kai-attachments-change', onMessageAction: 'kai-message-action', onModelChange: 'kai-model-change', onSearch: 'kai-search', onSubmit: 'kai-submit', onSuggestionClick: 'kai-suggestion-click', onValueChange: 'kai-value-change', onVoice: 'kai-voice' },
386
+ ["theme","search","value","placeholder","loading","suggestions","suggestionMode","persistSuggestions","proseSize","codeTheme","codeHighlight","chatTitle","models","currentModel","context","scrollButton","headerStart","headerEnd","headerFull","sidebar","empty","composer","composerActions","footer","voice","triggers","kindIcons","actionsReveal","accept","messages","cardTypes","cardSchemas"],
387
+ { onAttachmentsChange: 'kai-attachments-change', onAttachmentsRejected: 'kai-attachments-rejected', onMessageAction: 'kai-message-action', onModelChange: 'kai-model-change', onSearch: 'kai-search', onSubmit: 'kai-submit', onSuggestionClick: 'kai-suggestion-click', onValueChange: 'kai-value-change', onVoice: 'kai-voice' },
384
388
  () => import('@kitn.ai/ui/elements/chat'),
385
389
  );
386
390
 
@@ -973,11 +977,11 @@ export interface MenuProps extends WebComponentProps {
973
977
  placement?: string;
974
978
  /** Built-in trigger: leading icon (a named icon like `"plus"`, an image URL/data-URI, or text). Use this instead of slotting `slot="trigger"` for the common case; a slotted trigger overrides it. */
975
979
  triggerIcon?: string;
976
- /** Built-in trigger: a text label (e.g. `"High"`). */
980
+ /** Built-in trigger: a text label (e.g. `"High"`). This is the trigger's VISIBLE text, so it is also its accessible name, and `label` does not override it: an accessible name that does not contain the visible text is unreachable by speech input, which is what WCAG 2.5.3 (Label in Name) exists for. A slotted `slot="trigger"` replaces this built-in trigger entirely and is named differently; see `label`. */
977
981
  triggerLabel?: string;
978
982
  /** Built-in trigger: a trailing icon (e.g. `"chevron-down"` for a select look). */
979
983
  triggerIconTrailing?: string;
980
- /** Accessible name for an icon-only trigger (no visible label). */
984
+ /** Accessible name for a trigger with no visible label. Ignored when `triggerLabel` is set, which is already the visible name. It DOES name a slotted `slot="trigger"`, and that is a difference in what the two slots MEAN, not a limitation. `<kai-button>`'s slot IS the button's label, so text slotted there is the name and `label` steps aside. This slot is VISUAL content, a `+` or an `<svg>`, with the name supplied separately: decoration beside a name, never a second name competing with one. So `label` names the trigger here by design. Slotting a real WORD rather than a glyph makes that word a visible label, and an accessible name has to contain the visible text. Then either drop `label` or make it contain the word you slotted. */
981
985
  label?: string;
982
986
  /** Drive/observe open state (Shoelace-style: settable + reflected to the `open` attribute, the menu still self-manages on click/keyboard). Set `el.open = true`, or `<kai-menu open>`; listen for `kai-open-change`. */
983
987
  open?: boolean;
package/llms-full.txt CHANGED
@@ -449,7 +449,7 @@ _No events._
449
449
  | `size` | `size` | `undefined \| "sm" \| "md" \| "lg" \| "icon" \| "icon-sm"` | Size token. `icon` / `icon-sm` are square (for icon-only buttons); `sm` / `md` / `lg` size text buttons. Defaults to `md`. |
450
450
  | `icon` | `icon` | `undefined \| string` | Leading icon: a named icon (e.g. `"mic"`, `"plus"`), an image URL/data-URI, or plain text. Renders before any slotted label. |
451
451
  | `iconTrailing` | `icon-trailing` | `undefined \| string` | Trailing icon, after the label (e.g. `"chevron-down"` for a menu affordance). |
452
- | `label` | `label` | `undefined \| string` | Accessible name. REQUIRED for icon-only buttons (no visible text); ignored when you slot visible text, which already names the button. |
452
+ | `label` | `label` | `undefined \| string` | Accessible name. REQUIRED for icon-only buttons (no visible text); ignored when you slot visible text, which already names the button. An `aria-label` on top of visible text REPLACES that name rather than adding to it, so a button reading "Save" that answers to "Submit" is unusable by speech input (WCAG 2.5.3, Label in Name). The visible text wins. An `icon` / `icon-sm` size hides the slot, which makes the button icon-only whatever you slotted, so `label` is what names it there. |
453
453
  | `disabled` | `disabled` | `undefined \| false \| true` | Disable the button (non-interactive, dimmed). |
454
454
  | `full` | `full` | `undefined \| false \| true` | Stretch the button to the full width of its container (a block button), e.g. a card CTA or a stacked action. Attribute: `full`. |
455
455
  | `align` | `align` | `undefined \| "start" \| "center" \| "end"` | Justify the button's content: `start`, `center` (default), or `end`. Combine with `full` for a full-width, left-aligned button. |
@@ -629,6 +629,7 @@ _No events._
629
629
  | `triggers` | — | `undefined \| { char: string; kind: string; items?: undefined \| { id: string; label: string; icon?: undefined \| string; description?: undefined \| string; group?: undefined \| string; kind?: undefined \| string; promptText?: undefined \| string; data?: undefined \| Record<string, unknown> }[] }[]` | Rich entity triggers. Each `{ char, kind, items }` opens a caret-anchored menu that inserts an atomic pill (`/` skills, `@` agents/plugins). Set as a JS property; forwarded to the input. |
630
630
  | `kindIcons` | — | `undefined \| Record<string, string>` | Default icon per entity kind (kind → image src) for pills/menu items. |
631
631
  | `actionsReveal` | `actions-reveal` | `undefined \| "always" \| "hover"` | Whether each message's action bar is always visible (`'always'`, default) or only revealed on hover of that message row (`'hover'`). |
632
+ | `accept` | `accept` | `undefined \| string` | Which attachment media types the user may stage, in HTML `accept` syntax: `<kai-chat accept="image/*,application/pdf">`. A plain string, so unlike `messages` it DOES work as an attribute. Omitted means no filter. MEDIA TYPES ONLY -- exact (`image/png`) or subtype wildcard (`text/*`). HTML allows a file extension here and this does not: `accept=".py"` THROWS with the entry named, rather than silently resolving to a picker that accepts nothing. It can only NARROW what the kit can already encode: `accept="image/*"` resolves to the four image formats both APIs take, not to every image type the OS offers. Pass the SAME string to `toOpenAIMessages(msgs, { accept })` and the picker and the wire cannot disagree -- both resolve it through `resolveMediaPolicy` against one declaration. That declaration is readable as `encodableMediaTypes()` from `@kitn.ai/ui/wire`, if you would rather build your own picker than use this prop. |
632
633
  | `messages` | — | `undefined \| { id: string; role: "user" \| "assistant"; parts: ({ type: "text"; text: string; raw?: undefined \| { source: string; payload: unknown } } \| { type: "reasoning"; text: string; label?: undefined \| string; index?: undefined \| number; streamId?: undefined \| string; signature?: undefined \| string; raw?: undefined \| { source: string; payload: unknown } } \| { type: "tool"; tool: { type: string; kind?: undefined \| "command" \| "file-change" \| "search" \| "fetch" \| "mcp" \| "image" \| "generic"; state: "input-streaming" \| "input-available" \| "output-available" \| "output-error"; input?: undefined \| Record<string, unknown>; rawInput?: undefined \| string; output?: undefined \| Record<string, unknown>; toolCallId?: undefined \| string; errorText?: undefined \| string; raw?: undefined \| { source: string; payload: unknown } }; raw?: undefined \| { source: string; payload: unknown } } \| { type: "card"; envelope: { type: string; id: string; data: unknown; title?: undefined \| string; resolution?: undefined \| { kind: "action"; action: string; payload?: unknown; at?: undefined \| string } \| { kind: "submit"; data: unknown; at?: undefined \| string } \| { kind: "dismissed"; at?: undefined \| string } \| { kind: "expired"; reason?: undefined \| string; at?: undefined \| string } }; raw?: undefined \| { source: string; payload: unknown } } \| { type: "source"; source: { id?: undefined \| string; url?: undefined \| string; title?: undefined \| string; snippet?: undefined \| string; index?: undefined \| number }; raw?: undefined \| { source: string; payload: unknown } } \| { type: "file"; attachment: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }; raw?: undefined \| { source: string; payload: unknown } })[]; actions?: undefined \| ("copy" \| "like" \| "dislike" \| "regenerate" \| "edit" \| { id: string; label: string; icon?: undefined \| string; tooltip?: undefined \| string })[]; avatar?: undefined \| { src?: undefined \| string; fallback?: undefined \| string; alt?: undefined \| string }; feedback?: undefined \| "like" \| "dislike" }[]` | The full message thread to render, newest last. Each entry carries its role, ordered `parts`, and optional actions/avatar/feedback. Set as a JS property (`el.messages = [...]`); a NEW array reference per streaming chunk re-renders (mutating in place does not). Omit for an empty thread. Re-declared here (rather than inherited from `ChatThreadProps`) because the ELEMENT registers a `[]` default and renders the empty state without it, while the SolidJS `<ChatThread>` component still requires it. The facade hands it a validated array either way. Matches `<kai-thread>`. |
633
634
  | `cardTypes` | — | `undefined \| Record<string, string>` | Optional card type -> custom-element tag overrides/additions for `card` parts (merged over the built-ins). Property: `el.cardTypes`. Typed as a plain string map (not the `CardTagMap` alias) so the generated React wrapper inlines it instead of emitting an unresolved named type. |
634
635
  | `cardSchemas` | — | `undefined \| Record<string, object>` | JSON Schemas for the card types this app renders, keyed by envelope type. The companion of `cardTypes`, which says what DRAWS a card while this says what a VALID one looks like. An OBJECT, so it is a JS property only: `el.cardSchemas = { 'pricing-table': pricingSchema }`, never an attribute. `createCardRegistry(...).validationSchemas` is exactly this shape. Without it the kit validates its own seven built-ins and leaves your own card type, the one your app actually cares about, as the only unchecked thing on screen. A schema here WINS over a built-in of the same name. Typed `Record<string, object>` rather than `Record<string, JsonSchema>` deliberately: an imported `.json` schema widens `"type"` to `string`, and an authored one carries `$schema`/`title`/`description`/`additionalProperties`, so the tighter type would reject both of the normal ways to supply one. |
@@ -638,6 +639,7 @@ _No events._
638
639
  | Event | `detail` type | Description |
639
640
  |---|---|---|
640
641
  | `kai-attachments-change` | `CustomEvent<{ attachments: { id: string; type: "file" \| "source-document"; filename?: undefined \| string; mediaType?: undefined \| string; url?: undefined \| string; title?: undefined \| string }[] }>` | The staged attachments changed (file added or removed). Carries the full current list so a consumer can react in real time. |
642
+ | `kai-attachments-rejected` | `CustomEvent<{ rejected: { filename: string; mediaType: string; reason: "filtered" \| "unsupported" }[] }>` | One or more picked files were refused because `accept` excluded them. The element renders NO message of its own: it reports the facts (name, media type, whether the kit could have sent it) and what the user should see is the application's call. Only ever fires when `accept` is set. |
641
643
  | `kai-message-action` | `CustomEvent<{ messageId: string; action: string; state?: undefined \| "on" \| "off" }>` | An action button on a message was clicked. `action` is the built-in name or custom id. `state` is present only for the toggleable feedback votes: `'on'` when a like/dislike is set, `'off'` when re-tapped to clear. |
642
644
  | `kai-model-change` | `CustomEvent<{ modelId: string }>` | The header model switcher changed. |
643
645
  | `kai-search` | `CustomEvent<Record<string, never>>` | The Search button was clicked. |
@@ -951,6 +953,13 @@ _No events._
951
953
  |---|---|---|
952
954
  | `kai-threshold-change` | `CustomEvent<{ level: "ok" \| "warn" \| "danger" }>` | Fires when the computed severity level changes (ok → warn → danger or back). `detail.level` is `'ok'`, `'warn'`, or `'danger'`. |
953
955
 
956
+ **Styleable parts** (restyle from outside via `kai-context::part(name)`):
957
+
958
+ | Part | Description |
959
+ |---|---|
960
+ | `::part(track)` | The usage meter track inside the hover-card breakdown. Carries `role="progressbar"` and is in the DOM only while the card is open. Restyle its height, radius, or background from outside. — `kai-context::part(track) { height: 0.5rem }` |
961
+ | `::part(fill)` | The used-tokens portion of that meter. Its width follows `usedTokens / maxTokens`; its default color is the severity hue picked by `warnThreshold` / `dangerThreshold`, so recoloring it from outside replaces that signal. — `kai-context::part(fill) { background: var(--color-tool-blue) }` |
962
+
954
963
  ---
955
964
 
956
965
  ### `kai-conversations` / `Conversations`
@@ -1450,9 +1459,9 @@ _No events._
1450
1459
  | `items` | — | `undefined \| { id?: undefined \| string; label?: undefined \| string; icon?: undefined \| string; shortcut?: undefined \| string; checked?: undefined \| false \| true; radioGroup?: undefined \| string; disabled?: undefined \| false \| true; separator?: undefined \| false \| true; heading?: undefined \| false \| true; items?: undefined \| Record<string, unknown>[] }[]` | Tree of menu items. Set as a JS property, not an HTML attribute. |
1451
1460
  | `placement` | `placement` | `undefined \| string` | Optional placement hint (unused by the underlying Dropdown which always positions bottom-start, kept for future extension). |
1452
1461
  | `triggerIcon` | `trigger-icon` | `undefined \| string` | Built-in trigger: leading icon (a named icon like `"plus"`, an image URL/data-URI, or text). Use this instead of slotting `slot="trigger"` for the common case; a slotted trigger overrides it. |
1453
- | `triggerLabel` | `trigger-label` | `undefined \| string` | Built-in trigger: a text label (e.g. `"High"`). |
1462
+ | `triggerLabel` | `trigger-label` | `undefined \| string` | Built-in trigger: a text label (e.g. `"High"`). This is the trigger's VISIBLE text, so it is also its accessible name, and `label` does not override it: an accessible name that does not contain the visible text is unreachable by speech input, which is what WCAG 2.5.3 (Label in Name) exists for. A slotted `slot="trigger"` replaces this built-in trigger entirely and is named differently; see `label`. |
1454
1463
  | `triggerIconTrailing` | `trigger-icon-trailing` | `undefined \| string` | Built-in trigger: a trailing icon (e.g. `"chevron-down"` for a select look). |
1455
- | `label` | `label` | `undefined \| string` | Accessible name for an icon-only trigger (no visible label). |
1464
+ | `label` | `label` | `undefined \| string` | Accessible name for a trigger with no visible label. Ignored when `triggerLabel` is set, which is already the visible name. It DOES name a slotted `slot="trigger"`, and that is a difference in what the two slots MEAN, not a limitation. `<kai-button>`'s slot IS the button's label, so text slotted there is the name and `label` steps aside. This slot is VISUAL content, a `+` or an `<svg>`, with the name supplied separately: decoration beside a name, never a second name competing with one. So `label` names the trigger here by design. Slotting a real WORD rather than a glyph makes that word a visible label, and an accessible name has to contain the visible text. Then either drop `label` or make it contain the word you slotted. |
1456
1465
  | `open` | `open` | `undefined \| false \| true` | Drive/observe open state (Shoelace-style: settable + reflected to the `open` attribute, the menu still self-manages on click/keyboard). Set `el.open = true`, or `<kai-menu open>`; listen for `kai-open-change`. |
1457
1466
  | `defaultOpen` | `default-open` | `undefined \| false \| true` | Initial open state on mount (uncontrolled seed). |
1458
1467
  | `disabled` | `disabled` | `undefined \| false \| true` | Disable the trigger: click/keyboard and `show()` no longer open the menu. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kitn.ai/ui",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "nx": {
5
5
  "name": "ui",
6
6
  "targets": {
@@ -177,6 +177,7 @@
177
177
  "verify:ssr:imports": "node scripts/verify-ssr-imports.mjs",
178
178
  "verify:ssr:render": "node scripts/verify-ssr-render.mjs",
179
179
  "verify:pack": "node scripts/verify-pack-weight.mjs",
180
+ "verify:starters": "node scripts/verify-starters.mjs",
180
181
  "test": "vitest run",
181
182
  "test:react": "vitest run --config vitest.react.config.ts",
182
183
  "test:storybook": "vitest run --project=storybook",
@@ -55,6 +55,34 @@ export const archetypes: Archetype[] = [
55
55
  defaultPlacement: 'full-page',
56
56
  docsSlug: 'examples/voice-assistant',
57
57
  },
58
+ /**
59
+ * THE PRESET THAT EXISTS TO MAKE A CAPABILITY REACHABLE, not to name a product
60
+ * shape someone asked for.
61
+ *
62
+ * `kai-file-upload` and `kai-attachments` were registered elements that NO
63
+ * preset composed, and `listCapabilityGroups` derives its answer from this
64
+ * table — so the attachments capability reached neither `listSurfaceProbes`
65
+ * nor `verify:scaffold`, and `renderSurface` had no branch that emitted either
66
+ * tag. A caller could pass them in `components` (the axis takes any list) and
67
+ * get two bare `<kai-file-upload></kai-file-upload>` / `<kai-attachments>`
68
+ * siblings with nothing wired to them: on screen, inert, silent.
69
+ *
70
+ * The pair is ONE capability for the same reason `kai-artifact` +
71
+ * `kai-resizable` are: a dropzone with no list is a black hole, and a list
72
+ * with no dropzone can never fill. `hasAttachments` in scaffold.ts requires
73
+ * both for exactly that reason.
74
+ *
75
+ * The components match the create-kai spec's own feature table
76
+ * (`attachments` -> `kai-file-upload`, `kai-attachments`), so the CLI's
77
+ * multi-select and this preset name the same surface rather than two.
78
+ */
79
+ {
80
+ id: 'attachments',
81
+ title: 'File attachments',
82
+ components: ['kai-chat', 'kai-file-upload', 'kai-attachments'],
83
+ defaultPlacement: 'full-page',
84
+ docsSlug: 'examples/attachments',
85
+ },
58
86
  ];
59
87
 
60
88
  /** The component every surface composes around, so never a capability by itself. */
@@ -71,9 +99,10 @@ export interface CapabilityGroup {
71
99
  * The distinct capabilities the presets know about, DERIVED from them.
72
100
  *
73
101
  * A capability is one preset's components minus `kai-chat`, deduped across the
74
- * catalog. Today that is four: `sources`, `tool+reasoning`, `artifact+resizable`,
75
- * `voice-input` — and the six presets collapse onto them because `drop-in-chat`
76
- * and `support-widget` both add nothing (they differ only in `defaultPlacement`).
102
+ * catalog. Today that is five: `sources`, `tool+reasoning`, `artifact+resizable`,
103
+ * `voice-input`, `file-upload+attachments` — and the seven presets collapse onto
104
+ * them because `drop-in-chat` and `support-widget` both add nothing (they differ
105
+ * only in `defaultPlacement`).
77
106
  *
78
107
  * Derived rather than listed for the reason the verify script's other axes are:
79
108
  * a hand-written list would have to be edited by the same hand that adds a
@@ -83,9 +112,11 @@ export interface CapabilityGroup {
83
112
  *
84
113
  * The grouping is real and not an artifact of the derivation: `kai-tool` and
85
114
  * `kai-reasoning` are one capability because a tool panel with no reasoning
86
- * disclosure is not a surface anyone asks for, and `kai-artifact` /
115
+ * disclosure is not a surface anyone asks for, `kai-artifact` /
87
116
  * `kai-resizable` are one because `isWorkspace` only fires when BOTH are present
88
- * — split them and the renderer emits a bare artifact with no pane to put it in.
117
+ * — split them and the renderer emits a bare artifact with no pane to put it in
118
+ * — and `kai-file-upload` / `kai-attachments` are one because `hasAttachments`
119
+ * fires the same way, for the same kind of reason.
89
120
  */
90
121
  export function listCapabilityGroups(): CapabilityGroup[] {
91
122
  const seen = new Map<string, CapabilityGroup>();
@@ -109,14 +140,14 @@ export interface SurfaceProbe {
109
140
  * The surface axis for `verify:scaffold`: each capability ALONE, plus none, plus
110
141
  * ALL of them.
111
142
  *
112
- * WHY THESE AND NOT THE PRESETS. The presets are six cells covering five distinct
113
- * components lists (`support-widget` repeats `drop-in-chat`'s, and the matrix
114
- * holds `placement` at one value, so that pair compiled the same types twice).
115
- * More importantly they cover no combination at all — every preset is exactly one
116
- * capability — so the surfaces the multi-select makes reachable, which is most of
117
- * them, had nothing compiling them.
143
+ * WHY THESE AND NOT THE PRESETS. The presets are seven cells covering six
144
+ * distinct components lists (`support-widget` repeats `drop-in-chat`'s, and the
145
+ * matrix holds `placement` at one value, so that pair compiled the same types
146
+ * twice). More importantly they cover no combination at all — every preset is
147
+ * exactly one capability — so the surfaces the multi-select makes reachable,
148
+ * which is most of them, had nothing compiling them.
118
149
  *
119
- * WHY NOT THE POWER SET. 2^4 x 11 integrations x 8 frameworks is 1408 cells to
150
+ * WHY NOT THE POWER SET. 2^5 x 11 integrations x 8 frameworks is 2816 cells to
120
151
  * exercise no new branch. The renderers compose capabilities INDEPENDENTLY: each
121
152
  * companion tag is emitted on its own terms, and the one multi-component
122
153
  * predicate (`isWorkspace`) reads components from within a single group. So
@@ -35,10 +35,19 @@ const anthropic: Integration = {
35
35
  // No per-framework templates: the handler below is web-standard, so the
36
36
  // scaffolder wraps it in whatever the target framework routes with.
37
37
  routeTemplates: {},
38
- webRoute: `/** One Anthropic content block. Open union on the wire; these are the four the
38
+ webRoute: `/** Where an image or document's bytes come from. Anthropic takes both forms
39
+ * for both block types, which is why this route has no gap where the
40
+ * OpenAI-shaped one does: a chat-completions \`file\` part is base64-only. */
41
+ type AnthropicSource =
42
+ | { type: 'base64'; media_type: string; data: string }
43
+ | { type: 'url'; url: string };
44
+
45
+ /** One Anthropic content block. Open union on the wire; these are the ones the
39
46
  * scaffold's own thread can produce. */
40
47
  type AnthropicBlock =
41
48
  | { type: 'text'; text: string }
49
+ | { type: 'image'; source: AnthropicSource }
50
+ | { type: 'document'; source: AnthropicSource }
42
51
  | { type: 'tool_use'; id: string; name: string; input: unknown }
43
52
  | { type: 'tool_result'; tool_use_id: string; content: string };
44
53
 
@@ -121,12 +130,24 @@ function toAnthropicBody(messages: ChatRequestBody['messages']): {
121
130
  switch (message.role) {
122
131
  case 'system': {
123
132
  // TOP-LEVEL, not a message. Several system turns concatenate.
124
- const text = message.content ?? '';
133
+ const text = wireText(message.content);
125
134
  system = system === undefined ? text : \`\${system}\\n\\n\${text}\`;
126
135
  break;
127
136
  }
128
137
  case 'user': {
129
- if (message.content) pushUser([{ type: 'text', text: message.content }]);
138
+ // Attachments ride HERE, as image and document blocks beside the text,
139
+ // in the order the thread authored them.
140
+ const blocks = wireParts(message.content).map((part): AnthropicBlock => {
141
+ if (part.kind === 'text') return { type: 'text', text: part.text };
142
+ const source: AnthropicSource =
143
+ part.source.type === 'data'
144
+ ? { type: 'base64', media_type: part.mediaType, data: part.source.data }
145
+ : { type: 'url', url: part.source.url };
146
+ // \`image/png\` and the bare top-level \`image\` both mean an image; a URL
147
+ // source reports only the segment because the wire carries no more.
148
+ return part.mediaType.startsWith('image') ? { type: 'image', source } : { type: 'document', source };
149
+ });
150
+ if (blocks.length > 0) pushUser(blocks);
130
151
  break;
131
152
  }
132
153
  case 'tool': {
@@ -135,14 +156,15 @@ function toAnthropicBody(messages: ChatRequestBody['messages']): {
135
156
  {
136
157
  type: 'tool_result',
137
158
  tool_use_id: message.tool_call_id ?? '',
138
- content: message.content ?? '',
159
+ content: wireText(message.content),
139
160
  },
140
161
  ]);
141
162
  break;
142
163
  }
143
164
  case 'assistant': {
144
165
  const content: AnthropicBlock[] = [];
145
- if (message.content) content.push({ type: 'text', text: message.content });
166
+ const text = wireText(message.content);
167
+ if (text) content.push({ type: 'text', text });
146
168
  for (const call of message.tool_calls ?? []) {
147
169
  content.push({
148
170
  type: 'tool_use',
@@ -407,6 +429,8 @@ async function chatHandler(request: Request): Promise<Response> {
407
429
  // header is NOT `Authorization`, which is why the schema's detector matches
408
430
  // both spellings.
409
431
  keyExposure: 'needs-proxy',
432
+ // A remote HTTPS endpoint and a key. Nothing to install, nothing to start.
433
+ outOfBand: 'none',
410
434
  };
411
435
 
412
436
  export default anthropic;
@@ -167,6 +167,11 @@ export default {
167
167
  // the `env.AI` binding and holds no token in code at all, but a binding is a
168
168
  // Worker capability, not a browser one. Either way a server is required.
169
169
  keyExposure: 'needs-proxy',
170
+ // Workers AI is a remote HTTPS endpoint (the REST route) or a platform binding
171
+ // (the worker route) — either way there is nothing on the developer's machine
172
+ // to install or run. `wrangler` is a devDependency of a Worker app, not an
173
+ // out-of-band prerequisite of this integration.
174
+ outOfBand: 'none',
170
175
  };
171
176
 
172
177
  export default cloudflare;
@@ -109,6 +109,19 @@ async function chatHandler(request: Request): Promise<Response> {
109
109
  // No key appears in the route: `new ChatOpenAI(...)` reads OPENAI_API_KEY from
110
110
  // the environment itself. Same invisible-key shape as vercel-ai-sdk.
111
111
  keyExposure: 'needs-proxy',
112
+ // 'none', and this is the entry most likely to be "corrected" to something
113
+ // else, so the reason is here rather than assumed. The create-kai spec lists
114
+ // LangGraph under "Bring a server or runtime". That is WRONG for this route:
115
+ // the graph is built and compiled IN PROCESS above (`createReactAgent` over a
116
+ // `new ChatOpenAI(...)`), so there is no LangGraph server, no port and no
117
+ // second process — the four packages in `deps` are the whole install and
118
+ // `runNote` asks for a key and nothing else.
119
+ //
120
+ // LangGraph Platform / `langgraph dev` IS a server, and an integration that
121
+ // talked to one over HTTP would be 'local-server'. That is a different route
122
+ // than the one above, and if it is ever added it must be added as its own
123
+ // catalog entry rather than by changing this line.
124
+ outOfBand: 'none',
112
125
  };
113
126
 
114
127
  export default langgraph;
@@ -35,17 +35,43 @@ async function chatHandler(request: Request): Promise<Response> {
35
35
  // is never null. A 'tool' wire message is OUR client-side tool bookkeeping;
36
36
  // this agent owns its tools server-side (forwardsFromClient is empty), so it
37
37
  // has no schema for that shape and the entry is dropped rather than guessed.
38
- type MastraMessage = { role: 'system'; content: string } | { role: 'user'; content: string } | { role: 'assistant'; content: string };
38
+ //
39
+ // Only the USER role takes content parts: a system prompt is text by
40
+ // definition, and an assistant turn here is replayed history, not a new
41
+ // upload. \`data\` uses the bare base64-or-URL shorthand, which every AI SDK
42
+ // major Mastra accepts understands.
43
+ type MastraFilePart = { type: 'file'; data: string | URL; mediaType: string; filename?: string };
44
+ type MastraUserContent = string | Array<{ type: 'text'; text: string } | MastraFilePart>;
45
+ type MastraMessage = { role: 'system'; content: string } | { role: 'user'; content: MastraUserContent } | { role: 'assistant'; content: string };
39
46
  const mastraMessages: MastraMessage[] = [];
40
47
  for (const m of messages) {
41
- const content = m.content ?? '';
42
48
  // Each branch constructs a literal with ONE fixed role, not m.role (which is
43
49
  // still typed as the 4-way union): a union-VALUED field on a single object
44
50
  // does not structurally match a union of role-discriminated objects, so
45
51
  // widening back to m.role here would reintroduce the original TS2345.
46
- if (m.role === 'system') mastraMessages.push({ role: 'system', content });
47
- else if (m.role === 'user') mastraMessages.push({ role: 'user', content });
48
- else if (m.role === 'assistant') mastraMessages.push({ role: 'assistant', content });
52
+ if (m.role === 'system') mastraMessages.push({ role: 'system', content: wireText(m.content) });
53
+ else if (m.role === 'assistant') mastraMessages.push({ role: 'assistant', content: wireText(m.content) });
54
+ else if (m.role === 'user') {
55
+ const parts = wireParts(m.content);
56
+ // Plain string unless the turn actually carries an attachment.
57
+ if (parts.every((p) => p.kind === 'text')) {
58
+ mastraMessages.push({ role: 'user', content: wireText(m.content) });
59
+ } else {
60
+ mastraMessages.push({
61
+ role: 'user',
62
+ content: parts.map((p) =>
63
+ p.kind === 'text'
64
+ ? { type: 'text' as const, text: p.text }
65
+ : {
66
+ type: 'file' as const,
67
+ mediaType: p.mediaType,
68
+ ...(p.filename === undefined ? {} : { filename: p.filename }),
69
+ data: p.source.type === 'data' ? p.source.data : new URL(p.source.url),
70
+ },
71
+ ),
72
+ });
73
+ }
74
+ }
49
75
  }
50
76
 
51
77
  let agentStream: Awaited<ReturnType<ReturnType<typeof mastra.getAgent>['stream']>>;
@@ -130,6 +156,21 @@ async function chatHandler(request: Request): Promise<Response> {
130
156
  // no key leaks. The conservative direction is the cheap one: a needless server
131
157
  // hop costs a process, the other error costs the endpoint.
132
158
  keyExposure: 'needs-proxy',
159
+ // THE JUDGEMENT CALL FOR THIS FIELD, exactly as keyExposure above is, and for
160
+ // a related reason: no automatic check can decide it. MASTRA_URL is a base URL
161
+ // for a server this integration does not ship and cannot start; the route's
162
+ // only mention of loopback is inside a `throw new Error(...)` string, which
163
+ // LOOPBACK_FETCH deliberately does not match (a net that fired on prose would
164
+ // be right here by accident and wrong elsewhere). So 'none' would parse
165
+ // cleanly.
166
+ //
167
+ // It is 'local-server' on what the integration actually requires: a Mastra
168
+ // server has to be reachable at MASTRA_URL before the first message works,
169
+ // `mastra dev` is how a developer gets one (port 4111), and `@mastra/client-js`
170
+ // is a CLIENT for it, not the server itself. Printing "start your Mastra
171
+ // server" costs a line; omitting it produces a scaffold that fetches a URL
172
+ // nothing answers.
173
+ outOfBand: 'local-server',
133
174
  };
134
175
 
135
176
  export default mastra;
@@ -64,6 +64,10 @@ const mock: Integration = {
64
64
  // the one place where "declares nothing" genuinely means safe — which is
65
65
  // precisely why it still has to SAY so rather than be left blank.
66
66
  keyExposure: 'frontend-safe',
67
+ // Nothing at all, and here that is the literal truth rather than a shorthand:
68
+ // there is no route, no upstream and no process. This is the "No backend"
69
+ // group of create-kai's gateway prompt all by itself.
70
+ outOfBand: 'none',
67
71
  };
68
72
 
69
73
  export default mock;
@@ -79,6 +79,13 @@ const ollama: Integration = {
79
79
  // the browser-direct path (set OLLAMA_ORIGINS to allow the page origin), which
80
80
  // is the path this value unlocks.
81
81
  keyExposure: 'frontend-safe',
82
+ // The route fetches http://localhost:11434, so Ollama must ALREADY be running:
83
+ // `ollama serve`, then `ollama pull <model>` for the model the route pins.
84
+ // Nothing in package.json installs or starts it — `deps.npm` is empty because
85
+ // the route is bare `fetch`, which is exactly why this cannot be inferred from
86
+ // the dependency list. The schema's LOOPBACK_FETCH net catches this one
87
+ // independently, so a later edit to 'none' fails at the catalog boundary.
88
+ outOfBand: 'local-server',
82
89
  };
83
90
 
84
91
  export default ollama;
@@ -82,6 +82,8 @@ const openai: Integration = {
82
82
  // The route reads OPENAI_API_KEY and puts it in an `Authorization: Bearer`
83
83
  // header. A static bundle carrying that is a published key.
84
84
  keyExposure: 'needs-proxy',
85
+ // A remote HTTPS endpoint and a key. Nothing to install, nothing to start.
86
+ outOfBand: 'none',
85
87
  };
86
88
 
87
89
  export default openai;
@@ -69,6 +69,8 @@ const openrouter: Integration = {
69
69
  deps: { npm: [], pip: [] },
70
70
  // The route reads OPENROUTER_API_KEY into an `Authorization: Bearer` header.
71
71
  keyExposure: 'needs-proxy',
72
+ // A remote HTTPS endpoint and a key. Nothing to install, nothing to start.
73
+ outOfBand: 'none',
72
74
  };
73
75
 
74
76
  export default openrouter;
@@ -23,7 +23,25 @@ app.use(express.json());
23
23
  // POST /api/chat: bridge a Pi RPC session to the browser as SSE.
24
24
  app.post('/api/chat', (req, res) => {
25
25
  const { messages } = req.body as { messages: OpenAIWireMessage[] };
26
- const prompt = messages.at(-1)?.content ?? '';
26
+ const last = messages.at(-1)?.content;
27
+ // \`content\` is a plain string until the turn carries an attachment, at which
28
+ // point it is an ARRAY of content parts. Pi's RPC mode takes a TEXT prompt and
29
+ // has no channel for a file, so an attachment is REFUSED here. Passing the
30
+ // array straight through would JSON.stringify an object graph into the prompt
31
+ // — no type error, no crash, just a model reading serialised noise.
32
+ if (Array.isArray(last) && last.some((part) => part.type !== 'text')) {
33
+ res.status(400).json({
34
+ error: {
35
+ message:
36
+ 'This Pi bridge forwards a text prompt only and has no channel for an attachment. Extract the file content into the message text, or send it through a tool.',
37
+ },
38
+ });
39
+ return;
40
+ }
41
+ const prompt =
42
+ typeof last === 'string'
43
+ ? last
44
+ : (last ?? []).map((part) => (part.type === 'text' ? part.text : '')).join('');
27
45
 
28
46
  // Headers before the first frame: without text/event-stream the browser
29
47
  // buffers the body and readOpenAIStream never sees a frame.
@@ -91,6 +109,13 @@ app.listen(3001, () => console.log('chat api: http://localhost:3001/api/chat'));
91
109
  // is exposed. "No key" is not the same fact as "safe in the browser", and
92
110
  // collapsing the two is exactly how this flag would be got wrong.
93
111
  keyExposure: 'needs-proxy',
112
+ // Not 'local-server': nothing is listening in advance. The bridge spawns
113
+ // `pi --mode rpc` per request, so what the developer supplies is an EXECUTABLE
114
+ // on PATH ("Pi must be installed locally and available on PATH as 'pi'"), and
115
+ // "start the server first" would be the wrong instruction to print. Pi is not
116
+ // in `deps` for the same reason — the route reaches it with spawn(), not an
117
+ // import. The schema's SPAWNS_PROCESS net catches this one independently.
118
+ outOfBand: 'local-binary',
94
119
  };
95
120
 
96
121
  export default pi;
@@ -10,7 +10,7 @@ const pydanticAi: Integration = {
10
10
  routeTemplates: {
11
11
  fastapi: `# main.py
12
12
  import json
13
- from fastapi import FastAPI
13
+ from fastapi import FastAPI, HTTPException
14
14
  from fastapi.middleware.cors import CORSMiddleware
15
15
  from fastapi.responses import StreamingResponse
16
16
  from pydantic import BaseModel
@@ -23,15 +23,41 @@ app.add_middleware(
23
23
  CORSMiddleware, allow_origins=['*'], allow_methods=['*'], allow_headers=['*']
24
24
  )
25
25
 
26
+ class ContentPart(BaseModel):
27
+ type: str
28
+ text: str | None = None
29
+
26
30
  class Message(BaseModel):
27
31
  role: str
28
- content: str
32
+ # A plain string until the turn carries an attachment, at which point the
33
+ # wire sends an ARRAY of content parts. Declaring this \`str\` alone made a
34
+ # message with a file a 422 that named pydantic rather than the cause.
35
+ content: str | list[ContentPart] | None = None
29
36
 
30
37
  class ChatRequest(BaseModel):
31
38
  messages: list[Message]
32
39
 
40
+ def prompt_text(message: Message) -> str:
41
+ """The text of a turn, refusing what this route cannot carry.
42
+
43
+ agent.run_stream() takes a TEXT prompt, so an image or a document has no
44
+ channel here. Dropping it would send the model a turn that silently lost the
45
+ user's file; a 400 that names the reason is the honest failure.
46
+ """
47
+ if message.content is None:
48
+ return ''
49
+ if isinstance(message.content, str):
50
+ return message.content
51
+ if any(part.type != 'text' for part in message.content):
52
+ raise HTTPException(
53
+ status_code=400,
54
+ detail='This route forwards a text prompt only and has no channel for an attachment. '
55
+ 'Extract the file content into the message text, or send it through a tool.',
56
+ )
57
+ return ''.join(part.text or '' for part in message.content)
58
+
33
59
  async def openai_sse(messages: list[Message]):
34
- prompt = messages[-1].content if messages else ''
60
+ prompt = prompt_text(messages[-1]) if messages else ''
35
61
  async with agent.run_stream(prompt) as result:
36
62
  async for delta in result.stream_text(delta=True):
37
63
  chunk = {'choices': [{'delta': {'content': delta}}]}
@@ -69,6 +95,14 @@ async def chat(req: ChatRequest):
69
95
  // 'frontend-safe' — the flag asks where the SECRET lives, and it lives in the
70
96
  // python process. The FastAPI service IS the server hop.
71
97
  keyExposure: 'needs-proxy',
98
+ // The emitted backend is a FastAPI service: a python interpreter, the four
99
+ // `deps.pip` packages, and `uvicorn main:app` on its own port — none of which a
100
+ // node toolchain provides or starts. It also needs OPENAI_API_KEY, so this is
101
+ // the entry that proves the groups are not mutually exclusive: a runtime
102
+ // prerequisite and a key at once. The prompt should lead with the runtime,
103
+ // because a key is useless until the service runs. The schema's `language ===
104
+ // 'python'` net catches this one independently.
105
+ outOfBand: 'language-runtime',
72
106
  };
73
107
 
74
108
  export default pydanticAi;