@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,980 +0,0 @@
1
- import { z } from 'zod';
2
- import {
3
- KAI_TOOL_PREFIX,
4
- cardSchemaNames,
5
- cardSchemas,
6
- cardTools,
7
- isCardSchemaName,
8
- toolNameForCardType,
9
- } from '@kitn.ai/ui/schemas';
10
- import type { CardSchemaName, ToolProvider } from '@kitn.ai/ui/schemas';
11
- import type { Tool } from './types';
12
- import {
13
- cardHostTags,
14
- cardTagForType,
15
- cardTypeForTag,
16
- entryForTag,
17
- getElement,
18
- listElements,
19
- optInEntryForTag,
20
- } from '../manifest';
21
- import type { CemMember } from '../manifest';
22
- // The RAW literals, not listInvariants() / listSurfaceRecipes(). Those re-run a
23
- // zod parse over the whole catalog, and this is a per-lookup serving path called
24
- // once for every element a harness asks about. The parse is not skipped, only
25
- // moved: invariants.test.ts and surfaces.test.ts run it over these same literals
26
- // in CI, so a malformed record fails a test rather than a request.
27
- import { invariants } from '../../catalog/invariants';
28
- import { surfaceRecipes } from '../../catalog/surfaces';
29
- import type { TInvariant, TInvariantExample, TSurfaceRecipe } from '../../catalog/catalog-types';
30
- import { listCodeRecipes, getCodeRecipe } from '../../recipes';
31
- import type { CodeRecipe } from '../../recipes';
32
- import { readFileSync, existsSync } from 'node:fs';
33
- import { dirname, join } from 'node:path';
34
- import { resolveManifestPath } from '../manifest';
35
-
36
- /**
37
- * component_reference — look up AI/UI (kai-*) web components, their props,
38
- * events, attributes, and CSS custom properties from the live Custom Elements
39
- * Manifest (dist/custom-elements.json).
40
- *
41
- * Backed by manifest.ts which resolves the CEM in both the bundled bin
42
- * (dist/mcp.es.js sibling) and the Vitest source context (walks up to find
43
- * dist/custom-elements.json in the repo root).
44
- *
45
- * For a card-backed element it also serves the card's JSON Schema and a tool
46
- * definition GENERATED by `cardTools()`. See `renderCardContract` for why not one
47
- * byte of that is written here as a literal.
48
- */
49
-
50
- /** Types that warrant the JS-property contract note. */
51
- const JS_ONLY_TYPE_PATTERNS = /\[\]|\{|Record</;
52
-
53
- function isJsOnlyType(typeText: string | undefined): boolean {
54
- return typeText ? JS_ONLY_TYPE_PATTERNS.test(typeText) : false;
55
- }
56
-
57
- // ─────────────────────────────────────────────────────────────────────────────
58
- // The imperative half of the interaction surface
59
- // ─────────────────────────────────────────────────────────────────────────────
60
-
61
- /**
62
- * A CEM member of `kind: 'method'`.
63
- *
64
- * `CemMember` in manifest.ts types what EVERY member carries. `parameters` and
65
- * `return` exist only on methods, and gen-element-api.mjs writes the AUTHORED
66
- * parameter list into a single `parameters[0].name` rather than splitting it, so
67
- * the signature is rebuilt from those two fields rather than read off one.
68
- */
69
- interface CemMethodMember extends CemMember {
70
- kind: 'method';
71
- parameters?: { name: string }[];
72
- return?: { type?: { text?: string } };
73
- }
74
-
75
- function isPublicMethod(member: CemMember): member is CemMethodMember {
76
- return member.kind === 'method' && member.privacy === 'public';
77
- }
78
-
79
- /** `maximize(index: number): void` — the call as a consumer types it. */
80
- function methodSignature(method: CemMethodMember): string {
81
- return `${method.name}(${method.parameters?.[0]?.name ?? ''}): ${method.return?.type?.text ?? 'void'}`;
82
- }
83
-
84
- // ─────────────────────────────────────────────────────────────────────────────
85
- // The card contract
86
- // ─────────────────────────────────────────────────────────────────────────────
87
-
88
- const PROVIDERS = ['openai', 'anthropic', 'jsonschema'] as const;
89
-
90
- /**
91
- * What a caller gets when they do not name a provider.
92
- *
93
- * NOT a coin-flip between the two wire formats, and deliberately not "serve all
94
- * three". The three envelopes are not interchangeable in a way a reader can patch
95
- * up: OpenAI nests the schema at `function.parameters`, Anthropic puts it flat at
96
- * `input_schema`. Handing over one of those unasked means a harness on the other
97
- * provider pastes it in and gets a 400 it will blame on the kit.
98
- *
99
- * `jsonschema` is the only one of the three that is not a wire envelope at all —
100
- * `{ name, description, schema }` cannot be mistaken for either provider's shape, so
101
- * a caller who reads it as one is corrected by the field names before they ship it.
102
- * It is also directly usable, not a booby prize: `tool({ inputSchema: jsonSchema(
103
- * def.schema) })` is exactly what the Vercel AI SDK wants, and of the nine
104
- * integrations this MCP scaffolds, exactly one (openrouter) posts a raw OpenAI-wire
105
- * tools array; the rest go through an SDK or an agent framework that takes the bare
106
- * schema.
107
- *
108
- * Serving all three was the other candidate and it loses on cost for no gain in
109
- * safety: measured on this tree, one card's pretty-printed definition runs
110
- * 1.7-5.3 KB, so three of them plus the schema turns a single `kai-artifact` lookup
111
- * into roughly 20 KB of context. The response names all three modes and the exact
112
- * call that produces each, so a caller who does know their provider is one re-ask
113
- * away and still never invents an envelope.
114
- */
115
- const DEFAULT_PROVIDER: ToolProvider = 'jsonschema';
116
-
117
- function isProvider(value: string): value is ToolProvider {
118
- return (PROVIDERS as readonly string[]).includes(value);
119
- }
120
-
121
- /** Stable fence info strings, so a reader (and reference.test.ts) can lift the blocks back out exactly. */
122
- const TOOL_FENCE = 'kai-tool-definition';
123
- const SCHEMA_FENCE = 'kai-card-schema';
124
-
125
- function fence(info: string, value: unknown): string[] {
126
- return ['```json ' + info, JSON.stringify(value, null, 2), '```'];
127
- }
128
-
129
- /**
130
- * Render a card type as it would be written in JS source.
131
- *
132
- * The seven built-ins are all bare identifiers, but a card type is whatever string a
133
- * developer put in `CardEnvelope.type` — `pricing-table` is legal and reaches this
134
- * code the day an eighth built-in is hyphenated. Emitting `{ pricing-table: … }` as a
135
- * copy-paste sample would be a syntax error, so the quoting is decided, not assumed.
136
- */
137
- const BARE_IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
138
-
139
- function asKey(cardType: string): string {
140
- return BARE_IDENTIFIER.test(cardType) ? cardType : JSON.stringify(cardType);
141
- }
142
-
143
- function asMember(cardType: string): string {
144
- return BARE_IDENTIFIER.test(cardType) ? `.${cardType}` : `[${JSON.stringify(cardType)}]`;
145
- }
146
-
147
- /**
148
- * The loop, in the four facts a harness cannot infer from a prop table.
149
- *
150
- * Shared by the card-backed and the card-host sections because they are the same
151
- * contract seen from the two ends, and a second copy would be the drift this whole
152
- * task exists to remove, one level up from the tool definition.
153
- *
154
- * `example` is the card the caller actually asked about, so the illustration is about
155
- * their card rather than a fixed one they then have to translate.
156
- */
157
- function loopWiring(example: string): string[] {
158
- return [
159
- '',
160
- '#### Wiring the loop',
161
- `- **Tool names are \`${KAI_TOOL_PREFIX}<card type>\`.** \`${toolNameForCardType(example)}\` produces \`{ type: "${example}" }\`. ` +
162
- 'Use `isCardTool(name)` to split card calls from your own tools; never match the prefix by hand.',
163
- '- **`tool_call_id` becomes `CardEnvelope.id`, unchanged.** That is what makes a revision UPSERT: ' +
164
- 'a model that re-sends the same tool call id replaces the card in place instead of rendering a second copy of it. ' +
165
- 'Generating your own id breaks that silently.',
166
- '- **`cardFromToolCall(name, input, { id: call.id })`** turns the call into a renderable envelope, ' +
167
- 'or returns `null` when it is one of your own tools so the loop falls through to it. It never throws.',
168
- '- **Everything above is one import:** `import { cardTools, cardFromToolCall, isCardTool } from \'@kitn.ai/ui/schemas\'`. ' +
169
- `The raw documents also ship as JSON (\`@kitn.ai/ui/schemas/${example}.schema.json\`) for a Python or Go backend.`,
170
- ];
171
- }
172
-
173
- /** The card-backed section: schema + a GENERATED tool definition. */
174
- function renderCardContract(tag: string, cardType: CardSchemaName, provider: ToolProvider): string[] {
175
- const schema = cardSchemas[cardType];
176
-
177
- // GENERATED, NEVER RESTATED.
178
- //
179
- // This calls the same `cardTools` a consumer's route calls, on the same schema, so
180
- // the definition served here cannot diverge from the one they ship — not when a
181
- // schema gains a field, not when the projection changes what it strips. A literal
182
- // typed into this file would read identically today and rot on the first schema
183
- // change, which is exactly the hand-copy drift this tool exists to remove.
184
- // reference.test.ts asserts byte-equality against BOTH `cardTools({ [type]: schema },
185
- // …)` and the matching entry of the whole-registry `cardTools({ provider })`, so a
186
- // hand-edit here fails a test rather than shipping.
187
- const [def] = cardTools({ [cardType]: schema }, { provider });
188
-
189
- const lines: string[] = [
190
- '',
191
- '### Card contract',
192
- `\`<${tag}>\` renders \`CardEnvelope.type: "${cardType}"\`. A model does not emit an envelope; ` +
193
- `it calls the tool \`${toolNameForCardType(cardType)}\`, and the kit turns that call back into one.`,
194
- ];
195
-
196
- lines.push(
197
- '',
198
- `#### Tool definition — provider \`${provider}\``,
199
- `Generated by \`cardTools({ ${asKey(cardType)}: cardSchemas${asMember(cardType)} }, { provider: '${provider}' })\`. ` +
200
- 'Copy it; do not retype it, and do not hand-maintain a second copy — regenerate instead.',
201
- `Ask again with \`provider\` set to \`openai\`, \`anthropic\` or \`jsonschema\` for the other envelopes. ` +
202
- 'They are NOT interchangeable: OpenAI nests the schema at `function.parameters`, Anthropic puts it flat at `input_schema`.',
203
- ...fence(TOOL_FENCE, def),
204
- );
205
-
206
- lines.push(
207
- '',
208
- '#### Card data JSON Schema',
209
- `\`import { cardSchemas } from '@kitn.ai/ui/schemas'\` → \`cardSchemas${asMember(cardType)}\`. ` +
210
- `Raw document: \`@kitn.ai/ui/schemas/${cardType}.schema.json\`.`,
211
- 'This is the authored contract, and it is what `registry.validate()` checks arriving data against. ' +
212
- 'The tool definition above carries the same document with `$schema`, `$id` and `x-kai-*` stripped — ' +
213
- 'that projection is `cardTools`\' job, so do not put a raw schema on the wire yourself.',
214
- ...fence(SCHEMA_FENCE, schema),
215
- );
216
-
217
- lines.push(...loopWiring(cardType));
218
- return lines;
219
- }
220
-
221
- /** The card-host section: no schema of its own, but this is where the props live. */
222
- function renderCardHost(tag: string): string[] {
223
- const rows = cardSchemaNames.map((type) => {
224
- const cardTag = cardTagForType(type);
225
- return `- \`${type}\` → \`<${cardTag ?? '(no element)'}>\`, tool \`${toolNameForCardType(type)}\``;
226
- });
227
-
228
- return [
229
- '',
230
- '### Card contract',
231
- `\`<${tag}>\` renders generative-UI cards inside the thread. It is a HOST: it draws whatever ` +
232
- 'envelopes arrive, and carries the two props that say what a card may be.',
233
- '',
234
- '- **`cardTypes`** — `Record<envelope type, custom element tag>`. What DRAWS a card. ' +
235
- `Merged over the built-ins, so your entry overrides ours and the other ${cardSchemaNames.length - 1} still render.`,
236
- '- **`cardSchemas`** — `Record<envelope type, JSON Schema>`. What a VALID card looks like. ' +
237
- `This is the prop that carries YOUR OWN card schemas into the browser validator; the ${cardSchemaNames.length} built-ins are already known. ` +
238
- 'Both are objects, so both are JS properties (`el.cardSchemas = …`), never HTML attributes.',
239
- '',
240
- 'Write both once with `createCardRegistry({ use, custom })` from `@kitn.ai/ui/schemas` and thread it to both ends: ' +
241
- '`el.cardTypes = cards.tags` / `el.cardSchemas = cards.validationSchemas` on the client, `cardTools(cards, { provider })` on the route.',
242
- '',
243
- 'Built-in card types (ask `component_reference` for one of these tags to get its schema and tool definition):',
244
- ...rows,
245
- ...loopWiring(cardSchemaNames[0]),
246
- ];
247
- }
248
-
249
- // ─────────────────────────────────────────────────────────────────────────────
250
- // The composition catalog: invariants, and recipe membership
251
- // ─────────────────────────────────────────────────────────────────────────────
252
-
253
- /**
254
- * An invariant applies to a tag if it is unscoped by tag, or names it.
255
- *
256
- * `appliesTo.targets` deliberately does NOT filter here: `upgrade-race` is
257
- * scoped to script-tag DELIVERY, which is a fact about how the page loads and
258
- * not about which element is on it — every element can be dropped onto a CDN
259
- * page. So it is served everywhere and the scope is RENDERED (see `scopeOf`),
260
- * which is the difference between a conditional rule a reader can apply and an
261
- * unqualified one they will either over-apply or stop trusting.
262
- */
263
- function invariantsFor(tag: string): TInvariant[] {
264
- return invariants.filter((i) => !i.appliesTo.tags || i.appliesTo.tags.includes(tag));
265
- }
266
-
267
- /**
268
- * The recipe's nesting, as markup a reader can paste.
269
- *
270
- * `noteSep` is how much of the record to print: the appendix passes ' — ' and
271
- * gets the WHY, an element lookup passes '' and gets only the shape, on the same
272
- * split the wiring rows already use. A recipe with no `composition` prints
273
- * nothing at all rather than "flat" — the field being absent means nobody
274
- * decided, and a rendered "(none)" would turn a gap into a claim.
275
- */
276
- function compositionLines(r: TSurfaceRecipe, noteSep: string): string[] {
277
- if (!r.composition) return [];
278
- return [
279
- '- **Composition** — where the parts go (a slotted child, not a sibling):',
280
- ...r.composition.map(
281
- (c) =>
282
- ` - \`<${c.child} slot="${c.slot}">\` goes INSIDE \`<${c.parent}>\`` +
283
- (noteSep && c.note ? `${noteSep}${c.note}` : ''),
284
- ),
285
- ];
286
- }
287
-
288
- function recipesFor(tag: string): TSurfaceRecipe[] {
289
- return surfaceRecipes.filter((r) => r.ingredients.includes(tag));
290
- }
291
-
292
- /**
293
- * How the coverage of one invariant reads — never a bare status word, and never
294
- * silence.
295
- *
296
- * Three of the catalog's records are enforced by NOTHING and two more by half of
297
- * what they say. A section that printed every rule as an equal-looking bullet
298
- * would put back exactly the overstatement those records were rewritten to
299
- * remove, one layer further out where it is harder to see. The wording also says
300
- * whose code the guard reads: every path here is inside the KIT's own repo, and
301
- * none of them checks the consumer's.
302
- */
303
- function coverageOf(inv: TInvariant): string {
304
- const by = inv.enforcedBy;
305
- // `guard` is '' only for kind:'none', and that combination reaches neither the
306
- // 'enforced' nor the 'partial' branch below: invariants.test.ts ("open ⟺
307
- // enforcedBy.kind none") asserts the equivalence in BOTH directions, so a
308
- // kind:'none' record is always status:'open'. Named here because a reader of
309
- // this file alone cannot see what makes `enforced by ` with nothing after it
310
- // unreachable.
311
- const guard =
312
- by.kind === 'test'
313
- ? `the kit's own tests (${by.paths.join(', ')})`
314
- : by.kind === 'lint'
315
- ? `the kit's \`${by.script}\` script, in required CI`
316
- : by.kind === 'structural'
317
- ? `a structural guarantee in ${by.path}`
318
- : '';
319
-
320
- switch (inv.status) {
321
- case 'enforced':
322
- return `enforced by ${guard}`;
323
- case 'partial':
324
- return `PARTIALLY ENFORCED: one half is covered, by ${guard}; the statement says which half is not`;
325
- case 'open':
326
- return (
327
- 'NOT ENFORCED: nothing in the kit checks this' +
328
- (by.kind === 'none' && by.until ? `, until ${by.until}` : '') +
329
- '. A rule you apply, not a guarantee you will be warned about'
330
- );
331
- }
332
- }
333
-
334
- /** What narrows an invariant, when something does. `targets` is not `tags`. */
335
- function scopeOf(inv: TInvariant): string {
336
- const { tags, targets, parts } = inv.appliesTo;
337
- const bits: string[] = [];
338
- if (tags) bits.push(`only ${tags.join(', ')}`);
339
- if (targets) bits.push(`${targets.join(' / ')} delivery only`);
340
- if (parts) bits.push(`parts ${parts.join(', ')}`);
341
- return bits.join('; ');
342
- }
343
-
344
- /**
345
- * The wrong/right pairs, as one fenced block per invariant.
346
- *
347
- * A fence rather than inline code because a `right` form is allowed to span
348
- * lines (the URL guards do), and because the pairs are meant to be
349
- * pattern-matched and grepped verbatim — the self-audit checklist greps emitted
350
- * code for the `wrong` form and expects zero hits, which only works if what is
351
- * served here is the literal and not a paraphrase of it.
352
- */
353
- function exampleLines(examples: TInvariantExample[]): string[] {
354
- if (examples.length === 0) return [];
355
- const out: string[] = ['', '```js'];
356
- examples.forEach((ex, i) => {
357
- if (i > 0) out.push('');
358
- out.push('// WRONG', ex.wrong, '// RIGHT', ex.right);
359
- if (ex.note) out.push(...ex.note.split('\n').map((l) => `// ${l}`));
360
- });
361
- out.push('```');
362
- return out;
363
- }
364
-
365
- /**
366
- * The one sentence saying how much of this section is actually guarded.
367
- *
368
- * BOTH numerators are derived and both are needed. `open` alone was the first
369
- * form of this line, and it is the more quotable half: "3 of the 7 below are
370
- * enforced by NOTHING" compresses, in any reader summarising it, into "3
371
- * unenforced, 4 enforced" — and two of that four are `partial`, covered on one
372
- * half only. That is this repo's own compression-drops-the-qualifier failure,
373
- * sitting in the sentence written to prevent it. Naming both counts leaves the
374
- * compression nothing to invent.
375
- *
376
- * Returns '' when every applicable record is fully enforced, so the line can
377
- * never outlive the gap it describes and become a comfortable falsehood.
378
- *
379
- * Exported for reference.test.ts, which drives all four count combinations over
380
- * synthetic records — the real catalog can only instance one of them at a time.
381
- */
382
- export function coverageSummary(applicable: TInvariant[]): string {
383
- const open = applicable.filter((i) => i.status === 'open').length;
384
- const partial = applicable.filter((i) => i.status === 'partial').length;
385
- if (open === 0 && partial === 0) return '';
386
-
387
- const total = applicable.length;
388
- const half = (n: number) => `only half of what ${n === 1 ? 'it says' : 'they say'}`;
389
- const clauses: string[] = [];
390
-
391
- if (open > 0) {
392
- clauses.push(`${open} of the ${total} below ${open === 1 ? 'is' : 'are'} enforced by NOTHING at all`);
393
- }
394
- if (partial > 0) {
395
- clauses.push(
396
- open > 0
397
- ? `${partial} more by ${half(partial)}`
398
- : `${partial} of the ${total} below ${partial === 1 ? 'is' : 'are'} enforced by ${half(partial)}`,
399
- );
400
- }
401
- return ` ${clauses.join(', and ')}.`;
402
- }
403
-
404
- /**
405
- * `###` to match every other section heading in this file (see '### Props'),
406
- * `####` for the per-record blocks, matching the card contract's sub-headings.
407
- *
408
- * Each invariant gets its heading (id, scope, coverage) and its one-line
409
- * `statement` — enough to apply the rule and to know how much to trust it —
410
- * but NOT the diagnosis and wrong/right examples. Those are the bulk: measured
411
- * on this tree, the full body of the 7 catalog records runs ~13 KB, and every
412
- * unscoped record (5 of 7) applies to all 80 elements, so printing the full
413
- * body here paid that in full on every lookup. `renderInvariantAppendix`
414
- * carries the long form exactly once; this function points to it instead of
415
- * repeating it.
416
- */
417
- function catalogSectionLines(tag: string): string[] {
418
- const applicable = invariantsFor(tag);
419
- const lines: string[] = [];
420
-
421
- if (applicable.length > 0) {
422
- lines.push(
423
- '',
424
- '### Invariants',
425
- 'Rules that have already broken real consumers of this kit. Each block says what enforces ' +
426
- 'it — read that line rather than assuming CI catches a violation, because nothing here ' +
427
- 'reads YOUR code.' + coverageSummary(applicable) + ' Diagnosis and wrong/right examples for ' +
428
- 'every invariant are served once, not repeated per element — call component_reference with ' +
429
- '{ name: "invariants" }.',
430
- );
431
-
432
- for (const inv of applicable) {
433
- const scope = scopeOf(inv);
434
- lines.push('', `#### ${inv.id}${scope ? ` (${scope})` : ''} — ${coverageOf(inv)}`, inv.statement);
435
- }
436
- }
437
-
438
- // No recipe, no section. An element that is in none gets its universal
439
- // invariants and nothing else — a "part of: (none)" row would be a fabricated
440
- // membership claim dressed as an empty one.
441
- const recipes = recipesFor(tag);
442
- if (recipes.length > 0) {
443
- lines.push(
444
- '',
445
- '### Appears in surface recipes',
446
- 'Compositions this element is part of, each with the whole ingredient list and the host ' +
447
- 'wiring — nothing coordinates one element with another except your own code ' +
448
- '(host-coordinates). The WHY behind each wiring edge, and the caveats on the nesting ' +
449
- 'below, are served once rather than repeated per ingredient — call component_reference ' +
450
- 'with { name: "recipes" }.',
451
- );
452
- for (const r of recipes) {
453
- lines.push(
454
- '',
455
- `#### ${r.id} — ${r.intent}`,
456
- `- **Ingredients:** ${r.ingredients.map((t) => `\`<${t}>\``).join(', ')}`,
457
- `- **Delivery:** ${r.targets.join(', ')} · archetype: ${r.archetypes.join(', ')}`,
458
- `- **Backend:** your own endpoint, read with \`${r.backend.reader}\` from \`@kitn.ai/ui/wire\``,
459
- // WHERE the parts go. Printed per element and not only in the appendix,
460
- // because this is the one fact a builder cannot infer from anything else
461
- // on the page: the wiring rows say which event drives which property and
462
- // are silent about nesting, so an element lookup that omitted this left
463
- // the reader to guess between a child and a sibling. Short enough to
464
- // repeat; the `note` (the WHY) stays in the appendix with the wiring notes.
465
- ...compositionLines(r, ''),
466
- // Bare ids, deliberately. This row is a DEPENDENCY list, not a coverage
467
- // claim, and it can only ever be read a few lines below the full
468
- // `### Invariants` section in the same response, where each of these ids
469
- // already carries its own coverage line. Repeating the status here would
470
- // print it a second (and, with two recipes, a third) time per element for
471
- // no fact a reader does not already have on the page.
472
- `- **Invariants it leans on:** ${r.invariants.join(', ')}`,
473
- // Compact: the edge (event out of A, property into B) with no `note`.
474
- // The note is the WHY — often a full sentence, sometimes several — and
475
- // repeating it is the same shape of duplication task 4 removed from
476
- // invariants: this recipe's ingredients (kai-chat, kai-conversations,
477
- // kai-resizable, kai-artifact) all print this identical wiring list.
478
- // `renderRecipeAppendix` carries the notes exactly once.
479
- '- **Wiring** — event out of A, property into B, wired by the host:',
480
- );
481
- for (const w of r.wiring) {
482
- lines.push(` - \`<${w.from}>\` fires \`${w.event}\` → host sets \`${w.to}.${w.property}\``);
483
- }
484
- }
485
- }
486
-
487
- return lines;
488
- }
489
-
490
- /**
491
- * The long-form recipe reference, served exactly once rather than repeated on
492
- * every ingredient. `catalogSectionLines` prints the ingredients, delivery,
493
- * backend and a compact wiring list (edge only, no `note`) per element and
494
- * points here for the rest — the WHY behind each wiring edge, which is the
495
- * bulk: `workspace-chat` alone has 4 ingredients, and every one of them
496
- * printed every wiring note in full before this moved. Reached via
497
- * `component_reference({ name: "recipes" })`.
498
- */
499
- function renderRecipeAppendix(): string[] {
500
- const lines: string[] = [
501
- `## Surface recipe catalog (${surfaceRecipes.length} total) — wiring notes`,
502
- '',
503
- 'Every element lookup that appears in a recipe lists its ingredients, delivery, backend and ' +
504
- 'a compact wiring list (event out of A, property into B). This is the long form: the WHY ' +
505
- 'behind each wiring edge, written once here rather than repeated on every ingredient.',
506
- ];
507
-
508
- for (const r of surfaceRecipes) {
509
- lines.push(
510
- '',
511
- `#### ${r.id} — ${r.intent}`,
512
- `- **Ingredients:** ${r.ingredients.map((t) => `\`<${t}>\``).join(', ')}`,
513
- `- **Delivery:** ${r.targets.join(', ')} · archetype: ${r.archetypes.join(', ')}`,
514
- `- **Backend:** your own endpoint, read with \`${r.backend.reader}\` from \`@kitn.ai/ui/wire\``,
515
- `- **Invariants it leans on:** ${r.invariants.join(', ')}`,
516
- ...compositionLines(r, ' — '),
517
- '- **Wiring** — event out of A, property into B, wired by the host:',
518
- );
519
- for (const w of r.wiring) {
520
- lines.push(
521
- ` - \`<${w.from}>\` fires \`${w.event}\` → host sets \`${w.to}.${w.property}\`` +
522
- (w.note ? ` — ${w.note}` : ''),
523
- );
524
- }
525
- }
526
-
527
- return lines;
528
- }
529
-
530
- /**
531
- * The long-form invariant reference, served exactly once rather than repeated
532
- * on every element it applies to. `catalogSectionLines` prints only the id,
533
- * scope and one-line statement per element and points here for the rest —
534
- * this is that "rest": every record's full statement again for context, its
535
- * diagnosis rows, and its wrong/right examples, each written once for the
536
- * whole catalog regardless of how many of the 80 elements the record applies
537
- * to. Reached via `component_reference({ name: "invariants" })`.
538
- */
539
- function renderInvariantAppendix(): string[] {
540
- const lines: string[] = [
541
- `## Invariant catalog (${invariants.length} total) — diagnosis and examples`,
542
- '',
543
- 'Every element lookup lists the invariants that apply to it by id and one-line statement. ' +
544
- 'This is the long form: full statement, diagnosis and wrong/right examples for each record, ' +
545
- 'written once here rather than repeated on every element it applies to.',
546
- ];
547
-
548
- for (const inv of invariants) {
549
- const scope = scopeOf(inv);
550
- lines.push('', `#### ${inv.id}${scope ? ` (${scope})` : ''} — ${coverageOf(inv)}`, inv.statement);
551
- if (inv.diagnosis.length > 0) {
552
- lines.push('', 'If you are debugging:');
553
- for (const d of inv.diagnosis) lines.push(`- ${d.symptom} → ${d.cause}`);
554
- }
555
- lines.push(...exampleLines(inv.examples));
556
- }
557
-
558
- return lines;
559
- }
560
-
561
- // ─────────────────────────────────────────────────────────────────────────────
562
- // The programmatic layer and the code recipes
563
- // ─────────────────────────────────────────────────────────────────────────────
564
-
565
- /** The topic names that all resolve to the programmatic-layer appendix. */
566
- const PROGRAMMATIC_ALIASES = ['programmatic', 'state', 'wire', 'state-wire'] as const;
567
-
568
- /**
569
- * The `/state` + `/wire` appendix (rung-6 F-46): a marker-fenced section of the
570
- * package-root `llms-full.txt`, which `build:api` DERIVES from the shipped
571
- * `dist/state/*.d.ts` + `dist/wire/*.d.ts` (scripts/gen-llms-programmatic.mjs).
572
- * Read, never restated: this tool restating one signature by hand is exactly
573
- * the drift the generator exists to remove. The root copy is the CANONICAL and
574
- * ONLY shipped copy (owner-ruled 2026-08-25): a second copy under `dist/llms/`
575
- * used to ship too, ~293 KB of duplicate tarball weight — and it once went
576
- * stale against the root while both claimed to be the artifact. It resolves
577
- * relative to the manifest (`dist/custom-elements.json`), whose parent's
578
- * parent is the package root in both contexts — bundled bin and source/vitest.
579
- */
580
- /**
581
- * The fence gen-llms-programmatic.mjs writes around the section inside
582
- * llms-full.txt. REGISTERED COPY of `PROGRAMMATIC_MARKERS` in that script —
583
- * this TS module cannot import the generator .mjs. If either side moves, the
584
- * slice below misses and the reference tests fail on the "Missing build
585
- * artifact" branch, so the drift is loud.
586
- */
587
- const PROGRAMMATIC_START = '<!-- kai:programmatic:start -->';
588
- const PROGRAMMATIC_END = '<!-- kai:programmatic:end -->';
589
-
590
- function renderProgrammaticAppendix(): string {
591
- // Sliced out of the SHIPPED llms-full.txt rather than served from a file of
592
- // its own: a standalone copy was ~50 KB of duplicate tarball weight
593
- // (verify:pack priced it), and slicing keeps one derivation with zero extra
594
- // shipped bytes.
595
- const path = join(dirname(resolveManifestPath()), '..', 'llms-full.txt');
596
- const missing = (what: string) =>
597
- `Missing build artifact: ${what}\n\n` +
598
- 'The programmatic-layer reference is generated from the shipped dist/*.d.ts by the ' +
599
- 'build (a marker-fenced section of the package-root llms-full.txt). Run `nx build ui` ' +
600
- '(or `npm run build:api` in packages/ui) and ask again. It is deliberately not restated ' +
601
- 'here: a hand-typed copy would drift from the declarations your editor shows.';
602
- if (!existsSync(path)) return missing(path);
603
- const full = readFileSync(path, 'utf-8');
604
- const start = full.indexOf(PROGRAMMATIC_START);
605
- const end = full.indexOf(PROGRAMMATIC_END);
606
- if (start === -1 || end === -1 || end <= start) {
607
- return missing(`the ${PROGRAMMATIC_START} section inside ${path}`);
608
- }
609
- return full.slice(start + PROGRAMMATIC_START.length, end).trim();
610
- }
611
-
612
- /**
613
- * One code recipe as files a builder pastes. The catalog half (ingredients,
614
- * wiring edges, invariants — each executed by surfaces.test.ts) is the surface
615
- * recipe of the same id; this is the body a data record cannot carry, and
616
- * `verify:scaffold` compiles every `lang: 'ts'` file of it.
617
- */
618
- function renderCodeRecipe(r: CodeRecipe): string {
619
- const lines: string[] = [
620
- `## Recipe: ${r.title} (\`${r.id}\`)`,
621
- '',
622
- r.intent,
623
- '',
624
- `**Elements composed:** ${r.ingredients.map((t) => `\`<${t}>\``).join(', ')}`,
625
- '',
626
- '**Read these before pasting:**',
627
- ...r.notes.map((n) => `- ${n}`),
628
- ];
629
- for (const f of r.files) {
630
- lines.push('', `### \`${f.path}\``, '', '```' + f.lang, f.code.trimEnd(), '```');
631
- }
632
- lines.push(
633
- '',
634
- 'The wiring topology, invariants and composition claims behind this recipe are the ' +
635
- `surface recipe of the same id — call component_reference with { name: "recipes" }.`,
636
- );
637
- return lines.join('\n');
638
- }
639
-
640
- /**
641
- * The payload out of `CustomEvent<T>`, or `undefined` when there is no payload.
642
- *
643
- * The manifest types every event, but as the wrapper — printing that raw teaches
644
- * a reader to write `CustomEvent<...>` where a payload belongs. Anything that is
645
- * not of the form `CustomEvent<…>` has NO payload to print, and this returns
646
- * `undefined` so the caller omits the `detail` clause entirely.
647
- *
648
- * That is read off the generator rather than guessed at. gen-element-api.mjs
649
- * writes the CEM type as ``CustomEvent<${e.detail}>`` when the element declares a
650
- * detail and the bare string `CustomEvent` when it does not — exactly two shapes,
651
- * and the bare one means the detail is `void` (e.g. `'kai-click': void` in
652
- * button.tsx). Passing an unrecognised type through unchanged looked like a safe
653
- * fallback for an already-unwrapped payload; what it actually caught was the void
654
- * events, which then rendered ``Carries no detail. `detail`: `CustomEvent` `` — a
655
- * line contradicting itself, on 13 events across 10 elements. The whole-manifest
656
- * probe in reference.test.ts holds both directions of the rule.
657
- */
658
- function eventDetail(typeText: string | undefined): string | undefined {
659
- const m = /^CustomEvent<([\s\S]*)>$/.exec(typeText?.trim() ?? '');
660
- return m ? m[1].trim() || undefined : undefined;
661
- }
662
-
663
- function formatReference(tag: string, provider: ToolProvider): string {
664
- const el = getElement(tag);
665
-
666
- if (!el) {
667
- const all = listElements();
668
- const sample = all.slice(0, 5).join(', ');
669
- return (
670
- `Unknown element: ${tag}\n\n` +
671
- `Valid tags include: ${sample} (and ${Math.max(0, all.length - 5)} more).\n` +
672
- `Call component_reference with no name (or name: "list") to list all ${all.length} elements.`
673
- );
674
- }
675
-
676
- const lines: string[] = [];
677
-
678
- // ── Header ─────────────────────────────────────────────────────────────────
679
- lines.push(`## <${tag}>`);
680
- if (el.description) {
681
- lines.push('', el.description.trim());
682
- }
683
-
684
- // ── Getting the element ────────────────────────────────────────────────────
685
- // FIRST, above every other section. An element that never upgrades renders
686
- // nothing and logs NOTHING: the property assigns and reads back fine, and
687
- // whenDefined() never settles, so the listener is never attached. That symptom
688
- // is also what props-not-attributes blames on a different cause, so a reader
689
- // who has the invariants and not this line is steered to the wrong diagnosis.
690
- const entry = entryForTag(tag);
691
- const iface = el.name;
692
- const neverUpgrades =
693
- 'If you skip registering it the element never upgrades: nothing renders, ' +
694
- '**no error and no warning is logged**, a property you set assigns and reads ' +
695
- `back correctly, and \`customElements.whenDefined('${tag}')\` never resolves.`;
696
- lines.push('', '### Getting the element');
697
- if (entry) {
698
- lines.push(
699
- `Register it before you use it. ${neverUpgrades}`,
700
- '',
701
- '```ts',
702
- "import '@kitn.ai/ui/elements';",
703
- `import '@kitn.ai/ui/elements/${entry}'; // or just this one`,
704
- '```',
705
- );
706
- } else {
707
- // entryForTag is undefined exactly when register-impl.ts does not import this
708
- // element's source file — i.e. it is NOT in the register-all bundle. The only
709
- // current instance is kai-remote, a deliberate exception (opt-in cross-origin
710
- // iframe card) documented at src/elements/element-diagnostics.ts:347-363 and
711
- // vite.config.elements.ts:44-50. `import '@kitn.ai/ui/elements'` would NOT
712
- // register a tag in this state, so asserting it here is exactly the false
713
- // claim this section exists to prevent.
714
- //
715
- // The specifier is still NOT guessed — stripping `kai-` is wrong for ten of
716
- // the eighty elements. `optInEntryForTag` reads it off the module the
717
- // per-element build actually emitted, matched to this tag by the tag literal
718
- // it registers; see the note there. Telling the reader to go look it up was
719
- // the last place in this reference where "how to make the element exist" did
720
- // not answer itself, and the answer was in the build config all along.
721
- const optIn = optInEntryForTag(tag);
722
- lines.push(
723
- `**\`${tag}\` is opt-in — it is not part of \`import '@kitn.ai/ui/elements'\` ` +
724
- '(register-all), and that import alone will NOT register it.** ' +
725
- (optIn
726
- ? `Import its own entry point instead. ${neverUpgrades}`
727
- : // No built module claims this tag, so there is no specifier to name.
728
- // Stay honest rather than inventing one.
729
- 'Find its specific `@kitn.ai/ui/elements/<name>` entry point in the ' +
730
- `package's published exports before using it. ${neverUpgrades}`),
731
- );
732
- if (optIn) {
733
- lines.push('', '```ts', `import '@kitn.ai/ui/elements/${optIn}';`, '```');
734
- }
735
- }
736
- if (iface) {
737
- lines.push(
738
- '',
739
- `TypeScript: \`import type { ${iface} } from '@kitn.ai/ui/elements';\` — ` +
740
- 'the element interface ships with the package; do not hand-roll a structural type.',
741
- );
742
- }
743
-
744
- // ── Contract note ──────────────────────────────────────────────────────────
745
- lines.push(
746
- '',
747
- '### AI/UI contract',
748
- '`kai-*` elements accept **array and object data as JavaScript properties** ' +
749
- '(set in JavaScript via `el.property = value`, not as HTML attributes). ' +
750
- 'Events are native CustomEvents — listen with `el.addEventListener("event-name", handler)` ' +
751
- 'and read `event.detail` for the payload.',
752
- );
753
-
754
- // ── Card contract ─────────────────────────────────────────────────────────
755
- // Attached to the two populations manifest.ts derives, and to nothing else. A
756
- // reference that hangs card material off all 80 elements is worse than one that
757
- // hangs it off none: it teaches a harness that every element takes a tool call.
758
- const cardType = cardTypeForTag(tag);
759
- if (cardType !== undefined && isCardSchemaName(cardType)) {
760
- lines.push(...renderCardContract(tag, cardType, provider));
761
- } else if (cardHostTags().includes(tag)) {
762
- lines.push(...renderCardHost(tag));
763
- }
764
-
765
- // ── Public JS properties ───────────────────────────────────────────────────
766
- const publicFields = (el.members ?? []).filter(
767
- (m) => m.kind === 'field' && m.privacy === 'public',
768
- );
769
-
770
- if (publicFields.length > 0) {
771
- lines.push('', '### Props (JavaScript properties)');
772
- for (const field of publicFields) {
773
- const type = field.type?.text ?? 'unknown';
774
- const jsOnly = isJsOnlyType(type);
775
- const desc = field.description?.trim() ?? '';
776
- const note = jsOnly ? ' ⚑ set as a JS property, not an HTML attribute' : '';
777
- lines.push(`- **${field.name}** \`${type}\`${note}`);
778
- if (desc) {
779
- lines.push(` ${desc}`);
780
- }
781
- }
782
- }
783
-
784
- // ── HTML attributes ────────────────────────────────────────────────────────
785
- const attrs = el.attributes ?? [];
786
- if (attrs.length > 0) {
787
- lines.push('', '### Attributes (HTML-safe)');
788
- for (const attr of attrs) {
789
- const type = attr.type?.text ?? 'unknown';
790
- const desc = attr.description?.trim() ?? '';
791
- lines.push(`- **${attr.name}** \`${type}\``);
792
- if (desc) {
793
- lines.push(` ${desc}`);
794
- }
795
- }
796
- }
797
-
798
- // ── Events ────────────────────────────────────────────────────────────────
799
- const events = el.events ?? [];
800
- if (events.length > 0) {
801
- lines.push('', '### Events (CustomEvent, listen via addEventListener)');
802
- for (const ev of events) {
803
- const desc = ev.description?.trim() ?? '';
804
- const detail = eventDetail(ev.type?.text);
805
- lines.push(
806
- detail
807
- ? `- **${ev.name}** — ${desc} \`detail\`: \`${detail}\``
808
- : `- **${ev.name}** — ${desc}`,
809
- );
810
- }
811
- }
812
-
813
- // ── Methods ───────────────────────────────────────────────────────────────
814
- // The input half of the interaction surface. These were in the manifest and in
815
- // the shipped types and rendered in NO reference, so the only way to find one
816
- // was to read the kit's source.
817
- const methods = (el.members ?? []).filter(isPublicMethod);
818
- if (methods.length > 0) {
819
- lines.push(
820
- '',
821
- '### Methods (call these on the element instance)',
822
- `Get the element (\`const el = document.querySelector('${tag}')\`), then call it. ` +
823
- 'Methods drive the element from your code; the events above report back what the ' +
824
- 'user did. Both are public API.',
825
- );
826
- for (const method of methods) {
827
- const desc = method.description?.trim() ?? '';
828
- lines.push(`- **${methodSignature(method)}**${desc ? ` — ${desc}` : ''}`);
829
- }
830
- }
831
-
832
- // ── CSS custom properties ─────────────────────────────────────────────────
833
- const cssProps = el.cssProperties ?? [];
834
- if (cssProps.length > 0) {
835
- lines.push('', '### CSS custom properties');
836
- for (const prop of cssProps) {
837
- const desc = prop.description?.trim() ?? '';
838
- const def = prop.default ? ` (default: ${prop.default})` : '';
839
- lines.push(`- **${prop.name}**${def}${desc ? ` — ${desc}` : ''}`);
840
- }
841
- }
842
-
843
- // ── Composition slots ──────────────────────────────────────────────────────
844
- const slots = el.slots ?? [];
845
- if (slots.length > 0) {
846
- lines.push(
847
- '',
848
- '### Composition slots',
849
- 'Project your own markup into these named regions — add a light-DOM child ' +
850
- 'with a `slot="…"` attribute (e.g. `<div slot="sidebar">…</div>`).',
851
- );
852
- for (const slot of slots) {
853
- const desc = slot.description?.trim() ?? '';
854
- lines.push(`- **${slot.name}**${desc ? ` — ${desc}` : ''}`);
855
- }
856
- }
857
-
858
- // ── Styleable parts (::part) ───────────────────────────────────────────────
859
- const parts = el.cssParts ?? [];
860
- if (parts.length > 0) {
861
- lines.push(
862
- '',
863
- '### Styleable parts (`::part`)',
864
- 'Restyle these from outside the Shadow DOM via `' + tag + '::part(name) { … }`.',
865
- );
866
- for (const part of parts) {
867
- const desc = part.description?.trim() ?? '';
868
- lines.push(`- **${part.name}**${desc ? ` — ${desc}` : ''}`);
869
- if (part.recipe) {
870
- lines.push(' ```css', ` ${part.recipe}`, ' ```');
871
- }
872
- }
873
- }
874
-
875
- // ── The composition catalog ────────────────────────────────────────────────
876
- lines.push(...catalogSectionLines(tag));
877
-
878
- return lines.join('\n');
879
- }
880
-
881
- export const reference: Tool = {
882
- name: 'component_reference',
883
- description:
884
- 'Look up AI/UI (kai-*) web components: their tags, props, events, imperative methods, and usage examples. ' +
885
- 'For a card-backed element it also returns the card\'s JSON Schema and a ready-to-send ' +
886
- 'tool definition generated from it — pass `provider` to get that provider\'s envelope. ' +
887
- 'Also serves the programmatic layer (name: "programmatic" — the @kitn.ai/ui/state + ' +
888
- '@kitn.ai/ui/wire streaming/mock/encode API) and complete code recipes ' +
889
- '(e.g. name: "composed-thread" — a hand-composed thread with no <kai-chat>).',
890
- inputSchema: z.object({
891
- name: z
892
- .string()
893
- .optional()
894
- .describe(
895
- 'The element tag, e.g. "kai-chat". Omit it (or pass "list") to get the ' +
896
- 'index of every element with a one-line summary, then ask again for the one you want. ' +
897
- 'Pass "invariants" for the full diagnosis and wrong/right examples behind every ' +
898
- 'invariant id an element lookup shows. Pass "recipes" for the full wiring notes ' +
899
- 'behind every surface recipe an element lookup names. Pass "programmatic" (or ' +
900
- '"state" / "wire") for the @kitn.ai/ui/state + @kitn.ai/ui/wire API — streaming ' +
901
- 'folds, the mock responder, the SSE readers/encoders. Pass a code-recipe id ' +
902
- '(e.g. "composed-thread") for a complete pasteable composition.',
903
- ),
904
- provider: z
905
- .enum(PROVIDERS)
906
- .optional()
907
- .describe(
908
- 'Which tool-definition envelope to project for a card-backed element. ' +
909
- `Defaults to "${DEFAULT_PROVIDER}", the provider-neutral { name, description, schema } form.`,
910
- ),
911
- }),
912
- handler: async (args: Record<string, unknown>) => {
913
- const name = typeof args.name === 'string' ? args.name.trim() : undefined;
914
-
915
- // Validated HERE, not left to the zod schema above. server.ts advertises
916
- // `inputSchema` over the protocol but hands `request.params.arguments` straight
917
- // to this handler without parsing it, so an unrecognised provider would reach
918
- // `cardTools`, which treats anything that is not "openai"/"anthropic" as the
919
- // jsonschema branch. A caller asking for "gemini" would then get a definition in
920
- // a shape they did not ask for, with nothing saying so. Refusing is the only
921
- // answer that cannot be mistaken for an answer.
922
- const rawProvider = args.provider;
923
- if (rawProvider !== undefined && (typeof rawProvider !== 'string' || !isProvider(rawProvider))) {
924
- return {
925
- content: [
926
- {
927
- type: 'text' as const,
928
- text:
929
- `Unknown provider: ${JSON.stringify(rawProvider)}\n\n` +
930
- `Valid providers are ${PROVIDERS.map((p) => `"${p}"`).join(', ')}. ` +
931
- `Omit it to get "${DEFAULT_PROVIDER}", the provider-neutral { name, description, schema } form ` +
932
- 'that the Vercel AI SDK and most agent frameworks take directly.\n' +
933
- 'No tool definition was returned, because guessing which envelope you meant would hand you ' +
934
- 'a shape your provider rejects with nothing saying why.',
935
- },
936
- ],
937
- };
938
- }
939
- const provider: ToolProvider = rawProvider ?? DEFAULT_PROVIDER;
940
-
941
- let text: string;
942
-
943
- if (!name || name === 'list') {
944
- const tags = listElements();
945
- const cardRows = cardSchemaNames
946
- .map((t) => cardTagForType(t))
947
- .filter((t): t is string => t !== undefined);
948
- text =
949
- `AI/UI elements (${tags.length} total):\n\n` +
950
- tags.map((t) => ` ${t}`).join('\n') +
951
- '\n\nCall component_reference with a specific name (e.g. { name: "kai-chat" }) for full API details.' +
952
- `\n\nCard-backed elements — ask for one of these to get its JSON Schema and a generated ` +
953
- `\`${KAI_TOOL_PREFIX}*\` tool definition:\n ${cardRows.join(', ')}` +
954
- `\nThe elements that HOST them (and carry the \`cardTypes\` / \`cardSchemas\` props): ${cardHostTags().join(', ')}` +
955
- `\n\nCall component_reference with { name: "invariants" } for the full diagnosis and ` +
956
- 'wrong/right examples behind every invariant id shown on an element lookup.' +
957
- `\n\nCall component_reference with { name: "recipes" } for the full wiring notes behind ` +
958
- 'every surface recipe an element lookup names.' +
959
- `\n\nCall component_reference with { name: "programmatic" } for the @kitn.ai/ui/state + ` +
960
- '@kitn.ai/ui/wire API — createAssistantStream, the part folds, createMockResponder ' +
961
- '(and its scripted tool calls), the SSE readers and the provider encoders. That layer ' +
962
- 'is what a host wires when it composes elements by hand instead of using <kai-chat>.' +
963
- `\n\nComplete pasteable compositions (code recipes): ${listCodeRecipes()
964
- .map((r) => `{ name: "${r.id}" } — ${r.title}`)
965
- .join('; ')}.`;
966
- } else if (name === 'invariants') {
967
- text = renderInvariantAppendix().join('\n');
968
- } else if (name === 'recipes') {
969
- text = renderRecipeAppendix().join('\n');
970
- } else if ((PROGRAMMATIC_ALIASES as readonly string[]).includes(name)) {
971
- text = renderProgrammaticAppendix();
972
- } else if (getCodeRecipe(name)) {
973
- text = renderCodeRecipe(getCodeRecipe(name)!);
974
- } else {
975
- text = formatReference(name, provider);
976
- }
977
-
978
- return { content: [{ type: 'text' as const, text }] };
979
- },
980
- };