@kitn.ai/ui 0.28.0 → 0.29.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 (376) hide show
  1. package/README.md +1 -1
  2. package/dist/agent-tooling/construct/public.d.ts +15 -0
  3. package/dist/agent-tooling/construct/schema.d.ts +111 -0
  4. package/dist/construct-cli.es.js +3 -3
  5. package/dist/construct.d.ts +5 -0
  6. package/dist/construct.js +327 -0
  7. package/dist/core-C8fzo39E.js +1 -12
  8. package/dist/engine-javascript-C1x7zo1_.js +1 -141
  9. package/dist/mcp.es.js +1 -1
  10. package/package.json +16 -16
  11. package/bin/route.test.js +0 -58
  12. package/src/agent-tooling/README.md +0 -192
  13. package/src/agent-tooling/archetypes.ts +0 -205
  14. package/src/agent-tooling/catalog/README.md +0 -676
  15. package/src/agent-tooling/catalog/catalog-types.ts +0 -231
  16. package/src/agent-tooling/catalog/fabrications.ts +0 -96
  17. package/src/agent-tooling/catalog/invariants.ts +0 -284
  18. package/src/agent-tooling/catalog/labs-titles.ts +0 -114
  19. package/src/agent-tooling/catalog/scenarios.ts +0 -87
  20. package/src/agent-tooling/catalog/surfaces.ts +0 -347
  21. package/src/agent-tooling/construct/cli-entry.ts +0 -8
  22. package/src/agent-tooling/construct/cli.ts +0 -147
  23. package/src/agent-tooling/construct/codegen.ts +0 -1635
  24. package/src/agent-tooling/construct/construct.v1.schema.json +0 -319
  25. package/src/agent-tooling/construct/dev.ts +0 -148
  26. package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +0 -7
  27. package/src/agent-tooling/construct/fixtures/ops-console.construct.json +0 -44
  28. package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +0 -32
  29. package/src/agent-tooling/construct/schema.ts +0 -452
  30. package/src/agent-tooling/integrations/anthropic.ts +0 -442
  31. package/src/agent-tooling/integrations/cloudflare.ts +0 -218
  32. package/src/agent-tooling/integrations/langgraph.ts +0 -133
  33. package/src/agent-tooling/integrations/mastra.ts +0 -182
  34. package/src/agent-tooling/integrations/mock.ts +0 -148
  35. package/src/agent-tooling/integrations/ollama.ts +0 -97
  36. package/src/agent-tooling/integrations/openai.ts +0 -95
  37. package/src/agent-tooling/integrations/openrouter.ts +0 -82
  38. package/src/agent-tooling/integrations/pi.ts +0 -176
  39. package/src/agent-tooling/integrations/pydantic-ai.ts +0 -108
  40. package/src/agent-tooling/integrations/vercel-ai-sdk.ts +0 -398
  41. package/src/agent-tooling/mcp/css-raw.d.ts +0 -12
  42. package/src/agent-tooling/mcp/manifest.ts +0 -422
  43. package/src/agent-tooling/mcp/server.ts +0 -116
  44. package/src/agent-tooling/mcp/stdio.ts +0 -18
  45. package/src/agent-tooling/mcp/tools/construct.ts +0 -130
  46. package/src/agent-tooling/mcp/tools/debug.ts +0 -621
  47. package/src/agent-tooling/mcp/tools/reference.ts +0 -980
  48. package/src/agent-tooling/mcp/tools/scaffold.ts +0 -6708
  49. package/src/agent-tooling/mcp/tools/theme.ts +0 -456
  50. package/src/agent-tooling/mcp/tools/types.ts +0 -14
  51. package/src/agent-tooling/mcp/validate-args.ts +0 -141
  52. package/src/agent-tooling/recipes/composed-thread.ts +0 -714
  53. package/src/agent-tooling/recipes/index.ts +0 -22
  54. package/src/agent-tooling/recipes/types.ts +0 -32
  55. package/src/agent-tooling/registry.ts +0 -132
  56. package/src/agent-tooling/route-emit.ts +0 -305
  57. package/src/agent-tooling/types.ts +0 -390
  58. package/src/components/artifact-card.tsx +0 -120
  59. package/src/components/artifact.tsx +0 -914
  60. package/src/components/attachment-types.ts +0 -66
  61. package/src/components/attachments.tsx +0 -536
  62. package/src/components/audio-visualizer/audio-visualizer.voice-fixture.ts +0 -257
  63. package/src/components/audio-visualizer/aurora.glsl.ts +0 -330
  64. package/src/components/audio-visualizer/fit-scale.ts +0 -112
  65. package/src/components/audio-visualizer/index.tsx +0 -528
  66. package/src/components/audio-visualizer/labs/lab-choreography.ts +0 -187
  67. package/src/components/audio-visualizer/labs/lab-shaders.ts +0 -363
  68. package/src/components/audio-visualizer/labs/lab-visualizer.tsx +0 -102
  69. package/src/components/audio-visualizer/shader-canvas.tsx +0 -876
  70. package/src/components/audio-visualizer/sizes.ts +0 -54
  71. package/src/components/audio-visualizer/variant-aurora.tsx +0 -305
  72. package/src/components/audio-visualizer/variant-bar.tsx +0 -175
  73. package/src/components/audio-visualizer/variant-custom.tsx +0 -344
  74. package/src/components/audio-visualizer/variant-grid.tsx +0 -175
  75. package/src/components/audio-visualizer/variant-radial.tsx +0 -163
  76. package/src/components/audio-visualizer/variant-wave.tsx +0 -130
  77. package/src/components/audio-visualizer/wave.glsl.ts +0 -130
  78. package/src/components/card-fallback.tsx +0 -60
  79. package/src/components/card-renderer.tsx +0 -220
  80. package/src/components/card.tsx +0 -212
  81. package/src/components/chain-of-thought.tsx +0 -348
  82. package/src/components/chat-container.tsx +0 -89
  83. package/src/components/chat-scope-picker.tsx +0 -57
  84. package/src/components/chat-thread.tsx +0 -1014
  85. package/src/components/checkpoint.tsx +0 -88
  86. package/src/components/choice-card.tsx +0 -743
  87. package/src/components/coachmark.tsx +0 -249
  88. package/src/components/code-block.tsx +0 -181
  89. package/src/components/composer-dom.ts +0 -159
  90. package/src/components/composer-highlight.ts +0 -242
  91. package/src/components/composer-history.ts +0 -82
  92. package/src/components/composer.tsx +0 -1075
  93. package/src/components/confirm-card.tsx +0 -402
  94. package/src/components/context.tsx +0 -377
  95. package/src/components/conversation-item.tsx +0 -241
  96. package/src/components/conversation-list.tsx +0 -429
  97. package/src/components/conversation-panel.tsx +0 -118
  98. package/src/components/dismissed-stub.tsx +0 -80
  99. package/src/components/embed.tsx +0 -200
  100. package/src/components/empty.tsx +0 -177
  101. package/src/components/feedback-bar.tsx +0 -166
  102. package/src/components/file-tree.tsx +0 -530
  103. package/src/components/file-upload.tsx +0 -167
  104. package/src/components/form-widgets.tsx +0 -525
  105. package/src/components/form.tsx +0 -1296
  106. package/src/components/home-panel.tsx +0 -159
  107. package/src/components/image.tsx +0 -65
  108. package/src/components/link-preview.tsx +0 -195
  109. package/src/components/loader.tsx +0 -367
  110. package/src/components/markdown.tsx +0 -162
  111. package/src/components/message-skills.tsx +0 -40
  112. package/src/components/message.tsx +0 -765
  113. package/src/components/model-switcher.tsx +0 -132
  114. package/src/components/prompt-input.tsx +0 -188
  115. package/src/components/prompt-suggestion.tsx +0 -150
  116. package/src/components/reasoning.tsx +0 -293
  117. package/src/components/response-compare-types.ts +0 -152
  118. package/src/components/response-compare.tsx +0 -455
  119. package/src/components/response-stream.tsx +0 -109
  120. package/src/components/screen.tsx +0 -181
  121. package/src/components/scroll-button.tsx +0 -93
  122. package/src/components/source.tsx +0 -192
  123. package/src/components/tasks-card.tsx +0 -801
  124. package/src/components/text-shimmer.tsx +0 -37
  125. package/src/components/thinking-bar.tsx +0 -50
  126. package/src/components/thread.tsx +0 -228
  127. package/src/components/toast.tsx +0 -503
  128. package/src/components/tool-classify.ts +0 -26
  129. package/src/components/tool-types.ts +0 -49
  130. package/src/components/tool.tsx +0 -190
  131. package/src/components/use-card-resolution.ts +0 -44
  132. package/src/components/voice-input.tsx +0 -222
  133. package/src/components/voice-output.tsx +0 -242
  134. package/src/components/widget-tab-bar.tsx +0 -73
  135. package/src/components/workspace-shell.tsx +0 -285
  136. package/src/diagnostics/hook.ts +0 -352
  137. package/src/diagnostics/index.ts +0 -82
  138. package/src/diagnostics/report-request.ts +0 -296
  139. package/src/elements/agent-card.tsx +0 -67
  140. package/src/elements/artifact.tsx +0 -194
  141. package/src/elements/attachments.tsx +0 -140
  142. package/src/elements/audio-visualizer.tsx +0 -227
  143. package/src/elements/autoloader.ts +0 -89
  144. package/src/elements/avatar.tsx +0 -33
  145. package/src/elements/badge.tsx +0 -32
  146. package/src/elements/button.tsx +0 -149
  147. package/src/elements/card.tsx +0 -133
  148. package/src/elements/cards.tsx +0 -266
  149. package/src/elements/chain-of-thought.tsx +0 -141
  150. package/src/elements/chat-scope-picker.tsx +0 -65
  151. package/src/elements/chat-types.ts +0 -102
  152. package/src/elements/chat-workspace.tsx +0 -188
  153. package/src/elements/chat.tsx +0 -304
  154. package/src/elements/checkbox-group.tsx +0 -164
  155. package/src/elements/checkbox.tsx +0 -134
  156. package/src/elements/checkpoint.tsx +0 -47
  157. package/src/elements/choice.tsx +0 -100
  158. package/src/elements/coachmark.tsx +0 -90
  159. package/src/elements/code-block.tsx +0 -60
  160. package/src/elements/command.tsx +0 -197
  161. package/src/elements/compare.tsx +0 -111
  162. package/src/elements/compiled.css +0 -2
  163. package/src/elements/composer.tsx +0 -162
  164. package/src/elements/confirm-card.tsx +0 -76
  165. package/src/elements/context-meter.tsx +0 -107
  166. package/src/elements/conversation-item.tsx +0 -120
  167. package/src/elements/conversation-list.tsx +0 -203
  168. package/src/elements/css.ts +0 -5
  169. package/src/elements/default-input.tsx +0 -392
  170. package/src/elements/define-entry.ts +0 -12
  171. package/src/elements/define.tsx +0 -563
  172. package/src/elements/diagnostic-events.ts +0 -114
  173. package/src/elements/dialog.tsx +0 -145
  174. package/src/elements/disclosure.ts +0 -95
  175. package/src/elements/dock.tsx +0 -215
  176. package/src/elements/dropdown.tsx +0 -123
  177. package/src/elements/editable-label.tsx +0 -140
  178. package/src/elements/element-data-types.ts +0 -152
  179. package/src/elements/element-diagnostics.ts +0 -392
  180. package/src/elements/element-manifest.json +0 -354
  181. package/src/elements/element-nonscalar.json +0 -187
  182. package/src/elements/element-types.d.ts +0 -4353
  183. package/src/elements/embed.tsx +0 -35
  184. package/src/elements/empty.tsx +0 -29
  185. package/src/elements/feedback-bar.tsx +0 -66
  186. package/src/elements/file-tree.tsx +0 -62
  187. package/src/elements/file-upload.tsx +0 -44
  188. package/src/elements/form.tsx +0 -94
  189. package/src/elements/hover-card.tsx +0 -80
  190. package/src/elements/icon.tsx +0 -41
  191. package/src/elements/image.tsx +0 -32
  192. package/src/elements/input.tsx +0 -492
  193. package/src/elements/kbd.tsx +0 -45
  194. package/src/elements/link-preview.tsx +0 -34
  195. package/src/elements/loader.tsx +0 -25
  196. package/src/elements/markdown.tsx +0 -38
  197. package/src/elements/menu.tsx +0 -241
  198. package/src/elements/message-skills.tsx +0 -83
  199. package/src/elements/message.tsx +0 -361
  200. package/src/elements/model-switcher.tsx +0 -121
  201. package/src/elements/nav.tsx +0 -87
  202. package/src/elements/notice.tsx +0 -53
  203. package/src/elements/pane-grid.tsx +0 -111
  204. package/src/elements/pane-group.tsx +0 -119
  205. package/src/elements/pane.tsx +0 -129
  206. package/src/elements/popover.tsx +0 -81
  207. package/src/elements/progress-bar.tsx +0 -39
  208. package/src/elements/prompt-dock.tsx +0 -82
  209. package/src/elements/prompt-input.tsx +0 -240
  210. package/src/elements/prompt-suggestions.tsx +0 -113
  211. package/src/elements/radio-group.tsx +0 -129
  212. package/src/elements/reasoning.tsx +0 -71
  213. package/src/elements/register-impl.ts +0 -148
  214. package/src/elements/register.ts +0 -76
  215. package/src/elements/remote.tsx +0 -225
  216. package/src/elements/resizable.globals.d.ts +0 -36
  217. package/src/elements/resizable.tsx +0 -704
  218. package/src/elements/response-stream.tsx +0 -40
  219. package/src/elements/screen.tsx +0 -90
  220. package/src/elements/scroll-area.tsx +0 -33
  221. package/src/elements/scroll-button.tsx +0 -179
  222. package/src/elements/search.tsx +0 -185
  223. package/src/elements/segmented.tsx +0 -129
  224. package/src/elements/select.tsx +0 -165
  225. package/src/elements/separator.tsx +0 -36
  226. package/src/elements/setting-item.tsx +0 -50
  227. package/src/elements/settings-group.tsx +0 -37
  228. package/src/elements/skeleton.tsx +0 -45
  229. package/src/elements/slider.tsx +0 -171
  230. package/src/elements/slot-text.ts +0 -73
  231. package/src/elements/slots.ts +0 -787
  232. package/src/elements/source.tsx +0 -126
  233. package/src/elements/status.tsx +0 -42
  234. package/src/elements/styles.css +0 -328
  235. package/src/elements/switch.tsx +0 -118
  236. package/src/elements/tabs.tsx +0 -95
  237. package/src/elements/tasks.tsx +0 -103
  238. package/src/elements/text-shimmer.tsx +0 -28
  239. package/src/elements/thinking-bar.tsx +0 -43
  240. package/src/elements/thread.tsx +0 -139
  241. package/src/elements/toast.tsx +0 -106
  242. package/src/elements/tool.tsx +0 -78
  243. package/src/elements/tooltip.tsx +0 -85
  244. package/src/elements/validate-messages.ts +0 -76
  245. package/src/elements/voice-input.tsx +0 -100
  246. package/src/elements/voice-output.tsx +0 -82
  247. package/src/index.ts +0 -430
  248. package/src/primitives/audio-bands.ts +0 -174
  249. package/src/primitives/card-component-types.ts +0 -63
  250. package/src/primitives/card-contract.ts +0 -80
  251. package/src/primitives/card-data-types.ts +0 -361
  252. package/src/primitives/card-host.tsx +0 -37
  253. package/src/primitives/card-recovery.ts +0 -160
  254. package/src/primitives/card-registry.tsx +0 -78
  255. package/src/primitives/card-resolution.ts +0 -36
  256. package/src/primitives/card-routing.ts +0 -85
  257. package/src/primitives/card-schemas/artifact.schema.json +0 -92
  258. package/src/primitives/card-schemas/card-envelope.schema.json +0 -14
  259. package/src/primitives/card-schemas/card-event.schema.json +0 -12
  260. package/src/primitives/card-schemas/choice.schema.json +0 -75
  261. package/src/primitives/card-schemas/confirm.schema.json +0 -65
  262. package/src/primitives/card-schemas/embed.schema.json +0 -63
  263. package/src/primitives/card-schemas/form.result.schema.json +0 -7
  264. package/src/primitives/card-schemas/form.schema.json +0 -53
  265. package/src/primitives/card-schemas/link.schema.json +0 -54
  266. package/src/primitives/card-schemas/tasks.result.schema.json +0 -16
  267. package/src/primitives/card-schemas/tasks.schema.json +0 -83
  268. package/src/primitives/card-tags.ts +0 -61
  269. package/src/primitives/card-validate-cards.ts +0 -255
  270. package/src/primitives/card-validate-schemas.ts +0 -58
  271. package/src/primitives/card-validate.ts +0 -131
  272. package/src/primitives/chat-config.tsx +0 -103
  273. package/src/primitives/composer-model.ts +0 -49
  274. package/src/primitives/composer-triggers.ts +0 -35
  275. package/src/primitives/controllable.ts +0 -21
  276. package/src/primitives/conversation-store.ts +0 -248
  277. package/src/primitives/create-kai-chat.ts +0 -66
  278. package/src/primitives/create-tween.ts +0 -266
  279. package/src/primitives/embed-providers.ts +0 -254
  280. package/src/primitives/field-mask.ts +0 -256
  281. package/src/primitives/field-semantics.ts +0 -115
  282. package/src/primitives/highlighter.ts +0 -157
  283. package/src/primitives/input-mask.ts +0 -853
  284. package/src/primitives/link-preview.ts +0 -87
  285. package/src/primitives/message-feedback.ts +0 -124
  286. package/src/primitives/pdf-preview.ts +0 -121
  287. package/src/primitives/toast-store.ts +0 -338
  288. package/src/primitives/url-scheme-policy.ts +0 -70
  289. package/src/primitives/use-audio-analysis.ts +0 -340
  290. package/src/primitives/use-auto-resize.ts +0 -31
  291. package/src/primitives/use-resize-observer.ts +0 -45
  292. package/src/primitives/use-sequencer.ts +0 -72
  293. package/src/primitives/use-speech-recognition.ts +0 -146
  294. package/src/primitives/use-stick-to-bottom.ts +0 -75
  295. package/src/primitives/use-text-stream.ts +0 -143
  296. package/src/primitives/use-voice-recorder.ts +0 -62
  297. package/src/primitives/visualizer-sequences.ts +0 -350
  298. package/src/remote/host-embed.ts +0 -345
  299. package/src/remote/index.ts +0 -2
  300. package/src/remote/origin.ts +0 -30
  301. package/src/remote/provider-runtime.ts +0 -262
  302. package/src/remote/provider.ts +0 -2
  303. package/src/remote/validate.ts +0 -22
  304. package/src/remote/version.ts +0 -12
  305. package/src/remote/wire.ts +0 -48
  306. package/src/schemas/from-tool-call.ts +0 -151
  307. package/src/schemas/index.ts +0 -341
  308. package/src/schemas/provider-subsets.ts +0 -538
  309. package/src/schemas/registry.ts +0 -355
  310. package/src/schemas/tool-defs.ts +0 -793
  311. package/src/solid.ts +0 -173
  312. package/src/state/index.ts +0 -44
  313. package/src/state/messages.ts +0 -53
  314. package/src/state/mock.ts +0 -297
  315. package/src/state/parts.ts +0 -295
  316. package/src/state/persistence.ts +0 -180
  317. package/src/state/stream.ts +0 -246
  318. package/src/state/suggestions.ts +0 -9
  319. package/src/state/threads.ts +0 -142
  320. package/src/test-utils/fake-clock.ts +0 -88
  321. package/src/types.ts +0 -95
  322. package/src/ui/action-icons.ts +0 -53
  323. package/src/ui/agent-card.tsx +0 -218
  324. package/src/ui/avatar.tsx +0 -23
  325. package/src/ui/badge.tsx +0 -21
  326. package/src/ui/button.tsx +0 -49
  327. package/src/ui/card.tsx +0 -245
  328. package/src/ui/checkbox-group.tsx +0 -153
  329. package/src/ui/checkbox.tsx +0 -63
  330. package/src/ui/collapsible.tsx +0 -163
  331. package/src/ui/command.tsx +0 -103
  332. package/src/ui/dialog.tsx +0 -223
  333. package/src/ui/dock.tsx +0 -630
  334. package/src/ui/dropdown.tsx +0 -636
  335. package/src/ui/editable-label.tsx +0 -112
  336. package/src/ui/hover-card.tsx +0 -345
  337. package/src/ui/icon.tsx +0 -167
  338. package/src/ui/input.tsx +0 -391
  339. package/src/ui/kbd.tsx +0 -118
  340. package/src/ui/nav.tsx +0 -282
  341. package/src/ui/notice.tsx +0 -84
  342. package/src/ui/overlay.tsx +0 -277
  343. package/src/ui/pane-grid.tsx +0 -117
  344. package/src/ui/pane-group.tsx +0 -291
  345. package/src/ui/pane.tsx +0 -193
  346. package/src/ui/popover.tsx +0 -106
  347. package/src/ui/progress-bar.tsx +0 -83
  348. package/src/ui/prompt-dock.tsx +0 -145
  349. package/src/ui/radio.tsx +0 -150
  350. package/src/ui/resizable.tsx +0 -693
  351. package/src/ui/scroll-area.tsx +0 -26
  352. package/src/ui/segmented.tsx +0 -109
  353. package/src/ui/select.tsx +0 -168
  354. package/src/ui/separator.tsx +0 -10
  355. package/src/ui/settings-group.tsx +0 -67
  356. package/src/ui/skeleton.tsx +0 -73
  357. package/src/ui/slider.tsx +0 -178
  358. package/src/ui/status.tsx +0 -48
  359. package/src/ui/switch.tsx +0 -119
  360. package/src/ui/tabs.tsx +0 -168
  361. package/src/ui/textarea.tsx +0 -21
  362. package/src/ui/tooltip.tsx +0 -118
  363. package/src/utils/cn.ts +0 -30
  364. package/src/wire/chunk.ts +0 -248
  365. package/src/wire/consume.ts +0 -599
  366. package/src/wire/diagnostics.ts +0 -727
  367. package/src/wire/encode-probe.ts +0 -214
  368. package/src/wire/encode.ts +0 -947
  369. package/src/wire/files.ts +0 -342
  370. package/src/wire/formats/anthropic.ts +0 -274
  371. package/src/wire/formats/openai.ts +0 -228
  372. package/src/wire/index.ts +0 -95
  373. package/src/wire/media-types.ts +0 -344
  374. package/src/wire/read.ts +0 -396
  375. package/src/wire/sink-helpers.ts +0 -50
  376. package/src/wire/sse.ts +0 -153
