@kitn.ai/ui 0.26.0 → 0.28.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.
- package/README.md +25 -6
- package/bin/mcp.js +16 -3
- package/bin/route.js +23 -0
- package/bin/route.test.js +58 -0
- package/dist/{elements/chunks/Icon--Y8vsFal.js → Icon-DObhsRtm.js} +1 -1
- package/dist/action-icons-CcgNN2oT.js +1 -0
- package/dist/arrow-down-CjRU3JgV.js +1 -0
- package/dist/arrow-left-Bnpb1UEW.js +1 -0
- package/dist/artifact-Bwt7OgcR.js +1 -0
- package/dist/assets/dev-5XzXYR21.js +91 -0
- package/dist/attachments-Qr4kg-2b.js +1 -0
- package/dist/{elements/chunks/audio-visualizer-D7lB6mvR.js → audio-visualizer-DToYdfSc.js} +1 -1
- package/dist/badge-CdEu_jn2.js +1 -0
- package/dist/bash-InADTalH.js +1 -6
- package/dist/bell-DQAAOpDz.js +1 -0
- package/dist/{elements/chunks/button-flUbeufF.js → button-DkuOnGLy.js} +1 -1
- package/dist/card-renderer-Jx70m6_l.js +1 -0
- package/dist/card-routing-0YidER_3.js +1 -0
- package/dist/check-njkVdZu2.js +1 -0
- package/dist/checkbox-D7iRB3nL.js +1 -0
- package/dist/checkbox-group-B2_rpoPM.js +1 -0
- package/dist/chevron-down-B72s6PXR.js +1 -0
- package/dist/chevron-right-BatuHzUm.js +1 -0
- package/dist/choice-card-m0BYqqOK.js +1 -0
- package/dist/circle-check-B62pODB3.js +1 -0
- package/dist/circle-x-BPG0iPst.js +1 -0
- package/dist/circle-zx0ByzY9.js +1 -0
- package/dist/{elements/chunks/cn-CU7UAWzk.js → cn-DN5AWfiS.js} +1 -1
- package/dist/code-block-ClMLKb7C.js +1 -0
- package/dist/{elements/chunks/collapsible-BzbXpPzr.js → collapsible-B4DlWicL.js} +1 -1
- package/dist/components/attachment-types.d.ts +7 -0
- package/dist/components/audio-visualizer/fit-scale.d.ts +1 -1
- package/dist/components/card-renderer.d.ts +13 -0
- package/dist/components/card.d.ts +4 -0
- package/dist/components/chat-thread.d.ts +135 -2
- package/dist/components/conversation-item.d.ts +35 -7
- package/dist/components/conversation-list.d.ts +17 -5
- package/dist/components/conversation-panel.d.ts +29 -0
- package/dist/components/form-widgets.d.ts +67 -2
- package/dist/components/form.d.ts +48 -1
- package/dist/components/home-panel.d.ts +34 -0
- package/dist/components/message.d.ts +25 -3
- package/dist/components/reasoning.d.ts +6 -0
- package/dist/components/scroll-button.d.ts +11 -0
- package/dist/components/thread.d.ts +5 -0
- package/dist/components/tool-types.d.ts +9 -0
- package/dist/components/widget-tab-bar.d.ts +16 -0
- package/dist/composer-sQ0HcvyG.js +106 -0
- package/dist/confirm-card-rQWDQsnC.js +1 -0
- package/dist/construct-cli.es.js +1337 -0
- package/dist/{elements/chunks/context-VLIL3sJ-.js → context-0kg4ZX1J.js} +1 -1
- package/dist/controllable-CnGpMF6r.js +1 -0
- package/dist/conversation-item-BxwQLaFJ.js +1 -0
- package/dist/conversation-list-BCfGN1JU.js +1 -0
- package/dist/copy-n9u9rqpi.js +1 -0
- package/dist/core-AYMC6_lb.js +12 -5874
- package/dist/{create-tween-BFT6c-Y8.js → create-tween-C-q-JCM-.js} +1 -1
- package/dist/{create-tween-CJNlzoaO.js → create-tween-CRrJXxBd.js} +1 -1
- package/dist/{create-tween-CQED4T0g.js → create-tween-DZ3RRDHD.js} +1 -1
- package/dist/{elements/chunks/create-tween-c4Z8TDYz.js → create-tween-uPkArjIG.js} +2 -2
- package/dist/css-M7EaDHN_.js +1 -6
- package/dist/custom-elements.json +1433 -202
- package/dist/default-input-CaMrJkgH.js +1 -0
- package/dist/define-By-uL588.js +1 -0
- package/dist/define.d.ts +4 -0
- package/dist/define.js +510 -0
- package/dist/define.server.d.ts +5 -0
- package/dist/define.server.js +506 -0
- package/dist/{elements/chunks/disclosure-CcNbyLy0.js → disclosure-WUImBbDv.js} +1 -1
- package/dist/{elements/chunks/dropdown-BgWTthUx.js → dropdown-wZVeJOMd.js} +1 -1
- package/dist/elements/agent-card.js +1 -1
- package/dist/elements/artifact.js +1 -1
- package/dist/elements/attachments.js +1 -1
- package/dist/elements/audio-visualizer.js +1 -1
- package/dist/elements/autoloader.js +1 -1
- package/dist/elements/avatar.js +1 -1
- package/dist/elements/badge.js +1 -1
- package/dist/elements/button.js +1 -1
- package/dist/elements/card.js +1 -1
- package/dist/elements/cards.js +1 -1
- package/dist/elements/chain-of-thought.js +1 -1
- package/dist/elements/chat-scope-picker.js +1 -1
- package/dist/elements/chat-workspace.js +1 -1
- package/dist/elements/chat.js +1 -1
- package/dist/elements/checkbox-group.d.ts +1 -0
- package/dist/elements/checkbox-group.js +1 -0
- package/dist/elements/checkbox.d.ts +1 -0
- package/dist/elements/checkbox.js +1 -0
- package/dist/elements/checkpoint.js +1 -1
- package/dist/elements/choice.js +1 -1
- package/dist/elements/coachmark.js +1 -1
- package/dist/elements/code-block.js +1 -1
- package/dist/elements/command.js +1 -1
- package/dist/elements/compare.js +1 -1
- package/dist/elements/composer.js +1 -1
- package/dist/elements/confirm-card.js +1 -1
- package/dist/elements/context-meter.js +1 -1
- package/dist/elements/conversation-item.js +1 -1
- package/dist/elements/conversation-list.js +1 -1
- package/dist/elements/define-entry.d.ts +12 -0
- package/dist/elements/dialog.js +1 -1
- package/dist/elements/dock.js +102 -8
- package/dist/elements/dropdown.js +1 -1
- package/dist/elements/editable-label.js +1 -1
- package/dist/elements/element-data-types.d.ts +37 -0
- package/dist/elements/embed.js +1 -1
- package/dist/elements/empty.js +1 -1
- package/dist/elements/feedback-bar.js +1 -1
- package/dist/elements/file-tree.js +1 -1
- package/dist/elements/file-upload.js +1 -1
- package/dist/elements/form.js +1 -1
- package/dist/elements/hover-card.js +1 -1
- package/dist/elements/icon.js +1 -1
- package/dist/elements/image.js +1 -1
- package/dist/elements/input.js +1 -1
- package/dist/elements/kbd.js +1 -1
- package/dist/elements/link-preview.js +1 -1
- package/dist/elements/loader.js +1 -1
- package/dist/elements/markdown.js +1 -1
- package/dist/elements/menu.js +1 -1
- package/dist/elements/message-skills.js +1 -1
- package/dist/elements/message.js +1 -1
- package/dist/elements/model-switcher.js +1 -1
- package/dist/elements/nav.js +1 -1
- package/dist/elements/notice.js +1 -1
- package/dist/elements/pane-grid.d.ts +1 -0
- package/dist/elements/pane-grid.js +1 -0
- package/dist/elements/pane-group.js +1 -1
- package/dist/elements/pane.js +1 -1
- package/dist/elements/popover.js +1 -1
- package/dist/elements/progress-bar.js +1 -1
- package/dist/elements/prompt-dock.js +1 -1
- package/dist/elements/prompt-input.js +1 -1
- package/dist/elements/prompt-suggestions.js +1 -1
- package/dist/elements/radio-group.d.ts +1 -0
- package/dist/elements/radio-group.js +1 -0
- package/dist/elements/reasoning.js +1 -1
- package/dist/elements/remote.js +1 -1
- package/dist/elements/resizable.js +1 -1
- package/dist/elements/response-stream.js +2 -2
- package/dist/elements/screen.js +1 -1
- package/dist/elements/scroll-area.js +1 -1
- package/dist/elements/scroll-button.js +1 -1
- package/dist/elements/search.js +1 -1
- package/dist/elements/segmented.js +1 -1
- package/dist/elements/select.d.ts +1 -0
- package/dist/elements/select.js +1 -0
- package/dist/elements/separator.js +1 -1
- package/dist/elements/setting-item.js +1 -1
- package/dist/elements/settings-group.js +1 -1
- package/dist/elements/skeleton.js +1 -1
- package/dist/elements/slider.d.ts +1 -0
- package/dist/elements/slider.js +1 -0
- package/dist/elements/slots.d.ts +1 -1
- package/dist/elements/source.js +1 -1
- package/dist/elements/status.js +1 -1
- package/dist/elements/switch.js +1 -1
- package/dist/elements/tabs.js +1 -1
- package/dist/elements/tasks.js +1 -1
- package/dist/elements/text-shimmer.js +1 -1
- package/dist/elements/thinking-bar.js +1 -1
- package/dist/elements/thread.js +1 -1
- package/dist/elements/toast.js +1 -1
- package/dist/elements/tool.js +1 -1
- package/dist/elements/tooltip.js +1 -1
- package/dist/elements/voice-input.js +1 -1
- package/dist/elements/voice-output.js +1 -1
- package/dist/elements.d.ts +380 -26
- package/dist/{elements/chunks/ellipsis-7gU33mL2.js → ellipsis-CbOpJnU3.js} +1 -1
- package/dist/embed-CrfjkbUG.js +1 -0
- package/dist/engine-javascript-vq0WuIJl.js +14 -2516
- package/dist/{elements/chunks/external-link-CSOEsNlx.js → external-link-ByXAwBBQ.js} +1 -1
- package/dist/file-text-DMib2qwt.js +1 -0
- package/dist/{elements/chunks/file-tree-zvB2i7P9.js → file-tree-CJqM2aP7.js} +1 -1
- package/dist/folder-_BAb929C.js +1 -0
- package/dist/form-DSZgZQZk.js +1 -0
- package/dist/github-dark-dimmed-DUshB20C.js +1 -4
- package/dist/github-light-JYsPkUQd.js +1 -4
- package/dist/hover-card-BQbul9eq.js +1 -0
- package/dist/html-CPZ3oZQ7.js +1 -10
- package/dist/icon-CdcfRhYI.js +1 -0
- package/dist/{elements/chunks/index-BCRhi84a.js → index-xu_pdcPi.js} +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +7474 -5857
- package/dist/index.server.js +5965 -4762
- package/dist/info-DsFjGuI_.js +1 -0
- package/dist/input-DKbmkMos.js +1 -0
- package/dist/javascript-C25yR2R2.js +1 -6
- package/dist/json-DxJze_jm.js +1 -6
- package/dist/kai.es.js +1 -1
- package/dist/kbd-PvdGhX4s.js +1 -0
- package/dist/{elements/chunks/link-preview-OQ9IEq0f.js → link-preview-DbM1866N.js} +1 -1
- package/dist/link-zF8PzWfF.js +1 -0
- package/dist/loader-BM4CEz4G.js +1 -0
- package/dist/{elements/chunks/markdown-DgBFaGAz.js → markdown-LMvmAyBk.js} +7 -7
- package/dist/mcp.es.js +1361 -81
- package/dist/message-BSsgvHvf.js +1 -0
- package/dist/message-BYZECyek.js +1 -0
- package/dist/message-circle-DruwObfX.js +1 -0
- package/dist/message-square-BG6q6mlm.js +1 -0
- package/dist/{elements/chunks/minimize-2-DPIUKTrT.js → minimize-2-BQRoBV1V.js} +1 -1
- package/dist/{elements/chunks/model-switcher-DsT8tFB0.js → model-switcher-CMpCOYcB.js} +1 -1
- package/dist/{elements/chunks/overlay-DuiwdXjm.js → overlay-Cb-MkyDa.js} +1 -1
- package/dist/panel-right-i8warFxT.js +1 -0
- package/dist/paperclip-ByY3UVO6.js +1 -0
- package/dist/play-BvKKq3fX.js +1 -0
- package/dist/primitives/card-data-types.d.ts +49 -0
- package/dist/primitives/card-routing.d.ts +1 -33
- package/dist/primitives/card-validate.d.ts +2 -2
- package/dist/primitives/chat-config.d.ts +29 -2
- package/dist/primitives/conversation-store.d.ts +54 -0
- package/dist/primitives/field-mask.d.ts +96 -0
- package/dist/primitives/field-semantics.d.ts +36 -0
- package/dist/primitives/input-mask.d.ts +46 -0
- package/dist/primitives/url-scheme-policy.d.ts +34 -0
- package/dist/primitives/use-resize-observer.d.ts +2 -2
- package/dist/progress-bar-jVAHvMUf.js +1 -0
- package/dist/prompt-suggestion-CD-qtNgC.js +1 -0
- package/dist/radio-BsoBMhkF.js +1 -0
- package/dist/react/index.d.ts +586 -19
- package/dist/react.js +223 -187
- package/dist/reasoning-4kBPtlTx.js +1 -0
- package/dist/register-impl-B1hLkrI8.js +424 -0
- package/dist/{elements/chunks/resizable-D181b4fM.js → resizable-uPmY9VY-.js} +1 -1
- package/dist/rotate-ccw-B2xn93py.js +1 -0
- package/dist/rotate-cw-CMVLoCyH.js +1 -0
- package/dist/schemas/form.schema.json +21 -1
- package/dist/schemas/tool-defs.d.ts +11 -1
- package/dist/schemas.js +81 -81
- package/dist/scroll-area-BqLUo8ZZ.js +1 -0
- package/dist/scroll-button-DslXiJyL.js +1 -0
- package/dist/select-2cLRtPGT.js +1 -0
- package/dist/{elements/chunks/separator-CeJHQqLW.js → separator-BWQtNJcM.js} +1 -1
- package/dist/{elements/chunks/settings-CqaHgJF9.js → settings-BBR63jLg.js} +1 -1
- package/dist/{elements/chunks/settings-group-CjqjLCOn.js → settings-group-B2ciy-Iw.js} +1 -1
- package/dist/{elements/chunks/skeleton-D8akBnKS.js → skeleton-CkHI-HgC.js} +1 -1
- package/dist/slider-DDR9U-i0.js +1 -0
- package/dist/{solid-BtdqNZI7.js → solid-BzCFdL25.js} +9771 -7594
- package/dist/{solid-CnavwBye.js → solid-DruVOXnf.js} +7893 -6183
- package/dist/solid.d.ts +16 -2
- package/dist/solid.js +256 -243
- package/dist/solid.server.js +256 -243
- package/dist/source-GEnFF528.js +1 -0
- package/dist/star-CilHq8GP.js +1 -0
- package/dist/state/index.d.ts +1 -1
- package/dist/state/mock.d.ts +32 -2
- package/dist/state.js +133 -108
- package/dist/store-Ciyk6eZG.js +1 -0
- package/dist/svelte--5p79yCD.js +1 -15
- package/dist/switch-KkeQIbuH.js +1 -0
- package/dist/tasks-card-BrN4bggJ.js +1 -0
- package/dist/text-shimmer-Dcj1MHuq.js +1 -0
- package/dist/textarea-Dpvbngbg.js +1 -0
- package/dist/theme.tokens.css +110 -13
- package/dist/{elements/chunks/thumbs-up-DX2Ijk3H.js → thumbs-up-CsT9Wzb9.js} +1 -1
- package/dist/toast-store-tgK6MuMv.js +1 -0
- package/dist/{elements/chunks/tool-DKQ2D1DP.js → tool-DzUjIBT8.js} +1 -1
- package/dist/{elements/chunks/tooltip-B0G99y8w.js → tooltip-Bkkg_4t0.js} +1 -1
- package/dist/trash-2-jBKxjGGQ.js +1 -0
- package/dist/{elements/chunks/triangle-alert-F9cyUEyy.js → triangle-alert-DlQ68ppf.js} +1 -1
- package/dist/tsx-B8rCNbgL.js +1 -6
- package/dist/types.d.ts +48 -3
- package/dist/typescript-RycA9KXf.js +1 -6
- package/dist/ui/checkbox-group.d.ts +83 -0
- package/dist/ui/checkbox.d.ts +40 -0
- package/dist/ui/dock.d.ts +51 -0
- package/dist/ui/icon.d.ts +3 -0
- package/dist/ui/input.d.ts +48 -2
- package/dist/ui/radio.d.ts +85 -0
- package/dist/ui/select.d.ts +71 -0
- package/dist/ui/slider.d.ts +65 -0
- package/dist/ui/switch.d.ts +9 -2
- package/dist/upload-TcAHXv9W.js +1 -0
- package/dist/url-scheme-policy-DR5DgvkM.js +1 -0
- package/dist/use-card-resolution-DaZqAdWf.js +1 -0
- package/dist/{elements/chunks/variant-aurora-qt6Zct8M.js → variant-aurora-BhLuYxJR.js} +1 -1
- package/dist/{variant-aurora-B8II3-I1.js → variant-aurora-CNmfHhcx.js} +2 -2
- package/dist/{variant-aurora-CMNWOSJR.js → variant-aurora-D6GsdTPA.js} +2 -2
- package/dist/{variant-aurora-CcuDMWwQ.js → variant-aurora-DbW_OWL2.js} +1 -1
- package/dist/{elements/chunks/variant-custom-tJp22_BQ.js → variant-custom-BRvaNHSi.js} +1 -1
- package/dist/{variant-custom-L16JLLZy.js → variant-custom-BvYHR62b.js} +1 -1
- package/dist/{variant-custom-BdvYC7af.js → variant-custom-DMTrEO5w.js} +2 -2
- package/dist/{variant-custom-C714mqKl.js → variant-custom-DV2x254s.js} +2 -2
- package/dist/{variant-wave-ecwykeyL.js → variant-wave-BNMGrDfj.js} +2 -2
- package/dist/{variant-wave-2S4La2Ka.js → variant-wave-BpDd8t3Z.js} +1 -1
- package/dist/{variant-wave-D7ruiA1W.js → variant-wave-CyBueXj9.js} +2 -2
- package/dist/{elements/chunks/variant-wave-CaQ5ti39.js → variant-wave-sp-qwsdI.js} +1 -1
- package/dist/vue-BmIZj4XD.js +1 -33
- package/dist/x-Z5UntkIJ.js +1 -0
- package/frameworks/react/index.tsx +210 -22
- package/llms-full.txt +1184 -22
- package/llms.txt +6 -3
- package/package.json +17 -4
- package/src/agent-tooling/README.md +192 -0
- package/src/agent-tooling/catalog/invariants.ts +3 -3
- package/src/agent-tooling/catalog/surfaces.ts +72 -0
- package/src/agent-tooling/construct/cli-entry.ts +8 -0
- package/src/agent-tooling/construct/cli.ts +147 -0
- package/src/agent-tooling/construct/codegen.ts +1635 -0
- package/src/agent-tooling/construct/construct.v1.schema.json +319 -0
- package/src/agent-tooling/construct/dev.ts +148 -0
- package/src/agent-tooling/construct/fixtures/demo-widget.construct.json +7 -0
- package/src/agent-tooling/construct/fixtures/ops-console.construct.json +44 -0
- package/src/agent-tooling/construct/fixtures/owner-widget.construct.json +32 -0
- package/src/agent-tooling/construct/schema.ts +452 -0
- package/src/agent-tooling/mcp/server.ts +2 -1
- package/src/agent-tooling/mcp/tools/construct.ts +130 -0
- package/src/agent-tooling/mcp/tools/debug.ts +126 -4
- package/src/agent-tooling/mcp/tools/reference.ts +104 -3
- package/src/agent-tooling/recipes/composed-thread.ts +714 -0
- package/src/agent-tooling/recipes/index.ts +22 -0
- package/src/agent-tooling/recipes/types.ts +32 -0
- package/src/agent-tooling/registry.ts +5 -0
- package/src/components/attachment-types.ts +7 -0
- package/src/components/audio-visualizer/fit-scale.ts +1 -1
- package/src/components/card-renderer.tsx +39 -8
- package/src/components/card.tsx +10 -2
- package/src/components/chat-scope-picker.tsx +2 -2
- package/src/components/chat-thread.tsx +602 -43
- package/src/components/choice-card.tsx +102 -37
- package/src/components/coachmark.tsx +1 -1
- package/src/components/code-block.tsx +2 -2
- package/src/components/composer.tsx +89 -15
- package/src/components/confirm-card.tsx +20 -4
- package/src/components/conversation-item.tsx +56 -9
- package/src/components/conversation-list.tsx +25 -10
- package/src/components/conversation-panel.tsx +118 -0
- package/src/components/empty.tsx +14 -3
- package/src/components/file-tree.tsx +5 -1
- package/src/components/form-widgets.tsx +257 -195
- package/src/components/form.tsx +297 -9
- package/src/components/home-panel.tsx +159 -0
- package/src/components/loader.tsx +12 -5
- package/src/components/message-skills.tsx +1 -1
- package/src/components/message.tsx +71 -11
- package/src/components/prompt-input.tsx +7 -1
- package/src/components/prompt-suggestion.tsx +4 -1
- package/src/components/reasoning.tsx +30 -3
- package/src/components/response-compare.tsx +2 -2
- package/src/components/scroll-button.tsx +56 -4
- package/src/components/source.tsx +6 -1
- package/src/components/tasks-card.tsx +58 -16
- package/src/components/thread.tsx +10 -1
- package/src/components/toast.tsx +1 -1
- package/src/components/tool-types.ts +9 -0
- package/src/components/widget-tab-bar.tsx +73 -0
- package/src/elements/attachments.tsx +3 -1
- package/src/elements/chat-workspace.tsx +1 -1
- package/src/elements/chat.tsx +115 -5
- package/src/elements/checkbox-group.tsx +164 -0
- package/src/elements/checkbox.tsx +134 -0
- package/src/elements/compiled.css +1 -1
- package/src/elements/composer.tsx +12 -1
- package/src/elements/conversation-item.tsx +62 -22
- package/src/elements/conversation-list.tsx +2 -2
- package/src/elements/define-entry.ts +12 -0
- package/src/elements/dock.tsx +11 -0
- package/src/elements/element-data-types.ts +40 -0
- package/src/elements/element-manifest.json +24 -0
- package/src/elements/element-meta.json +767 -34
- package/src/elements/element-nonscalar.json +17 -0
- package/src/elements/element-types.d.ts +380 -26
- package/src/elements/feedback-bar.tsx +5 -0
- package/src/elements/icon-names.json +29 -0
- package/src/elements/input.tsx +312 -23
- package/src/elements/message.tsx +2 -0
- package/src/elements/pane-grid.tsx +111 -0
- package/src/elements/prompt-input.tsx +8 -2
- package/src/elements/radio-group.tsx +129 -0
- package/src/elements/register-impl.ts +6 -0
- package/src/elements/scroll-button.tsx +54 -6
- package/src/elements/select.tsx +165 -0
- package/src/elements/slider.tsx +171 -0
- package/src/elements/slots.ts +1 -1
- package/src/elements/styles.css +89 -3
- package/src/elements/thread.tsx +2 -0
- package/src/elements/toast.tsx +14 -1
- package/src/index.ts +5 -2
- package/src/primitives/card-data-types.ts +49 -0
- package/src/primitives/card-host.tsx +4 -2
- package/src/primitives/card-routing.ts +7 -56
- package/src/primitives/card-schemas/form.schema.json +21 -1
- package/src/primitives/card-validate.ts +10 -2
- package/src/primitives/chat-config.tsx +33 -6
- package/src/primitives/conversation-store.ts +248 -0
- package/src/primitives/field-mask.ts +256 -0
- package/src/primitives/field-semantics.ts +115 -0
- package/src/primitives/input-mask.ts +853 -0
- package/src/primitives/toast-store.ts +66 -4
- package/src/primitives/url-scheme-policy.ts +70 -0
- package/src/primitives/use-resize-observer.ts +2 -2
- package/src/schemas/tool-defs.ts +12 -2
- package/src/solid.ts +20 -2
- package/src/state/index.ts +1 -1
- package/src/state/mock.ts +92 -6
- package/src/types.ts +45 -3
- package/src/ui/agent-card.tsx +2 -2
- package/src/ui/avatar.tsx +1 -1
- package/src/ui/checkbox-group.tsx +153 -0
- package/src/ui/checkbox.tsx +63 -0
- package/src/ui/dock.tsx +192 -7
- package/src/ui/icon.tsx +53 -0
- package/src/ui/input.tsx +249 -9
- package/src/ui/kbd.tsx +1 -1
- package/src/ui/nav.tsx +1 -1
- package/src/ui/pane-group.tsx +2 -2
- package/src/ui/radio.tsx +150 -0
- package/src/ui/select.tsx +168 -0
- package/src/ui/slider.tsx +178 -0
- package/src/ui/switch.tsx +49 -18
- package/src/utils/cn.ts +11 -5
- package/theme.css +112 -13
- package/dist/elements/chunks/action-icons-D9u3OzYW.js +0 -1
- package/dist/elements/chunks/arrow-left-CVh1TeN7.js +0 -1
- package/dist/elements/chunks/artifact-BkYcfIlZ.js +0 -1
- package/dist/elements/chunks/attachments-DwfNlubq.js +0 -1
- package/dist/elements/chunks/badge-Q0-Cmwzz.js +0 -1
- package/dist/elements/chunks/bash-InADTalH.js +0 -1
- package/dist/elements/chunks/card-renderer-3Dyl-FpN.js +0 -1
- package/dist/elements/chunks/card-routing-BK09BdmL.js +0 -1
- package/dist/elements/chunks/check-B5kBPKeo.js +0 -1
- package/dist/elements/chunks/chevron-down-DZ1GS39h.js +0 -1
- package/dist/elements/chunks/chevron-right-CvP7OUd2.js +0 -1
- package/dist/elements/chunks/choice-card-D4QUIOWZ.js +0 -1
- package/dist/elements/chunks/circle-CDf9g6MQ.js +0 -1
- package/dist/elements/chunks/circle-check-CA47Nqbz.js +0 -1
- package/dist/elements/chunks/circle-x-CJ_HVbC8.js +0 -1
- package/dist/elements/chunks/code-block-BdLiTitd.js +0 -1
- package/dist/elements/chunks/composer-Byve-n8K.js +0 -69
- package/dist/elements/chunks/confirm-card-B0flE3MN.js +0 -1
- package/dist/elements/chunks/controllable-DTwTIlwp.js +0 -1
- package/dist/elements/chunks/conversation-item-BDztezOG.js +0 -1
- package/dist/elements/chunks/copy-Nf-kqxXH.js +0 -1
- package/dist/elements/chunks/core-AYMC6_lb.js +0 -12
- package/dist/elements/chunks/css-M7EaDHN_.js +0 -1
- package/dist/elements/chunks/default-input-C2iLFq0b.js +0 -1
- package/dist/elements/chunks/define-wjHR1MkU.js +0 -1
- package/dist/elements/chunks/download-C4V7tBdb.js +0 -1
- package/dist/elements/chunks/embed-D3KewsIJ.js +0 -1
- package/dist/elements/chunks/engine-javascript-vq0WuIJl.js +0 -141
- package/dist/elements/chunks/file-text-D1G8khle.js +0 -1
- package/dist/elements/chunks/folder-WW2LGLid.js +0 -1
- package/dist/elements/chunks/form-CQirdC4W.js +0 -1
- package/dist/elements/chunks/github-dark-dimmed-DUshB20C.js +0 -1
- package/dist/elements/chunks/github-light-JYsPkUQd.js +0 -1
- package/dist/elements/chunks/hover-card-CJJkfHTK.js +0 -1
- package/dist/elements/chunks/html-CPZ3oZQ7.js +0 -1
- package/dist/elements/chunks/icon-Cfgmf4fa.js +0 -1
- package/dist/elements/chunks/info-DX3YMOt6.js +0 -1
- package/dist/elements/chunks/input-BtW9U6Sq.js +0 -1
- package/dist/elements/chunks/javascript-C25yR2R2.js +0 -1
- package/dist/elements/chunks/json-DxJze_jm.js +0 -1
- package/dist/elements/chunks/kbd-LGXwuT_V.js +0 -1
- package/dist/elements/chunks/link-2My69BqT.js +0 -1
- package/dist/elements/chunks/loader-iOxc_Tu-.js +0 -1
- package/dist/elements/chunks/message-VMZohrXx.js +0 -1
- package/dist/elements/chunks/message-circle-CpXMM-_n.js +0 -1
- package/dist/elements/chunks/message-square-DxcWtETQ.js +0 -1
- package/dist/elements/chunks/message-tZmV2A3p.js +0 -1
- package/dist/elements/chunks/paperclip-CRZVp4z_.js +0 -1
- package/dist/elements/chunks/progress-bar-DNy66e_E.js +0 -1
- package/dist/elements/chunks/prompt-suggestion-Bn64Nl7L.js +0 -1
- package/dist/elements/chunks/reasoning-Emn9N4-T.js +0 -1
- package/dist/elements/chunks/rotate-cw-CPWe37D9.js +0 -1
- package/dist/elements/chunks/scroll-area-ClZkZxIk.js +0 -1
- package/dist/elements/chunks/scroll-button-DPE-t3x_.js +0 -1
- package/dist/elements/chunks/share-CyfnNEqp.js +0 -1
- package/dist/elements/chunks/source-UWCvmVak.js +0 -1
- package/dist/elements/chunks/star-DoHFAikQ.js +0 -1
- package/dist/elements/chunks/store-Cjq0qwYN.js +0 -1
- package/dist/elements/chunks/svelte--5p79yCD.js +0 -1
- package/dist/elements/chunks/tasks-card-CmZ5xh6v.js +0 -1
- package/dist/elements/chunks/text-shimmer-DYn09zrz.js +0 -1
- package/dist/elements/chunks/textarea-skiGHURM.js +0 -1
- package/dist/elements/chunks/toast-store-CFnQpThX.js +0 -1
- package/dist/elements/chunks/tsx-B8rCNbgL.js +0 -1
- package/dist/elements/chunks/typescript-RycA9KXf.js +0 -1
- package/dist/elements/chunks/use-card-resolution-DoA2f_EU.js +0 -1
- package/dist/elements/chunks/vue-BmIZj4XD.js +0 -1
- package/dist/elements/chunks/x-B6UnOiLw.js +0 -1
- package/dist/llms/llms-full.txt +0 -2949
- package/dist/llms/llms.txt +0 -156
- package/dist/register-impl-DpE7icIb.js +0 -293
- package/dist/ui/stat.d.ts +0 -16
- package/src/ui/stat.tsx +0 -41
- /package/dist/{elements/chunks/card-tags-D8lZ-C_U.js → card-tags-D8lZ-C_U.js} +0 -0
- /package/dist/{elements/chunks/link-preview-DNILK391.js → link-preview-DNILK391.js} +0 -0
- /package/dist/{elements/chunks/slots-DaNjX0rc.js → slots-DaNjX0rc.js} +0 -0
|
@@ -0,0 +1,1337 @@
|
|
|
1
|
+
import { existsSync, readFileSync, rmSync, mkdirSync, writeFileSync, copyFileSync } from "node:fs";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { join, dirname, resolve } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
const SAFE_SCHEMES = ["http:", "https:", "mailto:"];
|
|
7
|
+
function schemeOf(url) {
|
|
8
|
+
try {
|
|
9
|
+
return new URL(url, "http://_invalid_base").protocol;
|
|
10
|
+
} catch {
|
|
11
|
+
return void 0;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function isSafeUrl(url) {
|
|
15
|
+
const scheme = schemeOf(url);
|
|
16
|
+
return scheme !== void 0 && SAFE_SCHEMES.includes(scheme);
|
|
17
|
+
}
|
|
18
|
+
const TAG_RE = /^[a-z][a-z0-9]*-[a-z0-9-]+$/;
|
|
19
|
+
const ProviderSchema = z.discriminatedUnion("mode", [
|
|
20
|
+
z.object({ mode: z.literal("mock") }).strict(),
|
|
21
|
+
z.object({
|
|
22
|
+
mode: z.literal("endpoint"),
|
|
23
|
+
/** The CONSUMER's chat route. Kit parses, consumer fetches. */
|
|
24
|
+
url: z.string().min(1),
|
|
25
|
+
wire: z.enum(["openai", "anthropic"])
|
|
26
|
+
}).strict()
|
|
27
|
+
]);
|
|
28
|
+
const ConstructSchema = z.object({
|
|
29
|
+
$schema: z.string().optional(),
|
|
30
|
+
/** The emitted tag: <acme-support>. Must satisfy customElements.define. */
|
|
31
|
+
name: z.string().regex(TAG_RE, 'must be a valid custom-element tag: lowercase, with a hyphen (e.g. "acme-support")'),
|
|
32
|
+
// Widened progressively: fullscreen/aside/split landed in Task 12, custom in Task 13.
|
|
33
|
+
layout: z.enum(["widget", "fullscreen", "aside", "split", "custom"]),
|
|
34
|
+
provider: ProviderSchema,
|
|
35
|
+
/** Plain (unsigned) identity passthrough — no signing/auth infra (owner
|
|
36
|
+
* ruling, 2026-08-26: signed JWT/HMAC identity is later additive
|
|
37
|
+
* vocabulary, not this field). TOP-LEVEL, not nested under `provider`:
|
|
38
|
+
* local history scoping (`capabilities.history.persistence: 'local'`)
|
|
39
|
+
* needs userId independent of provider mode — nesting it inside the
|
|
40
|
+
* provider union would make it silently inert for `mode: 'mock'` +
|
|
41
|
+
* local history, a real and common combination. Threaded as the
|
|
42
|
+
* `x-kai-user-id` header on every emitted fetch to the CONSUMER's own
|
|
43
|
+
* backend (the endpoint provider's chat POST, history's endpoint
|
|
44
|
+
* GET/PUT), and folded into the localStorage key for `local` history so
|
|
45
|
+
* different users on the same browser profile don't share one thread.
|
|
46
|
+
* Constrained to printable-ASCII (no CR/LF, no non-ISO-8859-1 code
|
|
47
|
+
* points) beyond the usual construct-authored-text escaping: this value
|
|
48
|
+
* reaches an HTTP HEADER VALUE, not just a JS string literal, and
|
|
49
|
+
* `fetch()` throws at RUNTIME on a header value containing CR/LF or
|
|
50
|
+
* outside ISO-8859-1 — a bad userId would surface as an opaque crash in
|
|
51
|
+
* the CONSUMER's generated app, not a construct-validation error. Reject
|
|
52
|
+
* loudly here instead, where the author gets a path + message. */
|
|
53
|
+
userId: z.string().min(1).regex(
|
|
54
|
+
/^[\x20-\x7E]+$/,
|
|
55
|
+
"must be printable ASCII with no line breaks (it becomes an HTTP header value)"
|
|
56
|
+
).optional(),
|
|
57
|
+
theme: z.object({
|
|
58
|
+
/** Any CSS color; becomes --kai-color-primary on the host. */
|
|
59
|
+
accent: z.string().optional(),
|
|
60
|
+
/** Any CSS color; becomes --kai-color-unread on the host (owner
|
|
61
|
+
* ruling, 2026-08-26 — the unread-indicator round). Same treatment
|
|
62
|
+
* as `accent` in every way that matters for safety: construct-
|
|
63
|
+
* authored/untrusted text, carried to the emitted facade via
|
|
64
|
+
* `ctx.element.style.setProperty('--kai-color-unread',
|
|
65
|
+
* JSON.stringify(...))` — never string-interpolated into CSS
|
|
66
|
+
* text — so it can never break out into a new declaration or rule
|
|
67
|
+
* (see emitElement's doc on why `accent` uses setProperty at all).
|
|
68
|
+
* Unlike `accent`, this has no paired -foreground to compute: the
|
|
69
|
+
* three surfaces that read --color-unread (the conversation-list
|
|
70
|
+
* row dot, the header toggle's badge, Dock's own closed-launcher
|
|
71
|
+
* badge) are all small filled dots with no text sitting ON them, so
|
|
72
|
+
* there is nothing to contrast-pair — the value is set and used
|
|
73
|
+
* as-is. */
|
|
74
|
+
unreadColor: z.string().optional(),
|
|
75
|
+
mode: z.enum(["light", "dark", "system"]).default("system")
|
|
76
|
+
}).strict().optional(),
|
|
77
|
+
/** Construct-wide header chrome, valid on every layout (not layout-scoped
|
|
78
|
+
* like `widget`, and not a capability toggle — a header is a construct-
|
|
79
|
+
* wide fact, like `theme`). Rendered inside ChatThread's own built-in
|
|
80
|
+
* header bar; a logo/icon projects through the kit's EXISTING
|
|
81
|
+
* `header-start` named slot (`slots` vocabulary above), not a second
|
|
82
|
+
* image-prop convention here. */
|
|
83
|
+
header: z.object({
|
|
84
|
+
/** Rendered in ChatThread's built-in header bar (left side). Construct-
|
|
85
|
+
* authored/untrusted text, like theme.accent/provider.url — JSON.stringify'd
|
|
86
|
+
* at its one emit site, never a raw JSX attribute string. */
|
|
87
|
+
title: z.string().min(1).optional()
|
|
88
|
+
}).strict().optional(),
|
|
89
|
+
/** Greeting shown while the thread is empty (no messages yet) — the
|
|
90
|
+
* proven "welcome screen" pattern (Intercom-class): title + optional
|
|
91
|
+
* description + optional icon above, with `capabilities.starters`'
|
|
92
|
+
* chips and the composer still rendering below (ChatThread's `empty`
|
|
93
|
+
* REPLACE slot only stands in for the empty MESSAGE LIST — see
|
|
94
|
+
* chat-thread.tsx's own doc comment on `empty` — so the chips are
|
|
95
|
+
* never lost). Construct-wide like `header`, not a capability: it's a
|
|
96
|
+
* fact about the empty state, not a toggleable affordance.
|
|
97
|
+
* `title`/`description` are construct-authored/untrusted text, like
|
|
98
|
+
* `header.title`/`theme.accent`/`provider.url` — JSON.stringify'd at
|
|
99
|
+
* their one emit site, never a raw JSX attribute string. `icon` is a
|
|
100
|
+
* URL reaching an `<img src>` sink in emitted code, exactly like
|
|
101
|
+
* `widget.launcherIcon` — same `isSafeUrl` policy, same superRefine
|
|
102
|
+
* shape, below. */
|
|
103
|
+
empty: z.object({
|
|
104
|
+
title: z.string().min(1),
|
|
105
|
+
description: z.string().min(1).optional(),
|
|
106
|
+
icon: z.string().min(1).optional()
|
|
107
|
+
}).strict().optional(),
|
|
108
|
+
/** Home screen (spec 2026-08-27, H-1..H-5): Intercom-style landing view behind
|
|
109
|
+
* a Home/Messages tab bar. PRESENCE of `home` enables the tab chrome (H-4) —
|
|
110
|
+
* no `enabled` boolean, like `header`/`empty`. Every sub-key optional;
|
|
111
|
+
* `home: {}` still means "tabs on, defaults". Never requires another
|
|
112
|
+
* capability (H-3): the recent card simply renders nothing without
|
|
113
|
+
* `capabilities.conversations` (the CLI warns — see cli.ts). All strings are
|
|
114
|
+
* construct-authored/untrusted: JSON.stringify'd at every emit site; hrefs
|
|
115
|
+
* and URL-shaped icons through isSafeUrl in superRefine below. */
|
|
116
|
+
home: z.object({
|
|
117
|
+
greeting: z.object({ title: z.string().min(1).optional(), subtitle: z.string().min(1).optional() }).strict().optional(),
|
|
118
|
+
recentConversation: z.literal(true).optional(),
|
|
119
|
+
newConversation: z.object({ label: z.string().min(1).optional() }).strict().optional(),
|
|
120
|
+
links: z.array(
|
|
121
|
+
z.object({
|
|
122
|
+
label: z.string().min(1),
|
|
123
|
+
href: z.string().min(1).optional(),
|
|
124
|
+
description: z.string().min(1).optional(),
|
|
125
|
+
/** renderIcon name (e.g. 'book-open') or a safe URL — the icon NAME
|
|
126
|
+
* list is renderIcon's own vocabulary, never restated here; unknown
|
|
127
|
+
* names warn loudly at DEV runtime. */
|
|
128
|
+
icon: z.string().min(1).optional()
|
|
129
|
+
}).strict()
|
|
130
|
+
).optional()
|
|
131
|
+
}).strict().optional(),
|
|
132
|
+
// Capability vocabulary, widened one field at a time by later tasks.
|
|
133
|
+
capabilities: z.object({
|
|
134
|
+
/** Starter prompts shown on the empty thread; clicking one sends it.
|
|
135
|
+
* 1-6 non-empty strings — construct-authored text, escaped like
|
|
136
|
+
* `theme.accent`/`provider.url` at every emit interpolation site. */
|
|
137
|
+
starters: z.array(z.string().min(1)).min(1).max(6).optional(),
|
|
138
|
+
/** Enables the paperclip attach affordance; accept is a non-empty
|
|
139
|
+
* list of media types/globs, e.g. ["image/*", "application/pdf"] —
|
|
140
|
+
* WHETHER stays with the construct author (this field), HOW stays
|
|
141
|
+
* with the kit (ChatThread's own attach/accept props, threaded
|
|
142
|
+
* through by codegen). */
|
|
143
|
+
attachments: z.object({
|
|
144
|
+
/** Accept-list of media types/globs, e.g. ["image/*", "application/pdf"]. */
|
|
145
|
+
accept: z.array(z.string().min(1)).min(1)
|
|
146
|
+
}).strict().optional(),
|
|
147
|
+
/** Conversation persistence. `none` (default, nothing emitted): the
|
|
148
|
+
* thread lives only in memory for the tab's lifetime. `local`:
|
|
149
|
+
* persisted to this browser's localStorage, keyed by the construct's
|
|
150
|
+
* tag — a mechanism decision (WHERE); what to retain and for how
|
|
151
|
+
* long stays an app decision (component-scope-boundary), so no
|
|
152
|
+
* retention count/quota lands here. `endpoint`: the CONSUMER's own
|
|
153
|
+
* thread route (GET returns ChatMessage[], PUT stores them) —
|
|
154
|
+
* requires `url`; `url` is rejected for any other persistence
|
|
155
|
+
* (superRefine below, both directions loud). */
|
|
156
|
+
history: z.object({
|
|
157
|
+
persistence: z.enum(["none", "local", "endpoint"]),
|
|
158
|
+
/** endpoint persistence only: the CONSUMER's thread routes (GET returns
|
|
159
|
+
* ChatMessage[], PUT stores them). Refined below. */
|
|
160
|
+
url: z.string().min(1).optional()
|
|
161
|
+
}).strict().optional(),
|
|
162
|
+
/** How the model's thinking (reasoning parts) renders. `'full'`
|
|
163
|
+
* (the default when omitted — see codegen.ts's emitReasoningProp)
|
|
164
|
+
* is the collapsible "Thinking" disclosure, shimmering while it
|
|
165
|
+
* streams. `'compact'` shows only a shimmer/typing loader while
|
|
166
|
+
* reasoning streams, with no expandable detail. `'off'` hides
|
|
167
|
+
* reasoning entirely. This is HOW an existing medium fact (the
|
|
168
|
+
* model's thinking) displays — the kit's call — so it maps straight
|
|
169
|
+
* onto ChatThread's own `reasoning` prop; there is no app-layer
|
|
170
|
+
* quota or retention decision hiding in it. Left `.optional()`
|
|
171
|
+
* rather than `.default('full')`, matching every sibling field in
|
|
172
|
+
* this object: a zod `.default()` here would make `reasoning`
|
|
173
|
+
* REQUIRED on the inferred input type (z.infer is the output type),
|
|
174
|
+
* breaking every `capabilities: {...}` object literal in this file
|
|
175
|
+
* and its tests that doesn't mention it. */
|
|
176
|
+
reasoning: z.enum(["full", "compact", "off"]).optional(),
|
|
177
|
+
/** Seeds the reasoning disclosure open AND keeps it tracking the
|
|
178
|
+
* stream (open while streaming, closes when it settles) — the
|
|
179
|
+
* pre-Task-19f `full` behavior. Default false/absent: the panel
|
|
180
|
+
* starts closed (just the "Thinking" shimmer chip) and only opens
|
|
181
|
+
* on click — the current default (owner ruling, 2026-08-26).
|
|
182
|
+
* Meaningless when `reasoning` is `'compact'` (no expandable
|
|
183
|
+
* content exists) or `'off'` (nothing renders); rejected on both,
|
|
184
|
+
* loudly, below. */
|
|
185
|
+
reasoningOpen: z.boolean().optional(),
|
|
186
|
+
/** Turns on the prior-conversations list (C-1..C-9 of the
|
|
187
|
+
* conversations design). `true` only — there is no `false` form,
|
|
188
|
+
* matching this schema's other presence-only capability flags.
|
|
189
|
+
* Requires `capabilities.history.persistence` to be `local` or
|
|
190
|
+
* `endpoint` (superRefine below, loud): a conversation list with
|
|
191
|
+
* nowhere to persist conversations is not a coherent construct.
|
|
192
|
+
* WHAT persists (this field, plus history) stays construct
|
|
193
|
+
* vocabulary; HOW it persists (the ConversationStore adapter,
|
|
194
|
+
* localStorage vs. a fetch endpoint) is codegen's call, never
|
|
195
|
+
* vocabulary here (C-3 — no transport-layer vocabulary). */
|
|
196
|
+
conversations: z.literal(true).optional()
|
|
197
|
+
}).strict().optional(),
|
|
198
|
+
/** Named generative-UI card definitions the model can emit as tool calls,
|
|
199
|
+
* rendered in the thread. Top-level (not a capability): a card is
|
|
200
|
+
* something the construct's model can DO at any point once a card is
|
|
201
|
+
* declared, not a thread-affordance toggle like starters/attachments/
|
|
202
|
+
* reasoning. Reuses the kit's own card-tool projection end to end
|
|
203
|
+
* (`cardTools`/`toOpenAITools`/`toAnthropicTools`, `@kitn.ai/ui/schemas`)
|
|
204
|
+
* — never a second one authored here. `schema` is validated structurally
|
|
205
|
+
* only; deep card validation (incl. `x-kai-format` mask hints) is the
|
|
206
|
+
* kit's own card contract at render time. */
|
|
207
|
+
cards: z.array(
|
|
208
|
+
z.object({
|
|
209
|
+
/** Tool-facing card name, e.g. "refund_approval". */
|
|
210
|
+
name: z.string().regex(/^[a-z][a-z0-9_]*$/),
|
|
211
|
+
/** The kit's card schema JSON (incl. x-kai-format mask hints).
|
|
212
|
+
* Validated structurally here; deep card validation is the kit's
|
|
213
|
+
* own card contract at render time. */
|
|
214
|
+
schema: z.record(z.string(), z.unknown())
|
|
215
|
+
}).strict()
|
|
216
|
+
).min(1).optional(),
|
|
217
|
+
/** Named `<slot>` projection points the emitted web component exposes —
|
|
218
|
+
* the format's ONLY escape hatch (named slots, no code-in-JSON). Each
|
|
219
|
+
* name must be a valid HTML slot-attribute value AND a legible
|
|
220
|
+
* identifier: kebab-case, starting with a letter (the same shape as a
|
|
221
|
+
* CSS custom-ident) — `slot name="Header!"` is rejected, not sanitized,
|
|
222
|
+
* matching the format's loud-rejection discipline everywhere else.
|
|
223
|
+
* 1-8 entries, no duplicates (superRefine below — a regex alone can't
|
|
224
|
+
* see across array entries). `layout: 'custom'` requires at least one
|
|
225
|
+
* declared slot (superRefine below): `custom` IS the slots grain — a
|
|
226
|
+
* `custom` layout with nothing to project into is not meaningfully
|
|
227
|
+
* different from `fullscreen`. */
|
|
228
|
+
slots: z.array(z.string().regex(/^[a-z][a-z0-9-]*$/, "slot names must be kebab-case, starting with a letter")).min(1).max(8).optional(),
|
|
229
|
+
/** Layout-scoped FAB chrome, `layout: 'widget'` only (superRefine below).
|
|
230
|
+
* Purely additive sibling to `layout` (widen-never-restructure) — mirrors
|
|
231
|
+
* Dock's own props verbatim, threaded through by codegen's emitLayoutOpen. */
|
|
232
|
+
widget: z.object({
|
|
233
|
+
/** Which corner. Mirrors Dock's own DockPosition enum verbatim — not a
|
|
234
|
+
* new left/right binary. Omitted keeps Dock's own default (bottom-end). */
|
|
235
|
+
position: z.enum(["bottom-end", "bottom-start", "top-end", "top-start"]).optional(),
|
|
236
|
+
/** An image URL for the closed-state launcher glyph, replacing Dock's
|
|
237
|
+
* built-in chat-bubble icon. Construct-authored/untrusted text, like
|
|
238
|
+
* theme.accent and provider.url — escaped the same way at its one emit site. */
|
|
239
|
+
launcherIcon: z.string().min(1).optional(),
|
|
240
|
+
/** Seed Dock's open state at mount. Uncontrolled — never steals focus (Dock's
|
|
241
|
+
* own focus contract, dock.tsx). Omitted keeps Dock's own default (closed). */
|
|
242
|
+
defaultOpen: z.boolean().optional()
|
|
243
|
+
}).strict().optional()
|
|
244
|
+
}).strict().superRefine((construct, ctx) => {
|
|
245
|
+
if (construct.slots) {
|
|
246
|
+
const seen = /* @__PURE__ */ new Set();
|
|
247
|
+
construct.slots.forEach((name, i) => {
|
|
248
|
+
if (seen.has(name)) {
|
|
249
|
+
ctx.addIssue({
|
|
250
|
+
code: z.ZodIssueCode.custom,
|
|
251
|
+
path: ["slots", i],
|
|
252
|
+
message: `duplicate slot name "${name}"`
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
seen.add(name);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (construct.layout === "custom" && (!construct.slots || construct.slots.length === 0)) {
|
|
259
|
+
ctx.addIssue({
|
|
260
|
+
code: z.ZodIssueCode.custom,
|
|
261
|
+
path: ["slots"],
|
|
262
|
+
message: '"custom" layout requires at least one declared slot — custom IS the slots grain'
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
if (construct.layout === "split" && construct.slots) {
|
|
266
|
+
const i = construct.slots.indexOf("pane");
|
|
267
|
+
if (i !== -1) {
|
|
268
|
+
ctx.addIssue({
|
|
269
|
+
code: z.ZodIssueCode.custom,
|
|
270
|
+
path: ["slots", i],
|
|
271
|
+
message: `"pane" collides with the "split" layout's own fixed <slot name="pane"> — choose a different slot name`
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (construct.widget && construct.layout !== "widget") {
|
|
276
|
+
ctx.addIssue({
|
|
277
|
+
code: z.ZodIssueCode.custom,
|
|
278
|
+
path: ["widget"],
|
|
279
|
+
message: '"widget" is only valid on layout: "widget"'
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (construct.widget?.launcherIcon && !isSafeUrl(construct.widget.launcherIcon)) {
|
|
283
|
+
ctx.addIssue({
|
|
284
|
+
code: z.ZodIssueCode.custom,
|
|
285
|
+
path: ["widget", "launcherIcon"],
|
|
286
|
+
message: "launcherIcon must be an http(s)/mailto or relative URL — no javascript:/data: schemes"
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (construct.empty?.icon && !isSafeUrl(construct.empty.icon)) {
|
|
290
|
+
ctx.addIssue({
|
|
291
|
+
code: z.ZodIssueCode.custom,
|
|
292
|
+
path: ["empty", "icon"],
|
|
293
|
+
message: "icon must be an http(s)/mailto or relative URL — no javascript:/data: schemes"
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
const reasoning = construct.capabilities?.reasoning;
|
|
297
|
+
const reasoningOpen = construct.capabilities?.reasoningOpen;
|
|
298
|
+
if (reasoningOpen !== void 0 && (reasoning === "compact" || reasoning === "off")) {
|
|
299
|
+
ctx.addIssue({
|
|
300
|
+
code: z.ZodIssueCode.custom,
|
|
301
|
+
path: ["capabilities", "reasoningOpen"],
|
|
302
|
+
message: '"reasoningOpen" only applies when reasoning is "full" or omitted — "compact"/"off" have no disclosure to open'
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const history = construct.capabilities?.history;
|
|
306
|
+
if (construct.capabilities?.conversations && (!history || history.persistence === "none")) {
|
|
307
|
+
ctx.addIssue({
|
|
308
|
+
code: z.ZodIssueCode.custom,
|
|
309
|
+
path: ["capabilities", "conversations"],
|
|
310
|
+
message: '"conversations" requires capabilities.history.persistence to be "local" or "endpoint" — a conversation list needs somewhere to persist conversations'
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const URL_SHAPED = /^[a-zA-Z][a-zA-Z0-9+.-]*:|^\/\//;
|
|
314
|
+
for (const [i, link] of (construct.home?.links ?? []).entries()) {
|
|
315
|
+
if (link.href && !isSafeUrl(link.href)) {
|
|
316
|
+
ctx.addIssue({
|
|
317
|
+
code: z.ZodIssueCode.custom,
|
|
318
|
+
path: ["home", "links", i, "href"],
|
|
319
|
+
message: "href must be an http(s)/mailto or relative URL — no javascript:/data: schemes"
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
if (link.icon && URL_SHAPED.test(link.icon) && !isSafeUrl(link.icon)) {
|
|
323
|
+
ctx.addIssue({
|
|
324
|
+
code: z.ZodIssueCode.custom,
|
|
325
|
+
path: ["home", "links", i, "icon"],
|
|
326
|
+
message: "icon must be a kit icon name or an http(s)/relative URL — no javascript:/data: schemes"
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (!history) return;
|
|
331
|
+
if (history.persistence === "endpoint" && !history.url) {
|
|
332
|
+
ctx.addIssue({
|
|
333
|
+
code: z.ZodIssueCode.custom,
|
|
334
|
+
path: ["capabilities", "history", "url"],
|
|
335
|
+
message: '"endpoint" persistence requires a url'
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (history.persistence !== "endpoint" && history.url !== void 0) {
|
|
339
|
+
ctx.addIssue({
|
|
340
|
+
code: z.ZodIssueCode.custom,
|
|
341
|
+
path: ["capabilities", "history", "url"],
|
|
342
|
+
message: 'url is only valid with "endpoint" persistence'
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
function validateConstruct(input) {
|
|
347
|
+
const parsed = ConstructSchema.safeParse(input);
|
|
348
|
+
if (parsed.success) return { ok: true, construct: parsed.data };
|
|
349
|
+
return {
|
|
350
|
+
ok: false,
|
|
351
|
+
problems: parsed.error.issues.flatMap((issue) => {
|
|
352
|
+
if (issue.code === "unrecognized_keys") {
|
|
353
|
+
return issue.keys.map((key) => ({
|
|
354
|
+
path: [...issue.path.map(String), key].join("."),
|
|
355
|
+
message: `"${key}" is not construct vocabulary`
|
|
356
|
+
}));
|
|
357
|
+
}
|
|
358
|
+
return [
|
|
359
|
+
{
|
|
360
|
+
path: issue.path.map(String).join("."),
|
|
361
|
+
message: issue.message
|
|
362
|
+
}
|
|
363
|
+
];
|
|
364
|
+
})
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function kitVersion() {
|
|
368
|
+
const require2 = createRequire(import.meta.url);
|
|
369
|
+
const pkg = require2("@kitn.ai/ui/package.json");
|
|
370
|
+
return pkg.version;
|
|
371
|
+
}
|
|
372
|
+
const themeMode = (c) => c.theme?.mode === "light" ? "light" : c.theme?.mode === "dark" ? "dark" : "auto";
|
|
373
|
+
function srgbChannelToLinear(c) {
|
|
374
|
+
const cs = c / 255;
|
|
375
|
+
return cs <= 0.03928 ? cs / 12.92 : Math.pow((cs + 0.055) / 1.055, 2.4);
|
|
376
|
+
}
|
|
377
|
+
function relativeLuminance(r, g, b) {
|
|
378
|
+
return 0.2126 * srgbChannelToLinear(r) + 0.7152 * srgbChannelToLinear(g) + 0.0722 * srgbChannelToLinear(b);
|
|
379
|
+
}
|
|
380
|
+
function hslToRgb(h, s, l) {
|
|
381
|
+
const hue = (h % 360 + 360) % 360;
|
|
382
|
+
const chroma = (1 - Math.abs(2 * l - 1)) * s;
|
|
383
|
+
const x = chroma * (1 - Math.abs(hue / 60 % 2 - 1));
|
|
384
|
+
const m = l - chroma / 2;
|
|
385
|
+
const [r1, g1, b1] = hue < 60 ? [chroma, x, 0] : hue < 120 ? [x, chroma, 0] : hue < 180 ? [0, chroma, x] : hue < 240 ? [0, x, chroma] : hue < 300 ? [x, 0, chroma] : [chroma, 0, x];
|
|
386
|
+
return [Math.round((r1 + m) * 255), Math.round((g1 + m) * 255), Math.round((b1 + m) * 255)];
|
|
387
|
+
}
|
|
388
|
+
function parseAccentRgb(accent) {
|
|
389
|
+
const s = accent.trim();
|
|
390
|
+
const hex3 = /^#([0-9a-fA-F]{3})$/.exec(s);
|
|
391
|
+
if (hex3) {
|
|
392
|
+
const [r, g, b] = hex3[1].split("").map((ch) => parseInt(ch + ch, 16));
|
|
393
|
+
return [r, g, b];
|
|
394
|
+
}
|
|
395
|
+
const hex6 = /^#([0-9a-fA-F]{6})([0-9a-fA-F]{2})?$/.exec(s);
|
|
396
|
+
if (hex6) {
|
|
397
|
+
const hex = hex6[1];
|
|
398
|
+
return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
|
|
399
|
+
}
|
|
400
|
+
const rgbFn = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*[\d.]+\s*)?\)$/.exec(s);
|
|
401
|
+
if (rgbFn) {
|
|
402
|
+
const [r, g, b] = [rgbFn[1], rgbFn[2], rgbFn[3]].map(Number);
|
|
403
|
+
return [r, g, b].every((v) => v >= 0 && v <= 255) ? [r, g, b] : null;
|
|
404
|
+
}
|
|
405
|
+
const hslFn = /^hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%\s*(?:,\s*[\d.]+\s*)?\)$/.exec(s);
|
|
406
|
+
if (hslFn) {
|
|
407
|
+
return hslToRgb(Number(hslFn[1]), Number(hslFn[2]) / 100, Number(hslFn[3]) / 100);
|
|
408
|
+
}
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
function resolveContrastForeground(accent) {
|
|
412
|
+
const rgb = parseAccentRgb(accent);
|
|
413
|
+
if (!rgb) return null;
|
|
414
|
+
const luminance = relativeLuminance(...rgb);
|
|
415
|
+
return luminance <= 0.5 ? "#ffffff" : "#000000";
|
|
416
|
+
}
|
|
417
|
+
function commentSafe(text) {
|
|
418
|
+
return text.replace(/[\r\n]/g, " ").replace(/\*\//g, "* /");
|
|
419
|
+
}
|
|
420
|
+
const CONTRAST_COLOR_SUPPORTS = "@supports (color: contrast-color(red))";
|
|
421
|
+
function accentContrastNotice(construct) {
|
|
422
|
+
const accent = construct.theme?.accent;
|
|
423
|
+
if (!accent || resolveContrastForeground(accent) !== null) return null;
|
|
424
|
+
return `accent '${commentSafe(accent)}' not parseable for contrast; foreground left at theme default in browsers without CSS contrast-color() support`;
|
|
425
|
+
}
|
|
426
|
+
function generateProject(construct, opts = {}) {
|
|
427
|
+
const uiSpec = opts.uiSpec ?? `^${kitVersion()}`;
|
|
428
|
+
const files = [
|
|
429
|
+
{ path: "package.json", code: emitPackageJson(construct, uiSpec) },
|
|
430
|
+
{ path: "tsconfig.json", code: emitTsconfig() },
|
|
431
|
+
{ path: "vite.config.ts", code: emitViteDev() },
|
|
432
|
+
{ path: "vite.config.lib.ts", code: emitViteLib(construct) },
|
|
433
|
+
{ path: "index.html", code: emitIndexHtml(construct) },
|
|
434
|
+
{ path: "src/element.tsx", code: emitElement(construct) },
|
|
435
|
+
{ path: "src/App.tsx", code: emitApp(construct) }
|
|
436
|
+
];
|
|
437
|
+
if (construct.cards) files.push({ path: "src/cards.ts", code: emitCardsRegistry(construct.cards) });
|
|
438
|
+
return files;
|
|
439
|
+
}
|
|
440
|
+
function emitCardsRegistry(cards) {
|
|
441
|
+
const entries = cards.map((card) => ` ${card.name}: ${JSON.stringify(card.schema, null, 2).split("\n").join("\n ")},`).join("\n");
|
|
442
|
+
return `// src/cards.ts — the construct's card registry, verbatim from the construct.
|
|
443
|
+
// Tool definitions for YOUR backend derive from this same object via
|
|
444
|
+
// @kitn.ai/ui/schemas (cardTools / toOpenAITools / toAnthropicTools) — one
|
|
445
|
+
// projection, shared with the kit.
|
|
446
|
+
export const cards = {
|
|
447
|
+
${entries}
|
|
448
|
+
} as const;
|
|
449
|
+
`;
|
|
450
|
+
}
|
|
451
|
+
function emitCardComponentImport(c) {
|
|
452
|
+
return c.cards ? ", BUILTIN_CARD_COMPONENTS" : "";
|
|
453
|
+
}
|
|
454
|
+
function emitCardsImport(c) {
|
|
455
|
+
if (!c.cards) return "";
|
|
456
|
+
const toolsImport = c.provider.mode === "endpoint" ? c.provider.wire === "openai" ? ", toOpenAITools" : ", toAnthropicTools" : "";
|
|
457
|
+
return `import { cards } from './cards';
|
|
458
|
+
// Generative-UI cards, v1: every declared card renders as the kit's own
|
|
459
|
+
// schema-driven FORM (BUILTIN_CARD_COMPONENTS.form, components/form.tsx) — it
|
|
460
|
+
// walks the card's JSON Schema into real input fields, honoring
|
|
461
|
+
// x-kai-format/x-kai-mask/x-kai-mask-guide hints itself. ChatThread's own
|
|
462
|
+
// MessageBody already matches \`part.type === 'card'\` in its part rendering and
|
|
463
|
+
// draws it with the kit's own \`CardRenderer\` (components/card-renderer.tsx),
|
|
464
|
+
// which picks the component from \`cardTypes\` (below) by envelope.type — so
|
|
465
|
+
// there is nothing to hand-compose beyond that one map. Turning a model's tool
|
|
466
|
+
// call into that renderable part is \`cardFromToolCall\` (the inverse of
|
|
467
|
+
// \`cardTools\`), applied once per settled turn below; its data is then replaced
|
|
468
|
+
// with the DECLARED card schema (not the model's call arguments) — the fields
|
|
469
|
+
// on screen are the construct's own vocabulary, not whatever shape a model
|
|
470
|
+
// happened to send.
|
|
471
|
+
//
|
|
472
|
+
// UPDATE (CD-1, owner ruling 2026-08-26, Task 19g): the field SHAPE (title/
|
|
473
|
+
// type/widget/validation) still comes from the construct's own declared
|
|
474
|
+
// schema, never the model's — that part is unchanged. But discarding the
|
|
475
|
+
// model's call arguments wholesale also threw away any VALUE it wanted to
|
|
476
|
+
// pre-fill, breaking "model proposes, user confirms" (kai_refund_approval
|
|
477
|
+
// {amount:50} rendered an empty form). So the model's args are now
|
|
478
|
+
// shallow-merged onto the declared schema's field \`default\`s below
|
|
479
|
+
// (mergeToolArgsIntoFormDefaults) before the card is added — see
|
|
480
|
+
// emitApplyCardTools.
|
|
481
|
+
import { cardFromToolCall${toolsImport} } from '@kitn.ai/ui/schemas';
|
|
482
|
+
|
|
483
|
+
// Every declared card name routes to the SAME form renderer — cardFromToolCall
|
|
484
|
+
// makes envelope.type equal the card's own name (kai_refund_approval ->
|
|
485
|
+
// 'refund_approval'), and CardRenderer resolves a type's component from this
|
|
486
|
+
// map.
|
|
487
|
+
//
|
|
488
|
+
// Deliberately NOT also wiring ChatThread's \`cardSchemas\` prop to this
|
|
489
|
+
// registry below. That prop validates envelope.data AGAINST the named schema,
|
|
490
|
+
// and this card's data IS \`cards[name]\` itself (see emitApplyCardTools) — the
|
|
491
|
+
// construct's declared field schema, not values shaped like it. Wiring it as
|
|
492
|
+
// its own validator asks "does this FormDefinition itself have an \`amount\`
|
|
493
|
+
// key" and a well-formed FormDefinition never does, so every card would
|
|
494
|
+
// render the HARD validation-failure fallback instead of the form (caught
|
|
495
|
+
// live: eject + kai dev showed exactly that "(root).amount: required"
|
|
496
|
+
// failure before this comment existed). The construct's own schema.ts
|
|
497
|
+
// already checks \`cards\` structurally at validate time; there is nothing
|
|
498
|
+
// left for a second, self-referential check here to catch.
|
|
499
|
+
const cardTypes = Object.fromEntries(Object.keys(cards).map((name) => [name, BUILTIN_CARD_COMPONENTS.form] as const));
|
|
500
|
+
|
|
501
|
+
// CD-1 (owner ruling 2026-08-26, Task 19g): shallow-merge a model's card
|
|
502
|
+
// tool-call args onto a DECLARED form schema's field defaults — "model
|
|
503
|
+
// proposes, user confirms". Only top-level keys the args AND the schema both
|
|
504
|
+
// name are touched; the schema's own field shape (title/type/widget/
|
|
505
|
+
// validation) is never altered, and a key the model sent that isn't a
|
|
506
|
+
// declared field is ignored (the construct's vocabulary wins, not the
|
|
507
|
+
// model's). One level deep only — a nested object field's own defaults are
|
|
508
|
+
// not recursed into; no evidence of need yet (vocabulary-on-evidence).
|
|
509
|
+
function mergeToolArgsIntoFormDefaults(
|
|
510
|
+
schema: Record<string, unknown> & { properties?: Record<string, unknown> },
|
|
511
|
+
args: Record<string, unknown>,
|
|
512
|
+
): Record<string, unknown> {
|
|
513
|
+
const declared = schema.properties;
|
|
514
|
+
if (!declared || typeof declared !== 'object') return schema;
|
|
515
|
+
const patched: Record<string, unknown> = {};
|
|
516
|
+
for (const [key, value] of Object.entries(args)) {
|
|
517
|
+
if (!(key in declared)) continue;
|
|
518
|
+
patched[key] = { ...(declared[key] as Record<string, unknown>), default: value };
|
|
519
|
+
}
|
|
520
|
+
return { ...schema, properties: { ...schema.properties, ...patched } };
|
|
521
|
+
}`;
|
|
522
|
+
}
|
|
523
|
+
function emitCardTypesProp(c) {
|
|
524
|
+
return c.cards ? " cardTypes={cardTypes}" : "";
|
|
525
|
+
}
|
|
526
|
+
function emitApplyCardTools(c) {
|
|
527
|
+
if (!c.cards) return "";
|
|
528
|
+
return `
|
|
529
|
+
for (const part of chat.messages().find((m) => m.id === stream.id)?.parts ?? []) {
|
|
530
|
+
if (part.type !== 'tool' || part.tool.state !== 'input-available') continue;
|
|
531
|
+
const card = cardFromToolCall(part.tool.type, part.tool.input, { id: part.tool.toolCallId ?? crypto.randomUUID() });
|
|
532
|
+
if (card && card.type in cards) {
|
|
533
|
+
const declared = cards[card.type as keyof typeof cards];
|
|
534
|
+
const args = card.data as Record<string, unknown> | undefined;
|
|
535
|
+
const merged = args && typeof args === 'object'
|
|
536
|
+
? mergeToolArgsIntoFormDefaults(declared, args)
|
|
537
|
+
: declared;
|
|
538
|
+
stream.addCard({ ...card, data: merged });
|
|
539
|
+
}
|
|
540
|
+
}`;
|
|
541
|
+
}
|
|
542
|
+
function emitToolsField(c) {
|
|
543
|
+
if (!c.cards || c.provider.mode !== "endpoint") return "";
|
|
544
|
+
const toolsFn = c.provider.wire === "openai" ? "toOpenAITools" : "toAnthropicTools";
|
|
545
|
+
return `, tools: ${toolsFn}(cards)`;
|
|
546
|
+
}
|
|
547
|
+
function emitPackageJson(c, uiSpec) {
|
|
548
|
+
return `${JSON.stringify(
|
|
549
|
+
{
|
|
550
|
+
name: c.name,
|
|
551
|
+
private: true,
|
|
552
|
+
type: "module",
|
|
553
|
+
scripts: {
|
|
554
|
+
dev: "vite",
|
|
555
|
+
build: "vite build --config vite.config.lib.ts",
|
|
556
|
+
typecheck: "tsc --noEmit"
|
|
557
|
+
},
|
|
558
|
+
dependencies: {
|
|
559
|
+
"@kitn.ai/ui": uiSpec,
|
|
560
|
+
"solid-js": "^1.9.0"
|
|
561
|
+
},
|
|
562
|
+
devDependencies: {
|
|
563
|
+
typescript: "^5.6.0",
|
|
564
|
+
vite: "^6.0.0",
|
|
565
|
+
"vite-plugin-solid": "^2.11.0"
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
null,
|
|
569
|
+
2
|
|
570
|
+
)}
|
|
571
|
+
`;
|
|
572
|
+
}
|
|
573
|
+
function emitTsconfig() {
|
|
574
|
+
return `${JSON.stringify(
|
|
575
|
+
{
|
|
576
|
+
compilerOptions: {
|
|
577
|
+
target: "ES2022",
|
|
578
|
+
module: "ESNext",
|
|
579
|
+
moduleResolution: "bundler",
|
|
580
|
+
jsx: "preserve",
|
|
581
|
+
jsxImportSource: "solid-js",
|
|
582
|
+
strict: true,
|
|
583
|
+
noUnusedLocals: true,
|
|
584
|
+
skipLibCheck: true,
|
|
585
|
+
types: ["vite/client"]
|
|
586
|
+
},
|
|
587
|
+
include: ["src"]
|
|
588
|
+
},
|
|
589
|
+
null,
|
|
590
|
+
2
|
|
591
|
+
)}
|
|
592
|
+
`;
|
|
593
|
+
}
|
|
594
|
+
function emitViteDev() {
|
|
595
|
+
return `import { defineConfig } from 'vite';
|
|
596
|
+
import solid from 'vite-plugin-solid';
|
|
597
|
+
|
|
598
|
+
export default defineConfig({ plugins: [solid()] });
|
|
599
|
+
`;
|
|
600
|
+
}
|
|
601
|
+
function emitViteLib(c) {
|
|
602
|
+
return `import { defineConfig } from 'vite';
|
|
603
|
+
import solid from 'vite-plugin-solid';
|
|
604
|
+
|
|
605
|
+
// kai compile: ONE self-registering .js. Everything is inlined (no externals):
|
|
606
|
+
// the consumer installs nothing but this output.
|
|
607
|
+
export default defineConfig({
|
|
608
|
+
plugins: [solid()],
|
|
609
|
+
build: {
|
|
610
|
+
lib: { entry: 'src/element.tsx', formats: ['es'], fileName: () => '${c.name}.js' },
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
`;
|
|
614
|
+
}
|
|
615
|
+
function emitIndexHtml(c) {
|
|
616
|
+
const hint = c.layout === "widget" ? `
|
|
617
|
+
<p style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); margin: 0; color: #94a3b8; font: 14px system-ui, sans-serif; text-align: center; max-width: 28rem; padding: 0 1rem;">This blank page stands in for your site. The chat widget is in the bottom-right corner.</p>` : "";
|
|
618
|
+
const slotDemo = (c.slots ?? []).map(
|
|
619
|
+
(name) => `
|
|
620
|
+
<div slot="${name}" style="padding: 0.5rem 1rem; font: 13px system-ui, sans-serif; color: #64748b;">Projected into slot "${name}" — replace with your own markup.</div>`
|
|
621
|
+
).join("");
|
|
622
|
+
const body = slotDemo ? `
|
|
623
|
+
<${c.name}>${slotDemo}
|
|
624
|
+
</${c.name}>` : `
|
|
625
|
+
<${c.name}></${c.name}>`;
|
|
626
|
+
return `<!doctype html>
|
|
627
|
+
<html lang="en">
|
|
628
|
+
<head>
|
|
629
|
+
<meta charset="utf-8" />
|
|
630
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
631
|
+
<title>${c.name} — construct preview</title>
|
|
632
|
+
</head>
|
|
633
|
+
<body style="margin: 0;">${hint}${body}
|
|
634
|
+
<script type="module" src="/src/element.tsx"><\/script>
|
|
635
|
+
</body>
|
|
636
|
+
</html>
|
|
637
|
+
`;
|
|
638
|
+
}
|
|
639
|
+
function emitElement(c) {
|
|
640
|
+
const accent = c.theme?.accent;
|
|
641
|
+
const unreadColor = c.theme?.unreadColor;
|
|
642
|
+
if (!accent && !unreadColor) {
|
|
643
|
+
const facade2 = "() => <App />";
|
|
644
|
+
return `import { defineWebComponent } from '@kitn.ai/ui/define';
|
|
645
|
+
import { App } from './App';
|
|
646
|
+
|
|
647
|
+
// The one facade. Interior stays pure Solid (no nested element registrations);
|
|
648
|
+
// the kit CSS is injected into the shadow root by defineWebComponent itself.
|
|
649
|
+
defineWebComponent('${c.name}', { theme: '${themeMode(c)}' as 'light' | 'dark' | 'auto' }, ${facade2});
|
|
650
|
+
`;
|
|
651
|
+
}
|
|
652
|
+
const unreadColorSetProperty = unreadColor ? `
|
|
653
|
+
ctx.element.style.setProperty('--kai-color-unread', ${JSON.stringify(unreadColor)});` : "";
|
|
654
|
+
if (!accent) {
|
|
655
|
+
const facade2 = `(_props, ctx) => {${unreadColorSetProperty}
|
|
656
|
+
return <App />;
|
|
657
|
+
}`;
|
|
658
|
+
return `import { defineWebComponent } from '@kitn.ai/ui/define';
|
|
659
|
+
import { App } from './App';
|
|
660
|
+
|
|
661
|
+
// The one facade. Interior stays pure Solid (no nested element registrations);
|
|
662
|
+
// the kit CSS is injected into the shadow root by defineWebComponent itself.
|
|
663
|
+
defineWebComponent('${c.name}', { theme: '${themeMode(c)}' as 'light' | 'dark' | 'auto' }, ${facade2});
|
|
664
|
+
`;
|
|
665
|
+
}
|
|
666
|
+
const foreground = resolveContrastForeground(accent);
|
|
667
|
+
const foregroundCss = foreground !== null ? `:host { --kai-color-primary-foreground: ${foreground}; }
|
|
668
|
+
` : (
|
|
669
|
+
// Not guessed at: an unparseable accent (var(), a named color, a
|
|
670
|
+
// color-mix()/oklch() call, …) leaves NO base declaration, so the
|
|
671
|
+
// kit's own theme default stands — except in a browser new enough to
|
|
672
|
+
// resolve contrast-color() itself, where the @supports block below
|
|
673
|
+
// still gets it right natively.
|
|
674
|
+
`/* NOTICE: accent '${commentSafe(accent)}' not parseable for contrast at generation time; the paired foreground falls back to the theme default in browsers without CSS contrast-color() support. */
|
|
675
|
+
`
|
|
676
|
+
);
|
|
677
|
+
const styleText = foregroundCss + `${CONTRAST_COLOR_SUPPORTS} {
|
|
678
|
+
:host { --kai-color-primary-foreground: contrast-color(var(--kai-color-primary)); }
|
|
679
|
+
}`;
|
|
680
|
+
const facade = `(_props, ctx) => {
|
|
681
|
+
ctx.element.style.setProperty('--kai-color-primary', ${JSON.stringify(accent)});${unreadColorSetProperty}
|
|
682
|
+
return (
|
|
683
|
+
<>
|
|
684
|
+
<style>{${JSON.stringify(styleText)}}</style>
|
|
685
|
+
<App />
|
|
686
|
+
</>
|
|
687
|
+
);
|
|
688
|
+
}`;
|
|
689
|
+
return `import { defineWebComponent } from '@kitn.ai/ui/define';
|
|
690
|
+
import { App } from './App';
|
|
691
|
+
|
|
692
|
+
// The one facade. Interior stays pure Solid (no nested element registrations);
|
|
693
|
+
// the kit CSS is injected into the shadow root by defineWebComponent itself.
|
|
694
|
+
defineWebComponent('${c.name}', { theme: '${themeMode(c)}' as 'light' | 'dark' | 'auto' }, ${facade});
|
|
695
|
+
`;
|
|
696
|
+
}
|
|
697
|
+
function emitApp(c) {
|
|
698
|
+
if (c.layout === "custom") return emitCustomApp(c);
|
|
699
|
+
return `${emitSolidJsImport(c)}${emitConversationsSignalsImport(c)}import { ChatThread, createKaiChat${emitLayoutImport(c)}${emitCardComponentImport(c)}${emitEmptyComponentImport(c)}${emitHeaderCloseImport(c)} } from '@kitn.ai/ui/solid';
|
|
700
|
+
import type { AttachmentData${emitHistoryTypeImport(c)}${emitConversationsResetTypeImport(c)} } from '@kitn.ai/ui/solid';
|
|
701
|
+
${emitProviderImports(c)}
|
|
702
|
+
${emitCardsImport(c)}
|
|
703
|
+
${emitConversationsImport(c)}
|
|
704
|
+
|
|
705
|
+
${emitProviderSetup(c)}
|
|
706
|
+
${emitHistorySetup(c)}
|
|
707
|
+
|
|
708
|
+
// ChatThread is the kit's own MOST-INTEGRATED chat surface — the same
|
|
709
|
+
// composition <kai-chat>'s facade renders (src/elements/chat.tsx). It owns
|
|
710
|
+
// the message list, the composer (padding, focus ring, the send button) and
|
|
711
|
+
// their layout AS ONE UNIT, so nothing here re-derives spacing, alignment or
|
|
712
|
+
// focus styling by hand: every prior version of this file that hand-composed
|
|
713
|
+
// Thread + PromptInput + Button was restating layout the kit already owns,
|
|
714
|
+
// and every visual defect the owner hit (flush composer, a clipped focus
|
|
715
|
+
// ring) traced back to that restatement. Composing ChatThread directly
|
|
716
|
+
// leaves NOTHING here to restate it with.
|
|
717
|
+
//
|
|
718
|
+
// Capability gating (format rule: an undeclared capability's affordance must
|
|
719
|
+
// be OFF). The construct schema carries ONE capability field so far
|
|
720
|
+
// (capabilities.starters, Task 8) — every other affordance below is gated to
|
|
721
|
+
// "off" unconditionally, not per-construct, until there's a field to gate ON.
|
|
722
|
+
// - webSearch / voice: real ChatThreadProps booleans, default OFF when
|
|
723
|
+
// omitted — set to \`false\` explicitly rather than left implicit, so the
|
|
724
|
+
// gating decision is visible in the emitted source, not just inferred
|
|
725
|
+
// from an absent prop.
|
|
726
|
+
// - suggestions: ChatThread ALREADY owns starter prompts end to end — its
|
|
727
|
+
// own \`suggestions\` prop renders the chips, hides them once
|
|
728
|
+
// \`messages\` is non-empty, and (default \`suggestionMode="submit"\`)
|
|
729
|
+
// calls \`onSubmit\` with the clicked text exactly like a typed submit.
|
|
730
|
+
// So capabilities.starters threads straight into that prop; there is
|
|
731
|
+
// nothing to hand-compose. Omitted (undefined) when no starters are
|
|
732
|
+
// declared, same off-by-default effect as the booleans above.
|
|
733
|
+
// - models: omitted (undefined) — no model switcher; no capabilities field yet.
|
|
734
|
+
// - attachments (the paperclip): gated via ChatThread's \`attach\`/\`accept\`
|
|
735
|
+
// props (kit gap closed — ChatThread forwards both to DefaultPromptInput,
|
|
736
|
+
// mirroring webSearch/voice). ChatThread ALREADY owns the whole
|
|
737
|
+
// round-trip end to end — the paperclip button, staged previews, staging
|
|
738
|
+
// each file as a data URI (never a blob object URL; see
|
|
739
|
+
// AttachmentData.url's doc in components/attachment-types.ts), and
|
|
740
|
+
// handing the staged list back via onSubmit's \`attachments\` — and its
|
|
741
|
+
// Message component ALREADY groups consecutive file parts into one
|
|
742
|
+
// attachment row (message.tsx). So there is nothing to hand-compose
|
|
743
|
+
// here, same lesson as suggestions above: hand-rolling a second picker or
|
|
744
|
+
// a second file-part renderer would restate what ChatThread/Message
|
|
745
|
+
// already own. capabilities.attachments threads straight into
|
|
746
|
+
// attach/accept; the only App.tsx-owned piece is folding the picked
|
|
747
|
+
// attachments into the outgoing message's parts at the submit site
|
|
748
|
+
// (see emitProviderSetup) since createKaiChat's own append/streamAssistant
|
|
749
|
+
// ops don't do that folding themselves.
|
|
750
|
+
// - reasoning: gated via ChatThread's own \`reasoning\` prop (kit gap closed
|
|
751
|
+
// — ChatThread forwards it to every MessageBody as \`reasoningMode\`,
|
|
752
|
+
// mirroring attach/accept). \`'full'\` is both the schema default and
|
|
753
|
+
// ChatThread's own default, so it and an absent field emit no prop at
|
|
754
|
+
// all — the SAME off-by-default convention as every other capability
|
|
755
|
+
// here, just anchored on the medium's existing default instead of an
|
|
756
|
+
// "off" value, since a reasoning disclosure is normal chat behavior, not
|
|
757
|
+
// an opt-in affordance like the paperclip or a starter chip.
|
|
758
|
+
// - empty (the welcome-screen greeting, Task 14): gated via ChatThread's
|
|
759
|
+
// own \`emptyContent\` prop, plain JSX rendered in the SAME shadow tree
|
|
760
|
+
// this file's App already composes ChatThread inside of (see
|
|
761
|
+
// emitEmptyContentProp's own doc for why that boundary needs no Portal
|
|
762
|
+
// at all). \`capabilities.starters\`' chips and the composer still render
|
|
763
|
+
// underneath it: ChatThread's own doc comment on \`emptyContent\` is
|
|
764
|
+
// explicit that it replaces only the empty MESSAGE LIST.
|
|
765
|
+
// - the widget close control (owner feedback on the live demo): a declared
|
|
766
|
+
// \`header.title\` on a \`widget\` layout gets its close button threaded
|
|
767
|
+
// into ChatThread's own header row via \`headerEndContent\`, wired back to
|
|
768
|
+
// Dock's \`controllerRef\` seam through a local closure — see
|
|
769
|
+
// emitDockCloseVar/emitHeaderEndContentProp's docs. No header means no
|
|
770
|
+
// row for it to sit in, so that case is untouched and Dock's own built-in
|
|
771
|
+
// mobile X keeps covering it.
|
|
772
|
+
// - conversations (Task 5): gated via ChatThread's own \`conversations\`/
|
|
773
|
+
// \`store\` props (kit-owned end to end — the prior-conversations list,
|
|
774
|
+
// list/load/save, autosave on every \`chat.messages()\` change). Requires
|
|
775
|
+
// capabilities.history persistence \`local\` or \`endpoint\` (schema
|
|
776
|
+
// superRefine, C-4) and SUBSUMES this file's hand-rolled history effect
|
|
777
|
+
// when on — see emitHistorySetup's own doc for the persistence-ownership
|
|
778
|
+
// decision: the store prop is the ONLY persistence mechanism emitted,
|
|
779
|
+
// never both. ChatThread never mutates \`messages\` itself, so
|
|
780
|
+
// \`onConversationLoad\` is ALSO wired here (\`chat.setMessages(() =>
|
|
781
|
+
// messages)\`) — without it, select/new/mount-restore all update
|
|
782
|
+
// ChatThread's own internal view/list state while the rendered thread
|
|
783
|
+
// never changes (see emitConversationsProps's own doc for the Task 6
|
|
784
|
+
// live-browser bug this fixes).
|
|
785
|
+
// - conversations + widget (owner follow-up): closing the widget while its
|
|
786
|
+
// list view is open must not leave it there for the next open — see
|
|
787
|
+
// widgetHasConversationsChrome/emitDockOnOpenChangeProp's docs. Wired only
|
|
788
|
+
// for \`widget\`, the one layout with something that closes/reopens at all.
|
|
789
|
+
export function App() {
|
|
790
|
+
${emitDockCloseVar(c, " ")}${emitChatControllerVar(c, " ")}${emitConversationsSignalsVar(c, " ")} return (
|
|
791
|
+
${emitLayoutOpen(c)}${emitSlots(c.slots, " ")} <ChatThread messages={chat.messages()} loading={chat.loading()} placeholder="Ask anything" onSubmit={submit} webSearch={false} voice={false}${emitHeaderProp(c)}${emitHeaderEndContentProp(c)}${emitAttachProps(c)}${emitStartersProp(c)}${emitReasoningProp(c)}${emitReasoningOpenProp(c)}${emitEmptyContentProp(c)}${emitCardTypesProp(c)}${emitHomeProp(c)}${emitConversationsProps(c)}${emitChatControllerRefProp(c)}${emitChatThreadUnreadProps(c)} />
|
|
792
|
+
${emitLayoutClose(c)} );
|
|
793
|
+
}
|
|
794
|
+
`;
|
|
795
|
+
}
|
|
796
|
+
function emitCustomApp(c) {
|
|
797
|
+
const slots = c.slots ?? [];
|
|
798
|
+
const [headerSlot, ...restSlots] = slots;
|
|
799
|
+
const history = c.capabilities?.history;
|
|
800
|
+
const solidJsNames = ["createSignal", ...history && history.persistence !== "none" ? ["createEffect"] : []];
|
|
801
|
+
return `import { ${solidJsNames.join(", ")} } from 'solid-js';
|
|
802
|
+
import { Thread, PromptInput, PromptInputTextarea, PromptInputActions, Button, createKaiChat${emitCardComponentImport(c)} } from '@kitn.ai/ui/solid';
|
|
803
|
+
import type { AttachmentData${emitHistoryTypeImport(c)} } from '@kitn.ai/ui/solid';
|
|
804
|
+
${emitProviderImports(c)}
|
|
805
|
+
${emitCardsImport(c)}
|
|
806
|
+
|
|
807
|
+
${emitProviderSetup(c)}
|
|
808
|
+
${emitHistorySetup(c)}
|
|
809
|
+
|
|
810
|
+
// layout: custom — minimal chrome, no ChatThread/Dock/PaneGroup. The bare
|
|
811
|
+
// spine (Thread + PromptInput) plus the declared slots, positioned by hand so
|
|
812
|
+
// YOU own the surrounding DOM. Capabilities beyond the spine (starters,
|
|
813
|
+
// attachments, reasoning display-mode, reasoningOpen, header.title, empty,
|
|
814
|
+
// conversations) are NOT wired here in v1 — this file is the eject artifact; add them the
|
|
815
|
+
// way ChatThread composes them (components/chat-thread.tsx in the kit's own
|
|
816
|
+
// source) if this construct needs them on a custom layout.
|
|
817
|
+
export function App() {
|
|
818
|
+
const [value, setValue] = createSignal('');
|
|
819
|
+
|
|
820
|
+
const handleSubmit = () => {
|
|
821
|
+
const text = value();
|
|
822
|
+
if (!text.trim() || chat.loading()) return;
|
|
823
|
+
setValue('');
|
|
824
|
+
void submit({ value: text, attachments: [] });
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
return (
|
|
828
|
+
<div style={{ height: '100dvh', display: 'flex', 'flex-direction': 'column' }}>
|
|
829
|
+
${emitSlots(headerSlot ? [headerSlot] : void 0, " ")} <Thread messages={chat.messages()} loading={chat.loading()} class="min-h-0 flex-1"${emitCardTypesProp(c)} />
|
|
830
|
+
{/* Slot placement rule: first declared slot above the thread, every other
|
|
831
|
+
declared slot below the composer, in declaration order. Reorder by
|
|
832
|
+
ejecting — this is the whole grain dimmer. */}
|
|
833
|
+
<PromptInput value={value()} onValueChange={setValue} isLoading={chat.loading()} onSubmit={handleSubmit}>
|
|
834
|
+
<PromptInputTextarea placeholder="Ask anything" />
|
|
835
|
+
<PromptInputActions>
|
|
836
|
+
<Button onClick={handleSubmit}>Send</Button>
|
|
837
|
+
</PromptInputActions>
|
|
838
|
+
</PromptInput>
|
|
839
|
+
${emitSlots(restSlots, " ")} </div>
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
`;
|
|
843
|
+
}
|
|
844
|
+
function emitHeaderProp(c) {
|
|
845
|
+
const title = c.header?.title;
|
|
846
|
+
if (!title) return "";
|
|
847
|
+
return ` chatTitle={${JSON.stringify(title)}}`;
|
|
848
|
+
}
|
|
849
|
+
function emitEmptyContentProp(c) {
|
|
850
|
+
const empty = c.empty;
|
|
851
|
+
if (!empty) return "";
|
|
852
|
+
const title = `<EmptyTitle>{${JSON.stringify(empty.title)}}</EmptyTitle>`;
|
|
853
|
+
const icon = empty.icon ? `<EmptyMedia><img src={${JSON.stringify(empty.icon)}} alt="" style={{ width: '40px', height: '40px', 'border-radius': '9999px' }} /></EmptyMedia>` : "";
|
|
854
|
+
const description = empty.description ? `<EmptyDescription>{${JSON.stringify(empty.description)}}</EmptyDescription>` : "";
|
|
855
|
+
return ` emptyContent={<Empty><EmptyHeader>${icon}${title}${description}</EmptyHeader></Empty>}`;
|
|
856
|
+
}
|
|
857
|
+
function emitEmptyComponentImport(c) {
|
|
858
|
+
if (!c.empty) return "";
|
|
859
|
+
let names = ", Empty, EmptyHeader, EmptyTitle";
|
|
860
|
+
if (c.empty.icon) names += ", EmptyMedia";
|
|
861
|
+
if (c.empty.description) names += ", EmptyDescription";
|
|
862
|
+
return names;
|
|
863
|
+
}
|
|
864
|
+
function widgetHasHeaderClose(c) {
|
|
865
|
+
return c.layout === "widget" && !!c.header?.title;
|
|
866
|
+
}
|
|
867
|
+
function widgetHasConversationsChrome(c) {
|
|
868
|
+
return c.layout === "widget" && (!!c.capabilities?.conversations || !!c.home);
|
|
869
|
+
}
|
|
870
|
+
function emitChatControllerVar(c, indent) {
|
|
871
|
+
return widgetHasConversationsChrome(c) ? `${indent}let chatController: ChatThreadController | undefined;
|
|
872
|
+
` : "";
|
|
873
|
+
}
|
|
874
|
+
function emitChatControllerRefProp(c) {
|
|
875
|
+
return widgetHasConversationsChrome(c) ? " controllerRef={(api) => (chatController = api)}" : "";
|
|
876
|
+
}
|
|
877
|
+
function emitDockOnOpenChangeProp(c) {
|
|
878
|
+
return widgetHasConversationsChrome(c) ? " onOpenChange={(open) => { setDockOpen(open); if (!open) chatController?.closeConversationsList(); }}" : "";
|
|
879
|
+
}
|
|
880
|
+
function emitConversationsResetTypeImport(c) {
|
|
881
|
+
return widgetHasConversationsChrome(c) ? ", ChatThreadController" : "";
|
|
882
|
+
}
|
|
883
|
+
function emitConversationsSignalsImport(c) {
|
|
884
|
+
return widgetHasConversationsChrome(c) ? `import { createSignal } from 'solid-js';
|
|
885
|
+
` : "";
|
|
886
|
+
}
|
|
887
|
+
function emitConversationsSignalsVar(c, indent) {
|
|
888
|
+
if (!widgetHasConversationsChrome(c)) return "";
|
|
889
|
+
const w = c.layout === "widget" ? c.widget : void 0;
|
|
890
|
+
const defaultOpen = w?.defaultOpen === true ? "true" : "false";
|
|
891
|
+
return `${indent}const [dockOpen, setDockOpen] = createSignal(${defaultOpen});
|
|
892
|
+
${indent}const [anyUnread, setAnyUnread] = createSignal(false);
|
|
893
|
+
`;
|
|
894
|
+
}
|
|
895
|
+
function emitChatThreadUnreadProps(c) {
|
|
896
|
+
return widgetHasConversationsChrome(c) ? " hostOpen={dockOpen()} onUnreadChange={setAnyUnread}" : "";
|
|
897
|
+
}
|
|
898
|
+
function emitDockUnreadProp(c) {
|
|
899
|
+
return widgetHasConversationsChrome(c) ? " unread={anyUnread()}" : "";
|
|
900
|
+
}
|
|
901
|
+
function emitDockCloseVar(c, indent) {
|
|
902
|
+
return widgetHasHeaderClose(c) ? `${indent}let dockClose: (() => void) | undefined;
|
|
903
|
+
` : "";
|
|
904
|
+
}
|
|
905
|
+
function emitDockControllerRef(c) {
|
|
906
|
+
return widgetHasHeaderClose(c) ? " controllerRef={(api) => (dockClose = () => api.setOpen(false))}" : "";
|
|
907
|
+
}
|
|
908
|
+
function emitDockHideClose(c) {
|
|
909
|
+
return widgetHasHeaderClose(c) ? " hideClose={true}" : "";
|
|
910
|
+
}
|
|
911
|
+
function emitHeaderEndContentProp(c) {
|
|
912
|
+
if (!widgetHasHeaderClose(c)) return "";
|
|
913
|
+
return ` headerEndContent={<Button variant="ghost" size="icon-sm" aria-label="Close ${c.name}" onClick={() => dockClose?.()}><DockCloseGlyph /></Button>}`;
|
|
914
|
+
}
|
|
915
|
+
function emitHeaderCloseImport(c) {
|
|
916
|
+
return widgetHasHeaderClose(c) ? ", Button, DockCloseGlyph" : "";
|
|
917
|
+
}
|
|
918
|
+
function emitAttachProps(c) {
|
|
919
|
+
const attachments = c.capabilities?.attachments;
|
|
920
|
+
if (!attachments) return " attach={false}";
|
|
921
|
+
return ` attach={true} accept={${JSON.stringify(attachments.accept.join(","))}}`;
|
|
922
|
+
}
|
|
923
|
+
function emitStartersProp(c) {
|
|
924
|
+
const starters = c.capabilities?.starters;
|
|
925
|
+
if (!starters || starters.length === 0) return "";
|
|
926
|
+
return ` suggestions={${JSON.stringify(starters)}}`;
|
|
927
|
+
}
|
|
928
|
+
function emitReasoningProp(c) {
|
|
929
|
+
const reasoning = c.capabilities?.reasoning;
|
|
930
|
+
if (!reasoning || reasoning === "full") return "";
|
|
931
|
+
return ` reasoning="${reasoning}"`;
|
|
932
|
+
}
|
|
933
|
+
function emitReasoningOpenProp(c) {
|
|
934
|
+
return c.capabilities?.reasoningOpen === true ? " reasoningOpen={true}" : "";
|
|
935
|
+
}
|
|
936
|
+
function emitHomeProp(c) {
|
|
937
|
+
if (!c.home) return "";
|
|
938
|
+
return ` home={${JSON.stringify(c.home)}}`;
|
|
939
|
+
}
|
|
940
|
+
function emitConversationsProps(c) {
|
|
941
|
+
if (!c.capabilities?.conversations) return "";
|
|
942
|
+
const history = c.capabilities.history;
|
|
943
|
+
const storeCall = history?.persistence === "endpoint" ? `fetchStore(${JSON.stringify(history.url)}${c.userId ? `, ${JSON.stringify(c.userId)}` : ""})` : `localStorageStore('${c.name}'${c.userId ? `, ${JSON.stringify(c.userId)}` : ""})`;
|
|
944
|
+
return ` conversations={true} store={${storeCall}} onConversationLoad={(messages) => chat.setMessages(() => messages)}`;
|
|
945
|
+
}
|
|
946
|
+
function emitConversationsImport(c) {
|
|
947
|
+
if (!c.capabilities?.conversations) return "";
|
|
948
|
+
const name = c.capabilities.history?.persistence === "endpoint" ? "fetchStore" : "localStorageStore";
|
|
949
|
+
return `import { ${name} } from '@kitn.ai/ui/solid';`;
|
|
950
|
+
}
|
|
951
|
+
function emitSolidJsImport(c) {
|
|
952
|
+
const history = c.capabilities?.history;
|
|
953
|
+
if (!history || history.persistence === "none" || c.layout !== "custom" && c.capabilities?.conversations) return "";
|
|
954
|
+
return `import { createEffect } from 'solid-js';
|
|
955
|
+
`;
|
|
956
|
+
}
|
|
957
|
+
function emitHistoryTypeImport(c) {
|
|
958
|
+
const history = c.capabilities?.history;
|
|
959
|
+
if (!history || history.persistence === "none" || c.layout !== "custom" && c.capabilities?.conversations) return "";
|
|
960
|
+
return ", ChatMessage";
|
|
961
|
+
}
|
|
962
|
+
function emitUserIdHeaderEntry(c) {
|
|
963
|
+
return c.userId ? `, 'x-kai-user-id': ${JSON.stringify(c.userId)}` : "";
|
|
964
|
+
}
|
|
965
|
+
function emitHistorySetup(c) {
|
|
966
|
+
const history = c.capabilities?.history;
|
|
967
|
+
if (!history || history.persistence === "none") return "";
|
|
968
|
+
if (c.layout !== "custom" && c.capabilities?.conversations) return "";
|
|
969
|
+
if (history.persistence === "local") {
|
|
970
|
+
const key = JSON.stringify(c.userId ? `kai:${c.name}:${c.userId}:thread` : `kai:${c.name}:thread`);
|
|
971
|
+
return `
|
|
972
|
+
// History: persisted locally in this browser, keyed by the element tag. What to
|
|
973
|
+
// retain and for how long is an app decision — clear the key to reset.
|
|
974
|
+
const THREAD_KEY = ${key};
|
|
975
|
+
try {
|
|
976
|
+
const saved = localStorage.getItem(THREAD_KEY);
|
|
977
|
+
if (saved) {
|
|
978
|
+
const parsed: unknown = JSON.parse(saved);
|
|
979
|
+
if (Array.isArray(parsed)) {
|
|
980
|
+
chat.setMessages(() => parsed as ChatMessage[]);
|
|
981
|
+
} else {
|
|
982
|
+
console.warn(\`[\${THREAD_KEY}] stored history was not an array; ignoring and starting fresh\`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
} catch { /* storage unavailable or corrupt: run in-memory */ }
|
|
986
|
+
createEffect(() => {
|
|
987
|
+
try {
|
|
988
|
+
localStorage.setItem(THREAD_KEY, JSON.stringify(chat.messages()));
|
|
989
|
+
} catch { /* storage unavailable: run in-memory */ }
|
|
990
|
+
});
|
|
991
|
+
`;
|
|
992
|
+
}
|
|
993
|
+
const url = JSON.stringify(history.url);
|
|
994
|
+
return `
|
|
995
|
+
// History: persisted to your endpoint (GET on mount, PUT on every change) —
|
|
996
|
+
// the kit PARSES, this app FETCHES; your route owns the storage and what to
|
|
997
|
+
// retain and for how long. \`hydrated\` guards the mount-load from immediately
|
|
998
|
+
// PUTting back what it just loaded, but still flips on a FAILED load — one
|
|
999
|
+
// offline/CORS/DNS blip degrades to "start fresh, keep saving", not
|
|
1000
|
+
// "never save again".
|
|
1001
|
+
let hydrated = false;
|
|
1002
|
+
(async () => {
|
|
1003
|
+
try {
|
|
1004
|
+
const r = await fetch(${url}${c.userId ? `, { headers: { 'x-kai-user-id': ${JSON.stringify(c.userId)} } }` : ""});
|
|
1005
|
+
const saved: unknown = r.ok ? await r.json() : [];
|
|
1006
|
+
if (Array.isArray(saved)) {
|
|
1007
|
+
chat.setMessages(() => saved as ChatMessage[]);
|
|
1008
|
+
} else {
|
|
1009
|
+
console.warn('history endpoint returned a non-array body; ignoring and starting fresh');
|
|
1010
|
+
}
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
console.error('history endpoint GET failed; starting fresh (will keep saving)', err);
|
|
1013
|
+
} finally {
|
|
1014
|
+
hydrated = true;
|
|
1015
|
+
}
|
|
1016
|
+
})();
|
|
1017
|
+
createEffect(() => {
|
|
1018
|
+
const snapshot = chat.messages();
|
|
1019
|
+
if (!hydrated) return;
|
|
1020
|
+
fetch(${url}, {
|
|
1021
|
+
method: 'PUT',
|
|
1022
|
+
headers: { 'content-type': 'application/json'${emitUserIdHeaderEntry(c)} },
|
|
1023
|
+
body: JSON.stringify(snapshot),
|
|
1024
|
+
}).catch((err) => {
|
|
1025
|
+
console.error('history endpoint PUT failed; this change was not persisted', err);
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
1028
|
+
`;
|
|
1029
|
+
}
|
|
1030
|
+
function emitProviderImports(c) {
|
|
1031
|
+
if (c.provider.mode === "mock") {
|
|
1032
|
+
return `import { createMockResponder } from '@kitn.ai/ui/state';
|
|
1033
|
+
import { readOpenAIStream } from '@kitn.ai/ui/wire';`;
|
|
1034
|
+
}
|
|
1035
|
+
const read = c.provider.wire === "openai" ? "readOpenAIStream" : "readAnthropicStream";
|
|
1036
|
+
const encode = c.provider.wire === "openai" ? "toOpenAIMessages" : "toAnthropicMessages";
|
|
1037
|
+
return `import { ${read}, ${encode} } from '@kitn.ai/ui/wire';`;
|
|
1038
|
+
}
|
|
1039
|
+
function emitProviderSetup(c) {
|
|
1040
|
+
if (c.provider.mode === "mock") {
|
|
1041
|
+
const cardsNote = c.cards ? `
|
|
1042
|
+
// Cards demo keylessly: createMockResponder() can already SCRIPT a tool call
|
|
1043
|
+
// (\`replies: [{ toolCalls: [...] }]\`, F-35), so a scripted turn calling
|
|
1044
|
+
// \`kai_<card name>\` renders exactly like a live model's would, below.` : "";
|
|
1045
|
+
return `// Provider seam: mock — keyless, streams locally, announces itself once.
|
|
1046
|
+
// Swap for provider.mode "endpoint" in the construct and re-run kai dev; the
|
|
1047
|
+
// generated fetch keeps this exact shape (the seam is the point).${cardsNote}
|
|
1048
|
+
const respond = createMockResponder();
|
|
1049
|
+
const chat = createKaiChat();
|
|
1050
|
+
|
|
1051
|
+
async function submit(detail: { value: string; attachments: AttachmentData[] }) {
|
|
1052
|
+
if (!detail.value.trim() || chat.loading()) return;
|
|
1053
|
+
chat.append({
|
|
1054
|
+
id: crypto.randomUUID(),
|
|
1055
|
+
role: 'user',
|
|
1056
|
+
parts: [
|
|
1057
|
+
{ type: 'text', text: detail.value },
|
|
1058
|
+
...detail.attachments.map((attachment) => ({ type: 'file' as const, attachment })),
|
|
1059
|
+
],
|
|
1060
|
+
});
|
|
1061
|
+
const stream = chat.streamAssistant();
|
|
1062
|
+
try {
|
|
1063
|
+
await readOpenAIStream(respond(detail.value), stream);${emitApplyCardTools(c)}
|
|
1064
|
+
stream.done();
|
|
1065
|
+
} catch (err) {
|
|
1066
|
+
stream.abort(err instanceof Error ? err.message : String(err));
|
|
1067
|
+
}
|
|
1068
|
+
}`;
|
|
1069
|
+
}
|
|
1070
|
+
const { url, wire } = c.provider;
|
|
1071
|
+
const read = wire === "openai" ? "readOpenAIStream" : "readAnthropicStream";
|
|
1072
|
+
const encode = wire === "openai" ? "toOpenAIMessages" : "toAnthropicMessages";
|
|
1073
|
+
return `// Provider seam: YOUR endpoint (${wire} wire, see the fetch call below
|
|
1074
|
+
// for the URL). The kit PARSES, this component FETCHES — no key, no
|
|
1075
|
+
// provider SDK, no client in here. Your route holds the key and re-frames to
|
|
1076
|
+
// the provider; the kai MCP scaffold tool emits one for your framework.
|
|
1077
|
+
const chat = createKaiChat();
|
|
1078
|
+
|
|
1079
|
+
async function submit(detail: { value: string; attachments: AttachmentData[] }) {
|
|
1080
|
+
if (!detail.value.trim() || chat.loading()) return;
|
|
1081
|
+
chat.append({
|
|
1082
|
+
id: crypto.randomUUID(),
|
|
1083
|
+
role: 'user',
|
|
1084
|
+
parts: [
|
|
1085
|
+
{ type: 'text', text: detail.value },
|
|
1086
|
+
...detail.attachments.map((attachment) => ({ type: 'file' as const, attachment })),
|
|
1087
|
+
],
|
|
1088
|
+
});
|
|
1089
|
+
const stream = chat.streamAssistant();
|
|
1090
|
+
try {
|
|
1091
|
+
const response = await fetch(${JSON.stringify(url)}, {
|
|
1092
|
+
method: 'POST',
|
|
1093
|
+
headers: { 'content-type': 'application/json'${emitUserIdHeaderEntry(c)} },
|
|
1094
|
+
body: JSON.stringify({ messages: ${encode}(chat.messages())${emitToolsField(c)} }),
|
|
1095
|
+
});
|
|
1096
|
+
if (!response.ok) throw new Error(\`endpoint responded \${response.status}\`);
|
|
1097
|
+
await ${read}(response, stream);${emitApplyCardTools(c)}
|
|
1098
|
+
stream.done();
|
|
1099
|
+
} catch (err) {
|
|
1100
|
+
stream.abort(err instanceof Error ? err.message : String(err));
|
|
1101
|
+
}
|
|
1102
|
+
}`;
|
|
1103
|
+
}
|
|
1104
|
+
function emitLayoutImport(c) {
|
|
1105
|
+
switch (c.layout) {
|
|
1106
|
+
case "widget":
|
|
1107
|
+
return `, Dock${hasLauncherIcon(c) ? ", DockLauncherImage" : ""}`;
|
|
1108
|
+
case "split":
|
|
1109
|
+
return ", WorkspaceShell";
|
|
1110
|
+
case "fullscreen":
|
|
1111
|
+
case "aside":
|
|
1112
|
+
return "";
|
|
1113
|
+
case "custom":
|
|
1114
|
+
return "";
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function emitSlots(slots, indent) {
|
|
1118
|
+
if (!slots || slots.length === 0) return "";
|
|
1119
|
+
return slots.map((name) => `${indent}<slot name="${name}" />
|
|
1120
|
+
`).join("");
|
|
1121
|
+
}
|
|
1122
|
+
function emitDockPosition(c) {
|
|
1123
|
+
const w = c.layout === "widget" ? c.widget : void 0;
|
|
1124
|
+
return w?.position ? ` position="${w.position}"` : "";
|
|
1125
|
+
}
|
|
1126
|
+
function hasLauncherIcon(c) {
|
|
1127
|
+
return c.layout === "widget" && !!c.widget?.launcherIcon;
|
|
1128
|
+
}
|
|
1129
|
+
function emitDockLauncher(c) {
|
|
1130
|
+
const w = c.layout === "widget" ? c.widget : void 0;
|
|
1131
|
+
if (!w?.launcherIcon) return "";
|
|
1132
|
+
return ` launcher={<DockLauncherImage src={${JSON.stringify(w.launcherIcon)}} />}`;
|
|
1133
|
+
}
|
|
1134
|
+
function emitDockDefaultOpen(c) {
|
|
1135
|
+
const w = c.layout === "widget" ? c.widget : void 0;
|
|
1136
|
+
return w?.defaultOpen === true ? " defaultOpen={true}" : "";
|
|
1137
|
+
}
|
|
1138
|
+
function emitLayoutOpen(c) {
|
|
1139
|
+
switch (c.layout) {
|
|
1140
|
+
case "widget":
|
|
1141
|
+
return ` <Dock label="${c.name}"${emitDockPosition(c)}${emitDockLauncher(c)}${emitDockDefaultOpen(c)}${emitDockHideClose(c)}${emitDockControllerRef(c)}${emitDockOnOpenChangeProp(c)}${emitDockUnreadProp(c)}>
|
|
1142
|
+
`;
|
|
1143
|
+
case "fullscreen":
|
|
1144
|
+
return ` <div style={{ height: '100dvh', display: 'flex', 'flex-direction': 'column' }}>
|
|
1145
|
+
`;
|
|
1146
|
+
case "aside":
|
|
1147
|
+
return ` <aside data-kai-layout="aside" style={{ position: 'fixed', 'inset-block': '0', 'inset-inline-end': '0', width: '380px', display: 'flex', 'flex-direction': 'column', 'border-inline-start': '1px solid var(--kai-color-border)' }}>
|
|
1148
|
+
{/* Mirrors Dock's own narrow-viewport full-bleed rule (ui/dock.tsx:229-240)
|
|
1149
|
+
— aside has no dedicated kit component (see the emitLayoutOpen doc
|
|
1150
|
+
comment above), so this is the honest hand-rolled equivalent, not a
|
|
1151
|
+
new responsive strategy. */}
|
|
1152
|
+
<style>{\`@media (max-width: 480px) { [data-kai-layout="aside"] { inset: 0; width: auto; height: auto; border-inline-start: 0; } }\`}</style>
|
|
1153
|
+
`;
|
|
1154
|
+
case "split":
|
|
1155
|
+
return ` <div style={{ height: '100dvh' }}>
|
|
1156
|
+
<WorkspaceShell class="h-full" drawerBelow={480} end={
|
|
1157
|
+
<div style={{ height: '100%', overflow: 'auto' }}>
|
|
1158
|
+
<slot name="pane" />
|
|
1159
|
+
</div>
|
|
1160
|
+
}>
|
|
1161
|
+
`;
|
|
1162
|
+
case "custom":
|
|
1163
|
+
return "";
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function emitLayoutClose(c) {
|
|
1167
|
+
switch (c.layout) {
|
|
1168
|
+
case "widget":
|
|
1169
|
+
return ` </Dock>
|
|
1170
|
+
`;
|
|
1171
|
+
case "fullscreen":
|
|
1172
|
+
return ` </div>
|
|
1173
|
+
`;
|
|
1174
|
+
case "aside":
|
|
1175
|
+
return ` </aside>
|
|
1176
|
+
`;
|
|
1177
|
+
case "split":
|
|
1178
|
+
return ` </WorkspaceShell>
|
|
1179
|
+
</div>
|
|
1180
|
+
`;
|
|
1181
|
+
case "custom":
|
|
1182
|
+
return "";
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
function emitTypes(c) {
|
|
1186
|
+
return `declare global {
|
|
1187
|
+
interface HTMLElementTagNameMap {
|
|
1188
|
+
'${c.name}': HTMLElement & { theme: 'light' | 'dark' | 'auto' };
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
export {};
|
|
1192
|
+
`;
|
|
1193
|
+
}
|
|
1194
|
+
const MANIFEST = ".kai-manifest.json";
|
|
1195
|
+
function writeProject(files, dir) {
|
|
1196
|
+
const manifestPath = join(dir, MANIFEST);
|
|
1197
|
+
const previous = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, "utf8")) : [];
|
|
1198
|
+
const current = new Set(files.map((f) => f.path));
|
|
1199
|
+
for (const stale of previous) {
|
|
1200
|
+
if (!current.has(stale)) rmSync(join(dir, stale), { force: true });
|
|
1201
|
+
}
|
|
1202
|
+
const overwritten = [];
|
|
1203
|
+
for (const f of files) {
|
|
1204
|
+
const abs = join(dir, f.path);
|
|
1205
|
+
if (existsSync(abs)) overwritten.push(f.path);
|
|
1206
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
1207
|
+
writeFileSync(abs, f.code);
|
|
1208
|
+
}
|
|
1209
|
+
writeFileSync(manifestPath, `${JSON.stringify([...current].sort(), null, 2)}
|
|
1210
|
+
`);
|
|
1211
|
+
return overwritten;
|
|
1212
|
+
}
|
|
1213
|
+
const defaultIo = { log: (s) => console.log(s), error: (s) => console.error(s) };
|
|
1214
|
+
const USAGE = `usage: kai <command>
|
|
1215
|
+
|
|
1216
|
+
kai validate <construct.json> check a construct, print problems with paths
|
|
1217
|
+
kai eject <construct.json> <outDir> write the generated Solid project (it's yours)
|
|
1218
|
+
kai dev <construct.json> live preview with reload-on-edit (Task 5)
|
|
1219
|
+
kai compile <construct.json> [outDir] one self-registering .js (Task 6)
|
|
1220
|
+
`;
|
|
1221
|
+
function homeRecentConversationWarning(construct) {
|
|
1222
|
+
if (construct.home?.recentConversation && !construct.capabilities?.conversations) {
|
|
1223
|
+
return "warning: home.recentConversation is set but capabilities.conversations is not — the recent-conversation card will render nothing without it.";
|
|
1224
|
+
}
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
function loadConstruct(path, io) {
|
|
1228
|
+
const abs = resolve(path);
|
|
1229
|
+
let raw;
|
|
1230
|
+
try {
|
|
1231
|
+
raw = readFileSync(abs, "utf8");
|
|
1232
|
+
} catch {
|
|
1233
|
+
io.error(`cannot read ${abs}`);
|
|
1234
|
+
return null;
|
|
1235
|
+
}
|
|
1236
|
+
let json;
|
|
1237
|
+
try {
|
|
1238
|
+
json = JSON.parse(raw);
|
|
1239
|
+
} catch (err) {
|
|
1240
|
+
io.error(`${abs} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
const out = validateConstruct(json);
|
|
1244
|
+
if (!out.ok) {
|
|
1245
|
+
io.error(`${abs} is not a valid construct:`);
|
|
1246
|
+
for (const p of out.problems) io.error(` ${p.path || "(root)"}: ${p.message}`);
|
|
1247
|
+
return null;
|
|
1248
|
+
}
|
|
1249
|
+
return out.construct;
|
|
1250
|
+
}
|
|
1251
|
+
function parseUiFlag(rest) {
|
|
1252
|
+
const uiFlag = rest.indexOf("--ui");
|
|
1253
|
+
const uiSpec = uiFlag >= 0 ? rest[uiFlag + 1] : void 0;
|
|
1254
|
+
const positional = uiFlag >= 0 ? rest.filter((_, i) => i !== uiFlag && i !== uiFlag + 1) : rest;
|
|
1255
|
+
return { uiSpec, positional };
|
|
1256
|
+
}
|
|
1257
|
+
async function runCli(argv, io = defaultIo) {
|
|
1258
|
+
const [command, ...rest] = argv;
|
|
1259
|
+
switch (command) {
|
|
1260
|
+
case "validate": {
|
|
1261
|
+
const construct = loadConstruct(rest[0] ?? "", io);
|
|
1262
|
+
if (!construct) return 1;
|
|
1263
|
+
io.log(`valid construct: <${construct.name}> (layout: ${construct.layout}, provider: ${construct.provider.mode})`);
|
|
1264
|
+
const warning = homeRecentConversationWarning(construct);
|
|
1265
|
+
if (warning) io.log(warning);
|
|
1266
|
+
return 0;
|
|
1267
|
+
}
|
|
1268
|
+
case "eject": {
|
|
1269
|
+
const { uiSpec, positional } = parseUiFlag(rest);
|
|
1270
|
+
const [path, outDir] = positional;
|
|
1271
|
+
if (!path || !outDir) {
|
|
1272
|
+
io.error(USAGE);
|
|
1273
|
+
return 2;
|
|
1274
|
+
}
|
|
1275
|
+
const construct = loadConstruct(path, io);
|
|
1276
|
+
if (!construct) return 1;
|
|
1277
|
+
const overwritten = writeProject(generateProject(construct, { uiSpec }), resolve(outDir));
|
|
1278
|
+
if (overwritten.length > 0) {
|
|
1279
|
+
io.log(`overwriting ${overwritten.length} existing file(s)`);
|
|
1280
|
+
}
|
|
1281
|
+
const notice = accentContrastNotice(construct);
|
|
1282
|
+
if (notice) io.log(notice);
|
|
1283
|
+
io.log(`ejected <${construct.name}> to ${resolve(outDir)} — npm install && npm run dev. The source is yours.`);
|
|
1284
|
+
return 0;
|
|
1285
|
+
}
|
|
1286
|
+
case "dev": {
|
|
1287
|
+
const { uiSpec, positional } = parseUiFlag(rest);
|
|
1288
|
+
const path = positional[0];
|
|
1289
|
+
if (!path) {
|
|
1290
|
+
io.error(USAGE);
|
|
1291
|
+
return 2;
|
|
1292
|
+
}
|
|
1293
|
+
const { dev } = await import("./assets/dev-5XzXYR21.js");
|
|
1294
|
+
await dev(path, { io, uiSpec });
|
|
1295
|
+
return 0;
|
|
1296
|
+
}
|
|
1297
|
+
case "compile": {
|
|
1298
|
+
const { uiSpec, positional } = parseUiFlag(rest);
|
|
1299
|
+
const [path, outArg] = positional;
|
|
1300
|
+
if (!path) {
|
|
1301
|
+
io.error(USAGE);
|
|
1302
|
+
return 2;
|
|
1303
|
+
}
|
|
1304
|
+
const construct = loadConstruct(path, io);
|
|
1305
|
+
if (!construct) return 1;
|
|
1306
|
+
const outDir = resolve(outArg ?? "dist-construct");
|
|
1307
|
+
const { workDirFor, ensureInstalled } = await import("./assets/dev-5XzXYR21.js");
|
|
1308
|
+
const dir = workDirFor(construct.name, process.cwd());
|
|
1309
|
+
const files = generateProject(construct, { uiSpec });
|
|
1310
|
+
writeProject(files, dir);
|
|
1311
|
+
await ensureInstalled(dir, files, io);
|
|
1312
|
+
await new Promise((done, fail) => {
|
|
1313
|
+
const child = spawn("npm", ["run", "build"], { cwd: dir, stdio: "inherit" });
|
|
1314
|
+
child.on("exit", (code) => code === 0 ? done() : fail(new Error(`vite build exited ${code}`)));
|
|
1315
|
+
});
|
|
1316
|
+
mkdirSync(outDir, { recursive: true });
|
|
1317
|
+
copyFileSync(join(dir, "dist", `${construct.name}.js`), join(outDir, `${construct.name}.js`));
|
|
1318
|
+
writeFileSync(join(outDir, `${construct.name}.d.ts`), emitTypes(construct));
|
|
1319
|
+
writeProject(files, join(outDir, "source"));
|
|
1320
|
+
io.log(`compiled <${construct.name}> → ${outDir}/${construct.name}.js (source beside it in source/).`);
|
|
1321
|
+
io.log(`endpoint backends: the kai MCP scaffold tool emits a matching route — see its output for your framework.`);
|
|
1322
|
+
return 0;
|
|
1323
|
+
}
|
|
1324
|
+
default:
|
|
1325
|
+
io.error(USAGE);
|
|
1326
|
+
return 2;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
runCli(process.argv.slice(2)).then((code) => {
|
|
1330
|
+
process.exitCode = code;
|
|
1331
|
+
});
|
|
1332
|
+
export {
|
|
1333
|
+
accentContrastNotice as a,
|
|
1334
|
+
generateProject as g,
|
|
1335
|
+
validateConstruct as v,
|
|
1336
|
+
writeProject as w
|
|
1337
|
+
};
|