@kitn.ai/ui 0.21.0 → 0.22.1

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 +890 -78
  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 +4 -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 +913 -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 +234 -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
@@ -0,0 +1,342 @@
1
+ // Attachment -> wire classification, shared by both encoders.
2
+ //
3
+ // Split out of encode.ts because the two wires disagree about attachments in a
4
+ // way that is worth stating once: WHAT a file is (an image, a PDF, or something
5
+ // neither API takes as message content) is provider-neutral and decided here;
6
+ // HOW it rides on the wire is provider-specific and decided in encode.ts. The
7
+ // one gap that falls out of that split is OpenAI's, and it is real: a remote PDF
8
+ // has no representation in chat-completions at all.
9
+ //
10
+ // No I/O, exactly like the rest of this layer. Nothing here fetches a URL,
11
+ // reads a File, or base64s anything: an attachment either already carries bytes
12
+ // (a `data:` URI) or already carries an address the provider can resolve
13
+ // itself. That constraint is what makes a `blob:` URL unencodable rather than
14
+ // merely inconvenient -- resolving one requires the browser tab that minted it.
15
+ import type { AttachmentData } from '../components/attachment-types';
16
+ import {
17
+ DEFAULT_MEDIA_POLICY,
18
+ UNNAMED_TEXT_MEDIA_TYPE,
19
+ namesNothing,
20
+ type MediaPolicy,
21
+ } from './media-types';
22
+
23
+ /** `data:<media type>;base64,<data>`. Base64 is required -- both APIs need it,
24
+ * and a `data:` URI without it cannot become a content block without guessing.
25
+ * The media type is NOT required, because a browser omits it for a file it
26
+ * could not name: the File API says to emit a `data:` URL with no media type,
27
+ * and Chrome and jsdom both write `application/octet-stream` there instead.
28
+ * Either way there is no type to read, which `decide()` answers with
29
+ * `undetermined` and this file settles by decoding. */
30
+ const BASE64_DATA_URI = /^data:([^;,]*);base64,([\s\S]*)$/;
31
+
32
+ const SCHEME = /^([a-z][a-z0-9+.-]*):/i;
33
+
34
+ /** Where the bytes live. `base64` carries them inline; `remote` is an address
35
+ * the PROVIDER dereferences, never us. */
36
+ export type FileSource =
37
+ | { type: 'base64'; mediaType: string; data: string; dataUri: string }
38
+ | { type: 'remote'; url: string };
39
+
40
+ /** Bytes already in hand, shaped for the provider. A union rather than an
41
+ * optional `text` field so neither encoder can reach for text that is not
42
+ * there, and so adding a kind is a compile error at both wires rather than a
43
+ * silently unhandled branch. */
44
+ export type ClassifiedFile =
45
+ /** Named for the Anthropic blocks they become. `document` is PDF-only. */
46
+ | { kind: 'image'; mediaType: string; source: FileSource; filename?: string }
47
+ | { kind: 'document'; mediaType: string; source: FileSource; filename?: string }
48
+ /** The bytes, decoded. A text file rides as TEXT CONTENT on both wires --
49
+ * neither API has an arbitrary-file block, so this is the only representation
50
+ * the wire can express, not a preference. `source` is narrowed to `base64`
51
+ * because the text has to be in hand to inline it, and fetching a remote one
52
+ * would put I/O in the encoder. */
53
+ | {
54
+ kind: 'text';
55
+ mediaType: string;
56
+ source: Extract<FileSource, { type: 'base64' }>;
57
+ filename?: string;
58
+ text: string;
59
+ };
60
+
61
+ export type FileClassification =
62
+ /** Ready for either wire to shape. A wire may still refuse it -- see
63
+ * OpenAI and remote PDFs -- but the attachment itself is sound. */
64
+ | { status: 'encodable'; file: ClassifiedFile }
65
+ /** Not an upload at all. A `source-document` is the citation chip an app
66
+ * renders next to a RAG answer, not something the user staged to send, and
67
+ * its content is already in the prompt that produced the answer. Encoding it
68
+ * would send the same text twice. */
69
+ | { status: 'kit-side' }
70
+ /** Cannot reach a model. `reason` completes the sentence "Cannot encode file
71
+ * part N of message X: ...", so it explains AND says what to do. */
72
+ | { status: 'unencodable'; reason: string };
73
+
74
+ const quotedList = (values: readonly string[]): string => values.map((v) => `"${v}"`).join(', ');
75
+
76
+ /**
77
+ * Base64 to the bytes it stands for.
78
+ *
79
+ * NOT a violation of this file's no-I/O rule. The bytes are already inline in
80
+ * the `data:` URI the host staged; this decodes what is in hand and reaches for
81
+ * nothing. `atob` yields a binary string (one char per byte), which is then read
82
+ * as UTF-8 -- going straight from `atob` to text would mangle every non-ASCII
83
+ * character in the file.
84
+ *
85
+ * ★ WHAT A CLEAN DECODE PROVES, AND WHAT IT DOES NOT. It proves the bytes are
86
+ * well-formed UTF-8. It does not prove they are meaningful text, and the gap is
87
+ * not hypothetical: every byte below 0x80 is valid UTF-8 on its own, so a small
88
+ * binary made of low bytes decodes cleanly. A bare zip header -- `50 4b 03 04`
89
+ * followed by NULs -- is exactly that. This matters most on the path where
90
+ * nothing named the file, because there the decode is the ONLY evidence there is.
91
+ *
92
+ * A NUL-byte heuristic (git's rule for "is this binary") would catch most of
93
+ * them, and it is deliberately NOT here. This same function serves the LABELLED
94
+ * text path, where a host that said `text/plain` has asserted something the kit
95
+ * has no business overruling, and a text file that legitimately contains a NUL
96
+ * would start being refused on a rule nobody asked for. Weigh the two failures:
97
+ * being wrong the current way sends a block of gibberish the model can see and
98
+ * describe, and being wrong the other way silently refuses a real text file.
99
+ * Only the first is recoverable by the person it happens to.
100
+ */
101
+ function decodeBase64Text(data: string): { ok: true; text: string; bytes: number } | { ok: false } {
102
+ try {
103
+ const binary = atob(data);
104
+ const bytes = new Uint8Array(binary.length);
105
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
106
+ // `fatal` so mislabelled binary (a .zip renamed .txt) is refused with a
107
+ // reason instead of silently becoming a screenful of replacement characters.
108
+ return { ok: true, text: new TextDecoder('utf-8', { fatal: true }).decode(bytes), bytes: binary.length };
109
+ } catch {
110
+ return { ok: false };
111
+ }
112
+ }
113
+
114
+ /**
115
+ * XML attribute escaping, for the two attributes the envelope carries.
116
+ *
117
+ * A filename is whatever the user called their file, and it lands between double
118
+ * quotes: `notes" x="` would otherwise rewrite the header into attributes of its
119
+ * own choosing. Newlines are escaped for the same reason and not for tidiness --
120
+ * a raw one splits the header across lines, which is how a filename gets to look
121
+ * like the start of the content.
122
+ *
123
+ * The media type goes through the same function, which is not symmetry for its
124
+ * own sake: it comes from a `data:` URI, where it is anything up to the first
125
+ * `;`, and `text/pl"ain` matches the `text/*` capability and arrives here whole.
126
+ */
127
+ const escapeAttribute = (value: string): string =>
128
+ value
129
+ .replace(/&/g, '&amp;')
130
+ .replace(/</g, '&lt;')
131
+ .replace(/>/g, '&gt;')
132
+ .replace(/"/g, '&quot;')
133
+ .replace(/\r/g, '&#13;')
134
+ .replace(/\n/g, '&#10;');
135
+
136
+ /** The only sequence in the body that could end the block early, and so the only
137
+ * thing in the body that gets touched. Whitespace-tolerant and case-insensitive
138
+ * because the reader here is a model, and `</file >` reads as the end to one
139
+ * just as readily as `</file>` does. */
140
+ const CLOSING_DELIMITER = /<(\/file\s*)>/gi;
141
+
142
+ /**
143
+ * The text content a text attachment contributes to the prompt.
144
+ *
145
+ * ★ THE SWAP POINT for how a text file appears in a message, and the shape is
146
+ * SETTLED: `<file name="..." type="...">…</file>`. Two alternatives were weighed
147
+ * and rejected. A bare `filename:\n` prefix marks where the file BEGINS and not
148
+ * where it ends, so the next part of the turn reads as more file. An Anthropic
149
+ * `document` block would buy citations, but it is Anthropic-specific -- the
150
+ * OpenAI wire has no equivalent, so the two wires would disagree about one
151
+ * attachment -- and it needs a Files API upload round trip, which is I/O, in a
152
+ * layer that by design has none. Changing the envelope is still a change to this
153
+ * function and nothing else; it just is not an open question.
154
+ *
155
+ * ★ WHY THE BODY IS BARELY TOUCHED. Both attributes are escaped in full, as
156
+ * attributes always are. The body is not, and that is deliberate: these are
157
+ * source files, a reader will diff what they attached against what arrived, and
158
+ * XML-escaping every `<` in a TSX file would mangle the exact thing this feature
159
+ * exists to send and pay tokens to do it. So the body transform is the smallest
160
+ * one that makes the delimiter unforgeable -- the end tag, and nothing else.
161
+ *
162
+ * Left alone, that end tag is a way out of the block. A file containing `</file>`
163
+ * closes it early, and everything after that point reads to the model as the turn
164
+ * AROUND the file rather than as file content: user-supplied bytes in instruction
165
+ * position, reachable by anyone who can get a file in front of the composer.
166
+ * Escaped, it stays where it belongs. `&lt;/file&gt;` ends nothing, and it is
167
+ * visible in the output and reversible by eye, which a deletion or a silent
168
+ * substitution would not be -- a reader who spots it can say exactly what the
169
+ * original byte was. The OPENING tag is left untouched on purpose: it cannot end
170
+ * the block, and escaping it would corrupt every HTML file anyone ever attaches.
171
+ */
172
+ export function textFileContent(file: Extract<ClassifiedFile, { kind: 'text' }>): string {
173
+ const name = escapeAttribute(file.filename ?? 'attachment');
174
+ const type = escapeAttribute(file.mediaType);
175
+ const text = file.text.replace(CLOSING_DELIMITER, '&lt;$1&gt;');
176
+ return `<file name="${name}" type="${type}">\n${text}\n</file>`;
177
+ }
178
+
179
+ /**
180
+ * One attachment to the provider-neutral facts both encoders need.
181
+ *
182
+ * A `data:` URI's OWN media type wins over `attachment.mediaType` when they
183
+ * disagree: the URI describes the bytes actually present, while the field is a
184
+ * label the host set, and it is the bytes the provider will decode. The one
185
+ * exception is a URI whose media type names nothing, where there is no answer to
186
+ * prefer -- see below.
187
+ *
188
+ * When NOTHING names the file, it is settled by DECODING it rather than by
189
+ * reading its filename. That path can only ever produce text, and only when the
190
+ * policy still allows text, so it cannot be used to get around `accept`.
191
+ *
192
+ * `policy` narrows what counts as encodable. It defaults to the kit's full
193
+ * capability set, so an omitted policy behaves exactly as before for images and
194
+ * PDFs. It is the SAME object the composer resolves for its picker -- see
195
+ * `media-types.ts` for why that sharing is the point rather than a convenience.
196
+ */
197
+ export function classifyAttachment(
198
+ attachment: AttachmentData,
199
+ policy: MediaPolicy = DEFAULT_MEDIA_POLICY,
200
+ ): FileClassification {
201
+ if (attachment.type === 'source-document') return { status: 'kit-side' };
202
+
203
+ const url = attachment.url;
204
+ if (url === undefined || url === '') {
205
+ return {
206
+ status: 'unencodable',
207
+ reason:
208
+ 'it has no `url`, so there are no bytes and no address to send. Set `url` to a `data:` URI (read the File with FileReader.readAsDataURL before you stage it) or to an https URL the provider can fetch.',
209
+ };
210
+ }
211
+
212
+ let source: FileSource;
213
+ let mediaType: string;
214
+
215
+ const asData = BASE64_DATA_URI.exec(url);
216
+ if (asData) {
217
+ const inUri = asData[1].toLowerCase();
218
+ // The URI's own media type wins -- but only when it NAMES something. A
219
+ // browser writes `application/octet-stream` (or nothing) for a file it could
220
+ // not identify, and a non-answer describes the bytes no better than the
221
+ // host's `mediaType` field does. In that one case the field is the only
222
+ // label anybody actually wrote down, so it is read rather than overridden.
223
+ mediaType = namesNothing(inUri) ? (attachment.mediaType?.toLowerCase() ?? inUri) : inUri;
224
+ source = { type: 'base64', mediaType, data: asData[2], dataUri: url };
225
+ } else if (/^https?:\/\//i.test(url)) {
226
+ const declared = attachment.mediaType?.toLowerCase();
227
+ if (declared === undefined || declared === '') {
228
+ return {
229
+ status: 'unencodable',
230
+ reason: `its \`url\` is remote ("${url}") but it has no \`mediaType\`, so there is no way to tell an image from a document without fetching it -- and this layer does no I/O. Set \`mediaType\` when you stage the attachment.`,
231
+ };
232
+ }
233
+ mediaType = declared;
234
+ source = { type: 'remote', url };
235
+ } else if (url.startsWith('data:')) {
236
+ return {
237
+ status: 'unencodable',
238
+ reason:
239
+ 'its `url` is a `data:` URI that is not base64-encoded with an explicit media type. Both APIs need `data:<media type>;base64,<data>`.',
240
+ };
241
+ } else {
242
+ const scheme = SCHEME.exec(url)?.[1].toLowerCase();
243
+ // blob: is the one people hit, because it is what URL.createObjectURL and
244
+ // every drag-and-drop example hand you. It resolves only inside the tab that
245
+ // created it, so it looks perfect in the thread and is meaningless to a model.
246
+ const detail =
247
+ scheme === 'blob'
248
+ ? 'a `blob:` URL resolves only inside the browser tab that created it, so a model can never fetch it. Read the File with FileReader.readAsDataURL and stage the `data:` URI instead, or upload it and stage the resulting https URL'
249
+ : `a "${scheme ?? url}" URL is not something a provider can fetch. Use a \`data:\` URI or an https URL`;
250
+ return { status: 'unencodable', reason: `its \`url\` is "${url}": ${detail}.` };
251
+ }
252
+
253
+ const decision = policy.decide(mediaType);
254
+
255
+ if (decision.status === 'undetermined') {
256
+ // ★ NOBODY NAMED THIS FILE, SO READ IT. Seven of the file types a developer
257
+ // is likeliest to attach to a coding chat arrive from Chrome with no media
258
+ // type at all (the table in media-types.ts), and the tempting fix -- map
259
+ // `.rs` to text -- is a filename talking, which is the one thing this design
260
+ // refuses. The bytes are already in hand, so ask them instead: a clean UTF-8
261
+ // decode IS the evidence that this is text, and it is evidence about the
262
+ // actual file rather than about its name.
263
+ //
264
+ // The developer's filter is already enforced: `undetermined` comes back only
265
+ // when the effective policy still admits plain text, so reaching this line at
266
+ // all means a text file is something this policy would take. An
267
+ // images-only `accept` gets `unsupported` and never arrives here.
268
+ if (source.type !== 'base64') {
269
+ return {
270
+ status: 'unencodable',
271
+ reason: `it is at a remote URL ("${source.url}") and its media type "${mediaType}" says nothing about the bytes -- that is the label for "arbitrary binary", so it separates an image from a document not at all. Settling it by reading the bytes would need a fetch, and this layer does no I/O. Set \`mediaType\` to what the file actually is when you stage it.`,
272
+ };
273
+ }
274
+ const decoded = decodeBase64Text(source.data);
275
+ if (!decoded.ok) {
276
+ return {
277
+ status: 'unencodable',
278
+ reason: `nothing names it -- ${mediaType === '' ? 'it carries no media type' : `its media type is "${mediaType}"`}, so the only way to tell what it is was to read it -- and its bytes are not valid UTF-8, so it is binary of some unidentified kind. Neither API has an arbitrary-file block to carry that. Set \`mediaType\` if you know what it is, or hand the file to the model through a tool.`,
279
+ };
280
+ }
281
+ // `text/plain` and not the media type it arrived with: a decode establishes
282
+ // "this is text" and nothing more specific, and that is exactly what
283
+ // `text/plain` means. See UNNAMED_TEXT_MEDIA_TYPE.
284
+ return {
285
+ status: 'encodable',
286
+ file: {
287
+ kind: 'text',
288
+ mediaType: UNNAMED_TEXT_MEDIA_TYPE,
289
+ source,
290
+ filename: attachment.filename,
291
+ text: decoded.text,
292
+ },
293
+ };
294
+ }
295
+
296
+ if (decision.status === 'filtered') {
297
+ return {
298
+ status: 'unencodable',
299
+ reason: `its media type "${mediaType}" is one this kit can encode, but your \`accept\` filter excludes it. The types you allowed are: ${quotedList(policy.types)}. Widen \`accept\` to include it, or stop staging it.`,
300
+ };
301
+ }
302
+
303
+ if (decision.status === 'unsupported') {
304
+ return {
305
+ status: 'unencodable',
306
+ reason: `its media type "${mediaType}" is not one either API accepts as message content. Supported: ${quotedList(policy.types)}. Extract the content yourself and send it as text, or hand it to the model through a tool.`,
307
+ };
308
+ }
309
+
310
+ if (decision.kind === 'text') {
311
+ // A remote text file would have to be FETCHED to be inlined, and this layer
312
+ // does no I/O. Refused with the fix rather than silently dropped.
313
+ if (source.type !== 'base64') {
314
+ return {
315
+ status: 'unencodable',
316
+ reason: `it is a text file at a remote URL ("${source.url}"), and text has to ride as text CONTENT -- neither API has an arbitrary-file block to point at a URL with. Reading it here would put I/O in the encoder. Fetch it yourself and stage the \`data:\` URI, or paste the contents as a text part.`,
317
+ };
318
+ }
319
+ const decoded = decodeBase64Text(source.data);
320
+ if (!decoded.ok) {
321
+ return {
322
+ status: 'unencodable',
323
+ reason: `its media type "${mediaType}" says text, but its bytes are not valid UTF-8 -- so it is binary wearing a text label, and inlining it would send the model garbage. Send it under its real media type, or extract the text yourself.`,
324
+ };
325
+ }
326
+ return {
327
+ status: 'encodable',
328
+ file: {
329
+ kind: 'text',
330
+ mediaType,
331
+ source,
332
+ filename: attachment.filename,
333
+ text: decoded.text,
334
+ },
335
+ };
336
+ }
337
+
338
+ return {
339
+ status: 'encodable',
340
+ file: { kind: decision.kind, mediaType, source, filename: attachment.filename },
341
+ };
342
+ }
package/src/wire/index.ts CHANGED
@@ -18,13 +18,34 @@ export { applyToolOutput, applyToolFailure, bufferText } from './sink-helpers';
18
18
  export { toOpenAIMessages, toAnthropicMessages, WireEncodeError } from './encode';
19
19
  export type {
20
20
  AnthropicContentBlock,
21
+ AnthropicEncodeOptions,
21
22
  AnthropicWireMessage,
23
+ FileEncodeOptions,
24
+ OpenAIContentPart,
22
25
  OpenAIEncodeOptions,
23
26
  OpenAIReasoningDetail,
24
27
  OpenAIToolCall,
25
28
  OpenAIWireMessage,
29
+ UnencodableFilePolicy,
26
30
  } from './encode';
27
31
 
32
+ // What the encoders CAN represent, published as a value.
33
+ //
34
+ // The point is not introspection. A consumer who wants their own file picker,
35
+ // their own validation and their own error copy can build all three off
36
+ // `encodableMediaTypes()` and `resolveMediaPolicy(...).decide(...)` without
37
+ // using our composer at all -- the kit states a fact about itself and the
38
+ // application decides what to do about it. `<kai-chat accept="...">` is a
39
+ // convenience over exactly this, not a separate mechanism.
40
+ export { encodableMediaTypes, resolveMediaPolicy } from './media-types';
41
+ export type {
42
+ EncodableKind,
43
+ MediaDecision,
44
+ MediaPolicy,
45
+ MediaPolicyOptions,
46
+ MediaTypeFilter,
47
+ } from './media-types';
48
+
28
49
  export { openaiChatFormat } from './formats/openai';
29
50
  export { anthropicMessagesFormat } from './formats/anthropic';
30
51