@@ -1,228 +0,0 @@
1
- // OpenAI chat-completions SSE. Also the shape every non-mock integration in the
2
- // kit's catalog re-frames to server-side, so this one decoder covers all nine.
3
- //
4
- // Input is an ALREADY DECODED JSON frame typed `unknown`. Nothing here imports a
5
- // provider SDK, and nothing here throws: a frame this format does not recognise
6
- // yields [] so a provider adding a field cannot take a turn down.
7
- import type { MessageSource } from '../../elements/chat-types';
8
- import type {
9
- ModelStreamChunk,
10
- ModelToolCallDelta,
11
- ModelUsage,
12
- WireFormat,
13
- WireFormatReader,
14
- } from '../chunk';
15
-
16
- function isRecord(v: unknown): v is Record<string, unknown> {
17
- return typeof v === 'object' && v !== null && !Array.isArray(v);
18
- }
19
-
20
- function str(v: unknown): string | undefined {
21
- return typeof v === 'string' ? v : undefined;
22
- }
23
-
24
- function num(v: unknown): number | undefined {
25
- return typeof v === 'number' ? v : undefined;
26
- }
27
-
28
- function usageOf(raw: unknown): ModelUsage | undefined {
29
- if (!isRecord(raw)) return undefined;
30
- const out: ModelUsage = {};
31
- const input = num(raw.prompt_tokens);
32
- if (input !== undefined) out.inputTokens = input;
33
- const output = num(raw.completion_tokens);
34
- if (output !== undefined) out.outputTokens = output;
35
- const total = num(raw.total_tokens);
36
- if (total !== undefined) out.totalTokens = total;
37
- const details = raw.completion_tokens_details;
38
- if (isRecord(details)) {
39
- // The one number that proves reasoning happened even when no reasoning text
40
- // was streamed back.
41
- const reasoning = num(details.reasoning_tokens);
42
- if (reasoning !== undefined) out.reasoningTokens = reasoning;
43
- }
44
- const promptDetails = raw.prompt_tokens_details;
45
- if (isRecord(promptDetails)) {
46
- const cached = num(promptDetails.cached_tokens);
47
- if (cached !== undefined) out.cachedInputTokens = cached;
48
- }
49
- const cost = num(raw.cost);
50
- if (cost !== undefined) out.costUsd = cost;
51
- return Object.keys(out).length > 0 ? out : undefined;
52
- }
53
-
54
- function toolCallDelta(raw: unknown, position: number): ModelToolCallDelta | undefined {
55
- if (!isRecord(raw)) return undefined;
56
- const fn = isRecord(raw.function) ? raw.function : undefined;
57
- const out: ModelToolCallDelta = { index: num(raw.index) ?? position };
58
- const id = str(raw.id);
59
- if (id) out.id = id;
60
- const name = str(fn?.name);
61
- if (name) out.name = name;
62
- const args = str(fn?.arguments);
63
- if (args !== undefined) out.arguments = args;
64
- return out;
65
- }
66
-
67
- /** Sum the READABLE text across reasoning_details entries. Entries with no
68
- * readable text (`reasoning.encrypted`, opaque signed blobs) contribute nothing
69
- * here; they still ride along whole in `reasoningRaw`. */
70
- function detailText(details: unknown[]): string {
71
- let out = '';
72
- for (const d of details) {
73
- if (!isRecord(d)) continue;
74
- const text = str(d.text) ?? str(d.summary);
75
- if (text) out += text;
76
- }
77
- return out;
78
- }
79
-
80
- function detailField(details: unknown[], key: 'index' | 'signature'): unknown {
81
- for (const d of details) {
82
- if (isRecord(d) && d[key] !== undefined) return d[key];
83
- }
84
- return undefined;
85
- }
86
-
87
- /** The sibling reasoning STRINGS, in precedence order. See `applyReasoning`. */
88
- const REASONING_KEYS = ['reasoning', 'reasoning_content'] as const;
89
-
90
- /** First non-empty sibling string wins. These are alternative SPELLINGS of the
91
- * same text, never two halves of it, so this coalesces and never concatenates. */
92
- function siblingReasoning(delta: Record<string, unknown>): string | undefined {
93
- for (const key of REASONING_KEYS) {
94
- const raw = str(delta[key]);
95
- if (raw !== undefined && raw !== '') return raw;
96
- }
97
- return undefined;
98
- }
99
-
100
- /**
101
- * FINDINGS: OpenRouter frequently puts the SAME text in `reasoning` AND in
102
- * `reasoning_details` on the same delta. Concatenating both doubles every
103
- * reasoning token, so `reasoning` wins and details are only a text FALLBACK.
104
- *
105
- * `reasoning_details` is still read in BOTH cases, for `reasoningRaw`, the block
106
- * index and the signature. It is the provider's own block list, and dropping it
107
- * is exactly the Anthropic 400 this entry exists to avoid.
108
- *
109
- * SIBLING NAMES. Chat-completions says nothing about reasoning, so every vendor
110
- * extended it on its own and each parser in the wild reads a different subset:
111
- * `reasoning` is what OpenRouter normalises to, `reasoning_content` is what
112
- * DeepSeek's own API emits and what LiteLLM forwards. Nobody is ahead here and
113
- * there is no canonical spelling. A stream that only ever went through
114
- * OpenRouter never shows the other name, which is the whole reason this one
115
- * stayed invisible: reading `reasoning` alone loses a DeepSeek-direct stream's
116
- * reasoning completely, and silently.
117
- *
118
- * PRECEDENCE. The siblings coalesce, first non-empty wins, and both outrank the
119
- * details text. `reasoning` is checked first, which is what keeps this additive:
120
- * any stream that already carried `reasoning` parses byte for byte as it did
121
- * before, so the order is only observable on a provider that disagrees with
122
- * itself. A gateway that aliases one spelling onto the other sends identical
123
- * text in both, and summing them would double every token exactly the way
124
- * summing `reasoning` and `reasoning_details` does.
125
- *
126
- * SCOPE, and it is NOT a closed class. This covers the NAMING axis only.
127
- * Reasoning on this wire is fragmented three ways STRUCTURALLY: a sibling string
128
- * (here), a block array (`reasoning_details`), and reasoning carried inside a
129
- * polymorphic `content` array. Aliasing bridges names, not shapes. A fourth
130
- * spelling is one entry in REASONING_KEYS; a fourth SHAPE is not, and needs its
131
- * own branch plus its own fixture.
132
- */
133
- function applyReasoning(delta: Record<string, unknown>, out: ModelStreamChunk): void {
134
- const details = Array.isArray(delta.reasoning_details) ? delta.reasoning_details : undefined;
135
- const primary = siblingReasoning(delta);
136
- const fallback = details ? detailText(details) : undefined;
137
- const hasDetails = details !== undefined && details.length > 0;
138
- const text = primary ?? fallback;
139
- if (text === undefined && !hasDetails) return;
140
-
141
- // '' is deliberate and meaningful: an encrypted-only detail entry has no
142
- // readable text but must still produce a reasoning part carrying its payload.
143
- out.reasoning = text ?? '';
144
- if (!hasDetails) return;
145
-
146
- out.reasoningRaw = { source: 'openai.reasoning_details', payload: details };
147
- const index = num(detailField(details, 'index'));
148
- if (index !== undefined) out.reasoningIndex = index;
149
- const signature = str(detailField(details, 'signature'));
150
- if (signature !== undefined) out.reasoningSignature = signature;
151
- }
152
-
153
- function sourcesOf(delta: Record<string, unknown>): MessageSource[] {
154
- if (!Array.isArray(delta.annotations)) return [];
155
- const out: MessageSource[] = [];
156
- for (const a of delta.annotations) {
157
- if (!isRecord(a)) continue;
158
- const citation = isRecord(a.url_citation) ? a.url_citation : undefined;
159
- const url = str(citation?.url);
160
- if (!url) continue;
161
- const source: MessageSource = { url };
162
- const title = str(citation?.title);
163
- if (title) source.title = title;
164
- const snippet = str(citation?.content);
165
- if (snippet) source.snippet = snippet;
166
- out.push(source);
167
- }
168
- return out;
169
- }
170
-
171
- function pushOpenAI(frame: unknown): ModelStreamChunk[] {
172
- if (!isRecord(frame)) return [];
173
- const out: ModelStreamChunk = {};
174
-
175
- const err = frame.error;
176
- if (isRecord(err)) {
177
- const message = str(err.message);
178
- if (message) {
179
- out.error = { message };
180
- const code = err.code;
181
- if (typeof code === 'string' || typeof code === 'number') out.error.code = code;
182
- }
183
- }
184
-
185
- // Stated per frame and reported per frame, no dedupe: a stream that changes
186
- // its mind mid-turn is a finding, and collapsing it would hide one.
187
- const model = str(frame.model);
188
- if (model) out.model = model;
189
-
190
- const usage = usageOf(frame.usage);
191
- if (usage) out.usage = usage;
192
-
193
- const choices = Array.isArray(frame.choices) ? frame.choices : [];
194
- const choice = isRecord(choices[0]) ? choices[0] : undefined;
195
- if (choice) {
196
- const delta = isRecord(choice.delta) ? choice.delta : undefined;
197
- if (delta) {
198
- const content = str(delta.content);
199
- if (content) out.text = content;
200
-
201
- applyReasoning(delta, out);
202
-
203
- if (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0) {
204
- const calls = delta.tool_calls
205
- .map((raw, i) => toolCallDelta(raw, i))
206
- .filter((c): c is ModelToolCallDelta => c !== undefined);
207
- if (calls.length > 0) out.toolCalls = calls;
208
- }
209
-
210
- const sources = sourcesOf(delta);
211
- if (sources.length > 0) out.sources = sources;
212
- }
213
- const finish = str(choice.finish_reason);
214
- if (finish) out.finishReason = finish;
215
- }
216
-
217
- return Object.keys(out).length > 0 ? [out] : [];
218
- }
219
-
220
- /** Stateless: every frame is self-describing, so `open()` returns a reader with
221
- * no closure state. The interface still calls it per stream, which is what lets
222
- * the stateful Anthropic format use the same seam. */
223
- export const openaiChatFormat: WireFormat = {
224
- id: 'openai.chat-completions',
225
- open(): WireFormatReader {
226
- return { push: pushOpenAI };
227
- },
228
- };
package/src/wire/index.ts DELETED
@@ -1,95 +0,0 @@
1
- // @kitn.ai/ui/wire: the model-stream adapter.
2
- //
3
- // Separate from ./state on purpose. `state` is I/O-free pure functions over
4
- // ChatMessage[]; `wire` touches Response, TextDecoder and byte streams. Keeping
5
- // them apart leaves the bring-your-own-transport consumer at zero cost, and
6
- // gives a future AG-UI format an obvious home. The cost is that importing both
7
- // entries ships state/parts.ts twice, about 2 KB.
8
- //
9
- // The kit PARSES. The consumer FETCHES. There is no client, no key handling and
10
- // no provider SDK anywhere below this file.
11
-
12
- export { readModelStream, readOpenAIStream, readAnthropicStream, WireError } from './read';
13
- export type { StreamSource, ReadOptions } from './read';
14
-
15
- export { consumeModelStream, createToolCallAccumulator } from './consume';
16
- export { applyToolOutput, applyToolFailure, bufferText } from './sink-helpers';
17
-
18
- export { toOpenAIMessages, toAnthropicMessages, WireEncodeError } from './encode';
19
- export type {
20
- AnthropicContentBlock,
21
- AnthropicEncodeOptions,
22
- AnthropicWireMessage,
23
- FileEncodeOptions,
24
- OpenAIContentPart,
25
- OpenAIEncodeOptions,
26
- OpenAIReasoningDetail,
27
- OpenAIToolCall,
28
- OpenAIWireMessage,
29
- UnencodableFilePolicy,
30
- } from './encode';
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
-
49
- export { openaiChatFormat } from './formats/openai';
50
- export { anthropicMessagesFormat } from './formats/anthropic';
51
-
52
- export { sseDataFrames, sseJson, readableToAsyncIterable } from './sse';
53
- export type { ByteSource } from './sse';
54
-
55
- // The diagnostic event stream: metadata about what the pipeline SAW.
56
- //
57
- // Only the subscription and the types are public. `emitWireDiagnostic`,
58
- // `wireDiagnosticsActive` and `nextStreamId` stay internal on purpose -- they
59
- // are the producer side, and a consumer that can forge events or reset the
60
- // stream counter can make a panel lie about a stream that never happened.
61
- export { subscribeWireDiagnostics } from './diagnostics';
62
- export type {
63
- AppRequestEvent,
64
- EncodeAttachmentReport,
65
- EncodeDroppedEvent,
66
- EncodeRequestEvent,
67
- WireCloseEvent,
68
- WireDiagnosticBase,
69
- WireDiagnosticEvent,
70
- WireFailedEvent,
71
- WireFrameEvent,
72
- WireInterruptedEvent,
73
- WireOpenEvent,
74
- WirePartEvent,
75
- } from './diagnostics';
76
-
77
- export { normalizeStopReason } from './chunk';
78
- export type {
79
- AssistantStreamSink,
80
- ConsumeOptions,
81
- ModelStreamChunk,
82
- ModelToolCall,
83
- ModelToolCallDelta,
84
- ModelTurn,
85
- ModelUsage,
86
- StopReason,
87
- WireFormat,
88
- WireFormatReader,
89
- } from './chunk';
90
-
91
- // The content-model types every signature above mentions, re-exported so a
92
- // consumer importing only from '@kitn.ai/ui/wire' can annotate the values these
93
- // functions take and return without a second import.
94
- export type { ChatMessage, MessagePart, MessageSource, RawOrigin } from '../elements/chat-types';
95
- export type { ToolPart } from '../components/tool-types';
@@ -1,344 +0,0 @@
1
- // THE media-type declaration. One table, read by every layer that has an
2
- // opinion about attachments.
3
- //
4
- // WHY THIS FILE EXISTS AT ALL. Two features want the same knowledge: the
5
- // composer needs to know what a user is allowed to stage, and the encoders need
6
- // to know what they can turn into provider content. Those are the same set. When
7
- // each grows its own copy they drift, and the drift is not theoretical -- it is
8
- // the exact defect #186 fixed one layer down, where a composer happily staged a
9
- // `blob:` URL that the encoder could not represent. So the set is declared ONCE,
10
- // here, and both layers derive from it rather than restating it.
11
- //
12
- // THE BOUNDARY THIS DRAWS. The kit owns "what CAN be encoded" -- a fact about
13
- // `encode.ts`, not a policy. The developer owns "what I want to ALLOW", and can
14
- // only ever NARROW the kit's set, never widen it: widening would just move a
15
- // provider 400 later in the request. That asymmetry is why the filter is a
16
- // declarative list rather than a hook, and why `encodableMediaTypes()` is public.
17
- //
18
- // No I/O, like the rest of `wire/`. Pattern matching and, for text, decoding
19
- // base64 that is already inline -- computation over bytes already in hand, never
20
- // a fetch.
21
-
22
- /** What a media type BECOMES on the wire. Named for the Anthropic block set,
23
- * which is the narrower of the two: `text` / `image` / `document` and nothing
24
- * else. There is no arbitrary-file block on either API, which is why a text
25
- * file has to ride as text content and a `.zip` has no representation at all. */
26
- export type EncodableKind = 'image' | 'document' | 'text';
27
-
28
- // WHAT A BROWSER ACTUALLY CALLS A FILE -- ONE OBSERVATION, NOT A STANDING FACT.
29
- //
30
- // Observed 2026-08-13 (UTC), Chrome 151.0.7922.137, macOS 26.5 arm64, with the
31
- // files handed to a real `<input type="file">` so the browser built every `File`
32
- // itself. Re-run it rather than trusting it:
33
- // node scripts/probe-file-media-types.mjs
34
- //
35
- // .txt text/plain .js text/javascript .png image/png
36
- // .md text/markdown .css text/css .zip application/zip
37
- // .csv text/csv .html text/html .pdf application/pdf
38
- // .json application/json .sh text/x-sh
39
- // .xml text/xml .py text/x-python-script
40
- // .yaml application/x-yaml .yml application/x-yaml
41
- // .ts .tsx .rs .go .sql .toml .log -> "" (the browser could not name them)
42
- //
43
- // THESE VALUES ARE PLATFORM-SPECIFIC. Nineteen of the twenty-two match macOS's
44
- // own `preferredMIMEType`, so the browser is answering out of the OS type
45
- // database and Linux and Windows will answer differently. Nothing returned
46
- // `application/octet-stream` -- Chrome's failure mode here is the EMPTY STRING.
47
- // (`.ts` is not a TypeScript file to macOS: it resolves to an MPEG-2
48
- // transport-stream UTI, which carries no MIME string at all, which is why it
49
- // comes back empty rather than as `video/mp2t`.)
50
- //
51
- // EXACTLY TWO THINGS BELOW REST ON THIS TABLE, and both survive it being wrong
52
- // somewhere else:
53
- // 1. `application/x-yaml` is a row because it was measured here. A platform
54
- // that says something else does not make this row wrong, only incomplete.
55
- // 2. Seven of the twenty-two extensions are the ones a developer is likeliest
56
- // to attach to a coding chat, and the browser names NONE of them. That is
57
- // why an unnamed file is decided by DECODING its bytes and never by reading
58
- // its filename -- see `decide()`. Another OS moves WHICH rows come back
59
- // empty; it cannot move the fact that some do.
60
- // Anything else read off this table is being read too hard.
61
-
62
- /**
63
- * ★ THE DECLARATION. Everything else in this file, and both `accept` surfaces,
64
- * are derived from this array. Adding a row teaches the encoder AND widens the
65
- * composer's picker; deleting one narrows both. If you find yourself writing a
66
- * second list of media types anywhere in this repo, delete it and read this.
67
- *
68
- * Patterns are HTML `accept` syntax: an exact type (`image/png`) or a subtype
69
- * wildcard (`text/*`). File EXTENSIONS (`.md`) are deliberately not supported --
70
- * they map to media types only by convention, and guessing is how a `.md` full
71
- * of base64 becomes a 400. An extension in a developer's `accept` THROWS rather
72
- * than matching nothing; a file the browser could not name is settled by
73
- * decoding it. Neither path ever consults a filename.
74
- *
75
- * On the image list: JPEG, PNG, GIF and WebP are the four formats BOTH APIs
76
- * document. SVG and BMP are the ones people actually try, and both are a 400 at
77
- * request time, so they are absent on purpose.
78
- *
79
- * On the text list: `text/*` is the core, because a media type in that tree is
80
- * text by definition of the tree. The `application/` rows are the judgement
81
- * calls -- textual formats that IANA files outside `text/` for historical
82
- * reasons, and that people genuinely attach. `application/json` and
83
- * `application/xml` are the two obvious ones.
84
- *
85
- * On YAML, since it is two rows for one format: `application/x-yaml` is what
86
- * Chrome hands back for a `.yaml` on macOS, MEASURED in the table above.
87
- * `application/yaml` is the IANA-registered type (RFC 9512, 2024) and is what a
88
- * correctly configured server sends, so it is here on REGISTRY grounds rather
89
- * than on a measurement. `text/yaml` and `text/x-yaml` are in the wild too and
90
- * need no row of their own: `text/*` already covers both.
91
- */
92
- const ENCODABLE: readonly { readonly pattern: string; readonly kind: EncodableKind }[] = [
93
- { pattern: 'image/jpeg', kind: 'image' },
94
- { pattern: 'image/png', kind: 'image' },
95
- { pattern: 'image/gif', kind: 'image' },
96
- { pattern: 'image/webp', kind: 'image' },
97
- { pattern: 'application/pdf', kind: 'document' },
98
- { pattern: 'text/*', kind: 'text' },
99
- { pattern: 'application/json', kind: 'text' },
100
- { pattern: 'application/xml', kind: 'text' },
101
- { pattern: 'application/x-yaml', kind: 'text' },
102
- { pattern: 'application/yaml', kind: 'text' },
103
- ];
104
-
105
- /**
106
- * The media type an unnamed file gets once its BYTES have proven to be UTF-8
107
- * text, and the type whose presence in the effective policy is what permits that
108
- * proof to be attempted at all.
109
- *
110
- * `text/plain` is not a guess dressed up as a fact. It is the media type for
111
- * "text, no further structure claimed", which is exactly what a successful
112
- * decode establishes and the most any decode could ever establish. A `.rs` file
113
- * arriving as `text/plain` is true; arriving as `text/x-rust` would be the
114
- * filename talking.
115
- */
116
- export const UNNAMED_TEXT_MEDIA_TYPE = 'text/plain';
117
-
118
- /**
119
- * Media types that say nothing about the bytes they label.
120
- *
121
- * The empty string is what `File.type` is when the OS type database had no
122
- * answer (seven of the twenty-two rows above). `application/octet-stream` is the
123
- * same non-answer written down: RFC 2046 defines it as "arbitrary binary data",
124
- * and it is what `FileReader.readAsDataURL` substitutes for an empty type --
125
- * measured in Chrome 151 AND in the jsdom the unit suite runs on, both of which
126
- * produce `data:application/octet-stream;base64,...` for a `File` whose `type`
127
- * is `''`. So the same typeless file reaches the composer as `''` and reaches
128
- * the encoder as `application/octet-stream`, and both spellings have to mean the
129
- * same thing or the two layers disagree about one file.
130
- */
131
- const NAMES_NOTHING: readonly string[] = ['', 'application/octet-stream'];
132
-
133
- /** A developer's narrowing filter. A comma-separated string (so it works as an
134
- * HTML attribute under the `kai-` contract) or an array (so it composes in JS).
135
- * Both spell the same thing; the string form is what `<kai-chat accept="...">`
136
- * takes and what the `kai` MCP scaffolder can emit as data. */
137
- export type MediaTypeFilter = string | readonly string[];
138
-
139
- /** One media type, decided against the effective set.
140
- *
141
- * `unsupported` and `filtered` are split because the developer needs different
142
- * things from them: `unsupported` means fix your expectations (no API takes
143
- * this), `filtered` means fix your `accept` (the kit could have sent it). */
144
- export type MediaDecision =
145
- | { status: 'allowed'; kind: EncodableKind }
146
- | { status: 'unsupported' }
147
- | { status: 'filtered' }
148
- /**
149
- * Nobody named this file, so its media type cannot decide it. LOOK AT THE
150
- * BYTES: decode them as UTF-8 (`classifyAttachment` does), and it is `text`
151
- * if they decode and unencodable if they do not. Do not look at the filename.
152
- *
153
- * ★ THIS VARIANT IS WHERE THE DEVELOPER'S FILTER IS ENFORCED, so the caller
154
- * cannot get it wrong by forgetting to check. It is returned ONLY when the
155
- * effective policy still admits `text/plain` -- the one thing a decode can
156
- * establish. Narrow `accept` to `image/png` and an unnamed file comes back
157
- * `unsupported` instead, so the decode path is unreachable and a typeless
158
- * `.rs` cannot slip past an images-only filter. That is the whole reason the
159
- * check lives here rather than at each call site: there is one place to get
160
- * right, and the callers physically cannot reach the bytes without being told
161
- * to.
162
- */
163
- | { status: 'undetermined' };
164
-
165
- export interface MediaPolicy {
166
- /** The effective patterns: the kit's capability set narrowed by the
167
- * developer's filter. Never wider than `encodableMediaTypes()`. */
168
- readonly types: readonly string[];
169
- /** The same set as an HTML `accept` attribute value, ready to put on an
170
- * `<input type="file">`. This is what makes the picker and the encoder
171
- * provably one thing rather than two lists that agree today. */
172
- readonly accept: string;
173
- /**
174
- * What this policy makes of one media type, including "I cannot tell from
175
- * this, go and read the bytes" -- see `undetermined`.
176
- *
177
- * The primitive behind "expose information, do not make decisions": it answers
178
- * a question and returns a fact, so a consumer can build their own picker,
179
- * their own validation and their own error copy on top of it without the kit
180
- * deciding anything on their behalf.
181
- */
182
- decide(mediaType: string | undefined): MediaDecision;
183
- }
184
-
185
- export interface MediaPolicyOptions {
186
- /** Narrow the kit's capability set. Omitted means the full set. Anything here
187
- * that the kit cannot encode is dropped rather than honoured, because
188
- * allowing it would only relocate the failure to the provider. */
189
- accept?: MediaTypeFilter;
190
- }
191
-
192
- // NOTE ON WHAT IS DELIBERATELY ABSENT: there is no size cap, no file count, and
193
- // no truncation path here, and their absence is a decision rather than an
194
- // omission. How many bytes to send is a cost decision that lands on the
195
- // consumer's invoice, and a component library that grows a limits engine starts
196
- // making application choices on their behalf. The kit encodes what it is given.
197
- // A developer who wants a ceiling has `File.size` and `decide()` and can enforce
198
- // one in three lines of their own code, where it belongs.
199
-
200
- const normalize = (value: string): string => value.trim().toLowerCase();
201
-
202
- /** Does this media type say anything at all about the bytes it labels? Exported
203
- * so `files.ts` decides "nameless" by the same rule `decide()` does, rather
204
- * than growing a second opinion about which spellings mean "I don't know". */
205
- export const namesNothing = (mediaType: string | undefined): boolean =>
206
- NAMES_NOTHING.includes(mediaType === undefined ? '' : normalize(mediaType));
207
-
208
- /**
209
- * A file EXTENSION in `accept` is a hard error, and the loudness is the point.
210
- *
211
- * HTML's own `accept` takes extensions, the prop is called `accept`, and it is
212
- * documented as HTML `accept` syntax -- so `accept=".py"` is a reasonable thing
213
- * for a developer to write, and it is a trap. Measured on this branch before the
214
- * throw existed: `.py` alone resolved to zero effective types and a picker with
215
- * `accept=""`, a composer that silently accepted NOTHING. `".py,text/plain"` was
216
- * worse, because the half that worked made it read as correct while the `.py`
217
- * vanished without a word.
218
- *
219
- * A silent empty set is the one outcome nobody can debug, so this throws. It is
220
- * safe to throw: no released version of this kit ships the prop, so no existing
221
- * app can be relying on the old silence.
222
- */
223
- const assertMediaType = (entry: string): string => {
224
- if (!entry.startsWith('.')) return entry;
225
- throw new Error(
226
- `\`accept\` entry "${entry}" is a file extension, and this kit takes media types only. ` +
227
- `HTML's own accept attribute does take extensions, which is exactly why this throws rather ` +
228
- `than quietly matching nothing. Use a media type instead -- "text/*" covers source files, ` +
229
- `"image/*" images, "application/pdf" PDFs, and \`encodableMediaTypes()\` is the full set. ` +
230
- `You lose nothing by dropping the extension: a file the browser cannot name is decided by ` +
231
- `decoding its bytes, which is stronger than matching its name.`,
232
- );
233
- };
234
-
235
- const splitFilter = (filter: MediaTypeFilter): string[] =>
236
- (typeof filter === 'string' ? filter.split(',') : filter)
237
- .map(normalize)
238
- .filter((v) => v !== '')
239
- .map(assertMediaType);
240
-
241
- /** `image/png` -> `['image','png']`; `text/*` -> `['text','*']`. */
242
- const parts = (pattern: string): [string, string] => {
243
- const slash = pattern.indexOf('/');
244
- return slash === -1 ? [pattern, ''] : [pattern.slice(0, slash), pattern.slice(slash + 1)];
245
- };
246
-
247
- const matches = (pattern: string, mediaType: string): boolean => {
248
- const [pType, pSub] = parts(pattern);
249
- const [mType, mSub] = parts(mediaType);
250
- if (pType !== '*' && pType !== mType) return false;
251
- return pSub === '*' || pSub === mSub;
252
- };
253
-
254
- /**
255
- * The narrower of two patterns, or `undefined` when they do not overlap.
256
- *
257
- * This is what "narrow, never widen" means mechanically. `accept: 'image/*'`
258
- * against a capability of `image/png` yields `image/png`, not `image/*`: the
259
- * developer asked for images, and the kit's answer is the images it can actually
260
- * send. Conversely `accept: 'text/markdown'` against a capability of `text/*`
261
- * yields `text/markdown`, because there the developer is the narrower one.
262
- */
263
- const intersect = (capability: string, filter: string): string | undefined => {
264
- if (matches(capability, filter)) return filter;
265
- if (matches(filter, capability)) return capability;
266
- return undefined;
267
- };
268
-
269
- /**
270
- * Every media type this kit's encoders can represent, as HTML `accept` patterns.
271
- *
272
- * PUBLIC ON PURPOSE, and not merely for introspection: a consumer who wants
273
- * their own picker, their own validation and their own error copy can build all
274
- * three against this without touching `<kai-chat>` at all. That demotes the
275
- * `accept` prop from "the only door" to a convenience over a published fact,
276
- * which is the composition-first shape this project already committed to.
277
- */
278
- export function encodableMediaTypes(): readonly string[] {
279
- return ENCODABLE.map((entry) => entry.pattern);
280
- }
281
-
282
- /**
283
- * The effective policy: the kit's capability set narrowed by the developer's
284
- * filter, plus the text ceiling.
285
- *
286
- * BOTH LAYERS CALL THIS. The composer calls it to build its picker `accept`
287
- * attribute and to filter what actually gets staged; the encoders call it to
288
- * decide what reaches the wire. That shared call is the whole design -- it is
289
- * why changing `ENCODABLE` above moves the picker and the encoder together
290
- * rather than moving one and leaving the other to be noticed in production.
291
- */
292
- export function resolveMediaPolicy(options: MediaPolicyOptions = {}): MediaPolicy {
293
- const filter = options.accept === undefined ? undefined : splitFilter(options.accept);
294
-
295
- const effective: { pattern: string; kind: EncodableKind }[] = [];
296
- for (const capability of ENCODABLE) {
297
- if (filter === undefined) {
298
- effective.push({ ...capability });
299
- continue;
300
- }
301
- for (const wanted of filter) {
302
- const overlap = intersect(capability.pattern, wanted);
303
- // A capability can overlap several filter entries (`text/*` against
304
- // `text/plain,text/csv`), and each overlap is its own effective pattern.
305
- if (overlap !== undefined && !effective.some((e) => e.pattern === overlap)) {
306
- effective.push({ pattern: overlap, kind: capability.kind });
307
- }
308
- }
309
- }
310
-
311
- return {
312
- types: effective.map((e) => e.pattern),
313
- accept: effective.map((e) => e.pattern).join(','),
314
- decide(mediaType) {
315
- const type = mediaType === undefined ? '' : normalize(mediaType);
316
-
317
- if (namesNothing(type)) {
318
- // Nothing here names the bytes, and the filename is not evidence, so
319
- // the answer is in the bytes themselves -- but only if the developer's
320
- // filter would still take what a decode can prove. See the
321
- // `undetermined` variant: this is the single place that gate exists.
322
- return effective.some((e) => matches(e.pattern, UNNAMED_TEXT_MEDIA_TYPE))
323
- ? { status: 'undetermined' }
324
- : // `unsupported` rather than `filtered`, deliberately: `filtered`
325
- // claims the kit COULD have encoded this file, and nobody has
326
- // established what it is. Unread and unnamed, it is not a text
327
- // file the filter excluded -- it is a file with no known type.
328
- { status: 'unsupported' };
329
- }
330
-
331
- const hit = effective.find((e) => matches(e.pattern, type));
332
- if (hit) return { status: 'allowed', kind: hit.kind };
333
- // Distinguishing these two is the point: one is the kit's limit, the
334
- // other is the developer's own filter looking back at them.
335
- return ENCODABLE.some((e) => matches(e.pattern, type))
336
- ? { status: 'filtered' }
337
- : { status: 'unsupported' };
338
- },
339
- };
340
- }
341
-
342
- /** The policy every caller gets when nobody narrowed anything: the kit's full
343
- * capability set. Built once -- it has no inputs. */
344
- export const DEFAULT_MEDIA_POLICY: MediaPolicy = resolveMediaPolicy();