@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
@@ -53,6 +53,63 @@ export function listIntegrations(): Integration[] {
53
53
  return integrations;
54
54
  }
55
55
 
56
+ /** One heading in `create-kai`'s gateway prompt. */
57
+ export interface GatewayGroup {
58
+ id: 'no-backend' | 'bring-a-key' | 'bring-a-server';
59
+ title: string;
60
+ integrations: Integration[];
61
+ }
62
+
63
+ /**
64
+ * The gateway prompt's three groups, DERIVED — the thing the spec described and
65
+ * the catalog could not answer.
66
+ *
67
+ * It is here, and not in the CLI, for the reason `deps` is not in the spec's
68
+ * prose: a grouping maintained next to the menu that renders it is a second copy
69
+ * of the catalog, and it goes stale the first time an integration lands. Adding
70
+ * one now moves these groups by itself, because the only inputs are declared
71
+ * fields.
72
+ *
73
+ * THE ORDER OF THE TESTS IS THE DESIGN, not an implementation detail:
74
+ *
75
+ * 1. No route at all -> **No backend**. Exactly `mock` today, and derived from
76
+ * the absence rather than from `id === 'mock'` or `category === 'mock'`: a
77
+ * second no-backend entry (a canned transcript player, say) should land in
78
+ * this group without editing this function.
79
+ * 2. `outOfBand !== 'none'` -> **Bring a server or runtime**, and this is
80
+ * checked BEFORE the key test on purpose. `pydantic-ai` needs both a python
81
+ * runtime and OPENAI_API_KEY; the groups are not disjoint in reality, so
82
+ * something has to win, and the prerequisite does — a key is useless until
83
+ * the service it authenticates is running. The prompt still asks for the
84
+ * key afterwards; only the HEADING is decided here.
85
+ * 3. Everything else -> **Bring a key**.
86
+ *
87
+ * WHERE THIS DIVERGES FROM THE SPEC, ON PURPOSE. The spec's third group reads
88
+ * "Ollama, LangGraph, Mastra, Pydantic AI, Pi". `langgraph` comes out of this
89
+ * function in **Bring a key** instead, because its emitted route compiles the
90
+ * graph in process and asks for nothing but `OPENAI_API_KEY` — see the comment on
91
+ * its `outOfBand`. The spec sentence was written before the field existed and is
92
+ * wrong about that one entry; this is the derivation, so this is the answer.
93
+ */
94
+ export function listGatewayGroups(): GatewayGroup[] {
95
+ const hasRoute = (i: Integration) =>
96
+ i.webRoute !== undefined || Object.keys(i.routeTemplates).length > 0;
97
+
98
+ const groups: GatewayGroup[] = [
99
+ { id: 'no-backend', title: 'No backend', integrations: [] },
100
+ { id: 'bring-a-key', title: 'Bring a key', integrations: [] },
101
+ { id: 'bring-a-server', title: 'Bring a server or runtime', integrations: [] },
102
+ ];
103
+ const into = (id: GatewayGroup['id']) => groups.find((g) => g.id === id)!;
104
+
105
+ for (const integration of integrations) {
106
+ if (!hasRoute(integration)) into('no-backend').integrations.push(integration);
107
+ else if (integration.outOfBand !== 'none') into('bring-a-server').integrations.push(integration);
108
+ else into('bring-a-key').integrations.push(integration);
109
+ }
110
+ return groups;
111
+ }
112
+
56
113
  export function listArchetypes(): Archetype[] {
57
114
  return archetypes;
58
115
  }
@@ -44,6 +44,50 @@ export const Placement = z.enum(['side', 'full-page', 'docked-widget', 'inline']
44
44
  */
45
45
  export const KeyExposure = z.enum(['frontend-safe', 'needs-proxy']);
46
46
 
47
+ /**
48
+ * What this integration needs the developer to supply OUT OF BAND — beyond an
49
+ * install and a key.
50
+ *
51
+ * `create-kai` groups its gateway prompt three ways: **No backend**, **Bring a
52
+ * key**, **Bring a server or runtime**. The third group is defined by exactly
53
+ * this question, and until now the catalog could not answer it. Every derivation
54
+ * that reproduced the spec's split did so by accident and broke on inspection:
55
+ * · `category` does not separate them at all — `provider` holds `openai` and
56
+ * `ollama`, `framework` holds `vercel-ai-sdk` and `langgraph`.
57
+ * · a non-empty `routeTemplates` picks out `cloudflare`/`pi`/`pydantic-ai`,
58
+ * which is a fact about how a route is EXPRESSED, not about what it needs.
59
+ * · "declares a secret env var" puts `langgraph` and `pydantic-ai` in the key
60
+ * group, which is right for one and wrong for the other.
61
+ * So it is declared, for the reason `keyExposure`, `forwardsFromClient` and
62
+ * `clientToolFormat` are each declared: only the route knows.
63
+ *
64
+ * The four values, and what makes each its own case for the CLI rather than a
65
+ * boolean it could have been:
66
+ *
67
+ * · `'none'` ......... `npm install` plus (maybe) a key is the whole story.
68
+ * The upstream is a remote HTTPS endpoint, or the agent runs IN the route.
69
+ * `langgraph` is the one worth reading twice: it is in the spec's "server or
70
+ * runtime" group and does not belong there — its emitted route builds the
71
+ * graph in process (`createReactAgent` + `new ChatOpenAI(...)`), so there is
72
+ * no LangGraph server anywhere and its runNote asks only for a key.
73
+ * · `'local-server'` . a separate process must already be LISTENING before the
74
+ * app works. The CLI cannot create it and must say so: `ollama serve` on
75
+ * 11434, a `mastra dev` server behind `MASTRA_URL`.
76
+ * · `'local-binary'` . an executable must be on PATH; the route spawns it on
77
+ * demand rather than talking to a port. Nothing is listening beforehand, so
78
+ * "start the server" is the wrong instruction — `pi` is the case.
79
+ * · `'language-runtime'` the emitted backend runs on a runtime the JS toolchain
80
+ * does not provide, and the user starts it themselves. `pydantic-ai` is a
81
+ * FastAPI service: `deps.pip`, `uvicorn main:app`, a second port.
82
+ *
83
+ * A boolean would collapse the last three, and they are three different sentences
84
+ * to print next to a menu item. The refinement below REFUSES an integration that
85
+ * declares nothing, for the same reason `keyExposure` does: a missing value read
86
+ * as `'none'` is a scaffold that says "npm run dev" to someone who needs to start
87
+ * Ollama first, and then reports as a broken kit.
88
+ */
89
+ export const OutOfBand = z.enum(['none', 'local-server', 'local-binary', 'language-runtime']);
90
+
47
91
  /**
48
92
  * The packages an app must install BEYOND `@kitn.ai/ui` to run this
49
93
  * integration's route.
@@ -93,6 +137,25 @@ export const SECRET_ENV_VAR = /(?:KEY|TOKEN|SECRET|PASSWORD)$/;
93
137
  */
94
138
  const AUTH_HEADER = /Authorization\s*:|['"]x-api-key['"]\s*:/i;
95
139
 
140
+ /**
141
+ * A route that FETCHES a loopback address — so something local has to be
142
+ * listening. `ollama`'s `fetch('http://localhost:11434/v1/chat/completions')` is
143
+ * the case this catches.
144
+ *
145
+ * Anchored at the fetch/URL call, not matched anywhere in the source, and that
146
+ * precision is the whole point. Two other routes mention loopback in prose:
147
+ * mastra's `throw new Error('MASTRA_URL is not set… e.g. http://localhost:4111')`
148
+ * and pi's `console.log('chat api: http://localhost:3001/api/chat')`. A bare
149
+ * /localhost/ would flag both — mastra for the right answer by the wrong
150
+ * evidence, pi for a line about its OWN listening port, which says nothing about
151
+ * what it depends on. A net that is right by accident is the thing this field
152
+ * exists to replace, so it is narrowed until it only fires on a real dependency.
153
+ */
154
+ const LOOPBACK_FETCH = /\b(?:fetch|URL)\s*\(\s*['"`]https?:\/\/(?:localhost|127\.0\.0\.1)/;
155
+
156
+ /** A route that shells out. The program it names must exist on the host. */
157
+ const SPAWNS_PROCESS = /\bfrom\s+['"]node:child_process['"]|\bspawn\s*\(/;
158
+
96
159
  export const IntegrationSchema = z.object({
97
160
  id: z.string(),
98
161
  title: z.string(),
@@ -178,6 +241,13 @@ export const IntegrationSchema = z.object({
178
241
  * the one field where the reader needs to be told what is at stake.
179
242
  */
180
243
  keyExposure: KeyExposure.optional(),
244
+ /**
245
+ * See {@link OutOfBand}. Optional in the TYPE only so the refinement below owns
246
+ * the error message — Zod skips refinements when the base object already
247
+ * failed, so a plain required field would report "Invalid input" and this is a
248
+ * field whose reader needs to be told what the four values mean.
249
+ */
250
+ outOfBand: OutOfBand.optional(),
181
251
  }).superRefine((integration, ctx) => {
182
252
  // Enforced against the real catalog by `registry.test.ts`, which parses every
183
253
  // integration through this schema. A new host that forwards tools therefore
@@ -245,6 +315,68 @@ export const IntegrationSchema = z.object({
245
315
  });
246
316
  }
247
317
  }
318
+
319
+ // ---- outOfBand ----
320
+ //
321
+ // Same two-check shape as keyExposure, and the same asymmetry. The first
322
+ // refuses SILENCE. The second refuses a specific LIE, and only in the direction
323
+ // that costs the developer something: it can turn a `'none'` claim into an
324
+ // error, never the reverse. Over-declaring prints a prerequisite that was not
325
+ // strictly needed; under-declaring ships a scaffold that cannot possibly run
326
+ // and blames the kit.
327
+
328
+ if (integration.outOfBand === undefined) {
329
+ ctx.addIssue({
330
+ code: z.ZodIssueCode.custom,
331
+ path: ['outOfBand'],
332
+ message:
333
+ `integration '${integration.id}' declares no outOfBand. State what the developer must supply ` +
334
+ `BEYOND an install and a key, because that is the group create-kai's gateway prompt puts it in: ` +
335
+ `'none' (a remote endpoint, or the agent runs inside the route), 'local-server' (a process must ` +
336
+ `already be listening, e.g. 'ollama serve'), 'local-binary' (an executable on PATH that the route ` +
337
+ `spawns), or 'language-runtime' (the emitted backend needs a runtime the JS toolchain does not ` +
338
+ `provide, e.g. python + uvicorn). Read the route and the runNote to decide, not the category: ` +
339
+ `'provider' holds both openai and ollama. There is no default on purpose — a missing value read ` +
340
+ `as 'none' is a scaffold that says "npm run dev" to someone who has nothing to connect to.`,
341
+ });
342
+ }
343
+
344
+ if (integration.outOfBand === 'none') {
345
+ const routeSources = [
346
+ ...Object.values(integration.routeTemplates),
347
+ ...(integration.webRoute ? [integration.webRoute] : []),
348
+ ];
349
+ if (integration.language === 'python') {
350
+ ctx.addIssue({
351
+ code: z.ZodIssueCode.custom,
352
+ path: ['outOfBand'],
353
+ message:
354
+ `integration '${integration.id}' claims outOfBand 'none' but its route is python, so a consumer ` +
355
+ `whose whole toolchain is node cannot run it without an interpreter and an ASGI server they ` +
356
+ `start themselves. Use 'language-runtime'.`,
357
+ });
358
+ }
359
+ if (routeSources.some((code) => SPAWNS_PROCESS.test(code))) {
360
+ ctx.addIssue({
361
+ code: z.ZodIssueCode.custom,
362
+ path: ['outOfBand'],
363
+ message:
364
+ `integration '${integration.id}' claims outOfBand 'none' but its route spawns a child process, ` +
365
+ `so the program it spawns has to be installed on the host — nothing in package.json puts it ` +
366
+ `there. Use 'local-binary'.`,
367
+ });
368
+ }
369
+ if (routeSources.some((code) => LOOPBACK_FETCH.test(code))) {
370
+ ctx.addIssue({
371
+ code: z.ZodIssueCode.custom,
372
+ path: ['outOfBand'],
373
+ message:
374
+ `integration '${integration.id}' claims outOfBand 'none' but its route fetches a loopback ` +
375
+ `address, so something local has to be listening before the first message works. ` +
376
+ `Use 'local-server'.`,
377
+ });
378
+ }
379
+ }
248
380
  });
249
381
  export type Integration = z.infer<typeof IntegrationSchema>;
250
382
 
@@ -11,7 +11,8 @@ import {
11
11
  Context, ContextTrigger, ContextContent, ContextContentHeader,
12
12
  ContextContentBody, ContextContentFooter, ContextInputUsage, ContextOutputUsage,
13
13
  } from './context';
14
- import { DefaultPromptInput } from '../elements/default-input';
14
+ import { DefaultPromptInput, type RejectedAttachment } from '../elements/default-input';
15
+ import type { MediaTypeFilter } from '../wire/media-types';
15
16
  import type { TriggerDef } from './composer';
16
17
  import type { ChatMessage } from '../elements/chat-types';
17
18
  import type { ProseSize } from '../primitives/chat-config';
@@ -112,6 +113,13 @@ export interface ChatThreadProps {
112
113
  composerActions?: boolean;
113
114
  /** INJECT: footer row below the composer (disclaimers, token meter, …). */
114
115
  footer?: boolean;
116
+ /** Which attachment media types the user may stage, in HTML `accept` syntax
117
+ * (`'image/*,application/pdf'`). Omitted means no filter. Narrowed by what the
118
+ * encoders can actually send — the same string, and the same resolver, as
119
+ * `toOpenAIMessages(msgs, { accept })`. */
120
+ accept?: MediaTypeFilter;
121
+ /** Files the composer refused because `accept` excluded them. */
122
+ onAttachmentsRejected?: (rejected: RejectedAttachment[]) => void;
115
123
  /** Show a Search (Globe) button in the input toolbar; fires a `search` event. */
116
124
  search?: boolean;
117
125
  /** Show a Voice (Mic) button in the input toolbar; fires a `voice` event. */
@@ -392,6 +400,7 @@ export function ChatThread(props: ChatThreadProps) {
392
400
  <DefaultPromptInput
393
401
  value={current()} placeholder={props.placeholder} loading={props.loading === true}
394
402
  suggestions={visibleSuggestions()} attachments={attachments()}
403
+ accept={props.accept} onAttachmentsRejected={props.onAttachmentsRejected}
395
404
  search={props.search === true} voice={props.voice === true}
396
405
  triggers={props.triggers} kindIcons={props.kindIcons}
397
406
  onValueChange={handleChange} onSubmit={handleSubmit} onSuggestionClick={handleSuggestionClick}
@@ -2,12 +2,12 @@ import { type JSX, Show, createContext, createMemo, useContext } from 'solid-js'
2
2
  import { HoverCardRoot, HoverCardTrigger, HoverCardContent } from '../ui/hover-card';
3
3
  import { cn } from '../utils/cn';
4
4
  import { Button } from '../ui/button';
5
+ import { ProgressBar, type ProgressTone } from '../ui/progress-bar';
5
6
 
6
7
  const ICON_RADIUS = 10;
7
8
  const ICON_VIEWBOX = 24;
8
9
  const ICON_CENTER = 12;
9
10
  const ICON_STROKE_WIDTH = 2;
10
- const PERCENT_MAX = 100;
11
11
 
12
12
  /** Default fraction (0–1) at which the meter transitions to the warning colour. */
13
13
  export const DEFAULT_WARN_THRESHOLD = 0.7;
@@ -206,7 +206,6 @@ export function ContextContentHeader(props: ContextContentHeaderProps) {
206
206
  const displayPct = createMemo(() => fmtPercent.format(usedPercent()));
207
207
  const used = createMemo(() => fmtCompact.format(ctx.usedTokens));
208
208
  const total = createMemo(() => fmtCompact.format(ctx.maxTokens));
209
- const barWidth = createMemo(() => `${Math.min(usedPercent() * PERCENT_MAX, PERCENT_MAX)}%`);
210
209
 
211
210
  const severity = createMemo<ContextSeverity>(() => {
212
211
  const pct = usedPercent();
@@ -215,11 +214,16 @@ export function ContextContentHeader(props: ContextContentHeaderProps) {
215
214
  return 'ok';
216
215
  });
217
216
 
218
- const colorClass = createMemo(() => {
217
+ // Severity used to pick a bare hue class and nothing else, so the meter was a
218
+ // pair of styled divs: no role, no value, invisible to assistive tech. Map it to
219
+ // the shared ProgressBar's semantic `tone` instead and let that primitive supply
220
+ // `role="progressbar"` + aria-valuenow/min/max, rather than hand-rolling those
221
+ // onto the divs.
222
+ const tone = createMemo<ProgressTone>(() => {
219
223
  const s = severity();
220
- if (s === 'danger') return 'bg-red-400';
221
- if (s === 'warn') return 'bg-yellow-400';
222
- return 'bg-primary';
224
+ if (s === 'danger') return 'error';
225
+ if (s === 'warn') return 'warning';
226
+ return 'primary';
223
227
  });
224
228
 
225
229
  return (
@@ -232,12 +236,14 @@ export function ContextContentHeader(props: ContextContentHeaderProps) {
232
236
  </p>
233
237
  </div>
234
238
  <div class="space-y-2">
235
- <div class="h-1.5 w-full rounded-full bg-muted overflow-hidden">
236
- <div
237
- class={cn('h-full rounded-full transition-all', colorClass())}
238
- style={{ width: barWidth() }}
239
- />
240
- </div>
239
+ {/* Values are the raw token counts, so a screen reader announces
240
+ "190,000 of 200,000" rather than a bare percentage. */}
241
+ <ProgressBar
242
+ value={ctx.usedTokens}
243
+ max={ctx.maxTokens}
244
+ tone={tone()}
245
+ aria-label="Context usage"
246
+ />
241
247
  </div>
242
248
  </Show>
243
249
  </div>
@@ -43,6 +43,12 @@ export function ConversationItem(props: ConversationItemProps) {
43
43
  return (
44
44
  <button
45
45
  data-conversation-id={local.conversation.id}
46
+ // `isActive` drives the selected LOOK below; it has to reach assistive tech
47
+ // too, or the active conversation is visible only to sighted users. `true`
48
+ // rather than `page` because this selects a conversation within the app, it
49
+ // is not page navigation — same call as ui/agent-card.tsx and ui/pane-group.tsx
50
+ // (ui/nav.tsx uses `page` because its items really are nav links).
51
+ aria-current={local.isActive ? 'true' : undefined}
46
52
  onClick={() => local.onSelect(local.conversation.id)}
47
53
  class={cn(
48
54
  'w-full rounded-lg text-left transition-colors',
@@ -1,6 +1,6 @@
1
1
  import { splitProps, For, Show, createSignal } from 'solid-js';
2
2
  import { cn } from '../utils/cn';
3
- import { Dropdown, DropdownTrigger, DropdownContent, DropdownItem, type DropdownController } from '../ui/dropdown';
3
+ import { Dropdown, DropdownTrigger, DropdownContent, DropdownRadioItem, type DropdownController } from '../ui/dropdown';
4
4
  import { Button } from '../ui/button';
5
5
  import type { ModelOption } from '../types';
6
6
 
@@ -23,15 +23,37 @@ const Chevron = (props: { class?: string }) => (
23
23
  </svg>
24
24
  );
25
25
 
26
- function ModelRow(props: { model: ModelOption; currentModelId: string; onModelChange: (id: string) => void }) {
26
+ /**
27
+ * One selectable model.
28
+ *
29
+ * A single-select list of models IS a radio group, so this routes through
30
+ * `DropdownRadioItem` (`role="menuitemradio"` + `aria-checked`) rather than
31
+ * hand-rolling those attributes onto a plain `DropdownItem`. Being the current
32
+ * model used to render as `font-medium` and nothing else — invisible to a screen
33
+ * reader. `kai-menu` already renders its radio items this way.
34
+ *
35
+ * `DropdownRadioItem` deliberately does NOT close the menu on activation (it is
36
+ * built for groups the consumer keeps mutating), so `onSelect` closes it here to
37
+ * preserve the pick-and-close behaviour `DropdownItem` used to provide.
38
+ */
39
+ function ModelRow(props: {
40
+ model: ModelOption;
41
+ currentModelId: string;
42
+ onModelChange: (id: string) => void;
43
+ onClose: () => void;
44
+ }) {
27
45
  const subtitle = () => props.model.description ?? props.model.provider;
46
+ const checked = () => props.model.id === props.currentModelId;
28
47
  return (
29
- <DropdownItem onSelect={() => props.onModelChange(props.model.id)}>
48
+ <DropdownRadioItem
49
+ checked={checked()}
50
+ onSelect={() => { props.onModelChange(props.model.id); props.onClose(); }}
51
+ >
30
52
  <div class="flex flex-col">
31
- <span class={cn('text-body', props.model.id === props.currentModelId && 'font-medium text-foreground')}>{props.model.name}</span>
53
+ <span class={cn('text-body', checked() && 'font-medium text-foreground')}>{props.model.name}</span>
32
54
  <Show when={subtitle()}><span class="text-caption text-muted-foreground">{subtitle()}</span></Show>
33
55
  </div>
34
- </DropdownItem>
56
+ </DropdownRadioItem>
35
57
  );
36
58
  }
37
59
 
@@ -39,6 +61,14 @@ export function ModelSwitcher(props: ModelSwitcherProps) {
39
61
  const [local] = splitProps(props, ['models', 'currentModelId', 'onModelChange', 'class', 'defaultOpen', 'disabled', 'controllerRef']);
40
62
  const currentModel = () => local.models.find((m) => m.id === local.currentModelId);
41
63
 
64
+ // The rows are DropdownRadioItems, which stay open by design, so the switcher
65
+ // needs its own handle on the dropdown to close after a pick. Tapping
66
+ // `controllerRef` keeps that inside this component — the dropdown's own context
67
+ // is module-private — and the consumer's `controllerRef` is still called, so
68
+ // this is invisible from the outside.
69
+ let controller: DropdownController | undefined;
70
+ const closeMenu = () => controller?.setOpen(false);
71
+
42
72
  // Ungrouped models list first; grouped models collect under their group name,
43
73
  // preserving first-seen group order.
44
74
  const ungrouped = () => local.models.filter((m) => !m.group);
@@ -58,7 +88,7 @@ export function ModelSwitcher(props: ModelSwitcherProps) {
58
88
  <Dropdown
59
89
  defaultOpen={local.defaultOpen}
60
90
  disabled={local.disabled}
61
- controllerRef={local.controllerRef}
91
+ controllerRef={(api) => { controller = api; local.controllerRef?.(api); }}
62
92
  >
63
93
  <DropdownTrigger as={(triggerProps: any) => (
64
94
  <Button variant="ghost" size="sm" class={cn('gap-1 text-meta text-muted-foreground', local.class)} {...triggerProps}>
@@ -68,7 +98,7 @@ export function ModelSwitcher(props: ModelSwitcherProps) {
68
98
  )} />
69
99
  <DropdownContent>
70
100
  <For each={ungrouped()}>
71
- {(model) => <ModelRow model={model} currentModelId={local.currentModelId} onModelChange={local.onModelChange} />}
101
+ {(model) => <ModelRow model={model} currentModelId={local.currentModelId} onModelChange={local.onModelChange} onClose={closeMenu} />}
72
102
  </For>
73
103
  <For each={groups()}>
74
104
  {(group) => {
@@ -87,7 +117,7 @@ export function ModelSwitcher(props: ModelSwitcherProps) {
87
117
  <Show when={open()}>
88
118
  <div class="pl-2">
89
119
  <For each={group.models}>
90
- {(model) => <ModelRow model={model} currentModelId={local.currentModelId} onModelChange={local.onModelChange} />}
120
+ {(model) => <ModelRow model={model} currentModelId={local.currentModelId} onModelChange={local.onModelChange} onClose={closeMenu} />}
91
121
  </For>
92
122
  </div>
93
123
  </Show>
@@ -214,7 +214,12 @@ export function Toast(props: ToastProps) {
214
214
  <Show when={presence.present()}>
215
215
  <div
216
216
  ref={presence.setRef}
217
- role="status"
217
+ // `variant` already picks the error ICON; it has to pick the ROLE too, or
218
+ // a failure is announced with the same (polite, easily missed) urgency as
219
+ // a success. `alert` is implicitly assertive and interrupts, which is what
220
+ // the region's `aria-live="polite"` alone will not do for the toast that
221
+ // actually matters. Same expression as ui/notice.tsx.
222
+ role={variant() === 'error' ? 'alert' : 'status'}
218
223
  data-appearance={appearance()}
219
224
  data-inverse={inverse() ? '' : undefined}
220
225
  data-closed={presence.state() === 'closed' ? '' : undefined}
@@ -2,6 +2,7 @@ import { Show } from 'solid-js';
2
2
  import { Button } from '../ui/button';
3
3
  import { renderIcon } from '../ui/icon';
4
4
  import { defineWebComponent } from './define';
5
+ import { createSlotText } from './slot-text';
5
6
 
6
7
  interface Props extends Record<string, unknown> {
7
8
  /** Visual style. `default` (filled), `subtle` (muted text, hover tint, the
@@ -17,7 +18,12 @@ interface Props extends Record<string, unknown> {
17
18
  /** Trailing icon, after the label (e.g. `"chevron-down"` for a menu affordance). */
18
19
  iconTrailing?: string;
19
20
  /** Accessible name. REQUIRED for icon-only buttons (no visible text); ignored
20
- * when you slot visible text, which already names the button. */
21
+ * when you slot visible text, which already names the button. An `aria-label`
22
+ * on top of visible text REPLACES that name rather than adding to it, so a
23
+ * button reading "Save" that answers to "Submit" is unusable by speech input
24
+ * (WCAG 2.5.3, Label in Name). The visible text wins. An `icon` / `icon-sm`
25
+ * size hides the slot, which makes the button icon-only whatever you slotted,
26
+ * so `label` is what names it there. */
21
27
  label?: string;
22
28
  /** Disable the button (non-interactive, dimmed). */
23
29
  disabled?: boolean;
@@ -71,6 +77,23 @@ defineWebComponent<Props, Events>('kai-button', {
71
77
  // `label` prop still names it for assistive tech).
72
78
  const iconOnly = () => props.size === 'icon' || props.size === 'icon-sm';
73
79
 
80
+ // Slotted text names the button by itself (name-from-contents reaches through
81
+ // the slot), so `label` must NOT be emitted on top of it: aria-label REPLACES
82
+ // the computed name, and a name that disagrees with the visible text locks
83
+ // speech-input users out of the control (WCAG 2.5.3). This is what the `label`
84
+ // doc has always promised, and what `kai-checkpoint` already does.
85
+ //
86
+ // `flatten` so a consumer who wraps <kai-button> in their own component and
87
+ // forwards a <slot> still counts as having visible text. Safe here because the
88
+ // default slot has no fallback content — see createSlotText.
89
+ const slotted = createSlotText({ flatten: true });
90
+
91
+ // The name to emit, or nothing. An `icon`/`icon-sm` size does not render the
92
+ // default slot at all, so whatever was slotted is INVISIBLE and `label` is the
93
+ // only name available — dropping it there would leave icon buttons nameless,
94
+ // which is the worse bug.
95
+ const ariaLabel = () => (!iconOnly() && slotted.hasText() ? undefined : props.label);
96
+
74
97
  // ── Imperative API (instance methods on the host) ──────────────────────────
75
98
  // The real <button> lives in the shadow root, so native focus()/blur()/click()
76
99
  // on the host hit the wrapper, not the control. Forward to the inner button.
@@ -104,7 +127,7 @@ defineWebComponent<Props, Events>('kai-button', {
104
127
  full={flag('full') || !iconOnly()}
105
128
  type={props.type ?? 'button'}
106
129
  disabled={flag('disabled')}
107
- aria-label={props.label}
130
+ aria-label={ariaLabel()}
108
131
  onClick={() => dispatch('kai-click')}
109
132
  >
110
133
  {/* Leading icon: a slotted SVG (slot="icon") wins — drop in any icon
@@ -115,7 +138,7 @@ defineWebComponent<Props, Events>('kai-button', {
115
138
  </Show>
116
139
  </slot>
117
140
  <Show when={!iconOnly()}>
118
- <slot />
141
+ <slot ref={slotted.ref} />
119
142
  </Show>
120
143
  <Show when={props.iconTrailing && !iconOnly()}>
121
144
  {renderIcon(props.iconTrailing, { class: 'size-4 shrink-0 opacity-60', imgClass: 'size-4 shrink-0', spanClass: 'inline-flex size-4 shrink-0 items-center justify-center', ariaHidden: true })}
@@ -5,6 +5,7 @@ import { ChatThread, type ChatThreadProps, type ChatThreadContextUsage, type Cha
5
5
  import { cardComponentsFromTags } from './message';
6
6
  import { createMessagesGuard } from './validate-messages';
7
7
  import type { AttachmentData } from '../components/attachments';
8
+ import type { RejectedAttachment } from './default-input';
8
9
  import type { ChatMessage } from './chat-types';
9
10
  import type { TriggerDef } from '../components/composer';
10
11
  import type { ComposerDoc } from '../primitives/composer-model';
@@ -13,7 +14,25 @@ import type { ModelOption } from '../types';
13
14
 
14
15
  type Props = Omit<ChatThreadProps,
15
16
  'class' | 'onValueChange' | 'onSubmit' | 'onAttachmentsChange' | 'onSuggestionClick' | 'onModelChange'
16
- | 'onMessageAction' | 'onSearch' | 'onVoice' | 'controllerRef' | 'cardTypes' | 'cardSchemas' | 'messages'> & Record<string, unknown> & {
17
+ | 'onMessageAction' | 'onSearch' | 'onVoice' | 'controllerRef' | 'cardTypes' | 'cardSchemas' | 'messages'
18
+ | 'accept' | 'onAttachmentsRejected'> & Record<string, unknown> & {
19
+ /** Which attachment media types the user may stage, in HTML `accept` syntax:
20
+ * `<kai-chat accept="image/*,application/pdf">`. A plain string, so unlike
21
+ * `messages` it DOES work as an attribute. Omitted means no filter.
22
+ *
23
+ * MEDIA TYPES ONLY -- exact (`image/png`) or subtype wildcard (`text/*`).
24
+ * HTML allows a file extension here and this does not: `accept=".py"` THROWS
25
+ * with the entry named, rather than silently resolving to a picker that
26
+ * accepts nothing.
27
+ *
28
+ * It can only NARROW what the kit can already encode: `accept="image/*"`
29
+ * resolves to the four image formats both APIs take, not to every image type
30
+ * the OS offers. Pass the SAME string to `toOpenAIMessages(msgs, { accept })`
31
+ * and the picker and the wire cannot disagree -- both resolve it through
32
+ * `resolveMediaPolicy` against one declaration. That declaration is readable
33
+ * as `encodableMediaTypes()` from `@kitn.ai/ui/wire`, if you would rather
34
+ * build your own picker than use this prop. */
35
+ accept?: string;
17
36
  /** The full message thread to render, newest last. Each entry carries its
18
37
  * role, ordered `parts`, and optional actions/avatar/feedback. Set as a JS
19
38
  * property (`el.messages = [...]`); a NEW array reference per streaming
@@ -54,6 +73,11 @@ interface Events {
54
73
  /** The staged attachments changed (file added or removed). Carries the full
55
74
  * current list so a consumer can react in real time. */
56
75
  'kai-attachments-change': { attachments: AttachmentData[] };
76
+ /** One or more picked files were refused because `accept` excluded them. The
77
+ * element renders NO message of its own: it reports the facts (name, media
78
+ * type, whether the kit could have sent it) and what the user should see is
79
+ * the application's call. Only ever fires when `accept` is set. */
80
+ 'kai-attachments-rejected': { rejected: RejectedAttachment[] };
57
81
  /** A suggestion chip was clicked (only in `suggestion-mode="fill"`). */
58
82
  'kai-suggestion-click': { value: string };
59
83
  /** An action button on a message was clicked. `action` is the built-in name or
@@ -74,7 +98,7 @@ defineWebComponent<Props, Events>('kai-chat', {
74
98
  codeTheme: 'github-dark-dimmed', codeHighlight: true, chatTitle: undefined,
75
99
  models: undefined, currentModel: undefined, context: undefined, scrollButton: true,
76
100
  search: false, voice: false, triggers: undefined, kindIcons: undefined,
77
- actionsReveal: 'always', cardTypes: undefined, cardSchemas: undefined,
101
+ actionsReveal: 'always', cardTypes: undefined, cardSchemas: undefined, accept: undefined,
78
102
  }, (props, { dispatch, flag, element, expose }) => {
79
103
  // `messages` is an untyped boundary: a consumer can hand it anything at
80
104
  // runtime (a pre-0.20.0 `{ id, role, content }` array, in particular). Skip
@@ -145,7 +169,9 @@ defineWebComponent<Props, Events>('kai-chat', {
145
169
  cardSchemas={props.cardSchemas as Record<string, object> | undefined}
146
170
  onValueChange={(value) => dispatch('kai-value-change', { value })}
147
171
  onSubmit={(detail) => dispatch('kai-submit', detail)}
172
+ accept={props.accept as string | undefined}
148
173
  onAttachmentsChange={(attachments) => dispatch('kai-attachments-change', { attachments })}
174
+ onAttachmentsRejected={(rejected) => dispatch('kai-attachments-rejected', { rejected })}
149
175
  onSuggestionClick={(value) => dispatch('kai-suggestion-click', { value })}
150
176
  onModelChange={(modelId) => dispatch('kai-model-change', { modelId })}
151
177
  onMessageAction={(detail) => dispatch('kai-message-action', detail)}